Automating the entire infrastructure with Infrastructure as Code: CloudFormation, Terraform with a local S3 backend, serverless/SAM templates, and AWS CDK deployed to LocalStack.

In episode 9 you built an API Gateway with a dozen manual commands. Effective for learning, but not for teamwork: no audit trail, no review, and hard to reproduce. The solution is Infrastructure as Code (IaC) — infrastructure written as code that is versioned, reviewed, and deployed deterministically. Because LocalStack mimics AWS APIs, almost every IaC tool can target it without meaningful changes.
CloudFormation is AWS's built-in IaC: write a YAML template, deploy it as a stack, and let the emulator create all the resources. Let's start with a template containing an S3 bucket and a DynamoDB table:
AWSTemplateFormatVersion: "2010-09-09"
Resources:
DataBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: "data-bucket"
OrdersTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: "Orders"
AttributeDefinitions:
- AttributeName: "id"
AttributeType: "S"
KeySchema:
- AttributeName: "id"
KeyType: "HASH"
BillingMode: "PAY_PER_REQUEST"
Outputs:
BucketArn:
Value: !GetAtt DataBucket.Arnawslocal cloudformation create-stack \
--stack-name dev-stack --template-body file://stack.yml
awslocal cloudformation describe-stacks --stack-name dev-stack
awslocal s3 ls
awslocal dynamodb list-tablesAll the resources created automatically show up as separate services in LocalStack — the bucket and table genuinely work, not just as stack records.
CloudFormation's power lies in references between resources. Intrinsic functions like !Ref and !GetAtt flow one resource's output into another resource. This template fragment passes the bucket name into a Lambda environment variable:
AWSTemplateFormatVersion: "2010-09-09"
Resources:
DataBucket:
Type: AWS::S3::Bucket
Handler:
Type: AWS::Lambda::Function
Properties:
Runtime: python3.12
Handler: handler.handler
Role: arn:aws:iam::000000000000:role/lambda-role
Code:
ZipFile: |
def handler(event, context): return {"statusCode": 200}
Environment:
Variables:
BUCKET: !Ref DataBucketWarning
Templates with interpolation syntax like !Ref must always be inside an MDX fenced code block. Writing them in a plain paragraph breaks rendering — this applies to the entire episode.
Terraform manages infrastructure with the declarative HCL language. Point the AWS provider at LocalStack by overriding endpoints in the provider block:
provider "aws" {
region = "us-east-1"
access_key = "test"
secret_key = "test"
skip_credentials_validation = true
skip_metadata_api_check = true
endpoints {
s3 = "http://localhost:4566"
dynamodb = "http://localhost:4566"
}
}
resource "aws_s3_bucket" "data" {
bucket = "tf-data-bucket"
}The test/test credentials and the http://localhost:4566 endpoint are the only things that differ from production. Terraform state can be stored in an S3 backend that also runs on LocalStack:
terraform {
backend "s3" {
bucket = "tf-state"
key = "localstack/terraform.tfstate"
region = "us-east-1"
endpoint = "http://localhost:4566"
access_key = "test"
secret_key = "test"
skip_credentials_validation = true
skip_region_validation = true
skip_requesting_account_id = true
force_path_style = true
}
}awslocal s3 mb s3://tf-state
terraform init
terraform plan
terraform apply -auto-approve
terraform destroy -auto-approveAWS SAM simplifies serverless: a single Lambda function is written concisely, and SAM fills in the rest (permissions, event sources). A standard SAM template:
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Resources:
HelloFunction:
Type: AWS::Serverless::Function
Properties:
Runtime: python3.12
Handler: app.handler
InlineCode: |
def handler(event, context): return {"statusCode": 200, "body": "ok"}samlocal build
samlocal deploy --guided --capabilities CAPABILITY_IAMAWS CDK writes infrastructure in a programming language (TypeScript, Python, and others), then compiles it into CloudFormation:
import * as cdk from "aws-cdk-lib";
import * as s3 from "aws-cdk-lib/aws-s3";
import { Construct } from "constructs";
export class AppStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
new s3.Bucket(this, "DataBucket", { bucketName: "cdk-data-bucket" });
}
}
const app = new cdk.App();
new AppStack(app, "AppStack", { env: { region: "us-east-1", account: "000000000000" } });To target LocalStack, use cdklocal — the CDK CLI counterpart that points bootstrap and deploy at the emulator endpoint:
npm install -g aws-cdk-local aws-cdk
cdklocal bootstrap
cdklocal synth
cdklocal deploy
awslocal s3 lscdklocal bootstrap creates a CDK-specific staging bucket, then deploy creates all the resources. The synth output is a CloudFormation template — proof that CDK is an abstraction on top of CloudFormation.
Summary of this episode:
awslocal cloudformation create-stack.http://localhost:4566, store state in a local S3 backend.samlocal build then samlocal deploy translates a serverless template into a stack.cdklocal bootstrap then cdklocal deploy for programming-language-based infrastructure.!Ref may only be written inside fenced code blocks.Your infrastructure is now defined as code and reproducible at any time. In episode 11 we store and manage configuration and secrets with Secrets Manager and SSM Parameter Store. See you there!