Infrastructure as Code: Managing Cloud Resources with Terraform and CDK
Clicking through cloud consoles to create resources is fine for learning. For production? It’s a disaster waiting to happen.
I’ve seen companies lose entire environments because nobody documented the manual steps. I’ve debugged “works in staging, fails in production” issues caused by configuration drift. And I’ve watched teams spend weeks trying to replicate their infrastructure in a new region.
Infrastructure as Code (IaC) solves all of this. Your infrastructure becomes versioned, testable, and reproducible. Let me show you how.
Why Infrastructure as Code
The Problems IaC Solves
Configuration Drift: Manual changes create differences between environments. IaC keeps environments identical.
Lack of Documentation: Code is documentation. Want to know what’s in production? Read the Terraform files.
Difficult Replication: Spinning up a new environment means running terraform apply
, not hours of console clicking.
No Version Control: Infrastructure changes are tracked in Git like code changes.
Disaster Recovery: Rebuild your entire infrastructure from scratch in minutes.
Terraform vs AWS CDK
Terraform
Pros:
- Cloud-agnostic (AWS, Azure, GCP, 100+ providers)
- Mature ecosystem
- Declarative syntax
- Large community
Cons:
- HCL is another language to learn
- State management can be tricky
- Limited type safety
AWS CDK
Pros:
- Use TypeScript/Python/Java (languages you know)
- Type safety and IDE support
- Higher-level abstractions
- Better for complex logic
Cons:
- AWS-only
- Generates CloudFormation (can be messy)
- Steeper learning curve
My Take: Use Terraform for multi-cloud or simple infrastructure. Use CDK for complex AWS-specific solutions.
Terraform Fundamentals
Basic Structure
# provider.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
backend "s3" {
bucket = "my-terraform-state"
key = "prod/terraform.tfstate"
region = "us-east-1"
}
}
provider "aws" {
region = var.aws_region
}
# variables.tf
variable "aws_region" {
description = "AWS region"
type = string
default = "us-east-1"
}
variable "environment" {
description = "Environment name"
type = string
}
# main.tf
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.environment}-vpc"
Environment = var.environment
}
}
resource "aws_subnet" "public" {
count = 2
vpc_id = aws_vpc.main.id
cidr_block = "10.0.${count.index}.0/24"
availability_zone = data.aws_availability_zones.available.names[count.index]
tags = {
Name = "${var.environment}-public-${count.index + 1}"
}
}
data "aws_availability_zones" "available" {
state = "available"
}
# outputs.tf
output "vpc_id" {
description = "ID of the VPC"
value = aws_vpc.main.id
}
output "public_subnet_ids" {
description = "IDs of public subnets"
value = aws_subnet.public[*].id
}
Modules for Reusability
# modules/web-app/main.tf
resource "aws_security_group" "alb" {
name = "${var.name}-alb-sg"
description = "Security group for ALB"
vpc_id = var.vpc_id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_lb" "main" {
name = "${var.name}-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets = var.subnet_ids
tags = {
Name = "${var.name}-alb"
}
}
resource "aws_lb_target_group" "app" {
name = "${var.name}-tg"
port = var.app_port
protocol = "HTTP"
vpc_id = var.vpc_id
health_check {
enabled = true
path = "/health"
healthy_threshold = 2
unhealthy_threshold = 3
timeout = 5
interval = 30
}
}
# modules/web-app/variables.tf
variable "name" {
description = "Name of the application"
type = string
}
variable "vpc_id" {
description = "VPC ID"
type = string
}
variable "subnet_ids" {
description = "Subnet IDs for ALB"
type = list(string)
}
variable "app_port" {
description = "Application port"
type = number
default = 3000
}
# Using the module
module "web_app" {
source = "./modules/web-app"
name = "myapp"
vpc_id = aws_vpc.main.id
subnet_ids = aws_subnet.public[*].id
app_port = 3000
}
Managing Multiple Environments
# environments/prod/main.tf
module "infrastructure" {
source = "../../modules/infrastructure"
environment = "prod"
instance_type = "t3.large"
min_size = 3
max_size = 10
db_instance_class = "db.r6g.xlarge"
}
# environments/staging/main.tf
module "infrastructure" {
source = "../../modules/infrastructure"
environment = "staging"
instance_type = "t3.small"
min_size = 1
max_size = 3
db_instance_class = "db.t3.medium"
}
Remote State and Locking
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "prod/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-locks"
}
}
# Create S3 bucket and DynamoDB table for state
resource "aws_s3_bucket" "terraform_state" {
bucket = "my-terraform-state"
lifecycle {
prevent_destroy = true
}
}
resource "aws_s3_bucket_versioning" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_dynamodb_table" "terraform_locks" {
name = "terraform-locks"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
}
AWS CDK (TypeScript)
Basic Stack
import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as ecs_patterns from 'aws-cdk-lib/aws-ecs-patterns';
import { Construct } from 'constructs';
export class WebAppStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// VPC
const vpc = new ec2.Vpc(this, 'VPC', {
maxAzs: 2,
natGateways: 1,
});
// ECS Cluster
const cluster = new ecs.Cluster(this, 'Cluster', {
vpc,
containerInsights: true,
});
// Fargate Service with ALB
const fargateService = new ecs_patterns.ApplicationLoadBalancedFargateService(
this,
'FargateService',
{
cluster,
taskImageOptions: {
image: ecs.ContainerImage.fromRegistry('my-app:latest'),
containerPort: 3000,
environment: {
NODE_ENV: 'production',
},
},
desiredCount: 2,
cpu: 512,
memoryLimitMiB: 1024,
}
);
// Auto Scaling
const scaling = fargateService.service.autoScaleTaskCount({
minCapacity: 2,
maxCapacity: 10,
});
scaling.scaleOnCpuUtilization('CpuScaling', {
targetUtilizationPercent: 70,
});
// Output
new cdk.CfnOutput(this, 'LoadBalancerDNS', {
value: fargateService.loadBalancer.loadBalancerDnsName,
});
}
}
Reusable Constructs
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as rds from 'aws-cdk-lib/aws-rds';
export interface DatabaseProps {
vpc: ec2.IVpc;
instanceType?: ec2.InstanceType;
multiAz?: boolean;
}
export class Database extends Construct {
public readonly instance: rds.DatabaseInstance;
constructor(scope: Construct, id: string, props: DatabaseProps) {
super(scope, id);
// Security Group
const securityGroup = new ec2.SecurityGroup(this, 'SecurityGroup', {
vpc: props.vpc,
description: 'Database security group',
});
// Database Instance
this.instance = new rds.DatabaseInstance(this, 'Instance', {
engine: rds.DatabaseInstanceEngine.postgres({
version: rds.PostgresEngineVersion.VER_15,
}),
instanceType: props.instanceType || ec2.InstanceType.of(
ec2.InstanceClass.T3,
ec2.InstanceSize.MICRO
),
vpc: props.vpc,
vpcSubnets: {
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
},
securityGroups: [securityGroup],
multiAz: props.multiAz ?? false,
allocatedStorage: 20,
maxAllocatedStorage: 100,
backupRetention: cdk.Duration.days(7),
deletionProtection: true,
});
}
public allowConnectionsFrom(other: ec2.IConnectable): void {
this.instance.connections.allowDefaultPortFrom(other);
}
}
// Usage
const database = new Database(this, 'Database', {
vpc,
multiAz: true,
});
database.allowConnectionsFrom(fargateService.service);
Environment-Specific Configuration
// bin/app.ts
import * as cdk from 'aws-cdk-lib';
import { WebAppStack } from '../lib/web-app-stack';
const app = new cdk.App();
// Production
new WebAppStack(app, 'WebApp-Prod', {
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: 'us-east-1',
},
config: {
environment: 'prod',
desiredCount: 3,
instanceType: ec2.InstanceType.of(
ec2.InstanceClass.T3,
ec2.InstanceSize.LARGE
),
},
});
// Staging
new WebAppStack(app, 'WebApp-Staging', {
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: 'us-east-1',
},
config: {
environment: 'staging',
desiredCount: 1,
instanceType: ec2.InstanceType.of(
ec2.InstanceClass.T3,
ec2.InstanceSize.SMALL
),
},
});
Advanced Patterns
Blue-Green Deployments
import * as codedeploy from 'aws-cdk-lib/aws-codedeploy';
const deploymentGroup = new codedeploy.EcsDeploymentGroup(
this,
'BlueGreenDeployment',
{
service: fargateService.service,
blueGreenDeploymentConfig: {
blueTargetGroup: fargateService.targetGroup,
greenTargetGroup: greenTargetGroup,
listener: fargateService.listener,
testListener: testListener,
},
deploymentConfig: codedeploy.EcsDeploymentConfig.CANARY_10PERCENT_5MINUTES,
}
);
Multi-Region Deployment
# terraform
provider "aws" {
alias = "us_east"
region = "us-east-1"
}
provider "aws" {
alias = "eu_west"
region = "eu-west-1"
}
module "us_infrastructure" {
source = "./modules/infrastructure"
providers = {
aws = aws.us_east
}
}
module "eu_infrastructure" {
source = "./modules/infrastructure"
providers = {
aws = aws.eu_west
}
}
# Route53 for global routing
resource "aws_route53_record" "global" {
zone_id = aws_route53_zone.main.zone_id
name = "app.example.com"
type = "A"
set_identifier = "US"
latency_routing_policy {
region = "us-east-1"
}
alias {
name = module.us_infrastructure.alb_dns
zone_id = module.us_infrastructure.alb_zone_id
evaluate_target_health = true
}
}
Secrets Management
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
// Create secret
const dbSecret = new secretsmanager.Secret(this, 'DBSecret', {
generateSecretString: {
secretStringTemplate: JSON.stringify({ username: 'admin' }),
generateStringKey: 'password',
excludePunctuation: true,
},
});
// Use in RDS
const database = new rds.DatabaseInstance(this, 'Database', {
credentials: rds.Credentials.fromSecret(dbSecret),
// ...
});
// Use in ECS
const taskDefinition = new ecs.FargateTaskDefinition(this, 'Task');
const container = taskDefinition.addContainer('app', {
image: ecs.ContainerImage.fromRegistry('my-app'),
secrets: {
DB_PASSWORD: ecs.Secret.fromSecretsManager(dbSecret, 'password'),
},
});
Testing Infrastructure
Terraform Validation
# Validate syntax
terraform validate
# Format code
terraform fmt -recursive
# Plan changes
terraform plan -out=plan.tfplan
# Show plan in readable format
terraform show plan.tfplan
Terraform Testing with Terratest
package test
import (
"testing"
"github.com/gruntwork-io/terratest/modules/terraform"
"github.com/stretchr/testify/assert"
)
func TestVPCCreation(t *testing.T) {
terraformOptions := &terraform.Options{
TerraformDir: "../",
Vars: map[string]interface{}{
"environment": "test",
},
}
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
vpcID := terraform.Output(t, terraformOptions, "vpc_id")
assert.NotEmpty(t, vpcID)
}
CDK Testing
import { Template } from 'aws-cdk-lib/assertions';
import { WebAppStack } from '../lib/web-app-stack';
test('VPC Created', () => {
const app = new cdk.App();
const stack = new WebAppStack(app, 'TestStack');
const template = Template.fromStack(stack);
template.hasResourceProperties('AWS::EC2::VPC', {
CidrBlock: '10.0.0.0/16',
});
});
test('Fargate Service Has Auto Scaling', () => {
const app = new cdk.App();
const stack = new WebAppStack(app, 'TestStack');
const template = Template.fromStack(stack);
template.resourceCountIs('AWS::ApplicationAutoScaling::ScalingPolicy', 1);
});
CI/CD for Infrastructure
GitHub Actions for Terraform
name: Terraform
on:
push:
branches: [main]
paths:
- 'terraform/**'
pull_request:
paths:
- 'terraform/**'
jobs:
terraform:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: hashicorp/setup-terraform@v2
with:
terraform_version: 1.6.0
- name: Terraform Init
run: terraform init
working-directory: ./terraform
- name: Terraform Format
run: terraform fmt -check
working-directory: ./terraform
- name: Terraform Validate
run: terraform validate
working-directory: ./terraform
- name: Terraform Plan
run: terraform plan -out=plan.tfplan
working-directory: ./terraform
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Terraform Apply
if: github.ref == 'refs/heads/main'
run: terraform apply plan.tfplan
working-directory: ./terraform
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
GitHub Actions for CDK
name: CDK
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '20'
- run: npm ci
- name: CDK Diff
run: npx cdk diff
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: CDK Deploy
if: github.ref == 'refs/heads/main'
run: npx cdk deploy --all --require-approval never
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
Cost Optimization
Tagging Strategy
locals {
common_tags = {
Environment = var.environment
ManagedBy = "Terraform"
Project = var.project_name
CostCenter = var.cost_center
}
}
resource "aws_instance" "web" {
# ...
tags = merge(
local.common_tags,
{
Name = "${var.environment}-web"
Role = "web-server"
}
)
}
Resource Scheduling
# Auto Scaling Schedule for Dev Environments
resource "aws_autoscaling_schedule" "scale_down_evening" {
count = var.environment == "dev" ? 1 : 0
scheduled_action_name = "scale-down-evening"
min_size = 0
max_size = 0
desired_capacity = 0
recurrence = "0 18 * * MON-FRI" # 6 PM weekdays
autoscaling_group_name = aws_autoscaling_group.main.name
}
resource "aws_autoscaling_schedule" "scale_up_morning" {
count = var.environment == "dev" ? 1 : 0
scheduled_action_name = "scale-up-morning"
min_size = 1
max_size = 3
desired_capacity = 1
recurrence = "0 8 * * MON-FRI" # 8 AM weekdays
autoscaling_group_name = aws_autoscaling_group.main.name
}
Best Practices
State Management
Never commit state files:
# .gitignore
*.tfstate
*.tfstate.*
.terraform/
Always use remote state:
terraform {
backend "s3" {
# ...
}
}
Module Versioning
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.1.2" # Pin version
# ...
}
Sensitive Data
variable "db_password" {
type = string
sensitive = true
}
output "db_endpoint" {
value = aws_db_instance.main.endpoint
sensitive = false # OK to expose
}
output "db_password" {
value = var.db_password
sensitive = true # Hide in logs
}
Common Pitfalls
Hardcoded Values
# Bad
resource "aws_instance" "web" {
ami = "ami-12345"
instance_type = "t3.micro"
}
# Good
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"] # Canonical
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
}
}
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type
}
Circular Dependencies
# Causes issues
resource "aws_security_group" "app" {
ingress {
security_groups = [aws_security_group.db.id]
}
}
resource "aws_security_group" "db" {
ingress {
security_groups = [aws_security_group.app.id]
}
}
# Fix with security group rules
resource "aws_security_group" "app" {}
resource "aws_security_group" "db" {}
resource "aws_security_group_rule" "app_to_db" {
type = "ingress"
from_port = 5432
to_port = 5432
protocol = "tcp"
source_security_group_id = aws_security_group.app.id
security_group_id = aws_security_group.db.id
}
Conclusion
Infrastructure as Code transforms infrastructure management from risky manual work to safe, automated deployments.
Choose Terraform for multi-cloud, simple infrastructure, or when you need cloud-agnostic configuration.
Choose CDK for complex AWS infrastructure where programming logic adds value.
Start small. Automate one environment. Add testing. Expand to other environments. Soon manual infrastructure changes will feel as outdated as FTPing files to production.
Need help implementing Infrastructure as Code? VooStack specializes in cloud infrastructure automation with Terraform and AWS CDK. Let’s discuss your infrastructure needs.