├── .npmignore ├── cloudfront-cd.png ├── .gitignore ├── jest.config.js ├── CODE_OF_CONDUCT.md ├── .eslintrc.cjs ├── mysite-content ├── index2.html └── index.html ├── lib ├── app-stacks │ ├── cf-managed-policy-constants.ts │ ├── cf-distribution-config.ts │ ├── staticcontent-stack.ts │ ├── primary-distribution-stack.ts │ ├── iam-policy-stack.ts │ └── staging-distribution-stack.ts ├── cf-cd-pipeline │ ├── primary-distribution-stage.ts │ ├── staging-distribution-stage.ts │ ├── promotedist-stepfunction-step.ts │ ├── updatedist-stepfunction-step.ts │ ├── stepfunction-stack.ts │ ├── stepfunction-definition.json │ └── pipeline-stack.ts └── pipeline-input-variables.ts ├── tsconfig.json ├── LICENSE ├── bin └── cf-cd-sample-app.ts ├── package.json ├── cdk.json ├── CONTRIBUTING.md └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /cloudfront-cd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-cloudfront-continuous-deployment/HEAD/cloudfront-cd.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | !aggregated_results.txt -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: "node", 3 | roots: ["/test"], 4 | testMatch: ["**/*.test.ts"], 5 | transform: { 6 | "^.+\\.tsx?$": "ts-jest", 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = { 3 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], 4 | parser: '@typescript-eslint/parser', 5 | plugins: ['@typescript-eslint'], 6 | root: true, 7 | rules: { 8 | "@typescript-eslint/no-namespace": "off" 9 | }, 10 | }; -------------------------------------------------------------------------------- /mysite-content/index2.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 |

Welcome to Mysite Landing Page - 2

10 | 11 |
12 |

Lorem ipsum, dolor sit amet consectetur adipisicing elit. Aperiam qui quis nam sed officiis delectus in quibusdam? Ad, odio voluptatibus asperiores error maiores ea praesentium soluta tempora consequatur? Deserunt, tenetur.

13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /mysite-content/index.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 |
9 |

Welcome to MySite Landing Page

10 |
11 |
12 | Lorem ipsum, dolor sit amet consectetur adipisicing elit. Aperiam qui quis nam sed officiis delectus in quibusdam? Ad, odio voluptatibus asperiores error maiores ea praesentium soluta tempora consequatur? Deserunt, tenetur. 13 | 14 |
15 | 16 | -------------------------------------------------------------------------------- /lib/app-stacks/cf-managed-policy-constants.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | export const CfManagedPolicies = { 5 | // this file defines constants for AWS CloudFront managed policy identifiers 6 | CF_MANAGED_OPTIMIZED_CACHE_POLICY_ID: "658327ea-f89d-4fab-a63d-7e88639e58f6", 7 | CF_NO_CACHING_POLICY_ID: "4135ea2d-6df8-44a3-9df3-4b5a84be39ad", 8 | 9 | CF_MANAGED_ALL_VIEWER_REQUEST_POLICY_ID: 10 | "216adef6-5c7f-47e4-b989-5492eafa07d3", 11 | 12 | CF_ALL_VIWER_EXCEPT_HOST_POLICY_ID: "b689b0a8-53d0-40ab-baf2-68738e2966ac", 13 | CF_ALL_VIEWER_CF_HEADERS_POLICY_ID: "33f36d7e-f396-46d9-90e0-52428a34d9dc", 14 | }; 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": ["es2020"], 6 | "declaration": true, 7 | "strict": true, 8 | "noImplicitAny": true, 9 | "strictNullChecks": true, 10 | "noImplicitThis": true, 11 | "alwaysStrict": true, 12 | "noUnusedLocals": false, 13 | "noUnusedParameters": false, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": false, 16 | "inlineSourceMap": true, 17 | "inlineSources": true, 18 | "experimentalDecorators": true, 19 | "strictPropertyInitialization": false, 20 | "typeRoots": ["./node_modules/@types"] 21 | }, 22 | "exclude": ["node_modules", "cdk.out"] 23 | } 24 | -------------------------------------------------------------------------------- /lib/cf-cd-pipeline/primary-distribution-stage.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | import * as cdk from "aws-cdk-lib"; 5 | import { StackProps } from "aws-cdk-lib"; 6 | import { Construct } from "constructs"; 7 | import { PrimaryDistributionStack } from "../app-stacks/primary-distribution-stack"; 8 | 9 | export class PrimaryDistributionStage extends cdk.Stage { 10 | primaryDistributionStack: PrimaryDistributionStack; 11 | 12 | constructor( 13 | scope: Construct, 14 | id: string, 15 | env?: StackProps, 16 | props?: cdk.StageProps 17 | ) { 18 | super(scope, id, props); 19 | const primaryDistributionStack = new PrimaryDistributionStack( 20 | this, 21 | "cf-distribution-stack", 22 | env 23 | ); 24 | this.primaryDistributionStack = primaryDistributionStack; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT No Attribution 2 | 3 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | -------------------------------------------------------------------------------- /bin/cf-cd-sample-app.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import * as cdk from "aws-cdk-lib"; 3 | import { PipelineStack } from "../lib/cf-cd-pipeline/pipeline-stack"; 4 | import { PipelineInputVariables } from "../lib/pipeline-input-variables"; 5 | 6 | const app = new cdk.App(); 7 | 8 | new PipelineStack(app, PipelineInputVariables.PIPELINE_NAME, { 9 | /* If you don't specify 'env', this stack will be environment-agnostic. 10 | * Account/Region-dependent features and context lookups will not work, 11 | * but a single synthesized template can be deployed anywhere. */ 12 | /* Uncomment the next line to specialize this stack for the AWS Account 13 | * and Region that are implied by the current CLI configuration. */ 14 | //env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }, 15 | /* Uncomment the next line if you know exactly what Account and Region you 16 | * want to deploy the stack to. */ 17 | // env: { account: '123456789012', region: 'us-east-1' }, 18 | }); 19 | -------------------------------------------------------------------------------- /lib/pipeline-input-variables.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | 5 | export const PipelineInputVariables = { 6 | // Pipeline variables 7 | PIPELINE_CODE_REPO: "cf-cd-repository", 8 | PIPELINE_CODE_BRANCH: "main", 9 | 10 | PIPELINE_NAME: "cloudfront-cd-pipeline", 11 | // prefix 12 | STEP_FUNCTION_NAME: "CFCDPipelineStepFunction", 13 | 14 | ENABLE_CONTINUOUS_DEPLOYMENT: false, 15 | HEADER_BASED_TRAFFIC_CONFIG: true, 16 | 17 | // site access logs bucket name 18 | LOG_BUCKET_NAME: "mysite-logs", 19 | }; 20 | 21 | export const PipelineExportNames = { 22 | STEP_FUNCTION_ROLE_ARN: `${PipelineInputVariables.PIPELINE_NAME}-cd-pipeline-stepfunction-roleArn`, 23 | 24 | PRIMARY_DISTRIBUTION_ID: `${PipelineInputVariables.PIPELINE_NAME}-primary-distribution-id`, 25 | 26 | STAGING_DISTRIBUTION_ID: `${PipelineInputVariables.PIPELINE_NAME}-staging-distribution-id`, 27 | DEPLOYMENT_POLICY_ID: `${PipelineInputVariables.PIPELINE_NAME}-staging-deployment-policy-id`, 28 | }; 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cf-cd-sample-pipeline", 3 | "version": "0.1.0", 4 | "bin": { 5 | "cf-cd-sample-pipeline": "bin/cf-cd-sample-app.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest", 11 | "cdk": "cdk", 12 | "lint": "eslint --ext .ts .", 13 | "format": "prettier --ignore-path .gitignore --write \"**/*.+(js|ts|json)\"" 14 | }, 15 | "devDependencies": { 16 | "@types/jest": "^29.4.0", 17 | "@types/node": "18.14.6", 18 | "@typescript-eslint/eslint-plugin": "^5.59.7", 19 | "@typescript-eslint/parser": "^5.59.7", 20 | "aws-cdk": "^2.85.0", 21 | "eslint": "^8.41.0", 22 | "jest": "^29.5.0", 23 | "prettier": "^2.8.8", 24 | "ts-jest": "^29.0.5", 25 | "ts-node": "^10.9.1", 26 | "typescript": "~4.9.5" 27 | }, 28 | "dependencies": { 29 | "aws-cdk-lib": "^2.189.1", 30 | "cdk-nag": "^2.25.7", 31 | "constructs": "^10.0.0", 32 | "source-map-support": "^0.5.21" 33 | }, 34 | "overrides": { 35 | "word-wrap": "1.2.4" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/cf-cd-pipeline/staging-distribution-stage.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | import * as cdk from "aws-cdk-lib"; 5 | import { Construct } from "constructs"; 6 | import { StackProps } from "aws-cdk-lib"; 7 | import { StagingDistributionStack } from "../app-stacks/staging-distribution-stack"; 8 | 9 | export class StagingDistributionStage extends cdk.Stage { 10 | stagingDistributionOutput: cdk.CfnOutput; 11 | stagingDeploymentPolicyOutput: cdk.CfnOutput; 12 | 13 | constructor( 14 | scope: Construct, 15 | id: string, 16 | stackProps?: StackProps, 17 | props?: cdk.StageProps 18 | ) { 19 | super(scope, id, props); 20 | const stagingDistributionStack = new StagingDistributionStack( 21 | this, 22 | "cf-distribution-stack", 23 | stackProps 24 | ); 25 | this.stagingDistributionOutput = 26 | stagingDistributionStack.distributionIdOutput; 27 | this.stagingDeploymentPolicyOutput = 28 | stagingDistributionStack.deploymentPolicyIdOutput; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/app-stacks/cf-distribution-config.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | import { CfnDistribution } from "aws-cdk-lib/aws-cloudfront"; 5 | import { CfManagedPolicies } from "./cf-managed-policy-constants"; 6 | 7 | // separating distribution config from distribution simplifies switching between continous deployment vs. direct deployment 8 | export class CfDistributionConfiguration { 9 | private _configuration: CfnDistribution.DistributionConfigProperty; 10 | 11 | constructor(config: CfDistributionConfiguration.CfConfigInput) { 12 | this._configuration = { 13 | defaultCacheBehavior: { 14 | targetOriginId: config.bucketName, 15 | viewerProtocolPolicy: "https-only", 16 | cachePolicyId: CfManagedPolicies.CF_MANAGED_OPTIMIZED_CACHE_POLICY_ID, 17 | originRequestPolicyId: 18 | CfManagedPolicies.CF_ALL_VIWER_EXCEPT_HOST_POLICY_ID, 19 | }, 20 | 21 | enabled: true, 22 | staging: config.staging, 23 | comment: config.distributionName, 24 | origins: [ 25 | { 26 | id: config.bucketName, 27 | domainName: config.bucketDomainName, 28 | s3OriginConfig: { 29 | originAccessIdentity: "", 30 | }, 31 | originAccessControlId: config.originAccessControlId, 32 | }, 33 | ], 34 | defaultRootObject: "index.html", 35 | }; 36 | } 37 | public get configuration(): CfnDistribution.DistributionConfigProperty { 38 | return this._configuration; 39 | } 40 | } 41 | 42 | export declare namespace CfDistributionConfiguration { 43 | interface CfConfigInput { 44 | distributionName: string; 45 | bucketName: string; 46 | bucketDomainName: string; 47 | originAccessControlId: string; 48 | staging: boolean; 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /lib/app-stacks/staticcontent-stack.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | import * as cdk from "aws-cdk-lib"; 5 | import { Bucket } from "aws-cdk-lib/aws-s3"; 6 | import { BucketDeployment, Source } from "aws-cdk-lib/aws-s3-deployment"; 7 | import { Construct } from "constructs"; 8 | import { PipelineInputVariables } from "../pipeline-input-variables"; 9 | 10 | export class StaticContentStack extends cdk.NestedStack { 11 | bucketName: string; 12 | bucketDomainName: string; 13 | 14 | constructor(scope: Construct, id: string, props?: cdk.StackProps) { 15 | super(scope, id, props); 16 | 17 | const loggingBucket = new Bucket( 18 | this, 19 | PipelineInputVariables.LOG_BUCKET_NAME, 20 | { 21 | blockPublicAccess: cdk.aws_s3.BlockPublicAccess.BLOCK_ALL, 22 | encryption: cdk.aws_s3.BucketEncryption.S3_MANAGED, 23 | enforceSSL: true, 24 | removalPolicy: cdk.RemovalPolicy.RETAIN, 25 | } 26 | ); 27 | 28 | const targetBucket = new Bucket(this, "mysite-content", { 29 | blockPublicAccess: cdk.aws_s3.BlockPublicAccess.BLOCK_ALL, 30 | encryption: cdk.aws_s3.BucketEncryption.S3_MANAGED, 31 | enforceSSL: true, 32 | versioned: true, 33 | removalPolicy: cdk.RemovalPolicy.RETAIN, 34 | serverAccessLogsBucket: loggingBucket, 35 | serverAccessLogsPrefix: "mysite-content-access-log", 36 | }); 37 | 38 | new BucketDeployment(this, "DeployWebSite", { 39 | sources: [Source.asset("./mysite-content")], 40 | destinationBucket: targetBucket, 41 | }); 42 | 43 | this.bucketName = targetBucket.bucketName; 44 | this.bucketDomainName = targetBucket.bucketDomainName; 45 | } 46 | 47 | createCfnOutput(name: string, value: string): void { 48 | new cdk.CfnOutput(this, name, { value: value, exportName: name }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/cf-cd-sample-app.ts", 3 | "watch": { 4 | "include": ["**"], 5 | "exclude": [ 6 | "README.md", 7 | "cdk*.json", 8 | "**/*.d.ts", 9 | "**/*.js", 10 | "tsconfig.json", 11 | "package*.json", 12 | "yarn.lock", 13 | "node_modules", 14 | "test" 15 | ] 16 | }, 17 | "context": { 18 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 19 | "@aws-cdk/core:checkSecretUsage": true, 20 | "@aws-cdk/core:target-partitions": ["aws", "aws-cn"], 21 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 22 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 23 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 24 | "@aws-cdk/aws-iam:minimizePolicies": true, 25 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 26 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 27 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 28 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 29 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 30 | "@aws-cdk/core:enablePartitionLiterals": true, 31 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 32 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 33 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 34 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 35 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 36 | "@aws-cdk/aws-route53-patters:useCertificate": true, 37 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false, 38 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, 39 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, 40 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, 41 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, 42 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, 43 | "@aws-cdk/aws-redshift:columnId": true, 44 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/cf-cd-pipeline/promotedist-stepfunction-step.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | import { CfnOutput, Environment } from "aws-cdk-lib"; 5 | import { 6 | StateMachineInput, 7 | StepFunctionInvokeAction, 8 | } from "aws-cdk-lib/aws-codepipeline-actions"; 9 | import { IStage } from "aws-cdk-lib/aws-codepipeline/lib"; 10 | import { StateMachine } from "aws-cdk-lib/aws-stepfunctions"; 11 | import { 12 | CodePipelineActionFactoryResult, 13 | ICodePipelineActionFactory, 14 | ProduceActionOptions, 15 | StackOutputReference, 16 | Step, 17 | } from "aws-cdk-lib/pipelines"; 18 | 19 | export class PromoteDistributionPipelineStep 20 | extends Step 21 | implements ICodePipelineActionFactory 22 | { 23 | stagingDistrbutionId: StackOutputReference; 24 | primaryDistributionId: string; 25 | env: Environment; 26 | stepFunctionName: string; 27 | 28 | constructor( 29 | primaryDistributionId: string, 30 | stagingDistributionId: CfnOutput, 31 | stepFunctionName: string, 32 | env: Environment 33 | ) { 34 | super("PromoteStagingDistributionStep"); 35 | this.stagingDistrbutionId = StackOutputReference.fromCfnOutput( 36 | stagingDistributionId 37 | ); 38 | this.primaryDistributionId = primaryDistributionId; 39 | 40 | this.stepFunctionName = stepFunctionName; 41 | this.env = env; 42 | } 43 | 44 | produceAction( 45 | stage: IStage, 46 | options: ProduceActionOptions 47 | ): CodePipelineActionFactoryResult { 48 | const stepFunctionArn = `arn:aws:states:${this.env.region}:${this.env.account}:stateMachine:${this.stepFunctionName}`; 49 | const stateMachine = StateMachine.fromStateMachineArn( 50 | options.scope, 51 | "cf-promotion-state-machine", 52 | stepFunctionArn 53 | ); 54 | 55 | stage.addAction( 56 | new StepFunctionInvokeAction({ 57 | actionName: options.actionName, 58 | stateMachine: stateMachine, 59 | stateMachineInput: StateMachineInput.literal({ 60 | Id: this.primaryDistributionId, 61 | StagingDistributionId: options.stackOutputsMap.toCodePipeline( 62 | this.stagingDistrbutionId 63 | ), 64 | }), 65 | runOrder: 1, 66 | }) 67 | ); 68 | 69 | return { runOrdersConsumed: 1 }; 70 | } 71 | 72 | public get consumedStackOutputs(): StackOutputReference[] { 73 | return [this.stagingDistrbutionId]; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /lib/cf-cd-pipeline/updatedist-stepfunction-step.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | import { CfnOutput, Environment } from "aws-cdk-lib"; 5 | import { IStage } from "aws-cdk-lib/aws-codepipeline"; 6 | import { 7 | StateMachineInput, 8 | StepFunctionInvokeAction, 9 | } from "aws-cdk-lib/aws-codepipeline-actions"; 10 | import { StateMachine } from "aws-cdk-lib/aws-stepfunctions"; 11 | import { 12 | CodePipelineActionFactoryResult, 13 | ICodePipelineActionFactory, 14 | ProduceActionOptions, 15 | StackOutputReference, 16 | Step, 17 | } from "aws-cdk-lib/pipelines"; 18 | 19 | export class UpdateDistributionPipelineStep 20 | extends Step 21 | implements ICodePipelineActionFactory 22 | { 23 | deploymentPolicyId: StackOutputReference; 24 | primaryDistributionId: string; 25 | env: Environment; 26 | stepFunctionName: string; 27 | 28 | constructor( 29 | primaryDistributionId: string, 30 | stepFunctionName: string, 31 | env: Environment, 32 | deploymentPolicyId?: CfnOutput 33 | ) { 34 | super("UpdateDeploymentPolicyStep"); 35 | if (deploymentPolicyId) { 36 | this.deploymentPolicyId = 37 | StackOutputReference.fromCfnOutput(deploymentPolicyId); 38 | } 39 | 40 | this.primaryDistributionId = primaryDistributionId; 41 | this.stepFunctionName = stepFunctionName; 42 | this.env = env; 43 | } 44 | 45 | produceAction( 46 | stage: IStage, 47 | options: ProduceActionOptions 48 | ): CodePipelineActionFactoryResult { 49 | // Actions don't support stack outputs for state machine 50 | const stepFunctionArn = `arn:aws:states:${this.env.region}:${this.env.account}:stateMachine:${this.stepFunctionName}`; 51 | 52 | const stateMachine = StateMachine.fromStateMachineArn( 53 | options.scope, 54 | "cf-promotion-state-machine", 55 | stepFunctionArn 56 | ); 57 | stage.addAction( 58 | new StepFunctionInvokeAction({ 59 | actionName: options.actionName, 60 | stateMachine: stateMachine, 61 | stateMachineInput: StateMachineInput.literal({ 62 | Id: this.primaryDistributionId, 63 | DeploymentPolicy: { 64 | ContinuousDeploymentPolicyId: this.deploymentPolicyId 65 | ? options.stackOutputsMap.toCodePipeline(this.deploymentPolicyId) 66 | : "", 67 | }, 68 | }), 69 | runOrder: 1, 70 | }) 71 | ); 72 | 73 | return { runOrdersConsumed: 1 }; 74 | } 75 | 76 | public get consumedStackOutputs(): StackOutputReference[] { 77 | if (this.deploymentPolicyId) { 78 | return [this.deploymentPolicyId]; 79 | } 80 | 81 | return []; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /lib/app-stacks/primary-distribution-stack.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | import * as cdk from "aws-cdk-lib"; 5 | import { 6 | CfnDistribution, 7 | CfnOriginAccessControl, 8 | } from "aws-cdk-lib/aws-cloudfront"; 9 | import { Construct } from "constructs"; 10 | import { 11 | PipelineExportNames 12 | } from "../pipeline-input-variables"; 13 | import { CfDistributionConfiguration } from "./cf-distribution-config"; 14 | import { IamPolicyStack } from "./iam-policy-stack"; 15 | import { StaticContentStack } from "./staticcontent-stack"; 16 | 17 | export class PrimaryDistributionStack extends cdk.Stack { 18 | distributionIdOutput: cdk.CfnOutput; 19 | distributionDomainNameOutput: cdk.CfnOutput; 20 | 21 | constructor(scope: Construct, id: string, props?: cdk.StackProps) { 22 | super(scope, id, props); 23 | 24 | const staticContentStack = new StaticContentStack( 25 | this, 26 | "static-content-stack", 27 | props 28 | ); 29 | 30 | const bucketDomainName = staticContentStack.bucketDomainName; 31 | const s3BucketName = staticContentStack.bucketName; 32 | 33 | const originAccessControlId = this.createOriginAccessControl().attrId; 34 | 35 | const primaryDistribution = this.createCfDistribution( 36 | "mydistribution", 37 | s3BucketName, 38 | bucketDomainName, 39 | originAccessControlId 40 | ); 41 | 42 | const outputName = PipelineExportNames.PRIMARY_DISTRIBUTION_ID; 43 | 44 | this.distributionIdOutput = new cdk.CfnOutput(this, outputName, { 45 | value: primaryDistribution.attrId, 46 | exportName: outputName, 47 | }); 48 | 49 | this.distributionDomainNameOutput = new cdk.CfnOutput( 50 | this, 51 | "primary-distribution-domainName-output", 52 | { value: primaryDistribution.attrDomainName } 53 | ); 54 | 55 | new IamPolicyStack( 56 | this, 57 | "distribution-iam-policies", 58 | { 59 | bucketName: s3BucketName, 60 | primaryDistributionId: primaryDistribution.attrId, 61 | bucketDomainName: bucketDomainName, 62 | }, 63 | props 64 | ); 65 | } 66 | 67 | createCfDistribution( 68 | distributionName: string, 69 | s3BucketName: string, 70 | bucketDomainName: string, 71 | originAccessControlId: string 72 | ): CfnDistribution { 73 | const configuration = new CfDistributionConfiguration({ 74 | distributionName: distributionName, 75 | bucketName: s3BucketName, 76 | bucketDomainName: bucketDomainName, 77 | originAccessControlId: originAccessControlId, 78 | staging: false, 79 | }).configuration; 80 | 81 | return new CfnDistribution(this, distributionName, { 82 | distributionConfig: configuration, 83 | }); 84 | } 85 | 86 | createOriginAccessControl(): CfnOriginAccessControl { 87 | return new CfnOriginAccessControl(this, "MyCfnOriginAccessControl", { 88 | originAccessControlConfig: { 89 | name: "S3OriginAccessControl", 90 | originAccessControlOriginType: "s3", 91 | signingBehavior: "always", 92 | signingProtocol: "sigv4", 93 | }, 94 | }); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /lib/cf-cd-pipeline/stepfunction-stack.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | import * as cdk from "aws-cdk-lib"; 5 | import { CfnOutput } from "aws-cdk-lib"; 6 | import { 7 | PolicyDocument, 8 | PolicyStatement, 9 | Role, 10 | ServicePrincipal, 11 | } from "aws-cdk-lib/aws-iam"; 12 | import { CfnStateMachine } from "aws-cdk-lib/aws-stepfunctions"; 13 | import { NagSuppressions } from "cdk-nag"; 14 | import { Construct } from "constructs"; 15 | import { 16 | PipelineExportNames, 17 | PipelineInputVariables, 18 | } from "../pipeline-input-variables"; 19 | import fs = require("fs"); 20 | import path = require("path"); 21 | 22 | export class StepFunctionStack extends cdk.NestedStack { 23 | stepFunctionName: string; 24 | stateMachineRoleArn: string; 25 | 26 | constructor(scope: Construct, id: string, props?: cdk.StackProps) { 27 | super(scope, id, props); 28 | 29 | const logGroup = new cdk.aws_logs.LogGroup( 30 | this, 31 | "CloudFrontPromotion-StepFunction-LogGroup" 32 | ); 33 | 34 | const stateMachineRole = new Role(this, "StepFunctionRole", { 35 | assumedBy: new ServicePrincipal("states.amazonaws.com"), 36 | description: "CloudFront Continuous Deployment StepFunction Role", 37 | inlinePolicies: { 38 | "Allow-LogGroup-Permissions": this.createLogGroupPolicy() 39 | }, 40 | }); 41 | 42 | this.stateMachineRoleArn = stateMachineRole.roleArn; 43 | const stepFunctionDefinition = fs.readFileSync( 44 | path.join(__dirname, "./stepfunction-definition.json"), 45 | "utf8" 46 | ); 47 | 48 | const stepFunctionName = PipelineInputVariables.STEP_FUNCTION_NAME; 49 | const stateMachine = new CfnStateMachine(this, stepFunctionName, { 50 | roleArn: stateMachineRole.roleArn, 51 | definitionString: stepFunctionDefinition, 52 | loggingConfiguration: { 53 | level: "ALL", 54 | destinations: [ 55 | { 56 | cloudWatchLogsLogGroup: { 57 | logGroupArn: logGroup.logGroupArn, 58 | }, 59 | }, 60 | ], 61 | }, 62 | tracingConfiguration: { 63 | enabled: true, 64 | }, 65 | }); 66 | 67 | this.stepFunctionName = stateMachine.attrName; 68 | new CfnOutput(this, "stepfunction-role-arn", { 69 | value: stateMachine.roleArn, 70 | exportName: PipelineExportNames.STEP_FUNCTION_ROLE_ARN, 71 | }); 72 | 73 | NagSuppressions.addResourceSuppressions( 74 | this, 75 | [ 76 | { 77 | id: "AwsSolutions-IAM5", 78 | reason: 79 | "Wildcard IAM permissions are used by auto-created Codepipeline policies and custom policies to allow flexible creation of resources", 80 | }, 81 | ], 82 | true 83 | ); 84 | } 85 | 86 | createLogGroupPolicy(): cdk.aws_iam.PolicyDocument { 87 | return new PolicyDocument({ 88 | statements: [ 89 | new PolicyStatement({ 90 | actions: [ 91 | "logs:CreateLogDelivery", 92 | "logs:CreateLogStream", 93 | "logs:GetLogDelivery", 94 | "logs:UpdateLogDelivery", 95 | "logs:DeleteLogDelivery", 96 | "logs:ListLogDeliveries", 97 | "logs:PutLogEvents", 98 | "logs:PutResourcePolicy", 99 | "logs:DescribeResourcePolicies", 100 | "logs:DescribeLogGroups", 101 | ], 102 | resources: ["*"], 103 | }), 104 | ], 105 | }); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /lib/cf-cd-pipeline/stepfunction-definition.json: -------------------------------------------------------------------------------- 1 | { 2 | "Comment": "StateMachine to update Amazon CloudFront Distribution. Supports updating ContinuousDeploymentPolicyId and Promoting a Staging Configuration", 3 | "StartAt": "UpdateTypeChoice", 4 | "States": { 5 | "UpdateTypeChoice": { 6 | "Type": "Choice", 7 | "Choices": [ 8 | { 9 | "Not": { 10 | "Variable": "$.StagingDistributionId", 11 | "IsPresent": true 12 | }, 13 | "Next": "Choice" 14 | } 15 | ], 16 | "Default": "GetDistribution" 17 | }, 18 | "Choice": { 19 | "Type": "Choice", 20 | "Choices": [ 21 | { 22 | "Variable": "$.DeploymentPolicy.ContinuousDeploymentPolicyId", 23 | "StringEquals": "", 24 | "Next": "GetPrimaryDistribution-Latest" 25 | } 26 | ], 27 | "Default": "GetContinuousDeploymentPolicyConfig" 28 | }, 29 | "GetDistribution": { 30 | "Type": "Task", 31 | "Next": "GetStagingDistribution", 32 | "Parameters": { 33 | "Id.$": "$.Id" 34 | }, 35 | "Resource": "arn:aws:states:::aws-sdk:cloudfront:getDistribution", 36 | "ResultPath": "$.PrimaryDistribution" 37 | }, 38 | "GetContinuousDeploymentPolicyConfig": { 39 | "Type": "Task", 40 | "Next": "PolicyEnabledChoice", 41 | "Parameters": { 42 | "Id.$": "$.DeploymentPolicy.ContinuousDeploymentPolicyId" 43 | }, 44 | "Resource": "arn:aws:states:::aws-sdk:cloudfront:getContinuousDeploymentPolicyConfig", 45 | "ResultPath": "$.DeploymentPolicyConfig", 46 | "ResultSelector": { 47 | "ContinuousDeploymentPolicyConfig.$": "$.ContinuousDeploymentPolicyConfig", 48 | "Update": { 49 | "Enabled": true 50 | }, 51 | "ETag.$": "$.ETag" 52 | } 53 | }, 54 | "PolicyEnabledChoice": { 55 | "Type": "Choice", 56 | "Choices": [ 57 | { 58 | "Variable": "$.DeploymentPolicyConfig.ContinuousDeploymentPolicyConfig.Enabled", 59 | "BooleanEquals": false, 60 | "Next": "UpdateContinuousDeploymentPolicy" 61 | } 62 | ], 63 | "Default": "GetPrimaryDistribution-Latest" 64 | }, 65 | "UpdateContinuousDeploymentPolicy": { 66 | "Type": "Task", 67 | "Parameters": { 68 | "ContinuousDeploymentPolicyConfig.$": "States.JsonMerge($.DeploymentPolicyConfig.ContinuousDeploymentPolicyConfig, $.DeploymentPolicyConfig.Update, false)", 69 | "Id.$": "$.DeploymentPolicy.ContinuousDeploymentPolicyId", 70 | "IfMatch.$": "$.DeploymentPolicyConfig.ETag" 71 | }, 72 | "Resource": "arn:aws:states:::aws-sdk:cloudfront:updateContinuousDeploymentPolicy", 73 | "Next": "GetPrimaryDistribution-Latest", 74 | "ResultPath": null 75 | }, 76 | "GetPrimaryDistribution-Latest": { 77 | "Type": "Task", 78 | "Next": "UpdateDistribution", 79 | "Parameters": { 80 | "Id.$": "$.Id" 81 | }, 82 | "Resource": "arn:aws:states:::aws-sdk:cloudfront:getDistribution", 83 | "ResultPath": "$.PrimaryDistribution" 84 | }, 85 | "UpdateDistribution": { 86 | "Type": "Task", 87 | "End": true, 88 | "Parameters": { 89 | "DistributionConfig.$": "States.JsonMerge($.PrimaryDistribution.Distribution.DistributionConfig, $.DeploymentPolicy, false)", 90 | "Id.$": "$.Id", 91 | "IfMatch.$": "$.PrimaryDistribution.ETag" 92 | }, 93 | "Resource": "arn:aws:states:::aws-sdk:cloudfront:updateDistribution" 94 | }, 95 | "GetStagingDistribution": { 96 | "Type": "Task", 97 | "Next": "UpdateDistributionWithStagingConfig", 98 | "Parameters": { 99 | "Id.$": "$.StagingDistributionId" 100 | }, 101 | "Resource": "arn:aws:states:::aws-sdk:cloudfront:getDistribution", 102 | "ResultPath": "$.StagingDistribution", 103 | "ResultSelector": { 104 | "ETag.$": "$.ETag" 105 | } 106 | }, 107 | "UpdateDistributionWithStagingConfig": { 108 | "Type": "Task", 109 | "Parameters": { 110 | "Id.$": "$.Id", 111 | "StagingDistributionId.$": "$.StagingDistributionId", 112 | "IfMatch.$": "States.Format('{},{}', $.PrimaryDistribution.ETag, $.StagingDistribution.ETag)" 113 | }, 114 | "Resource": "arn:aws:states:::aws-sdk:cloudfront:updateDistributionWithStagingConfig", 115 | "End": true 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /lib/app-stacks/iam-policy-stack.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | import * as cdk from "aws-cdk-lib"; 5 | 6 | import { 7 | Policy, 8 | PolicyDocument, 9 | PolicyStatement, 10 | Role, 11 | ServicePrincipal, 12 | } from "aws-cdk-lib/aws-iam"; 13 | import { CfnBucketPolicy } from "aws-cdk-lib/aws-s3"; 14 | import { Construct } from "constructs"; 15 | 16 | // grant required permissions to distribution to read origin bucket and for stepfunction to update distributions 17 | export class IamPolicyStack extends cdk.NestedStack { 18 | constructor( 19 | scope: Construct, 20 | id: string, 21 | input: IamPolicyStack.IamPolicyInput, 22 | props?: cdk.StackProps 23 | ) { 24 | super(scope, id, props); 25 | 26 | // attach s3 policy 27 | if (input.continuousDeployment) { 28 | const cd = input.continuousDeployment; 29 | 30 | this.attachS3ReadPolicy( 31 | input.bucketName, 32 | input.primaryDistributionId, 33 | cd.stagingDistributionId 34 | ); 35 | 36 | const stepFunctionRole = Role.fromRoleArn( 37 | this, 38 | "step-function-role", 39 | cd.stepFunctionRoleArn 40 | ); 41 | new Policy(this, "stepfunction-distribution-permissions", { 42 | policyName: "stepfunction-distribution-policy", 43 | roles: [stepFunctionRole], 44 | document: this.createStepFunctionPolicy( 45 | input.primaryDistributionId, 46 | cd.stagingDistributionId, 47 | cd.deploymentPolicyId 48 | ), 49 | }); 50 | } else { 51 | this.attachS3ReadPolicy(input.bucketName, input.primaryDistributionId); 52 | } 53 | } 54 | 55 | attachS3ReadPolicy( 56 | bucketName: string, 57 | primaryDistributionId: string, 58 | stagingDistributionId?: string 59 | ): void { 60 | const bucketResource = `arn:aws:s3:::${bucketName}/*`; 61 | const primaryDistributionArn = `arn:aws:cloudfront::${this.account}:distribution/${primaryDistributionId}`; 62 | 63 | const statements = [ 64 | new PolicyStatement({ 65 | actions: ["s3:GetObject"], 66 | principals: [new ServicePrincipal("cloudfront.amazonaws.com")], 67 | resources: [bucketResource], 68 | conditions: { 69 | StringEquals: { "AWS:SourceArn": primaryDistributionArn }, 70 | }, 71 | }), 72 | ]; 73 | 74 | if (stagingDistributionId) { 75 | const stagingDistributionArn = `arn:aws:cloudfront::${this.account}:distribution/${stagingDistributionId}`; 76 | statements.push( 77 | new PolicyStatement({ 78 | actions: ["s3:GetObject"], 79 | principals: [new ServicePrincipal("cloudfront.amazonaws.com")], 80 | resources: [bucketResource], 81 | conditions: { 82 | StringEquals: { "AWS:SourceArn": stagingDistributionArn }, 83 | }, 84 | }) 85 | ); 86 | } 87 | 88 | const policyDocument = new PolicyDocument({ 89 | statements: statements, 90 | }); 91 | 92 | new CfnBucketPolicy(this, "CF-distribution-read", { 93 | bucket: bucketName, 94 | policyDocument: policyDocument, 95 | }); 96 | } 97 | 98 | createStepFunctionPolicy( 99 | primaryDistributionId: string, 100 | stagingDistributionId: string, 101 | deploymentPolicyId: string 102 | ): PolicyDocument { 103 | const stagingDistributionResourceArn = this.formatArnString( 104 | stagingDistributionId 105 | ); 106 | const primaryDistributionResourceArn = this.formatArnString( 107 | primaryDistributionId 108 | ); 109 | const deploymentPolicyArn = `arn:aws:cloudfront::${this.account}:continuous-deployment-policy/${deploymentPolicyId}`; 110 | 111 | return new PolicyDocument({ 112 | statements: [ 113 | new PolicyStatement({ 114 | actions: [ 115 | "cloudfront:GetDistribution", 116 | "cloudfront:UpdateDistribution", 117 | "cloudfront:UpdateDistributionWithStagingConfig", 118 | ], 119 | resources: [ 120 | stagingDistributionResourceArn, 121 | primaryDistributionResourceArn, 122 | ], 123 | }), 124 | new PolicyStatement({ 125 | actions: [ 126 | "cloudfront:GetContinuousDeploymentPolicyConfig", 127 | "cloudfront:UpdateContinuousDeploymentPolicy", 128 | ], 129 | resources: [deploymentPolicyArn], 130 | }), 131 | ], 132 | }); 133 | } 134 | 135 | formatArnString(distributionId: string): string { 136 | return `arn:aws:cloudfront::${this.account}:distribution/${distributionId}`; 137 | } 138 | } 139 | 140 | export declare namespace IamPolicyStack { 141 | interface IamPolicyInput { 142 | primaryDistributionId: string; 143 | bucketName: string; 144 | bucketDomainName: string; 145 | continuousDeployment?: { 146 | stagingDistributionId: string; 147 | deploymentPolicyId: string; 148 | stepFunctionRoleArn: string; 149 | }; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /lib/app-stacks/staging-distribution-stack.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | import * as cdk from "aws-cdk-lib"; 5 | import { Construct } from "constructs"; 6 | import { 7 | CfnContinuousDeploymentPolicy, 8 | CfnDistribution, 9 | CfnOriginAccessControl, 10 | } from "aws-cdk-lib/aws-cloudfront"; 11 | import { CfDistributionConfiguration } from "./cf-distribution-config"; 12 | import { Fn } from "aws-cdk-lib"; 13 | import { StaticContentStack } from "./staticcontent-stack"; 14 | import { IamPolicyStack } from "./iam-policy-stack"; 15 | import { 16 | PipelineExportNames, 17 | PipelineInputVariables, 18 | } from "../pipeline-input-variables"; 19 | 20 | export class StagingDistributionStack extends cdk.Stack { 21 | distributionIdOutput: cdk.CfnOutput; 22 | deploymentPolicyIdOutput: cdk.CfnOutput; 23 | 24 | constructor(scope: Construct, id: string, props?: cdk.StackProps) { 25 | super(scope, id, props); 26 | 27 | const staticContentStack = new StaticContentStack( 28 | this, 29 | "static-content-stack", 30 | props 31 | ); 32 | const bucketDomainName = staticContentStack.bucketDomainName; 33 | const s3BucketName = staticContentStack.bucketName; 34 | 35 | const originAccessControlId = this.createOriginAccessControl().attrId; 36 | const stagingDistribution = this.createCfDistribution( 37 | "mystagingdistribution", 38 | s3BucketName, 39 | bucketDomainName, 40 | originAccessControlId 41 | ); 42 | const deploymentPolicy = this.createDeploymentPolicy( 43 | stagingDistribution.attrDomainName 44 | ); 45 | 46 | const outputName = PipelineExportNames.STAGING_DISTRIBUTION_ID; 47 | this.distributionIdOutput = new cdk.CfnOutput(this, outputName, { 48 | value: stagingDistribution.attrId, 49 | exportName: outputName, 50 | }); 51 | 52 | const cdPolicyIdOutput = PipelineExportNames.DEPLOYMENT_POLICY_ID; 53 | this.deploymentPolicyIdOutput = new cdk.CfnOutput(this, cdPolicyIdOutput, { 54 | value: deploymentPolicy.attrId, 55 | exportName: cdPolicyIdOutput, 56 | }); 57 | const primaryDistributionId = Fn.importValue( 58 | PipelineExportNames.PRIMARY_DISTRIBUTION_ID 59 | ); 60 | 61 | const stepFunctionRoleArn = Fn.importValue( 62 | PipelineExportNames.STEP_FUNCTION_ROLE_ARN 63 | ); 64 | 65 | new IamPolicyStack( 66 | this, 67 | "distribution-iam-policies", 68 | { 69 | bucketName: s3BucketName, 70 | primaryDistributionId: primaryDistributionId, 71 | bucketDomainName: bucketDomainName, 72 | continuousDeployment: { 73 | stagingDistributionId: stagingDistribution.attrId, 74 | deploymentPolicyId: deploymentPolicy.attrId, 75 | stepFunctionRoleArn: stepFunctionRoleArn, 76 | }, 77 | }, 78 | props 79 | ); 80 | } 81 | 82 | createCfDistribution( 83 | distributionName: string, 84 | s3BucketName: string, 85 | bucketDomainName: string, 86 | originAccessControlId: string 87 | ): CfnDistribution { 88 | return new CfnDistribution(this, distributionName, { 89 | distributionConfig: new CfDistributionConfiguration({ 90 | distributionName: distributionName, 91 | bucketName: s3BucketName, 92 | bucketDomainName: bucketDomainName, 93 | originAccessControlId: originAccessControlId, 94 | staging: true, 95 | }).configuration, 96 | }); 97 | } 98 | 99 | createOriginAccessControl(): CfnOriginAccessControl { 100 | return new CfnOriginAccessControl(this, "MyStagingCfnOriginAccessControl", { 101 | originAccessControlConfig: { 102 | name: "StagingS3OriginAccessControl", 103 | originAccessControlOriginType: "s3", 104 | signingBehavior: "always", 105 | signingProtocol: "sigv4", 106 | }, 107 | }); 108 | } 109 | 110 | createDeploymentPolicy( 111 | stagingDistributionDnsName: string 112 | ): CfnContinuousDeploymentPolicy { 113 | // swith the below flag to false to switch to a weight based traffic configuration 114 | const headerPolicy = PipelineInputVariables.HEADER_BASED_TRAFFIC_CONFIG; 115 | 116 | const tafficConfig = headerPolicy 117 | ? this.createHeaderBasedTrafficConfiguration() 118 | : this.createWeightBasedTrafficConfiguration(); 119 | 120 | return new CfnContinuousDeploymentPolicy( 121 | this, 122 | "MyCfnContinuousDeploymentPolicy", 123 | { 124 | continuousDeploymentPolicyConfig: { 125 | enabled: true, 126 | stagingDistributionDnsNames: [stagingDistributionDnsName], 127 | 128 | trafficConfig: tafficConfig, 129 | }, 130 | } 131 | ); 132 | } 133 | 134 | createHeaderBasedTrafficConfiguration(): CfnContinuousDeploymentPolicy.TrafficConfigProperty { 135 | return { 136 | type: "SingleHeader", 137 | singleHeaderConfig: { 138 | header: "aws-cf-cd-test", 139 | value: "blue", 140 | }, 141 | }; 142 | } 143 | 144 | createWeightBasedTrafficConfiguration(): CfnContinuousDeploymentPolicy.TrafficConfigProperty { 145 | return { 146 | type: "SingleWeight", 147 | singleWeightConfig: { 148 | weight: 0.1, 149 | sessionStickinessConfig: { 150 | idleTtl: 300, 151 | maximumTtl: 1800, 152 | }, 153 | }, 154 | }; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /lib/cf-cd-pipeline/pipeline-stack.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | import * as cdk from "aws-cdk-lib"; 5 | import { Construct } from "constructs"; 6 | import { 7 | CodePipeline, 8 | CodePipelineSource, 9 | ManualApprovalStep, 10 | ShellStep, 11 | } from "aws-cdk-lib/pipelines"; 12 | import { Repository } from "aws-cdk-lib/aws-codecommit"; 13 | import { CfnBucket } from "aws-cdk-lib/aws-s3"; 14 | import { CfnKey } from "aws-cdk-lib/aws-kms"; 15 | import { AwsSolutionsChecks, NagSuppressions } from "cdk-nag"; 16 | import { Aspects, Fn, StageProps } from "aws-cdk-lib"; 17 | import { StagingDistributionStage } from "./staging-distribution-stage"; 18 | import { PrimaryDistributionStage } from "./primary-distribution-stage"; 19 | import { PromoteDistributionPipelineStep } from "./promotedist-stepfunction-step"; 20 | import { UpdateDistributionPipelineStep } from "./updatedist-stepfunction-step"; 21 | import { StepFunctionStack } from "./stepfunction-stack"; 22 | import { 23 | PipelineExportNames, 24 | PipelineInputVariables, 25 | } from "../pipeline-input-variables"; 26 | 27 | export class PipelineStack extends cdk.Stack { 28 | constructor(scope: Construct, id: string, props?: cdk.StackProps) { 29 | super(scope, id, props); 30 | 31 | const pipeline = new CodePipeline(this, "Pipeline", { 32 | pipelineName: PipelineInputVariables.PIPELINE_NAME, 33 | synth: new ShellStep("Synth", { 34 | input: this.getCodePipelineSource(), 35 | commands: ["npm ci", "npm run build", "npx cdk synth"], 36 | }), 37 | crossAccountKeys: true, 38 | }); 39 | 40 | const stepFunctionStack = new StepFunctionStack( 41 | this, 42 | "cf-promotion-pipeline-step-function", 43 | props 44 | ); 45 | 46 | // Note: set continuous deployment to false for the first time to deploy the primary distribution 47 | const continousDeployment = 48 | PipelineInputVariables.ENABLE_CONTINUOUS_DEPLOYMENT; 49 | 50 | if (continousDeployment) { 51 | this.createContinuousDeployment( 52 | pipeline, 53 | stepFunctionStack.stepFunctionName, 54 | props 55 | ); 56 | } else { 57 | this.createPrimaryDistribution(pipeline); 58 | } 59 | 60 | Aspects.of(this).add( 61 | new AwsSolutionsChecks({ 62 | verbose: true, 63 | reports: true, 64 | }) 65 | ); 66 | 67 | pipeline.buildPipeline(); 68 | 69 | const artifactBucket = pipeline.pipeline.artifactBucket.node 70 | .defaultChild as CfnBucket; 71 | artifactBucket.loggingConfiguration = { 72 | logFilePrefix: "artifact_access_log", 73 | }; 74 | artifactBucket.versioningConfiguration = { status: "Enabled" }; 75 | 76 | if (pipeline.pipeline.artifactBucket.encryptionKey) { 77 | const key = pipeline.pipeline.artifactBucket.encryptionKey.node 78 | .defaultChild as CfnKey; 79 | key.enableKeyRotation = true; 80 | } 81 | 82 | NagSuppressions.addResourceSuppressions( 83 | pipeline, 84 | [ 85 | { 86 | id: "AwsSolutions-IAM5", 87 | reason: 88 | "Wildcard IAM permissions are used by auto-created Codepipeline policies and custom policies to allow flexible creation of resources", 89 | }, 90 | ], 91 | true 92 | ); 93 | } 94 | 95 | private getCodePipelineSource(): cdk.pipelines.IFileSetProducer | undefined { 96 | // using CodeCommit repository, but can easily switched to github or S3 97 | // Please refer to CodePipelineSource documentation for using different repository 98 | return CodePipelineSource.codeCommit( 99 | Repository.fromRepositoryName( 100 | this, 101 | "CD-Pipeline-Repository", 102 | PipelineInputVariables.PIPELINE_CODE_REPO 103 | ), 104 | PipelineInputVariables.PIPELINE_CODE_BRANCH 105 | ); 106 | } 107 | 108 | createPrimaryDistribution( 109 | pipeline: CodePipeline, 110 | props?: StageProps, 111 | stackProps?: cdk.StackProps 112 | ): void { 113 | const primaryDistribution = new PrimaryDistributionStage( 114 | this, 115 | "PrimaryDistribution-Change", 116 | stackProps 117 | ); 118 | 119 | const stage = pipeline.addStage(primaryDistribution); 120 | 121 | stage.addPost( 122 | new ShellStep("test distribution", { 123 | envFromCfnOutputs: { 124 | distributionDomainName: 125 | primaryDistribution.primaryDistributionStack 126 | .distributionDomainNameOutput, 127 | }, 128 | commands: ['curl -v "https://$distributionDomainName"'], 129 | }) 130 | ); 131 | } 132 | 133 | createContinuousDeployment( 134 | pipeline: CodePipeline, 135 | stepFunctionName: string, 136 | props?: cdk.StackProps 137 | ): void { 138 | if (!props || !props.env) { 139 | throw Error( 140 | "Account and Region are required if continuous deployment is enabled. Please uncomment or pass env in file cf-cd-sample-app.ts" 141 | ); 142 | } 143 | 144 | const stageDistribution = new StagingDistributionStage( 145 | this, 146 | "StagingDistribution-Change", 147 | props 148 | ); 149 | pipeline.addStage(stageDistribution); 150 | 151 | const updateDeploymentPolicyWave = pipeline.addWave( 152 | "Update-DeploymentPolicy" 153 | ); 154 | const primaryDistributionId = Fn.importValue( 155 | PipelineExportNames.PRIMARY_DISTRIBUTION_ID 156 | ); 157 | 158 | // use step function to avoid over-writing primary distribution configuration with old configuration 159 | // and update primary distribution to attach deployment policy id. 160 | const updateStep = new UpdateDistributionPipelineStep( 161 | primaryDistributionId, 162 | stepFunctionName, 163 | props.env, 164 | stageDistribution.stagingDeploymentPolicyOutput 165 | ); 166 | const manualStep = new ManualApprovalStep( 167 | "Approve-Promote-StagingDistribution", 168 | { 169 | comment: 170 | "Validate and approve staging distribution changes. Promote step will promote staging configuration changes to primary distribution.", 171 | } 172 | ); 173 | manualStep.addStepDependency(updateStep); 174 | 175 | updateDeploymentPolicyWave.addPost(updateStep, manualStep); 176 | 177 | pipeline 178 | .addWave("Promote-StagingDistribution") 179 | .addPost( 180 | new PromoteDistributionPipelineStep( 181 | primaryDistributionId, 182 | stageDistribution.stagingDistributionOutput, 183 | stepFunctionName, 184 | props.env 185 | ) 186 | ); 187 | } 188 | 189 | addAspect(construct: Construct): void { 190 | Aspects.of(construct).add( 191 | new AwsSolutionsChecks({ 192 | verbose: true, 193 | reports: true, 194 | }) 195 | ); 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Amazon CloudFront Continuous Deployment CDK Pipeline Sample 2 | 3 | Repository hosting sample AWS CDK Pipeline for deploying changes Amazon CloudFront Distribution using CloudFront continuous deployment 4 | 5 | ---- 6 | ## Architecture Diagram 7 | 8 | ![Architecture Diagram](/cloudfront-cd.png)*Continuous Delivery of CloudFront Distribution Configuration Changes* 9 | 10 | ---- 11 | 12 | ## Instructions to Use/Deploy this solution 13 | 14 | ### Pre-requisites 15 | 1. Install [NPM](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) 16 | 1. Install [CDK](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html) 17 | 1. AWS CodeBuild Repository 18 | 19 | ---- 20 | ### Instructions 21 | The following sections contain steps A through D with instructions to create a distribution, test distribution, and release distribution configuration changes using Amazon CloudFront Continuous deployment. 22 | 23 | #### Step - A: Setup Repository and Deploy Pipeline 24 | 1. Clone this repository to make changes and commit to your code repository 25 | 1. Update the repository settings by editing `lib/pipeline-input-variables.ts` change the value of `PIPELINE_CODE_REPO` variable to point to your code repository name 26 | 1. Add environment (account and region) by editing `bin/cf-cd-sample-app.ts` follow comments in the file to uncomment and edit the right environment entry 27 | 1. Bootstrap CDK using command `cdk bootstrap` if not already done 28 | 1. Update/customize the other variables per applicable naming standards 29 | 1. Update the git remote to point to code repository by running `git remote set-url origin ` 30 | 1. Commit your changes to code repository 31 | 1. Run command `npm install` to install dependencies 32 | 1. Run the command `cdk deploy` to deploy the pipeline 33 | 34 | > Executing instructions listed above creates a AWS CodePipeline and runs the pipeline. Running the pipeline will result in creating a Amazon CloudFront Distribution with S3 Origin. 35 | 36 | ---- 37 | #### Step - B: Validate the Pipeline and Test Distribution 38 | 1. Open AWS Console and navigate to AWS CodePipeline and validate that the pipeline was successful 39 | 1. Locate the distribution with description as `mydistribution` in Amazon CloudFront console and copy the `Distribution domain name` 40 | 1. Test the distribution by opening distribution URL in a browser or using command `curl ` 41 | 1. Opening the URL should display content of index.html 42 | 43 | #### Step - C: Change Distribution Configuration and Deploy 44 | 45 | 1. Make distribution configuration updates by editing `lib/app-stacks/cf-distribution-config.ts` file 46 | 1. Enable continuous deployment by edit `pipeline-input-variables.ts` file to update `ENABLE_CONTINUOUS_DEPLOYMENT` to true 47 | 1. commit code changes to repository 48 | 49 | > The above code commit will trigger the pipeline, which will result in deploying a staging distribution and deployment policy attached to the primary distribution created above in `Step - B` 50 | 51 | #### Step - D: Test changes and Promote 52 | > Below steps assume that the `SingleHeader` traffic configuration is in place. Http header is not required to test `SingleWeight` traffic configuration 53 | 1. Validate that the pipeline is waiting for approval on `Promote stage` 54 | 1. Test the configuration changes by using command line `curl -H "aws-cf-cd-test: blue" ` or through browser by adding additional http header `aws-cf-cd-test: blue` to the request 55 | 1. After completing testing, you can promote staging distribution configuration to primary distribution by approving the `Promote stage` step in the pipeline 56 | 57 | 58 | ### Cleanup 59 | Follow below instructions if you want to remove the Pipeline and stacks created by this sample. Please note that deleting a Pipeline doesn't remove the stacks created by pipeline and must be deleted using AWS CLI or AWS Console. 60 | > Please note that deleting stacks will remove the distribution. Do not run below steps on live distributions or distributions serving real users 61 | > If you are running with `ENABLE_CONTINUOUS_DEPLOYMENT` set to `true` then follow steps `Delete Deployment Policy` and `Delete Staging Distribution Stack` before `Delete Primary Distribution Stack` 62 | 63 | 64 | #### Delete Deployment Policy 65 | 1. Open CloudFront Console and locate the primary distribution. Click on Primary distribution and scroll down to `Continuous deployment` section. 66 | 1. Remove the deployment policy from primary distribution by clicking on `Delete` button 67 | 68 | #### Delete Staging Distribution Stack 69 | 1. Once deployment policy is removed, we will need to deploy primary distribution before we can delete staging distribution stack by disabling continuous deployment and running the pipeline 70 | 1. Disable continuous deployment by editing `pipeline-input-variables.ts` and setting `ENABLE_CONTINUOUS_DEPLOYMENT` to false 71 | 1. Commit the change to repository 72 | 1. Commit to repository will trigger the pipeline to deploy latest changes to primary distribution 73 | 1. Once the primary distribution is deployed, you can delete the staging distribution stack by running below command in through AWS CLI or from CloudFormation console 74 | 1. `aws cloudformation delete-stack --stack-name StagingDistribution-Change-cf-distribution-stack` 75 | 76 | 77 | #### Delete Primary Distribution Stack 78 | 1. run the following commands to delete the primary distribution stack or from CloudFormation console 79 | 1. `aws cloudformation delete-stack --stack-name PrimaryDistribution-Change-cf-distribution-stack` 80 | 81 | 82 | #### Delete Pipeline Stack 83 | 1. run the following command from repository root directory or delete the pipeline stack from CloudFormation console 84 | 1. `cdk destroy` 85 | 86 | #### Cleanup S3 Buckets 87 | 1. Remove S3 Origin bucket and Site logs bucket using S3 Console 88 | 89 | 90 | ## Pricing Calculations 91 | Cost for running this solution consists of Code Repository cost, CI/CD Pipeline cost, and Amazon CloudFront Distribution cost 92 | 93 | ### Assumptions 94 | 95 | | Assumption | Parameters | 96 | | --- | --- | 97 | | Region | us-east-1 | 98 | | AWS CodeCommit Repositories | 1 | 99 | | Number of AWS CodePipeline pipelines per month | 4 | 100 | | ***AWS CodeCommit Usage*** | | 101 | | Number of Active AWS CodeCommit Users per month | 5 | 102 | | ***Pipeline Usage*** | | | 103 | | Number of Continuous deployments to Amazon CloudFront Distribution per month | 64 | 104 | | ***Amazon CloudFront Usage*** | | | 105 | | CloudFront Data transfer out to internet per month | 100 GB | 106 | | CloudFront requests per month | 1,000,000 | 107 | 108 | 109 | | Total Cost | $13.53 | 110 | | --- | --- | 111 | | AWS CodePipeline | $4.00 | 112 | | AWS CodeCommit | $0.00 | 113 | | Amazon CloudFront | $9.50 | 114 | | StepFunctions | $0.025 | 115 | 116 | ### AWS Service wise break up of calculations 117 | 118 | | [AWS CodePipeline Pricing](https://aws.amazon.com/codepipeline/pricing/) | $4.00 | 119 | | --- | --- | 120 | | Number of active CodePipelines | 4 | 121 | | Cost per active CodePipeline per month | $1.00 | 122 | | ***Total Pipeline cost*** | $4.00 | 123 | 124 | | [StepFunctions Pricing](https://aws.amazon.com/step-functions/pricing/) | $0.028 | 125 | | --- | --- | 126 | | Number of state transitions | 1024 | 127 | | Cost per 1000 state transition | $0.025 | 128 | | ***Total Workflow cost*** | $0.0256 | 129 | 130 | | [AWS CodeCommit Pricing](https://aws.amazon.com/codecommit/pricing/) | $0.00 | 131 | | --- | --- | 132 | | Number of active users | 5 | 133 | | Cost for First 5 active users | $0.00 | 134 | | ***Total CodeCommit cost*** | $0.00 | 135 | 136 | | [Amazon CloudFront Pricing](https://aws.amazon.com/cloudfront/pricing/) | $9.50 | 137 | | --- | --- | 138 | | Total for data transfer out to internet per month | 100 GB | 139 | | Cost for data transfer out to internet for per GB | 0.085 | 140 | | ***Total CloudFront data transfer out to internet*** | $8.50 | 141 | | Total number of CloudFront requests per month | 1,000,000 | 142 | | Cost for 1,000,000 requests | $1.00 | 143 | | ***Total CloudFront requests cost*** | $1.00 | 144 | 145 | 146 | 147 | ## Troubleshooting 148 | 1. If the pipeline fails during stack creation, then the stack must be manually deleted before retrying the pipeline steps 149 | 2. set account via CDK_DEFAULT_ACCOUNT for agnostic or explicitly set account number if you see error 'Pipeline stack which uses cross-environment actions must have an explicitly set account' 150 | 151 | ## Security 152 | 153 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 154 | 155 | ## License 156 | 157 | This library is licensed under the MIT-0 License. See the LICENSE file. 158 | 159 | 160 | 161 | --------------------------------------------------------------------------------