Container mode: build once, deploy many
!!! abstract "What you'll build" - Repo 1 — CI pipeline: runs CI and pushes a config-agnostic deployer image to ECR — it deploys
nothing. - Repo 2 — CD pipeline: a config-only deploy.config.ts whose pipeline pulls that image and
deploys each target (or run the same executor locally with cdk-cicd deploy --from-image). - An understanding of the "one image → many deployments" scale-out and rollback-by-retag.
For an enterprise / "Automation Framework" flow, Autopilot supports a two-repo split:
- Repo 1 (CI pipeline) — the CDK app repo. Its pipeline runs CI and then builds & pushes a config-agnostic deployer image (your CDK code + its npm deps + the wrapper tooling) to ECR. It deploys nothing.
- Repo 2 (CD pipeline) — a config-only repo (no CDK code) whose pipeline pulls that image and runs it against each target's config, synthesizing per target at run time. One image → many deployments.
Repo 1 — turn the pipeline into an image builder
Add a deployerImage to the config. The pipeline then renders as Source → BuildImage — no deploy
stages:
import { defineCICD, Repository, BuildImage } from '@cdklabs/cdk-cicd-wrapper';
export default defineCICD({
application: 'my-app',
repository: Repository.github('my-org/my-app'),
deployerImage: BuildImage.docker({
dockerfile: 'Dockerfile', // default; the image payload is your app + deps, NOT cdk.out
// repositoryName: 'my-app-deployer', // reference an existing ECR repo; omit to provision one
// tagStrategy: ImageTagStrategy.GIT_SHA, // default: tag by commit; or LATEST
}),
});
What the Repo 1 pipeline does
cdk-cicd deploy-ci provisions a secondary CodePipeline whose single build project:
- runs
npm ciand your CI scripts (npm run audit/build/test, CI as a validation gate), - logs in to ECR (
aws ecr get-login-password | docker login …), docker builds your Dockerfile and pushes the image, tagged by the resolved commit.
If you don't name an existing repo, the pipeline provisions one (<application>-deployer); a
disposable pipeline empties and deletes it on teardown.
Why the image, not cdk.out
The image bakes code + deps but never cdk.out, so Repo 2 can synth-and-deploy it offline against
any target's config — no npm install and no registry access at deploy time. That's what collapses the
per-target pipeline sprawl: targets become config rows a single image is run against, not pipeline
resources.
Repo 2 — the CD pipeline that deploys the image
Repo 2 is a small, app-agnostic config repo (no CDK code) that says which image to run and
where to deploy it. Describe that with defineDeployment in a deploy.config.ts:
// deploy.config.ts
import { defineDeployment, Repository } from '@cdklabs/cdk-cicd-wrapper';
export default defineDeployment({
// The BASE deployer image repo (no tag). The per-stage version is appended at deploy time.
image: '111111111111.dkr.ecr.eu-west-1.amazonaws.com/my-app-deployer',
// The config-only source repo this CD pipeline watches (deploy.config.ts + config/<stage>.json here).
repository: Repository.codecommit('my-app-deploy-config'),
// Targets say WHERE (account/region/role) + gating. The VERSION each runs comes from config/<stage>.json.
targets: [
{ stage: 'dev', env: { account: '111111111111', region: 'eu-west-1' } },
{ stage: 'int', env: { account: '111111111111', region: 'eu-west-1' }, manualApproval: true },
{
stage: 'prod',
env: { account: '222222222222', regions: ['eu-west-1', 'us-east-1'] },
manualApproval: true,
deployment: { deployRole: 'arn:aws:iam::222222222222:role/automation-deployer' },
},
],
});
Each stage's version lives in its own config file in the CD repo (a hash or semver) — not baked in the image:
// config/dev.json config/int.json config/prod.json
{ "version": "1.5.0" } { "version": "1.4.2" } { "version": "1.4.2" }
The deploy resolves image = <base-repo>:<version-from-config/<stage>.json>. Provision the CD pipeline —
the deploy-side twin of deploy-ci for a CI pipeline:
npx cdk-cicd deploy-ci # sees deploy.config.ts (not cicd.config.ts) → provisions the CD pipeline
This renders a second CodePipeline with one Deploy action per target. Each action runs
cdk-cicd deploy --from-image --target <stage>, which reads that stage's version from
config/<stage>.json at run time, pulls <base-repo>:<version>, and synth-and-deploys the stage. So:
- Per-stage versions in config: bump
config/dev.json'sversion, commit → onlydevredeploys on it.devcan run a newer version thanprod, and the version is plain config, reviewable in a PR. - Parallel deploys: ungated targets deploy in parallel; a gated target (e.g.
int/prod) waits on its manual-approval action, then runs — sointandprodpromote in parallel once approved. - Two pipelines total: the CI pipeline (Repo 1) pushes the image; the CD pipeline (Repo 2) pulls it.
Run it locally without a pipeline
The CodeBuild step is just the cdk-cicd deploy --from-image --yes executor, which you can also run
from any machine or CI runner (it pulls the image and deploys each target). Omit repository from
deploy.config.ts to use only the local executor with no CD pipeline. On a runner whose default docker
network can't reach AWS, add --docker-network host.
Either way, the pinned image runs once per (target × region): for each run the container synthesizes that stage against the target's injected environment — offline, inside the image — and deploys the result. One build produces one image, and that image drives N deploys.
Division of authority: WHAT vs WHERE
The image is authoritative for what to deploy — the app code and its stage definitions are baked
in at build time. The deploy.config.ts target is authoritative for where — the account,
region(s), and role for each stage. A target's deployment.deployRole (chapter 2's shape) is threaded
through to each target's deploy.
Manual-approval gates
A target's manualApproval: true is honored on both paths. The CD pipeline renders two deploy
stages: a Deploy stage runs every ungated target in parallel, then a DeployGated stage places each
gated target behind its own manual-approval action (so int and prod each wait on their own
approval, then deploy). The local executor (cdk-cicd deploy --from-image) is fail-closed — it
refuses a gated target unless you pass --yes.
Rollback is a retag
To roll back, point image: at the previous version tag (e.g. :1.4.1) and re-run
cdk-cicd deploy --from-image. Because the image pins code + deps and config supplies the lookups,
the same image + same config always synthesizes the same template — a deterministic rollback with no
rebuild.
Verify
!!! success "Verify" - Repo 1 (CI pipeline): renders as Source → BuildImage (no deploy stages), and after a run the
tagged image is present in ECR (aws ecr describe-images --repository-name my-app-deployer). - Repo 2 (CD pipeline): deploy-ci provisions a Source → Deploy CodePipeline; a config change
triggers the CodeBuild that pulls the image and creates/updates each target's CloudFormation stack. - Change nothing but the image: tag and re-run — the deploy reproduces the pinned version, proving
one image serves many deployments and rollbacks.
Recap
Container mode splits build from deploy into two pipelines: the CI pipeline (Repo 1) builds and pushes
one config-agnostic, offline-capable image; the CD pipeline (Repo 2, config-only) pulls that image and
deploys as many targets as you like — the same cdk-cicd deploy --from-image executor you can also run
locally. Targets are config rows, not pipeline resources — that's the scale-out win — and rollback is just
pointing at a previous image tag. Next: migrating an existing Blueprint app to any of these Autopilot patterns.