UNPKG

@cpmech/az-cdk

Version:
1,144 lines (1,113 loc) 47 kB
'use strict'; 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;