Skip to content
6 min read·Lesson 6 of 8

Production CDK Pipelines and CI/CD

Automating CDK deployments — CDK Pipelines for self-mutating pipelines, GitHub Actions for lightweight CI, and multi-stage promotion.

Writing the infrastructure code is one thing; running it safely in a multi-stage pipeline with approval gates, tests, and automatic rollback is another. CDK has a first-class answer.

CDK Pipelines

CDK Pipelines is a L3 construct that deploys a CodePipeline which manages itself. The "self-mutating" property means when you push a change to the pipeline's infrastructure definition, the pipeline updates itself before deploying anything downstream.

Structure

// lib/pipeline-stack.ts
import * as cdk from 'aws-cdk-lib'
import * as pipelines from 'aws-cdk-lib/pipelines'
import { Construct } from 'constructs'
import { WebStage } from './web-stage'

export class PipelineStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props: cdk.StackProps) {
    super(scope, id, props)

    const pipeline = new pipelines.CodePipeline(this, 'Pipeline', {
      pipelineName: 'WebDeployment',
      synth: new pipelines.ShellStep('Synth', {
        input: pipelines.CodePipelineSource.connection(
          'acme/infra',
          'main',
          { connectionArn: 'arn:aws:codestar-connections:...' }
        ),
        commands: [
          'npm ci',
          'npm run build',
          'npm test',
          'npx cdk synth',
        ],
      }),
    })

    // dev stage — deploys immediately
    pipeline.addStage(new WebStage(this, 'Dev', {
      env: { account: '111111111', region: 'us-east-1' },
      stage: 'dev',
    }))

    // staging — approval required
    pipeline.addStage(new WebStage(this, 'Staging', {
      env: { account: '222222222', region: 'us-east-1' },
      stage: 'staging',
    }), {
      pre: [new pipelines.ShellStep('RunIntegTests', {
        commands: ['npx integ-runner --parallel-regions us-east-1'],
      })],
    })

    // prod — manual approval gate
    pipeline.addStage(new WebStage(this, 'Prod', {
      env: { account: '333333333', region: 'eu-west-1' },
      stage: 'prod',
    }), {
      pre: [new pipelines.ManualApprovalStep('PromoteToProd')],
    })
  }
}

// lib/web-stage.ts
interface WebStageProps extends cdk.StageProps { stage: string }

export class WebStage extends cdk.Stage {
  constructor(scope: Construct, id: string, props: WebStageProps) {
    super(scope, id, props)
    new WebStack(this, 'Web', { stage: props.stage })
  }
}

Bootstrapping for CDK Pipelines

CDK Pipelines requires newer-style bootstrapping with cross-account trust:

# Bootstrap the tools account (where CodePipeline lives)
cdk bootstrap aws://111111111/us-east-1   --cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess

# Bootstrap target accounts, trusting the tools account
cdk bootstrap aws://222222222/us-east-1   --trust 111111111   --cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess

GitHub Actions — the Lightweight Alternative

For teams without CodePipeline requirements, GitHub Actions with OIDC-to-AWS is often simpler:

# .github/workflows/deploy.yml
name: Deploy Infrastructure
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

permissions:
  id-token: write
  contents: read

jobs:
  diff:
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '22', cache: npm }
      - run: npm ci
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::111111111:role/GithubActionsRole
          aws-region: us-east-1
      - run: npx cdk diff --app "npx ts-node bin/app.ts"

  deploy-dev:
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: dev
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '22', cache: npm }
      - run: npm ci
      - run: npm test
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::111111111:role/GithubActionsRole
          aws-region: us-east-1
      - run: npx cdk deploy --all --require-approval never --app "npx ts-node bin/app.ts"

  deploy-prod:
    needs: deploy-dev
    runs-on: ubuntu-latest
    environment: production    # requires manual approval via GitHub Environments
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '22', cache: npm }
      - run: npm ci
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::333333333:role/GithubActionsRole
          aws-region: eu-west-1
      - run: npx cdk deploy --all --require-approval never

