> ## Documentation Index
> Fetch the complete documentation index at: https://notes.vvkhash.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Terraform IaC Workflow, Modules, and Practical Patterns

> Core Terraform workflow, provider config, variables, remote state with S3 backend, modules, and a working AWS EC2 example for infrastructure engineers.

Terraform is my go-to tool for provisioning any cloud infrastructure that needs to be reproducible, reviewed, and version-controlled. These notes cover the workflow I use daily — from first `init` through module composition and remote state management. Code examples use AWS but the patterns apply equally to GCP and Azure by swapping the provider block.

***

## Core Workflow

<Steps>
  <Step title="terraform init — initialize the working directory">
    Downloads provider plugins and sets up the backend. Run this once per checkout, and again whenever you add providers or change the backend config.

    ```bash theme={null}
    terraform init

    # Upgrade providers to the latest allowed by version constraints
    terraform init -upgrade

    # Reconfigure the backend (e.g., changing S3 bucket or key)
    terraform init -reconfigure
    ```
  </Step>

  <Step title="terraform fmt — format code">
    Standardize HCL formatting before committing. Add this to your pre-commit hooks.

    ```bash theme={null}
    # Format all .tf files in the current directory recursively
    terraform fmt -recursive

    # Check formatting without modifying files (use in CI)
    terraform fmt -check -recursive
    ```
  </Step>

  <Step title="terraform validate — check configuration syntax">
    Validates the configuration without accessing any remote services or state. Fast and safe to run locally.

    ```bash theme={null}
    terraform validate
    ```
  </Step>

  <Step title="terraform plan — preview changes">
    Shows exactly what Terraform will create, modify, or destroy. Always review this before applying.

    ```bash theme={null}
    terraform plan

    # Save the plan to a file (use this in CI/CD for consistent apply)
    terraform plan -out=tfplan

    # Plan with variable overrides
    terraform plan -var="instance_type=t3.small" -var="env=staging"

    # Plan with a tfvars file
    terraform plan -var-file=staging.tfvars
    ```
  </Step>

  <Step title="terraform apply — provision resources">
    ```bash theme={null}
    # Interactive apply with confirmation prompt
    terraform apply

    # Apply a saved plan (no prompt, safe for CI/CD)
    terraform apply tfplan

    # Auto-approve without prompt (use carefully)
    terraform apply -auto-approve
    ```
  </Step>

  <Step title="terraform destroy — tear down resources">
    ```bash theme={null}
    # Destroy all resources managed by this state
    terraform destroy

    # Destroy a specific resource (target flag)
    terraform destroy -target=aws_instance.web_server

    # Preview what would be destroyed
    terraform plan -destroy
    ```
  </Step>
</Steps>

<Warning>
  Never run `terraform apply` or `terraform destroy` directly in production without a saved plan file reviewed in a pull request. Even `terraform apply -auto-approve` in CI should only run after a plan output has been reviewed and approved.
</Warning>

***

## Provider Configuration

```hcl theme={null}
# versions.tf
terraform {
  required_version = ">= 1.6.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
    random = {
      source  = "hashicorp/random"
      version = "~> 3.6"
    }
  }
}

provider "aws" {
  region = var.aws_region

  # Tag every resource managed by this configuration
  default_tags {
    tags = {
      ManagedBy   = "terraform"
      Environment = var.environment
      Project     = var.project_name
    }
  }
}

# Second provider block for a different region (e.g., us-west-2 for DR)
provider "aws" {
  alias  = "dr"
  region = "us-west-2"
}
```

<Note>
  Use `~>` (pessimistic constraint) for provider versions: `~> 5.0` allows `5.x` but not `6.x`. This protects against breaking changes in major versions while still getting patch updates automatically on `init -upgrade`.
</Note>

***

## Variables, Outputs, and Locals

<Tabs>
  <Tab title="variables.tf">
    ```hcl theme={null}
    variable "aws_region" {
      description = "AWS region to deploy resources into"
      type        = string
      default     = "us-east-1"
    }

    variable "environment" {
      description = "Deployment environment (dev, staging, prod)"
      type        = string

      validation {
        condition     = contains(["dev", "staging", "prod"], var.environment)
        error_message = "environment must be one of: dev, staging, prod."
      }
    }

    variable "project_name" {
      description = "Project identifier used in resource naming and tags"
      type        = string
    }

    variable "instance_type" {
      description = "EC2 instance type"
      type        = string
      default     = "t3.micro"
    }

    variable "allowed_cidr_blocks" {
      description = "List of CIDR blocks allowed SSH access"
      type        = list(string)
      default     = []
    }

    variable "tags" {
      description = "Additional tags to apply to all resources"
      type        = map(string)
      default     = {}
    }
    ```
  </Tab>

  <Tab title="locals.tf">
    ```hcl theme={null}
    locals {
      # Computed naming convention — consistent across all resources
      name_prefix = "${var.project_name}-${var.environment}"

      # Merge default tags with any extra tags passed in
      common_tags = merge(
        {
          Project     = var.project_name
          Environment = var.environment
          ManagedBy   = "terraform"
        },
        var.tags
      )

      # Is this a production deployment?
      is_prod = var.environment == "prod"

      # Derive instance count from environment
      instance_count = local.is_prod ? 3 : 1
    }
    ```
  </Tab>

  <Tab title="outputs.tf">
    ```hcl theme={null}
    output "instance_id" {
      description = "EC2 instance ID"
      value       = aws_instance.web_server.id
    }

    output "instance_public_ip" {
      description = "Public IP address of the web server"
      value       = aws_instance.web_server.public_ip
    }

    output "security_group_id" {
      description = "ID of the web server security group"
      value       = aws_security_group.web.id
    }

    output "bucket_name" {
      description = "Name of the S3 bucket"
      value       = aws_s3_bucket.app_assets.id
    }

    # Sensitive outputs are redacted in plan/apply output
    output "db_password" {
      description = "RDS master password"
      value       = random_password.db.result
      sensitive   = true
    }
    ```
  </Tab>

  <Tab title="terraform.tfvars">
    ```hcl theme={null}
    # terraform.tfvars — committed defaults (no secrets here!)
    # Override with staging.tfvars or prod.tfvars as needed

    aws_region   = "us-east-1"
    project_name = "myapp"
    environment  = "dev"
    instance_type = "t3.micro"

    allowed_cidr_blocks = [
      "10.0.0.0/8"
    ]

    tags = {
      CostCenter = "engineering"
      Owner      = "platform-team"
    }
    ```
  </Tab>
</Tabs>

***

## Remote State with S3 Backend

Storing state locally is fine for learning, but any team environment needs remote state. S3 + DynamoDB is the standard AWS pattern.

<Steps>
  <Step title="Create the S3 bucket and DynamoDB table">
    Bootstrap these resources once, manually or with a separate Terraform root. Do **not** put the state bucket itself in the state it manages.

    ```bash theme={null}
    # Create versioned S3 bucket for state
    aws s3api create-bucket \
      --bucket my-tf-state-bucket \
      --region us-east-1

    aws s3api put-bucket-versioning \
      --bucket my-tf-state-bucket \
      --versioning-configuration Status=Enabled

    aws s3api put-public-access-block \
      --bucket my-tf-state-bucket \
      --public-access-block-configuration \
        "BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"

    # Enable default encryption
    aws s3api put-bucket-encryption \
      --bucket my-tf-state-bucket \
      --server-side-encryption-configuration '{
        "Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"AES256"}}]
      }'

    # Create DynamoDB table for state locking
    aws dynamodb create-table \
      --table-name terraform-state-lock \
      --attribute-definitions AttributeName=LockID,AttributeType=S \
      --key-schema AttributeName=LockID,KeyType=HASH \
      --billing-mode PAY_PER_REQUEST
    ```
  </Step>

  <Step title="Configure the backend block">
    ```hcl theme={null}
    # backend.tf
    terraform {
      backend "s3" {
        bucket         = "my-tf-state-bucket"
        key            = "environments/prod/myapp/terraform.tfstate"
        region         = "us-east-1"
        encrypt        = true
        dynamodb_table = "terraform-state-lock"
      }
    }
    ```
  </Step>

  <Step title="Reference outputs from another state (remote state data source)">
    ```hcl theme={null}
    # Read outputs from the networking layer state
    data "terraform_remote_state" "network" {
      backend = "s3"
      config = {
        bucket = "my-tf-state-bucket"
        key    = "environments/prod/networking/terraform.tfstate"
        region = "us-east-1"
      }
    }

    # Use VPC and subnet IDs provisioned by the networking stack
    resource "aws_instance" "app" {
      ami           = data.aws_ami.amazon_linux.id
      instance_type = var.instance_type
      subnet_id     = data.terraform_remote_state.network.outputs.private_subnet_ids[0]
    }
    ```
  </Step>
</Steps>

***

## Resource and Data Source Patterns

```hcl theme={null}
# data sources — read existing infrastructure, don't manage it
data "aws_vpc" "default" {
  default = true
}

data "aws_subnets" "public" {
  filter {
    name   = "vpc-id"
    values = [data.aws_vpc.default.id]
  }
  filter {
    name   = "tag:Tier"
    values = ["public"]
  }
}

# Fetch the latest Amazon Linux 2023 AMI dynamically
data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["al2023-ami-*-x86_64"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}

# Resource — Terraform manages this
resource "aws_security_group" "web" {
  name_prefix = "${local.name_prefix}-web-"
  vpc_id      = data.aws_vpc.default.id
  description = "Web server security group"

  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"]
  }

  lifecycle {
    create_before_destroy = true
  }

  tags = local.common_tags
}
```

***

## Modules

Modules let you package reusable infrastructure patterns. The standard layout for a module:

```
modules/
  ec2-web-server/
    main.tf
    variables.tf
    outputs.tf
    README.md
```

<CodeGroup>
  ```hcl modules/ec2-web-server/main.tf theme={null}
  resource "aws_instance" "this" {
    ami                    = var.ami_id
    instance_type          = var.instance_type
    subnet_id              = var.subnet_id
    vpc_security_group_ids = var.security_group_ids
    iam_instance_profile   = var.instance_profile_name
    user_data              = var.user_data

    root_block_device {
      volume_type           = "gp3"
      volume_size           = var.root_volume_size
      delete_on_termination = true
      encrypted             = true
    }

    tags = merge(var.tags, {
      Name = var.name
    })
  }
  ```

  ```hcl modules/ec2-web-server/variables.tf theme={null}
  variable "name"               { type = string }
  variable "ami_id"             { type = string }
  variable "subnet_id"          { type = string }
  variable "security_group_ids" { type = list(string) }

  variable "instance_type" {
    type    = string
    default = "t3.micro"
  }

  variable "instance_profile_name" {
    type    = string
    default = null
  }

  variable "user_data" {
    type    = string
    default = null
  }

  variable "root_volume_size" {
    type    = number
    default = 20
  }

  variable "tags" {
    type    = map(string)
    default = {}
  }
  ```

  ```hcl modules/ec2-web-server/outputs.tf theme={null}
  output "instance_id"  { value = aws_instance.this.id }
  output "private_ip"   { value = aws_instance.this.private_ip }
  output "public_ip"    { value = aws_instance.this.public_ip }
  ```

  ```hcl Call the module from a root config theme={null}
  module "web_server" {
    source = "./modules/ec2-web-server"

    name               = "${local.name_prefix}-web"
    ami_id             = data.aws_ami.amazon_linux.id
    instance_type      = var.instance_type
    subnet_id          = data.aws_subnets.public.ids[0]
    security_group_ids = [aws_security_group.web.id]
    root_volume_size   = 30
    tags               = local.common_tags
  }
  ```
</CodeGroup>

***

## Complete Example: AWS EC2 Instance

A self-contained, working Terraform configuration for a single EC2 instance with a security group, S3 bucket, and IAM role.

