Skip to content

Getting Started with the CDK CI/CD Wrapper

This guide walks through turning a plain AWS CDK app into a CI/CD pipeline with the CDK CI/CD Wrapper: install two packages, write one cicd.config.ts file, and run one CLI command. It follows the same shape as the cdk-cicd-wrapper-example sample in the repository — clone that sample if you want a working starting point instead of typing this out.

Overview

There is no wrapper code in your app — for a TypeScript/JavaScript CDK app. Your bin/ entry point stays exactly what cdk init produced — a plain App with your stacks. A separate cicd.config.ts file, next to cdk.json, describes the pipeline (source repository, stages, CI steps, …). The wrapper is injected at synth time through cdk.json's app command (a Node require preload); with no cicd.config.ts present your app deploys as stock CDK, unmodified. This walkthrough is TS/JS-specific: the preload mechanism can't attach to a non-Node app entry (e.g. Python), so those apps use the explicit CdkCicd.attach(app) call in bin/ instead of the zero-touch cdk-cicd exec path — see the package's jsii-published bindings for the equivalent in your language.

Prerequisites

See Prerequisites for the full list (AWS CLI, Docker, Node.js, etc.). You will also need:

  1. AWS accounts for each stage you plan to deploy to (or a single account for everything, while you evaluate).
  2. A source repository — AWS CodeCommit, GitHub (via an AWS CodeStar connection), or S3.

New CDK project

If you don't already have a CDK project, create one first:

mkdir my-project
cd my-project
npx aws-cdk@latest init app --language typescript

Installation

Install the wrapper library and its CLI:

npm install @cdklabs/cdk-cicd-wrapper @cdklabs/cdk-cicd-wrapper-cli

Note: If the @cdklabs scope is not resolvable from the public npm registry (for example while a pre-release version is only published under the next dist-tag, or your organization proxies npm through a private registry), configure a private NPM registry or AWS CodeArtifact first.

Write cicd.config.ts

Create cicd.config.ts next to your cdk.json:

import { defineCICD, Repository } from '@cdklabs/cdk-cicd-wrapper';

export default defineCICD({
  application: 'my-project',
  repository: Repository.codecommit('my-project'), // or Repository.s3('bucket/key'), or Repository.codestarConnection('org/my-project', connectionArn) for GitHub
  // 'dev' auto-approves (inner loop); 'prod' is gated by a manual approval by default.
  stages: ['dev', { name: 'prod', env: { account: '111111111111', region: 'eu-west-1' } }],
});

See the CD developer guide for the full stage shape (multi-region stages, per-stage manual approval, forced deploy roles) and Repository sources for CodeCommit/GitHub/S3 specifics.

Point cdk.json at cdk-cicd exec

cdk.json's app command is what turns your plain app into a wrapped one — nothing else in bin/ needs to change:

{
  "app": "npx cdk-cicd exec bin/my-project.ts"
}

How the account/region are resolved. When you run a plain cdk synth/cdk deploy for one stage (the inner loop), cdk-cicd exec resolves the target account and region in this order, first match wins:

  1. the active stage's config file — config/<STAGE>.json aws.accountId / aws.region
  2. the matching cicd.config.ts stage's env.account / env.region
  3. the per-stage ACCOUNT_<STAGE> / REGION_<STAGE> environment variables
  4. the ambient CDK_DEFAULT_ACCOUNT / CDK_DEFAULT_REGION

It then exports the resolved values as CDK_DEFAULT_ACCOUNT / CDK_DEFAULT_REGION, so the stock env: { account: process.env.CDK_DEFAULT_ACCOUNT, ... } line below reads them. Put each stage's account and region in config/<STAGE>.json and you declare them once — nothing to repeat in cicd.config.ts. (Inside the self-mutating pipeline, each stage's target is pinned by the pipeline as it synthesizes every stage, so this inner-loop order does not apply there.)

cdk-cicd exec also runs your entry file under a preload that applies the wrapper's runtime hooks (tagging, default security aspects, etc.) — with zero references to the wrapper in your own code:

// bin/my-project.ts — ordinary CDK, no wrapper imports required
import * as cdk from 'aws-cdk-lib';
import { MyStack } from '../lib/my-stack';

const app = new cdk.App();
new MyStack(app, 'my-project', {
  env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },
});

Optional: to control the CloudFormation stack name per stage (for example my-project-dev/my-project-prod), use the stageStackName helper:

import { stageStackName } from '@cdklabs/cdk-cicd-wrapper';

new MyStack(app, 'my-project', {
  stackName: stageStackName('my-project'),
  env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },
});

Bootstrap your stages

The CDK CI/CD Wrapper uses the AWS CDK Toolkit with a cross-account trust relationship to deploy to multiple AWS accounts. Bootstrap every account/region a stage in cicd.config.ts targets, trusting the account the pipeline itself runs in (the account your ambient credentials point at when you run cdk-cicd deploy-ci below):

npx cdk bootstrap aws://<STAGE_ACCOUNT>/<STAGE_REGION> --trust <PIPELINE_ACCOUNT> \
  --cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess

If you are reusing an existing CDK bootstrap setup that already trusts the pipeline account, you can skip this step.

Deploy the pipeline

From the account/region the pipeline itself should run in:

npx cdk-cicd deploy-ci

This provisions the pipeline from cicd.config.ts alone — nothing else needs to exist yet. From here, the pipeline self-updates from cicd.config.ts on every run, so you only run deploy-ci by hand once (and again if you ever need to recover a deleted pipeline stack).

Once deployed, the pipeline runs: SourceBuild (npm ci, then either your configured ci.steps or, if you set none, the default scripts npm run audit/build/test, then cdk synth with CDK Nag) → self-update → one deploy action per configured stage, in order, gated by a manual approval on every stage except your inner-loop ones (dev/res) unless you set manualApproval explicitly.

Configuring Continuous Integration

Leave ci.steps unset and the build runs your project's own npm scripts by default — npm run audit, npm run build, then npm run test. Each runs only when your package.json defines it; a missing script prints a warning pointing at the recommended checks and continues, so it never fails the build. This keeps CI identical to what you run locally and treats the checks as encouraged guidance. See the Audit guide for the recommended audit command to point your script at.

Setting ci.steps replaces those default scripts rather than adding to them, so list everything you want the build to run:

export default defineCICD({
  // ...
  ci: {
    steps: {
      audit: 'npm run audit',
      build: 'npm run build',
      test: 'npm run test',
    },
  },
});

See the CI developer guide for the full picture, including the CI build's synth ordering (always appended, never replaced) and the partialBuildSpec escape hatch.

Deploy changes with GitOps

After the pipeline is deployed, push to the tracked branch to trigger it. For a CodeCommit repository:

sudo pip3 install git-remote-codecommit  # once per machine
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
git remote add origin "codecommit::${AWS_REGION}://${GIT_REPOSITORY}"
git push -u origin "${CURRENT_BRANCH}:main"

For GitHub, add the remote the normal way and push — the CodeStar connection ARN you passed to Repository.codestarConnection(...) is what lets the pipeline read it. See GitHub Integration for the connection setup and why Repository.github(...) alone is not enough outside the GitHub Actions engine.

Migrating an existing Blueprint project

If you have an existing PipelineBlueprint.builder()…synth(app) project, cdk-cicd migrate scaffolds the cicd.config.ts for you:

npx cdk-cicd migrate --entry src/main.ts --application my-project   # add --dry-run to preview

It extracts your stage list (falling back to Blueprint's default RES/DEV/INT when no .defineStages(...) call is found), flags anything it can't safely determine — the repository is always flagged as unresolved today (set repository: Repository.*(...) yourself), plus hooks/phases, workbench, … — and prints the remaining manual steps. It deliberately does not rewrite your entry file's stack construction. Read the full mapping table and the Preserving already-deployed resources section in the repository's MIGRATION.md before switching a production pipeline over — getting the CloudFormation stack name right is what decides whether your existing resources are updated in place or recreated.

Security plugins

The wrapper applies a set of default-on security-hardening Aspects tree-wide: AwsSolutionsChecks (cdk-nag), LogRetention, EncryptBucketOnTransit, EncryptSNSTopicOnTransit, RotateEncryptionKeys, and DisablePublicIPAssignmentForEC2.

Which path applies them depends on the app command in your cdk.json — that is what cdk deploy (or npm run cdk deploy) actually runs:

  • cdk.json app is npx cdk-cicd exec … — the wrapper's runtime preload applies the Aspects automatically. You do not add anything to bin/; CdkCicd.attach(app) would be redundant.
  • cdk.json app runs your own bin/ entry (e.g. npx ts-node --prefer-ts-exts bin/app.ts) — nothing wraps the app, so add one line to that bin/ entry to apply the Aspects yourself:
// cdk.json — this entry is what `cdk deploy` runs
{ "app": "npx ts-node --prefer-ts-exts bin/app.ts" }
// bin/app.ts
import { CdkCicd } from '@cdklabs/cdk-cicd-wrapper';

const app = new cdk.App();
new MyStack(app, 'my-stack', { /* … */ });
CdkCicd.attach(app); // applies the security plugins tree-wide

Choosing which plugins apply

Each plugin has a { name, version }. Configure the set in cicd.config.ts:

export default defineCICD({
  application: 'my-app',
  repository: Repository.codecommit('my-app'),
  stages: ['dev', 'prod'],
  // Omit `plugins` entirely to keep the default-on set.
  // An empty list opts out of all of them.
  // A non-empty list COMPLETELY overrides the defaults — only the plugins named here apply.
  plugins: [
    { name: 'AwsSolutionsChecks', version: '1' },
    { name: 'EncryptSNSTopicOnTransit', version: '1' },
  ],
});

From code, the same selection is available on attach:

CdkCicd.attach(app, { plugins: [{ name: 'EncryptSNSTopicOnTransit', version: '1' }] });
CdkCicd.attach(app, { skipDefaults: true }); // opt out of all

Adding a custom plugin

A custom plugin is any IAspect. Because a live Aspect cannot travel through config, declare its { name, version } in cicd.config.ts's plugins list and register the instance in bin/:

class RequireOwnerTagAspect implements IAspect { /* … */ }

CdkCicd.addPlugin(app, new RequireOwnerTagAspect(), { name: 'RequireOwnerTag', version: '1.0.0' });
CdkCicd.attach(app);

Naming a custom plugin in the config without a matching addPlugin in bin/ fails synth with an actionable error. A complete, runnable example is in samples/security-plugins-proof.

Note: custom plugins (addPlugin) require the explicit CdkCicd.attach(app) path — the zero-touch cdk-cicd exec preload constructs the App before your bin/ runs, so an addPlugin call there cannot be seen. Built-in selection and opt-out work on both paths.

Next steps

Read the Developer Guide for the full picture: repository sources, stage/CD configuration, CI steps, security scanning, VPC/proxy/private-registry support, and the container ("two-repo") deployment mode.