DEV Community

Cover image for Automating Cross-Account CDK Bootstrapping Using AWS Lambda
Max Kryvych
Max Kryvych

Posted on

Automating Cross-Account CDK Bootstrapping Using AWS Lambda

Introduction

In this article, we will explore how to perform cross-account AWS CDK Bootstrapping using AWS Lambda. Bootstrapping is a critical step when deploying infrastructure defined using AWS CDK, as it sets up the necessary resources for subsequent deployments.

What is CDK Bootstrapping?

CDK Bootstrapping involves creating a CloudFormation stack (CDKToolkit) that contains essential resources. The template for this stack can be found here.

There are two primary approaches to CDK Bootstrapping:

  1. Using CloudFormation StackSet: Create a StackSet with the provided template. Learn more in this AWS blog post.
  2. Using cdk bootstrap: Run the command directly whenever a new account is created.

Running cdk bootstrap in a Lambda function simplifies the process by eliminating the need to sync the template with a StackSet.


Lambda Function for CDK Bootstrapping

The Lambda function accepts an event containing a list of accounts to bootstrap. It logs into each account and executes the cdk bootstrap command.

Note: Ensure all required cross-account permissions are in place, and the Lambda function can assume a role in the target account.

Source Code

Here’s the implementation of the Lambda function:

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 }),
    };
};
Enter fullscreen mode Exit fullscreen mode

Deployment

Define the Lambda function in your CDK application using TypeScript. Below is the Lambda function definition:

stack.ts

const lambdaFunction = 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,
  }
});
Enter fullscreen mode Exit fullscreen mode

The DockerImageFunction enables the use of the CDK CLI. Below is the Dockerfile and .dockerignore for creating a lightweight Lambda image.
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"]
Enter fullscreen mode Exit fullscreen mode

.dockerignore

node_modules
npm-debug.log
dist
.git
.env
Enter fullscreen mode Exit fullscreen mode

Package Configuration

Below is a sample package.json for the Lambda function:

{
  "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"
  }
}
Enter fullscreen mode Exit fullscreen mode

Additional Resources

Top comments (0)