1. OpenTofu vs Terraform
OpenTofu adalah fork open-source (Linux Foundation) dari Terraform yang dibuat sebagai respons terhadap perubahan lisensi Terraform dari MPL 2.0 ke BSL 1.1. OpenTofu 100% kompatibel dengan Terraform HCL dan menambahkan fitur-fitur baru yang tidak ada di Terraform.
1.1 Mengapa OpenTofu?
| Fitur | OpenTofu | Terraform |
|---|---|---|
| Lisensi | Apache 2.0 (open-source) | BSL 1.1 (non-open-source) |
| State Encryption | ✅ Native support | ❌ Tidak ada |
| Removed Block | ✅ Ya | ❌ Tidak ada |
| Dynamic Provider Config | ✅ Ya | ❌ Tidak ada |
| State File Encryption | ✅ Native | Butuh external wrapper |
| Community Governance | Linux Foundation | HashiCorp (IBM) |
| Backend Compatibility | S3, GCS, Azure, K8s, HTTP, local | Sama |
| Provider Ecosystem | 100% kompatibel dengan Terraform Registry | Native |
Modules, Providers
Variables, Outputs
Preview changes
No modifications
Execute changes
Update state
Lock via DynamoDB
Version history
2. Instalasi & Setup
# Method 1: Official installer (Linux/macOS) curl -fsSL https://get.opentofu.org/install.sh | bash -s -- --install-method standalone # Method 2: Package manager (Homebrew) brew install opentofu # Method 3: Binary download # Download dari https://github.com/opentofu/opentofu/releases wget https://github.com/opentofu/opentofu/releases/download/v1.8.0/tofu_1.8.0_linux_amd64.zip unzip tofu_1.8.0_linux_amd64.zip sudo mv tofu /usr/local/bin/ # Verifikasi instalasi tofu version # OpenTofu v1.8.0 # Migration dari Terraform — sangat mudah! # 1. Backup state cp terraform.tfstate terraform.tfstate.backup # 2. Jalankan migrate command tofu init -migrate-state # 3. Done! Semua .tf file langsung bekerja # Perintah terraform → tofu (drop-in replacement) # terraform init → tofu init # terraform plan → tofu plan # terraform apply → tofu apply
2.1 Project Structure
# Struktur project OpenTofu yang baik infra/ ├── environments/ │ ├── dev/ │ │ ├── main.tf │ │ ├── variables.tf │ │ ├── outputs.tf │ │ ├── backend.tf │ │ └── terraform.tfvars │ ├── staging/ │ │ ├── main.tf │ │ ├── variables.tf │ │ ├── outputs.tf │ │ ├── backend.tf │ │ └── terraform.tfvars │ └── production/ │ ├── main.tf │ ├── variables.tf │ ├── outputs.tf │ ├── backend.tf │ └── terraform.tfvars ├── modules/ │ ├── vpc/ │ │ ├── main.tf │ │ ├── variables.tf │ │ └── outputs.tf │ ├── eks-cluster/ │ │ ├── main.tf │ │ ├── variables.tf │ │ └── outputs.tf │ └── rds/ │ ├── main.tf │ ├── variables.tf │ └── outputs.tf └── README.md
3. State Management
OpenTofu mendukung native state encryption — fitur yang tidak ada di Terraform. State bisa dienkripsi baik saat remote storage maupun local.
# Remote backend: S3 dengan DynamoDB locking
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "production/terraform.tfstate"
region = "ap-southeast-1"
encrypt = true
dynamodb_table = "terraform-locks"
}
}
# OpenTofu: State encryption dengan passphrase
terraform {
encryption {
key_provider "pbkdf2" "my_key" {
passphrase = var.state_encryption_passphrase
}
state {
method = method.aes_gcm.my_key
}
plan {
method = method.aes_gcm.my_key
}
}
}
# OpenTofu: State encryption dengan AWS KMS
terraform {
encryption {
key_provider "aws_kms" "my_kms_key" {
kms_key_id = "arn:aws:kms:ap-southeast-1:123456789:key/abc-123"
region = "ap-southeast-1"
}
state {
method = method.aes_gcm.my_kms_key
}
}
}
# OpenTofu: State encryption dengan GCP KMS
terraform {
encryption {
key_provider "gcp_kms" "my_key" {
kms_encryption_key = "projects/my-project/locations/global/keyRings/my-ring/cryptoKeys/my-key"
}
state {
method = method.aes_gcm.my_key
}
}
}
Dengan OpenTofu, Anda tidak perlu lagi mengandalkan backend encryption yang terbatas. State file dienkripsi secara native — bahkan jika seseorang mendapatkan akses ke S3 bucket, mereka tidak bisa membaca state tanpa key. Ini termasuk plan files yang sebelumnya selalu plaintext.
4. Providers
# OpenTofu menggunakan Terraform Registry yang sama
terraform {
required_version = ">= 1.6.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
kubernetes = {
source = "hashicorp/kubernetes"
version = "~> 2.25"
}
helm = {
source = "hashicorp/helm"
version = "~> 2.12"
}
}
}
# AWS Provider
provider "aws" {
region = var.aws_region
default_tags {
tags = {
Environment = var.environment
ManagedBy = "opentofu"
Project = var.project_name
}
}
}
# Assume role untuk multi-account
provider "aws" {
alias = "production"
region = "ap-southeast-1"
assume_role {
role_arn = "arn:aws:iam::${var.prod_account_id}:role/DeployRole"
}
}
# Kubernetes Provider
provider "kubernetes" {
host = module.eks.cluster_endpoint
cluster_ca_certificate = base64decode(module.eks.cluster_ca_certificate)
exec {
api_version = "client.authentication.k8s.io/v1beta1"
command = "aws"
args = ["eks", "get-token", "--cluster-name", module.eks.cluster_name]
}
}
5. Modules
# modules/vpc/main.tf
variable "environment" {
type = string
}
variable "vpc_cidr" {
type = string
default = "10.0.0.0/16"
}
variable "availability_zones" {
type = list(string)
default = ["ap-southeast-1a", "ap-southeast-1b", "ap-southeast-1c"]
}
variable "enable_nat_gateway" {
type = bool
default = true
}
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.environment}-vpc"
}
}
resource "aws_subnet" "private" {
count = length(var.availability_zones)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index)
availability_zone = var.availability_zones[count.index]
tags = {
Name = "${var.environment}-private-${var.availability_zones[count.index]}"
}
}
resource "aws_subnet" "public" {
count = length(var.availability_zones)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index + 100)
availability_zone = var.availability_zones[count.index]
map_public_ip_on_launch = true
tags = {
Name = "${var.environment}-public-${var.availability_zones[count.index]}"
}
}
output "vpc_id" {
value = aws_vpc.main.id
}
output "private_subnet_ids" {
value = aws_subnet.private[*].id
}
output "public_subnet_ids" {
value = aws_subnet.public[*].id
}
---
# Menggunakan module
module "vpc" {
source = "../../modules/vpc"
environment = var.environment
vpc_cidr = "10.0.0.0/16"
enable_nat_gateway = true
}
module "eks" {
source = "../../modules/eks-cluster"
environment = var.environment
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids
cluster_version = "1.29"
}
# Module dari registry
module "eks" {
source = "terraform-aws-modules/eks/aws"
version = "20.0.0"
# ...
}
6. Workspaces
# List workspaces
tofu workspace list
# Buat workspace baru
tofu workspace new development
tofu workspace new staging
tofu workspace new production
# Switch workspace
tofu workspace select development
# Gunakan workspace di configuration
# environments/dev/main.tf
locals {
env_config = {
development = {
instance_type = "t3.micro"
min_size = 1
max_size = 2
}
staging = {
instance_type = "t3.small"
min_size = 2
max_size = 4
}
production = {
instance_type = "t3.large"
min_size = 3
max_size = 10
}
}
config = local.env_config[terraform.workspace]
}
resource "aws_instance" "web" {
instance_type = local.config.instance_type
# ...
}
7. Drift Detection
# Drift detection — cek apakah ada perubahan di cloud
# yang tidak tercatat di state
tofu plan -detailed-exitcode
# Exit code: 0 = no changes, 2 = changes detected, 1 = error
# Script CI/CD untuk drift detection
#!/bin/bash
set -euo pipefail
echo "Running drift detection..."
tofu plan -detailed-exitcode -out=drift.tfplan 2>&1 | tee plan_output.txt
EXIT_CODE=${PIPESTATUS[0]}
case $EXIT_CODE in
0)
echo "✅ No drift detected"
exit 0
;;
2)
echo "⚠️ Drift detected!"
tofu show -json drift.tfplan | jq '.resource_changes[] | select(.change.actions != ["no-op"])'
# Kirim notifikasi
curl -X POST "$SLACK_WEBHOOK" -d "{\"text\":\"Drift detected in ${ENVIRONMENT}\"}"
exit 1
;;
*)
echo "❌ Error during drift detection"
exit $EXIT_CODE
;;
esac
# OpenTofu: removed block — hapus dari state tanpa destroy
# Berguna saat ingin me-"lepaskan" resource dari management
removed {
from = aws_instance.legacy_server
lifecycle {
destroy = false # Jangan destroy, hanya lepas dari state
}
}
8. Fitur Baru OpenTofu
# 1. State Encryption (sudah dibahas di section 3)
# 2. Removed block — lepaskan resource tanpa destroy
removed {
from = aws_s3_bucket.legacy
lifecycle {
destroy = false
}
}
# 3. Dynamic provider configuration
# Provider yang bergantung pada variable/for_each
provider "aws" {
for_each = toset(var.regions)
alias = each.key
region = each.value
}
# 4. Client-side state encryption
terraform {
encryption {
key_provider "pbkdf2" "my_key" {
passphrase = var.encryption_passphrase
}
state {
method = method.aes_gcm.my_key
# Enkripsi hanya specific fields
enforced = true
}
}
}
# 5. Loop over providers
module "regional_infra" {
source = "./modules/regional"
for_each = toset(var.regions)
providers = {
aws = aws[each.key]
}
region = each.key
}
# 6. Moved block (sama dengan Terraform 1.1+)
moved {
from = aws_instance.old_name
to = aws_instance.new_name
}
# 7. Import block (declarative import)
import {
to = aws_s3_bucket.existing
id = "my-existing-bucket"
}
Migrasi dari Terraform ke OpenTofu sangat mudah — cukup jalankan tofu init -migrate-state. Namun, pastikan untuk backup state sebelumnya. Jika Anda menggunakan encryption block (fitur OpenTofu), state tidak bisa dibaca lagi oleh Terraform. Lakukan migrasi secara bertahap dan test di environment non-production terlebih dahulu.
9. Quiz: Uji Pemahamanmu!
Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut: