Terraform
TerraformIntermediate

Terraform Best Practices: A Comprehensive Guide

DeveloperHat Team
10 min read
IaCAWSDevOpsBest Practices

TL;DR

Learn essential best practices for managing your infrastructure as code with Terraform, including project structure, module design, and workflow optimization

import { MermaidDiagram } from '@/components/mermaid-diagram'

Discover how to effectively manage your infrastructure as code using Terraform. This guide covers best practices, patterns, and workflows for maintaining scalable and maintainable infrastructure.

diagram={

graph TB

subgraph "Project Structure"

Root["Project Root"]

Env["Environments"]

Mod["Modules"]

subgraph "Environment Types"

Dev["Development"]

Stage["Staging"]

Prod["Production"]

end

subgraph "Module Types"

Core["Core Infrastructure"]

App["Application"]

Data["Data Layer"]

end

end

Root --> Env

Root --> Mod

Env --> Dev

Env --> Stage

Env --> Prod

Mod --> Core

Mod --> App

Mod --> Data

Dev -->|"Promotes to"| Stage

Stage -->|"Promotes to"| Prod

style Root fill:#3b82f6,stroke:#2563eb,color:white

style Env fill:#3b82f6,stroke:#2563eb,color:white

style Mod fill:#3b82f6,stroke:#2563eb,color:white

style Dev fill:#f1f5f9,stroke:#64748b

style Stage fill:#f1f5f9,stroke:#64748b

style Prod fill:#f1f5f9,stroke:#64748b

style Core fill:#f1f5f9,stroke:#64748b

style App fill:#f1f5f9,stroke:#64748b

style Data fill:#f1f5f9,stroke:#64748b

}

/>

diagram={

sequenceDiagram

participant Dev as Developer

participant Git as Version Control

participant CI as CI/CD Pipeline

participant State as State Backend

participant Cloud as Cloud Provider

Dev->>Git: Commit Changes

Git->>CI: Trigger Pipeline

CI->>State: Initialize Backend

State-->>CI: Load Current State

CI->>CI: Terraform Plan

alt Plan Has Changes

CI->>Dev: Request Approval

Dev->>CI: Approve Changes

CI->>Cloud: Apply Changes

Cloud-->>State: Update State

else No Changes

CI->>Dev: No Changes Required

end

State-->>Dev: State Updated

}

/>

$1

A well-organized Terraform project structure is crucial for maintainability. Here's a detailed breakdown of a recommended project structure:

``plaintext

project/

├── environments/ # Environment-specific configurations

│ ├── dev/ # Development environment

│ │ ├── main.tf # Main configuration file

│ │ ├── variables.tf # Variable declarations

│ │ ├── outputs.tf # Output definitions

│ │ └── terraform.tfvars # Environment-specific values

│ ├── staging/ # Staging environment

│ └── prod/ # Production environment

├── modules/ # Reusable module definitions

│ ├── vpc/ # Network infrastructure

│ │ ├── main.tf # Module logic

│ │ ├── variables.tf # Input variables

│ │ └── outputs.tf # Output values

│ ├── ec2/ # Compute resources

│ └── rds/ # Database resources

├── scripts/ # Helper scripts

│ ├── init.sh # Initialization script

│ └── cleanup.sh # Cleanup script

├── .gitignore # Git ignore patterns

├── README.md # Project documentation

└── versions.tf # Version constraints

`

Each directory serves a specific purpose:

  • environments/: Contains environment-specific configurations
  • modules/: Houses reusable infrastructure components
  • scripts/: Stores automation and helper scripts
  • $1

    $1

    Each module should have a single responsibility. Here's an example of a well-structured VPC module:

    `hcl

    modules/vpc/main.tf

    Purpose: Creates a VPC with public and private subnets across multiple AZs

    module "vpc" {

    source = "terraform-aws-modules/vpc/aws"

    version = "5.0.0" # Always pin module versions

    name = var.vpc_name

    cidr = var.vpc_cidr

    # Availability Zones configuration

    azs = var.availability_zones

    private_subnets = var.private_subnet_cidrs

    public_subnets = var.public_subnet_cidrs

    # NAT Gateway configuration

    enable_nat_gateway = true

    single_nat_gateway = var.environment != "production" # Use single NAT for non-prod

    # VPC DNS Settings

    enable_dns_hostnames = true

    enable_dns_support = true

    # VPC Flow Logs

    enable_flow_log = true

    create_flow_log_cloudwatch_log_group = true

    create_flow_log_cloudwatch_iam_role = true

    # Resource tags

    tags = merge(var.tags, {

    Environment = var.environment

    ManagedBy = "terraform"

    Name = var.vpc_name

    })

    }

    Output essential information

    output "vpc_id" {

    description = "The ID of the VPC"

    value = module.vpc.vpc_id

    }

    output "private_subnet_ids" {

    description = "List of private subnet IDs"

    value = module.vpc.private_subnets

    }

    `

    $1

    Data sources help you reference existing resources and maintain infrastructure relationships:

    `hcl

    Example: Reference existing resources and create dependencies

    Find existing VPC by tags

    data "aws_vpc" "existing" {

    tags = {

    Environment = var.environment

    Project = var.project_name

    }

    }

    Get all private subnets

    data "aws_subnet_ids" "private" {

    vpc_id = data.aws_vpc.existing.id

    tags = {

    Tier = "Private"

    Environment = var.environment

    }

    }

    Use the data in a resource

    resource "aws_security_group" "app" {

    name = "${var.environment}-app-sg"

    description = "Security group for application servers"

    vpc_id = data.aws_vpc.existing.id

    # Ingress rules

    ingress {

    from_port = 443

    to_port = 443

    protocol = "tcp"

    cidr_blocks = data.aws_vpc.existing.cidr_block_associations[*].cidr_block

    description = "HTTPS from VPC"

    }

    # Egress rules

    egress {

    from_port = 0

    to_port = 0

    protocol = "-1"

    cidr_blocks = ["0.0.0.0/0"]

    description = "Allow all outbound traffic"

    }

    tags = merge(var.tags, {

    Name = "${var.environment}-app-sg"

    })

    }

    `

    $1

    $1

    Implement robust remote state management with proper backend configuration:

    `hcl

    backend.tf

    Purpose: Configure remote state storage with encryption and locking

    terraform {

    backend "s3" {

    bucket = "terraform-state-bucket"

    key = "env/dev/terraform.tfstate"

    region = "us-west-2"

    encrypt = true

    dynamodb_table = "terraform-locks"

    # Additional security measures

    kms_key_id = "arn:aws:kms:us-west-2:ACCOUNT_ID:key/KEY_ID"

    # Versioning and logging

    versioning {

    enabled = true

    }

    # Access logging

    logging {

    target_bucket = "access-logs-bucket"

    target_prefix = "terraform-state/"

    }

    }

    }

    Create the state bucket with proper configuration

    resource "aws_s3_bucket" "terraform_state" {

    bucket = "terraform-state-bucket"

    # Enable versioning

    versioning {

    enabled = true

    }

    # Enable server-side encryption

    server_side_encryption_configuration {

    rule {

    apply_server_side_encryption_by_default {

    kms_master_key_id = aws_kms_key.terraform_state.id

    sse_algorithm = "aws:kms"

    }

    }

    }

    # Enable access logging

    logging {

    target_bucket = aws_s3_bucket.access_logs.id

    target_prefix = "terraform-state/"

    }

    # Block public access

    block_public_acls = true

    block_public_policy = true

    ignore_public_acls = true

    restrict_public_buckets = true

    }

    `

    $1

    Ensure safe concurrent operations with state locking:

    `hcl

    state-locking.tf

    Purpose: Implement state locking using DynamoDB

    resource "aws_dynamodb_table" "terraform_locks" {

    name = "terraform-locks"

    billing_mode = "PAY_PER_REQUEST"

    hash_key = "LockID"

    attribute {

    name = "LockID"

    type = "S"

    }

    # Enable point-in-time recovery

    point_in_time_recovery {

    enabled = true

    }

    # Enable server-side encryption

    server_side_encryption {

    enabled = true

    kms_key_arn = aws_kms_key.terraform_locks.arn

    }

    tags = merge(var.tags, {

    Name = "terraform-locks"

    Description = "DynamoDB table for Terraform state locking"

    })

    }

    KMS key for encryption

    resource "aws_kms_key" "terraform_locks" {

    description = "KMS key for Terraform state locking"

    deletion_window_in_days = 7

    enable_key_rotation = true

    tags = merge(var.tags, {

    Name = "terraform-locks-key"

    })

    }

    `

    $1

    $1

    Implement comprehensive validation rules:

    `hcl

    variables.tf

    Purpose: Define and validate input variables

    variable "environment" {

    type = string

    description = "Environment name (dev, staging, prod)"

    validation {

    condition = contains(["dev", "staging", "prod"], var.environment)

    error_message = "Environment must be dev, staging, or prod."

    }

    }

    variable "instance_type" {

    type = string

    description = "EC2 instance type"

    validation {

    condition = can(regex("^t[23]|m[45]|c[56]", var.instance_type))

    error_message = "Instance type must be a valid AWS instance type."

    }

    }

    variable "vpc_cidr" {

    type = string

    description = "CIDR block for VPC"

    validation {

    condition = can(cidrhost(var.vpc_cidr, 0))

    error_message = "VPC CIDR must be a valid IPv4 CIDR block."

    }

    validation {

    condition = tonumber(split("/", var.vpc_cidr)[1]) <= 16

    error_message = "VPC CIDR block must be /16 or larger."

    }

    }

    variable "tags" {

    type = map(string)

    description = "Resource tags"

    validation {

    condition = contains(keys(var.tags), "Project")

    error_message = "Tags must include a 'Project' key."

    }

    }

    `

    $1

    Organize variables by environment with detailed documentation:

    `hcl

    environments/dev/terraform.tfvars

    Purpose: Development environment configuration

    Network Configuration

    environment = "dev"

    vpc_cidr = "10.0.0.0/16"

    region = "us-west-2"

    availability_zones = ["us-west-2a", "us-west-2b"]

    Compute Resources

    instance_type = "t3.micro"

    asg_min_size = 1

    asg_max_size = 3

    Database Configuration

    db_instance_class = "db.t3.small"

    db_allocated_storage = 20

    db_engine_version = "13.7"

    Monitoring

    enable_detailed_monitoring = true

    retention_in_days = 7

    Tags

    tags = {

    Project = "MyApp"

    Environment = "dev"

    Owner = "DevOps"

    CostCenter = "12345"

    }

    Backup Configuration

    backup_retention_period = 7

    enable_cross_region_backup = false

    `

    $1

    $1

    `hcl

    providers.tf

    Purpose: Configure providers with advanced settings

    provider "aws" {

    region = var.region

    # Default tags applied to all resources

    default_tags {

    tags = {

    Environment = var.environment

    ManagedBy = "terraform"

    Project = var.project_name

    }

    }

    # Assume role for cross-account management

    assume_role {

    role_arn = "arn:aws:iam::${var.target_account_id}:role/TerraformExecutionRole"

    session_name = "TerraformSession"

    }

    # S3 endpoint configuration

    endpoints {

    s3 = "s3.${var.region}.amazonaws.com"

    }

    }

    Additional region for disaster recovery

    provider "aws" {

    alias = "dr"

    region = var.dr_region

    assume_role {

    role_arn = "arn:aws:iam::${var.dr_account_id}:role/TerraformExecutionRole"

    session_name = "TerraformDRSession"

    }

    }

    `

    $1

    `hcl

    monitoring.tf

    Purpose: Configure comprehensive monitoring

    CloudWatch Dashboard

    resource "aws_cloudwatch_dashboard" "main" {

    dashboard_name = "${var.environment}-infrastructure-dashboard"

    dashboard_body = jsonencode({

    widgets = [

    {

    type = "metric"

    x = 0

    y = 0

    width = 12

    height = 6

    properties = {

    metrics = [

    ["AWS/EC2", "CPUUtilization", "AutoScalingGroupName", aws_autoscaling_group.app.name],

    [".", "NetworkIn", ".", "."],

    [".", "NetworkOut", ".", "."]

    ]

    period = 300

    stat = "Average"

    region = var.region

    title = "EC2 Metrics"

    }

    },

    {

    type = "metric"

    x = 12

    y = 0

    width = 12

    height = 6

    properties = {

    metrics = [

    ["AWS/RDS", "CPUUtilization", "DBInstanceIdentifier", aws_db_instance.main.id],

    [".", "FreeStorageSpace", ".", "."],

    [".", "DatabaseConnections", ".", "."]

    ]

    period = 300

    stat = "Average"

    region = var.region

    title = "RDS Metrics"

    }

    }

    ]

    })

    }

    CloudWatch Alarms

    resource "aws_cloudwatch_metric_alarm" "high_cpu" {

    alarm_name = "${var.environment}-high-cpu-utilization"

    comparison_operator = "GreaterThanThreshold"

    evaluation_periods = "2"

    metric_name = "CPUUtilization"

    namespace = "AWS/EC2"

    period = "300"

    statistic = "Average"

    threshold = "80"

    alarm_description = "This metric monitors EC2 CPU utilization"

    alarm_actions = [aws_sns_topic.alerts.arn]

    dimensions = {

    AutoScalingGroupName = aws_autoscaling_group.app.name

    }

    }

    `

    $1

    $1

    `hcl

    iam.tf

    Purpose: Define IAM roles with least privilege

    Application Role

    resource "aws_iam_role" "app_role" {

    name = "${var.environment}-app-role"

    assume_role_policy = jsonencode({

    Version = "2012-10-17"

    Statement = [

    {

    Action = "sts:AssumeRole"

    Effect = "Allow"

    Principal = {

    Service = "ec2.amazonaws.com"

    }

    }

    ]

    })

    # Inline policy for specific permissions

    inline_policy {

    name = "app-permissions"

    policy = jsonencode({

    Version = "2012-10-17"

    Statement = [

    {

    Effect = "Allow"

    Action = [

    "s3:GetObject",

    "s3:ListBucket"

    ]

    Resource = [

    aws_s3_bucket.app_data.arn,

    "${aws_s3_bucket.app_data.arn}/*"

    ]

    },

    {

    Effect = "Allow"

    Action = [

    "secretsmanager:GetSecretValue"

    ]

    Resource = [

    aws_secretsmanager_secret.app_secrets.arn

    ]

    }

    ]

    })

    }

    tags = merge(var.tags, {

    Name = "${var.environment}-app-role"

    })

    }

    `

    $1

    `hcl

    security.tf

    Purpose: Define security groups with detailed rules

    resource "aws_security_group" "app_sg" {

    name = "${var.environment}-app-sg"

    description = "Security group for application servers"

    vpc_id = aws_vpc.main.id

    # Web traffic

    ingress {

    description = "HTTPS from ALB"

    from_port = 443

    to_port = 443

    protocol = "tcp"

    security_groups = [aws_security_group.alb.id]

    }

    # Application port

    ingress {

    description = "Application port"

    from_port = var.app_port

    to_port = var.app_port

    protocol = "tcp"

    security_groups = [aws_security_group.alb.id]

    }

    # Monitoring

    ingress {

    description = "Prometheus metrics"

    from_port = 9090

    to_port = 9090

    protocol = "tcp"

    security_groups = [aws_security_group.monitoring.id]

    }

    # Outbound rules

    egress {

    description = "Allow all outbound traffic"

    from_port = 0

    to_port = 0

    protocol = "-1"

    cidr_blocks = ["0.0.0.0/0"]

    }

    tags = merge(var.tags, {

    Name = "${var.environment}-app-sg"

    })

    lifecycle {

    create_before_destroy = true

    }

    }

    ``

    $1

    Following these enhanced best practices will help you build and maintain a robust infrastructure codebase. Key takeaways:

    1. Implement comprehensive project structure

    2. Use focused, well-documented modules

    3. Configure secure and scalable state management

    4. Implement thorough variable validation

    5. Set up detailed monitoring and alerting

    6. Follow security best practices

    7. Maintain clear documentation

    Remember to regularly review and update your infrastructure code to incorporate new best practices and security improvements.

    $1

    1. [Terraform Best Practices Guide](https://www.terraform.io/docs/cloud/guides/recommended-practices/index.html)

    2. [AWS Architecture Center](https://aws.amazon.com/architecture/)

    3. [HashiCorp Learn](https://learn.hashicorp.com/terraform)

    4. [Terraform Module Registry](https://registry.terraform.io/)

    Why This Matters

    Understanding the business and technical context helps you make informed decisions rather than blindly following patterns.

    Trade-offs to Consider

    Every architectural decision involves trade-offs. Consider your specific requirements, team expertise, and scale when evaluating options.

    When NOT to Use This

    Knowing when a solution doesn't apply is as valuable as knowing when it does. Consider alternatives for your specific situation.

    Decision Framework

    Use this framework to evaluate whether this approach is right for your use case based on your specific constraints and requirements.