UNPKG

@faceteer/cdk

Version:

CDK 2.0 constructs and helpers that make composing a Lambda powered service easier.

301 lines (300 loc) 13.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.LambdaService = void 0; const cdk = __importStar(require("aws-cdk-lib")); const apigwv2 = __importStar(require("aws-cdk-lib/aws-apigatewayv2")); const iam = __importStar(require("aws-cdk-lib/aws-iam")); const sns = __importStar(require("aws-cdk-lib/aws-sns")); const route53 = __importStar(require("aws-cdk-lib/aws-route53")); const route53Targets = __importStar(require("aws-cdk-lib/aws-route53-targets")); const change_case_1 = require("change-case"); const constructs_1 = require("constructs"); const extract_handlers_1 = require("../extract/extract-handlers"); const service_api_function_1 = require("./service-api-function"); const service_notification_function_1 = require("./service-notification-function"); const service_queue_function_1 = require("./service-queue-function"); const service_cron_function_1 = require("./service-cron-function"); const service_event_function_1 = require("./service-event-function"); const validate_path_parameters_1 = require("../util/validate-path-parameters"); const api_gateway_1 = require("./api-gateway"); const aws_apigatewayv2_1 = require("aws-cdk-lib/aws-apigatewayv2"); class LambdaService extends constructs_1.Construct { api; stage; grantPrincipal; authorizer; /** Maps queue names to the queue handlers of this service, if any. */ queues = new Map(); functions = []; environmentVariables = new Map(); snsTopics = new Map(); constructor(scope, id, { handlersFolder, authorizer, jwtAuthorizer, lambdaAuthorizer, bundlingOptions = {}, role, defaults, defaultScopes, domain, eventBuses, api, stage, layers, network, }) { super(scope, id); this.environmentVariables.set('NODE_OPTIONS', '--enable-source-maps'); this.environmentVariables.set('ACCOUNT_ID', cdk.Fn.ref('AWS::AccountId')); if (network && defaults?.vpc === undefined) { // If a VPC is supplied, then enable VPC use for functions by default. It // could be an easy mistake to add a VPC but not enable its usage. defaults = { ...defaults, vpc: true, }; } if (!role) { /** * Role that the API lambda functions will assume */ role = new iam.Role(this, 'LambdaRole', { assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), description: `Role for ${cdk.Names.uniqueId(this)}`, managedPolicies: [ iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'), iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaVPCAccessExecutionRole'), ], }); } this.grantPrincipal = role.grantPrincipal; /** * The HTTP api. * * We create a new API gateway and stage if one was not provided. If one is * provided, for example if this is being used as a module in a larger * stack, then we'll use the provided gateway and stage. */ this.api = api ?? new api_gateway_1.ApiGateway(this, 'Api', { name: cdk.Names.uniqueId(this) }); this.stage = stage ?? new api_gateway_1.ApiStage(this, 'Stage', { api: this.api }); if (authorizer === undefined) { // Keep backward compatibility authorizer = lambdaAuthorizer ?? jwtAuthorizer; } /** * If an existing authorizer is provided, we'll use that. If not, we'll * create a new one with the given config. */ if (authorizer instanceof aws_apigatewayv2_1.CfnAuthorizer) { this.authorizer = authorizer; } else if ((0, api_gateway_1.isLambdaAuthorizerConfig)(authorizer)) { this.authorizer = new api_gateway_1.LambdaAuthorizer(this, 'Authorizer', { api: this.api, config: authorizer, name: `${cdk.Names.uniqueId(this)}LambdaAuthorizer`, }); } else if ((0, api_gateway_1.isJwtAuthorizerConfig)(authorizer)) { this.authorizer = new api_gateway_1.JwtAuthorizer(this, 'Authorizer', { api: this.api, config: authorizer, name: `${cdk.Names.uniqueId(this)}JwtAuthorizer`, }); } /** * Get all handler information from handlers */ const handlers = handlersFolder ? (0, extract_handlers_1.extractHandlers)(handlersFolder) : { api: {}, notification: {}, queue: {}, cron: {}, event: {} }; if (domain) { const { certificate, domainName, route53Zone } = domain; const apiGatewayDomain = new apigwv2.CfnDomainName(this, 'ApiDomain', { domainName: domainName, domainNameConfigurations: [ { certificateArn: certificate.certificateArn, endpointType: 'REGIONAL', }, ], }); new apigwv2.CfnApiMapping(this, 'ApiMapping', { apiId: this.api.ref, domainName: apiGatewayDomain.ref, stage: this.stage.ref, }); if (route53Zone) { const target = new route53Targets.ApiGatewayv2DomainProperties(apiGatewayDomain.attrRegionalDomainName, apiGatewayDomain.attrRegionalHostedZoneId); new route53.ARecord(this, 'ApiARecord', { zone: route53Zone, target: route53.RecordTarget.fromAlias(target), recordName: domainName, }); } } /** Function props shared by all functions. Just add definition and any handler-specific props! */ const baseFunctionProps = { role, bundlingOptions, layers, defaults, network, }; /** * Create all of the API handlers */ for (const apiHandler of Object.values(handlers.api)) { /** * Validate that `pathParameters` and `route` are consistent */ (0, validate_path_parameters_1.validatePathParameters)(apiHandler.route, [ ...(apiHandler.pathParameters ?? []), ]); /** * Add a new function to the API */ const apiFn = new service_api_function_1.ServiceApiFunction(this, apiHandler.name, { ...baseFunctionProps, defaultScopes, definition: apiHandler, httpApi: this.api, authorizer: this.authorizer, }); this.functions.push(apiFn); } for (const queueHandler of Object.values(handlers.queue)) { /** * Create the queue handlers and their respective queues */ const queueFn = new service_queue_function_1.ServiceQueueFunction(this, queueHandler.name, { ...baseFunctionProps, definition: queueHandler, }); this.functions.push(queueFn); this.queues.set(queueFn.definition.queueName, queueFn); this.environmentVariables.set(queueFn.queueEnvironmentVariable, queueFn.queue.queueName); this.environmentVariables.set(queueFn.dlqEnvironmentVariable, queueFn.dlq.queueName); } for (const eventHandler of Object.values(handlers.event)) { if (!eventBuses) { throw new Error('Tried to create an event handler without any configured event buses'); } let eventBus; if (eventBuses[eventHandler.eventBusName]) { // Treated `eventBusName` as a key to reference configured event buses eventBus = eventBuses[eventHandler.eventBusName]; } else { // Treated `eventBusName` as the aws event bus name const matchedEventBus = Object.values(eventBuses).find((bus) => bus.eventBusName === eventHandler.eventBusName); if (!matchedEventBus) { throw new Error(` Could not find the event bus "${eventHandler.eventBusName}" specified event bus name. Please make sure the event handler "${eventHandler.name}" is configured properly or that you have configured the appropriate event buses. `); } eventBus = matchedEventBus; } const eventFn = new service_event_function_1.ServiceEventFunction(this, eventHandler.name, { ...baseFunctionProps, definition: eventHandler, eventBus, }); this.functions.push(eventFn); } for (const notificationHandler of Object.values(handlers.notification)) { /** * Create any notification handlers along with any topics that * haven't been created yet */ let topic = this.snsTopics.get(notificationHandler.topicName); if (!topic) { topic = new sns.Topic(this, notificationHandler.topicName); this.snsTopics.set(notificationHandler.topicName, topic); this.environmentVariables.set(`TOPIC_${(0, change_case_1.constantCase)(notificationHandler.topicName)}`, topic.topicName); this.environmentVariables.set(`TOPIC_${(0, change_case_1.constantCase)(notificationHandler.topicName)}_ARN`, topic.topicArn); topic.grantPublish(role); } const notificationFn = new service_notification_function_1.ServiceNotificationFunction(this, notificationHandler.name, { ...baseFunctionProps, definition: notificationHandler, topic, }); this.functions.push(notificationFn); } for (const cronHandler of Object.values(handlers.cron)) { /** * Create cron handlers */ const cronFn = new service_cron_function_1.ServiceCronFunction(this, cronHandler.name, { ...baseFunctionProps, definition: cronHandler, }); this.functions.push(cronFn); } /** * Add all environment variables */ for (const fn of this.functions) { for (const [key, value] of this.environmentVariables.entries()) { fn.addEnvironment(key, value); } } } /** * Add an environment variable to the service * @param key * @param value */ addEnvironment(key, value) { this.environmentVariables.set(key, value); for (const fn of this.functions) { fn.addEnvironment(key, value); } } /** Allows this service to send messages to the queue handled by this * function. * * This is only necessary if you are sending messages across services. * The service always has access to its own queues. */ grantSendToQueue(queueFn) { this.addEnvironment(queueFn.queueEnvironmentVariable, queueFn.queue.queueName); this.addEnvironment(queueFn.dlqEnvironmentVariable, queueFn.dlq.queueName); queueFn.queue.grantSendMessages(this); queueFn.dlq.grantSendMessages(this); } /** * Retrieves an SNS topic by it's name * @param topicName */ getSnsTopic(topicName) { const topic = this.snsTopics.get(topicName); if (!topic) { throw new Error(`Unable to find a topic with the name: ${topicName}. Make sure that topic has been configured in a lambda handler already`); } return topic; } } exports.LambdaService = LambdaService;