Session 009 — CDK Pipelines: cross-account bootstrap, OIDC connection and pipeline structure
Estimated duration: 60 minutes
Prerequisites: session-008 — CDK: Testing with assertions
Objective
By the end, you will be able to run cdk bootstrap on deploy accounts with a trust policy pointing to the pipeline account, configure the GitHub connection via AWS CodeStar Connections (OIDC — no PAT), and create a CDK Pipeline with Source + Build + UpdatePipeline stages, understanding what each stage does before the first real application deploy.
Context
[FACT] CDK Pipelines is a high-level construct (aws-cdk-lib/pipelines) that uses AWS CodePipeline as its execution engine. It adds an opinionated abstraction layer on top of CodePipeline, with two core features that make it different from a regular pipeline: self-mutation (the pipeline updates itself before any application deploy) and native multi-account deployment (the pipeline runs in one account and deploys to others).
[CONSENSUS] For organizations with multiple AWS accounts (dev, staging, prod), CDK Pipelines is the most direct path to implementing a complete CD pipeline using only CDK — without needing to manually configure CodeBuild projects, cross-account IAM roles, or multi-stage pipelines in the console.
Key concepts
1. The accounts involved — the multi-account topology
A CDK Pipeline always has at least two types of accounts:
┌─────────────────────────────────────────────────────────────┐
│ CONTA TOOLS / PIPELINE (onde a pipeline roda) │
│ ┌──────────────────────────────────────┐ │
│ │ CodePipeline │ │
│ │ Source → Build → UpdatePipeline │ │
│ │ → [Stages da aplicação] │ │
│ │ │ │
│ │ CodeBuild (synth + deploy) │ │
│ │ S3 (artifacts) │ │
│ │ ECR (imagens) │ │
│ └──────────────────────────────────────┘ │
│ │ │ │ │
└───────────┼───────────┼───────────┼─────────────────────────┘
│ assume role │ assume role
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ CONTA DEV │ │ CONTA PROD │
│ (deploy target) │ │ (deploy target) │
│ │ │ │
│ CDKToolkit │ │ CDKToolkit │
│ (bootstrapped │ │ (bootstrapped │
│ com --trust) │ │ com --trust) │
└──────────────────┘ └──────────────────┘
The tools account needs to be able to assume roles in the deploy accounts. This is configured in the bootstrap of each deploy account via --trust.
2. Cross-account bootstrap — the correct sequence
The default bootstrap (cdk bootstrap) creates the CDKToolkit stack with roles that only the account itself can assume. For multi-account CDK Pipelines, you need to modify this behavior.
Step 1 — Bootstrap the tools (pipeline) account:
# A conta tools não precisa de --trust especial
# mas precisa do bootstrap moderno
cdk bootstrap aws://PIPELINE_ACCOUNT_ID/us-east-1 \
--profile pipeline
Step 2 — Bootstrap the deploy accounts with trust:
# Conta de dev — confia na conta da pipeline para fazer deploys
cdk bootstrap aws://DEV_ACCOUNT_ID/us-east-1 \
--profile dev \
--trust PIPELINE_ACCOUNT_ID \
--cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess
# Conta de prod — mesma coisa
cdk bootstrap aws://PROD_ACCOUNT_ID/us-east-1 \
--profile prod \
--trust PIPELINE_ACCOUNT_ID \
--cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess
What --trust does in practice:
[FACT] The --trust PIPELINE_ACCOUNT_ID modifies the trust policy of the CDKToolkit IAM roles in the deploy account to include the pipeline account as an authorized principal:
// Role: cdk-XXXX-deploy-role-DEV_ACCOUNT-us-east-1
// Trust Policy gerada pelo --trust:
{
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::PIPELINE_ACCOUNT_ID:root"
},
"Action": "sts:AssumeRole"
}
]
}
This allows CodeBuild running in the pipeline account to perform sts:AssumeRole in the deploy account and execute CloudFormation there.
--cloudformation-execution-policies — what it is and why it matters:
[FACT] This flag defines the IAM policies that the CloudFormation execution role (the one that actually creates resources) will have in the deploy account. AdministratorAccess is convenient but permissive — for production, consider creating a custom policy that only allows the resources your application uses.
# Versão mais restrita para prod (exemplo)
cdk bootstrap aws://PROD_ACCOUNT_ID/us-east-1 \
--profile prod \
--trust PIPELINE_ACCOUNT_ID \
--cloudformation-execution-policies arn:aws:iam::PROD_ACCOUNT_ID:policy/CdkDeployPolicy
Adding trust to an existing bootstrap:
[FACT] When re-running cdk bootstrap with --trust, you must list all accounts that should have trust — not just the new ones. Accounts not listed in the new command lose their trust.
# Re-bootstrap adicionando uma segunda conta de pipeline (ex: após migração)
cdk bootstrap aws://DEV_ACCOUNT_ID/us-east-1 \
--profile dev \
--trust PIPELINE_ACCOUNT_ID_1 \
--trust PIPELINE_ACCOUNT_ID_2 \ # ← novo; PIPELINE_ACCOUNT_ID_1 deve ser repetido
--cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess
3. CodeStar Connections — GitHub without PAT
[FACT] The legacy way to integrate CodePipeline with GitHub used a Personal Access Token (PAT) stored in Secrets Manager. The modern way uses AWS CodeStar Connections, which implements OAuth via an AWS App installed on GitHub — without any static token.
Why CodeStar Connections is preferable:
PAT (legado) CodeStar Connections (moderno)
───────────────────────────────── ────────────────────────────────
Token estático com validade OAuth gerenciado pela AWS
Precisa rotacionar manualmente Sem rotação necessária
Armazenado no Secrets Manager Sem segredo a gerenciar
Permissões amplas por repositório Permissões por instalação da App
Webhook manual ou polling Webhook automático via App
Creating the connection (can only be done via Console or CLI — not via CDK):
# Etapa 1: criar a connection (fica em PENDING até ser autorizada)
aws codestar-connections create-connection \
--provider-type GitHub \
--connection-name minha-org-github \
--profile pipeline
# Retorna: { "ConnectionArn": "arn:aws:codestar-connections:us-east-1:..." }
# Etapa 2: autorizar no console
# Console → Developer Tools → Connections → [sua connection] → Update pending connection
# → Conecta com sua conta GitHub → autoriza a AWS App
# Status muda de PENDING → AVAILABLE
[FACT] The connection authorization must go through the Console — there is no way to do this via CLI or CDK. This is intentional: AWS requires human interaction to authorize OAuth access to GitHub.
Verify that the connection is available:
aws codestar-connections get-connection \
--connection-arn arn:aws:codestar-connections:us-east-1:ACCOUNT:connection/UUID \
--profile pipeline \
--query 'Connection.ConnectionStatus'
# "AVAILABLE" ← pronto para uso
# "PENDING" ← ainda precisa ser autorizada no console
4. CDK Pipeline structure — the automatic stages
A minimal CDK Pipeline has 3 automatically generated stages before any application stage:
GitHub CodePipeline (conta tools)
│ │
│ push para main │
├──────────────────────────────►
│ ┌────▼──────────┐
│ │ Source │ Baixa o código do GitHub
│ │ │ via CodeStar Connection
│ └────┬──────────┘
│ │
│ ┌────▼──────────┐
│ │ Build │ npm ci + cdk synth
│ │ (Synth) │ Gera o cloud assembly
│ └────┬──────────┘
│ │
│ ┌────▼──────────┐
│ │ UpdatePipeline│ Aplica mudanças na
│ │ (Self-Mutate) │ própria pipeline
│ └────┬──────────┘
│ │
│ ┌─────────▼──────────┐
│ │ Assets │ Upload S3/ECR
│ └─────────┬──────────┘
│ │
│ ┌─────────▼──────────┐
│ │ Stage: Dev │ Deploy nas contas
│ │ Stage: Prod │ de aplicação
│ └────────────────────┘
What each automatic stage does:
Source:
- Downloads the code from the GitHub repository via CodeStar Connection
- Stores the artifact in S3 (pipeline's artifact bucket)
- Triggered by push on the configured branch
Build (Synth):
- Runs npm ci (or equivalent) to install dependencies
- Executes cdk synth to generate the cloud assembly (cdk.out/)
- The cloud assembly is the artifact that feeds all subsequent stages
UpdatePipeline (Self-Mutate):
- Compares the pipeline definition in the cloud assembly with the current pipeline
- If the pipeline changed: applies the changes and restarts the execution from scratch
- If it didn't change: advances to the application stages
5. Self-mutation — the mechanism that changes everything
Self-mutation is the most important and most confusing feature of CDK Pipelines.
[FACT] When you change the pipeline code (add a stage, change a ShellStep, etc.) and push, the cycle is:
Push para main
│
▼
Source → Build → UpdatePipeline
│
Pipeline mudou?
│ │
Sim Não
│ │
▼ ▼
Pipeline Continua
se atualiza para os
e REINICIA stages de
do zero aplicação
│
▼
Source → Build → UpdatePipeline
│
Agora igual
│
▼
Assets → Dev → Prod
Practical implications of self-mutation:
-
The first execution never deploys the application — it only updates the pipeline (which was just created) and restarts. The second execution is the one that deploys.
-
Pipeline changes are atomic — you cannot have "the production pipeline is on the old version while I test the new version". Every change goes through the pipeline before reaching the application.
-
Debugging becomes slower — to test a change in the pipeline definition, you need to push, wait for the pipeline to run, see the result. There's no way to test locally.
-
selfMutation: falsefor development — during initial pipeline development, you can disable self-mutation to iterate faster. But never in production.
6. Creating the pipeline — complete code
// lib/pipeline-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as pipelines from 'aws-cdk-lib/pipelines';
import { Construct } from 'constructs';
import { ApplicationStage } from './application-stage';
export class PipelineStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: cdk.StackProps) {
super(scope, id, props);
// 1. Fonte: GitHub via CodeStar Connection
const source = pipelines.CodePipelineSource.connection(
'minha-org/meu-repo', // owner/repo no GitHub
'main', // branch
{
connectionArn: 'arn:aws:codestar-connections:us-east-1:PIPELINE_ACCOUNT:connection/UUID',
triggerOnPush: true, // padrão: true
}
);
// 2. Synth step: instala deps e executa cdk synth
const synth = new pipelines.ShellStep('Synth', {
input: source,
commands: [
'npm ci', // instala dependências (lockfile)
'npm run build', // compila TypeScript (se necessário)
'npx cdk synth', // gera o cloud assembly
],
// primaryOutputDirectory: 'cdk.out', // padrão quando cdk synth é o último comando
});
// 3. Definição da pipeline
const pipeline = new pipelines.CodePipeline(this, 'Pipeline', {
pipelineName: 'MeuAppPipeline',
synth,
// CodeBuild image para o synth (padrão: aws/codebuild/standard:7.0)
codeBuildDefaults: {
buildEnvironment: {
buildImage: codebuild.LinuxBuildImage.STANDARD_7_0,
},
},
// Self-mutation habilitado (padrão: true)
selfMutation: true,
// Publicar assets em paralelo (mais rápido para múltiplos assets)
publishAssetsInParallel: true,
// Docker necessário? (se algum asset usa Docker)
dockerEnabledForSynth: false,
dockerEnabledForSelfMutation: false,
});
// 4. Adicionar stages de aplicação
pipeline.addStage(new ApplicationStage(this, 'Dev', {
env: { account: 'DEV_ACCOUNT_ID', region: 'us-east-1' },
}));
// Prod com aprovação manual antes do deploy
pipeline.addStage(new ApplicationStage(this, 'Prod', {
env: { account: 'PROD_ACCOUNT_ID', region: 'us-east-1' },
}), {
pre: [new pipelines.ManualApprovalStep('ApproveProductionDeploy')],
});
}
}
// bin/app.ts
const app = new cdk.App();
// A pipeline stack roda na conta tools
new PipelineStack(app, 'PipelineStack', {
env: { account: 'PIPELINE_ACCOUNT_ID', region: 'us-east-1' },
});
First deploy — bootstrapping the pipeline:
# 1. Bootstrap de todas as contas (uma vez)
cdk bootstrap aws://PIPELINE_ACCOUNT_ID/us-east-1 --profile pipeline
cdk bootstrap aws://DEV_ACCOUNT_ID/us-east-1 \
--profile dev \
--trust PIPELINE_ACCOUNT_ID \
--cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess
cdk bootstrap aws://PROD_ACCOUNT_ID/us-east-1 \
--profile prod \
--trust PIPELINE_ACCOUNT_ID \
--cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess
# 2. Deploy inicial da pipeline stack (só precisa rodar uma vez)
cdk deploy PipelineStack --profile pipeline
# A partir daqui, todo push para main atualiza e executa a pipeline automaticamente
7. What happens before the first application deploy
Detailed sequence on the first execution after cdk deploy PipelineStack:
Execução #1 (após cdk deploy PipelineStack):
Source: Baixa o código do GitHub
Build: npm ci + cdk synth → gera cloud assembly
UpdatePipeline: Compara pipeline atual vs cloud assembly
→ A pipeline foi criada pelo cdk deploy manual
→ Cloud assembly tem a mesma definição
→ Sem mudanças → avança
Assets: Faz upload dos assets para S3/ECR
Dev: Deploya ApplicationStage na conta dev ✅
Approve: Aguarda aprovação manual
Prod: Deploya ApplicationStage na conta prod ✅
───────────────────────────────────────────────────────────────
Depois, você faz uma mudança na pipeline (ex: adiciona um stage):
push para main
Execução #2:
Source: Baixa o novo código
Build: cdk synth com o novo stage
UpdatePipeline: Pipeline mudou → aplica mudança → REINICIA
Execução #3 (após restart):
Source → Build → UpdatePipeline (sem mudança agora)
Assets → Dev → Approve → Prod
Practical example — verifying the pipeline after deploy
# Ver o status da pipeline
aws codepipeline get-pipeline-state \
--name MeuAppPipeline \
--profile pipeline \
--query 'stageStates[*].{Stage:stageName,Status:latestExecution.status}' \
--output table
# Ver os logs do Synth step
aws codebuild batch-get-builds \
--ids $(aws codepipeline get-pipeline-state \
--name MeuAppPipeline \
--query 'stageStates[?stageName==`Build`].actionStates[0].latestExecution.externalExecutionId' \
--output text) \
--query 'builds[0].logs.deepLink'
# Forçar nova execução manualmente
aws codepipeline start-pipeline-execution \
--name MeuAppPipeline \
--profile pipeline
# Ver a definição atual da pipeline
aws codepipeline get-pipeline \
--name MeuAppPipeline \
--profile pipeline \
| jq '.pipeline.stages[].name'
Common pitfalls
1. Connection in PENDING status — pipeline stalls at Source
If you create the connection via CLI but forget to authorize it in the Console, the pipeline will stall at the Source stage with the error Connection is not authorized. Always verify the connection status before creating the pipeline.
2. Bootstrap without --trust — deploy fails with AccessDenied
If you forget the --trust PIPELINE_ACCOUNT_ID in the bootstrap of the deploy accounts, the pipeline will run successfully until the deploy stage and fail with AccessDenied when trying to assume the role in the target account. The error is not obvious — it appears in the CodeBuild logs as an sts:AssumeRole failure.
3. Self-mutation and pipeline development — confusing loop
During initial development, every change to the pipeline requires a push, waiting for Source + Build + UpdatePipeline, and a restart. If the pipeline is breaking at Synth, you get stuck in a loop: push → fail → fix → push → fail. To escape faster: test cdk synth locally before pushing, and use selfMutation: false temporarily while building the pipeline for the first time.
4. dockerEnabledForSynth: false with Docker assets
If any of your Lambda assets uses DockerImageFunction or PythonFunction with Docker bundling, the Synth step needs Docker. Without dockerEnabledForSynth: true, the synth will fail with a Docker not available error. Enabling Docker in CodeBuild increases the build startup time.
Reflection exercise
You have a CDK Pipeline running in the tools account that deploys to dev and prod. The pipeline is working well. Then you need to add a third environment staging (new account) between dev and prod.
Describe the exact sequence of steps needed — including which commands to run in which account, in which order, and what happens to the pipeline during this transition. In particular: when does the pipeline need to be paused? Is there a risk of downtime in dev or prod during the addition of the new stage? What happens if you add the stage in the code and push without having bootstrapped the staging account first?
Resources for further study
CDK Pipelines:
- Continuous integration and delivery (CI/CD) using CDK Pipelines — complete guide with examples of multi-account, self-mutation, and ShellSteps.
Cross-account bootstrap:
- Bootstrap your environment for use with the AWS CDK — --trust and --cloudformation-execution-policies options with command examples.
CodeStar Connections:
- GitHub connections — connection creation and authorization process in the Console, including IAM permission requirements for creating connections.