@faceteer/cdk
Version:
CDK 2.0 constructs and helpers that make composing a Lambda powered service easier.
301 lines (300 loc) • 13.3 kB
JavaScript
"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;