Infrastructure as Code with Terraform: Best Practices

Master Terraform with production-ready best practices, real-world patterns, and proven methodologies to automate cloud infrastructure safely and efficiently.

8 min read

🎯 Benefits in Numbers

147
Infrastructure Components
Managed via code
-73%
Manual Configuration Time
vs traditional approach
99.2%
Infrastructure Consistency
Across environments
8
Reusable Modules
Best practice library

⏱️ Reading time: 14 min | 💡 Level: Intermediate to Expert


📋 Why This Guide?

Challenge: Organizations struggle with infrastructure consistency across multiple cloud environments, spending excessive time on manual configurations, and face critical risks from configuration drift and human error. Infrastructure as Code (IaC) with Terraform solves these issues by making infrastructure predictable, version-controlled, and repeatable.

Measured Impact

Deployment Speed & Reliability


🗓️ 5-Step Terraform Mastery Framework

Calyo Infrastructure Automation Framework™


📝 Step 1: Foundation & Terraform Setup

🎯 Measurable Objectives

100%
Remote State Setup
Centralized configuration
5
Provider Configurations
AWS, Azure, GCP, K8s, etc.
7
Days Setup Time
Complete foundation

⚠️ Critical Setup Pitfalls vs Solutions

Common Setup Mistakes & Workarounds

Classic Trap
Risk Level
Calyo Solution
Local state files onlyCriticalAlways use remote state (S3, Azure Storage, Terraform Cloud) with state locking enabled
Hardcoded credentials in codeCriticalUse IAM roles, environment variables, or Terraform variables with sensitive flag
No .tfstate backup strategyHighEnable versioning on state bucket, implement daily backups, use state snapshots
Inconsistent naming conventionsMediumDefine naming standard from day 1: resource_type-environment-purpose format
Missing variable validationMediumUse variable type constraints, add validation blocks, document all inputs
No state isolation per environmentHighSeparate state files for dev/staging/prod using workspaces or separate backends

✅ Foundation Checklist Completion

Setup Phase Completion (%)

100Total
Completed tasks 92 (92.0%)
In progress 6 (6.0%)
Remaining 2 (2.0%)

Core Setup Template:

# main.tf - Provider Configuration
terraform {
  required_version = ">= 1.5"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
  backend "s3" {
    bucket         = "terraform-state-prod"
    key            = "infrastructure/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "terraform-locks"
  }
}

provider "aws" {
  region = var.aws_region

  default_tags {
    tags = {
      Environment = var.environment
      ManagedBy   = "Terraform"
      CostCenter  = var.cost_center
    }
  }
}

💡 Calyo Tip: Always enable state encryption and DynamoDB locking from day one. These take 5 minutes to configure but prevent catastrophic data loss and concurrent modification conflicts.


📝 Step 2: Module Architecture & Reusability

Essential Terraform Modules by Use Case

Module Type
Typical Scope
Reusability Score
Complexity
Networking (VPC)VPC, subnets, security groups, routing95%High
Compute (EC2)Instances, auto-scaling, load balancing88%Medium
DatabaseRDS, DynamoDB, ElastiCache instances92%Medium
Kubernetes (EKS)Cluster, node groups, add-ons85%High
Security & IAMRoles, policies, KMS keys90%Medium
Logging & MonitoringCloudWatch, S3 logging, alarms87%Medium
Storage (S3)Buckets, versioning, lifecycle rules91%Low
CDN & DNSCloudFront, Route53 records89%Low

📊 Best Practice Module Structure

Code Organization Score by Component (/100)

02448719595Input v...Input variables documentation9288Variabl...Variable validation rules8582Conditi...Conditional logic clarity90

Module Directory Structure:

terraform/
├── modules/
│   ├── networking/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   ├── outputs.tf
│   │   ├── locals.tf
│   │   └── terraform.tf
│   ├── compute/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   ├── outputs.tf
│   │   └── ec2.tf
│   ├── database/
│   │   └── ...
│   └── security/
│       └── ...
├── environments/
│   ├── dev/
│   │   └── main.tf
│   ├── staging/
│   │   └── main.tf
│   └── production/
│       └── main.tf
├── global/
│   └── terraform.tf
└── _docs/
    └── MODULE_GUIDELINES.md

Complete Module Example - Networking:

# modules/networking/main.tf
module "vpc" {
  source = "terraform-aws-modules/vpc/aws"
  version = "~> 5.0"

  name = "${var.project}-${var.environment}-vpc"
  cidr = var.vpc_cidr

  azs              = data.aws_availability_zones.available.names
  private_subnets = var.private_subnets
  public_subnets  = var.public_subnets

  enable_nat_gateway   = true
  single_nat_gateway   = var.environment != "production"
  enable_vpn_gateway   = var.enable_vpn
  enable_dns_hostnames = true

  tags = {
    Name = "${var.project}-vpc"
  }
}

resource "aws_security_group" "alb" {
  name        = "${var.project}-${var.environment}-alb-sg"
  description = "Security group for ALB"
  vpc_id      = module.vpc.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"]
  }

  tags = {
    Name = "${var.project}-alb-sg"
  }
}

💡 Calyo Tip: Always use the Terraform Registry modules as your foundation (terraform-aws-modules, terraform-google-modules). They’re battle-tested and regularly updated, reducing 80% of your module development time.


📝 Step 3: Multi-Environment Implementation

Environment Strategy Comparison

Which environment strategy to choose?

Critère
Workspaces
Single backend, multiple states
Separate Backends
Isolated state per environment
Hybrid Approach
Workspaces for dev, separate backends for prod
State Isolation
Team Safety
Operational Complexity
Disaster Recovery
Cost Visibility

Production-Ready Environment Configuration:

# environments/production/main.tf
terraform {
  backend "s3" {
    bucket         = "terraform-state-prod"
    key            = "prod/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "terraform-locks"
  }
}

provider "aws" {
  region = "us-east-1"

  assume_role {
    role_arn = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/TerraformRole"
  }
}

module "networking" {
  source = "../../modules/networking"

  project          = var.project
  environment      = "production"
  vpc_cidr         = "10.0.0.0/16"
  private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
  public_subnets  = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]

  enable_vpn = true
}

module "eks" {
  source = "../../modules/compute"

  project              = var.project
  environment          = "production"
  cluster_name         = "${var.project}-eks-prod"
  kubernetes_version   = "1.28"

  vpc_id                    = module.networking.vpc_id
  subnet_ids                = module.networking.private_subnet_ids

  node_groups = {
    primary = {
      desired_size       = 3
      min_size           = 3
      max_size           = 10
      instance_types     = ["t3.large"]
      disk_size          = 50
    }
  }
}

module "database" {
  source = "../../modules/database"

  project              = var.project
  environment          = "production"
  database_name        = "${replace(var.project, "-", "_")}_prod"

  engine               = "postgres"
  engine_version       = "15.4"
  instance_class       = "db.r6i.xlarge"
  allocated_storage    = 100
  max_allocated_storage = 500

  vpc_id              = module.networking.vpc_id
  subnet_ids          = module.networking.private_subnet_ids

  backup_retention_days = 30
  enable_multiaz       = true
  enable_encryption    = true
}

📝 Step 4: Governance & Policy as Code

🎯 Governance Objectives

15
Security Policies
Enforced via Sentinel/OPA
98%
Policy Compliance Rate
Across infrastructure
$47K
Monthly Cost Savings
Via policy enforcement

⚠️ Governance Pitfalls vs Solutions

Common Governance Mistakes & Enforcement