<CodeGroup>
  ```hcl main.tf theme={null}
  terraform {
    required_version = ">= 1.6.0"
    required_providers {
      aws = { source = "hashicorp/aws", version = "~> 5.0" }
    }
  }

  provider "aws" {
    region = var.aws_region
    default_tags {
      tags = {
        ManagedBy   = "terraform"
        Environment = var.environment
        Project     = var.project_name
      }
    }
  }

  # --- Data sources ---
  data "aws_vpc" "selected" {
    default = true
  }

  data "aws_subnets" "default" {
    filter {
      name   = "vpc-id"
      values = [data.aws_vpc.selected.id]
    }
  }

  data "aws_ami" "amazon_linux" {
    most_recent = true
    owners      = ["amazon"]
    filter {
      name   = "name"
      values = ["al2023-ami-*-x86_64"]
    }
  }

  # --- IAM role for EC2 ---
  resource "aws_iam_role" "ec2_role" {
    name = "${var.project_name}-${var.environment}-ec2-role"

    assume_role_policy = jsonencode({
      Version = "2012-10-17"
      Statement = [{
        Effect    = "Allow"
        Principal = { Service = "ec2.amazonaws.com" }
        Action    = "sts:AssumeRole"
      }]
    })
  }

  resource "aws_iam_role_policy_attachment" "ssm" {
    role       = aws_iam_role.ec2_role.name
    policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
  }

  resource "aws_iam_instance_profile" "ec2" {
    name = "${var.project_name}-${var.environment}-ec2-profile"
    role = aws_iam_role.ec2_role.name
  }

  # --- Security group ---
  resource "aws_security_group" "web" {
    name_prefix = "${var.project_name}-${var.environment}-web-"
    vpc_id      = data.aws_vpc.selected.id
    description = "Web server - HTTP/HTTPS only"

    ingress {
      from_port   = 80
      to_port     = 80
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
      description = "HTTP"
    }

    ingress {
      from_port   = 443
      to_port     = 443
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
      description = "HTTPS"
    }

    egress {
      from_port   = 0
      to_port     = 0
      protocol    = "-1"
      cidr_blocks = ["0.0.0.0/0"]
    }

    lifecycle { create_before_destroy = true }
  }

  # --- EC2 instance ---
  resource "aws_instance" "web_server" {
    ami                    = data.aws_ami.amazon_linux.id
    instance_type          = var.instance_type
    subnet_id              = data.aws_subnets.default.ids[0]
    vpc_security_group_ids = [aws_security_group.web.id]
    iam_instance_profile   = aws_iam_instance_profile.ec2.name

    user_data = <<-EOF
      #!/bin/bash
      dnf update -y
      dnf install -y nginx
      systemctl enable --now nginx
      echo "<h1>Hello from ${var.project_name} (${var.environment})</h1>" \
        > /usr/share/nginx/html/index.html
    EOF

    root_block_device {
      volume_type           = "gp3"
      volume_size           = 20
      encrypted             = true
      delete_on_termination = true
    }

    tags = { Name = "${var.project_name}-${var.environment}-web" }
  }

  # --- S3 bucket for app assets ---
  resource "aws_s3_bucket" "app_assets" {
    bucket = "${var.project_name}-${var.environment}-assets-${data.aws_caller_identity.current.account_id}"
  }

  resource "aws_s3_bucket_versioning" "app_assets" {
    bucket = aws_s3_bucket.app_assets.id
    versioning_configuration { status = "Enabled" }
  }

  resource "aws_s3_bucket_public_access_block" "app_assets" {
    bucket                  = aws_s3_bucket.app_assets.id
    block_public_acls       = true
    ignore_public_acls      = true
    block_public_policy     = true
    restrict_public_buckets = true
  }

  data "aws_caller_identity" "current" {}
  ```

  ```hcl variables.tf theme={null}
  variable "aws_region" {
    type    = string
    default = "us-east-1"
  }

  variable "environment" {
    type    = string
    default = "dev"
  }

  variable "project_name" {
    type    = string
    default = "myapp"
  }

  variable "instance_type" {
    type    = string
    default = "t3.micro"
  }
  ```

  ```hcl outputs.tf theme={null}
  output "instance_id"       { value = aws_instance.web_server.id }
  output "instance_public_ip"{ value = aws_instance.web_server.public_ip }
  output "bucket_name"       { value = aws_s3_bucket.app_assets.id }
  ```
</CodeGroup>

***

## Useful Commands and Tips

```bash theme={null}
# Show the current state (list all managed resources)
terraform state list

# Inspect a specific resource in state
terraform state show aws_instance.web_server

# Move a resource to a new address (after rename/refactor)
terraform state mv \
  aws_instance.web_server \
  module.web_server.aws_instance.this

# Remove a resource from state without destroying it
# (useful when importing existing infra management)
terraform state rm aws_s3_bucket.legacy

# Import an existing AWS resource into state
terraform import aws_s3_bucket.existing my-existing-bucket-name

# Refresh state to match real infrastructure
terraform apply -refresh-only

# Graph resource dependencies (requires graphviz)
terraform graph | dot -Tsvg > graph.svg

# Show what the workspace thinks is deployed
terraform show

# Taint a resource to force recreation on next apply
terraform taint aws_instance.web_server   # Terraform < 1.0
terraform apply -replace=aws_instance.web_server  # Terraform ≥ 1.0

# Output a specific value
terraform output instance_public_ip
terraform output -json  # all outputs as JSON
```

<Tip>
  Use **workspaces** (`terraform workspace new staging`) for managing multiple environments from a single config, but be aware they share the same backend bucket. Many teams prefer separate state files per environment with different `key` paths in the S3 backend — it's more explicit and easier to reason about access controls.
</Tip>

***

## Related Notes

<CardGroup cols={2}>
  <Card title="AWS Reference" icon="aws" href="cloud/aws">
    AWS CLI commands and service details that pair with Terraform-provisioned infrastructure.
  </Card>

  <Card title="GCP & Azure" icon="cloud" href="cloud/gcp-azure">
    Apply the same Terraform patterns to GCP and Azure by swapping the provider block.
  </Card>

  <Card title="GitLab CI/CD" icon="gitlab" href="devops/gitlab-cicd">
    Automate Terraform plan and apply in CI/CD pipelines with GitLab.
  </Card>

  <Card title="FinOps & Cost Management" icon="dollar-sign" href="cloud/finops">
    Tag strategy and cost allocation practices to bake into Terraform from day one.
  </Card>
</CardGroup>
