Cross Account CI/CD Pipeline#

Sometimes we need to run the Pipeline in an account, but deploy product into another account for

  • Further sepratation between development and production envionment
  • Protect production environment
  • For customer in another account
  • Github

This note follows the reference workshop here

Todo

  • Python CDK version
  • CloudFormation version
  • Roles and policies created with CloudFormation

Architecture#

cross_account_ci_cd_pipeline drawio (1)

Setup CDK project#

create an empty directory

mkdir cdk-test-cicd-pipeline

init cdk project

cdk init --language=typescript

Create a CodeCommit by a repository stack#

create lib/repository-stack.ts

import { aws_codecommit } from 'aws-cdk-lib'
import { App, Stack, StackProps } from 'aws-cdk-lib'
export class RepositoryStack extends Stack {
constructor(app: App, id: string, props?: StackProps) {
super(app, id, props)
new aws_codecommit.Repository(this, 'CodeCommitRepo', {
repositoryName: `repo-${this.account}`
})
}
}

Create a lambda stack for testing purpose#

create lib/lambda-stack.ts

import { Stack, StackProps } from 'aws-cdk-lib'
import { Construct } from 'constructs'
import { aws_lambda } from 'aws-cdk-lib'
const path = require('path')
export class LambdaStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props)
new aws_lambda.Function(this, 'CdkTsLambdaFunctionTest', {
runtime: aws_lambda.Runtime.PYTHON_3_8,
handler: 'handler.handler',
code: aws_lambda.Code.fromAsset(path.join(__dirname, 'lambda'))
})
}
}

Build and check CDK generated stacks#

import 'source-map-support/register'
import * as cdk from 'aws-cdk-lib'
import { CdkTsCicdPipelineStack } from '../lib/cdk-ts-cicd-pipeline-stack'
import { RepositoryStack } from '../lib/repository-stack'
import { LambdaStack } from '../lib/lambda-stack'
const app = new cdk.App()
new CdkTsCicdPipelineStack(app, 'CdkTsCicdPipelineStack', {})
new LambdaStack(app, 'CdkTsLambdaStack', {})
new RepositoryStack(app, 'CdkTsRepositoryStack', {})

build and check stacks

npm install
npm run build
cdk ls

should see multiple stack

CdkTsCicdPipelineStack
CdkTsLambdaStack
CdkTsRepositoryStack

Deploy a stack and test output

cdk deploy CdkTsLambdaStack

Application Stack#

This is a smple lambda function with code from local assset folder. The important thing is that

  • CodeBuild will output DevApplicationStack.template.json in the artifact bucket
  • CodeBuild will output ProdApplicationStack.template.json in the artifact bucket
  • CloudFormation running in the production account need to access the artifact bucket in the dev environment that is why we need roles below. This is the application stack which is a simple lambda api.
import { aws_lambda } from 'aws-cdk-lib'
import { aws_apigateway } from 'aws-cdk-lib'
import { App, Stack, StackProps } from 'aws-cdk-lib'
import * as path from 'path'
export interface ApplicationStackProps extends StackProps {
readonly stageName: string
}
export class ApplicationStack extends Stack {
// lambad code from
public readonly lambdaCode: aws_lambda.CfnParametersCode
// constructor
constructor(app: App, id: string, props: ApplicationStackProps) {
super(app, id, props)
// lambda code
this.lambdaCode = aws_lambda.Code.fromCfnParameters()
// create a lambda function
const lambda_function = new aws_lambda.Function(
this,
`CdkTsLambdaApplicationFunction-${props.stageName}`,
{
runtime: aws_lambda.Runtime.NODEJS_12_X,
handler: 'index.handler',
code: this.lambdaCode,
environment: {
STAGE_NAME: props.stageName
}
}
)
// create an api gateway
new aws_apigateway.LambdaRestApi(
this,
`CdkTsApiGatewayRestApi-${props.stageName}`,
{
handler: lambda_function,
endpointExportName: `CdkTsLambdaRestApiEndpoint-${props.stageName}`,
deployOptions: {
stageName: props.stageName
}
}
)
// lambda version alias function
// codedeploy
}
}

CodePipeline Stack#

  • The CodePipeline lives in the dev account
  • The CodePipeline need to deploy things into the production account
  • Need to assume role which setup from the production account

This role enables creating resources inside the productiona account

const prodDeploymentRole = aws_iam.Role.fromRoleArn(
this,
'ProdDeploymentRole',
`arn:aws:iam::product_account_id:role/CloudFormationDeploymentRole`,
{
mutable: false
}
)

and this role enables accessing the S3 artifact from the production account

const prodCrossAccountRole = aws_iam.Role.fromRoleArn(
this,
'ProdCrossAccountRole',
`arn:aws:iam::product_account_id:role/CdkCodePipelineCrossAcccountRole`,
{
mutable: false
}
)

CodeBuild#

  • Build the lambda code
  • Build the application stack
