5 min read

Using AWS CDK constructs with Pulumi

Kicking the tires on Pulumi's AWS CDK integration.
A photo of a pug dog wearing a denim jacket.
A pug wearing a jean jacket, because why not. (Photo by charlesdeluvio / Unsplash)

It may still be in beta, but Pulumi's AWS CDK interop certainly works — and it's neat. If you're a fan of CDK, or even if you'd just like to take advantage of one of the many CDK constructs out there on the internet, you'll be happy to learn that it lets you drop CDK constructs right into your Pulumi programs and work with them as though they were regular ol' Pulumi resources.

The blog post announcing the beta is a good place to learn more about how the two-way integration works, but the gist is that by wrapping CDK constructs in a class that inherits from a @pulumi/cdk Stack, you define the boundary between your CDK "stuff" and the rest of the Pulumi program, and expose properties on that class for whatever CDK-based resources you want to expose to the rest of the program. Deployments happen entirely with Pulumi, too — no CloudFormation in sight — so they're especially fast compared to what you might be used to with CDK and CloudFormation.

Here's a super-simple example showing two S3 buckets, one defined with CDK and the other with Pulumi, in the same program, just to give you an idea how things are typically set up structurally:

import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
import * as pulumicdk from "@pulumi/cdk";
import * as cdk from "aws-cdk-lib";

// The CDK stack declares and exposes an S3 bucket.
class CDKStack extends pulumicdk.Stack {
    bucket: pulumi.Output<string>;

