Running CDK Bootstrap from AWS Lambda
In this article I will go over a cross-account CDK Bootstrapping from AWS Lambda. Bootstrapping is a mandatory step for further deployment of infrastructure defined with AWS CDK. CDK Bootstrap creates a Cloudformation Stack (CDKToolkit) which contains various resource. The template of this stack could be found here. There is two ways to solve the problem: Create a Cloudformation StackSet with the provided template. Read AWS Blog post (Bootstrapping multiple AWS accounts for AWS CDK using Stacksets). Run cdk bootstrap whenever a new account is created. The biggest advantage of running cdk bootstrap in a lambda is that it's not needed to sync the template with the stackset. Source code of the lambda The lambda function receives an event with a list of accounts it must to login, and execute cdk bootstrap Make sure that all permissions for cross-account access are set, and the lambda function can assume a role in the target account. /src/bootstrap/index.ts import { execSync } from 'child_process'; import { STSClient, AssumeRoleCommand } from "@aws-sdk/client-sts"; const getCredentials = async (accountId: string, roleName: string, region: string): Promise => { console.log(`Assuming role: ${roleName} in account: ${accountId}`); const stsClient = new STSClient({ region: region }); const response = await stsClient.send(new AssumeRoleCommand({ RoleArn: `arn:aws:iam::${accountId}:role/${roleName}`, RoleSessionName: "CDKBootstrap" })); console.log(`Assumed role: ${roleName} in account: ${accountId}`); return { accessKeyId: response.Credentials?.AccessKeyId || '', secretAccessKey: response.Credentials?.SecretAccessKey || '', sessionToken: response.Credentials?.SessionToken || '', }; }; async function runBootstrap(accountId: string, roleName: string, region: string) { const credentials = await getCredentials(accountId, roleName, region) const env = { ...process.env, AWS_ACCESS_KEY_ID: credentials.accessKeyId, AWS_SECRET_ACCESS_KEY: credentials.secretAccessKey, AWS_SESSION_TOKEN: credentials.sessionToken } const multiline = [ `pnpm cdk bootstrap aws://${accountId}/${region}`, '--cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess', '--trust 1111111111111', // { const output = execSync('pnpm cdk version').toString(); for(const accountId in event.accountIds) { console.log(`Bootstrapping account ${accountId}`); await runBootstrap(accountId, "cdk-bootstrap-role", "eu-west-1"); console.log(`Bootstrapped account ${accountId}`); } return { statusCode: 200, body: JSON.stringify({ cdkVersion: output.trim(), accountIds: event.accountIds }), }; }; Deployment Let's define a lambda function in CDK using typescript. It's not a complete deletion of the CDK App. const function = new lambda.DockerImageFunction(this, 'CdkBootstrapLambda', { code: lambda.DockerImageCode.fromImageAsset( path.join(this._baseFolder, 'src/cdk_bootstrap') ), functionName: 'cdk-bootstrap-lambda', tracing: lambda.Tracing.ACTIVE, role: this.lambdaExecutionRole, //
In this article I will go over a cross-account CDK Bootstrapping from AWS Lambda. Bootstrapping is a mandatory step for further deployment of infrastructure defined with AWS CDK.
CDK Bootstrap creates a Cloudformation Stack (CDKToolkit
) which contains various resource. The template of this stack could be found here.
There is two ways to solve the problem:
Create a Cloudformation StackSet with the provided template. Read AWS Blog post (Bootstrapping multiple AWS accounts for AWS CDK using Stacksets).
Run
cdk bootstrap
whenever a new account is created.
The biggest advantage of running cdk bootstrap
in a lambda is that it's not needed to sync the template with the stackset.
Source code of the lambda
The lambda function receives an event with a list of accounts it must to login, and execute cdk bootstrap
Make sure that all permissions for cross-account access are set, and the lambda function can assume a role in the target account.
/src/bootstrap/index.ts
import { execSync } from 'child_process';
import { STSClient, AssumeRoleCommand } from "@aws-sdk/client-sts";
const getCredentials = async (accountId: string, roleName: string, region: string): Promise<any> => {
console.log(`Assuming role: ${roleName} in account: ${accountId}`);
const stsClient = new STSClient({ region: region });
const response = await stsClient.send(new AssumeRoleCommand({
RoleArn: `arn:aws:iam::${accountId}:role/${roleName}`, RoleSessionName: "CDKBootstrap"
}));
console.log(`Assumed role: ${roleName} in account: ${accountId}`);
return {
accessKeyId: response.Credentials?.AccessKeyId || '',
secretAccessKey: response.Credentials?.SecretAccessKey || '',
sessionToken: response.Credentials?.SessionToken || '',
};
};
async function runBootstrap(accountId: string, roleName: string, region: string) {
const credentials = await getCredentials(accountId, roleName, region)
const env = {
...process.env,
AWS_ACCESS_KEY_ID: credentials.accessKeyId,
AWS_SECRET_ACCESS_KEY: credentials.secretAccessKey,
AWS_SESSION_TOKEN: credentials.sessionToken
}
const multiline = [
`pnpm cdk bootstrap aws://${accountId}/${region}`,
'--cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess',
'--trust 1111111111111', // <- Replace with your account ID
];
const bootstrapCommand = multiline.join(' ');
const output = execSync(bootstrapCommand, { env: env }).toString();
// console.log(output);
}
export const handler = async (event: any): Promise<any> => {
const output = execSync('pnpm cdk version').toString();
for(const accountId in event.accountIds) {
console.log(`Bootstrapping account ${accountId}`);
await runBootstrap(accountId, "cdk-bootstrap-role", "eu-west-1");
console.log(`Bootstrapped account ${accountId}`);
}
return {
statusCode: 200,
body: JSON.stringify({ cdkVersion: output.trim(), accountIds: event.accountIds }),
};
};
Deployment
Let's define a lambda function in CDK using typescript. It's not a complete deletion of the CDK App.
const function = new lambda.DockerImageFunction(this, 'CdkBootstrapLambda', {
code: lambda.DockerImageCode.fromImageAsset(
path.join(this._baseFolder, 'src/cdk_bootstrap')
),
functionName: 'cdk-bootstrap-lambda',
tracing: lambda.Tracing.ACTIVE,
role: this.lambdaExecutionRole, // <--- role that allows to assume roles in a target account
environment: handlerEnvironmentParams,
memorySize: 512,
timeout: Duration.minutes(10),
currentVersionOptions: {
removalPolicy: RemovalPolicy.RETAIN,
}
});
As you can see, we use DockerImageFunction
for the lambda so that we can use cdk cli. Here is the Dockerfile
, and .dockerignore
to reduce size of the image.
/src/bootstrap/Dockerfile
FROM public.ecr.aws/lambda/nodejs:22 AS builder
WORKDIR /var/task
# Install pnpm
RUN npm install -g pnpm
# Copy package files and configs
COPY package.json pnpm-lock.yaml .npmrc tsconfig.json ./
COPY index.ts ./
# Install dependencies and build
RUN pnpm install --frozen-lockfile
RUN pnpm build
FROM public.ecr.aws/lambda/nodejs:22
WORKDIR /var/task
# Install pnpm
RUN npm install -g pnpm
# Copy package files and built assets
COPY --from=builder /var/task/dist ./dist
COPY package.json pnpm-lock.yaml .npmrc ./
# Install production dependencies only
RUN pnpm install --frozen-lockfile --prod
# Set the Lambda handler
CMD ["dist/index.handler"]
and
/src/bootstrap/.dockerignore
node_modules
npm-debug.log
dist
.git
.env
Last but not least is package.json
/src/bootstrap/package.json
{
"name": "cdk-bootstrap",
"description": "Lambda for cdk bootstrapping",
"version": "1.0.0",
"engines": {
"node": ">=20.0.0"
},
"scripts": {
"build": "pnpm clean && tsc",
"clean": "rimraf dist",
},
"dependencies": {
"@aws-sdk/client-sts": "^3.723.0",
"aws-cdk": "^2.174.1",
},
"devDependencies": {
"@types/aws-lambda": "^8.10.92",
"@types/jest": "^29.5.11",
"@types/node": "^20.0.0",
"rimraf": "^5.0.5",
"typescript": "^4.9.0"
}
}
I did not include any files related to project configuration, because it will be different for everyone.
I suggest checking out other articles to get the list of IAM permissions needed for the cdk bootstrapping: