Terraform has become the standard tool for Infrastructure as Code (IaC). It allows you to define, provision, and manage cloud resources across providers using declarative configuration. This guide covers practical Terraform patterns for production use.


Core Concepts


Terraform uses a declarative approach -- you describe the desired state, and Terraform figures out how to reach it:



# main.tf

terraform {

  required_version = ">= 1.8"

  required_providers {

    aws = {

      source  = "hashicorp/aws"

      version = "~> 5.0"

    }

  }

  backend "s3" {

    bucket = "myapp-terraform-state"

    key    = "production/terraform.tfstate"

    region = "us-east-1"

  }

}



provider "aws" {

  region = var.aws_region

}



resource "aws_s3_bucket" "app_data" {

  bucket = "myapp-production-data"

  tags = {

    Name        = "Application Data"

    Environment = "production"

  }

}


Key components: providers connect to cloud APIs, resources define infrastructure components, and the backend stores state.


State Management


State is the most critical part of Terraform. It maps configuration to real-world resources.


Remote State Backend


Always use remote state storage with locking:



# backend configuration during init: terraform init -backend-config=backend.hcl

bucket         = "company-terraform-state"

key            = "env:/${environment}/networking/terraform.tfstate"

region         = "us-east-1"

dynamodb_table = "terraform-state-lock"

encrypt        = true


DynamoDB provides state locking to prevent concurrent modifications. S3 versioning provides state history for rollback.


State Access for Other Configurations


Share outputs across configurations:



data "terraform_remote_state" "vpc" {

  backend = "s3"

  config = {

    bucket = "company-terraform-state"

    key    = "env:/production/vpc/terraform.tfstate"

    region = "us-east-1"

  }

}



resource "aws_instance" "app" {

  subnet_id = data.terraform_remote_state.vpc.outputs.private_subnet_ids[0]

}


Module Design


Modules are reusable Terraform configurations. Design them for composability:



# modules/vpc/main.tf

variable "vpc_cidr" {

  description = "CIDR block for the VPC"

  type        = string

  validation {

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

    error_message = "Must be a valid CIDR notation."

  }

}



variable "environment" {

  description = "Environment name for tagging"

  type        = string

}



resource "aws_vpc" "this" {

  cidr_block           = var.vpc_cidr

  enable_dns_hostnames = true

  enable_dns_support   = true



  tags = {

    Name        = "vpc-${var.environment}"

    Environment = var.environment

  }

}



output "vpc_id" {

  value = aws_vpc.this.id

}



output "vpc_cidr" {

  value = aws_vpc.this.cidr_block

}


Use input validation to catch errors early. Document all variables and outputs with descriptions.


Workspace and Environment Management


Use workspaces or directory structure for environment isolation:



# Directory structure:

terraform/

  env/

    production/

      main.tf

      terraform.tfvars

    staging/

      main.tf

      terraform.tfvars


Or use Terraform workspaces:



terraform workspace new staging

terraform workspace new production

terraform workspace select staging



terraform plan -var-file=staging.tfvars


Workspaces are simpler but can become confusing with many environments. Directory-based separation is clearer for complex setups.


Terraform Plan and Apply Workflow


The standard workflow in CI/CD:



# Initialize with backend

terraform init -backend-config=backend-$ENV.hcl



# Format and validate

terraform fmt -check

terraform validate



# Plan

terraform plan -out=tfplan -var-file=$ENV.tfvars



# Apply (typically in CI with approval gate)

terraform apply tfplan


Never run `terraform apply` without a plan file in CI. Always review the plan output before applying.


Managing Secrets


Never hardcode secrets. Use variables with sensitive flag:



variable "db_password" {

  description = "Database administrator password"

  type        = string

  sensitive   = true

}


For secrets that must be in state, encrypt with a key management service:



resource "aws_db_instance" "main" {

  password = var.db_password  # Still goes to state, but you can encrypt state

}


Better approach: use AWS Secrets Manager or Vault and reference secrets via data sources:



data "aws_secretsmanager_secret_version" "db_password" {

  secret_id = "production/db/password"

}


Testing Terraform Code


`terraform plan` as a Test


Run `terraform plan` in CI to detect drift and validate changes without applying:



terraform plan -detailed-exitcode

# Exit code 0: no changes

# Exit code 1: error

# Exit code 2: changes needed


Terratest for Integration Tests



// test/vpc_test.go

func TestVPC(t *testing.T) {

  terraformOptions := &terraform.Options{

    TerraformDir: "../modules/vpc",

    Vars: map[string]interface{}{

      "vpc_cidr": "10.0.0.0/16",

      "environment": "test",

    },

  }

  defer terraform.Destroy(t, terraformOptions)

  terraform.InitAndApply(t, terraformOptions)

  output := terraform.Output(t, terraformOptions, "vpc_id")

  assert.NotEmpty(t, output)

}


Common Pitfalls


  • **State file leaks**: Never commit state files to Git. Use `.gitignore` and remote backends.
  • **Hardcoded values**: Use variables and locals for everything that varies.
  • **Missing `prevent_destroy`**: Protect critical resources:
  • ```hcl

    resource "aws_db_instance" "production" {

    lifecycle {

    prevent_destroy = true

    }

    }

    ```

  • **Large state files**: Split infrastructure into manageable chunks by service layer (networking, compute, data).

  • Sentinel and Policy as Code


    For teams, enforce policies with Sentinel (HashiCorp Enterprise) or Open Policy Agent:


    
    # Deny public S3 buckets
    
    deny[msg] {
    
      resource := input.resource_changes[_]
    
      resource.type == "aws_s3_bucket"
    
      resource.change.after.acl == "public-read"
    
      msg := "S3 buckets must not be publicly readable"
    
    }
    
    

    Summary


    Terraform brings software engineering practices to infrastructure. Use remote state with locking, compose resources into modules, separate environments, and integrate planning into CI/CD. Never hardcode secrets, always validate configurations, and protect critical resources from accidental destruction. With these practices, Terraform enables infrastructure that is versioned, reviewable, reproducible, and auditable.