Become Zero to Hero with AWS CDK

Bishwas JhaBishwas Jha
7 min read

What is AWS CDK?

The AWS Cloud Development Kit (CDK) is an open-source framework that lets you define and provision cloud infrastructure using familiar programming languages (TypeScript, Python, Java, .NET, etc.) instead of raw JSON or YAML CloudFormation templates.

Level of Constructs. what are they?
L1 constructs maps 1:1 to CloudFormation resources.
L2 constructs provide opinionated, higher-level abstractions with sensible defaults.
L3 (patterns) bundle multiple resources into a single, reusable component (e.g. a ECR-backed Fargate service with logging, auto-scaling, and alarms).

Why to use CDK?

  • Infrastructure as Code (IaC)

    • Imperative power: Use loops, functions, and abstractions instead of JSON/YAML snippets.

    • L1 vs. L2(Level 1 and Level 2): CDK’s L1 constructs mirror raw CloudFormation resources. L2 constructs provide sensible defaults and higher-level APIs.

    • Gaps in coverage: When your architecture spans multiple resources for example: an encrypted S3 bucket wired to a Lambda function, you bundle them into a single reusable construct. That prevents duplication and enforces your standards.

    • Use BluePrints: Blueprints take this a step further by allowing you to inject defaults into existing L2 constructs.

  • Polyglot support via jsii
    Under the hood, CDK’s core is written in TypeScript and compiled by jsii into libraries for Python, Java, and .NET, so you get first-class support in your language of choice.

  • Some more features that you cannot miss

  • CLI tooling
    cdk synth generates CloudFormation templates.
    cdk diff shows changes vs. deployed stacks.
    cdk deploy provisions or updates stacks.
    cdk destroy deletes stacks.

  • Extensibility
    Custom constructs let you encapsulate multi-resource patterns (e.g. an encrypted S3 bucket with Lambda event-handlers) into shareable libraries.
    Blueprints (via IPropertyInjector) let you enforce organizational standards (encryption, logging, tagging) across any L2 construct without modifying your app code.
    Aspects (e.g. cdk-nag) allow you to apply linting and policy checks at synth time.

  • Testing
    CDK apps can be unit-tested using the @aws-cdk/assertions library to validate that your stacks and constructs produce the cloud resources and configurations you expect.

Lets get our hands dirty!

Prerequisites

Before you begin, make sure you have:

  • Node.js (>=18.x) and npm installed

  • AWS CLI configured with credentials and a default region

  • Your local machine’s AWS credentials set up (~/.aws/credentials)


Step 1: Install and Bootstrap AWS CDK

1.1. Install the CDK CLI globally

npm install -g aws-cdk
npm -g install typescript

1.2. Verify your installation

cdk --version

1.3. Bootstrap your AWS environment (This is an one-time action per account/region)

cdk bootstrap aws://<ACCOUNT_ID>/<REGION>

This provisions the CDK Toolkit stack (S3 bucket, IAM roles) needed for deployments.

Step 2: Initialize a TypeScript CDK Project

2.1. Create a new directory & initialize

mkdir cdk-demo && cd cdk-demo
cdk init app --language typescript

2.2. Install core dependencies

npm install aws-cdk-lib constructs

Step 3: Building a Simple s3 bucket

3.1. Import and instantiate the bucket
Open the generated stack file (usually lib/cdk-demo-stack.ts) and modify it like so:

import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
// 1. Import the S3 Bucket construct
import { Bucket } from 'aws-cdk-lib/aws-s3';

export class CdkDemoStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    // 2. Create a simple S3 bucket
    new Bucket(this, 'MyFirstBucket', {
      // optional: you can set properties here, e.g.:
      // versioned: true,
      // encryption: BucketEncryption.S3_MANAGED,
    });
  }
}

3.2. Synthesize your template

cdk synth

You should see a CloudFormation template printed, with an AWS::S3::Bucket resource named MyFirstBucket.

3.3. Deploy to AWS

cdk deploy

Confirm the deploy prompt, and CDK will provision your bucket.

3.4. Verify
After deployment, you can visit the AWS S3 console and confirm that a bucket named something like cdk-demo-MyFirstBucket-XXXXXXXXX exists.

Step 4: Building a Custom Blueprint via Property Injectors

