CDK Pipelines can build a fully functional CI/CD pipeline for your AWS CDK applications, but its internal workings are often misunderstood, leading to unexpected behavior and configuration headaches.

Let’s see what a CDK Pipeline looks like in action. Imagine you have a CDK app in my-cdk-app/ and you want to deploy it to two environments: dev and prod.

Here’s a simplified lib/pipeline-stack.ts in your CDK app:

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { CodePipeline, CodePipelineSource, ShellStep } from 'aws-cdk-lib/pipelines';
import { MyCdkAppStage } from './my-cdk-app-stage';

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

    const source = CodePipelineSource.gitHub(
      'your-github-username/my-cdk-app',
      'main',
      {
        // This is a webhook configuration, not a token itself.
        // The token is managed by AWS Secrets Manager.
        authentication: cdk.SecretValue.secretsManager('github-token'),
      }
    );

    const pipeline = new CodePipeline(this, 'Pipeline', {
      pipelineName: 'MyCdkAppPipeline',
      synth: new ShellStep('Synth', {
        input: source,
        commands: [
          'npm ci',
          'npm run build',
          'npx cdk synth',
        ],
      }),
      // This is where the pipeline definition truly starts to shine.
      // We're not just deploying, we're defining stages.
      stages: [
        new MyCdkAppStage(scope, 'Dev', {
          env: { account: '111111111111', region: 'us-east-1' },
        }),
        new MyCdkAppStage(scope, 'Prod', {
          env: { account: '222222222222', region: 'us-east-1' },
        }),
      ],
    });
  }
}

And here’s lib/my-cdk-app-stage.ts:

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { MyCdkAppStack } from './my-cdk-app-stack';

export class MyCdkAppStage extends cdk.Stage {
  constructor(scope: Construct, id: string, props?: cdk.StageProps) {
    super(scope, id, props);

    new MyCdkAppStack(this, 'MyCdkAppStack', {
      // The environment is critical for Stage deployments.
      // This defines the target account and region for the stack within the stage.
      env: props?.env,
    });
  }
}

When you deploy this PipelineStack using cdk deploy, it doesn’t immediately deploy your application. Instead, it provisions an AWS CodePipeline resource. This CodePipeline is configured to:

  1. Source Stage: Pull code from your specified GitHub repository.
  2. Build Stage (Synth): Run npm ci, npm run build, and npx cdk synth within a CodeBuild environment. The output of cdk synth is an asset.zip containing your CloudFormation template and any assets.
  3. Deploy Stages: For each MyCdkAppStage defined, it creates a corresponding stage in the CodePipeline. Each stage triggers a CloudFormation deployment to the target account and region specified in the Stage’s env property.

The key insight is that CodePipeline itself is a CloudFormation resource. Your PipelineStack deploys the CodePipeline, which then manages the deployment of your application stacks. The stages property on CodePipeline is not for defining your application’s deployment stages directly, but rather for defining the stages within the CodePipeline itself. Each cdk.Stage you pass to stages is translated into a CloudFormation deployment action within the CodePipeline.

The problem that most people miss is how the env property on cdk.Stage interacts with the CodePipeline’s deployment actions. When you define new MyCdkAppStage(scope, 'Dev', { env: { account: '111111111111', region: 'us-east-1' } }), CDK Pipelines creates a CloudFormation Deploy action. This action uses the env specified on the Stage to determine the target account and region for that specific deployment action within the CodePipeline. It’s not about the env of the PipelineStack itself, but the env of the Stage being deployed.

If you want to add manual approval steps before deploying to production, you would add an approval action between the Dev and Prod stages within the CodePipeline definition in your PipelineStack.

The next concept you’ll grapple with is managing secrets for your application stacks that are deployed across different accounts, especially when those secrets need to be accessed by resources in the target account.

Want structured learning?

Take the full Cdk course →