DevOps & Cloud

Terraform: Infrastructure as Code

Tutorial lengkap Terraform untuk Infrastructure as Code β€” konsep IaC, HCL syntax, providers, resources, state management, modules, dan best practices untuk mengelola infrastruktur cloud secara terprogram

1. Konsep Infrastructure as Code

Infrastructure as Code (IaC) adalah pendekatan pengelolaan infrastruktur IT melalui kode yang dapat dibaca manusia, bukan melalui konfigurasi manual melalui web console. Dengan IaC, Anda bisa membuat, memodifikasi, dan menghapus infrastruktur cloud secara konsisten dan reproducible.

Terraform adalah tool IaC open-source yang dikembangkan oleh HashiCorp. Terraform menggunakan bahasa deklaratif bernama HCL (HashiCorp Configuration Language) untuk mendeskripsikan infrastruktur yang diinginkan, lalu menghitung dan menjalankan langkah-langkah yang diperlukan untuk mencapai state tersebut.

Mengapa Terraform?

Keunggulan Penjelasan
Multi-CloudBisa mengelola infrastruktur di AWS, GCP, Azure, dan 1000+ provider lainnya
DeclarativeCukup deskripsikan akhir state yang diinginkan, Terraform menangani cara mencapainya
Version ControlInfrastruktur di-version sebagai kode di Git β€” bisa track perubahan dan rollback
ReusableModul bisa digunakan kembali di berbagai project dan environment
PlanningFitur terraform plan menunjukkan perubahan sebelum diterapkan
State TrackingTerraform melacak state aktual infrastruktur dan membandingkannya dengan desired state
Dependency GraphSecara otomatis menentukan urutan pembuatan resource berdasarkan dependency

IaC vs Manual Provisioning

Aspek Manual (Console) IaC (Terraform)
KecepatanπŸ”΄ Lambat, satu per satu🟒 Cepat, sekaligus
KonsistensiπŸ”΄ Rentan human error🟒 Identik setiap kali
DokumentasiπŸ”΄ Dokumentasi terpisah🟒 Kode = dokumentasi
ReproduksiπŸ”΄ Sulit mengulang🟒 Jalankan ulang kapan saja
CollaborationπŸ”΄ Satu orang tahu🟒 Semua orang bisa baca kode
RollbackπŸ”΄ Manual dan rumit🟒 terraform apply versi sebelumnya
ScalabilityπŸ”΄ Tidak practical🟒 Scale dengan mudah
Diagram: Cara Kerja Terraform
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                TERRAFORM WORKFLOW                        β”‚
β”‚                                                          β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”           β”‚
β”‚  β”‚ 1. Write │───▢│ 2. Plan  │───▢│ 3. Apply β”‚           β”‚
β”‚  β”‚    Code  β”‚    β”‚          β”‚    β”‚          β”‚           β”‚
β”‚  β”‚  (.tf)   β”‚    β”‚ Preview  β”‚    β”‚ Execute  β”‚           β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚ Changes  β”‚    β”‚ Changes  β”‚           β”‚
β”‚       β”‚          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜           β”‚
β”‚       β”‚               β”‚               β”‚                 β”‚
β”‚       β–Ό               β–Ό               β–Ό                 β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”          β”‚
β”‚  β”‚ HCL      β”‚    β”‚ Diff:    β”‚    β”‚ API      β”‚          β”‚
β”‚  β”‚ Config   β”‚    β”‚ Current  β”‚    β”‚ Calls to β”‚          β”‚
β”‚  β”‚ Files    β”‚    β”‚ vs       β”‚    β”‚ Cloud    β”‚          β”‚
β”‚  β”‚          β”‚    β”‚ Desired  β”‚    β”‚ Provider β”‚          β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜          β”‚
β”‚                                          β”‚              β”‚
β”‚                                          β–Ό              β”‚
β”‚                                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”        β”‚
β”‚                                    β”‚ State    β”‚        β”‚
β”‚                                    β”‚ File     β”‚        β”‚
β”‚                                    β”‚ (tfstate)β”‚        β”‚
β”‚                                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Install Terraform

Bash β€” Install Terraform
# ═══ macOS (Homebrew) ═══
brew tap hashicorp/tap
brew install hashicorp/tap/terraform

# ═══ Ubuntu/Debian ═══
wget -O- https://apt.releases.hashicorp.com/gpg | \
  sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] \
  https://apt.releases.hashicorp.com $(lsb_release -cs) main" | \
  sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraform

# ═══ Windows (Chocolatey) ═══
choco install terraform

# ═══ Verifikasi instalasi ═══
terraform version
# Terraform v1.10.x on linux_amd64

# ═══ Setup autocomplete (opsional) ═══
terraform -install-autocomplete
πŸ’‘ Terraform Cloud vs Open Source

Terraform tersedia dalam versi open-source (gratis) dan Terraform Cloud/Enterprise (berbayar). Untuk belajar dan proyek kecil, versi open-source sudah sangat cukup. Terraform Cloud menambahkan fitur team collaboration, remote state management, dan policy enforcement.

2. HCL Syntax

HCL (HashiCorp Configuration Language) adalah bahasa deklaratif yang digunakan oleh Terraform untuk mendeskripsikan infrastruktur. HCL dirancang agar mudah dibaca manusia sekaligus bisa diproses oleh mesin.

Dasar HCL

HCL β€” Sintaks Dasar
# ═══ Struktur dasar HCL ═══
# block_type "label" "label" {
#   key = value
#   nested_block {
#     key = value
#   }
# }

# ═══ Tipe Data ═══
# String
app_name = "beebane-api"

# Number
instance_count = 3

# Boolean
enable_monitoring = true

# List
availability_zones = ["us-east-1a", "us-east-1b", "us-east-1c"]

# Map
tags = {
  Environment = "production"
  Project     = "beebane"
  ManagedBy   = "terraform"
}

# ═══ Variable Declarations ═══
variable "region" {
  description = "AWS region untuk deployment"
  type        = string
  default     = "ap-southeast-1"  # Singapore
}

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

variable "enable_ssl" {
  description = "Aktifkan SSL certificate"
  type        = bool
  default     = true
}

variable "allowed_ports" {
  description = "Daftar port yang diizinkan"
  type        = list(number)
  default     = [80, 443, 22]
}

variable "environment_config" {
  description = "Konfigurasi per environment"
  type = map(object({
    instance_type = string
    min_size      = number
    max_size      = number
  }))
  default = {
    dev = {
      instance_type = "t3.micro"
      min_size      = 1
      max_size      = 2
    }
    staging = {
      instance_type = "t3.small"
      min_size      = 1
      max_size      = 3
    }
    production = {
      instance_type = "t3.medium"
      min_size      = 2
      max_size      = 10
    }
  }
}

# ═══ Output Declarations ═══
output "instance_public_ip" {
  description = "Public IP dari EC2 instance"
  value       = aws_instance.web_server.public_ip
}

output "database_endpoint" {
  description = "Endpoint database RDS"
  value       = aws_db_instance.main.endpoint
  sensitive   = true  # Tidak ditampilkan di terminal
}

# ═══ Locals β€” computed values ═══
locals {
  common_tags = {
    Project     = var.project_name
    Environment = var.environment
    ManagedBy   = "terraform"
    CreatedAt   = timestamp()
  }

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

Expressions & Functions

HCL β€” Expressions & Built-in Functions
# ═══ Conditional Expressions ═══
instance_type = var.environment == "production" ? "t3.medium" : "t3.micro"

# ═══ For Expressions ═══
# Create list dari transformation
instance_ids = [for instance in aws_instance.server : instance.id]

# Create map dari list
instance_map = { for az in var.availability_zones : az => "${az}-instance" }

# Filter
public_instances = [for i in aws_instance.server : i if i.public_ip != ""]

# ═══ Splat Expression ═══
# Shortcut untuk [for x in list : x.attribute]
instance_ids = aws_instance.server[*].id

# ═══ String Interpolation ═══
name_tag = "${var.project_name}-${var.environment}-server"

# ═══ Built-in Functions ═══

# String functions
upper("hello")          # "HELLO"
lower("WORLD")          # "world"
format("Hello, %s!", var.name)  # "Hello, Budi!"
join(", ", ["a", "b", "c"])    # "a, b, c"
split(",", "a,b,c")            # ["a", "b", "c"]
replace("hello-world", "-", "_")  # "hello_world"
trimspace("  hello  ")         # "hello"

# Number functions
max(5, 12, 9)          # 12
min(5, 12, 9)          # 5
ceil(4.2)              # 5
floor(4.8)             # 4

# Collection functions
length(["a", "b", "c"])       # 3
contains(["a", "b"], "a")     # true
flatten([["a", "b"], ["c"]])  # ["a", "b", "c"]
distinct(["a", "b", "a"])     # ["a", "b"]
merge({a=1}, {b=2})           # {a=1, b=2}
lookup({a=1, b=2}, "a", 0)   # 1

# File functions
file("${path.module}/config.json")
templatefile("${path.module}/init.tpl", { name = "server1" })
filebase64("${path.module}/cert.pem")

# Crypto functions
md5("hello")
sha256("hello")
bcrypt("password")

# Date/Time
timestamp()               # "2026-06-25T10:00:00Z"
formatdate("DD MMM YYYY", timestamp())  # "25 Jun 2026"

# IP functions
cidrsubnet("10.0.0.0/16", 8, 1)  # "10.0.1.0/24"
cidrhost("10.0.1.0/24", 5)       # "10.0.1.5"
πŸ“Œ HCL vs JSON

Terraform mendukung dua format file: .tf (HCL) dan .tf.json (JSON). HCL lebih mudah dibaca dan merupakan format yang direkomendasikan. JSON berguna untuk programmatically generate config, tetapi jauh lebih verbose dan sulit dibaca.

3. Providers

Provider adalah plugin Terraform yang berinteraksi dengan API cloud provider, SaaS, atau service lainnya. Provider meng-ekspose resource dan data source yang bisa Anda gunakan dalam konfigurasi Terraform. Terraform memiliki 3000+ provider dari berbagai vendor.

Provider Configuration

HCL β€” Provider Setup
# ═══ providers.tf ═══

# Required providers β€” mendefinisikan provider yang dibutuhkan
terraform {
  required_version = ">= 1.5.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
    google = {
      source  = "hashicorp/google"
      version = "~> 5.0"
    }
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.0"
    }
  }

  # Remote state storage (S3)
  backend "s3" {
    bucket         = "beebane-terraform-state"
    key            = "prod/terraform.tfstate"
    region         = "ap-southeast-1"
    dynamodb_table = "terraform-locks"
    encrypt        = true
  }
}

# ═══ AWS Provider ═══
provider "aws" {
  region  = var.aws_region
  profile = "beebane"

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

# ═══ Google Cloud Provider ═══
provider "google" {
  project = var.gcp_project_id
  region  = var.gcp_region
}

# ═══ Azure Provider ═══
provider "azurerm" {
  features {}
  subscription_id = var.azure_subscription_id
}

# ═══ Multiple AWS Regions ═══
# Untuk deploy ke beberapa region sekaligus
provider "aws" {
  alias  = "us_east"
  region = "us-east-1"
}

provider "aws" {
  alias  = "eu_west"
  region = "eu-west-1"
}

Install & Manage Providers

Bash β€” Terraform Commands
# ═══ Inisialisasi Terraform (download providers) ═══
terraform init

# Output:
# Initializing the backend...
# Initializing provider plugins...
# - Finding hashicorp/aws versions matching "~> 5.0"...
# - Installing hashicorp/aws v5.82.2...
# Terraform has been successfully initialized!

# ═══ Upgrade provider ke versi terbaru (sesuai constraint) ═══
terraform init -upgrade

# ═══ Format kode HCL agar konsisten ═══
terraform fmt
terraform fmt -recursive  # Termasuk sub-folder

# ═══ Validasi konfigurasi ═══
terraform validate

# ═══ Lihat daftar provider yang digunakan ═══
terraform providers

# ═══ Lihat dependency tree ═══
terraform graph | dot -Tpng > graph.png
Diagram: Terraform Provider Ecosystem
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              TERRAFORM PROVIDER ECOSYSTEM                β”‚
β”‚                                                          β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚              Terraform Core                        β”‚  β”‚
β”‚  β”‚  β€’ HCL Parser    β€’ State Manager                   β”‚  β”‚
β”‚  β”‚  β€’ Plan/Apply    β€’ Dependency Graph                β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚                          β”‚                               β”‚
β”‚        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”             β”‚
β”‚        β–Ό                 β–Ό                 β–Ό             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”       β”‚
β”‚  β”‚  AWS     β”‚    β”‚  Google  β”‚    β”‚  Kubernetes  β”‚       β”‚
β”‚  β”‚  Providerβ”‚    β”‚  Cloud   β”‚    β”‚  Provider    β”‚       β”‚
β”‚  β”‚          β”‚    β”‚  Providerβ”‚    β”‚              β”‚       β”‚
β”‚  β”‚ β€’ EC2    β”‚    β”‚ β€’ GCE    β”‚    β”‚ β€’ Pods       β”‚       β”‚
β”‚  β”‚ β€’ S3     β”‚    β”‚ β€’ GKE    β”‚    β”‚ β€’ Services   β”‚       β”‚
β”‚  β”‚ β€’ RDS    β”‚    β”‚ β€’ Cloud  β”‚    β”‚ β€’ Deploymentsβ”‚       β”‚
β”‚  β”‚ β€’ VPC    β”‚    β”‚   SQL    β”‚    β”‚ β€’ Ingress    β”‚       β”‚
β”‚  β”‚ β€’ Lambda β”‚    β”‚ β€’ Cloud  β”‚    β”‚              β”‚       β”‚
β”‚  β”‚ β€’ EKS    β”‚    β”‚   Run    β”‚    β”‚              β”‚       β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜       β”‚
β”‚                                                          β”‚
β”‚  3000+ Providers tersedia di registry.terraform.io       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
⚠️ Version Constraint

Gunakan version constraint untuk menghindari breaking changes. Tanda ~> 5.0 berarti "gunakan versi 5.x terbaru, tetapi jangan upgrade ke 6.0". Tanda = 5.82.2 berarti "gunakan persis versi ini". Untuk production, selalu pin versi dengan ketat.

4. Resources & Data Sources

Resource adalah blok yang mendeskripsikan satu atau lebih infrastruktur objects β€” seperti EC2 instance, VPC, S3 bucket, atau DNS record. Data source memungkinkan Anda mengambil data dari provider yang sudah ada (bukan membuat baru) untuk digunakan dalam konfigurasi.

Resource Blocks

HCL β€” Resources
# ═══ main.tf β€” AWS EC2 Instance ═══

# VPC β€” Virtual Private Cloud
resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Name = "${local.name_prefix}-vpc"
  }
}

# Internet Gateway
resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "${local.name_prefix}-igw"
  }
}

# Public Subnet
resource "aws_subnet" "public" {
  count                   = 2
  vpc_id                  = aws_vpc.main.id
  cidr_block              = cidrsubnet("10.0.0.0/16", 8, count.index)
  availability_zone       = data.aws_availability_zones.available.names[count.index]
  map_public_ip_on_launch = true

  tags = {
    Name = "${local.name_prefix}-public-${count.index + 1}"
    Tier = "public"
  }
}

# Private Subnet
resource "aws_subnet" "private" {
  count             = 2
  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet("10.0.0.0/16", 8, count.index + 10)
  availability_zone = data.aws_availability_zones.available.names[count.index]

  tags = {
    Name = "${local.name_prefix}-private-${count.index + 1}"
    Tier = "private"
  }
}

