Andrew Ridgway bec1eaac87
Some checks failed
Test / test (push) Has been cancelled
first pass at the newspaper builder
2026-09-14 11:57:22 +10:00

6.7 KiB

AWS CDK Best Practices

Purpose

Guidelines for building maintainable, secure, and testable infrastructure using the AWS Cloud Development Kit. These practices apply to CDK v2 with TypeScript (the recommended language for most teams).

Construct Levels

L1 (Cfn Resources)

  • Direct CloudFormation resource wrappers (e.g., CfnBucket)
  • Use only when L2 constructs do not expose a needed property
  • Require manual configuration of all properties (no defaults)

L2 (Curated Constructs)

  • AWS-maintained constructs with sensible defaults (e.g., Bucket, Function, Table)
  • Include helper methods (e.g., bucket.grantRead(lambda))
  • Preferred for most use cases — they encode AWS best practices

L3 (Patterns)

  • Higher-level constructs combining multiple resources (e.g., LambdaRestApi)
  • Use when the pattern fits your needs exactly
  • Avoid if you need significant customization — drop down to L2 instead

Construct Design Patterns

Single Responsibility

Each custom construct should represent one logical unit (a service, a data pipeline stage, a monitoring stack). Do not create constructs that build unrelated resources.

Props Interface Pattern

export interface OrderServiceProps {
  readonly vpc: ec2.IVpc;
  readonly table: dynamodb.ITable;
  readonly environment: string;         // 'dev' | 'staging' | 'prod'
  readonly alarmTopic?: sns.ITopic;     // optional props use ?
}

export class OrderService extends Construct {
  public readonly api: apigateway.RestApi;  // expose outputs as public readonly

  constructor(scope: Construct, id: string, props: OrderServiceProps) {
    super(scope, id);
    // ...
  }
}

Rules

  • Accept dependencies via props (dependency injection), do not create shared resources inside constructs
  • Use interface types (IVpc, ITable) for props, not concrete types — enables cross-stack references
  • Expose outputs as public readonly properties for consuming constructs
  • Prefix optional props with documentation explaining the default behavior

Stack Organization

/infrastructure
  /bin
    app.ts              # CDK app entry point, environment configuration
  /lib
    /constructs         # Reusable L3 constructs
      order-service.ts
      monitoring.ts
    /stacks
      network-stack.ts      # VPC, subnets, security groups
      data-stack.ts         # DynamoDB, S3, RDS
      compute-stack.ts      # Lambda, ECS, API Gateway
      monitoring-stack.ts   # CloudWatch, alarms, dashboards
  /test
    order-service.test.ts
    data-stack.test.ts

Stack Separation Guidelines

  • Separate stacks by lifecycle: resources that change together should be in the same stack
  • Stateful resources (databases, S3 buckets) in separate stacks from stateless (Lambda, API Gateway)
  • Stateful stacks change rarely; stateless stacks deploy frequently
  • Use cross-stack references sparingly — they create deployment coupling

Environment-Aware Stacks

Pattern

// bin/app.ts
const app = new cdk.App();
const env = app.node.tryGetContext('env') || 'dev';

const config = {
  dev:     { instanceType: 't3.small', minCapacity: 1, maxCapacity: 2 },
  staging: { instanceType: 't3.medium', minCapacity: 2, maxCapacity: 4 },
  prod:    { instanceType: 't3.large', minCapacity: 3, maxCapacity: 10 },
}[env];

new ComputeStack(app, `ComputeStack-${env}`, {
  env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: 'us-east-1' },
  config,
});

Rules

  • Never hardcode account IDs or regions — use environment variables or context
  • Use the same code for all environments; parameterize differences through config
  • Production stacks must specify explicit env (account + region) — do not rely on defaults

CDK Testing

Assertion Tests (Fine-Grained)

test('DynamoDB table has encryption enabled', () => {
  const app = new cdk.App();
  const stack = new DataStack(app, 'TestStack');
  const template = Template.fromStack(stack);

  template.hasResourceProperties('AWS::DynamoDB::Table', {
    SSESpecification: {
      SSEEnabled: true,
    },
  });
});

Snapshot Tests (Regression Detection)

test('stack matches snapshot', () => {
  const app = new cdk.App();
  const stack = new DataStack(app, 'TestStack');
  const template = Template.fromStack(stack);
  expect(template.toJSON()).toMatchSnapshot();
});
  • Update snapshots intentionally (jest --updateSnapshot) after deliberate changes
  • Review snapshot diffs in pull requests — they show exactly what infrastructure changes

What to Test

  • Security properties: encryption enabled, public access blocked, least-privilege policies
  • Critical configuration: retention policies, backup settings, auto-scaling parameters
  • Resource counts: expected number of Lambda functions, tables, queues
  • Do NOT test CDK internals or CloudFormation implementation details

Security Defaults

Encryption

  • S3: encryption: s3.BucketEncryption.S3_MANAGED (minimum) or KMS for sensitive data
  • DynamoDB: encryption: dynamodb.TableEncryption.AWS_MANAGED (default) or customer-managed KMS
  • SQS: encryption: sqs.QueueEncryption.KMS for sensitive message content
  • EBS: Enable encryption by default in account settings

Access Control

  • S3: blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL (always, unless serving public static content)
  • Lambda: Use grant* methods instead of writing IAM policies manually
  • API Gateway: Add authorization on every route (IAM, Cognito, or Lambda authorizer)

Logging

  • S3: Enable access logging to a dedicated logging bucket
  • API Gateway: Enable access logging and execution logging
  • Lambda: Logs go to CloudWatch automatically; set retention (logRetention: logs.RetentionDays.ONE_MONTH)

Least Privilege

// Good: specific grant
table.grantReadData(lambdaFunction);

// Bad: overly broad
lambdaFunction.addToRolePolicy(new iam.PolicyStatement({
  actions: ['dynamodb:*'],
  resources: ['*'],
}));

CDK Aspects for Compliance

Purpose

Aspects visit every construct in the tree and can validate, warn, or modify resources.

class EncryptionChecker implements cdk.IAspect {
  public visit(node: IConstruct): void {
    if (node instanceof s3.CfnBucket) {
      if (!node.bucketEncryption) {
        Annotations.of(node).addError('S3 bucket must have encryption enabled');
      }
    }
  }
}

// Apply to the entire app
Aspects.of(app).add(new EncryptionChecker());

Common Compliance Aspects

  • Verify all S3 buckets have encryption and block public access
  • Verify all DynamoDB tables have point-in-time recovery enabled
  • Verify all Lambda functions have reserved concurrency set
  • Verify all security groups do not allow 0.0.0.0/0 ingress
  • Tag all resources with required cost allocation tags