    constructor(id: string, options?: pulumicdk.StackOptions) {
        super(id, options);

        // 1️⃣ Declare a bucket with CDK. 
        const bucket = new cdk.aws_s3.Bucket(this, "cdk-bucket", {
            accessControl: cdk.aws_s3.BucketAccessControl.PUBLIC_READ,

        // The stack exposes the bucket name as a public property.
        this.bucket = this.asOutput(bucket.bucketName);

        // The constructor runs CDK synth.

// Instantiate the CDKStack.
const cdkStack =  new CDKStack("cdk-stack");

// 2️⃣ Declare a bucket with @pulumi/aws.
const bucket = new aws.s3.Bucket("pulumi-bucket", {
    acl: aws.s3.PublicReadAcl,

// 3️⃣ Export both bucket names.
export const cdkBucketName = cdkStack.bucket;
export const pulumiBucketName = bucket.bucket;

Notice the bucket defined on the CDKStack class is typed as a pulumi.Output<string>, and it's set using the class's inherited .asOutput() method. That bit of Pulumi magic is what lets you use the computed property of the CDK Bucket instance (here, its name) as a Pulumi output — which in turn can be passed as an input to other Pulumi resources. (And the reverse is also true: You can pass outputs of Pulumi resources to CDK declarations as well.) For example, to add a file to the bucket we declared with CDK above, you'd add these few lines:

// ...

// Add a file to the CDK bucket.
const file = new aws.s3.BucketObject("some-file.txt", {
    bucket: cdkStack.bucket,  // ⬅️ Where the magic happens.
    acl: aws.s3.PublicReadAcl,
    content: "Hello, world!",

As of now, the integration is only supported in TypeScript, and to use it, you'll need to make sure your AWS account and target region are properly "bootstrapped" first. (CDK constructs require this.) But once you've done so, you should be able to integrate any CDK construct into your program using the same general approach.

Here's another example combining three resources — two declared with AWS CDK and one with Pulumi — to deploy a static website on AWS. The CDK subclass defines a bucket and CloudFront distribution, exposing the bucket and distribution on its public API as Pulumi outputs. The outer Pulumi program uses those outputs to upload files to the bucket using a Synced Folder component and then finishes by exporting the generated CloudFront distribution URL:

import * as pulumi from "@pulumi/pulumi";
import * as pulumicdk from "@pulumi/cdk";
import * as synced from "@pulumi/synced-folder";
import * as cdk from "aws-cdk-lib";

const config = new pulumi.Config();
const path = "./www";
const indexDocument = "index.html";
const errorDocument = "error.html";

// The CDK stack defines an S3 bucket and CloudFront distribution.
class CDKStack extends pulumicdk.Stack {
    bucketName: pulumi.Output<string>;
    originURL: pulumi.Output<string>;
    cdnURL: pulumi.Output<string>;

    constructor(id: string, options?: pulumicdk.StackOptions) {
        super(id, options);

        // Origin bucket.
        const bucket = new cdk.aws_s3.Bucket(this, "bucket", {
            websiteIndexDocument: indexDocument,
            websiteErrorDocument: errorDocument,
            accessControl: cdk.aws_s3.BucketAccessControl.PUBLIC_READ,

        // CloudFront distribution.
        const cdn = new cdk.aws_cloudfront.Distribution(this, "cdn", {
            defaultRootObject: indexDocument,
            defaultBehavior: {
                origin: new cdk.aws_cloudfront_origins.S3Origin(bucket),

        // Export the bucket name, bucket URL, and CDN URL as Pulumi outputs.
        this.bucketName = this.asOutput(bucket.bucketName);
        this.originURL = this.asOutput(bucket.bucketWebsiteUrl);
        this.cdnURL = this.asOutput(`https://${cdn.distributionDomainName}`);


const cdkStack =  new CDKStack("cdk-stack");

// Sync a local folder to the bucket exported by the CDK stack.
const folder = new synced.S3BucketFolder("folder", {
    bucketName: cdkStack.bucketName,
    path: path,
    acl: "public-read",

// Export the CloudFront URLs.
export const cdnURL = cdkStack.cdnURL;

When you deploy this program with Pulumi — assuming you've got a ./www folder alongside index.ts with a few web pages in it — you'll see that in a few minutes, the CloudFront CDN is deployed and the website's ready for browsing:

$ pulumi up
Updating (dev)

View Live: https://app.pulumi.com/cnunciato/cdk-test/dev/updates/1

     Type                                            Name                        Status
 +   pulumi:pulumi:Stack                             cdk-test-dev                created (267s)
 +   ├─ cdk:index:Stack                              cdk-stack                   created (0.38s)
 +   │  └─ cdk:construct:CDKStack                    cdk-stack/cdk-stack         created (0.51s)
 +   │     ├─ cdk:construct:Bucket                   cdk-stack/cdk-stack/bucket  created (0.40s)
 +   │     │  └─ aws-native:s3:Bucket                bucket43879c71              created (27s)
 +   │     └─ cdk:construct:Distribution             cdk-stack/cdk-stack/cdn     created (0.66s)
 +   │        └─ aws-native:cloudfront:Distribution  cdnE31FB0B1                 created (233s)
 +   └─ synced-folder:index:S3BucketFolder           folder                      created (0.76s)
 +      ├─ aws:s3:BucketObject                       index.html                  created (0.56s)
 +      └─ aws:s3:BucketObject                       error.html                  created (0.68s)

    cdnURL   : "https://d22v9g8qsaxm4x.cloudfront.net"

    + 10 created

Duration: 4m29s

Neat, right?

Now, I'll admit I haven't spent a ton of time with this stuff yet — and it is still in preview, so I wouldn't be surprised if there were still a few bugs to be found here and there. Also, because it's built on the AWS Native provider (which is itself built on the AWS Cloud Control API), not all AWS resources are available out of the box just yet, so you may need to "map in" a few resources here if you run into those gaps. (Which reminds me — I should probably add an example that covers this as well!) But so far, it definitely works as advertised, so I'm sure I'll be poking around on the Construct Hub in my free time in the weeks ahead.

Hope it helps! You'll find the full code for the example in the book's examples repository on GitHub:

examples/website/cdk-static-website at main · pulumibook/examples
Code snippets and examples from The Pulumi Book and companion website. - examples/website/cdk-static-website at main · pulumibook/examples

Have fun!