CDK Pipelines and Continuous Deployment
CDK Pipelines and Continuous Deployment
CDK Pipelines is a construct that creates a CodePipeline which deploys your CDK application. The key innovation: the pipeline deploys itself. When you change the pipeline definition (add a stage, modify a step), the pipeline updates itself first, then continues with the deployment. No chicken-and-egg problem.
Pipeline Architecture
from aws_cdk import (
Stack, Stage, Environment,
pipelines,
aws_codebuild as codebuild,
)
from constructs import Construct
# Stage: A complete deployment of your application to one environment
class ApplicationStage(Stage):
def __init__(self, scope, id, *, env, **kwargs):
super().__init__(scope, id, env=env, **kwargs)
# Each stage contains all your stacks
network = NetworkStack(self, 'Network')
data = DataStack(self, 'Data', vpc=network.vpc)
service = ServiceStack(self, 'Service',
table=data.orders_table, vpc=network.vpc)
# Pipeline Stack: Lives in the CI/CD account
class PipelineStack(Stack):
def __init__(self, scope, id, **kwargs):
super().__init__(scope, id, **kwargs)
# Source: GitHub/CodeCommit
source = pipelines.CodePipelineSource.connection(
'myorg/myrepo', 'main',
connection_arn='arn:aws:codestar-connections:us-east-1:123456789012:connection/xxx'
)
# Synth step: Install deps + synthesize CDK
synth = pipelines.ShellStep('Synth',
input=source,
commands=[
'npm ci',
'npx cdk synth'
],
primary_output_directory='cdk.out'
)
# The pipeline itself
pipeline = pipelines.CodePipeline(self, 'Pipeline',
pipeline_name='app-deployment',
synth=synth,
cross_account_keys=True, # Required for cross-account deployments
docker_enabled_for_synth=True,
code_build_defaults=pipelines.CodeBuildOptions(
build_environment=codebuild.BuildEnvironment(
compute_type=codebuild.ComputeType.MEDIUM,
build_image=codebuild.LinuxBuildImage.STANDARD_7_0
)
)
)
# Deploy to staging first
staging = ApplicationStage(self, 'Staging',
env=Environment(account='111111111111', region='us-east-1')
)
staging_deployment = pipeline.add_stage(staging,
pre=[
# Run unit tests before deploying
pipelines.ShellStep('UnitTests',
input=source,
commands=[
'pip install -r requirements-dev.txt',
'pytest tests/unit/ -v'
]
)
],
post=[
# Integration tests after deploying to staging
pipelines.ShellStep('IntegrationTests',
input=source,
env_from_cfn_outputs={
'API_URL': service.api_url_output,
},
commands=[
'pip install -r requirements-dev.txt',
'pytest tests/integration/ -v --api-url $API_URL'
]
)
]
)
# Deploy to production with manual approval
prod = ApplicationStage(self, 'Production',
env=Environment(account='222222222222', region='us-east-1')
)
pipeline.add_stage(prod,
pre=[
pipelines.ManualApprovalStep('PromoteToProd',
comment='Staging integration tests passed. Approve production deployment?'
)
]
)
package com.mycompany.pipeline;
import software.amazon.awscdk.*;
import software.amazon.awscdk.pipelines.*;
import software.amazon.awscdk.services.codebuild.*;
import software.constructs.Construct;
import java.util.*;
public class PipelineStack extends Stack {
public PipelineStack(Construct scope, String id, StackProps props) {
super(scope, id, props);
// Source
CodePipelineSource source = CodePipelineSource.connection(
"myorg/myrepo", "main",
ConnectionSourceOptions.builder()
.connectionArn("arn:aws:codestar-connections:us-east-1:123456789012:connection/xxx")
.build());
// Synth
ShellStep synth = ShellStep.Builder.create("Synth")
.input(source)
.commands(List.of("npm ci", "npx cdk synth"))
.primaryOutputDirectory("cdk.out")
.build();
// Pipeline
CodePipeline pipeline = CodePipeline.Builder.create(this, "Pipeline")
.pipelineName("app-deployment")
.synth(synth)
.crossAccountKeys(true)
.build();
// Staging
Stage staging = new ApplicationStage(this, "Staging",
StageProps.builder()
.env(Environment.builder()
.account("111111111111").region("us-east-1").build())
.build());
pipeline.addStage(staging, AddStageOpts.builder()
.post(List.of(
ShellStep.Builder.create("IntegrationTests")
.input(source)
.commands(List.of(
"mvn test -Dtest=IntegrationTest",
"echo 'Integration tests passed'"
))
.build()
))
.build());
// Production with approval
Stage prod = new ApplicationStage(this, "Production",
StageProps.builder()
.env(Environment.builder()
.account("222222222222").region("us-east-1").build())
.build());
pipeline.addStage(prod, AddStageOpts.builder()
.pre(List.of(
ManualApprovalStep.Builder.create("PromoteToProd")
.comment("Approve production deployment?")
.build()
))
.build());
}
}
Wave-Based Deployment (Multi-Region)
For multi-region deployments, use waves to deploy to multiple regions in parallel:
# Deploy to multiple regions within a wave
wave = pipeline.add_wave('MultiRegion')
wave.add_stage(ApplicationStage(self, 'US-East',
env=Environment(account='222222222222', region='us-east-1')))
wave.add_stage(ApplicationStage(self, 'EU-West',
env=Environment(account='222222222222', region='eu-west-1')))
wave.add_stage(ApplicationStage(self, 'AP-Southeast',
env=Environment(account='222222222222', region='ap-southeast-1')))
# All three regions deploy simultaneously
Deployment Safety
# Canary deployment for Lambda using CodeDeploy
from aws_cdk import aws_codedeploy as codedeploy
class SafeLambdaDeployment(Construct):
def __init__(self, scope, id, *, function: lambda_.Function):
super().__init__(scope, id)
# Create alias for traffic shifting
alias = function.add_alias('live')
# CodeDeploy deployment group with canary traffic shifting
deployment_group = codedeploy.LambdaDeploymentGroup(self, 'DeploymentGroup',
alias=alias,
deployment_config=codedeploy.LambdaDeploymentConfig.CANARY_10_PERCENT_5_MINUTES,
# Route 10% of traffic to new version for 5 minutes
# If no errors: shift 100% to new version
# If errors: automatic rollback to previous version
alarms=[
function.metric_errors(period=Duration.minutes(1))
.create_alarm(self, 'ErrorAlarm', threshold=5, evaluation_periods=1)
],
auto_rollback=codedeploy.AutoRollbackConfig(
failed_deployment=True,
stopped_deployment=True,
deployment_in_alarm=True # Rollback if alarm fires during deployment
)
)
Cross-Account Bootstrapping
Before CDK Pipelines can deploy to other accounts, those accounts must be bootstrapped with trust:
# Bootstrap the CI/CD account (where pipeline lives)
cdk bootstrap aws://000000000000/us-east-1 \
--qualifier myapp \
--cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess
# Bootstrap target accounts with trust to the CI/CD account
cdk bootstrap aws://111111111111/us-east-1 \
--qualifier myapp \
--trust 000000000000 \
--trust-for-lookup 000000000000 \
--cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess
cdk bootstrap aws://222222222222/us-east-1 \
--qualifier myapp \
--trust 000000000000 \
--trust-for-lookup 000000000000 \
--cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess
Pipeline self-mutation: When you push a change to your CDK code that modifies the pipeline itself (add a stage, change a build step), the pipeline first deploys its own update, then restarts and continues with the application deployment. This eliminates the “how do I update my pipeline?” bootstrap problem.