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, //

Jan 9, 2025 - 14:37
 0
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:

  1. Create a Cloudformation StackSet with the provided template. Read AWS Blog post (Bootstrapping multiple AWS accounts for AWS CDK using Stacksets).

  2. 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: