Skip to content
Published on

Terraform IaC Complete Guide 2025: Managing AWS/GCP Infrastructure as Code

Authors

Introduction

The era of managing infrastructure manually is over. When cloud resources grow to dozens or hundreds, console clicking makes reproducibility, audit trails, and collaboration impossible. Terraform is an open-source IaC (Infrastructure as Code) tool by HashiCorp, supporting over 3,000 providers including AWS, GCP, and Azure.

This guide covers everything from Terraform basics to production use. HCL syntax, State management, module design, real AWS/GCP infrastructure, Terragrunt, CI/CD automation, security scanning, and cost estimation — all the knowledge needed to build production-level IaC.


1. Why IaC Matters

1.1 Manual Management vs IaC

AspectManual (Console)IaC (Terraform)
ReproducibilityImpossible100% via code
Audit TrailLimitedGit history
CollaborationImpossiblePR reviews
Environment CloneHoursMinutes
RollbackManual recoveryterraform apply
DocumentationSeparateCode is the doc

1.2 Terraform vs Other IaC Tools

ToolLanguageCloudStateLearning Curve
TerraformHCLMulti-cloudState fileMedium
CloudFormationJSON/YAMLAWS onlyStacksLow
PulumiPython/TS/GoMulti-cloudState fileHigh
AWS CDKPython/TS/GoAWS onlyCloudFormationHigh
OpenTofuHCLMulti-cloudState fileMedium

Why choose Terraform:

  • Multi-cloud support (AWS + GCP + Azure)
  • Largest community and module registry
  • Declarative syntax makes intent clear
  • Plan -> Apply workflow for safe changes

2. Terraform Fundamentals

2.1 HCL Basic Syntax

# Provider configuration
terraform {
  required_version = ">= 1.7.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.40"
    }
  }

  backend "s3" {
    bucket         = "my-terraform-state"
    key            = "prod/terraform.tfstate"
    region         = "ap-northeast-2"
    dynamodb_table = "terraform-locks"
    encrypt        = true
  }
}

provider "aws" {
  region = var.aws_region

  default_tags {
    tags = {
      Environment = var.environment
      ManagedBy   = "terraform"
      Project     = var.project_name
    }
  }
}

2.2 Variables

# variables.tf
variable "aws_region" {
  description = "AWS region to deploy resources"
  type        = string
  default     = "ap-northeast-2"
}

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

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

variable "instance_config" {
  description = "EC2 instance configuration"
  type = object({
    instance_type     = string
    volume_size       = number
    enable_monitoring = bool
  })
  default = {
    instance_type     = "t3.medium"
    volume_size       = 50
    enable_monitoring = true
  }
}

variable "allowed_cidrs" {
  description = "List of CIDR blocks allowed for ingress"
  type        = list(string)
  default     = ["10.0.0.0/8"]
}

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

2.3 Resources

# main.tf
resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true

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

resource "aws_subnet" "public" {
  count = length(var.availability_zones)

  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index)
  availability_zone = var.availability_zones[count.index]

  map_public_ip_on_launch = true

  tags = {
    Name = "public-subnet-${count.index + 1}"
    Type = "public"
  }
}

2.4 Data Sources

# Latest Amazon Linux 2023 AMI lookup
data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["amazon"]

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

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

# Current AWS account info
data "aws_caller_identity" "current" {}

# Current region
data "aws_region" "current" {}

2.5 Outputs

# outputs.tf
output "vpc_id" {
  description = "VPC ID"
  value       = aws_vpc.main.id
}

output "public_subnet_ids" {
  description = "Public subnet IDs"
  value       = aws_subnet.public[*].id
}

output "db_password" {
  description = "Database password"
  value       = random_password.db.result
  sensitive   = true
}

2.6 Locals

locals {
  name_prefix = "${var.project_name}-${var.environment}"

  common_tags = merge(var.tags, {
    Environment = var.environment
    ManagedBy   = "terraform"
    Project     = var.project_name
  })

  env_config = {
    dev = {
      instance_type = "t3.small"
      min_size      = 1
      max_size      = 2
    }
    staging = {
      instance_type = "t3.medium"
      min_size      = 2
      max_size      = 4
    }
    prod = {
      instance_type = "t3.large"
      min_size      = 3
      max_size      = 10
    }
  }

  config = local.env_config[var.environment]
}

3. State Management

3.1 What is State?

Terraform State stores mapping information between actual infrastructure and code. All resource states are recorded as JSON in the terraform.tfstate file.

Without State:

  • Terraform cannot know which resources it manages
  • Plan tries to create all resources from scratch
  • Cannot track dependencies between resources

3.2 Remote State (S3 + DynamoDB)

# backend.tf
terraform {
  backend "s3" {
    bucket         = "mycompany-terraform-state"
    key            = "environments/prod/terraform.tfstate"
    region         = "ap-northeast-2"
    dynamodb_table = "terraform-locks"
    encrypt        = true
  }
}

Bootstrap S3 bucket and DynamoDB table:

# bootstrap/main.tf
resource "aws_s3_bucket" "terraform_state" {
  bucket = "mycompany-terraform-state"

  lifecycle {
    prevent_destroy = true
  }
}

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

resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "aws:kms"
    }
  }
}

resource "aws_dynamodb_table" "terraform_locks" {
  name         = "terraform-locks"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"

  attribute {
    name = "LockID"
    type = "S"
  }
}

3.3 State Commands

# List resources in state
terraform state list

# Show details for a specific resource
terraform state show aws_vpc.main

# Rename a resource (during refactoring)
terraform state mv aws_instance.old aws_instance.new

# Remove resource from state (keeps actual infrastructure)
terraform state rm aws_instance.legacy

# Pull remote state
terraform state pull > backup.tfstate

4. Modules

4.1 Creating Modules

modules/
  vpc/
    main.tf
    variables.tf
    outputs.tf
  ec2/
    main.tf
    variables.tf
    outputs.tf
  rds/
    main.tf
    variables.tf
    outputs.tf
# modules/vpc/main.tf
resource "aws_vpc" "this" {
  cidr_block           = var.cidr_block
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = merge(var.tags, {
    Name = "${var.name_prefix}-vpc"
  })
}

resource "aws_subnet" "public" {
  count = length(var.public_subnet_cidrs)

  vpc_id                  = aws_vpc.this.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.tags, {
    Name = "${var.name_prefix}-public-${count.index + 1}"
    Type = "public"
  })
}

resource "aws_subnet" "private" {
  count = length(var.private_subnet_cidrs)

  vpc_id            = aws_vpc.this.id
  cidr_block        = var.private_subnet_cidrs[count.index]
  availability_zone = var.availability_zones[count.index]

  tags = merge(var.tags, {
    Name = "${var.name_prefix}-private-${count.index + 1}"
    Type = "private"
  })
}

resource "aws_internet_gateway" "this" {
  vpc_id = aws_vpc.this.id

  tags = merge(var.tags, {
    Name = "${var.name_prefix}-igw"
  })
}

resource "aws_nat_gateway" "this" {
  count = var.enable_nat_gateway ? length(var.public_subnet_cidrs) : 0

  allocation_id = aws_eip.nat[count.index].id
  subnet_id     = aws_subnet.public[count.index].id

  tags = merge(var.tags, {
    Name = "${var.name_prefix}-nat-${count.index + 1}"
  })
}

resource "aws_eip" "nat" {
  count  = var.enable_nat_gateway ? length(var.public_subnet_cidrs) : 0
  domain = "vpc"
}
# modules/vpc/outputs.tf
output "vpc_id" {
  value = aws_vpc.this.id
}

output "public_subnet_ids" {
  value = aws_subnet.public[*].id
}

output "private_subnet_ids" {
  value = aws_subnet.private[*].id
}

4.2 Using Modules

# environments/prod/main.tf
module "vpc" {
  source = "../../modules/vpc"

  name_prefix = "prod"
  cidr_block  = "10.0.0.0/16"

  public_subnet_cidrs  = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
  private_subnet_cidrs = ["10.0.11.0/24", "10.0.12.0/24", "10.0.13.0/24"]
  availability_zones   = ["ap-northeast-2a", "ap-northeast-2b", "ap-northeast-2c"]

  enable_nat_gateway = true
  tags               = local.common_tags
}

4.3 Terraform Registry Modules

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.5.0"

  name = "my-vpc"
  cidr = "10.0.0.0/16"

  azs             = ["ap-northeast-2a", "ap-northeast-2b", "ap-northeast-2c"]
  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_nat_gateway = true
  single_nat_gateway = false

  tags = local.common_tags
}

5. Real AWS Infrastructure

5.1 VPC + EC2 + ALB + RDS

resource "aws_launch_template" "app" {
  name_prefix   = "${local.name_prefix}-app-"
  image_id      = data.aws_ami.amazon_linux.id
  instance_type = local.config.instance_type

  vpc_security_group_ids = [aws_security_group.app.id]

  user_data = base64encode(templatefile("userdata.sh", {
    environment = var.environment
    db_host     = aws_db_instance.main.endpoint
  }))

  iam_instance_profile {
    name = aws_iam_instance_profile.app.name
  }

  block_device_mappings {
    device_name = "/dev/xvda"
    ebs {
      volume_size = var.instance_config.volume_size
      volume_type = "gp3"
      encrypted   = true
    }
  }
}

resource "aws_autoscaling_group" "app" {
  name                = "${local.name_prefix}-app-asg"
  desired_capacity    = local.config.min_size
  min_size            = local.config.min_size
  max_size            = local.config.max_size
  vpc_zone_identifier = module.vpc.private_subnet_ids
  target_group_arns   = [aws_lb_target_group.app.arn]

  launch_template {
    id      = aws_launch_template.app.id
    version = "$Latest"
  }

  health_check_type         = "ELB"
  health_check_grace_period = 300
}

5.2 ALB Configuration

resource "aws_lb" "app" {
  name               = "${local.name_prefix}-alb"
  internal           = false
  load_balancer_type = "application"
  security_groups    = [aws_security_group.alb.id]
  subnets            = module.vpc.public_subnet_ids

  enable_deletion_protection = var.environment == "prod"
  tags                       = local.common_tags
}

resource "aws_lb_target_group" "app" {
  name     = "${local.name_prefix}-app-tg"
  port     = 8080
  protocol = "HTTP"
  vpc_id   = module.vpc.vpc_id

  health_check {
    path                = "/actuator/health"
    port                = "traffic-port"
    healthy_threshold   = 3
    unhealthy_threshold = 3
    timeout             = 5
    interval            = 30
    matcher             = "200"
  }

  deregistration_delay = 30
}

resource "aws_lb_listener" "https" {
  load_balancer_arn = aws_lb.app.arn
  port              = 443
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-TLS13-1-2-2021-06"
  certificate_arn   = aws_acm_certificate.main.arn

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.app.arn
  }
}

5.3 RDS (PostgreSQL)

resource "aws_db_instance" "main" {
  identifier = "${local.name_prefix}-db"

  engine         = "postgres"
  engine_version = "15.5"
  instance_class = "db.r6g.large"

  allocated_storage     = 100
  max_allocated_storage = 500
  storage_type          = "gp3"
  storage_encrypted     = true

  db_name  = "myapp"
  username = "admin"
  password = random_password.db.result

  multi_az               = var.environment == "prod"
  db_subnet_group_name   = aws_db_subnet_group.main.name
  vpc_security_group_ids = [aws_security_group.db.id]

  backup_retention_period = 7
  skip_final_snapshot     = var.environment != "prod"

  performance_insights_enabled = true
  tags                         = local.common_tags
}

6. Real GCP Infrastructure

6.1 VPC + GKE

provider "google" {
  project = var.gcp_project_id
  region  = var.gcp_region
}

resource "google_compute_network" "main" {
  name                    = "${local.name_prefix}-vpc"
  auto_create_subnetworks = false
}

resource "google_compute_subnetwork" "main" {
  name          = "${local.name_prefix}-subnet"
  ip_cidr_range = "10.0.0.0/20"
  region        = var.gcp_region
  network       = google_compute_network.main.id

  secondary_ip_range {
    range_name    = "pods"
    ip_cidr_range = "10.1.0.0/16"
  }

  secondary_ip_range {
    range_name    = "services"
    ip_cidr_range = "10.2.0.0/20"
  }
}

resource "google_container_cluster" "primary" {
  name     = "${local.name_prefix}-gke"
  location = var.gcp_region

  remove_default_node_pool = true
  initial_node_count       = 1

  network    = google_compute_network.main.name
  subnetwork = google_compute_subnetwork.main.name

  ip_allocation_policy {
    cluster_secondary_range_name  = "pods"
    services_secondary_range_name = "services"
  }

  workload_identity_config {
    workload_pool = "${var.gcp_project_id}.svc.id.goog"
  }

  release_channel {
    channel = "REGULAR"
  }
}

resource "google_container_node_pool" "primary" {
  name       = "${local.name_prefix}-node-pool"
  location   = var.gcp_region
  cluster    = google_container_cluster.primary.name
  node_count = 3

  node_config {
    machine_type = "e2-standard-4"
    disk_size_gb = 100
    disk_type    = "pd-ssd"
  }

  autoscaling {
    min_node_count = 3
    max_node_count = 10
  }
}

6.2 Cloud SQL + Cloud Run

resource "google_sql_database_instance" "main" {
  name             = "${local.name_prefix}-db"
  database_version = "POSTGRES_15"
  region           = var.gcp_region

  settings {
    tier = "db-custom-4-16384"

    ip_configuration {
      ipv4_enabled    = false
      private_network = google_compute_network.main.id
    }

    backup_configuration {
      enabled                        = true
      point_in_time_recovery_enabled = true
      start_time                     = "03:00"
    }

    availability_type = var.environment == "prod" ? "REGIONAL" : "ZONAL"
  }

  deletion_protection = var.environment == "prod"
}

resource "google_cloud_run_v2_service" "app" {
  name     = "${local.name_prefix}-app"
  location = var.gcp_region

  template {
    containers {
      image = "gcr.io/${var.gcp_project_id}/myapp:latest"

      ports {
        container_port = 8080
      }

      resources {
        limits = {
          cpu    = "2"
          memory = "1Gi"
        }
      }

      env {
        name  = "SPRING_PROFILES_ACTIVE"
        value = var.environment
      }

      startup_probe {
        http_get {
          path = "/actuator/health"
        }
        initial_delay_seconds = 10
        period_seconds        = 3
      }
    }

    scaling {
      min_instance_count = var.environment == "prod" ? 2 : 0
      max_instance_count = 10
    }
  }
}

7. Advanced Patterns

7.1 Workspaces

# Create workspaces
terraform workspace new dev
terraform workspace new staging
terraform workspace new prod

# Switch workspace
terraform workspace select prod
locals {
  env_config = {
    dev     = { instance_type = "t3.small", db_instance = "db.t3.medium" }
    staging = { instance_type = "t3.medium", db_instance = "db.r6g.large" }
    prod    = { instance_type = "t3.large", db_instance = "db.r6g.xlarge" }
  }

  config = local.env_config[terraform.workspace]
}

7.2 Dynamic Blocks

resource "aws_security_group" "app" {
  name        = "${local.name_prefix}-app-sg"
  description = "Application security group"
  vpc_id      = module.vpc.vpc_id

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

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

7.3 for_each vs count

# count -- index-based (recreation issues on deletion)
resource "aws_subnet" "example" {
  count      = 3
  cidr_block = "10.0.${count.index}.0/24"
  vpc_id     = aws_vpc.main.id
}

# for_each -- key-based (safe deletion/addition)
resource "aws_subnet" "example" {
  for_each = {
    "public-a"  = { cidr = "10.0.1.0/24", az = "ap-northeast-2a" }
    "public-b"  = { cidr = "10.0.2.0/24", az = "ap-northeast-2b" }
    "private-a" = { cidr = "10.0.11.0/24", az = "ap-northeast-2a" }
  }

  cidr_block        = each.value.cidr
  availability_zone = each.value.az
  vpc_id            = aws_vpc.main.id

  tags = {
    Name = "${local.name_prefix}-${each.key}"
  }
}

7.4 Lifecycle Rules

resource "aws_instance" "app" {
  ami           = data.aws_ami.amazon_linux.id
  instance_type = local.config.instance_type

  lifecycle {
    create_before_destroy = true
    prevent_destroy       = false
    ignore_changes        = [ami]
  }
}

moved {
  from = aws_instance.old_name
  to   = aws_instance.new_name
}

8. Import and Migration

8.1 terraform import Command

terraform import aws_vpc.main vpc-0123456789abcdef0
terraform import aws_instance.web i-0123456789abcdef0

8.2 Import Block (Terraform 1.5+)

import {
  to = aws_vpc.main
  id = "vpc-0123456789abcdef0"
}

import {
  to = aws_instance.web
  id = "i-0123456789abcdef0"
}
# Auto-generate code
terraform plan -generate-config-out=generated.tf

# Review and apply
terraform apply

8.3 Migration Strategy

Step-by-step approach:

  1. Inventory — List current resources
  2. Write Code — Create HCL for each resource
  3. Import — Bring existing resources into State
  4. Verify Plan — Confirm terraform plan shows no changes
  5. Gradual Adoption — Create new resources with Terraform first

9. Terragrunt

9.1 DRY Environment Management

# infrastructure/terragrunt.hcl (root)
remote_state {
  backend = "s3"
  generate = {
    path      = "backend.tf"
    if_exists = "overwrite"
  }
  config = {
    bucket         = "mycompany-terraform-state"
    key            = "${path_relative_to_include()}/terraform.tfstate"
    region         = "ap-northeast-2"
    dynamodb_table = "terraform-locks"
    encrypt        = true
  }
}

generate "provider" {
  path      = "provider.tf"
  if_exists = "overwrite"
  contents  = <<EOF
provider "aws" {
  region = "ap-northeast-2"
}
EOF
}
# infrastructure/environments/prod/vpc/terragrunt.hcl
include "root" {
  path = find_in_parent_folders()
}

terraform {
  source = "../../../modules/vpc"
}

inputs = {
  name_prefix          = "prod"
  cidr_block           = "10.0.0.0/16"
  public_subnet_cidrs  = ["10.0.1.0/24", "10.0.2.0/24"]
  private_subnet_cidrs = ["10.0.11.0/24", "10.0.12.0/24"]
  availability_zones   = ["ap-northeast-2a", "ap-northeast-2b"]
  enable_nat_gateway   = true
}

9.2 Dependency Management

# infrastructure/environments/prod/rds/terragrunt.hcl
include "root" {
  path = find_in_parent_folders()
}

terraform {
  source = "../../../modules/rds"
}

dependency "vpc" {
  config_path = "../vpc"
}

inputs = {
  name_prefix = "prod"
  vpc_id      = dependency.vpc.outputs.vpc_id
  subnet_ids  = dependency.vpc.outputs.private_subnet_ids
}

10. CI/CD Automation

10.1 GitHub Actions + Terraform

# .github/workflows/terraform.yml
name: Terraform CI/CD

on:
  pull_request:
    paths:
      - 'infrastructure/**'
  push:
    branches:
      - main
    paths:
      - 'infrastructure/**'

permissions:
  id-token: write
  contents: read
  pull-requests: write

jobs:
  plan:
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/terraform-ci
          aws-region: ap-northeast-2

      - uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: 1.7.0

      - name: Terraform Init
        working-directory: infrastructure/environments/prod
        run: terraform init

      - name: Terraform Plan
        working-directory: infrastructure/environments/prod
        run: terraform plan -no-color -out=tfplan

  apply:
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4

      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/terraform-ci
          aws-region: ap-northeast-2

      - uses: hashicorp/setup-terraform@v3

      - name: Terraform Init & Apply
        working-directory: infrastructure/environments/prod
        run: |
          terraform init
          terraform apply -auto-approve

11. Cost Management (Infracost)

# Run Infracost
infracost breakdown --path=infrastructure/environments/prod

# JSON output for CI
infracost breakdown \
  --path=infrastructure/environments/prod \
  --format=json \
  --out-file=infracost.json

Tagging Strategy

locals {
  required_tags = {
    Environment = var.environment
    Project     = var.project_name
    Team        = var.team_name
    CostCenter  = var.cost_center
    ManagedBy   = "terraform"
  }
}

12. Security (tfsec, Checkov)

12.1 tfsec Static Analysis

tfsec ./infrastructure/

12.2 Checkov

checkov -d ./infrastructure/ --framework terraform

12.3 Secrets Management (Vault)

data "vault_generic_secret" "db" {
  path = "secret/data/myapp/database"
}

resource "aws_db_instance" "main" {
  username = data.vault_generic_secret.db.data["username"]
  password = data.vault_generic_secret.db.data["password"]
}

13. Terraform Certification Guide

HashiCorp Certified: Terraform Associate (003)

Exam overview:

  • Duration: 60 minutes
  • Questions: 57 (multiple choice + fill in the blank)
  • Passing score: 70%
  • Validity: 2 years
  • Cost: approximately 70 USD

Core domains:

  1. IaC Concepts (15%) — IaC benefits, Terraform workflow
  2. Terraform Fundamentals (20%) — Providers, resources, variables, outputs
  3. State Management (15%) — Remote state, locking, commands
  4. Modules (15%) — Creating, using, registry
  5. Terraform CLI (15%) — init, plan, apply, destroy, import
  6. Workflow (10%) — Team workflows, Terraform Cloud
  7. Implementation and Maintenance (10%) — Debugging, upgrades

14. Interview Questions (15)

Basic (1-5)

Q1. Describe the core Terraform workflow.

Write (author code) -> Plan (preview changes) -> Apply (execute). terraform plan shows what will change without modifying infrastructure, and terraform apply actually changes it.

Q2. What is the role and importance of Terraform State?

State stores the mapping between actual infrastructure and code. Without it, Terraform cannot know which resources it manages. Use remote backends (S3+DynamoDB) for team sharing and locking.

Q3. What is the difference between a Provider and a Resource?

A Provider is a plugin handling API connections to clouds/services (AWS, GCP, etc.). A Resource is an actual infrastructure object (VPC, EC2, etc.) managed through a Provider.

Q4. Difference between terraform plan and terraform apply?

plan shows a preview of changes without modifying infrastructure. apply actually modifies infrastructure. apply internally runs plan first.

Q5. Explain variable precedence in Terraform.

Environment variables TF_VAR_xxx, terraform.tfvars, terraform.tfvars.json, auto.tfvars, -var or -var-file CLI options in increasing priority order.

Advanced (6-10)

Q6. Difference between count and for_each, and when to use each?

count is index-based; deleting a middle element recreates all subsequent resources. for_each is key-based; specific elements can be safely added/removed. Generally prefer for_each.

Q7. Why is State Locking needed?

If two people run terraform apply simultaneously, State conflicts can corrupt infrastructure. DynamoDB lock tables prevent concurrent execution.

Q8. Module benefits and design principles?

Reusability, DRY principle, and encapsulation. Design with single responsibility, externalize settings via variables, connect to other modules via outputs.

Q9. Difference between terraform import and Import Block?

terraform import imports one resource at a time via CLI without auto-generating code. Import Block (1.5+) declaratively imports multiple resources at once with -generate-config-out for auto code generation.

Q10. How to safely manage secrets in Terraform?

State files store sensitive data, so encrypt remote backends. Use sensitive = true variables, integrate HashiCorp Vault or AWS Secrets Manager, add .tfvars to .gitignore, inject via environment variables.

Practical (11-15)

Q11. What is Drift Detection and how do you handle it?

Drift is when actual infrastructure differs from Terraform code. Detect with terraform plan, resolve by applying code state or updating code. Run periodic plans to monitor drift.

Q12. Why use Terragrunt?

Compensates for Terraform's DRY principle gaps. Manages environment configs via inheritance, automates remote State setup, and handles inter-module dependencies.

Q13. Terraform CI/CD pipeline best practices?

Run plan on PRs and post results as comments. Apply on main branch merge. Use OIDC for AWS auth (no secret keys). Validate with terraform fmt + tfsec. Apply environment-specific approval processes (GitHub Environments).

Q14. When should you use Terraform alongside CloudFormation?

Use CFN when AWS-only and needing native features like StackSets or Service Catalog. Use Terraform for multi-cloud or SaaS resource management.

Q15. How to manage costs with Infracost?

Auto-estimates cost impact of infrastructure changes on PRs. Visualizes monthly cost increases/decreases so teams approve changes with cost awareness.


15. Quiz

Q1. What AWS service is needed for State Locking when using S3 as remote backend?

DynamoDB. S3 stores the State file, while DynamoDB provides the lock table to prevent concurrent execution. Configure with the dynamodb_table attribute.

Q2. Why is for_each preferred over count?

count is index-based, so removing a middle element recreates all subsequent resources. for_each is key-based, allowing safe addition and removal of specific elements.

Q3. What is the purpose of terraform plan -generate-config-out?

It auto-generates HCL code for resources declared in Import Blocks. When migrating existing infrastructure to Terraform, you can refine the auto-generated code instead of writing it from scratch.

Q4. What is the role of Terragrunt's dependency block?

It manages execution order and data passing between modules. For example, if an RDS module depends on a VPC module's outputs (subnet_ids), Terragrunt applies VPC first and passes its outputs to the RDS module.

Q5. What is the difference between tfsec and Checkov?

tfsec is a Terraform-specific security scanner that checks HCL code for security vulnerabilities. Checkov is a general-purpose policy checking tool supporting Terraform, CloudFormation, Kubernetes, Docker, and more.


References

  1. Terraform Official Documentation
  2. Terraform Registry
  3. Terraform AWS Provider
  4. Terraform GCP Provider
  5. Terragrunt Documentation
  6. Infracost Documentation
  7. tfsec Guide
  8. Checkov Documentation
  9. HashiCorp Learn Terraform
  10. Terraform Up and Running (O'Reilly)
  11. AWS VPC Terraform Module
  12. Terraform Best Practices
  13. Atlantis Documentation
  14. OpenTofu
  15. HashiCorp Vault Terraform Provider
  16. Terraform Associate Exam Guide
  17. AWS IAM Terraform Best Practices
  18. GKE Terraform Module