Become Zero to Hero with AWS CDK


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 (viaIPropertyInjector
) 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:
Install & bootstrap CDK
Initialize your TypeScript app
Author custom injectors via CDK Blueprints
Leverage jsii for multi-language support
Compose multiple injectors and guard against recursion
Enforce standards with cdk-nag
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.
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)