open atlas
↑ Back to track
AWS, hands-on AWS · 06 · 01

Infrastructure as code on AWS: CloudFormation and CDK

CloudFormation is AWS-native declarative IaC: a template defines resources, a stack is a deployed instance, and change sets preview adds/modifies/replacements. CDK writes infra in a real language and synthesizes to CloudFormation — same stacks, change sets, drift underneath.

AWS Senior ◷ 20 min
Level
FoundationsJuniorMiddleSenior

At 2 a.m. an on-call engineer needs more storage on a production RDS instance, so they bump the size in the console — fast, done, back to bed. Two weeks later a routine deploy runs and CloudFormation, which still believes the database has the old size and a different engine version, computes a diff against the template, not against reality. One of the properties it wants to “correct” requires recreating the resource: the change set quietly lists the database under Replacement. The deploy executes, CloudFormation creates a brand-new empty instance, swings the connection, and deletes the old one. The post-incident timeline traced the data loss not to the deploy, but to that 2 a.m. console edit — drift that nobody detected — colliding with a property whose update behavior is Replacement. Infrastructure as code is supposed to prevent exactly this, and it does — but only if the code is the source of truth and nobody edits around it.

CloudFormation: the declarative core

CloudFormation is AWS’s native infrastructure-as-code service. You write a template — a YAML or JSON document — that declares the desired end state of your infrastructure. A template has a handful of top-level sections: Resources (the only required one — the actual AWS resources to create), Parameters (typed inputs you pass at deploy time), Mappings (static lookup tables, e.g. region to AMI), Conditions (booleans that gate whether a resource or property is included), and Outputs (values the stack exports, like a VPC ID another stack can import). You describe what you want; CloudFormation figures out the order, the dependencies, and the API calls.

A stack is a deployed instance of a template — the running collection of resources CloudFormation created and now manages as one unit. The relationship is exactly like class and object: one template can back many stacks (dev, staging, prod). Crucially, CloudFormation manages the state for you. There is no state file to store, lock, or corrupt — AWS tracks which real resources belong to which stack server-side. That is the single biggest operational difference from third-party IaC tools.

AWSTemplateFormatVersion: "2010-09-09"
Description: A bucket and a table, parameterized by environment.

Parameters:
  Environment:
    Type: String
    AllowedValues: [dev, prod]
    Default: dev

Conditions:
  IsProd: !Equals [!Ref Environment, prod]

Resources:
  AssetsBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub "myapp-assets-${Environment}"
      VersioningConfiguration:
        Status: !If [IsProd, Enabled, Suspended]

  SessionsTable:
    Type: AWS::DynamoDB::Table
    Properties:
      # Renaming a DynamoDB table requires REPLACEMENT — a new table, data gone.
      TableName: !Sub "sessions-${Environment}"
      BillingMode: PAY_PER_REQUEST
      AttributeDefinitions:
        - AttributeName: sessionId
          AttributeType: S
      KeySchema:
        - AttributeName: sessionId
          KeyType: HASH

Outputs:
  BucketName:
    Value: !Ref AssetsBucket
    Export:
      Name: !Sub "${Environment}-assets-bucket"

When you update a stack, CloudFormation compares the new template against the current stack and produces a change set — a preview of exactly what will happen before anything changes. The change set classifies each resource as add, modify, or remove, and for modifications it tells you the update behavior: No interruption (the resource is updated in place with no downtime), Some interruption (updated in place but briefly disrupted), or — the dangerous one — Replacement (CloudFormation creates a new resource, then deletes the old one). Replacement is property-specific: certain properties simply cannot be changed on a live resource, so changing them recreates it. For a database or a stateful store, Replacement can be destructive — the new resource is empty and the old one is deleted. The whole point of a change set is to surface that word Replacement before you execute, not after.

CloudFormation also rolls back on failure: if any resource in an update fails, it attempts to return every resource to its previous state. That safety net has a notorious failure mode of its own — UPDATE_ROLLBACK_FAILED, where the rollback itself can’t complete (often because a resource was changed out-of-band), and the stack is wedged until you manually fix or skip the offending resource.

Drift: when reality stops matching the template

The 2 a.m. console edit in the hook has a name: drift. CloudFormation defines drift as a stack’s actual configuration differing from its expected configuration as recorded in the template. Any change made outside CloudFormation — through the console, the CLI, or another tool, often called ClickOps — can introduce it. Drift detection compares each supported resource’s live property values against the template and reports a status: IN_SYNC when they match, MODIFIED when a property value differs, DELETED when the resource was removed out-of-band, and NOT_CHECKED for resource types that don’t support drift detection. A stack is DRIFTED if even one resource has drifted.

Drift is not just cosmetic. As the hook showed, the next deploy diffs against the template’s view of the world, so an out-of-band change can be silently reverted — or, worse, can interact with a Replacement property to destroy data. Mature teams run drift detection on a schedule and treat any DRIFTED stack as an incident, because it means the code is no longer the source of truth. The discipline that makes IaC valuable is not editing around it; drift detection is the alarm that tells you someone did.

For large systems you split templates: nested stacks (a parent stack whose resources are themselves child stacks) and cross-stack references (one stack’s Outputs exported and imported by another via Fn::ImportValue) let you compose infrastructure without one unmanageable 3,000-line file. Note one sharp edge: drift detection on a parent stack does not recurse into nested stacks — you detect drift on each nested stack directly.

CDK: a real language that compiles to CloudFormation

Hand-writing YAML gets painful fast: no loops, no real conditionals, no types, and a five-resource intent ballooning into hundreds of lines. The AWS Cloud Development Kit (CDK) is the answer — you define infrastructure in a general-purpose language (TypeScript, Python, Java, C#, Go) using constructs, the reusable building blocks. Constructs come in three levels: L1 (Cfn* classes) are raw one-to-one mappings of CloudFormation resources; L2 wrap them with sensible, secure defaults and helper methods (an s3.Bucket that wires up encryption and a removal policy for you); L3 are patterns that compose many resources into one opinionated unit (an ApplicationLoadBalancedFargateService that stands up a VPC, cluster, load balancer, and service together).

The mechanism is the key insight: cdk synth synthesizes your code into a CloudFormation template, and cdk deploy hands that template to CloudFormation to provision. CDK is a higher-level generator on top of CloudFormation, not a replacement for it. You still get stacks, change sets, rollback, and drift detection — because what actually runs is still a CloudFormation stack. A few dozen lines of CDK routinely emit a 500+ line template and 50+ resources.

import { Stack, StackProps, RemovalPolicy } from "aws-cdk-lib";
import { Construct } from "constructs";
import * as s3 from "aws-cdk-lib/aws-s3";
import * as dynamodb from "aws-cdk-lib/aws-dynamodb";

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

    const isProd = id.includes("prod");

    // L2 construct: sensible, secure defaults (encryption on by default).
    new s3.Bucket(this, "AssetsBucket", {
      versioned: isProd,                      // a real boolean, not a CFN Condition
      removalPolicy: isProd
        ? RemovalPolicy.RETAIN                // protect prod data on stack delete
        : RemovalPolicy.DESTROY,
    });

    new dynamodb.Table(this, "SessionsTable", {
      partitionKey: { name: "sessionId", type: dynamodb.AttributeType.STRING },
      billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
    });
  }
}
// `cdk synth` turns this into the CloudFormation template above (and more);
// `cdk deploy` runs it through CloudFormation as a stack.
Why this works

Why does “read the synth output” keep coming up? Because CDK’s convenience is also its risk: an L2 or L3 construct can quietly create more than you asked for — extra IAM policies, a NAT gateway that bills hourly, a log group with infinite retention. The CDK code reads like five lines of intent, but the deployed reality is whatever the generated template says, and that template is what CloudFormation executes. cdk synth and cdk diff are how you keep code and reality honest: you review the emitted CloudFormation, not just the TypeScript, the same way you would read a compiler’s output when the stakes are a production resource being replaced.

The tradeoff is real. CDK buys you loops, conditionals, type-checking, unit tests, and shareable abstractions — the full toolbox of a programming language applied to infrastructure. It costs you a build step, generated templates that can be large and hard to read, and the obligation to understand the CloudFormation it emits. Raw CloudFormation is the opposite: more verbose and less reusable, but utterly transparent — what you write is exactly what deploys, with no synthesis layer to reason about.

DimensionCloudFormation (YAML/JSON)CDK (TypeScript/Python…)
Authoring modelDeclarative template, what-you-write-is-what-deploysImperative code that synthesizes a template
Abstraction / reuseLow — copy-paste, nested stacks, no real loopsHigh — loops, conditionals, types, L2/L3 constructs
Build stepNone — deploy the file directlyRequired — cdk synth compiles to CFN
TransparencyTotal — the template is the artifactIndirect — must read synth output to know what deploys
Underlying engineCloudFormation: stacks, change sets, drift, rollbackSame — CDK deploys through CloudFormation
Pick the best fit

A product team of app developers (strong in TypeScript) must stand up repeatable per-environment infrastructure — VPC, ECS Fargate service, RDS, load balancer — across dev/staging/prod, reviewed in pull requests, with the same shape duplicated three times. Pick the approach.

Quiz

A teammate ran cdk deploy and asks where the Terraform-style state file is stored so they can lock it. What's the correct response?

Quiz

A change set for a stack update lists your production database under 'Replacement: True'. You execute it anyway. What most likely happens?

Recall before you leave
  1. 01
    Explain the relationship between a CloudFormation template, a stack, a change set, and drift — and where CDK fits.
  2. 02
    What are L1/L2/L3 constructs, and what do you trade by choosing CDK over hand-written CloudFormation?
Recap

Infrastructure as code on AWS has a native path with two faces. CloudFormation is the declarative core: a template — Resources, Parameters, Mappings, Conditions, Outputs — declares the desired end state, a stack is a deployed instance of that template, and CloudFormation manages the state for you with no state file to lock or corrupt. When you update, it computes a change set that previews every add, modify, and remove, labeling modifications No interruption, Some interruption, or Replacement; Replacement recreates the resource and deletes the old one, which for a database or stateful store destroys data, so the change set exists to surface that word before you execute. Drift detection catches the out-of-band, ClickOps changes that desync the template from reality — reporting IN_SYNC, MODIFIED, DELETED, or NOT_CHECKED — because a drifted stack means the code is no longer the source of truth and the next deploy can silently revert or destroy. CDK is a generator on top: you define infrastructure in a real language using L1 (raw), L2 (sensible defaults), and L3 (patterns) constructs, then cdk synth produces a CloudFormation template and cdk deploy runs it — so you keep stacks, change sets, rollback, and drift underneath, while gaining loops, types, and reuse at the cost of a build step and the duty to read the synth output. Choose CloudFormation for transparent AWS-native declarative infrastructure; choose CDK for programmatic abstraction when your team already writes application code — and in both cases, never edit around the code. Now when you see the word Replacement in a change set or a teammate proposes a console fix for “just this once,” you’ll know exactly which failure it sets up.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 5 done

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

Apply this

Put this lesson to work on a real build.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.