OIDC Trust Policy

Set up once via CDK itself or the AWS console:

const githubProvider = new iam.OpenIdConnectProvider(this, 'GitHub', {
  url: 'https://token.actions.githubusercontent.com',
  clientIds: ['sts.amazonaws.com'],
})

new iam.Role(this, 'GitHubActionsRole', {
  assumedBy: new iam.WebIdentityPrincipal(
    githubProvider.openIdConnectProviderArn,
    {
      StringEquals: {
        'token.actions.githubusercontent.com:aud': 'sts.amazonaws.com',
      },
      StringLike: {
        'token.actions.githubusercontent.com:sub': 'repo:acme/infra:ref:refs/heads/main',
      },
    }
  ),
  managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName('AdministratorAccess')],
})

This is the "no long-lived secret" approach — GitHub issues OIDC JWTs that AWS validates via the IdP trust.

The PR / Main Branch Model

EventAction
PR opened / updatednpm test + cdk diff — comment diff output to the PR
PR merged to mainDeploy to dev; run integration tests
Git tag (v1.2.3)Deploy to staging → approval → prod

Security Gates in CI

# In the diff step, fail if any IAM statement with * resource or action
- name: Security check
  run: |
    cdk synth 2>/dev/null
    # check for overly permissive IAM
    node -e "
      const fs = require('fs')
      const template = JSON.parse(fs.readFileSync('cdk.out/AppStack.template.json', 'utf8'))
      const resources = template.Resources || {}
      Object.values(resources).forEach((r) => {
        if (r.Type === 'AWS::IAM::Policy') {
          const stmts = r.Properties?.PolicyDocument?.Statement || []
          stmts.forEach((s) => {
            if (s.Resource === '*' && s.Effect === 'Allow') {
              console.error('Over-privileged statement:', JSON.stringify(s))
              process.exit(1)
            }
          })
        }
      })
    "
    echo 'IAM check passed'

Drift Detection

CloudFormation stacks can drift when someone modifies resources through the console. Add a weekly check:

# .github/workflows/drift.yml
on:
  schedule: [{ cron: '0 9 * * 1' }]  # Every Monday 9am

jobs:
  drift:
    runs-on: ubuntu-latest
    steps:
      - uses: aws-actions/configure-aws-credentials@v4
        with: { role-to-assume: ${{ secrets.AWS_ROLE_ARN }}, aws-region: us-east-1 }
      - run: |
          aws cloudformation detect-stack-drift --stack-name WebStack-Prod
          sleep 60
          status=$(aws cloudformation describe-stack-drift-detection-status             --stack-drift-detection-id $id --query 'StackDriftStatus' --output text)
          if [[ "$status" != "IN_SYNC" ]]; then echo "Drift detected!"; exit 1; fi

What a Mature Pipeline Looks Like

A production CDK pipeline:

  1. PR: npm test + cdk diff + IAM policy checker + cdk-nag
  2. Merge to main: auto-deploy dev; run integration tests
  3. Weekly: staging deployment + full regression
  4. Release tag: staging → manual approval → prod (with automatic rollback on CloudWatch alarm)
  5. Monday 9am: drift detection across all stacks

That is safe, auditable, and automated. Two lessons remain — multi-cloud IaC with Pulumi, and Kubernetes manifests with cdk8s.

Key Takeaways

  • CDK Pipelines (pipelines.CodePipeline) is a self-mutating pipeline construct that deploys itself.
  • It models the full dev → staging → prod promotion with pre/post-deploy hook support.
  • For GitHub-centric teams, GitHub Actions + OIDC to AWS is often simpler than CodePipeline.
  • Always run cdk diff as a plan step — treat unexpected IAM changes as blockers.
  • Asset publishing, stack deployment, and rollback are all handled by CDK Pipelines.

Test your knowledge

Try exam-style practice questions to reinforce what you've learned.

Practice Questions →