Blueprints hook into CDK’s tree by implementing IPropertyInjector. You can inject defaults into any L2 construct at instantiation time.

4.1. Create Your Library

mkdir custom-construct && cd custom-construct
cdk init lib --language typescript
mkdir lib/src cd lib/src/
touch s3.ts
npm install aws-cdk-lib constructs jsii

4.2. Secure S3 Bucket Injector: This customisation can be used with blueprints property injector. we wiil further discuss about these features in our future article but reference github link can be found here.

// lib/secure-bucket-injector.ts
import { IPropertyInjector, InjectionContext } from 'aws-cdk-lib';
import { Bucket, BucketProps, BlockPublicAccess } from 'aws-cdk-lib/aws-s3';

export class SecureBucketDefaults implements IPropertyInjector {
  public readonly constructUniqueId = Bucket.PROPERTY_INJECTION_ID;

  inject(originalProps: BucketProps, _ctx: InjectionContext): BucketProps {
    return {
      // Organizational security defaults
      blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
      enforceSSL: true,

      // Allow consumers to override
      ...originalProps,
    };
  }
}

4.2. Secure S3 Bucket module: Reference github link can be found here.

import * as cdk from 'aws-cdk-lib';
import * as s3 from 'aws-cdk-lib/aws-s3';
import { Construct } from 'constructs';

export interface SecureBucketInjectorProps {
  bucketName?: string;
  versioned?: boolean;
  encryption?: s3.BucketEncryption;
  blockPublicAccess?: s3.BlockPublicAccess;
  removalPolicy?: cdk.RemovalPolicy;
}

export class SecureBucketInjector extends Construct {
  public readonly bucket: s3.Bucket;

  constructor(scope: Construct, id: string, props: SecureBucketInjectorProps = {}) {
    super(scope, id);

    this.bucket = new s3.Bucket(this, 'SecureBucket', {
      bucketName: props.bucketName,
      versioned: props.versioned ?? true,
      encryption: props.encryption ?? s3.BucketEncryption.S3_MANAGED,
      blockPublicAccess: props.blockPublicAccess ?? s3.BlockPublicAccess.BLOCK_ALL,
      removalPolicy: props.removalPolicy ?? cdk.RemovalPolicy.DESTROY,
      enforceSSL: true,
    });
  }
}

4.3. Configure jsii

In your package.json add:

{
  "jsii": {
    "outdir": "dist",
    "targets": {
      "python": { "module": "cdk_blueprints_demo" },
    }
  }
}

4.4. Compile and run jsii:

npm run build      # compiles your .ts to .js
jsii               # emits bindings in dist/
npm publish        # share on npm

Step 6: Applying Blueprints in Your CDK App

Back in your app directory:

6.1. Install your blueprint package

npm install cdk-blueprints-demo

6.2. Wire in your injector

// bin/app.ts
import { App, Stack, PropertyInjectors } from 'aws-cdk-lib';
import { SecureBucketDefaults } from 'cdk-blueprints-demo';

const app = new App();
PropertyInjectors.of(app).add(new SecureBucketDefaults());

const stack = new Stack(app, 'MySecureStack');
// Every Bucket you instantiate below gets your defaults
new Bucket(stack, 'DataBucket', { bucketName: 'my-data' });

6.3. Synthesize & deploy

cdk synth
cdk deploy

Step 7: Composing Multiple Injectors

You can register as many injectors as you like, in registration order:

// lib/lambda-logging-injector.ts
import { IPropertyInjector } from 'aws-cdk-lib';
import { FunctionProps, Function } from 'aws-cdk-lib/aws-lambda';

export class LambdaLoggingDefaults implements IPropertyInjector {
  public readonly constructUniqueId = Function.PROPERTY_INJECTION_ID;
  inject(orig: FunctionProps) {
    return { logRetention: 14, ...orig };
  }
}

// lib/global-tag-injector.ts
import { IPropertyInjector } from 'aws-cdk-lib';

export class GlobalTagInjector implements IPropertyInjector {
  public readonly constructUniqueId = '*'; // wildcard for all constructs
  inject(orig: any) {
    return {
      tags: { Team: 'Platform', ...orig.tags },
      ...orig,
    };
  }
}