# Security Group
resource "aws_security_group" "web" {
  name        = "${local.name_prefix}-web-sg"
  description = "Security group untuk web server"
  vpc_id      = aws_vpc.main.id

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

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

  # Allow SSH (hanya dari IP tertentu)
  ingress {
    description = "SSH"
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = [var.admin_ip]
  }

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

  tags = {
    Name = "${local.name_prefix}-web-sg"
  }
}

# EC2 Instance
resource "aws_instance" "web_server" {
  ami                    = data.aws_ami.ubuntu.id
  instance_type          = var.instance_type
  subnet_id              = aws_subnet.public[0].id
  vpc_security_group_ids = [aws_security_group.web.id]
  key_name               = var.ssh_key_name

  root_block_device {
    volume_size = 30
    volume_type = "gp3"
    encrypted   = true
  }

  user_data = base64encode(templatefile("${path.module}/scripts/init.sh", {
    app_name = var.project_name
  }))

  tags = {
    Name = "${local.name_prefix}-web-server"
  }

  lifecycle {
    create_before_destroy = true
    prevent_destroy       = false
  }
}

Data Sources

HCL β€” Data Sources
# ═══ Data Sources β€” Mengambil informasi dari provider ═══

# Ambil daftar availability zones
data "aws_availability_zones" "available" {
  state = "available"
}

# Ambil AMI Ubuntu terbaru
data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"]  # Canonical

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

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

# Ambil informasi caller (current AWS account)
data "aws_caller_identity" "current" {}

# Ambil IP publik saat ini (untuk SSH access)
data "http" "my_ip" {
  url = "https://api.ipify.org"
}

# Menggunakan data source
output "account_id" {
  value = data.aws_caller_identity.current.account_id
}

output "ubuntu_ami_id" {
  value = data.aws_ami.ubuntu.id
}

output "my_public_ip" {
  value = data.http.my_ip.response_body
}

# Data source dari Terraform state lain
data "terraform_remote_state" "network" {
  backend = "s3"
  config = {
    bucket = "beebane-terraform-state"
    key    = "network/terraform.tfstate"
    region = "ap-southeast-1"
  }
}

Terraform Lifecycle & Meta-Arguments

HCL β€” Meta-Arguments
# ═══ count β€” Membuat multiple resource ═══
resource "aws_instance" "server" {
  count         = var.instance_count
  ami           = data.aws_ami.ubuntu.id
  instance_type = var.instance_type
  subnet_id     = aws_subnet.public[count.index % length(aws_subnet.public)]

  tags = {
    Name = "${local.name_prefix}-server-${count.index + 1}"
  }
}

# ═══ for_each β€” Membuat resource dari map/set ═══
resource "aws_s3_bucket" "buckets" {
  for_each = toset(["logs", "assets", "backups"])

  bucket = "${local.name_prefix}-${each.key}"

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

# ═══ depends_on β€” Explicit dependency ═══
resource "aws_instance" "app" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = var.instance_type

  depends_on = [
    aws_internet_gateway.main,
    aws_security_group.web,
  ]
}

# ═══ lifecycle β€” Control resource behavior ═══
resource "aws_db_instance" "main" {
  engine               = "postgres"
  engine_version       = "16"
  instance_class       = "db.t3.micro"
  allocated_storage    = 20
  db_name              = "beebane"
  username             = var.db_username
  password             = var.db_password
  skip_final_snapshot  = false
  final_snapshot_identifier = "${local.name_prefix}-db-final-snapshot"

  lifecycle {
    prevent_destroy = true    # JANGAN HAPUS database ini!
    ignore_changes  = [password]  # Abaikan perubahan password
  }
}
πŸ’‘ count vs for_each