stageName: 'Build',
actions: [
new aws_codepipeline_actions.CodeBuildAction({
actionName: 'Application_Build',
project: lambdaBuild,
input: sourceOutput,
outputs: [lambdaBuildOutput],
}),
new aws_codepipeline_actions.CodeBuildAction({
actionName: 'CDK_Synth',
project: cdkBuild,
input: sourceOutput,
outputs: [cdkBuildOutput],
}),
],

CodeDeploy#

  • Deploy the application stack into the dev environment
stageName: 'Deploy_Dev',
actions: [
new aws_codepipeline_actions.CloudFormationCreateUpdateStackAction({
actionName: 'Deploy',
templatePath: cdkBuildOutput.atPath('DevApplicationStack.template.json'),
stackName: 'DevApplicationDeploymentStack',
adminPermissions: true,
parameterOverrides: {
...props.devApplicationStack.lambdaCode.assign(lambdaBuildOutput.s3Location),
},
extraInputs: [lambdaBuildOutput]
})
],
  • Deploy the application stack into the production environment
stageName: 'Deploy_Prod',
actions: [
new aws_codepipeline_actions.CloudFormationCreateUpdateStackAction({
actionName: 'Deploy',
templatePath: cdkBuildOutput.atPath('ProdApplicationStack.template.json'),
stackName: 'ProdApplicationDeploymentStack',
adminPermissions: true,
parameterOverrides: {
...props.prodApplicationStack.lambdaCode.assign(lambdaBuildOutput.s3Location),
},
deploymentRole: prodDeploymentRole,
cfnCapabilities: [CfnCapabilities.ANONYMOUS_IAM],
role: prodCrossAccountRole,
extraInputs: [lambdaBuildOutput],
}),
],

Note to add the inline policy to the assume role

pipeline.addToRolePolicy(
new aws_iam.PolicyStatement({
actions: ['sts:AssumeRole'],
resources: ['arn:aws:iam::product_account_id:role/*']
})
)

Note the KMS key which encrypt the S3 artifact bucket

new CfnOutput(this, 'ArtifactBucketEncryptionKeyArn', {
value: key.keyArn,
exportName: 'ArtifactBucketEncryptionKey'
})

Policy CodePipelineCrossAccountRole#

This allow the ProductionAccount when deploying CloudFormation can access the S3 ArtifactBucket where code for the Lambda function is stored.

{
"Version": "2012-10-17",
"Statement": [
{
"Action": ["cloudformation:*", "iam:PassRole"],
"Resource": "*",
"Effect": "Allow"
},
{
"Action": ["s3:Get*", "s3:Put*", "s3:ListBucket"],
"Resource": [
"arn:aws:s3:::artifact-bucket-{DEV_ACCOUNT_ID}",
"arn:aws:s3:::artifact-bucket-{DEV_ACCOUNT_ID}/*"
],
"Effect": "Allow"
},
{
"Action": [
"kms:DescribeKey",
"kms:GenerateDataKey*",
"kms:Encrypt",
"kms:ReEncrypt*",
"kms:Decrypt"
],
"Resource": "{KEY_ARN}",
"Effect": "Allow"
}
]
}

Policy CloudFormationDeploymentRole#

This allow the CloudFormation in the ProductionAccount can create resources when deploying stacks developed from the develop account.

{
"Version": "2012-10-17",
"Statement": [
{
"Action": "iam:PassRole",
"Resource": "arn:aws:iam::{PROD_ACCOUNT_ID}:role/*",
"Effect": "Allow"
},
{
"Action": ["iam:GetRole", "iam:CreateRole", "iam:AttachRolePolicy"],
"Resource": "arn:aws:iam::{PROD_ACCOUNT_ID}:role/*",
"Effect": "Allow"
},
{
"Action": "lambda:*",
"Resource": "*",
"Effect": "Allow"
},
{
"Action": "apigateway:*",
"Resource": "*",
"Effect": "Allow"
},
{
"Action": "codedeploy:*",
"Resource": "*",
"Effect": "Allow"
},
{
"Action": ["s3:GetObject*", "s3:GetBucket*", "s3:List*"],
"Resource": [
"arn:aws:s3:::artifact-bucket-{DEV_ACCOUNT_ID}",
"arn:aws:s3:::artifact-bucket-{DEV_ACCOUNT_ID}/*"
],
"Effect": "Allow"
},
{
"Action": ["kms:Decrypt", "kms:DescribeKey"],
"Resource": "{KEY_ARN}",
"Effect": "Allow"
},
{
"Action": [
"cloudformation:CreateStack",
"cloudformation:DescribeStack*",
"cloudformation:GetStackPolicy",
"cloudformation:GetTemplate*",
"cloudformation:SetStackPolicy",
"cloudformation:UpdateStack",
"cloudformation:ValidateTemplate"
],
"Resource": "arn:aws:cloudformation:us-east-2:{PROD_ACCOUNT_ID}:stack/ProdApplicationDeploymentStack/*",
"Effect": "Allow"
}
]
}

Connect to AWS CodeCommit by HTTPS#

Goto AWS IAM console and download credential to access AWS CodeCommit

git config --global credential.helper '!aws codecommit credential-helper $@'

and

git config --global credential.UseHttpPath true