The most surprising thing about AWS CDK is that it’s not actually about writing AWS infrastructure code; it’s about writing applications that generate AWS infrastructure code.

Let’s see it in action. Imagine you want to create a simple S3 bucket. In the AWS console, you’d click around, choose a name, maybe set some permissions. With CDK, you write this:

import software.amazon.awscdk.core.Construct;
import software.amazon.awscdk.core.Stack;
import software.amazon.awscdk.core.StackProps;
import software.amazon.awscdk.services.s3.Bucket;
import software.amazon.awscdk.services.s3.BucketEncryption;

public class MyCdkStack extends Stack {
    public MyCdkStack(final Construct scope, final String id) {
        this(scope, id, null);
    }

    public MyCdkStack(final Construct scope, final String id, final StackProps props) {
        super(scope, id, props);

        new Bucket(this, "MyFirstBucket", BucketProps.builder()
            .bucketName("my-unique-cdk-bucket-name-12345") // Must be globally unique
            .encryption(BucketEncryption.S3_MANAGED)
            .versioned(true)
            .build());
    }
}

When you run cdk synth (synthesize), this Java code produces a CloudFormation template. Then, cdk deploy takes that template and tells AWS to create the bucket. You’re not directly commanding AWS; you’re writing a program that describes your desired AWS state, and CDK translates that description into CloudFormation.

This approach solves the problem of managing infrastructure as code in a way that feels more like application development. Instead of declarative JSON or YAML, you use a familiar programming language (Java, in this case) with its own logic, loops, and abstractions. You can define reusable components, abstract away common patterns, and even test your infrastructure definitions programmatically.

Internally, CDK uses "constructs." A construct is the basic building block. Bucket is a construct. MyCdkStack is also a construct (a "stack" construct). When you instantiate a construct, like new Bucket(...), you’re telling CDK to add its definition to the overall "tree" of your infrastructure. The Stack construct represents a single CloudFormation stack. The App construct (which you’d have in your main entry point, not shown here) is the root of this tree, and it orchestrates the synthesis of all the stacks it contains.

The exact levers you control are primarily the properties you pass to the constructs. For the Bucket construct, these include bucketName, encryption, versioned, removalPolicy (which dictates what happens to the bucket when you cdk destroy), and many more. You can also define custom constructs to encapsulate complex or repetitive infrastructure patterns. For example, you could create a DatabaseClusterConstruct that provisions not just a database instance but also its associated security groups, parameter groups, and read replicas, all with a single instantiation.

When you define a resource like a Bucket, you can pass in a RemovalPolicy. If you set removalPolicy to RemovalPolicy.DESTROY, then running cdk destroy will delete the S3 bucket. However, if the bucket is not empty, CloudFormation will fail to delete it, and you’ll have to manually empty it before cdk destroy can succeed. This is a safety mechanism, but it can be frustrating if you just want to nuke everything.

The next concept you’ll likely encounter is how to manage multiple stacks and their dependencies within a single CDK application.

Want structured learning?

Take the full Cdk course →