Automated Infrastructure Provisioning Menggunakan Terraform di AWS
1. Konteks Industri & Analisis Akar Masalah
Dalam lanskap operasional sistem berbasis komputasi awan modern, manajemen infrastruktur manual melalui GUI web console (ClickOps) telah menjadi salah satu sumber kegagalan sistem paling kritis di level enterprise. Pendekatan ad-hoc ini memicu apa yang dikenal di industri sebagai Configuration Driftβkondisi di mana konfigurasi riil infrastruktur produksi berdeviasi secara substansial dari dokumentasi, diagram arsitektur, atau environment staging.
Bottleneck operasional yang sering ditemui meliputi:
- Human Error pada Security Layer: Terbukanya port sensitif (seperti MySQL 3306 atau Redis 6379) ke subnet publik (
0.0.0.0/0) akibat ketidaktelitian konfigurasi Security Group secara manual di AWS Management Console. - Snowflake Servers: Server yang dibangun secara manual dengan dependensi paket sistem operasi yang tidak terdokumentasi, membuat proses disaster recovery memakan waktu berjam-jam bahkan berhari-hari karena konfigurasi sistem tidak dapat direproduksi secara deterministik.
- Ketiadaan Audit Trail dan Version Control: Tidak tersedianya riwayat perubahan konfigurasi jaringan, siapa yang mengubah nilai MTU, merotasi route table, atau menurunkan kapasitas alokasi IOPS pada EBS storage.
| Aspek Evaluasi | Sebelum: Manual/ClickOps & Ad-hoc Bash | Sesudah: Declarative IaC (Terraform) |
|---|---|---|
| Reproducibility | Nol; konfigurasi bergantung pada catatan manual dan ingatan engineer. | Deterministik 100%; seluruh infrastruktur didefinisikan dalam kode tervirtualisasi. |
| Auditability & Compliance | Audit log terdistribusi pada AWS CloudTrail tanpa representasi state utuh. | Riwayat Git commit mencatat setiap perubahan, reviewer pull request, dan riwayat rollback. |
| Disaster Recovery (RTO) | Jam hingga hari untuk membangun ulang seluruh topologi VPC dan sub-layanan. | Menit; dieksekusi via pipeline otomatis (terraform apply). |
| State Verification | Manual checking via CLI/Console yang rawan terlewat. | Deteksi deviasi instan melalui terraform plan berbasis cryptographic hash. |
| Environment Parity | Dev, Staging, dan Prod memiliki deviasi konfigurasi tersembunyi. | Identik; menggunakan modul kode yang sama dengan injeksi variabel parameter terisolasi. |
Pendekatan Declarative Infrastructure as Code (IaC) melalui Terraform mengubah paradigma rekayasa infrastruktur dari instruksional imperatif ("jalankan script X lalu restart service Y") menjadi penetapan target akhir kondisi sistem ("sediakan arsitektur dengan spesifikasi Z, dan biarkan engine mengalkulasi langkah transisinya").
2. Konsep Inti & Deep Dive Arsitektur
Cara Kerja di Balik Layar: The Declarative Engine
Terraform tidak bekerja layaknya shell script linier. Ketika berkas berekstensi .tf dievaluasi, Terraform Core membangun sebuah Directed Acyclic Graph (DAG). Node dalam graph merepresentasikan resource (misalnya aws_vpc, aws_subnet, aws_instance), sementara edge merepresentasikan dependensi antar-resource (misalnya subnet bergantung pada ketersediaan VPC ID).
+-------------------------------------------------------------+
| Terraform Core |
| +--------------------+ +--------------------+ |
| | HCL Configuration | ----------> | Dependency Graph | |
| +--------------------+ | (DAG) | |
| +--------------------+ |
| | |
| v |
| +--------------------+ +--------------------+ |
| | Current State File | ----------> | State Comparator | |
| | (terraform.tfstate) | & Delta Plan | |
| +--------------------+ +--------------------+ |
+-------------------------------------------------------------+
|
[RPC Calls via Terraform Plugin Protocol]
v
+-------------------------------------------------------------+
| AWS Provider Engine |
| +--------------------+ +--------------------+ |
| | EC2 / VPC API | | RDS / S3 API | |
| +--------------------+ +--------------------+ |
+-------------------------------------------------------------+
|
[HTTPS AWS REST Endpoints]
v
+-------------------------------------------------------------+
| AWS Cloud Edge |
+-------------------------------------------------------------+
Siklus hidup eksekusi terdiri atas 4 fase mendasar:
terraform init: Mengunduh provider plugin (AWS provider binary) dan menginisialisasi remote backend backend S3/DynamoDB.terraform refresh: Mengirimkan query API ke AWS untuk mencocokkan status riil resource saat ini dengan metadata yang tersimpan di state file.terraform plan: Membandingkan konfigurasi HCL lokal dengan kondisi state file terkini. Graph engine mengalkulasi delta (apa yang harus dibuat+, diubah~, atau dimusnahkan-) tanpa mengeksekusi mutasi pada cloud.terraform apply: Mengonversi plan menjadi panggilan API concurrent (secara paralel berdasarkan tree dependensi) menuju target endpoint AWS.
Desain Arsitektur Jaringan Produksi
Untuk meng-host stack web server (Nginx reverse proxy + Node.js/PHP-FPM backend) dan database engine (MySQL RDS) secara aman, arsitektur harus membagi jaringan menjadi 3 tingkatan (tiers) di minimal 2 Availability Zones (AZ):
Internet Traffic
|
v
+-----------------------------+
| Internet Gateway (IGW) |
+-----------------------------+
|
================================|================================ [VPC: 10.0.0.0/16]
v
+-------------------------------------------------------+
| [PUBLIC TIER] |
| |
| AZ-a: Public Subnet (10.0.1.0/24) |
| +-----------------------------------------------+ |
| | Application Load Balancer (ALB) | |
| | NAT Gateway AZ-a | |
| +-----------------------------------------------+ |
| |
| AZ-b: Public Subnet (10.0.2.0/24) |
| +-----------------------------------------------+ |
| | Application Load Balancer (ALB Node 2) | |
| | NAT Gateway AZ-b | |
| +-----------------------------------------------+ |
+-------------------------------------------------------+
|
+--------------------+--------------------+
| |
v v
+-------------------------------------------------------+
| [APPLICATION TIER - PRIVATE SUBNETS] |
| Route: 0.0.0.0/0 -> NAT Gateway (Egress Only) |
| |
| AZ-a: Private App Subnet (10.0.10.0/24) |
| +-----------------------------------------------+ |
| | EC2: Nginx + PHP-FPM / Node.js Engine | |
| +-----------------------------------------------+ |
| |
| AZ-b: Private App Subnet (10.0.20.0/24) |
| +-----------------------------------------------+ |
| | EC2: Nginx + PHP-FPM / Node.js Engine | |
| +-----------------------------------------------+ |
+-------------------------------------------------------+
|
+--------------------+--------------------+
| |
v v
+-------------------------------------------------------+
| [DATABASE TIER - ISOLATED PRIVATE SUBNETS] |
| Route: No Internet / No NAT Gateway Route |
| |
| AZ-a: Private DB Subnet (10.0.100.0/24) |
| +-----------------------------------------------+ |
| | RDS MySQL Primary (Port 3306) | |
| +-----------------------------------------------+ |
| |
| AZ-b: Private DB Subnet (10.0.200.0/24) |
| +-----------------------------------------------+ |
| | RDS MySQL Standby / Read-Replica | |
| +-----------------------------------------------+ |
+-------------------------------------------------------+
=================================================================
3. Perbandingan Komparatif Pendekatan IaC
Memilih tool IaC harus disesuaikan dengan kapabilitas tim, batas toleransi risiko vendor lock-in, dan kompleksitas ekosistem target.
| Parameter Evaluasi | HashiCorp Terraform | AWS CloudFormation / CDK | Pulumi | Ansible (Cloud Modules) |
|---|---|---|---|---|
| Paradigma | Declarative | Declarative (CFN) / Imperative (CDK) | Imperative / Object-Oriented | Procedural / Idempotent Imperative |
| Bahasa Konfigurasi | HCL (HashiCorp Config Lang) | JSON/YAML (CFN) atau TS/Python/Go (CDK) | TypeScript, Python, Go, C# | YAML Playbooks |
| State Management | External Explicit (.tfstate) via S3/GCS/Consul |
Managed Implicit via AWS Internal Engine | Managed Cloud Service / Custom Backend | Stateless (Mengharuskan query langsung ke target host) |
| Multi-Cloud Readiness | Luas; provider resmi untuk AWS, GCP, Azure, Cloudflare | Terkunci pada ekosistem AWS | Luas; multi-cloud native | Luas, tetapi kurang efisien untuk resource lifecycle cloud |
| Dependency Resolution | Otomatis via Dynamic Directed Acyclic Graph (DAG) | Otomatis via internal CFN engine | Otomatis via promise/async tree runtime | Sekuensial deklaratif manual via playbook order |
| Ecosystem & Community | Standar defacto industri IaC global | Sangat kuat di komunitas AWS | Sedang, terus bertumbuh | Sangat kuat di ranah OS Config Management |
[!IMPORTANT] Terraform sangat unggul dalam memprovisi infrastructure fabric (jaringan, firewall, database instans, DNS, CDN). Namun, untuk configuration management level OS (patching security Linux, kompilasi modul Apache/Nginx kompleks, konfigurasi Postfix/Mail server), pola terbaik enterprise adalah memadukan Terraform (memprovisi VM dan base image) dengan Packer (golden AMI) atau Ansible (post-boot configuration).
4. Prasyarat Sistem & Persiapan Lingkungan
Sebelum menulis modul infrastruktur, persiapkan dependensi lokal dan autentikasi kredensial AWS berikut:
1. Spesifikasi Binary & Tools
- Terraform CLI: Versi
1.7.0atau yang lebih baru. - AWS CLI: Versi
2.15.0atau yang lebih baru. - OpenSSL & SSH-Keygen: Untuk pembuatan keypair administratif darurat.
Verifikasi instalasi di terminal:
terraform version
# Terraform v1.7.5
# on linux_amd64
aws --version
# aws-cli/2.15.15 Python/3.11.6 Linux/6.5.0-generic
2. Konfigurasi IAM Principle of Least Privilege
Pastikan entitas eksekutor (IAM User atau IAM Role CI/CD) memiliki policy yang mengizinkan manipulasi VPC, EC2, RDS, S3, dan DynamoDB. Hindari penggunaan policy AdministratorAccess pada runner CI/CD produksi.
3. Bootstrap Remote State Storage & Locking Engine
Jangan gunakan lokal file untuk menyimpan state. Eksekusi script bootstrap berikut sekali saja via AWS CLI untuk menyiapkan S3 bucket terenkripsi dan tabel penguncian DynamoDB:
# Variabel Penamaan
export AWS_REGION="ap-southeast-1"
export STATE_BUCKET_NAME="enterprise-tfstate-prod-$(openssl rand -hex 4)"
export LOCK_TABLE_NAME="terraform-lock-table-prod"
# 1. Buat S3 Bucket untuk State Storage
aws s3api create-bucket \
--bucket "${STATE_BUCKET_NAME}" \
--region "${AWS_REGION}" \
--create-bucket-configuration LocationConstraint="${AWS_REGION}"
# 2. Wajibkan Enkripsi AES256 pada Bucket
aws s3api put-bucket-encryption \
--bucket "${STATE_BUCKET_NAME}" \
--server-side-encryption-configuration '{
"Rules": [{
"ApplyServerSideEncryptionByDefault": {
"SSEAlgorithm": "AES256"
}
}]
}'
# 3. Aktifkan Versioning Bucket untuk Rollback State jika Terjadi Kerusakan Data
aws s3api put-bucket-versioning \
--bucket "${STATE_BUCKET_NAME}" \
--versioning-configuration Status=Enabled
# 4. Blokir Total Akses Publik S3
aws s3api put-public-access-block \
--bucket "${STATE_BUCKET_NAME}" \
--public-access-block-configuration '{
"BlockPublicAcls": true,
"IgnorePublicAcls": true,
"BlockPublicPolicy": true,
"RestrictPublicBuckets": true
}'
# 5. Buat DynamoDB Table untuk State Concurrency Locking (Kunci: LockID string)
aws dynamodb create-table \
--table-name "${LOCK_TABLE_NAME}" \
--attribute-definitions AttributeName=LockID,AttributeType=S \
--key-schema AttributeName=LockID,KeyType=HASH \
--billing-mode PAY_PER_REQUEST \
--region "${AWS_REGION}"
5. Panduan Implementasi Langkah demi Langkah
Struktur direktori modular yang akan kita bangun mengikuti standar korporat:
terraform-aws-web-stack/
βββ backend.tf
βββ providers.tf
βββ variables.tf
βββ outputs.tf
βββ vpc.tf
βββ security_groups.tf
βββ compute.tf
βββ database.tf
βββ terraform.tfvars
File 1: backend.tf
Menghubungkan Terraform core ke S3 remote backend dan DynamoDB state lock.
terraform {
required_version = ">= 1.7.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.40.0"
}
}
backend "s3" {
bucket = "enterprise-tfstate-prod-d3a9b1c2" # Ganti sesuai nama bucket bootstrap Anda
key = "production/web-infrastructure/terraform.tfstate"
region = "ap-southeast-1"
dynamodb_table = "terraform-lock-table-prod"
encrypt = true
}
}
File 2: providers.tf
Mendefinisikan provider AWS dan default resource tags untuk kepatuhan tata kelola biaya (FinOps).
provider "aws" {
region = var.aws_region
default_tags {
tags = {
Environment = var.environment
ManagedBy = "Terraform"
Project = var.project_name
Owner = "DevOps Infrastructure Team"
}
}
}
File 3: variables.tf
Mendefinisikan skema variabel beserta aturan validasi nilai input.
variable "aws_region" {
type = string
description = "Region AWS target deployment."
default = "ap-southeast-1"
}
variable "environment" {
type = string
description = "Target environment (prod/staging/dev)."
default = "prod"
validation {
condition = contains(["prod", "staging", "dev"], var.environment)
error_message = "Environment harus salah satu dari: prod, staging, atau dev."
}
}
variable "project_name" {
type = string
description = "Nama namespace proyek untuk identifikasi tag dan prefix resource."
default = "core-enterprise"
}
variable "vpc_cidr" {
type = string
description = "Blok CIDR untuk VPC utama."
default = "10.0.0.0/16"
}
variable "availability_zones" {
type = list(string)
description = "Daftar Availability Zones yang dialokasikan."
default = ["ap-southeast-1a", "ap-southeast-1b"]
}
variable "public_subnet_cidrs" {
type = list(string)
description = "Alokasi CIDR untuk public subnets."
default = ["10.0.1.0/24", "10.0.2.0/24"]
}
variable "private_app_subnet_cidrs" {
type = list(string)
description = "Alokasi CIDR untuk private web application subnets."
default = ["10.0.10.0/24", "10.0.20.0/24"]
}
variable "private_db_subnet_cidrs" {
type = list(string)
description = "Alokasi CIDR untuk isolated private database subnets."
default = ["10.0.100.0/24", "10.0.200.0/24"]
}
variable "ec2_instance_type" {
type = string
description = "Tipe instans EC2 untuk application server worker."
default = "t3.medium"
}
variable "db_allocated_storage" {
type = number
description = "Ukuran storage awal database RDS MySQL (GB)."
default = 50
}
variable "db_max_allocated_storage" {
type = number
description = "Batas maksimal autoscaling storage database RDS MySQL (GB)."
default = 200
}
variable "db_instance_class" {
type = string
description = "Kelas instans komputasi RDS database."
default = "db.t3.medium"
}
variable "db_name" {
type = string
description = "Nama default database engine MySQL."
default = "enterprisedb"
}
variable "db_username" {
type = string
description = "Root administrative username untuk RDS MySQL."
default = "appadmin"
sensitive = true
}
variable "db_password" {
type = string
description = "Root administrative password untuk RDS MySQL."
sensitive = true
}
File 4: vpc.tf
Menyusun isolasi topologi Virtual Private Cloud, Subnetting 3-Tier, Route Tables, Internet Gateway, dan Egress NAT Gateways.
# 1. Definisi VPC Utama
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.project_name}-${var.environment}-vpc"
}
}
# 2. Internet Gateway untuk Ingress & Egress Publik
resource "aws_internet_gateway" "gw" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.project_name}-${var.environment}-igw"
}
}
# 3. Public Subnets (ALB & NAT Gateways)
resource "aws_subnet" "public" {
count = length(var.public_subnet_cidrs)
vpc_id = aws_vpc.main.id
cidr_block = var.public_subnet_cidrs[count.index]
availability_zone = var.availability_zones[count.index]
map_public_ip_on_launch = true
tags = {
Name = "${var.project_name}-${var.environment}-public-sub-${var.availability_zones[count.index]}"
Tier = "Public"
}
}
# 4. Elastic IP untuk Setiap NAT Gateway (Multi-AZ Resiliency)
resource "aws_eip" "nat" {
count = length(var.public_subnet_cidrs)
domain = "vpc"
tags = {
Name = "${var.project_name}-${var.environment}-nat-eip-${var.availability_zones[count.index]}"
}
}
# 5. NAT Gateways (Satu di Setiap AZ untuk Mencegah Single Point of Failure)
resource "aws_nat_gateway" "nat" {
count = length(var.public_subnet_cidrs)
allocation_id = aws_eip.nat[count.index].id
subnet_id = aws_subnet.public[count.index].id
tags = {
Name = "${var.project_name}-${var.environment}-nat-gw-${var.availability_zones[count.index]}"
}
depends_on = [aws_internet_gateway.gw]
}
# 6. Private Application Subnets (EC2 Web / App Servers)
resource "aws_subnet" "private_app" {
count = length(var.private_app_subnet_cidrs)
vpc_id = aws_vpc.main.id
cidr_block = var.private_app_subnet_cidrs[count.index]
availability_zone = var.availability_zones[count.index]
tags = {
Name = "${var.project_name}-${var.environment}-private-app-sub-${var.availability_zones[count.index]}"
Tier = "PrivateApp"
}
}
# 7. Private Database Subnets (RDS MySQL / Redis - Isolasi Tanpa Egress Internet)
resource "aws_subnet" "private_db" {
count = length(var.private_db_subnet_cidrs)
vpc_id = aws_vpc.main.id
cidr_block = var.private_db_subnet_cidrs[count.index]
availability_zone = var.availability_zones[count.index]
tags = {
Name = "${var.project_name}-${var.environment}-private-db-sub-${var.availability_zones[count.index]}"
Tier = "PrivateDatabase"
}
}
# 8. Route Table untuk Public Subnet
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.gw.id
}
tags = {
Name = "${var.project_name}-${var.environment}-public-rt"
}
}
# 9. Asosiasi Route Table Public
resource "aws_route_table_association" "public" {
count = length(var.public_subnet_cidrs)
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}
# 10. Route Tables untuk Private App Subnets (Mengarah ke NAT Gateway Lokal per AZ)
resource "aws_route_table" "private_app" {
count = length(var.private_app_subnet_cidrs)
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.nat[count.index].id
}
tags = {
Name = "${var.project_name}-${var.environment}-priv-app-rt-${var.availability_zones[count.index]}"
}
}
# 11. Asosiasi Route Table Private App
resource "aws_route_table_association" "private_app" {
count = length(var.private_app_subnet_cidrs)
subnet_id = aws_subnet.private_app[count.index].id
route_table_id = aws_route_table.private_app[count.index].id
}
# 12. Route Table untuk Database (Isolated, hanya rute lokal VPC)
resource "aws_route_table" "private_db" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.project_name}-${var.environment}-priv-db-rt"
}
}
# 13. Asosiasi Route Table Private DB
resource "aws_route_table_association" "private_db" {
count = length(var.private_db_subnet_cidrs)
subnet_id = aws_subnet.private_db[count.index].id
route_table_id = aws_route_table.private_db.id
}
File 5: security_groups.tf
Menyusun aturan firewall software secara berjenjang (chained security groups).
# 1. Security Group untuk External Application Load Balancer
resource "aws_security_group" "alb" {
name = "${var.project_name}-${var.environment}-alb-sg"
description = "Mengizinkan ingress HTTP/HTTPS publik dan egress terbatas ke app server."
vpc_id = aws_vpc.main.id
ingress {
description = "Inbound HTTP dari public"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "Inbound HTTPS TLS dari public"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
description = "Mengizinkan outbound traffic sepenuhnya"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.project_name}-${var.environment}-alb-sg"
}
}
# 2. Security Group untuk Web Application Servers (EC2)
resource "aws_security_group" "web_app" {
name = "${var.project_name}-${var.environment}-web-app-sg"
description = "Membatasi akses: HANYA menerima trafik masuk dari ALB Security Group."
vpc_id = aws_vpc.main.id
ingress {
description = "Reverse Proxy traffic dari ALB pada port 80"
from_port = 80
to_port = 80
protocol = "tcp"
security_groups = [aws_security_group.alb.id]
}
egress {
description = "Outbound ke NAT Gateway untuk package update dan API calls"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.project_name}-${var.environment}-web-app-sg"
}
}
# 3. Security Group untuk Database Layer (RDS MySQL)
resource "aws_security_group" "db" {
name = "${var.project_name}-${var.environment}-db-sg"
description = "HANYA mengizinkan koneksi MySQL dari EC2 Web App Security Group."
vpc_id = aws_vpc.main.id
ingress {
description = "Koneksi MySQL port 3306 eksklusif dari application instances"
from_port = 3306
to_port = 3306
protocol = "tcp"
security_groups = [aws_security_group.web_app.id]
}
egress {
description = "Blokir outbound inisiasi dari database; zero egress requirement"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["127.0.0.1/32"]
}
tags = {
Name = "${var.project_name}-${var.environment}-db-sg"
}
}
File 6: compute.tf
Menyiapkan Application Load Balancer, Launch Template, dan Auto Scaling Group yang mengonfigurasi stack web server (Nginx + runtime) secara otomatis via cloud-init userdata.
# 1. Mengambil AMI Ubuntu 22.04 LTS Resmi Terbaru
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"] # Canonical Official Owner ID
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}
# 2. IAM Role untuk SSM Agent (Menghilangkan Kebutuhan Port SSH 22 Terbuka)
resource "aws_iam_role" "app_ec2_role" {
name = "${var.project_name}-${var.environment}-ec2-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = { Service = "ec2.amazonaws.com\