Governance Gap
Risk Level
Best Practice Solution
No cost controlsHighImplement cost limits via Sentinel, use budget alerts, enforce instance rightsizing
Missing encryption enforcementCriticalPolicy: All S3 buckets must have encryption. All RDS must have EBS encryption enabled
Untagged resourcesMediumMandatory tags on all resources. Tag enforcement via Sentinel or OPA policies
Open security groupsCriticalRestrict SSH/RDP to specific IPs. Deny 0.0.0.0/0 for dangerous ports
No audit loggingCriticalEnable CloudTrail, S3 access logs, VPC Flow Logs for all infrastructure
Unmanaged database backupsHighEnforce backup retention (min 14 days), enable automated backups

Sentinel Policy Example - Cost Control:

# policies/cost_control.sentinel
import "tfplan/v2" as tfplan

# Allowed instance types by environment
allowed_types = {
  "development": ["t3.micro", "t3.small", "t3.medium"],
  "staging":     ["t3.medium", "t3.large", "m6i.large"],
  "production":  ["m6i.large", "m6i.xlarge", "c6i.large"]
}

# Deny expensive instance types in non-prod
deny_expensive_instances = rule {
  all tfplan.resource_changes as address, rc {
    rc.type == "aws_instance" {
      rc.change.after.instance_type in allowed_types[rc.change.after.environment]
    }
  }
}

main = rule {
  deny_expensive_instances
}

📝 Step 5: Advanced Patterns & Optimization

🛠️ Advanced Terraform Features

Advanced Features by Maturity Level

Feature
Use Case
Complexity
Impact
Dynamic BlocksMulti-item configurationsMediumReduces 40% code duplication
For_each LoopsMultiple resource instancesMediumEnables parametric scaling
Computed ValuesComplex configurationsHighAllows intelligent defaults
Resource TargetingPartial deploymentsLowCrisis recovery tool
Import Existing ResourcesState migrationHigh50% faster legacy adoption
Custom ProvidersSpecial integrationsExpertUnlimited extensibility

Advanced Pattern: Dynamic Web Server Deployment

# Advanced example with dynamic blocks
variable "web_servers" {
  type = map(object({
    instance_type = string
    volume_size   = number
    environment   = string
  }))
}

# Dynamic security group rules
resource "aws_security_group" "web" {
  name = "${var.project}-web-sg"

  dynamic "ingress" {
    for_each = var.allowed_ports
    content {
      from_port   = ingress.value.port
      to_port     = ingress.value.port
      protocol    = ingress.value.protocol
      cidr_blocks = ingress.value.cidr_blocks
    }
  }
}

# Multiple instances using for_each
resource "aws_instance" "servers" {
  for_each = var.web_servers

  instance_type = each.value.instance_type
  root_block_device {
    volume_size = each.value.volume_size
    encrypted   = true
  }

  tags = {
    Name        = each.key
    Environment = each.value.environment
  }
}

Optimizing State Performance:

# Use local_file for large data to avoid state bloat
resource "local_file" "large_config" {
  content  = jsonencode(var.complex_config)
  filename = "${path.module}/configs/generated.json"
}

# Reference in outputs, not state
output "config_path" {
  value = local_file.large_config.filename
}

# Use data sources to avoid state storage
data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"]

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
  }
}

💡 Calyo Tip: Keep your state files lean. Use data sources for lookups, local_file for large configs, and separate state for independent components. A 500MB state file will kill your Terraform performance.


📊 Implementation Success Metrics

Essential KPIs

  • Infrastructure Deployment Time: Measure from code commit to production. Target: < 15 minutes
  • Configuration Drift: Monthly percentage of manual changes outside Terraform. Target: < 2%
  • Team Adoption Rate: Percentage of infrastructure deployed via Terraform. Target: > 95%
  • Plan Success Rate: Plans that apply without errors. Target: > 98%
  • Cost Forecast Accuracy: Budget vs actual. Target: ±5%
  • State Lock Contention: Instances of simultaneous apply attempts. Target: 0

Critical Monitoring Dashboard Elements

  • Real-time plan execution logs
  • State file size and change frequency trends
  • Cost estimates vs actuals by resource type
  • Drift detection scan results (daily)
  • Policy violation trends by type
  • Team deploy frequency and success rates

💡 Expert Tips & Quick Wins

Week 1 Quick Wins

  1. Set up remote state with locking - Takes 30 minutes, prevents 90% of state conflicts
  2. Create first 3 reusable modules - VPC, EC2, Security Group - reduces future setup by 70%
  3. Establish naming conventions - Prevents resource chaos as infrastructure scales
  4. Configure provider defaults - Default tags on all resources for billing and compliance

Long-term Investments

  • Build internal module registry - Centralizes best practices, accelerates team onboarding
  • Implement policy framework - Sentinel/OPA policies enforce compliance without manual reviews
  • Multi-cloud strategy - Design modules for AWS, Azure, GCP compatibility
  • Advanced state management - Separate state backends per environment, implement automated backups
  • Custom providers development - Integrate with proprietary systems and legacy infrastructure
  • Team training program - Invest in Terraform certification and advanced training

🚀 Going Further

Complementary Resources

Advanced Use Cases

  • Multi-account AWS deployments - Terraform with AWS Organizations and Service Control Policies
  • Kubernetes at scale - EKS provisioning with auto-scaling and GitOps integration (ArgoCD/Flux)
  • Disaster recovery automation - Cross-region failover orchestration with Terraform
  • Compliance as Code - Automated compliance checking for HIPAA, PCI-DSS, SOC 2 standards
  • FinOps automation - Automated resource rightsizing and cost optimization enforcement

❓ Frequently Asked Questions

Q: How do we migrate existing infrastructure to Terraform? A: Use terraform import for existing resources, organize by module, then plan and apply incrementally. Start with non-critical infrastructure (dev/staging) to validate your module structure before touching production.

Q: What’s the best way to manage secrets in Terraform? A: Never store secrets in code. Use AWS Secrets Manager, Azure Key Vault, or Terraform Cloud’s sensitive variables. Reference them via data sources (aws_secretsmanager_secret_version) rather than hardcoding.

Q: How do we handle team collaboration safely? A: Always use remote state with state locking (DynamoDB + S3 or Terraform Cloud). Require code reviews for production changes, use Terraform Cloud enforcement for plan reviews, and separate dev/staging/prod state files.

Q: Should we use workspaces or separate backends? A: For production safety, separate backends per environment are recommended. Workspaces are convenient for dev but lack the isolation needed for production environments with different access controls.

Q: How often should we refactor our Terraform code? A: Plan quarterly refactoring reviews. As infrastructure grows from 50 to 500+ resources, refactor into more granular modules. Use terraform state mv to reorganize without destroying resources.

Q: What’s the typical learning curve for teams new to Terraform? A: Basic proficiency (simple modules, multi-environment setup): 3-4 weeks. Intermediate (advanced features, governance): 8-12 weeks. Expert level (custom providers, large-scale optimization): 6+ months.


📊 Terraform Maturity Roadmap

Month 1-2: Foundation
├─ Remote state with locking
├─ Basic module structure
└─ First environment deployment

Month 3-4: Scaling
├─ Multi-environment setup
├─ Team collaboration workflows
└─ 6-8 production modules

Month 5-6: Governance
├─ Policy as Code (Sentinel/OPA)
├─ Cost controls & monitoring
└─ Compliance automation

Month 7+: Advanced
├─ Multi-cloud strategies
├─ Internal module registry
└─ Advanced state patterns & self-service platforms

Azzeddine AMIAR
Written by
Azzeddine AMIAR
Founder & CEO
Calyo Consulting
Connect
  • terraform
  • infrastructure-as-code
  • cloud-automation
  • devops
  • best-practices
  • aws
  • azure
Share:

Related Posts

View All Posts »