@cpmech/az-cdk
Version:
AmaZon AWS-CDK tools
1,144 lines (1,113 loc) • 47 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', { value: true });
var core = require('@aws-cdk/core');
var awsApigateway = require('@aws-cdk/aws-apigateway');
var awsS3 = require('@aws-cdk/aws-s3');
var awsCognito = require('@aws-cdk/aws-cognito');
var awsLambda = require('@aws-cdk/aws-lambda');
var awsIam = require('@aws-cdk/aws-iam');
var awsCloudformation = require('@aws-cdk/aws-cloudformation');
var awsDynamodb = require('@aws-cdk/aws-dynamodb');
var awsCertificatemanager = require('@aws-cdk/aws-certificatemanager');
var basic = require('@cpmech/basic');
var awsSns = require('@aws-cdk/aws-sns');
var awsEvents = require('@aws-cdk/aws-events');
var awsEventsTargets = require('@aws-cdk/aws-events-targets');
var awsSnsSubscriptions = require('@aws-cdk/aws-sns-subscriptions');
var awsSqs = require('@aws-cdk/aws-sqs');
var awsRoute53 = require('@aws-cdk/aws-route53');
var awsRoute53Targets = require('@aws-cdk/aws-route53-targets');
var awsCloudfront = require('@aws-cdk/aws-cloudfront');
var awsCodepipeline = require('@aws-cdk/aws-codepipeline');
var awsCodebuild = require('@aws-cdk/aws-codebuild');
var awsCodepipelineActions = require('@aws-cdk/aws-codepipeline-actions');
class AuthorizerConstruct extends core.Construct {
constructor(scope, id, props) {
super(scope, id);
const region = core.Aws.REGION;
const account = core.Aws.ACCOUNT_ID;
const cognitoArn = `arn:aws:cognito-idp:${region}:${account}:userpool/${props.cognitoUserPoolId}`;
this.authorizer = new awsApigateway.CfnAuthorizer(this, 'Authorizer', {
name: 'authorizer',
restApiId: props.restApiId,
type: 'COGNITO_USER_POOLS',
identitySource: 'method.request.header.Authorization',
providerArns: [cognitoArn],
});
}
protectRoute(method) {
const child = method.node.findChild('Resource');
child.addPropertyOverride('AuthorizationType', 'COGNITO_USER_POOLS');
child.addPropertyOverride('AuthorizerId', { Ref: this.authorizer.logicalId });
}
}
class BucketsConstruct extends core.Construct {
constructor(scope, id, props) {
super(scope, id);
this.buckets = [];
props.buckets.forEach((b, i) => {
let cors = undefined;
if (b.corsGET || b.corsPUT || b.corsPOST || b.corsDELETE) {
const allowedMethods = [];
if (b.corsGET) {
allowedMethods.push(awsS3.HttpMethods.GET);
}
if (b.corsPUT) {
allowedMethods.push(awsS3.HttpMethods.PUT);
}
if (b.corsPOST) {
allowedMethods.push(awsS3.HttpMethods.POST);
}
if (b.corsDELETE) {
allowedMethods.push(awsS3.HttpMethods.DELETE);
}
cors = [
{
allowedOrigins: ['*'],
allowedHeaders: ['*'],
allowedMethods,
},
];
}
const bucket = new awsS3.Bucket(this, `Bucket${i}`, {
bucketName: b.name,
removalPolicy: core.RemovalPolicy.DESTROY,
cors,
});
this.buckets.push(bucket);
});
}
}
const defaults = {
runtime: awsLambda.Runtime.NODEJS_16_X,
};
const defaultDescription = 'Common NodeJS Libraries';
const defaultDirLayer = 'layers';
const defaultLicense = 'MIT';
const commonLayer = {
name: 'CommonLibs',
description: defaultDescription,
runtime: defaults.runtime,
dirLayer: defaultDirLayer,
license: defaultLicense,
};
class LambdaLayersConstruct extends core.Construct {
constructor(scope, id, props) {
super(scope, id);
this.all = [];
this.name2layer = {};
const specs = props && props.list && props.list.length > 0 ? props.list : [commonLayer];
specs.forEach((spec) => {
let layer;
if (spec.arn) {
layer = awsLambda.LayerVersion.fromLayerVersionArn(this, spec.name, spec.arn);
}
else {
layer = new awsLambda.LayerVersion(this, spec.name, {
code: awsLambda.Code.fromAsset(spec.dirLayer || defaultDirLayer),
compatibleRuntimes: [spec.runtime || defaults.runtime],
description: spec.description || defaultDescription,
license: spec.license || defaultLicense,
});
}
this.all.push(layer);
this.name2layer[spec.name] = layer;
});
}
}
const CRL_DIR_PROD = 'node_modules/@cpmech/az-cdk-crl/dist/';
const CRL_DIR_TEST = 'src/custom-resources/__tests__/az-cdk-crl/dist/';
const crlDir = process.env.NODE_ENV === 'test' ? CRL_DIR_TEST : CRL_DIR_PROD;
class CognitoEnableProvidersConstruct extends core.Construct {
constructor(scope, id, props) {
super(scope, id);
const fcn = new awsLambda.Function(this, 'Function', {
code: awsLambda.Code.fromAsset(crlDir),
handler: 'index.cognitoEnableProviders',
runtime: props.runtime || defaults.runtime,
timeout: core.Duration.minutes(1),
});
fcn.addToRolePolicy(new awsIam.PolicyStatement({
actions: ['cognito-idp:*'],
resources: ['*'],
}));
const cbUrls = props.callbackUrls ? props.callbackUrls : ['https://localhost:3000/'];
const lgUrls = props.logoutUrls ? props.logoutUrls : ['https://localhost:3000/'];
new awsCloudformation.CustomResource(this, 'Resource', {
provider: awsCloudformation.CustomResourceProvider.lambda(fcn),
properties: {
UserPoolId: props.userPoolId,
UserPoolClientId: props.userPoolClientId,
Providers: props.providers,
CallbackUrls: cbUrls,
LogoutUrls: lgUrls,
},
});
}
}
class CognitoPoolDomainConstruct extends core.Construct {
constructor(scope, id, props) {
super(scope, id);
const fcn = new awsLambda.Function(this, 'Function', {
code: awsLambda.Code.fromAsset(crlDir),
handler: 'index.cognitoPoolDomain',
runtime: props.runtime || defaults.runtime,
timeout: core.Duration.minutes(1),
});
fcn.addToRolePolicy(new awsIam.PolicyStatement({
actions: ['cognito-idp:*'],
resources: ['*'],
}));
new awsCloudformation.CustomResource(this, 'Resource', {
provider: awsCloudformation.CustomResourceProvider.lambda(fcn),
properties: {
UserPoolId: props.userPoolId,
DomainPrefix: props.domainPrefix,
},
});
}
}
/*
The API Gateway HostedZoneId for each region has to be looked up here:
https://docs.aws.amazon.com/general/latest/gr/rande.html#apigateway_region
Examples:
US East (N. Virginia) us-east-1 execute-api.us-east-1.amazonaws.com Z1UJRXOUMOOFQ8
US West (N. California) us-west-1 execute-api.us-west-1.amazonaws.com Z2MUQ32089INYE
US East (Ohio) us-east-2 execute-api.us-east-2.amazonaws.com ZOJJZC49E0EPZ
US West (Oregon) us-west-2 execute-api.us-west-2.amazonaws.com Z2OJLYMUO9EFXC
Asia Pacific (Hong Kong) ap-east-1 execute-api.ap-east-1.amazonaws.com Z3FD1VL90ND7K5
Asia Pacific (Singapore) ap-southeast-1 execute-api.ap-southeast-1.amazonaws.com ZL327KTPIQFUL
Asia Pacific (Sydney) ap-southeast-2 execute-api.ap-southeast-2.amazonaws.com Z2RPCDW04V8134
South America (Sao Paulo) sa-east-1 execute-api.sa-east-1.amazonaws.com ZCMLWB8V5SYIT
*/
const apiGatewayHostedZoneId = (region) => {
switch (region.toLowerCase()) {
case 'us-east-2':
return 'ZOJJZC49E0EPZ';
case 'us-east-1':
return 'Z1UJRXOUMOOFQ8';
case 'us-west-1':
return 'Z2MUQ32089INYE';
case 'us-west-2':
return 'Z2OJLYMUO9EFXC';
case 'ap-east-1':
return 'Z3FD1VL90ND7K5';
case 'ap-south-1':
return 'Z3VO1THU9YC4UR';
case 'ap-northeast-2':
return 'Z20JF4UZKIW1U8';
case 'ap-southeast-1':
return 'ZL327KTPIQFUL';
case 'ap-southeast-2':
return 'Z2RPCDW04V8134';
case 'ap-northeast-1':
return 'Z1YSHQZHG15GKL';
case 'ca-central-1':
return 'Z19DQILCV0OWEC';
case 'eu-central-1':
return 'Z1U9ULNL0V5AJ3';
case 'eu-west-1':
return 'ZLY8HYME6SFDD';
case 'eu-west-2':
return 'ZJ5UAJN8Y3Z2Q';
case 'eu-west-3':
return 'Z3KY65QIEKYHQQ';
case 'eu-north-1':
return 'Z3UWIKFBOOGXPP';
case 'me-south-1':
return 'Z20ZBPC0SS8806';
case 'sa-east-1':
return 'ZCMLWB8V5SYIT';
}
throw new Error(`cannot get API Gateway HostedId for region "${region}"`);
};
class Route53AliasConstruct extends core.Construct {
constructor(scope, id, props) {
super(scope, id);
const fcn = new awsLambda.Function(this, 'Function', {
code: awsLambda.Code.fromAsset(crlDir),
handler: 'index.route53Alias',
runtime: props.runtime || defaults.runtime,
timeout: core.Duration.minutes(1),
});
const actions = ['route53:ChangeResourceRecordSets', 'route53:GetChange'];
actions.forEach((action) => {
fcn.addToRolePolicy(new awsIam.PolicyStatement({
actions: [action],
resources: [`arn:aws:route53:::hostedzone/${props.hostedZoneId}`],
}));
});
const region = props.apiRegion || 'us-east-1';
const aliasTarget = {
DNSName: props.apiDomainNameAlias,
HostedZoneId: apiGatewayHostedZoneId(region),
EvaluateTargetHealth: false,
};
new awsCloudformation.CustomResource(this, 'Resource', {
provider: awsCloudformation.CustomResourceProvider.lambda(fcn),
properties: {
HostedZoneId: props.hostedZoneId,
DomainName: props.prefixedDomain,
TheAliasTarget: aliasTarget,
},
});
}
}
class SESDefaultRuleSetConstruct extends core.Construct {
constructor(scope, id, props) {
super(scope, id);
const fcn = new awsLambda.Function(this, 'Function', {
code: awsLambda.Code.fromAsset(crlDir),
handler: 'index.sesDefaultRuleSet',
runtime: props.runtime || defaults.runtime,
timeout: core.Duration.minutes(1),
});
const actions = [
'ses:CreateReceiptRule',
'ses:CreateReceiptRuleSet',
'ses:DeleteReceiptRule',
'ses:DeleteReceiptRuleSet',
'ses:DescribeActiveReceiptRuleSet',
'ses:DescribeReceiptRuleSet',
'ses:ListReceiptRuleSets',
'ses:SetActiveReceiptRuleSet',
'ses:UpdateReceiptRule',
];
actions.forEach((action) => {
fcn.addToRolePolicy(new awsIam.PolicyStatement({
actions: [action],
resources: ['*'],
}));
});
new awsCloudformation.CustomResource(this, 'Resource', {
provider: awsCloudformation.CustomResourceProvider.lambda(fcn),
properties: {
Emails: props.emails,
TopicArns: props.topicArns,
},
});
}
}
class VerifyDomainConstruct extends core.Construct {
constructor(scope, id, props) {
super(scope, id);
const certificateArn = props.certificateArn || 'null';
const fcn = new awsLambda.Function(this, 'Function', {
code: awsLambda.Code.fromAsset(crlDir),
handler: 'index.verifyDomain',
runtime: props.runtime || defaults.runtime,
timeout: core.Duration.minutes(1),
});
const sesActions = [
'acm:DescribeCertificate',
'ses:VerifyDomainIdentity',
'ses:VerifyDomainDkim',
'ses:GetIdentityVerificationAttributes',
'ses:GetIdentityDkimAttributes',
'ses:DeleteIdentity',
];
const r53Actions = ['route53:ChangeResourceRecordSets', 'route53:GetChange'];
sesActions.forEach((action) => {
fcn.addToRolePolicy(new awsIam.PolicyStatement({
actions: [action],
resources: ['*'],
}));
});
r53Actions.forEach((action) => {
fcn.addToRolePolicy(new awsIam.PolicyStatement({
actions: [action],
resources: [`arn:aws:route53:::hostedzone/${props.hostedZoneId}`],
}));
});
new awsCloudformation.CustomResource(this, 'Resource', {
provider: awsCloudformation.CustomResourceProvider.lambda(fcn),
properties: {
HostedZoneId: props.hostedZoneId,
DomainName: props.domain,
CertificateArn: certificateArn,
},
});
}
}
class CognitoConstruct extends core.Construct {
constructor(scope, id, props) {
super(scope, id);
// constants
const region = core.Aws.REGION;
const account = core.Aws.ACCOUNT_ID;
// postConfirmation lambda trigger
// tslint:disable-next-line: ban-types
let postConfirmation;
if (props.postConfirmTrigger) {
let layers;
if (props.useLayers) {
layers = new LambdaLayersConstruct(this, 'Layers', {
list: [{ name: 'CommonLibs', dirLayer: props.dirLayers }],
});
}
postConfirmation = new awsLambda.Function(this, 'PostConfirm', {
runtime: props.postConfirmRuntime || defaults.runtime,
code: awsLambda.Code.fromAsset(props.dirDist || 'dist'),
handler: `cognitoPostConfirm.handler`,
layers: layers ? layers.all : undefined,
environment: props.envars,
timeout: core.Duration.minutes(1),
});
postConfirmation.role.addToPrincipalPolicy(new awsIam.PolicyStatement({
actions: ['cognito-idp:*'],
resources: ['*'],
}));
if (props.postConfirmSendEmail) {
postConfirmation.role.addToPrincipalPolicy(new awsIam.PolicyStatement({
actions: ['ses:SendEmail'],
resources: ['*'],
}));
}
if (props.postConfirmDynamoTable) {
postConfirmation.role.addToPrincipalPolicy(new awsIam.PolicyStatement({
actions: ['dynamodb:*'],
resources: [
`arn:aws:dynamodb:${region}:*:table/${props.postConfirmDynamoTable}`,
`arn:aws:dynamodb:${region}:*:table/${props.postConfirmDynamoTable}/index/*`,
],
}));
}
}
// pool
const pool = new awsCognito.UserPool(this, 'Pool', {
userPoolName: props.poolName,
signInAliases: { email: true },
autoVerify: { email: true },
lambdaTriggers: {
postConfirmation,
},
selfSignUpEnabled: props.noSelfSignUp ? false : true,
userVerification: props.userVerification,
passwordPolicy: props.passwordPolicy,
});
this.poolId = pool.userPoolId;
// client
const client = new awsCognito.UserPoolClient(this, 'PoolCient', {
userPoolClientName: `${props.poolName}-client`,
userPool: pool,
generateSecret: false, // Amplify cannot handle secrets
});
this.clientId = client.userPoolClientId;
// cfnPool
const cfnPool = pool.node.findChild('Resource');
// set sending email
cfnPool.emailConfiguration = {
emailSendingAccount: 'DEVELOPER',
sourceArn: `arn:aws:ses:${region}:${account}:identity/${props.emailSendingAccount}`,
};
// add custom attributes
if (props.customAttributes) {
cfnPool.schema = props.customAttributes.map((name) => ({
attributeDataType: 'String',
mutable: true,
name,
}));
}
// some flags
const hasFacebook = !!(props.facebookClientId && props.facebookClientSecret);
const hasGoogle = !!(props.googleClientId && props.googleClientSecret);
const hasIdp = hasFacebook || hasGoogle;
const providers = [];
// Facebook identity provider
if (hasFacebook) {
new awsCognito.CfnUserPoolIdentityProvider(this, 'PoolFacebookIdP', {
userPoolId: pool.userPoolId,
providerName: 'Facebook',
providerType: 'Facebook',
providerDetails: {
client_id: props.facebookClientId,
client_secret: props.facebookClientSecret,
authorize_scopes: 'public_profile,email',
},
attributeMapping: {
email: 'email',
name: 'name',
},
});
providers.push('Facebook');
}
// Google identity provider
if (hasGoogle) {
new awsCognito.CfnUserPoolIdentityProvider(this, 'PoolGoogleIdP', {
userPoolId: pool.userPoolId,
providerName: 'Google',
providerType: 'Google',
providerDetails: {
client_id: props.googleClientId,
client_secret: props.googleClientSecret,
authorize_scopes: 'profile email openid',
},
attributeMapping: {
email: 'email',
name: 'name',
},
});
providers.push('Google');
}
// update pool client with IdPs data
if (hasIdp) {
new CognitoEnableProvidersConstruct(this, 'EnableProviders', {
userPoolId: pool.userPoolId,
userPoolClientId: client.userPoolClientId,
providers,
callbackUrls: props.callbackUrls,
logoutUrls: props.logoutUrls,
});
}
// set pool domain
if (props.domainPrefix) {
new CognitoPoolDomainConstruct(this, 'PoolDomain', {
userPoolId: pool.userPoolId,
domainPrefix: props.domainPrefix,
});
}
}
}
class DynamoConstruct extends core.Construct {
constructor(scope, id, props) {
super(scope, id);
this.name2table = {};
props.dynamoTables.forEach((t, i) => {
// new table
const table = new awsDynamodb.Table(this, `Table${i}`, {
tableName: t.name,
partitionKey: {
name: t.partitionKey,
type: t.partitionKeyType ? t.partitionKeyType : awsDynamodb.AttributeType.STRING,
},
sortKey: t.sortKey
? {
name: t.sortKey,
type: t.sortKeyType ? t.sortKeyType : awsDynamodb.AttributeType.STRING,
}
: undefined,
billingMode: t.onDemand ? awsDynamodb.BillingMode.PAY_PER_REQUEST : undefined,
timeToLiveAttribute: t.ttlAttribute ? t.ttlAttribute : undefined,
pointInTimeRecovery: t.pitRecovery ? t.pitRecovery : undefined,
readCapacity: t.readCapacity ? t.readCapacity : undefined,
writeCapacity: t.writeCapacity ? t.writeCapacity : undefined,
});
// indices
if (t.gsis) {
t.gsis.forEach((gsi) => table.addGlobalSecondaryIndex(gsi));
}
if (t.lsis) {
t.lsis.forEach((lsi) => table.addLocalSecondaryIndex(lsi));
}
// save
this.name2table[t.name] = table;
});
}
}
const addCorsToApiResource = (resource, methods = ['GET', 'PUT', 'POST', 'DELETE']) => {
const m = methods.join(',');
resource.addMethod('OPTIONS', new awsApigateway.MockIntegration({
integrationResponses: [
{
statusCode: '200',
responseParameters: {
'method.response.header.Access-Control-Allow-Headers': `'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'`,
'method.response.header.Access-Control-Allow-Origin': `'*'`,
'method.response.header.Access-Control-Allow-Credentials': `'false'`,
'method.response.header.Access-Control-Allow-Methods': `'OPTIONS,${m}'`,
},
},
],
passthroughBehavior: awsApigateway.PassthroughBehavior.NEVER,
requestTemplates: {
'application/json': '{"statusCode": 200}',
},
}), {
methodResponses: [
{
statusCode: '200',
responseParameters: {
'method.response.header.Access-Control-Allow-Headers': true,
'method.response.header.Access-Control-Allow-Methods': true,
'method.response.header.Access-Control-Allow-Credentials': true,
'method.response.header.Access-Control-Allow-Origin': true,
},
},
],
});
};
class LambdaApiConstruct extends core.Construct {
constructor(scope, id, props) {
super(scope, id);
const api = new awsApigateway.RestApi(this, `${props.gatewayName}`);
this.apiUrl = api.url;
if (props.customDomain && basic.allFilled(props.customDomain, ['apiRegion'])) {
const name = api.addDomainName('ApiDomainName', {
domainName: props.customDomain.prefixedDomain,
certificate: awsCertificatemanager.Certificate.fromCertificateArn(this, 'ApiCertificate', props.customDomain.certificateArn),
});
new Route53AliasConstruct(this, 'Route53Alias', {
hostedZoneId: props.customDomain.r53HostedZoneId,
prefixedDomain: props.customDomain.prefixedDomain,
apiDomainNameAlias: name.domainNameAliasDomainName,
apiRegion: props.customDomain.apiRegion,
});
}
let authorizer;
if (props.cognitoPoolId) {
authorizer = new AuthorizerConstruct(this, 'Authorizer', {
cognitoUserPoolId: props.cognitoPoolId,
restApiId: api.restApiId,
});
}
let allLayers;
if (props.useLayers || props.specLayers) {
const list = [];
if (props.useLayers) {
list.push(commonLayer);
}
if (props.specLayers) {
list.push(...props.specLayers);
}
allLayers = new LambdaLayersConstruct(this, 'Layers', { list });
}
props.lambdas.forEach((spec) => {
if (!spec.unprotected && !props.cognitoPoolId) {
throw new Error('cognitoPoolId is required for protected routes');
}
let layers;
if (allLayers) {
layers = [];
if (!spec.noCommonLayers) {
layers.push(allLayers.name2layer[commonLayer.name]);
}
if (spec.layers) {
for (const name of spec.layers) {
layers.push(allLayers.name2layer[name]);
}
}
}
const lambda = new awsLambda.Function(this, basic.camelize(spec.route, true), {
runtime: spec.runtime || defaults.runtime,
code: awsLambda.Code.fromAsset(spec.dirDist || 'dist'),
handler: `${spec.filenameKey}.${spec.handlerName}`,
environment: spec.envars,
timeout: spec.timeout,
memorySize: spec.memorySize,
layers,
});
const integration = new awsApigateway.LambdaIntegration(lambda);
const res = api.root.addResource(spec.route);
addCorsToApiResource(res, spec.httpMethods);
if (spec.subroute) {
const subres = res.addResource(spec.subroute);
spec.httpMethods.forEach((method) => {
const m = subres.addMethod(method, integration);
if (!spec.unprotected) {
authorizer.protectRoute(m);
}
});
}
else {
spec.httpMethods.forEach((method) => {
const m = res.addMethod(method, integration);
if (!spec.unprotected) {
authorizer.protectRoute(m);
}
});
}
if (spec.extraPermissions) {
spec.extraPermissions.forEach((s) => {
lambda.role.addToPrincipalPolicy(new awsIam.PolicyStatement({
actions: [s],
resources: ['*'],
}));
});
}
if (spec.accessDynamoTables) {
const region = lambda.stack.region;
spec.accessDynamoTables.forEach((t) => {
lambda.role.addToPrincipalPolicy(new awsIam.PolicyStatement({
actions: ['dynamodb:*'],
resources: [
`arn:aws:dynamodb:${region}:*:table/${t}`,
`arn:aws:dynamodb:${region}:*:table/${t}/index/*`,
],
}));
});
}
if (spec.accessBuckets) {
spec.accessBuckets.forEach((b) => {
lambda.role.addToPrincipalPolicy(new awsIam.PolicyStatement({
actions: ['s3:*'],
resources: [`arn:aws:s3:::${b}`, `arn:aws:s3:::${b}/*`],
}));
});
}
});
}
}
class LambdaConstruct extends core.Construct {
constructor(scope, id, props) {
super(scope, id);
let layers;
if (props.layerArns) {
layers = props.layerArns.map((arn) => awsLambda.LayerVersion.fromLayerVersionArn(this, 'Layer', arn));
}
const lambda = new awsLambda.Function(this, 'Lambda', {
runtime: props.runtime || defaults.runtime,
code: awsLambda.Code.fromAsset(props.dirDist || 'dist'),
handler: `${props.filenameKey}.${props.handlerName}`,
layers,
environment: props.envars,
timeout: props.timeout,
memorySize: props.memorySize,
});
this.functionName = lambda.functionName;
this.functionArn = lambda.functionArn;
if (props.extraPermissions) {
props.extraPermissions.forEach((s) => {
var _a;
(_a = lambda.role) === null || _a === void 0 ? void 0 : _a.addToPrincipalPolicy(new awsIam.PolicyStatement({
actions: [s],
resources: ['*'],
}));
});
}
if (props.accessDynamoTables) {
const region = lambda.stack.region;
props.accessDynamoTables.forEach((t) => {
var _a;
(_a = lambda.role) === null || _a === void 0 ? void 0 : _a.addToPrincipalPolicy(new awsIam.PolicyStatement({
actions: ['dynamodb:*'],
resources: [
`arn:aws:dynamodb:${region}:*:table/${t}`,
`arn:aws:dynamodb:${region}:*:table/${t}/index/*`,
],
}));
});
}
if (props.accessBuckets) {
props.accessBuckets.forEach((b) => {
var _a;
(_a = lambda.role) === null || _a === void 0 ? void 0 : _a.addToPrincipalPolicy(new awsIam.PolicyStatement({
actions: ['s3:*'],
resources: [`arn:aws:s3:::${b}`, `arn:aws:s3:::${b}/*`],
}));
});
}
}
}
class PipelineNotificationConstruct extends core.Construct {
constructor(scope, id, props) {
super(scope, id);
const topic = new awsSns.Topic(this, 'Topic', {
topicName: props.topicName,
});
props.emails.forEach(email => {
topic.addSubscription(new awsSnsSubscriptions.EmailSubscription(email));
});
const ePipeline = awsEvents.EventField.fromPath('$.detail.pipeline');
const eState = awsEvents.EventField.fromPath('$.detail.state');
props.pipeline.onStateChange('OnPipelineStateChange', {
eventPattern: {
detail: {
state: ['STARTED', 'FAILED', 'SUCCEEDED'],
},
},
target: new awsEventsTargets.SnsTopic(topic, {
message: awsEvents.RuleTargetInput.fromText(`Pipeline ${ePipeline} changed state to ${eState}`),
}),
});
}
}
class ReceiveEmailSQSConstruct extends core.Construct {
constructor(scope, id, props) {
super(scope, id);
this.topicArns = [];
this.queueUrls = [];
for (let i = 0; i < props.emails.length; i++) {
const title = props.topicNames ? props.topicNames[i] : basic.email2key(props.emails[i]);
const topic = new awsSns.Topic(this, `Topic${i}`, { topicName: title });
const queue = new awsSqs.Queue(this, `Queue${i}`, { queueName: title });
topic.addSubscription(new awsSnsSubscriptions.SqsSubscription(queue));
this.topicArns.push(topic.topicArn);
this.queueUrls.push(`https://sqs.${queue.stack.region}.amazonaws.com/${queue.stack.account}/${title}`);
}
new SESDefaultRuleSetConstruct(this, 'DefaultRuleSet', {
emails: props.emails,
topicArns: this.topicArns,
});
}
}
class Route53RecvEmailConstruct extends core.Construct {
constructor(scope, id, props) {
super(scope, id);
const certificateArn = props.certificateArn || 'null';
let zone;
if (props.hostedZoneId) {
zone = awsRoute53.PublicHostedZone.fromHostedZoneAttributes(this, 'HostedZone', {
hostedZoneId: props.hostedZoneId,
zoneName: props.domain,
});
}
else {
zone = new awsRoute53.PublicHostedZone(this, 'HostedZone', {
zoneName: props.domain,
comment: props.comment,
caaAmazon: true,
});
}
new awsRoute53.MxRecord(this, 'MX', {
zone,
values: [
{
priority: 10,
hostName: `inbound-smtp.${zone.stack.region}.amazonaws.com`,
},
],
});
new VerifyDomainConstruct(this, 'VerifyDomain', {
hostedZoneId: zone.hostedZoneId,
domain: props.domain,
certificateArn,
});
}
}
// Website creates bucket, cloudformation, certificate, and route53 data for a website
class WebsiteConstruct extends core.Construct {
constructor(scope, id, props) {
super(scope, id);
const certificateArn = props.certificateArn || 'null';
const prefixedDomain = `${props.prefix ? props.prefix + '.' : ''}${props.domain}`;
const errorCodes = props.errorCodes || [403, 404];
const errorRoute = props.errorRoute || '/index.html';
let zone;
if (props.hostedZoneId) {
zone = awsRoute53.PublicHostedZone.fromHostedZoneAttributes(this, 'HostedZone', {
hostedZoneId: props.hostedZoneId,
zoneName: props.domain,
});
}
else {
zone = new awsRoute53.PublicHostedZone(this, 'HostedZone', {
zoneName: props.domain,
comment: props.comment,
caaAmazon: true,
});
}
const bucket = new awsS3.Bucket(this, 'Bucket', {
bucketName: `${prefixedDomain}-${id.toLowerCase()}`,
removalPolicy: core.RemovalPolicy.DESTROY,
});
let aliasConfiguration;
if (certificateArn !== 'null') {
aliasConfiguration = {
acmCertRef: certificateArn,
names: [prefixedDomain],
};
if (!props.prefix) {
aliasConfiguration.names.push(`www.${props.domain}`);
}
}
const distribution = new awsCloudfront.CloudFrontWebDistribution(this, 'Distribution', {
aliasConfiguration,
defaultRootObject: undefined,
originConfigs: [
{
s3OriginSource: {
s3BucketSource: bucket,
},
behaviors: [
{
isDefaultBehavior: true,
},
],
},
],
priceClass: awsCloudfront.PriceClass.PRICE_CLASS_ALL,
errorConfigurations: errorCodes.map((code) => ({
errorCachingMinTtl: 300,
errorCode: code,
responseCode: 200,
responsePagePath: errorRoute,
})),
});
this.cloudfrontDistributionId = distribution.distributionId;
const target = new awsRoute53Targets.CloudFrontTarget(distribution);
// prefixed
if (props.prefix) {
new awsRoute53.ARecord(this, 'A', {
zone,
recordName: props.prefix,
target: awsRoute53.RecordTarget.fromAlias(target),
});
new awsRoute53.AaaaRecord(this, 'AAA', {
zone,
recordName: props.prefix,
target: awsRoute53.RecordTarget.fromAlias(target),
});
// standard
}
else {
new awsRoute53.ARecord(this, 'A', {
zone,
target: awsRoute53.RecordTarget.fromAlias(target),
});
new awsRoute53.AaaaRecord(this, 'AAAA', {
zone,
target: awsRoute53.RecordTarget.fromAlias(target),
});
new awsRoute53.ARecord(this, 'WWW', {
zone,
recordName: 'www',
target: awsRoute53.RecordTarget.fromAlias(target),
});
if (!props.skipMX) {
new awsRoute53.MxRecord(this, 'MX', {
zone,
values: [
{
priority: 10,
hostName: `inbound-smtp.${core.Aws.REGION}.amazonaws.com`,
},
],
});
}
if (!props.skipVerification) {
new VerifyDomainConstruct(this, 'VerifyDomain', {
hostedZoneId: zone.hostedZoneId,
domain: props.domain,
certificateArn,
runtime: props.runtimeForVErify,
});
}
}
}
}
// envars2cdk converts Ienvars to AWS CDK format
const envars2cdk = (envars) => {
return Object.keys(envars).reduce((acc, curr) => ({ ...acc, [curr]: { value: envars[curr] } }), {});
};
const npmCommands = (extraBeforeTest = '', useYarn = false) => {
if (useYarn) {
return {
install: [
'curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -',
'echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list',
'sudo apt-get update && sudo apt-get install yarn',
'yarn',
],
build: ['yarn test', 'yarn build'],
};
}
return {
install: ['npm install'],
build: extraBeforeTest
? [extraBeforeTest, 'npm run test', 'npm run build']
: ['npm run test', 'npm run build'],
};
};
// ssmSecret creates a reference to a parameter in the SSM Parameter Store (not Secrets Manager!)
const ssmSecret = (input) => {
return core.SecretValue.cfnDynamicReference(new core.CfnDynamicReference(core.CfnDynamicReferenceService.SSM, `${input.name}:${input.version}`));
};
// synthApp synthetizes App and returns object
const synthApp = (app) => {
const assembly = app.synth();
const synthesized = assembly.stacks[0];
return synthesized.template;
};
// synthAppString synthetizes App and returns stringifyied object
const synthAppString = (app) => {
return JSON.stringify(synthApp(app), undefined, 2);
};
class PipelineStack extends core.Stack {
constructor(scope, id, props) {
super(scope, id, props);
const group = props.group || 'service';
const stage = props.stage || '';
const cmds = npmCommands(props.npmBeforeTest, props.useYarn);
const commands = props.useYarn
? [`yarn cdk ${group} deploy ${stage} --require-approval never`]
: [`npm run cdk -- ${group} deploy ${stage} --require-approval never`];
const project = new awsCodebuild.PipelineProject(this, 'Project', {
environment: {
buildImage: props.buildImage || awsCodebuild.LinuxBuildImage.STANDARD_5_0,
},
environmentVariables: props.envars ? envars2cdk(props.envars) : {},
buildSpec: awsCodebuild.BuildSpec.fromObject({
version: '0.2',
phases: {
install: {
commands: cmds.install,
},
build: {
commands: cmds.build,
},
post_build: {
commands,
},
},
}),
});
const services = ['cloudformation', 's3', 'iam', ...props.services];
services.forEach((s) => {
project.role.addToPrincipalPolicy(new awsIam.PolicyStatement({
actions: [`${s}:*`],
resources: ['*'],
}));
});
const sourceOutput = new awsCodepipeline.Artifact();
const sourceAction = new awsCodepipelineActions.GitHubSourceAction({
actionName: 'SourceAction',
owner: props.githubUser,
repo: props.githubRepo,
branch: props.githubBranch || 'main',
oauthToken: props.githubSecret,
output: sourceOutput,
});
const buildOutput = new awsCodepipeline.Artifact();
const buildAction = new awsCodepipelineActions.CodeBuildAction({
actionName: 'BuildAction',
project,
input: sourceOutput,
outputs: [buildOutput],
});
const artifacts = new awsS3.Bucket(this, 'Artifacts', {
bucketName: `${id.toLowerCase()}-artifacts`,
removalPolicy: core.RemovalPolicy.DESTROY,
});
const pipeline = new awsCodepipeline.Pipeline(this, 'Pipeline', {
artifactBucket: artifacts,
restartExecutionOnUpdate: true,
});
pipeline.addStage({ stageName: 'Source', actions: [sourceAction] });
if (props.useConfirmation) {
pipeline.addStage({
stageName: 'Confirmation',
actions: [new awsCodepipelineActions.ManualApprovalAction({ actionName: 'AreYouSure' })],
});
}
pipeline.addStage({ stageName: 'Build', actions: [buildAction] });
if (props.notificationEmails) {
new PipelineNotificationConstruct(this, `${id}PNC`, {
topicName: `${id}-notifications`,
emails: props.notificationEmails,
pipeline,
});
}
}
}
class WebsitePipelineStack extends core.Stack {
constructor(scope, id, props) {
super(scope, id, props);
const assets = props.assetsDir || 'dist';
const nocacheFiles = props.nocacheFiles || ['index.html'];
const nocachePaths = '/' + nocacheFiles.join(' /');
const nocacheOptions = 'max-age=0, no-cache, no-store, must-revalidate';
const distroId = props.cloudfrontDistributionId;
const cmds = npmCommands(props.npmBeforeTest, props.useYarn);
const bucket = awsS3.Bucket.fromBucketName(this, 'Deployment', props.websiteBucketName);
const name = bucket.bucketName;
const project = new awsCodebuild.PipelineProject(this, 'Project', {
environment: {
buildImage: props.buildImage || awsCodebuild.LinuxBuildImage.STANDARD_5_0,
},
environmentVariables: props.envars ? envars2cdk(props.envars) : {},
buildSpec: awsCodebuild.BuildSpec.fromObject({
version: '0.2',
phases: {
install: {
commands: cmds.install,
},
build: {
commands: cmds.build,
},
post_build: {
commands: [
`aws s3 cp --recursive --acl public-read ./${assets} s3://${name}/`,
...nocacheFiles.map((f) => `aws s3 cp --acl public-read --cache-control="${nocacheOptions}" ./${assets}/${f} s3://${name}/`),
`aws cloudfront create-invalidation --distribution-id ${distroId} --paths ` +
nocachePaths,
],
},
},
artifacts: {
files: [`**/*`],
'base-directory': assets,
},
}),
});
bucket.grantPut(project);
project.role.addToPrincipalPolicy(new awsIam.PolicyStatement({
actions: ['cloudfront:CreateInvalidation'],
resources: ['*'],
}));
const sourceOutput = new awsCodepipeline.Artifact();
const sourceAction = new awsCodepipelineActions.GitHubSourceAction({
actionName: 'SourceAction',
owner: props.githubUser,
repo: props.githubRepo,
branch: props.githubBranch || 'main',
oauthToken: props.githubSecret,
output: sourceOutput,
});
const buildOutput = new awsCodepipeline.Artifact();
const buildAction = new awsCodepipelineActions.CodeBuildAction({
actionName: 'BuildAction',
project,
input: sourceOutput,
outputs: [buildOutput],
});
const artifacts = new awsS3.Bucket(this, 'Artifacts', {
bucketName: `${id.toLowerCase()}-artifacts`,
removalPolicy: core.RemovalPolicy.DESTROY,
});
const pipeline = new awsCodepipeline.Pipeline(this, 'Pipeline', {
artifactBucket: artifacts,
restartExecutionOnUpdate: true,
});
pipeline.addStage({ stageName: 'Source', actions: [sourceAction] });
if (props.useConfirmation) {
pipeline.addStage({
stageName: 'Confirmation',
actions: [new awsCodepipelineActions.ManualApprovalAction({ actionName: 'AreYouSure' })],
});
}
pipeline.addStage({ stageName: 'Build', actions: [buildAction] });
if (props.notificationEmails) {
new PipelineNotificationConstruct(this, `${id}PNC`, {
topicName: `${id}-notifications`,
emails: props.notificationEmails,
pipeline,
});
}
}
}
exports.AuthorizerConstruct = AuthorizerConstruct;
exports.BucketsConstruct = BucketsConstruct;
exports.CognitoConstruct = CognitoConstruct;
exports.CognitoEnableProvidersConstruct = CognitoEnableProvidersConstruct;
exports.CognitoPoolDomainConstruct = CognitoPoolDomainConstruct;
exports.DynamoConstruct = DynamoConstruct;
exports.LambdaApiConstruct = LambdaApiConstruct;
exports.LambdaConstruct = LambdaConstruct;
exports.LambdaLayersConstruct = LambdaLayersConstruct;
exports.PipelineNotificationConstruct = PipelineNotificationConstruct;
exports.PipelineStack = PipelineStack;
exports.ReceiveEmailSQSConstruct = ReceiveEmailSQSConstruct;
exports.Route53AliasConstruct = Route53AliasConstruct;
exports.Route53RecvEmailConstruct = Route53RecvEmailConstruct;
exports.SESDefaultRuleSetConstruct = SESDefaultRuleSetConstruct;
exports.VerifyDomainConstruct = VerifyDomainConstruct;
exports.WebsiteConstruct = WebsiteConstruct;
exports.WebsitePipelineStack = WebsitePipelineStack;
exports.apiGatewayHostedZoneId = apiGatewayHostedZoneId;
exports.commonLayer = commonLayer;
exports.defaults = defaults;
exports.envars2cdk = envars2cdk;
exports.npmCommands = npmCommands;
exports.ssmSecret = ssmSecret;
exports.synthApp = synthApp;
exports.synthAppString = synthAppString;