DE
Infrastructure as Code
Devops core v1.0.0
Infrastructure as Code
Overview
Infrastructure as Code (IaC) defines cloud resources — compute, networking, storage, databases — as version-controlled, declarative configuration files. IaC ensures reproducible environments, eliminates configuration drift, and enables infrastructure changes to flow through the same CI/CD pipeline as application code. Terraform is the default multi-cloud IaC tool; AWS CDK is used for AWS-native projects.
Key Concepts
IaC Tool Comparison
| Feature | Terraform | AWS CDK | CloudFormation | Pulumi |
|---|---|---|---|---|
| Language | HCL | TypeScript/Python/Java | YAML/JSON | TypeScript/Python/Go |
| Cloud Support | Multi-cloud | AWS only | AWS only | Multi-cloud |
| State | Remote (S3) | CloudFormation stack | CloudFormation stack | Pulumi Cloud |
| Maturity | Very high | High | High | Medium |
| Best For | Multi-cloud, modular | AWS-native, complex logic | AWS simple setups | Developers who prefer code |
Terraform Project Structure
infrastructure/
├── environments/
│ ├── dev/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── terraform.tfvars
│ ├── staging/
│ └── production/
├── modules/
│ ├── networking/ # VPC, subnets, security groups
│ ├── compute/ # ECS, EC2, Lambda
│ ├── database/ # RDS, ElastiCache
│ ├── messaging/ # SQS, SNS, MSK
│ └── monitoring/ # CloudWatch, alarms
├── backend.tf # Remote state configuration
└── versions.tf # Provider version constraints
Best Practices
- Use remote state — S3 + DynamoDB for Terraform state locking
- Modularize resources — Reusable modules for networking, compute, database
- Use workspaces or directories per environment — Separate dev/staging/prod state
- Pin provider versions —
required_providers { aws = { version = "~> 5.0" } } - Use variables and outputs — Never hardcode values; parameterize everything
- Plan before apply — Always review
terraform planoutput before applying - Tag all resources — Environment, team, cost-center, managed-by tags
- Enable drift detection — Scheduled
terraform planto detect manual changes
Code Examples
✅ Good: Terraform Module for ECS Service
# modules/compute/ecs-service/main.tf
resource "aws_ecs_service" "this" {
name = var.service_name
cluster = var.cluster_id
task_definition = aws_ecs_task_definition.this.arn
desired_count = var.desired_count
launch_type = "FARGATE"
network_configuration {
subnets = var.private_subnet_ids
security_groups = [aws_security_group.service.id]
}
load_balancer {
target_group_arn = aws_lb_target_group.this.arn
container_name = var.service_name
container_port = var.container_port
}
deployment_circuit_breaker {
enable = true
rollback = true
}
tags = merge(var.tags, {
Service = var.service_name
ManagedBy = "terraform"
})
}
resource "aws_ecs_task_definition" "this" {
family = var.service_name
requires_compatibilities = ["FARGATE"]
network_mode = "awsvpc"
cpu = var.cpu
memory = var.memory
execution_role_arn = var.execution_role_arn
task_role_arn = var.task_role_arn
container_definitions = jsonencode([{
name = var.service_name
image = var.container_image
essential = true
portMappings = [{
containerPort = var.container_port
protocol = "tcp"
}]
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = var.log_group_name
"awslogs-region" = var.aws_region
"awslogs-stream-prefix" = var.service_name
}
}
healthCheck = {
command = ["CMD-SHELL", "curl -f http://localhost:${var.container_port}/actuator/health || exit 1"]
interval = 30
timeout = 5
retries = 3
startPeriod = 60
}
environment = var.environment_variables
secrets = var.secrets
}])
}
# variables.tf
variable "service_name" {
type = string
description = "Name of the ECS service"
}
variable "container_image" {
type = string
description = "Docker image URI"
}
variable "desired_count" {
type = number
default = 2
description = "Number of tasks to run"
}
variable "tags" {
type = map(string)
default = {}
description = "Resource tags"
}
✅ Good: AWS CDK Stack
// lib/app-stack.ts
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 rds from 'aws-cdk-lib/aws-rds';
export class AppStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props: AppStackProps) {
super(scope, id, props);
const vpc = new ec2.Vpc(this, 'AppVpc', {
maxAzs: 2,
natGateways: 1,
});
const cluster = new ecs.Cluster(this, 'AppCluster', { vpc });
const database = new rds.DatabaseInstance(this, 'AppDb', {
engine: rds.DatabaseInstanceEngine.postgres({
version: rds.PostgresEngineVersion.VER_16,
}),
instanceType: ec2.InstanceType.of(
ec2.InstanceClass.T4G, ec2.InstanceSize.MEDIUM
),
vpc,
multiAz: props.isProduction,
deletionProtection: props.isProduction,
});
}
}
❌ Bad: IaC Anti-Patterns
# Hardcoded values everywhere
resource "aws_instance" "web" {
ami = "ami-abc123" # Hardcoded AMI
instance_type = "t2.micro" # No variable
# No tags, no security group, no state management
}
Anti-Patterns
- Manual resource creation — ClickOps in console creates untracked resources
- Local state —
terraform.tfstatein local filesystem; use S3 + DynamoDB - Hardcoded values — AMI IDs, subnets, credentials in code
- No modules — Copy-paste duplicated blocks instead of reusable modules
- Missing tags — Untagged resources impossible to track or cost-allocate
- Applying without plan —
terraform applywithout reviewing plan output
Testing Strategies
terraform validate— Syntax and configuration validationterraform plan— Preview changes before applytflint— Linting for Terraform best practicescheckov— Security policy-as-code scanningterratest— Go-based integration tests for Terraform modules