Introduction to Infrastructure as Code
Infrastructure as Code (IaC) has revolutionized how we design, deploy, and manage cloud infrastructure. Rather than manually configuring servers, networks, and services through web consoles or command-line interfaces, IaC allows us to define infrastructure using code that can be version-controlled, tested, and automated.
Terraform, developed by HashiCorp, has emerged as the leading IaC tool, supporting over 3,000 providers and enabling teams to manage infrastructure across multiple cloud platforms using a single, declarative language called HCL (HashiCorp Configuration Language).
Traditional infrastructure management leads to configuration drift, inconsistent environments, and manual errors. IaC eliminates these issues by treating infrastructure like software, bringing software engineering practices to infrastructure management.
Core Benefits of Infrastructure as Code
Consistency & Reproducibility
Eliminate configuration drift and ensure identical environments across development, staging, and production.
Speed & Efficiency
Provision complex infrastructure in minutes rather than days or weeks of manual configuration.
Risk Reduction
Version control, peer reviews, and automated testing reduce human error and increase reliability.
Cost Optimization
Easily tear down unused resources and implement cost-conscious infrastructure patterns.
Terraform Core Concepts and Fundamentals
Understanding Terraform's core concepts is essential for building scalable and maintainable infrastructure. Let's explore the fundamental building blocks that make Terraform so powerful.
The Terraform Workflow
The iterative process of infrastructure development and management
1. Configuration Files (.tf)
Terraform uses HCL (HashiCorp Configuration Language) to define infrastructure resources in .tf
files. Here's a basic example:
# Configure the AWS Provider
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = var.aws_region
}
# Define a VPC
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
}
}
# Create public subnet
resource "aws_subnet" "public" {
count = length(var.availability_zones)
vpc_id = aws_vpc.main.id
cidr_block = "10.0.${count.index + 1}.0/24"
availability_zone = var.availability_zones[count.index]
map_public_ip_on_launch = true
tags = {
Name = "${var.environment}-public-subnet-${count.index + 1}"
Type = "Public"
}
}
2. State Management
Terraform maintains a state file that tracks the current state of your infrastructure. This is critical for Terraform to know what resources it manages and their current configuration.
State files contain sensitive information and should never be stored in version control. Always use remote state backends like S3, Azure Storage, or Terraform Cloud for production environments.
# Configure remote state backend
terraform {
backend "s3" {
bucket = "my-terraform-state-bucket"
key = "infrastructure/terraform.tfstate"
region = "us-west-2"
encrypt = true
dynamodb_table = "terraform-state-locks"
# Enable versioning for state file recovery
versioning = true
}
}
3. Variables and Outputs
Variables make your Terraform configurations flexible and reusable, while outputs provide information about the created resources.
# Input Variables
variable "environment" {
description = "Environment name (dev, staging, prod)"
type = string
default = "dev"
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod."
}
}
variable "aws_region" {
description = "AWS region for resources"
type = string
default = "us-west-2"
}
variable "availability_zones" {
description = "List of availability zones"
type = list(string)
default = ["us-west-2a", "us-west-2b", "us-west-2c"]
}
# Local values for computed configurations
locals {
common_tags = {
Environment = var.environment
ManagedBy = "Terraform"
Project = "Infrastructure-Demo"
CreatedDate = timestamp()
}
}
# Output Values
output "vpc_id" {
description = "ID of the VPC"
value = aws_vpc.main.id
}
output "vpc_cidr_block" {
description = "CIDR block of the VPC"
value = aws_vpc.main.cidr_block
}
output "public_subnet_ids" {
description = "List of IDs of the public subnets"
value = aws_subnet.public[*].id
}
output "environment_info" {
description = "Environment configuration summary"
value = {
environment = var.environment
region = var.aws_region
vpc_id = aws_vpc.main.id
subnets = length(aws_subnet.public)
}
}
Terraform Best Practices and Patterns
Following established best practices ensures your Terraform code is maintainable, secure, and scalable. Here are the essential patterns every Terraform practitioner should implement.
1. Module-Driven Architecture
Modules are the key to creating reusable, maintainable Terraform code. They allow you to encapsulate related resources and create abstractions that can be shared across projects.
project/ βββ modules/ β βββ networking/ β β βββ main.tf β β βββ variables.tf β β βββ outputs.tf β βββ compute/ β β βββ main.tf β β βββ variables.tf β β βββ outputs.tf β βββ database/ β βββ main.tf β βββ variables.tf β βββ outputs.tf βββ environments/ βββ dev/ βββ staging/ βββ prod/
# Networking Module
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = var.enable_dns_hostnames
enable_dns_support = var.enable_dns_support
tags = merge(var.common_tags, {
Name = "${var.name_prefix}-vpc"
})
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = merge(var.common_tags, {
Name = "${var.name_prefix}-igw"
})
}
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 = merge(var.common_tags, {
Name = "${var.name_prefix}-public-rt"
Type = "Public"
})
}
resource "aws_subnet" "public" {
count = length(var.public_subnet_cidrs)
vpc_id = aws_vpc.main.id
cidr_block = var.public_subnet_cidrs[count.index]
availability_zone = var.availability_zones[count.index]
map_public_ip_on_launch = true
tags = merge(var.common_tags, {
Name = "${var.name_prefix}-public-subnet-${count.index + 1}"
Type = "Public"
})
}
resource "aws_route_table_association" "public" {
count = length(aws_subnet.public)
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}
2. State Management Best Practices
3. Security and Compliance Patterns
Always implement security controls at the infrastructure level. It's much easier to build secure infrastructure from the start than to retrofit security later.
# Security Group with least privilege principle
resource "aws_security_group" "web_server" {
name_prefix = "${var.name_prefix}-web-"
description = "Security group for web servers"
vpc_id = var.vpc_id
# Only allow specific inbound traffic
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
security_groups = [aws_security_group.load_balancer.id]
description = "HTTPS from load balancer"
}
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
security_groups = [aws_security_group.load_balancer.id]
description = "HTTP from load balancer (redirect to HTTPS)"
}
# Minimal outbound access
egress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "HTTPS outbound"
}
tags = merge(var.common_tags, {
Name = "${var.name_prefix}-web-sg"
})
}
# WAF for application protection
resource "aws_wafv2_web_acl" "main" {
name = "${var.name_prefix}-waf"
scope = "REGIONAL"
default_action {
allow {}
}
# Rate limiting rule
rule {
name = "RateLimitRule"
priority = 1
action {
block {}
}
statement {
rate_based_statement {
limit = 2000
aggregate_key_type = "IP"
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "RateLimitRule"
sampled_requests_enabled = true
}
}
# AWS Managed Rules for common attacks
rule {
name = "AWSManagedRulesCommonRuleSet"
priority = 2
override_action {
none {}
}
statement {
managed_rule_group_statement {
name = "AWSManagedRulesCommonRuleSet"
vendor_name = "AWS"
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "CommonRuleSetMetric"
sampled_requests_enabled = true
}
}
tags = var.common_tags
}
Multi-Cloud and Hybrid Cloud Strategies
Modern enterprises increasingly adopt multi-cloud and hybrid cloud strategies to avoid vendor lock-in, improve reliability, and optimize costs. Terraform's provider ecosystem makes it the ideal tool for managing infrastructure across multiple cloud platforms.
Multi-Cloud Architecture Patterns
βββββββββββββββββββ¬ββββββββββββββββββ¬ββββββββββββββββββ β AWS Region β Azure Region β GCP Region β βββββββββββββββββββΌββββββββββββββββββΌββββββββββββββββββ€ β β’ Compute β β’ App Service β β’ GKE β β β’ RDS β β’ SQL Database β β’ Cloud SQL β β β’ S3 β β’ Blob Storage β β’ Cloud Store β βββββββββββββββββββ΄ββββββββββββββββββ΄ββββββββββββββββββ β β β βββββββββββββββββΌββββββββββββββββ β βββββββββββββββββββ β Terraform State β β & Configuration β βββββββββββββββββββ
# Multi-cloud provider configuration
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.0"
}
google = {
source = "hashicorp/google"
version = "~> 4.0"
}
}
}
# AWS Provider Configuration
provider "aws" {
region = var.aws_region
default_tags {
tags = local.common_tags
}
}
# Azure Provider Configuration
provider "azurerm" {
features {}
subscription_id = var.azure_subscription_id
}
# Google Cloud Provider Configuration
provider "google" {
project = var.gcp_project_id
region = var.gcp_region
}
# AWS Infrastructure
module "aws_infrastructure" {
source = "./modules/aws"
environment = var.environment
region = var.aws_region
# Pass common configuration
common_tags = local.common_tags
}
# Azure Infrastructure
module "azure_infrastructure" {
source = "./modules/azure"
environment = var.environment
location = var.azure_location
resource_group_name = "${var.environment}-rg"
# Pass common configuration
common_tags = local.common_tags
}
# Google Cloud Infrastructure
module "gcp_infrastructure" {
source = "./modules/gcp"
environment = var.environment
project_id = var.gcp_project_id
region = var.gcp_region
# Pass common configuration
labels = local.common_tags
}
Cross-Cloud Networking
Implementing secure connectivity between cloud providers requires careful planning and implementation of VPN connections, private connectivity, and DNS resolution.
# AWS VPN Gateway for cross-cloud connectivity
resource "aws_vpn_gateway" "main" {
vpc_id = module.aws_infrastructure.vpc_id
amazon_side_asn = 64512
tags = merge(local.common_tags, {
Name = "${var.environment}-aws-vpn-gateway"
})
}
# Azure VPN Gateway
resource "azurerm_virtual_network_gateway" "main" {
name = "${var.environment}-azure-vpn-gateway"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
type = "Vpn"
vpn_type = "RouteBased"
sku = "VpnGw2"
ip_configuration {
name = "vnetGatewayConfig"
public_ip_address_id = azurerm_public_ip.vpn_gateway.id
private_ip_address_allocation = "Dynamic"
subnet_id = azurerm_subnet.gateway.id
}
tags = local.common_tags
}
# Site-to-Site VPN Connection
resource "aws_vpn_connection" "aws_to_azure" {
vpn_gateway_id = aws_vpn_gateway.main.id
customer_gateway_id = aws_customer_gateway.azure.id
type = "ipsec.1"
static_routes_only = true
tags = merge(local.common_tags, {
Name = "${var.environment}-aws-to-azure-vpn"
})
}
# DNS Resolution across clouds
resource "aws_route53_zone" "private" {
name = "${var.environment}.internal"
vpc {
vpc_id = module.aws_infrastructure.vpc_id
}
tags = merge(local.common_tags, {
Name = "${var.environment}-private-dns"
})
}
Practical Implementation Examples
Let's explore comprehensive, real-world examples that demonstrate Terraform's capabilities in building production-ready infrastructure.
Example 1: High-Availability Web Application
This example shows how to build a scalable, highly available web application infrastructure with load balancing, auto-scaling, and database replication.
# Application Load Balancer
resource "aws_lb" "main" {
name = "${var.environment}-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets = var.public_subnet_ids
enable_deletion_protection = var.environment == "prod"
tags = merge(var.common_tags, {
Name = "${var.environment}-alb"
})
}
resource "aws_lb_target_group" "web" {
name = "${var.environment}-web-tg"
port = 80
protocol = "HTTP"
vpc_id = var.vpc_id
health_check {
enabled = true
healthy_threshold = 2
interval = 30
matcher = "200"
path = "/health"
port = "traffic-port"
protocol = "HTTP"
timeout = 5
unhealthy_threshold = 2
}
tags = var.common_tags
}
# Auto Scaling Group
resource "aws_launch_template" "web" {
name_prefix = "${var.environment}-web-"
image_id = data.aws_ami.amazon_linux.id
instance_type = var.instance_type
key_name = var.key_pair_name
vpc_security_group_ids = [aws_security_group.web.id]
user_data = base64encode(templatefile("${path.module}/user-data.sh", {
environment = var.environment
db_endpoint = aws_db_instance.main.endpoint
}))
tag_specifications {
resource_type = "instance"
tags = merge(var.common_tags, {
Name = "${var.environment}-web-server"
})
}
lifecycle {
create_before_destroy = true
}
}
resource "aws_autoscaling_group" "web" {
name = "${var.environment}-web-asg"
vpc_zone_identifier = var.private_subnet_ids
target_group_arns = [aws_lb_target_group.web.arn]
health_check_type = "ELB"
health_check_grace_period = 300
min_size = var.min_capacity
max_size = var.max_capacity
desired_capacity = var.desired_capacity
launch_template {
id = aws_launch_template.web.id
version = "$Latest"
}
# Auto Scaling policies
enabled_metrics = [
"GroupMinSize",
"GroupMaxSize",
"GroupDesiredCapacity",
"GroupInServiceInstances",
"GroupTotalInstances"
]
tag {
key = "Name"
value = "${var.environment}-web-asg"
propagate_at_launch = false
}
dynamic "tag" {
for_each = var.common_tags
content {
key = tag.key
value = tag.value
propagate_at_launch = true
}
}
}
# RDS Database with Multi-AZ
resource "aws_db_instance" "main" {
identifier = "${var.environment}-database"
engine = "mysql"
engine_version = "8.0"
instance_class = var.db_instance_class
allocated_storage = var.db_allocated_storage
max_allocated_storage = var.db_max_allocated_storage
storage_type = "gp2"
storage_encrypted = true
db_name = var.db_name
username = var.db_username
password = var.db_password
vpc_security_group_ids = [aws_security_group.rds.id]
db_subnet_group_name = aws_db_subnet_group.main.name
# High Availability
multi_az = var.environment == "prod"
backup_retention_period = var.environment == "prod" ? 30 : 7
backup_window = "03:00-04:00"
maintenance_window = "sun:04:00-sun:05:00"
# Security
deletion_protection = var.environment == "prod"
skip_final_snapshot = var.environment != "prod"
tags = merge(var.common_tags, {
Name = "${var.environment}-database"
})
}
Example 2: Kubernetes Cluster with Terraform
Managing Kubernetes infrastructure with Terraform provides excellent control over cluster configuration and integrates well with existing infrastructure.
# EKS Cluster
resource "aws_eks_cluster" "main" {
name = "${var.environment}-eks-cluster"
role_arn = aws_iam_role.eks_cluster.arn
version = var.kubernetes_version
vpc_config {
subnet_ids = concat(var.private_subnet_ids, var.public_subnet_ids)
endpoint_private_access = true
endpoint_public_access = true
public_access_cidrs = var.cluster_endpoint_public_access_cidrs
}
# Enable logging
enabled_cluster_log_types = ["api", "audit", "authenticator", "controllerManager", "scheduler"]
# Encryption at rest
encryption_config {
resources = ["secrets"]
provider {
key_id = aws_kms_key.eks.arn
}
}
depends_on = [
aws_iam_role_policy_attachment.eks_cluster_policy,
aws_iam_role_policy_attachment.eks_service_policy,
]
tags = var.common_tags
}
# Node Groups
resource "aws_eks_node_group" "main" {
cluster_name = aws_eks_cluster.main.name
node_group_name = "${var.environment}-eks-nodes"
node_role_arn = aws_iam_role.eks_node_group.arn
subnet_ids = var.private_subnet_ids
# Instance configuration
capacity_type = var.node_capacity_type
instance_types = var.node_instance_types
ami_type = var.node_ami_type
disk_size = var.node_disk_size
# Scaling configuration
scaling_config {
desired_size = var.node_desired_size
max_size = var.node_max_size
min_size = var.node_min_size
}
# Update configuration
update_config {
max_unavailable_percentage = 25
}
# Launch template for advanced configuration
launch_template {
name = aws_launch_template.eks_nodes.name
version = aws_launch_template.eks_nodes.latest_version
}
depends_on = [
aws_iam_role_policy_attachment.eks_worker_node_policy,
aws_iam_role_policy_attachment.eks_cni_policy,
aws_iam_role_policy_attachment.eks_container_registry_policy,
]
tags = var.common_tags
}
# Launch template for EKS nodes
resource "aws_launch_template" "eks_nodes" {
name_prefix = "${var.environment}-eks-nodes-"
block_device_mappings {
device_name = "/dev/xvda"
ebs {
volume_size = var.node_disk_size
volume_type = "gp3"
encrypted = true
}
}
metadata_options {
http_endpoint = "enabled"
http_tokens = "required"
http_put_response_hop_limit = 2
}
monitoring {
enabled = true
}
network_interfaces {
associate_public_ip_address = false
delete_on_termination = true
security_groups = [aws_security_group.eks_nodes.id]
}
user_data = base64encode(templatefile("${path.module}/eks-node-userdata.sh", {
cluster_name = aws_eks_cluster.main.name
endpoint = aws_eks_cluster.main.endpoint
ca_data = aws_eks_cluster.main.certificate_authority[0].data
}))
tag_specifications {
resource_type = "instance"
tags = merge(var.common_tags, {
Name = "${var.environment}-eks-node"
})
}
lifecycle {
create_before_destroy = true
}
}
# KMS key for EKS encryption
resource "aws_kms_key" "eks" {
description = "EKS Secret Encryption Key"
tags = merge(var.common_tags, {
Name = "${var.environment}-eks-kms-key"
})
}
resource "aws_kms_alias" "eks" {
name = "alias/${var.environment}-eks-key"
target_key_id = aws_kms_key.eks.key_id
}
CI/CD Integration and Automation
Integrating Terraform with CI/CD pipelines enables automated infrastructure deployment, testing, and management. This section covers best practices for implementing Terraform in automated workflows.
GitOps Workflow for Infrastructure
Automated infrastructure deployment with human oversight
name: 'Terraform Infrastructure Pipeline'
on:
push:
branches:
- main
- develop
paths:
- 'infrastructure/**'
pull_request:
branches:
- main
paths:
- 'infrastructure/**'
env:
TF_VERSION: '1.5.0'
AWS_REGION: 'us-west-2'
jobs:
terraform-plan:
name: 'Terraform Plan'
runs-on: ubuntu-latest
strategy:
matrix:
environment: [dev, staging, prod]
defaults:
run:
working-directory: infrastructure/environments/${{ matrix.environment }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
cli_config_credentials_token: ${{ secrets.TF_API_TOKEN }}
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Terraform Format Check
id: fmt
run: terraform fmt -check -recursive
continue-on-error: true
- name: Terraform Init
id: init
run: terraform init
- name: Terraform Validate
id: validate
run: terraform validate
- name: Terraform Plan
id: plan
run: |
terraform plan -detailed-exitcode -out=tfplan
echo "exitcode=$?" >> $GITHUB_OUTPUT
continue-on-error: true
- name: Security Scan with Checkov
uses: bridgecrewio/checkov-action@master
with:
directory: infrastructure/
quiet: true
soft_fail: true
framework: terraform
- name: Cost Estimation
uses: infracost/actions/setup@v2
with:
api-key: ${{ secrets.INFRACOST_API_KEY }}
- name: Generate cost estimate
run: |
infracost breakdown --path . \
--format json \
--out-file /tmp/infracost-base.json
- name: Post plan results
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const botComment = comments.find(comment => {
return comment.user.type === 'Bot' && comment.body.includes('Terraform Plan Results');
});
const output = `### Terraform Plan Results for ${{ matrix.environment }}
#### Terraform Format and Style π\`${{ steps.fmt.outcome }}\`
#### Terraform Initialization βοΈ\`${{ steps.init.outcome }}\`
#### Terraform Validation π€\`${{ steps.validate.outcome }}\`
#### Terraform Plan π\`${{ steps.plan.outcome }}\`
Show Plan
\`\`\`terraform
${{ steps.plan.outputs.stdout }}
\`\`\`
*Pusher: @${{ github.actor }}, Action: \`${{ github.event_name }}\`*`;
if (botComment) {
github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: output
});
} else {
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: output
});
}
terraform-apply:
name: 'Terraform Apply'
needs: terraform-plan
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
strategy:
matrix:
environment: [dev, staging]
environment:
name: ${{ matrix.environment }}
defaults:
run:
working-directory: infrastructure/environments/${{ matrix.environment }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Terraform Init
run: terraform init
- name: Terraform Apply
run: terraform apply -auto-approve
- name: Update deployment status
if: always()
run: |
if [ "${{ job.status }}" == "success" ]; then
echo "β
Deployment to ${{ matrix.environment }} successful"
else
echo "β Deployment to ${{ matrix.environment }} failed"
fi
terraform-prod-apply:
name: 'Terraform Apply to Production'
needs: terraform-plan
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
environment:
name: production
url: https://prod.example.com
defaults:
run:
working-directory: infrastructure/environments/prod
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID_PROD }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY_PROD }}
aws-region: ${{ env.AWS_REGION }}
- name: Terraform Init
run: terraform init
- name: Terraform Apply
run: terraform apply -auto-approve
- name: Post-deployment tests
run: |
# Run infrastructure validation tests
./scripts/validate-infrastructure.sh
# Run health checks
./scripts/health-check.sh
Testing Infrastructure Code
Infrastructure testing is crucial for maintaining reliability and catching issues before they reach production.
package test
import (
"fmt"
"testing"
"time"
"github.com/gruntwork-io/terratest/modules/aws"
"github.com/gruntwork-io/terratest/modules/http-helper"
"github.com/gruntwork-io/terratest/modules/terraform"
"github.com/stretchr/testify/assert"
)
func TestTerraformWebAppInfrastructure(t *testing.T) {
// Configure Terraform options
terraformOptions := &terraform.Options{
TerraformDir: "../examples/web-app",
Vars: map[string]interface{}{
"environment": "test",
"aws_region": "us-west-2",
"instance_type": "t3.micro",
"min_capacity": 1,
"max_capacity": 2,
"desired_capacity": 1,
},
}
// Clean up resources after test
defer terraform.Destroy(t, terraformOptions)
// Deploy infrastructure
terraform.InitAndApply(t, terraformOptions)
// Validate outputs
vpcId := terraform.Output(t, terraformOptions, "vpc_id")
albDnsName := terraform.Output(t, terraformOptions, "alb_dns_name")
assert.NotEmpty(t, vpcId)
assert.NotEmpty(t, albDnsName)
// Test VPC exists and is properly configured
vpc := aws.GetVpcById(t, vpcId, "us-west-2")
assert.Equal(t, "10.0.0.0/16", *vpc.CidrBlock)
// Test load balancer is accessible
url := fmt.Sprintf("http://%s", albDnsName)
http_helper.HttpGetWithRetry(t, url, nil, 200, "healthy", 30, 5*time.Second)
// Test auto-scaling group is properly configured
asgName := terraform.Output(t, terraformOptions, "asg_name")
asg := aws.GetAutoScalingGroup(t, asgName, "us-west-2")
assert.Equal(t, int64(1), *asg.MinSize)
assert.Equal(t, int64(2), *asg.MaxSize)
assert.Equal(t, int64(1), *asg.DesiredCapacity)
// Test security groups have proper rules
webSgId := terraform.Output(t, terraformOptions, "web_security_group_id")
webSg := aws.GetSecurityGroupById(t, webSgId, "us-west-2")
// Check that web security group only allows traffic from ALB
assert.Len(t, webSg.IpPermissions, 2) // HTTP and HTTPS
}
func TestTerraformEKSCluster(t *testing.T) {
terraformOptions := &terraform.Options{
TerraformDir: "../examples/eks-cluster",
Vars: map[string]interface{}{
"environment": "test",
"aws_region": "us-west-2",
"kubernetes_version": "1.27",
"node_instance_types": []string{"t3.medium"},
"node_desired_size": 2,
},
}
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
clusterName := terraform.Output(t, terraformOptions, "cluster_name")
clusterEndpoint := terraform.Output(t, terraformOptions, "cluster_endpoint")
assert.NotEmpty(t, clusterName)
assert.NotEmpty(t, clusterEndpoint)
// Validate cluster is accessible and healthy
cluster := aws.GetEksCluster(t, "us-west-2", clusterName)
assert.Equal(t, "ACTIVE", *cluster.Status)
assert.Equal(t, "1.27", *cluster.Version)
}
Security Considerations and Compliance
Security must be built into your Terraform practices from day one. This section covers essential security patterns, compliance frameworks, and tools for maintaining secure infrastructure.
Security Scanning and Policy as Code
# Policy as Code with Sentinel (Terraform Cloud/Enterprise)
policy "enforce-https-alb" {
enforcement_level = "hard-mandatory"
}
policy "require-encryption" {
enforcement_level = "hard-mandatory"
}
policy "restrict-instance-types" {
enforcement_level = "soft-mandatory"
}
# Open Policy Agent (OPA) Rego policies
# File: policies/encryption.rego
package terraform.encryption
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_s3_bucket"
not resource.change.after.server_side_encryption_configuration
msg := "S3 buckets must have encryption enabled"
}
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_db_instance"
resource.change.after.storage_encrypted != true
msg := "RDS instances must have encryption at rest enabled"
}
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_ebs_volume"
resource.change.after.encrypted != true
msg := "EBS volumes must be encrypted"
}
Secrets Management
Always use external secret management systems like AWS Secrets Manager, Azure Key Vault, or HashiCorp Vault. Never hardcode sensitive values in your Terraform configurations.
# Retrieve secrets from AWS Secrets Manager
data "aws_secretsmanager_secret_version" "db_password" {
secret_id = "prod/database/password"
}
locals {
db_password = jsondecode(data.aws_secretsmanager_secret_version.db_password.secret_string)["password"]
}
# Use secret in resource configuration
resource "aws_db_instance" "main" {
identifier = "${var.environment}-database"
engine = "postgresql"
engine_version = "14.9"
instance_class = var.db_instance_class
allocated_storage = 100
storage_encrypted = true
db_name = var.db_name
username = var.db_username
password = local.db_password # Retrieved from Secrets Manager
backup_retention_period = 30
backup_window = "03:00-04:00"
tags = var.common_tags
}
# HashiCorp Vault integration
data "vault_generic_secret" "api_keys" {
path = "secret/api-keys"
}
resource "aws_lambda_function" "api_processor" {
filename = "lambda_function_payload.zip"
function_name = "${var.environment}-api-processor"
role = aws_iam_role.lambda_role.arn
handler = "index.handler"
runtime = "python3.9"
environment {
variables = {
API_KEY = data.vault_generic_secret.api_keys.data["external_api_key"]
DB_HOST = aws_db_instance.main.endpoint
}
}
}
Compliance and Governance
Implementing compliance frameworks requires systematic approach to resource tagging, access controls, and audit logging.
Conclusion and Future Outlook
Infrastructure as Code with Terraform has fundamentally transformed how organizations approach infrastructure management. By treating infrastructure as software, teams can achieve unprecedented levels of automation, consistency, and scalability.
Key Takeaways
Start Small, Think Big
Begin with simple modules and gradually build complexity. Focus on establishing solid foundations with state management and security practices.
Embrace Automation
Integrate Terraform into CI/CD pipelines early. Automated testing and deployment reduce errors and increase confidence in infrastructure changes.
Security by Design
Implement security controls, compliance checks, and secrets management from the beginning. It's much harder to retrofit security later.
Continuous Improvement
Regularly review and refactor your Terraform code. Keep modules updated, optimize costs, and incorporate new provider features.
The Future of Infrastructure as Code
Looking ahead, several trends are shaping the future of IaC:
- AI-Assisted Infrastructure: Machine learning tools will help optimize resource configurations and predict capacity needs
- Policy as Code Evolution: More sophisticated governance frameworks with automated compliance checking
- Edge Computing Integration: Better support for distributed edge infrastructure and IoT device management
- Sustainability Focus: Built-in carbon footprint monitoring and green computing optimizations
- Developer Experience: Improved tooling, better error messages, and more intuitive abstractions
Start implementing these practices in your organization today. Begin with a small project, establish your basic patterns, and gradually expand. Remember that Infrastructure as Code is as much about culture and process as it is about technology.
The journey to mature Infrastructure as Code practices requires investment in learning, tooling, and organizational change. However, the benefitsβincreased reliability, faster deployment cycles, better security posture, and reduced operational overheadβmake this investment worthwhile for any organization serious about scaling their infrastructure efficiently.
As cloud environments continue to evolve and new technologies emerge, Terraform's provider ecosystem and HashiCorp's continued innovation ensure that Infrastructure as Code will remain a cornerstone of modern DevOps practices. Start your IaC journey today, and build the foundation for tomorrow's infrastructure challenges.