Acumatica · Cdk

AWS CDK for Infrastructure as Code

AWS CDK for Infrastructure as Code is the work that turns a deploy into a system. The deployment is one moment; the system is the next 18 months of uptime, incidents, and.

John Kihiu12 min read

AWS CDK lets you define infrastructure in a real programming language — TypeScript, Python, Java, C#, Go — instead of hand-writing YAML or JSON templates. Under the hood it still synthesizes to CloudFormation, which means you get CloudFormation's mature deployment engine, drift detection, and rollback behavior, but you write the definitions as code with loops, conditionals, functions, and types instead of copy-pasted template blocks.

Constructs: the core abstraction

Everything in CDK is a construct — a reusable cloud component ranging from L1 (direct, one-to-one mappings to CloudFormation resources, prefixed Cfn), through L2 (higher-level, sensible-defaults wrappers around L1s — an S3 bucket construct that handles encryption and access defaults for you), to L3 (patterns composed from multiple L2 constructs, like a complete API-Gateway-plus-Lambda-plus-DynamoDB pattern). Most day-to-day CDK code uses L2 constructs; you drop to L1 only when you need a CloudFormation feature the L2 wrapper hasn't exposed yet.

TYPESCRIPT · L2 CONSTRUCT STACK
import * as cdk from 'aws-cdk-lib';
import { Bucket, BlockPublicAccess } from 'aws-cdk-lib/aws-s3';
import { Function, Runtime, Code } from 'aws-cdk-lib/aws-lambda';

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

    const bucket = new Bucket(this, 'UploadsBucket', {
      versioned: true,
      blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
      removalPolicy: cdk.RemovalPolicy.RETAIN,
    });

    const processor = new Function(this, 'ProcessorFn', {
      runtime: Runtime.NODEJS_20_X,
      handler: 'index.handler',
      code: Code.fromAsset('lambda/processor'),
      environment: { BUCKET_NAME: bucket.bucketName },
    });

    bucket.grantReadWrite(processor);
  }
}

The permission-grant pattern

The line bucket.grantReadWrite(processor) above is the pattern that makes CDK meaningfully less error-prone than writing IAM policies by hand: the construct generates a least-privilege IAM policy scoped to that specific bucket and function, rather than you hand-writing a policy document and hoping the resource ARNs and actions line up correctly. This grant pattern exists across most CDK constructs — DynamoDB tables, SQS queues, Secrets Manager secrets — and is one of the biggest practical reasons teams adopt CDK over raw CloudFormation.

Let grant methods write your IAM policies

Hand-written IAM policies are a common source of either overly broad permissions (a wildcard resource because getting the exact ARN right was fiddly) or broken deployments (a typo in an ARN). The grant* methods on CDK constructs compute the correct scoped policy from the actual resources involved — use them instead of writing policy JSON directly wherever a construct offers one.

Stacks, environments, and cdk diff

A CDK app is composed of one or more stacks, each of which maps to one CloudFormation stack and deploys independently. Before every deploy, cdk diff shows exactly what will change in the target environment — resources added, modified, or destroyed — which is the primary safety check against an unintended destructive change reaching production. Multi-environment setups typically parameterize the stack by environment (dev/staging/prod) and target different AWS accounts or regions per environment using CDK's environment context.

Testing infrastructure code

Because CDK stacks are real code, they can be unit tested with the CDK assertions library, checking that the synthesized CloudFormation template contains the resources and properties you expect — catching a missing encryption setting or an overly permissive security group rule before it ever reaches cdk deploy.

TYPESCRIPT · CDK ASSERTIONS TEST
import { Template } from 'aws-cdk-lib/assertions';
import { App } from 'aws-cdk-lib';
import { UploadsStack } from '../lib/uploads-stack';

test('bucket blocks public access', () => {
  const app = new App();
  const stack = new UploadsStack(app, 'TestStack');
  const template = Template.fromStack(stack);

  template.hasResourceProperties('AWS::S3::Bucket', {
    PublicAccessBlockConfiguration: {
      BlockPublicAcls: true,
      BlockPublicPolicy: true,
    },
  });
});

RemovalPolicy and the destroy-by-default trap

By default, CloudFormation (and therefore CDK) deletes stateful resources like S3 buckets and RDS databases when the stack is destroyed or the resource is removed from the stack definition, unless you explicitly set a retention policy. For anything holding data you can't regenerate, set removalPolicy: cdk.RemovalPolicy.RETAIN explicitly rather than relying on the default — a cdk destroy run against the wrong stack, or an accidental resource removal in a refactor, is a much less catastrophic mistake when retention is explicit.

Bootstrap resources are shared — treat cdk bootstrap carefully

CDK deploys need a one-time cdk bootstrap per account/region, which creates an S3 bucket and IAM roles CDK uses for asset staging and deployment. These bootstrap resources are shared across every CDK app deploying into that account and region — don't manually delete or modify them without understanding every stack that depends on them.

Wrapping up

CDK's value is writing infrastructure as testable, composable code that still synthesizes down to CloudFormation's proven deployment engine. Lean on L2 constructs and their grant methods for correctly-scoped IAM policies, run cdk diff before every deploy, and set explicit removal policies on anything stateful — the defaults are not what you want for data you can't regenerate.

John Kihiu
Acumatica ERP Developer · Laravel Engineer

Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.