Infrastructure as Code: Managing Cloud Resources with Code
The old way: You need a VPC, some EC2 instances, and an S3 bucket. You log into AWS console, click around for an hour, create everything manually, and hope you remember what you did. The new way: You write code that defines your infrastructure. Run a command, and everything is created. Need to change something? Update the code. Need to recreate it? Run the code again. That’s Infrastructure as Code (IaC).
What is Infrastructure as Code?
Infrastructure as Code (IaC) means managing infrastructure (servers, networks, databases) using code and automation, rather than manual processes.
Think of it like this:
- Manual: Like building a house by telling workers what to do each day
- IaC: Like having a blueprint (code) that workers follow exactly
Benefits:
- Version Control - Track changes to infrastructure
- Reproducibility - Create identical environments
- Consistency - Same infrastructure every time
- Speed - Deploy in minutes, not hours
- Documentation - Code IS documentation
Terraform vs Pulumi
Terraform
- Uses HCL (HashiCorp Configuration Language)
- Declarative (you describe what you want)
- Very popular, huge ecosystem
- State management built-in
Pulumi
- Uses real programming languages (Python, TypeScript, Go)
- More flexible, can use loops, functions, etc.
- Modern, cloud-native
- Great for complex logic
For beginners: Start with Terraform (simpler syntax). Then learn Pulumi if you need more flexibility.
Getting Started with Terraform
Installation
1
2
3
4
5
6
7
# macOS
brew install terraform
# Linux
wget https://releases.hashicorp.com/terraform/1.6.0/terraform_1.6.0_linux_amd64.zip
unzip terraform_1.6.0_linux_amd64.zip
sudo mv terraform /usr/local/bin/
Your First Terraform File
Create main.tf:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# Configure AWS Provider
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
# Create an S3 bucket
resource "aws_s3_bucket" "compliance_reports" {
bucket = "compliance-reports-2025"
tags = {
Name = "Compliance Reports"
Environment = "Production"
}
}
# Enable encryption
resource "aws_s3_bucket_server_side_encryption_configuration" "compliance_reports" {
bucket = aws_s3_bucket.compliance_reports.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
# Block public access
resource "aws_s3_bucket_public_access_block" "compliance_reports" {
bucket = aws_s3_bucket.compliance_reports.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
Initialize and Apply
1
2
3
4
5
6
7
8
9
10
11
# Initialize Terraform (downloads providers)
terraform init
# See what will be created (dry run)
terraform plan
# Create the resources
terraform apply
# Destroy everything (cleanup)
terraform destroy
Building a Complete VPC with Terraform
Here’s a complete VPC setup:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# variables.tf
variable "vpc_cidr" {
description = "CIDR block for VPC"
type = string
default = "10.0.0.0/16"
}
variable "environment" {
description = "Environment name"
type = string
default = "production"
}
# main.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
# VPC
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.environment}-vpc"
}
}
# Internet Gateway
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.environment}-igw"
}
}
# Public Subnet
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
availability_zone = "us-east-1a"
map_public_ip_on_launch = true
tags = {
Name = "${var.environment}-public-subnet"
}
}
# Private Subnet
resource "aws_subnet" "private" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.2.0/24"
availability_zone = "us-east-1a"
tags = {
Name = "${var.environment}-private-subnet"
}
}
# NAT Gateway (needs Elastic IP)
resource "aws_eip" "nat" {
domain = "vpc"
tags = {
Name = "${var.environment}-nat-eip"
}
}
resource "aws_nat_gateway" "main" {
allocation_id = aws_eip.nat.id
subnet_id = aws_subnet.public.id
tags = {
Name = "${var.environment}-nat"
}
}
# Route Tables
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
tags = {
Name = "${var.environment}-public-rt"
}
}
resource "aws_route_table" "private" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.main.id
}
tags = {
Name = "${var.environment}-private-rt"
}
}
# Route Table Associations
resource "aws_route_table_association" "public" {
subnet_id = aws_subnet.public.id
route_table_id = aws_route_table.public.id
}
resource "aws_route_table_association" "private" {
subnet_id = aws_subnet.private.id
route_table_id = aws_route_table.private.id
}
Getting Started with Pulumi
Installation
1
2
3
4
5
# Install Pulumi CLI
curl -fsSL https://get.pulumi.com | sh
# Or with Homebrew
brew install pulumi
Your First Pulumi Program (Python)
Create __main__.py:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import pulumi
import pulumi_aws as aws
# Create S3 bucket
bucket = aws.s3.Bucket(
"compliance-reports",
bucket="compliance-reports-2025",
tags={
"Name": "Compliance Reports",
"Environment": "Production"
}
)
# Enable encryption
encryption = aws.s3.BucketServerSideEncryptionConfigurationV2(
"compliance-reports-encryption",
bucket=bucket.id,
rules=[aws.s3.BucketServerSideEncryptionConfigurationV2RuleArgs(
apply_server_side_encryption_by_default=aws.s3.BucketServerSideEncryptionConfigurationV2RuleApplyServerSideEncryptionByDefaultArgs(
sse_algorithm="AES256"
)
)]
)
# Block public access
public_access_block = aws.s3.BucketPublicAccessBlock(
"compliance-reports-pab",
bucket=bucket.id,
block_public_acls=True,
block_public_policy=True,
ignore_public_acls=True,
restrict_public_buckets=True
)
# Export bucket name
pulumi.export("bucket_name", bucket.id)
Run Pulumi
1
2
3
4
5
6
7
8
9
10
11
# Initialize (creates project)
pulumi new aws-python
# Preview changes
pulumi preview
# Deploy
pulumi up
# Destroy
pulumi destroy
Building VPC with Pulumi
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
import pulumi
import pulumi_aws as aws
config = pulumi.Config()
vpc_cidr = config.get("vpc_cidr") or "10.0.0.0/16"
environment = config.get("environment") or "production"
# VPC
vpc = aws.ec2.Vpc(
"main-vpc",
cidr_block=vpc_cidr,
enable_dns_hostnames=True,
enable_dns_support=True,
tags={
"Name": f"{environment}-vpc"
}
)
# Internet Gateway
igw = aws.ec2.InternetGateway(
"main-igw",
vpc_id=vpc.id,
tags={
"Name": f"{environment}-igw"
}
)
# Public Subnet
public_subnet = aws.ec2.Subnet(
"public-subnet",
vpc_id=vpc.id,
cidr_block="10.0.1.0/24",
availability_zone="us-east-1a",
map_public_ip_on_launch=True,
tags={
"Name": f"{environment}-public-subnet"
}
)
# Private Subnet
private_subnet = aws.ec2.Subnet(
"private-subnet",
vpc_id=vpc.id,
cidr_block="10.0.2.0/24",
availability_zone="us-east-1a",
tags={
"Name": f"{environment}-private-subnet"
}
)
# Elastic IP for NAT
eip = aws.ec2.Eip(
"nat-eip",
domain="vpc",
tags={
"Name": f"{environment}-nat-eip"
}
)
# NAT Gateway
nat_gw = aws.ec2.NatGateway(
"main-nat",
allocation_id=eip.id,
subnet_id=public_subnet.id,
tags={
"Name": f"{environment}-nat"
}
)
# Public Route Table
public_rt = aws.ec2.RouteTable(
"public-rt",
vpc_id=vpc.id,
routes=[
aws.ec2.RouteTableRouteArgs(
cidr_block="0.0.0.0/0",
gateway_id=igw.id
)
],
tags={
"Name": f"{environment}-public-rt"
}
)
# Private Route Table
private_rt = aws.ec2.RouteTable(
"private-rt",
vpc_id=vpc.id,
routes=[
aws.ec2.RouteTableRouteArgs(
cidr_block="0.0.0.0/0",
nat_gateway_id=nat_gw.id
)
],
tags={
"Name": f"{environment}-private-rt"
}
)
# Route Table Associations
public_rta = aws.ec2.RouteTableAssociation(
"public-rta",
subnet_id=public_subnet.id,
route_table_id=public_rt.id
)
private_rta = aws.ec2.RouteTableAssociation(
"private-rta",
subnet_id=private_subnet.id,
route_table_id=private_rt.id
)
# Export outputs
pulumi.export("vpc_id", vpc.id)
pulumi.export("public_subnet_id", public_subnet.id)
pulumi.export("private_subnet_id", private_subnet.id)
Security Best Practices
1. Use Remote State
Store Terraform state remotely (S3, not local):
1
2
3
4
5
6
7
8
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "compliance-tool/terraform.tfstate"
region = "us-east-1"
encrypt = true
}
}
2. Enable State Locking
Prevent concurrent modifications:
1
2
3
4
5
6
terraform {
backend "s3" {
# ... other config ...
dynamodb_table = "terraform-state-lock"
}
}
3. Use Variables, Not Hardcoded Values
Bad:
1
2
3
resource "aws_s3_bucket" "bucket" {
bucket = "my-hardcoded-bucket-name"
}
Good:
1
2
3
4
5
6
7
variable "bucket_name" {
type = string
}
resource "aws_s3_bucket" "bucket" {
bucket = var.bucket_name
}
4. Scan Infrastructure Code
Use tools like tfsec or checkov:
1
2
3
4
5
# Install tfsec
brew install tfsec
# Scan Terraform code
tfsec .
5. Use Modules
Reusable components:
1
2
3
4
5
6
module "vpc" {
source = "./modules/vpc"
vpc_cidr = "10.0.0.0/16"
environment = "production"
}
Key Takeaways
- IaC = Infrastructure as Code - Manage infrastructure with code
- Terraform = HCL - Declarative, popular
- Pulumi = Real Languages - More flexible
- Version Control - Track all changes
- Reproducible - Same infrastructure every time
- Secure State - Store state remotely, encrypted
- Scan Code - Use security scanning tools
Practice Exercise
Try this yourself:
- Install Terraform
- Create a simple S3 bucket with Terraform
- Add encryption and public access block
- Deploy it
- Modify it
- Destroy it
Resources to Learn More
What’s Next?
Now that you understand IaC, you’re ready to:
- Build complete infrastructure stacks
- Version control your infrastructure
- Automate deployments
Remember: Infrastructure as Code is about treating infrastructure like software. Version it, test it, review it!
💡 Pro Tip: Start small with Terraform. Create one resource (like an S3 bucket), understand how it works, then gradually build more complex infrastructure. Don’t try to recreate your entire AWS account on day one!
Ready to understand compliance? Check out our final post on Compliance Frameworks, where we’ll learn about SOC 2, ISO 27001, and NIST!