Gunakan count untuk membuat N resource yang identik. Gunakan for_each ketika resource memiliki nama/konfigurasi yang berbeda satu sama lain. Peringatan: Hindari menggunakan count dengan list yang bisa berubah urutan β€” ini bisa menyebabkan resource yang tidak seharusnya berubah jadi di-replace.

5. State Management

State adalah file JSON yang berisi mapping antara konfigurasi Terraform dan resource aktual di infrastruktur. State memungkinkan Terraform mengetahui resource apa saja yang sudah dibuat, atributnya, dan dependency-nya. Tanpa state, Terraform tidak bisa menentukan perubahan apa yang perlu dilakukan.

Local vs Remote State

HCL β€” State Configuration
# ═══ Local State (default) ═══
# File: terraform.tfstate di direktori kerja
# ⚠️ JANGAN commit ke Git β€” bisa mengandung secrets!
# ⚠️ Tidak aman untuk team collaboration

# ═══ Remote State β€” S3 Backend (recommended) ═══
terraform {
  backend "s3" {
    bucket         = "beebane-terraform-state"
    key            = "production/terraform.tfstate"
    region         = "ap-southeast-1"
    dynamodb_table = "terraform-locks"   # State locking
    encrypt        = true                # Enkripsi at rest
  }
}

# ═══ Remote State β€” Terraform Cloud ═══
terraform {
  cloud {
    organization = "beebane-labs"

    workspaces {
      name = "production"
    }
  }
}

# ═══ State Locking (menghindari race condition) ═══
# Saat menggunakan S3 backend, DynamoDB table diperlukan:
resource "aws_dynamodb_table" "terraform_locks" {
  name         = "terraform-locks"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"

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

Terraform CLI Commands

Bash β€” Terraform Workflow Commands
# ═══ WORKFLOW: init β†’ plan β†’ apply β†’ destroy ═══

# 1. Initialize β€” Download providers dan backend
terraform init

# 2. Format β€” Rapihkan kode
terraform fmt -recursive

# 3. Validate β€” Cek syntax
terraform validate

# 4. Plan β€” Preview perubahan yang akan dilakukan
terraform plan
terraform plan -out=tfplan    # Simpan plan ke file
terraform plan -var="instance_type=t3.medium"  # Override variable

# 5. Apply β€” Terapkan perubahan ke infrastruktur
terraform apply
terraform apply tfplan          # Apply dari file plan
terraform apply -auto-approve   # Skip confirmation prompt
terraform apply -target=aws_instance.web_server  # Target spesifik

# 6. Destroy β€” Hapus semua resource (GUNAKAN DENGAN HATI-HATI!)
terraform destroy
terraform destroy -target=aws_instance.web_server  # Target spesifik

# ═══ STATE COMMANDS ═══

# Lihat state list
terraform state list

# Lihat detail resource
terraform state show aws_instance.web_server

# Import existing resource ke Terraform state
terraform import aws_instance.web_server i-1234567890abcdef0

# Remove resource dari state (tanpa menghapus resource aktual)
terraform state rm aws_instance.web_server

# Move resource dalam state (rename)
terraform state mv aws_instance.old_name aws_instance.new_name

# Refresh state dari infrastructure aktual
terraform refresh

# Taint β€” Tandai resource untuk di-replace saat apply berikutnya
terraform taint aws_instance.web_server
# (deprecated, gunakan -replace flag)
terraform apply -replace=aws_instance.web_server

# ═══ WORKSPACE COMMANDS ═══
terraform workspace list
terraform workspace new staging
terraform workspace select production
Diagram: State Management Flow
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              STATE MANAGEMENT ARCHITECTURE               β”‚
β”‚                                                          β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”              β”‚
β”‚  β”‚ .tf      │────────▢│   Terraform      β”‚              β”‚
β”‚  β”‚ Config   β”‚         β”‚   Engine         β”‚              β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜              β”‚
β”‚                              β”‚                           β”‚
β”‚                     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                β”‚
β”‚                     β”‚   State File      β”‚                β”‚
β”‚                     β”‚   (tfstate)       β”‚                β”‚
β”‚                     β”‚                   β”‚                β”‚
β”‚                     β”‚ β€’ Resource IDs    β”‚                β”‚
β”‚                     β”‚ β€’ Attributes      β”‚                β”‚
β”‚                     β”‚ β€’ Dependencies    β”‚                β”‚
β”‚                     β”‚ β€’ Outputs         β”‚                β”‚
β”‚                     β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                β”‚
β”‚                              β”‚                           β”‚
β”‚                     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                β”‚
β”‚                     β”‚  Remote Backend   β”‚                β”‚
β”‚                     β”‚  (S3 + DynamoDB)  β”‚                β”‚
β”‚                     β”‚                   β”‚                β”‚
β”‚                     β”‚  Encrypted        β”‚                β”‚
β”‚                     β”‚  Locked           β”‚                β”‚
β”‚                     β”‚  Shared           β”‚                β”‚
β”‚                     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                β”‚
β”‚                                                          β”‚
β”‚  ⚠️  JANGAN edit state file secara manual!              β”‚
β”‚  ⚠️  Selalu gunakan remote backend untuk production!    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
⚠️ Keamanan State
  • JANGAN commit *.tfstate ke Git β€” file ini bisa berisi password, API keys, dan secrets
  • Tambahkan *.tfstate dan *.tfstate.backup ke .gitignore
  • Gunakan remote backend (S3, GCS, Terraform Cloud) dengan enkripsi untuk production
  • Aktifkan state locking (DynamoDB) untuk menghindari race condition saat banyak orang apply
  • Gunakan sensitive = true pada output yang mengandung data sensitif

6. Modules

Module adalah kumpulan file Terraform (.tf) yang dikemas bersama untuk digunakan kembali. Module memungkinkan Anda mengorganisir kode, membagikannya ke tim, dan menghindari duplikasi. Setiap direktori yang berisi file .tf adalah sebuah module.

Root Module vs Child Module

HCL β€” Module Structure
# ═══ STRUKTUR DIREKTORI ═══
#
# project/
# β”œβ”€β”€ main.tf              ← Root module (panggil child modules)
# β”œβ”€β”€ variables.tf
# β”œβ”€β”€ outputs.tf
# β”œβ”€β”€ providers.tf
# └── 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 β€” Child Module ═══
resource "aws_vpc" "this" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true
  enable_dns_support   = true

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

resource "aws_subnet" "public" {
  count             = length(var.public_subnets)
  vpc_id            = aws_vpc.this.id
  cidr_block        = var.public_subnets[count.index]
  availability_zone = var.availability_zones[count.index]

  tags = {
    Name = "${var.name_prefix}-public-${count.index + 1}"
  }
}

resource "aws_subnet" "private" {
  count             = length(var.private_subnets)
  vpc_id            = aws_vpc.this.id
  cidr_block        = var.private_subnets[count.index]
  availability_zone = var.availability_zones[count.index]

  tags = {
    Name = "${var.name_prefix}-private-${count.index + 1}"
  }
}

# ═══ modules/vpc/variables.tf ═══
variable "vpc_cidr" {
  type    = string
  default = "10.0.0.0/16"
}
variable "name_prefix" { type = string }
variable "environment" { type = string }
variable "availability_zones" { type = list(string) }
variable "public_subnets" { type = list(string) }
variable "private_subnets" { type = list(string) }

# ═══ 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
}

Using Modules

HCL β€” Module Usage (main.tf)
# ═══ Menggunakan Local Module ═══
module "vpc" {
  source = "./modules/vpc"

  name_prefix        = local.name_prefix
  environment        = var.environment
  vpc_cidr           = "10.0.0.0/16"
  availability_zones = ["ap-southeast-1a", "ap-southeast-1b"]
  public_subnets     = ["10.0.1.0/24", "10.0.2.0/24"]
  private_subnets    = ["10.0.10.0/24", "10.0.11.0/24"]
}

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

  name_prefix = local.name_prefix
  environment = var.environment
  subnet_ids  = module.vpc.public_subnet_ids
  # Menggunakan output dari module VPC!
}

# ═══ Menggunakan Remote Module (Terraform Registry) ═══
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.1.2"

  name = "${local.name_prefix}-vpc"
  cidr = "10.0.0.0/16"

  azs             = ["ap-southeast-1a", "ap-southeast-1b"]
  private_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
  public_subnets  = ["10.0.101.0/24", "10.0.102.0/24"]

  enable_nat_gateway = true
  single_nat_gateway = true

  tags = {
    Environment = var.environment
    ManagedBy   = "terraform"
  }
}

# ═══ Output dari Module ═══
output "vpc_id" {
  value = module.vpc.vpc_id
}

output "public_subnets" {
  value = module.vpc.public_subnet_ids
}

Best Practices untuk Modules

πŸ“Œ Module Best Practices
  • Single Responsibility β€” Setiap module menangani satu concern (VPC, EC2, RDS)
  • Parameterize β€” Gunakan variable untuk input yang berbeda, bukan hardcode
  • Document β€” Tulis README.md untuk setiap module dengan usage example
  • Version β€” Gunakan version pinning untuk remote modules
  • Test β€” Gunakan terratest atau tflint untuk testing module
  • Output β€” Ekspose semua informasi yang dibutuhkan caller melalui outputs
  • Terraform Registry β€” Publish module yang reusable ke registry untuk sharing
Bash β€” Terraform Testing & Linting
# ═══ tflint β€” Terraform Linter ═══
# Install
brew install tflint

# Run
tflint --init
tflint --recursive

# ═══ terraform validate ═══
terraform validate
# Success! The configuration is valid.

# ═══ checkov β€” Security scanning ═══
pip install checkov
checkov -d .

# ═══ terraform-docs β€” Generate documentation ═══
brew install terraform-docs
terraform-docs markdown table ./modules/vpc > ./modules/vpc/README.md
πŸ’‘ Terraform Registry

Kunjungi registry.terraform.io untuk menemukan ribuan module dan provider yang sudah dibuat komunitas. Module populer seperti terraform-aws-modules/vpc sudah digunakan oleh ribuan perusahaan dan sangat teruji. Jangan reinvent the wheel β€” gunakan module yang sudah ada jika memungkinkan.

7. Quiz: Uji Pemahamanmu!

Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut untuk menguji pemahamanmu tentang Terraform dan Infrastructure as Code:

Pertanyaan 1: Apa perbedaan utama antara IaC (deklaratif) dan scripting (imperatif)?

a) IaC hanya bisa digunakan di cloud, scripting bisa di mana saja
b) IaC mendeskripsikan state akhir yang diinginkan, scripting mendeskripsikan langkah-langkah untuk mencapainya
c) IaC lebih lambat dari scripting
d) Tidak ada perbedaan

Pertanyaan 2: Perintah apa yang digunakan untuk preview perubahan sebelum diterapkan ke infrastruktur?

a) terraform apply
b) terraform init
c) terraform plan
d) terraform validate

Pertanyaan 3: Mengapa file terraform.tfstate TIDAK boleh di-commit ke Git?

a) Terlalu besar ukurannya
b) Bisa mengandung password, API keys, dan data sensitif lainnya
c) Formatnya tidak kompatibel dengan Git
d) Tidak ada alasan khusus

Pertanyaan 4: Apa fungsi dari "data source" dalam Terraform?

a) Membuat resource baru di cloud
b) Menghapus resource yang sudah ada
c) Mengambil informasi dari resource atau provider yang sudah ada untuk digunakan dalam konfigurasi
d) Mengatur authentication ke cloud provider

Pertanyaan 5: Apa kepanjangan dari HCL dalam konteks Terraform?

a) HashiCorp Cloud Language
b) HashiCorp Configuration Language
c) High-level Configuration Language
d) Hybrid Cloud Language
πŸ” Zoom
100%
🎨 Tema