Dung (Donny) Nguyen

Senior Software Engineer

AWS CDK Full Life-Cycle Example

This article walks through a complete, end-to-end example of working with the AWS Cloud Development Kit (CDK)—from installing the tools to writing, deploying, updating, and finally tearing down a real application stack. The goal is to show how every phase of the CDK life-cycle fits together in a realistic workflow.

We will build a small serverless application: an API Gateway endpoint backed by a Lambda function that stores and retrieves items from a DynamoDB table.

1. Prerequisites

Before starting, make sure you have:

npm install -g aws-cdk
cdk --version

2. Initialize the Project

Create a new directory and scaffold a TypeScript CDK app:

mkdir my-serverless-app && cd my-serverless-app
cdk init app --language typescript

This generates a project structure:

my-serverless-app/
├── bin/
│   └── my-serverless-app.ts   # App entry point
├── lib/
│   └── my-serverless-app-stack.ts  # Your stack definition
├── test/
├── cdk.json                   # CDK configuration
├── package.json
└── tsconfig.json

Install the construct libraries used in this example:

npm install aws-cdk-lib constructs

3. Bootstrap the Environment

The CDK needs a one-time bootstrap per account/region. This provisions an S3 bucket and IAM roles the CDK uses to store assets and perform deployments:

cdk bootstrap aws://123456789012/us-east-1

4. Write the Lambda Handler

Create the function code at lambda/handler.ts:

import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import {
  DynamoDBDocumentClient,
  PutCommand,
  GetCommand,
} from '@aws-sdk/lib-dynamodb';

const client = DynamoDBDocumentClient.from(new DynamoDBClient({}));
const TABLE_NAME = process.env.TABLE_NAME!;

export const handler = async (event: any) => {
  if (event.httpMethod === 'POST') {
    const body = JSON.parse(event.body ?? '{}');
    await client.send(
      new PutCommand({ TableName: TABLE_NAME, Item: { id: body.id, data: body.data } }),
    );
    return { statusCode: 201, body: JSON.stringify({ message: 'Created' }) };
  }

  const id = event.queryStringParameters?.id;
  const result = await client.send(
    new GetCommand({ TableName: TABLE_NAME, Key: { id } }),
  );
  return { statusCode: 200, body: JSON.stringify(result.Item ?? {}) };
};

5. Define the Stack

Describe the infrastructure in lib/my-serverless-app-stack.ts:

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';

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

    // DynamoDB table
    const table = new dynamodb.Table(this, 'ItemsTable', {
      partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING },
      billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
      removalPolicy: cdk.RemovalPolicy.DESTROY, // For demo only
    });

    // Lambda function
    const fn = new NodejsFunction(this, 'ItemsHandler', {
      entry: 'lambda/handler.ts',
      handler: 'handler',
      environment: { TABLE_NAME: table.tableName },
    });

    // Grant the function read/write access to the table
    table.grantReadWriteData(fn);

    // API Gateway REST API backed by the Lambda
    const api = new apigateway.LambdaRestApi(this, 'ItemsApi', {
      handler: fn,
    });

    new cdk.CfnOutput(this, 'ApiUrl', { value: api.url });
  }
}

Notice how L2 constructs reduce boilerplate: table.grantReadWriteData(fn) automatically creates the correct IAM policy, and LambdaRestApi wires up the integration for you.

6. Synthesize the Template

Generate the CloudFormation template to inspect what will be deployed:

cdk synth

This outputs the synthesized CloudFormation (to the console and the cdk.out/ directory) without deploying anything—useful for review and for committing to version control.

7. Review Changes with a Diff

Before deploying, compare your code against what is currently deployed:

cdk diff

On the first deployment, everything shows as new (+). On later runs, cdk diff highlights exactly what will be added, modified, or removed, along with IAM and security-related changes.

8. Deploy the Stack

Deploy the application to AWS:

cdk deploy

The CDK prompts you to approve any security-sensitive changes (such as new IAM permissions), then creates the CloudFormation stack. When it finishes, the ApiUrl output is printed so you can test the endpoint:

# Create an item
curl -X POST "$API_URL" -d '{"id":"1","data":"hello"}'

# Retrieve it
curl "$API_URL?id=1"

9. Update the Stack

Infrastructure evolves. Suppose you want to enable point-in-time recovery on the table. Update the construct:

const table = new dynamodb.Table(this, 'ItemsTable', {
  partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING },
  billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
  pointInTimeRecovery: true, // New setting
  removalPolicy: cdk.RemovalPolicy.DESTROY,
});

Then run the diff-and-deploy cycle again:

cdk diff     # Shows only the changed property
cdk deploy   # Applies the update via a CloudFormation change set

CloudFormation performs the update in place, and rolls back automatically if anything fails.

10. Test and Iterate

During active development, you can speed up the inner loop with hotswap deployments, which bypass a full CloudFormation update for supported resources like Lambda code:

cdk deploy --hotswap   # Faster iteration; use in development only, not production

Because the CDK uses real programming languages, you can unit-test your infrastructure. Using Jest with the CDK assertions module:

import { Template } from 'aws-cdk-lib/assertions';
import * as cdk from 'aws-cdk-lib';
import { MyServerlessAppStack } from '../lib/my-serverless-app-stack';

test('creates a DynamoDB table', () => {
  const app = new cdk.App();
  const stack = new MyServerlessAppStack(app, 'TestStack');
  const template = Template.fromStack(stack);

  template.resourceCountIs('AWS::DynamoDB::Table', 1);
  template.hasResourceProperties('AWS::DynamoDB::Table', {
    BillingMode: 'PAY_PER_REQUEST',
  });
});

Run the tests:

npm test

12. Integrate into CI/CD

In a pipeline, the deployment steps are automated. A typical flow runs:

npm ci
npm test
cdk synth
cdk deploy --require-approval never

For more advanced setups, CDK Pipelines (aws-cdk-lib/pipelines) can define a self-mutating pipeline in code that builds, tests, and deploys your stacks across multiple environments.

13. Tear Down

When the environment is no longer needed, remove all resources to avoid ongoing charges:

cdk destroy

Because the demo table uses RemovalPolicy.DESTROY, it is deleted along with the stack. In production, you would typically use RETAIN for stateful resources to protect data.

Life-Cycle Summary

flowchart LR
    A[cdk init] --> B[cdk bootstrap]
    B --> C[Write constructs]
    C --> D[cdk synth]
    D --> E[cdk diff]
    E --> F[cdk deploy]
    F --> G[Update code]
    G --> D
    F --> H[cdk destroy]
Phase Command Purpose
Initialize cdk init Scaffold a new CDK project
Bootstrap cdk bootstrap Prepare the account/region for deployments
Synthesize cdk synth Generate the CloudFormation template
Preview cdk diff Review changes before deploying
Deploy cdk deploy Create or update the stack
Iterate cdk deploy --hotswap Fast development-time updates
Test npm test Validate infrastructure with assertions
Destroy cdk destroy Tear down all resources

Conclusion

This full life-cycle example shows how the AWS CDK supports every stage of infrastructure development—initializing a project, bootstrapping, writing constructs, synthesizing and reviewing changes, deploying, updating, testing, automating in CI/CD, and finally destroying resources. By treating infrastructure like application code, the CDK brings a familiar, iterative, and testable engineering workflow to provisioning cloud resources on AWS.