// In your app:
PropertyInjectors.of(app).add(
  new SecureBucketDefaults(),
  new LambdaLoggingDefaults(),
  new GlobalTagInjector(),
);

Tip: Defaults should be registered before consumer props; forced props go after.


Step 8: Avoiding Infinite Recursion

If your injector itself creates the same construct type, use the InjectionContext’s skipNestedInjection flag:

export class AccessLogInjector implements IPropertyInjector {
  public readonly constructUniqueId = Bucket.PROPERTY_INJECTION_ID;

  inject(orig: BucketProps, ctx: InjectionContext): BucketProps {
    if (ctx.skipNestedInjection) {
      return orig;
    }

    const logBucket = new Bucket(ctx.host, 'LogBucket', { /* ... */ });

    return {
      ...orig,
      serverAccessLogsBucket: logBucket,
      accessControl: BucketAccessControl.LOG_DELIVERY_WRITE,
      // prevent recursion
      _skipNestedInjection: true,
    };
  }
}

Step 9: Integrating cdk-nag for Policy Checks

Add cdk-nag to enforce AWS Solutions best practices at synth time:

npm install cdk-nag

In your app:

import { Aspects } from 'aws-cdk-lib';
import { AwsSolutionsChecks } from 'cdk-nag';

Aspects.of(app).add(new AwsSolutionsChecks());

Now, when you run cdk synth, cdk-nag will emit warnings or errors for any violations (e.g., public S3 buckets).


Step 10: Testing Your Constructs & Injectors

Use the CDK assertions library to write unit tests:

10.1 Install test dependencies

npm install --save-dev jest @aws-cdk/assertions

10.2 Example test

// test/secure-bucket.test.ts
import { App, Stack } from 'aws-cdk-lib';
import { Template } from 'aws-cdk-lib/assertions';
import { SecureBucketDefaults } from '../lib/secure-bucket-injector';
import { Bucket } from 'aws-cdk-lib/aws-s3';

test('SecureBucketDefaults enforces encryption & block public', () => {
  const app = new App({ propertyInjectors: [new SecureBucketDefaults()] });
  const stack = new Stack(app, 'TestStack');
  new Bucket(stack, 'TestBucket');

  const tpl = Template.fromStack(stack);
  tpl.hasResourceProperties('AWS::S3::Bucket', {
    BlockPublicAccessConfiguration: { BlockPublicAcls: true },
    BucketEncryption: { ServerSideEncryptionConfiguration: Match.anyValue() },
  });
});

10.3. Run tests

npm test

Conclusion

You now have a full end-to-end workflow:

  1. Install & bootstrap CDK

  2. Initialize your TypeScript app

  3. Author custom injectors via CDK Blueprints

  4. Leverage jsii for multi-language support

  5. Compose multiple injectors and guard against recursion

  6. Enforce standards with cdk-nag

  7. Validate via unit tests

With these patterns in place, your organisation’s security, naming, tagging, and logging standards are baked in to every CDK construct, while still letting individual teams override when and only when they need to. This gives a good segway for our next read topic that is creating custom cdk-nag.

0
Subscribe to my newsletter

Read articles from Bishwas Jha directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Bishwas Jha
Bishwas Jha

Hello, I am a Cloud Solutions Architect. I’ve worn many hats at work: DevOps Engineer, Cloud Engineer, Systems Engineer, and Cloud Solutions Architect. Before even starting with cloud, I was mostly writing code, and like any other dev, I’d blame the server for not keeping up with my code. Although, as an electronics engineer, I felt the real pain of keeping those servers up and running, but I was no different than any other dev. I started using cloud around a decade ago to host servers that were destined for physical data centers, and I was amazed at what the cloud could bring to the table. Fascinated with my exposure, I dived deep into the world of cloud, and now, after architecting, developing, and maintaining hundreds of AWS accounts, several applications, different stacks, and varied system designs, I still feel there’s so much more to get out of the cloud. Another field I believe can help devs keep up with multi-bug environments is using LLMs and machine learning, they truly have the capacity to automate mundane or boring tasks. When I’m not coding or "clouding" in the tech space, I love to write articles, try new tools, and simply let my thoughts float like a vibe coder sometimes. So far, I maintain the following on PyPI and VS Code Marketplace: python-maithili (PyPI) cdk-nag-extension (VS Code extension)