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

πŸ‘‹ While you are here

Reinvent your career. Join DEV.

It takes one minute and is worth it for your career.

Get started

Top comments (0)