├── .npmignore ├── images ├── UAT-Deployment-Workflow.png ├── Cross-Account-Deployment-Arch.png └── Production-Deployment-Workflow.png ├── jest.config.js ├── .gitignore ├── CODE_OF_CONDUCT.md ├── app └── index.js ├── lib ├── repository-stack.ts ├── application-stack.ts └── pipeline-stack.ts ├── package.json ├── tsconfig.json ├── LICENSE ├── bin └── pipeline.ts ├── scripts ├── automate_cleanup.sh └── automate_deployment.sh ├── cdk.json ├── CONTRIBUTING.md ├── templates ├── CodePipelineCrossAccountRole.yml └── CloudFormationDeploymentRole.yml └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /images/UAT-Deployment-Workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/automate-cross-account-cicd-cfn-cdk/HEAD/images/UAT-Deployment-Workflow.png -------------------------------------------------------------------------------- /images/Cross-Account-Deployment-Arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/automate-cross-account-cicd-cfn-cdk/HEAD/images/Cross-Account-Deployment-Arch.png -------------------------------------------------------------------------------- /images/Production-Deployment-Workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/automate-cross-account-cicd-cfn-cdk/HEAD/images/Production-Deployment-Workflow.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | bin/*.js 4 | lib/*.js 5 | !jest.config.js 6 | *.d.ts 7 | node_modules 8 | 9 | # CDK asset staging directory 10 | .cdk.staging 11 | cdk.out 12 | .cdk_output 13 | .cfn_outputs -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | exports.handler = async (event) => { 5 | const response = { 6 | statusCode: 200, 7 | body: `Hello from the ${process.env.STAGE_NAME} environment!\n`, 8 | }; 9 | return response; 10 | }; 11 | -------------------------------------------------------------------------------- /lib/repository-stack.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | import * as codecommit from 'aws-cdk-lib/aws-codecommit'; 5 | import { App, Stack, StackProps } from 'aws-cdk-lib' 6 | 7 | export class RepositoryStack extends Stack { 8 | constructor(app: App, id: string, props?: StackProps) { 9 | 10 | super(app, id, props); 11 | 12 | new codecommit.Repository(this, 'CodeCommitRepo', { 13 | repositoryName: `repo-${this.account}` 14 | }); 15 | 16 | } 17 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "automate-cross-account-cicd-cfn-cdk-v2", 3 | "version": "0.1.0", 4 | "bin": { 5 | "automate-cross-account-cicd-cfn-cdk-v2": "bin/automate-cross-account-cicd-cfn-cdk-v2.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest", 11 | "cdk": "cdk" 12 | }, 13 | "devDependencies": { 14 | "@types/jest": "^29.5.4", 15 | "@types/node": "20.5.3", 16 | "aws-cdk": "2.93.0", 17 | "jest": "^29.6.3", 18 | "ts-jest": "^29.1.1", 19 | "ts-node": "^10.9.1", 20 | "typescript": "~5.1.6" 21 | }, 22 | "dependencies": { 23 | "aws-cdk-lib": "^2.189.1", 24 | "constructs": "^10.0.0", 25 | "source-map-support": "^0.5.21" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2020", 7 | "dom" 8 | ], 9 | "declaration": true, 10 | "strict": true, 11 | "noImplicitAny": true, 12 | "strictNullChecks": true, 13 | "noImplicitThis": true, 14 | "alwaysStrict": true, 15 | "noUnusedLocals": false, 16 | "noUnusedParameters": false, 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": false, 19 | "inlineSourceMap": true, 20 | "inlineSources": true, 21 | "experimentalDecorators": true, 22 | "strictPropertyInitialization": false, 23 | "typeRoots": [ 24 | "./node_modules/@types" 25 | ] 26 | }, 27 | "exclude": [ 28 | "node_modules", 29 | "cdk.out" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /bin/pipeline.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | // SPDX-License-Identifier: MIT-0 5 | 6 | import * as cdk from 'aws-cdk-lib'; 7 | import { ApplicationStack } from '../lib/application-stack'; 8 | import { PipelineStack } from '../lib/pipeline-stack'; 9 | import { RepositoryStack } from '../lib/repository-stack'; 10 | 11 | const app = new cdk.App(); 12 | const uatAccountId = app.node.tryGetContext('uat-account') || process.env.CDK_INTEG_ACCOUNT || process.env.CDK_DEFAULT_ACCOUNT; 13 | const prodAccountId = app.node.tryGetContext('prod-account') || process.env.CDK_INTEG_ACCOUNT || process.env.CDK_DEFAULT_ACCOUNT; 14 | 15 | new RepositoryStack(app, 'RepositoryStack'); 16 | 17 | const uatApplicationStack = new ApplicationStack(app, 'UatApplicationStack', { stageName: 'uat' }); 18 | const prodApplicationStack = new ApplicationStack(app, 'ProdApplicationStack', { stageName: 'prod' }); 19 | new PipelineStack(app, 'CrossAccountPipelineStack', { 20 | uatApplicationStack: uatApplicationStack, 21 | uatAccountId: uatAccountId, 22 | prodApplicationStack: prodApplicationStack, 23 | prodAccountId: prodAccountId, 24 | }); 25 | -------------------------------------------------------------------------------- /lib/application-stack.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | import { StackProps, App, Stack } from 'aws-cdk-lib'; 5 | import * as lambda from 'aws-cdk-lib/aws-lambda'; 6 | import * as apigateway from 'aws-cdk-lib/aws-apigateway'; 7 | import * as codedeploy from 'aws-cdk-lib/aws-codedeploy'; 8 | 9 | export interface ApplicationStackProps extends StackProps { 10 | readonly stageName: string; 11 | } 12 | 13 | export class ApplicationStack extends Stack { 14 | public readonly lambdaCode: lambda.CfnParametersCode; 15 | 16 | constructor(app: App, id: string, props: ApplicationStackProps) { 17 | super(app, id, props); 18 | 19 | this.lambdaCode = lambda.Code.fromCfnParameters(); 20 | 21 | const func = new lambda.Function(this, 'Lambda', { 22 | functionName: 'HelloLambda', 23 | code: this.lambdaCode, 24 | handler: 'index.handler', 25 | runtime: lambda.Runtime.NODEJS_LATEST, 26 | environment: { 27 | STAGE_NAME: props.stageName 28 | } 29 | }); 30 | 31 | new apigateway.LambdaRestApi(this, 'HelloLambdaRestApi', { 32 | handler: func, 33 | endpointExportName: 'HelloLambdaRestApiEmdpoint', 34 | deployOptions: { 35 | stageName: props.stageName 36 | } 37 | }); 38 | 39 | const version = func.currentVersion; 40 | const alias = new lambda.Alias(this, 'LambdaAlias', { 41 | aliasName: props.stageName, 42 | version, 43 | }); 44 | 45 | new codedeploy.LambdaDeploymentGroup(this, 'DeploymentGroup', { 46 | alias, 47 | deploymentConfig: codedeploy.LambdaDeploymentConfig.ALL_AT_ONCE, 48 | }); 49 | 50 | } 51 | } -------------------------------------------------------------------------------- /scripts/automate_cleanup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | # SPDX-License-Identifier: MIT-0 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | # software and associated documentation files (the "Software"), to deal in the Software 7 | # without restriction, including without limitation the rights to use, copy, modify, 8 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | # 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 IMPLIED, 12 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | 19 | # Run this script in the tools account to clean up what was deployed 20 | # Prerequisites: 21 | # - Set up .aws/credentials profiles for pipeline, uat, and prod 22 | # - Set TOOLS_ACCOUNT_ID env variable 23 | # - Deploy the solution using automate_deployment.sh 24 | 25 | # If prerequisite account values aren't set, exit 26 | if [[ -z "${TOOLS_ACCOUNT_ID}" ]]; then 27 | printf "Please set TOOLS_ACCOUNT_ID, UAT_ACCOUNT_ID, and PROD_ACCOUNT_ID" 28 | printf "TOOLS_ACCOUNT_ID =" ${TOOLS_ACCOUNT_ID} 29 | exit 30 | fi 31 | 32 | # Delete CloudFormation deployments in UAT and Prod accts 33 | aws cloudformation delete-stack --stack-name UatApplicationDeploymentStack --profile uat & 34 | aws cloudformation delete-stack --stack-name ProdApplicationDeploymentStack --profile prod & 35 | 36 | # Empty artifact bucket in pipeline acct (prerequisite for destroying the pipeline stack and its S3 bucket) 37 | aws s3 rm s3://artifact-bucket-${TOOLS_ACCOUNT_ID} --recursive --profile pipeline 38 | 39 | # Destroy Cross-Account Pipeline 40 | cdk destroy CrossAccountPipelineStack --profile pipeline 41 | 42 | # Delete Cross-Account roles 43 | aws cloudformation delete-stack --stack-name CodePipelineCrossAccountRole --profile uat & 44 | aws cloudformation delete-stack --stack-name CodePipelineCrossAccountRole --profile prod & 45 | aws cloudformation delete-stack --stack-name CloudFormationDeploymentRole --profile uat & 46 | aws cloudformation delete-stack --stack-name CloudFormationDeploymentRole --profile prod & 47 | 48 | # Delete repository stack 49 | cdk destroy RepositoryStack --profile pipeline -------------------------------------------------------------------------------- /cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/pipeline.ts", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "**/*.d.ts", 11 | "**/*.js", 12 | "tsconfig.json", 13 | "package*.json", 14 | "yarn.lock", 15 | "node_modules", 16 | "test" 17 | ] 18 | }, 19 | "context": { 20 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 21 | "@aws-cdk/core:checkSecretUsage": true, 22 | "@aws-cdk/core:target-partitions": [ 23 | "aws", 24 | "aws-cn" 25 | ], 26 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 27 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 28 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 29 | "@aws-cdk/aws-iam:minimizePolicies": true, 30 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 31 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 32 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 33 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 34 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 35 | "@aws-cdk/core:enablePartitionLiterals": true, 36 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 37 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 38 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 39 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 40 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 41 | "@aws-cdk/aws-route53-patters:useCertificate": true, 42 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false, 43 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, 44 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, 45 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, 46 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, 47 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, 48 | "@aws-cdk/aws-redshift:columnId": true, 49 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, 50 | "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, 51 | "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, 52 | "@aws-cdk/aws-kms:aliasNameRef": true, 53 | "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, 54 | "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, 55 | "@aws-cdk/aws-efs:denyAnonymousAccess": true, 56 | "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, 57 | "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, 58 | "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /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 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /templates/CodePipelineCrossAccountRole.yml: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | # software and associated documentation files (the "Software"), to deal in the Software 6 | # without restriction, including without limitation the rights to use, copy, modify, 7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | # permit persons to whom the Software is furnished to do so. 9 | # 10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | 17 | AWSTemplateFormatVersion: 2010-09-09 18 | 19 | Description: The AWS CloudFormation template that creates the CodePipeline 20 | Cross Account Role that will be assumed by the tooling account to deploy 21 | into target accounts 22 | 23 | Parameters: 24 | ToolsAccountID: 25 | Description: Account ID of the tooling AWS Account that initiates deployments to this account. 26 | Type: String 27 | ConstraintDescription: Must be a valid AWS Account ID without hyphens. 28 | AllowedPattern: '\d{12}' 29 | MinLength: 12 30 | MaxLength: 12 31 | Stage: 32 | Description: Stage of the account we're deploying to 33 | Type: String 34 | AllowedValues: 35 | - Prod 36 | - Uat 37 | KeyArn: 38 | Description: Provide the KMS Key ARN if you've deployed the pipeline stack 39 | Type: String 40 | ConstraintDescription: Must be a valid AWS ARN for a KMS Key in the TOOLS account 41 | Default: '' 42 | 43 | # If the Key Arn is blank, it's our initial deployment, 44 | # so don't deploy the policy which references the bucket and key 45 | Conditions: 46 | DeployPolicy: !Not [!Equals [!Ref KeyArn, '']] 47 | 48 | Resources: 49 | CrossAccountDeploymentRole: 50 | Type: AWS::IAM::Role 51 | Properties: 52 | RoleName: CodePipelineCrossAccountRole 53 | AssumeRolePolicyDocument: 54 | Version: 2012-10-17 55 | Statement: 56 | - 57 | Effect: Allow 58 | Principal: 59 | AWS: 60 | - !Sub arn:${AWS::Partition}:iam::${ToolsAccountID}:root 61 | Action: 62 | - sts:AssumeRole 63 | 64 | CrossAccountDeploymentPolicy: 65 | Condition: DeployPolicy 66 | Type: AWS::IAM::ManagedPolicy 67 | Properties: 68 | Description: Allows pipeline in master account to deploy API Gateway, Lambda 69 | ManagedPolicyName: CodePipelineCrossAccountPolicy 70 | Roles: 71 | - !Ref CrossAccountDeploymentRole 72 | PolicyDocument: 73 | Version: 2012-10-17 74 | Statement: 75 | - Action: 76 | - cloudformation:CreateStack 77 | - cloudformation:GetTemplate 78 | - cloudformation:ValidateTemplate 79 | - cloudformation:DeleteStack 80 | - cloudformation:UpdateStack 81 | - cloudformation:DescribeStack* 82 | Resource: !Sub arn:${AWS::Partition}:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/${Stage}ApplicationDeploymentStack/* 83 | Effect: Allow 84 | - Action: 85 | - iam:PassRole 86 | Resource: !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:role/CloudFormationDeploymentRole 87 | Effect: Allow 88 | - Action: 89 | - s3:Get* 90 | - s3:Put* 91 | - s3:ListBucket 92 | Resource: 93 | - !Sub arn:${AWS::Partition}:s3:::artifact-bucket-${ToolsAccountID} 94 | - !Sub arn:${AWS::Partition}:s3:::artifact-bucket-${ToolsAccountID}/* 95 | Effect: Allow 96 | - Action: 97 | - kms:DescribeKey 98 | - kms:GenerateDataKey* 99 | - kms:Encrypt 100 | - kms:ReEncrypt* 101 | - kms:Decrypt 102 | Resource: 103 | - !Sub ${KeyArn} 104 | Effect: Allow 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # re:Invent 2021 DOP402 - Automating Cross-Account CI/CD Pipelines 2 | 3 | When building a deployment strategy for your applications, using a multi-account approach is a recommended best practice. This limits the impact for changes made and results in better modularity, security, and governance. In this session, we will dive deep into an example multi-account deployment using infrastructure-as-code (IaC) services such as the AWS CDK, AWS CodePipeline, and AWS CloudFormation. We will also explore a real-world customer use case that is deploying at scale across hundreds of AWS accounts. 4 | 5 | View the recording of the session including the live demo on YouTube: https://m.youtube.com/watch?v=AF-pSRSGNks 6 | 7 | **NOTE: This repository was upgraded to CDK v2 August 22, 2023.** 8 | 9 | ![Architecture](images/Cross-Account-Deployment-Arch.png) 10 | 11 | The solution consists of: 12 | - 2 bash scripts to automate deployment and cleanup, respectively 13 | - 2 CloudFormation templates for the prerequisite roles that need to be deployed to the target accounts 14 | - 3 CDK stacks to define: 15 | - Repository (kept separate for the pipeline so one could re-deploy or start over with the Pipeline without losing the code) 16 | - Application (API Gateway backed by a simple "Hello Lambda" function) 17 | - Pipeline that deploys the Application to the target accounts 18 | 19 | The pipeline uses the aforementioned AWS CodeCommit repository as its source, AWS CodeBuild to package the source code for a sample Lambda and to build a CloudFormation template for our application, and a CloudFormation Create/Update Stack Action to deploy each stage of the sample application. You could update the Application to be anything you like - make sure you update the CloudFormation cross-account role actions and resources. It doesn't matter what application is being deployed, the goal is to understand the dependencies on the roles, resources, and pipelines involved in cross-account deployments. 20 | 21 | The pipeline is provisioned in a tooling account, and deploys the sample application into UAT and Production accounts. This means you need a total of 3 accounts to successfully run this solution. 22 | 23 | We'll use AWS CloudFormation templates to deploy the prerequisite cross-account roles for the tooling account to use, and the AWS CDK to deploy the pipeline. 24 | 25 | ![UAT Deployment Workflow](images/UAT-Deployment-Workflow.png) 26 | 27 | ![Production Deployment Workflow](images/Production-Deployment-Workflow.png) 28 | 29 | ## Deploy 30 | 31 | Prerequisites: 32 | - Install and configure git 33 | - Clone repo locally using git 34 | - Set up .aws/credentials profiles for pipeline, uat, and prod so you can execute CLI and CDK commands against them 35 | - Set TOOLS_ACCOUNT_ID, UAT_ACCOUNT_ID, and PROD_ACCOUNT_ID env variables so they can be used by the scripts 36 | - Install npm and the AWS CDK 37 | - Bootstrap the CDK against the Tools account (you do not need to bootstrap the CDK against the target accounts since the pipeline uses the CloudFormation templates ' 38 | generated by the CDK - not the CDK itself - to deploy the application) 39 | 40 | Run the following command in the root directory of the project: 41 | 42 | ```bash 43 | ./scripts/automate_deployment.sh 44 | ``` 45 | 46 | This script will: 47 | - Verify that you have set the prerequisite environment variables 48 | - Deploy the CDK Repository Stack 49 | - Create the UAT and Production cross-account roles using CloudFormation templates 50 | - Deploy the CDK Pipeline Stack (output: KMS key ARN) 51 | - Update the UAT and Production roles with policies referencing the S3 bucket and KMS key created in the previous step 52 | - Push the initial code base to the new repository, which will start the pipeline execution to ensure a successful application deployment to both UAT and Production. 53 | 54 | Note that the roles are deployed twice. This is because there is a circular dependency on the roles in the target accounts and the pipeline artifact resources provisioned by the CDK. The pipeline needs to reference and resolve the ARNs of the roles it needs to assume to deploy the application to the target accounts, so the roles need to be deployed before the 55 | pipeline is provisioned. However, the policies attached to the roles need to include the S3 bucket and KMS key to read and write encrypted artifacts from and to the pipeline artifact bucket. But the S3 bucket and KMS key don't exist until the CDK application deploys. So, we deploy the roles twice, once without a policy so their ARNs resolve, and a second time to attach policies to the existing roles that reference the CDK resources. The mechanism to trigger whether the policy should be deployed is a CloudFormation Condition on the KMS `KeyArn` parameter passed into the CloudFormation deploy action. 56 | 57 | # Clean Up 58 | 59 | Run the following command in the root directory of the project: 60 | 61 | ```bash 62 | ./scripts/automate_cleanup.sh 63 | ``` 64 | 65 | This script will: 66 | - Verify that you have set the prerequisite environment variables 67 | - Destroy the UAT and Production application deployments 68 | - Empty the Pipeline Stack's artifact S3 bucket 69 | - Destroy the Pipeline Stack 70 | - Destroy the UAT and Production cross-account roles 71 | 72 | # Troubleshooting 73 | If your account permissions aren't set up correctly, or if you stop a deployment or cleanup mid-way through, you may have to manually clean up deployed resources. Navigate to the CloudFormation console, select the stack that needs to be cleaned up, and click the Delete button to delete the stack manually and start over. 74 | 75 | ## License 76 | 77 | This library is licensed under the MIT-0 License. See the LICENSE file. 78 | -------------------------------------------------------------------------------- /scripts/automate_deployment.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | # SPDX-License-Identifier: MIT-0 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | # software and associated documentation files (the "Software"), to deal in the Software 7 | # without restriction, including without limitation the rights to use, copy, modify, 8 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | # 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 IMPLIED, 12 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | # Prerequisites: 19 | # - Set up .aws/credentials profiles for pipeline, uat, and prod 20 | # - Set TOOLS_ACCOUNT_ID, UAT_ACCOUNT_ID, and PROD_ACCOUNT_ID env variables 21 | # - Clone repo with CloudFormation templates and CDK code locally 22 | # - Initialize and bootstrap CDK in the Tools account 23 | # - Install and configure git 24 | 25 | # If prerequisite account values aren't set, exit 26 | if [[ -z "${TOOLS_ACCOUNT_ID}" || -z "${UAT_ACCOUNT_ID}" || -z "${PROD_ACCOUNT_ID}" ]]; then 27 | printf "Please set TOOLS_ACCOUNT_ID, UAT_ACCOUNT_ID, and PROD_ACCOUNT_ID" 28 | printf "TOOLS_ACCOUNT_ID =" ${TOOLS_ACCOUNT_ID} 29 | printf "UAT_ACCOUNT_ID =" ${UAT_ACCOUNT_ID} 30 | printf "PROD_ACCOUNT_ID =" ${PROD_ACCOUNT_ID} 31 | exit 32 | fi 33 | 34 | # Deploy roles without policies so the ARNs exist when the CDK Stack is deployed in parallel 35 | printf "\nDeploying roles to UAT and Prod\n" 36 | aws cloudformation deploy --template-file templates/CodePipelineCrossAccountRole.yml \ 37 | --stack-name CodePipelineCrossAccountRole \ 38 | --capabilities CAPABILITY_NAMED_IAM \ 39 | --profile uat \ 40 | --parameter-overrides ToolsAccountID=${TOOLS_ACCOUNT_ID} Stage=Uat & 41 | 42 | aws cloudformation deploy --template-file templates/CloudFormationDeploymentRole.yml \ 43 | --stack-name CloudFormationDeploymentRole \ 44 | --capabilities CAPABILITY_NAMED_IAM \ 45 | --profile uat \ 46 | --parameter-overrides ToolsAccountID=${TOOLS_ACCOUNT_ID} Stage=Uat & 47 | 48 | aws cloudformation deploy --template-file templates/CodePipelineCrossAccountRole.yml \ 49 | --stack-name CodePipelineCrossAccountRole \ 50 | --capabilities CAPABILITY_NAMED_IAM \ 51 | --profile prod --parameter-overrides ToolsAccountID=${TOOLS_ACCOUNT_ID} Stage=Prod & 52 | 53 | aws cloudformation deploy --template-file templates/CloudFormationDeploymentRole.yml \ 54 | --stack-name CloudFormationDeploymentRole \ 55 | --capabilities CAPABILITY_NAMED_IAM \ 56 | --profile prod \ 57 | --parameter-overrides ToolsAccountID=${TOOLS_ACCOUNT_ID} Stage=Prod 58 | 59 | 60 | # Deploy Repository CDK Stack 61 | printf "\nDeploying Repository Stack\n" 62 | npm install 63 | npm audit fix 64 | npm run build 65 | cdk synth 66 | cdk deploy RepositoryStack —profile pipeline 67 | 68 | # Deploy Pipeline CDK stack, write output to a file to gather key arn 69 | printf "\nDeploying Cross-Account Deployment Pipeline Stack\n" 70 | 71 | CDK_OUTPUT_FILE='.cdk_output' 72 | rm -rf ${CDK_OUTPUT_FILE} .cfn_outputs 73 | npx cdk deploy CrossAccountPipelineStack \ 74 | --context prod-account=${PROD_ACCOUNT_ID} \ 75 | --context uat-account=${UAT_ACCOUNT_ID} \ 76 | --profile pipeline \ 77 | --require-approval never \ 78 | 2>&1 | tee -a ${CDK_OUTPUT_FILE} 79 | sed -n -e '/Outputs:/,/^$/ p' ${CDK_OUTPUT_FILE} > .cfn_outputs 80 | KEY_ARN=$(awk -F " " '/KeyArn/ { print $3 }' .cfn_outputs) 81 | 82 | # Check that KEY_ARN is set after the CDK deployment 83 | if [[ -z "${KEY_ARN}" ]]; then 84 | printf "\nSomething went wrong - we didn't get a Key ARN as an output from the CDK Pipeline deployment" 85 | exit 86 | fi 87 | 88 | # Update the CloudFormation roles with the Key ARNy in parallel 89 | printf "\nUpdating roles with policies in UAT and Prod\n" 90 | aws cloudformation deploy --template-file templates/CodePipelineCrossAccountRole.yml \ 91 | --stack-name CodePipelineCrossAccountRole \ 92 | --capabilities CAPABILITY_NAMED_IAM \ 93 | --profile uat \ 94 | --parameter-overrides ToolsAccountID=${TOOLS_ACCOUNT_ID} Stage=Uat KeyArn=${KEY_ARN} & 95 | 96 | aws cloudformation deploy --template-file templates/CloudFormationDeploymentRole.yml \ 97 | --stack-name CloudFormationDeploymentRole \ 98 | --capabilities CAPABILITY_NAMED_IAM \ 99 | --profile uat \ 100 | --parameter-overrides ToolsAccountID=${TOOLS_ACCOUNT_ID} Stage=Uat KeyArn=${KEY_ARN} & 101 | 102 | aws cloudformation deploy --template-file templates/CloudFormationDeploymentRole.yml \ 103 | --stack-name CloudFormationDeploymentRole \ 104 | --capabilities CAPABILITY_NAMED_IAM \ 105 | --profile prod \ 106 | --parameter-overrides ToolsAccountID=${TOOLS_ACCOUNT_ID} Stage=Prod KeyArn=${KEY_ARN} & 107 | 108 | aws cloudformation deploy --template-file templates/CodePipelineCrossAccountRole.yml \ 109 | --stack-name CodePipelineCrossAccountRole \ 110 | --capabilities CAPABILITY_NAMED_IAM \ 111 | --profile prod \ 112 | --parameter-overrides ToolsAccountID=${TOOLS_ACCOUNT_ID} Stage=Prod KeyArn=${KEY_ARN} 113 | 114 | # Commit initial code to new repo (which will trigger a fresh pipeline execution) 115 | printf "\nCommitting code to repository\n" 116 | git init && git branch -m main && git add . && git commit -m "Initial commit" && git remote rm origin 117 | git remote add origin https://git-codecommit.us-west-2.amazonaws.com/v1/repos/repo-${TOOLS_ACCOUNT_ID} 118 | git config main.remove origin && git config main.merge refs/heads/main && git push --set-upstream origin main 119 | 120 | # Get deployed API Gateway endpoints 121 | printf "\nUse the following commands to get the Endpoints for deployed environemnts: " 122 | printf "\n aws cloudformation describe-stacks --stack-name UatApplicationDeploymentStack \ 123 | --profile uat | grep OutputValue" 124 | printf "\n aws cloudformation describe-stacks --stack-name ProdApplicationDeploymentStack \ 125 | --profile prod | grep OutputValue" 126 | 127 | # Clean up temporary files 128 | rm ${CDK_OUTPUT_FILE} .cfn_outputs 129 | -------------------------------------------------------------------------------- /templates/CloudFormationDeploymentRole.yml: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | # software and associated documentation files (the "Software"), to deal in the Software 6 | # without restriction, including without limitation the rights to use, copy, modify, 7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | # permit persons to whom the Software is furnished to do so. 9 | # 10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | 17 | AWSTemplateFormatVersion: 2010-09-09 18 | 19 | Description: | 20 | The AWS CloudFormation template for creating Cloudformation execution role to be used by 21 | Cloudformation Service to create resources in the application stack. 22 | This role is passed to the CloudFormation service by the CodePipeline Cross Account Role 23 | 24 | Parameters: 25 | ToolsAccountID: 26 | Description: Account ID of the TOOLS AWS Account that initiates code deployment to this account. 27 | Type: String 28 | ConstraintDescription: Must be a valid AWS Account ID without hyphens. 29 | AllowedPattern: '\d{12}' 30 | MinLength: 12 31 | MaxLength: 12 32 | Stage: 33 | Description: Stage of the account we're deploying to 34 | Type: String 35 | AllowedValues: 36 | - Prod 37 | - Uat 38 | KeyArn: 39 | Description: Provide the KMS Key ARN if you've deployed the pipeline stack 40 | Type: String 41 | ConstraintDescription: Must be a valid AWS ARN for a KMS Key in the TOOLS account 42 | Default: '' 43 | 44 | # If the Key Arn is blank, it's our initial deployment, 45 | # so don't deploy the policy which references the bucket and key 46 | Conditions: 47 | DeployPolicy: !Not [!Equals [!Ref KeyArn, '']] 48 | 49 | Resources: 50 | CloudFormationDeploymentRole: 51 | Type: AWS::IAM::Role 52 | Properties: 53 | RoleName: CloudFormationDeploymentRole 54 | AssumeRolePolicyDocument: 55 | Version: 2012-10-17 56 | Statement: 57 | - 58 | Effect: Allow 59 | Principal: 60 | Service: 61 | - cloudformation.amazonaws.com 62 | Action: 63 | - sts:AssumeRole 64 | 65 | CloudFormationDeploymentPolicy: 66 | Condition: DeployPolicy 67 | Type: AWS::IAM::ManagedPolicy 68 | Properties: 69 | Description: Allows pipeline in TOOLS account to deploy API Gateway, Lambda 70 | ManagedPolicyName: CloudFormationDeploymentPolicy 71 | Roles: 72 | - !Ref CloudFormationDeploymentRole 73 | PolicyDocument: 74 | Version: 2012-10-17 75 | Statement: 76 | - Action: iam:PassRole 77 | Resource: !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:role/CodePipelineCrossAccountRole 78 | Effect: Allow 79 | - Action: 80 | - iam:Get* 81 | - iam:*Role* 82 | - iam:AttachRolePolicy 83 | - iam:DetachRolePolicy 84 | - iam:CreateServiceLinkedRole 85 | - iam:DeleteServiceLinkedRole 86 | - iam:CreatePolicy 87 | - iam:DeletePolicy 88 | - iam:*PolicyVersion* 89 | Resource: !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:role/* 90 | Effect: Allow 91 | - Action: 92 | - lambda:Create* 93 | - lambda:Delete* 94 | - lambda:Get* 95 | - lambda:Update* 96 | - lambda:List* 97 | - lambda:AddPermission 98 | - lambda:PublishVersion 99 | - lambda:RemovePermission 100 | Resource: !Sub arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:HelloLambda* 101 | Effect: Allow 102 | - Action: 103 | - apigateway:GET 104 | - apigateway:PATCH 105 | - apigateway:POST 106 | - apigateway:PUT 107 | - apigateway:DELETE 108 | - apigateway:GetResources 109 | Resource: 110 | - !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:* 111 | - !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}::/restapis* 112 | - !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}::/account 113 | Effect: Allow 114 | - Action: 115 | - codedeploy:Get* 116 | - codedeploy:List* 117 | - codedeploy:*Deployment* 118 | - codedeploy:CreateApplication 119 | - codedeploy:CreateDeploymentGroup 120 | - codedeploy:DeleteApplication 121 | Resource: 122 | - !Sub arn:${AWS::Partition}:codedeploy:${AWS::Region}:${AWS::AccountId}:application:${Stage}ApplicationDeploymentStack* 123 | - !Sub arn:${AWS::Partition}:codedeploy:${AWS::Region}:${AWS::AccountId}:deploymentgroup:${Stage}ApplicationDeploymentStack* 124 | Effect: Allow 125 | - Action: 126 | - s3:GetObject* 127 | - s3:GetBucket* 128 | - s3:List* 129 | Resource: 130 | - !Sub arn:${AWS::Partition}:s3:::artifact-bucket-${ToolsAccountID} 131 | - !Sub arn:${AWS::Partition}:s3:::artifact-bucket-${ToolsAccountID}/* 132 | Effect: Allow 133 | - Action: 134 | - kms:Decrypt 135 | - kms:DescribeKey 136 | Resource: !Sub ${KeyArn} 137 | Effect: Allow 138 | - Action: 139 | - cloudformation:CreateStack 140 | - cloudformation:DescribeStack* 141 | - cloudformation:GetStackPolicy 142 | - cloudformation:GetTemplate* 143 | - cloudformation:SetStackPolicy 144 | - cloudformation:UpdateStack 145 | - cloudformation:ValidateTemplate 146 | Resource: !Sub arn:${AWS::Partition}:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/${Stage}ApplicationDeploymentStack/* 147 | Effect: Allow 148 | 149 | -------------------------------------------------------------------------------- /lib/pipeline-stack.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | import * as codebuild from 'aws-cdk-lib/aws-codebuild'; 5 | import * as codecommit from 'aws-cdk-lib/aws-codecommit'; 6 | import * as codepipeline from 'aws-cdk-lib/aws-codepipeline'; 7 | import * as codepipeline_actions from 'aws-cdk-lib/aws-codepipeline-actions'; 8 | import * as s3 from 'aws-cdk-lib/aws-s3'; 9 | import * as iam from 'aws-cdk-lib/aws-iam'; 10 | import * as kms from 'aws-cdk-lib/aws-kms'; 11 | import { App, Stack, StackProps, RemovalPolicy, CfnOutput, CfnCapabilities } from 'aws-cdk-lib'; 12 | 13 | import { ApplicationStack } from '../lib/application-stack'; 14 | 15 | export interface PipelineStackProps extends StackProps { 16 | readonly uatApplicationStack: ApplicationStack; 17 | readonly uatAccountId: string; 18 | readonly prodApplicationStack: ApplicationStack; 19 | readonly prodAccountId: string; 20 | } 21 | 22 | export class PipelineStack extends Stack { 23 | 24 | constructor(app: App, id: string, props: PipelineStackProps) { 25 | 26 | super(app, id, props); 27 | 28 | const repository = codecommit.Repository.fromRepositoryName(this, 29 | 'CodeCommitRepo', 30 | `repo-${this.account}`); 31 | 32 | // Resolve ARNs of cross-account roles for the UAT account 33 | const uatCloudFormationRole = iam.Role.fromRoleArn(this, 34 | 'UatDeploymentRole', 35 | `arn:aws:iam::${props.uatAccountId}:role/CloudFormationDeploymentRole`, { 36 | mutable: false 37 | }); 38 | const uatCodePipelineRole = iam.Role.fromRoleArn(this, 39 | 'UatCrossAccountRole', 40 | `arn:aws:iam::${props.uatAccountId}:role/CodePipelineCrossAccountRole`, { 41 | mutable: false 42 | }); 43 | 44 | // Resolve ARNS of cross-account roles for the Prod account 45 | const prodCloudFormationRole = iam.Role.fromRoleArn(this, 46 | 'ProdDeploymentRole', 47 | `arn:aws:iam::${props.prodAccountId}:role/CloudFormationDeploymentRole`, { 48 | mutable: false 49 | }); 50 | const prodCodeDeployRole = iam.Role.fromRoleArn(this, 51 | 'ProdCrossAccountRole', 52 | `arn:aws:iam::${props.prodAccountId}:role/CodePipelineCrossAccountRole`, { 53 | mutable: false 54 | }); 55 | 56 | // Resolve root Principal ARNs for both deployment accounts 57 | const uatAccountRootPrincipal = new iam.AccountPrincipal(props.uatAccountId); 58 | const prodAccountRootPrincipal = new iam.AccountPrincipal(props.prodAccountId); 59 | 60 | // Create KMS key and update policy with cross-account access 61 | const key = new kms.Key(this, 'ArtifactKey', { 62 | alias: 'key/pipeline-artifact-key', 63 | }); 64 | key.grantDecrypt(uatAccountRootPrincipal); 65 | key.grantDecrypt(uatCodePipelineRole); 66 | key.grantDecrypt(prodAccountRootPrincipal); 67 | key.grantDecrypt(prodCodeDeployRole); 68 | 69 | // Create S3 bucket with target account cross-account access 70 | const artifactBucket = new s3.Bucket(this, 'ArtifactBucket', { 71 | bucketName: `artifact-bucket-${this.account}`, 72 | removalPolicy: RemovalPolicy.DESTROY, 73 | encryption: s3.BucketEncryption.KMS, 74 | encryptionKey: key 75 | }); 76 | artifactBucket.grantPut(uatAccountRootPrincipal); 77 | artifactBucket.grantRead(uatAccountRootPrincipal); 78 | artifactBucket.grantPut(prodAccountRootPrincipal); 79 | artifactBucket.grantRead(prodAccountRootPrincipal); 80 | 81 | // CDK build definition 82 | const cdkBuild = new codebuild.PipelineProject(this, 'CdkBuild', { 83 | buildSpec: codebuild.BuildSpec.fromObject({ 84 | version: '0.2', 85 | phases: { 86 | install: { 87 | commands: [ 88 | 'npm install' 89 | ], 90 | }, 91 | build: { 92 | commands: [ 93 | 'npm run build', 94 | 'npm run cdk synth -- -o dist', 95 | ], 96 | }, 97 | }, 98 | artifacts: { 99 | 'base-directory': 'dist', 100 | files: [ 101 | '*ApplicationStack.template.json', 102 | ], 103 | }, 104 | }), 105 | environment: { 106 | buildImage: codebuild.LinuxBuildImage.AMAZON_LINUX_2_3, 107 | }, 108 | // use the encryption key for build artifacts 109 | encryptionKey: key 110 | }); 111 | 112 | // Lambda build definition 113 | const lambdaBuild = new codebuild.PipelineProject(this, 'LambdaBuild', { 114 | buildSpec: codebuild.BuildSpec.fromObject({ 115 | version: '0.2', 116 | phases: { 117 | install: { 118 | commands: [ 119 | 'cd app', 120 | 'npm install', 121 | ], 122 | }, 123 | build: { 124 | commands: 'npm run build', 125 | }, 126 | }, 127 | artifacts: { 128 | 'base-directory': 'app', 129 | files: [ 130 | 'index.js', 131 | 'node_modules/**/*', 132 | ], 133 | }, 134 | }), 135 | environment: { 136 | buildImage: codebuild.LinuxBuildImage.AMAZON_LINUX_2_3, 137 | }, 138 | // use the encryption key for build artifacts 139 | encryptionKey: key 140 | }); 141 | 142 | // Define pipeline stage output artifacts 143 | const sourceOutput = new codepipeline.Artifact(); 144 | const cdkBuildOutput = new codepipeline.Artifact('CdkBuildOutput'); 145 | const lambdaBuildOutput = new codepipeline.Artifact('LambdaBuildOutput'); 146 | 147 | // Pipeline definition 148 | const pipeline = new codepipeline.Pipeline(this, 'Pipeline', { 149 | pipelineName: 'CrossAccountPipeline', 150 | artifactBucket: artifactBucket, 151 | stages: [ 152 | { 153 | stageName: 'Source', 154 | actions: [ 155 | new codepipeline_actions.CodeCommitSourceAction({ 156 | actionName: 'CodeCommit_Source', 157 | repository: repository, 158 | output: sourceOutput, 159 | branch: 'main' 160 | }), 161 | ], 162 | }, 163 | { 164 | stageName: 'Build', 165 | actions: [ 166 | new codepipeline_actions.CodeBuildAction({ 167 | actionName: 'Application_Build', 168 | project: lambdaBuild, 169 | input: sourceOutput, 170 | outputs: [lambdaBuildOutput], 171 | }), 172 | new codepipeline_actions.CodeBuildAction({ 173 | actionName: 'CDK_Synth', 174 | project: cdkBuild, 175 | input: sourceOutput, 176 | outputs: [cdkBuildOutput], 177 | }), 178 | ], 179 | }, 180 | { 181 | stageName: 'Deploy_Uat', 182 | actions: [ 183 | new codepipeline_actions.CloudFormationCreateUpdateStackAction({ 184 | actionName: 'Deploy', 185 | templatePath: cdkBuildOutput.atPath('UatApplicationStack.template.json'), 186 | stackName: 'UatApplicationDeploymentStack', 187 | adminPermissions: false, 188 | parameterOverrides: { 189 | ...props.uatApplicationStack.lambdaCode.assign( 190 | lambdaBuildOutput.s3Location), 191 | }, 192 | extraInputs: [lambdaBuildOutput], 193 | cfnCapabilities: [CfnCapabilities.ANONYMOUS_IAM], 194 | role: uatCodePipelineRole, 195 | deploymentRole: uatCloudFormationRole, 196 | }) 197 | ], 198 | }, 199 | { 200 | stageName: 'Deploy_Prod', 201 | actions: [ 202 | new codepipeline_actions.CloudFormationCreateUpdateStackAction({ 203 | actionName: 'Deploy', 204 | templatePath: cdkBuildOutput.atPath('ProdApplicationStack.template.json'), 205 | stackName: 'ProdApplicationDeploymentStack', 206 | adminPermissions: false, 207 | parameterOverrides: { 208 | ...props.prodApplicationStack.lambdaCode.assign( 209 | lambdaBuildOutput.s3Location), 210 | }, 211 | extraInputs: [lambdaBuildOutput], 212 | cfnCapabilities: [CfnCapabilities.ANONYMOUS_IAM], 213 | role: prodCodeDeployRole, 214 | deploymentRole: prodCloudFormationRole, 215 | }), 216 | ], 217 | }, 218 | ], 219 | }); 220 | 221 | // Add the target accounts to the pipeline policy 222 | pipeline.addToRolePolicy(new iam.PolicyStatement({ 223 | actions: ['sts:AssumeRole'], 224 | resources: [ 225 | `arn:aws:iam::${props.uatAccountId}:role/*`, 226 | `arn:aws:iam::${props.prodAccountId}:role/*` 227 | ] 228 | })); 229 | 230 | // Publish the KMS Key ARN as an output 231 | new CfnOutput(this, 'ArtifactBucketEncryptionKeyArn', { 232 | value: key.keyArn, 233 | exportName: 'ArtifactBucketEncryptionKey' 234 | }); 235 | 236 | } 237 | } 238 | --------------------------------------------------------------------------------