Introduction
Membangun pondasi Kubernetes ala production-grade di AWS, dengan Terraform module yang reusable diorkestrasi oleh Terragrunt. Dari VPC sampai aplikasi yang di-deploy via Helm dan di-expose lewat Application Load Balancer.
- VPC dedicated (public + private subnet di 2 AZ)
- EKS control plane + worker node group managed (2×
t3.small) - Aplikasi contoh (
gunawand/learn-devops-1:v1.0) di-deploy via custom Helm chart - Opsional: AWS Application Load Balancer lewat Ingress yang didefinisikan di chart
| Masalah | Solusi |
|---|---|
| Kubernetes ribet di-bootstrap manual | EKS handle control plane (managed) |
| Terraform state berantakan lintas banyak stack | Terragrunt’s dependency + mock_outputs bikin stack DRY |
| Helm chart “drift” dari state cluster | Terraform’s helm_release = GitOps untuk Helm |
| Production butuh ALB + TLS + path routing | AWS Load Balancer Controller menghubungkan K8s Ingress ↔ AWS ALB |
Kalau Anda sudah baca tutorial saya sebelumnya tentang Terragrunt untuk VPC/EC2, ini langkah berikutnya yang natural — pola sama, tapi sekarang orchestrasi resource Kubernetes.

+-------------------+
| Internet |
| (kubectl client) |
+---------+---------+
|
| HTTPS (443) — public endpoint
| HTTP (80) — ALB
+---------v---------+
| EKS Cluster |
| Control Plane |
| (managed by AWS)|
+---------+---------+
|
+---------------+---------------+
| |
+----------v---------+ +-----------v---------+
| Private Subnet | | Private Subnet |
| AZ-a | | AZ-b |
+----------+---------+ +-----------+---------+
| |
+----------v---------+ +-----------v---------+
| t3.small worker | | t3.small worker |
| (managed node grp)| | (managed node grp) |
+--------------------+ +--------------------+
| |
+----------v-------------------------------v---------+
| VPC (10.20.0.0/16) |
+-------------------------+--------------------------+
|
+-------------------------+--------------------------+
| Public subnets (10.20.1.0/24, 10.20.2.0/24) |
| - 1x NAT Gateway (hemat biaya untuk dev) |
| - 1x Internet Gateway |
+----------------------------------------------------+
Side components (di-deploy sebagai Terragrunt stack):
┌──────────────────────────────────────────────┐
│ AWS Load Balancer Controller │
│ - Helm release di namespace kube-system │
│ - ServiceAccount dengan IRSA annotation │
│ - Watch K8s Ingress → provision ALB │
└──────────────────────────────────────────────┘
Struktur Folder
days-5-eks/
├── README.md
├── charts/ # Custom Helm chart (versioned di Git)
│ └── learn/
│ ├── Chart.yaml
│ ├── values.yaml
│ └── templates/
│ ├── _helpers.tpl
│ ├── deployment.yaml
│ ├── service.yaml
│ └── ingress.yaml # Opsional: ALB Ingress
├── live/ # Terragrunt config (env-specific)
│ └── aws/
│ ├── root.hcl # Region AWS + generator provider
│ └── dev/
│ ├── vpc/terragrunt.hcl
│ ├── eks/terragrunt.hcl
│ ├── alb-controller/terragrunt.hcl # LBC + IRSA (cluster-level)
│ └── learn/terragrunt.hcl # Deploy app via Helm
└── modules/ # Terraform module (reusable)
└── aws/
├── vpc/
├── eks/
├── helm-app/ # Generic Helm chart deployer
├── aws-load-balancer-controller/ # Installer LBC
└── alb-ingress/ # Alternatif Ingress via Terraform
Syarat
- Terraform ≥ 1.5.0
- Terragrunt ≥ 0.55
- AWS CLI sudah dikonfigurasi (
aws configure) - kubectl untuk verifikasi
- Helm CLI (opsional, untuk validasi chart)
- AWS IAM permissions: EC2, EKS, IAM, VPC, ELB (full access cukup untuk dev)
Step 1: Root Config (live/aws/root.hcl)
Satu file ini di-share oleh semua Terragrunt stack di bawah live/aws/**. File ini men-generate AWS provider dengan region + default tags yang konsisten.
# Root config untuk semua AWS live stack.
locals {
aws_region = "ap-southeast-3"
}
# Remote state — DIMATIKAN untuk iterasi pertama.
# Uncomment setelah bootstrap S3 + DynamoDB.
# remote_state {
# backend = "s3"
# generate = { path = "backend.tf", if_exists = "overwrite_terragrunt" }
# config = {
# bucket = "eks-dev-tf-state-${local.aws_region}"
# key = "${path_relative_to_include()}/terraform.tfstate"
# region = local.aws_region
# encrypt = true
# dynamodb_table = "eks-dev-tf-locks"
# }
# }
generate "provider" {
path = "provider.tf"
if_exists = "overwrite_terragrunt"
contents = <<EOT
provider "aws" {
region = "${local.aws_region}"
default_tags {
tags = {
ManagedBy = "Terragrunt"
Repo = "days-5-eks"
Project = "eks-learn"
}
}
}
EOT
}
Kenapa pakai generate "provider"?
- DRY: tulis konfigurasi provider sekali, berlaku untuk semua stack
- Auto-rewrite kalau root berubah (tidak ada
provider.tfyang stale)
Step 2: VPC Module
Module VPC ini generic dan reusable. Ia menerima:
- List AZ (minimal 2 untuk EKS)
- CIDR untuk public + private subnet
- Optional cluster name (menambahkan tag Kubernetes ke subnet)
# modules/aws/vpc/main.tf
resource "aws_vpc" "this" {
cidr_block = var.cidr
enable_dns_hostnames = true
enable_dns_support = true
}
resource "aws_subnet" "public" {
for_each = {
for idx, cidr in var.public_subnet_cidrs :
var.azs[idx] => cidr
}
vpc_id = aws_vpc.this.id
cidr_block = each.value
availability_zone = each.key
map_public_ip_on_launch = true
tags = merge(
var.tags,
{ Name = "${var.name}-public-${each.key}", Tier = "public" },
var.cluster_name != "" ? {
"kubernetes.io/cluster/${var.cluster_name}" = "shared"
"kubernetes.io/role/elb" = "1"
} : {}
)
}
# ... (private subnet, IGW, NAT GW, route table — kode lengkap di repo)
Penting: Tag kubernetes.io/cluster/<name> di subnet wajib ada agar in-tree AWS cloud provider di Kubernetes bisa men-provision load balancer. Kita pass cluster_name hanya setelah EKS cluster ada — tapi Terragrunt handle urutannya secara otomatis lewat dependency.
Stack Terragrunt-nya:
# live/aws/dev/vpc/terragrunt.hcl
include "root" { path = find_in_parent_folders("root.hcl") }
terraform { source = "${get_terragrunt_dir()}/../../../../modules/aws/vpc" }
inputs = {
name = "eks-dev-vpc"
cidr = "10.20.0.0/16"
azs = ["ap-southeast-3a", "ap-southeast-3b"]
public_subnet_cidrs = ["10.20.1.0/24", "10.20.2.0/24"]
private_subnet_cidrs = ["10.20.11.0/24", "10.20.12.0/24"]
cluster_name = "eks-dev" # Untuk tag K8s di subnet
tags = { Environment = "dev", Project = "eks-learn" }
}
Step 3: EKS Module (Manual, Bukan terraform-aws-modules)
Saya pilih menulis resource EKS secara manual daripada pakai module populer terraform-aws-modules/eks/aws. Alasannya:
- Lebih edukatif — Anda lihat setiap IAM role, policy attachment, security group
- Tidak terkunci versi di module pihak ketiga
- Kontrol penuh atas setiap detail
Module ini membuat:
- Cluster IAM role +
AmazonEKSClusterPolicy - Cluster security group (ingress 443 dari VPC CIDR)
- EKS cluster dengan endpoint public + private
- Worker node IAM role + 3 policy standar (
WorkerNodePolicy,CNIPolicy,ECR-ReadOnly) - Managed node group dengan
t3.small+ AMIAL2023_x86_64
# modules/aws/eks/main.tf (cuplikan)
resource "aws_eks_cluster" "this" {
name = var.cluster_name
version = var.kubernetes_version
role_arn = aws_iam_role.cluster.arn
vpc_config {
subnet_ids = var.subnet_ids
security_group_ids = [aws_security_group.cluster.id]
endpoint_public_access = true
endpoint_private_access = true
public_access_cidrs = ["0.0.0.0/0"]
}
depends_on = [aws_iam_role_policy_attachment.cluster_policy]
}
resource "aws_eks_node_group" "this" {
cluster_name = aws_eks_cluster.this.name
node_group_name = "${var.cluster_name}-nodes"
node_role_arn = aws_iam_role.node.arn
subnet_ids = var.subnet_ids
instance_types = [var.instance_type]
ami_type = "AL2023_x86_64" # AL2 sudah deprecated Nov 2025
capacity_type = "ON_DEMAND"
scaling_config {
desired_size = var.desired_size
min_size = var.min_size
max_size = var.max_size
}
depends_on = [
aws_iam_role_policy_attachment.node_worker_policy,
aws_iam_role_policy_attachment.node_cni_policy,
aws_iam_role_policy_attachment.node_registry_policy,
]
}
Stack Terragrunt-nya pakai dependency "vpc" untuk baca subnet ID:
# live/aws/dev/eks/terragrunt.hcl
include "root" { path = find_in_parent_folders("root.hcl") }
terraform { source = "${get_terragrunt_dir()}/../../../../modules/aws/eks" }
dependency "vpc" {
config_path = "../vpc"
mock_outputs = {
vpc_id = "vpc-00000000000000000"
vpc_cidr = "10.20.0.0/16"
private_subnet_ids = jsonencode(["subnet-aaa", "subnet-bbb"])
private_subnet_ids_by_az = jsonencode({
"ap-southeast-3a" = "subnet-aaa"
"ap-southeast-3b" = "subnet-bbb"
})
}
mock_outputs_allowed_terraform_commands = ["validate", "plan", "destroy"]
}
inputs = {
cluster_name = "eks-dev"
kubernetes_version = "1.32"
vpc_id = dependency.vpc.outputs.vpc_id
vpc_cidr = dependency.vpc.outputs.vpc_cidr
subnet_ids = try(
jsondecode(dependency.vpc.outputs.private_subnet_ids),
dependency.vpc.outputs.private_subnet_ids
)
instance_type = "t3.small"
desired_size = 2
min_size = 1
max_size = 3
tags = { Environment = "dev", Project = "eks-learn" }
}
Perhatikan pattern jsonencode / try-jsondecode — ini workaround untuk bug Terragrunt 1.0.x di mana mock_outputs dengan map value gagal di-parse. Dengan encode map sebagai JSON string, kita side-step bug-nya. Saat runtime, map HCL asli di-pass lewat jsondecode.
Step 4: Deploy App dengan Helm + Terraform
Di sinilah bagian menariknya. Kita deploy aplikasi contoh menggunakan:
- Custom Helm chart (versioned di Git)
helm_releaseresource Terraform (supaya deploy-an = GitOps)
Helm Chart (charts/learn/)
charts/learn/
├── Chart.yaml
├── values.yaml
└── templates/
├── _helpers.tpl
├── deployment.yaml
├── service.yaml
└── ingress.yaml # Kondisional
Chart ini mengikuti konvensi Helm 3 (apiVersion: v2) dengan label yang proper lewat _helpers.tpl.
Module Helm-nya generic — menerima chart path dan values map apapun:
# modules/aws/helm-app/main.tf (cuplikan)
data "aws_eks_cluster" "this" {
name = var.cluster_name
}
provider "kubernetes" {
host = data.aws_eks_cluster.this.endpoint
cluster_ca_certificate = base64decode(data.aws_eks_cluster.this.certificate_authority[0].data)
exec {
api_version = "client.authentication.k8s.io/v1beta1"
command = "aws"
args = ["eks", "get-token", "--cluster-name", var.cluster_name, "--region", var.aws_region]
}
}
provider "helm" {
kubernetes { /* sama seperti di atas */ }
}
resource "helm_release" "this" {
name = var.release_name
chart = var.chart_path
namespace = var.namespace
create_namespace = true
values = [yamlencode(var.values)]
wait = true
cleanup_on_fail = true
}
Insight penting: Kita pakai aws eks get-token via exec auth plugin — tidak perlu setup kubeconfig manual. Terraform authenticate ke EKS pakai IAM credential yang sama dengan AWS provider.
Stack Terragrunt menyambungkan semuanya:
# live/aws/dev/learn/terragrunt.hcl (cuplikan)
dependency "eks" {
config_path = "../eks"
mock_outputs = { cluster_name = "eks-dev" }
mock_outputs_allowed_terraform_commands = ["validate", "plan", "destroy"]
}
inputs = {
cluster_name = dependency.eks.outputs.cluster_name
chart_path = "${get_terragrunt_dir()}/../../../../charts/learn"
release_name = "learn"
namespace = "be"
values = {
replicaCount = 2
image = {
repository = "gunawand/learn-devops-1"
tag = "v1.0"
}
containerPort = 80
service = { type = "ClusterIP", port = 80 }
# ... resources, ingress, dll
}
}
Step 5 (Opsional): Expose via ALB dengan AWS Load Balancer Controller
Untuk setup yang beneran public-facing, Anda butuh ALB (bukan CLB default). Ini butuh AWS Load Balancer Controller (LBC) — sebuah pod controller yang watch Kubernetes Ingress dan men-provision ALB.
Kenapa LBC? Kenapa tidak cukup type: LoadBalancer?
| Fitur | In-tree type: LoadBalancer | AWS LBC |
|---|---|---|
| Yang dibuat | Classic Load Balancer (legacy) | Application / Network LB |
| Path-based routing | ❌ | ✅ |
| TLS termination | ❌ | ✅ |
| Host-based routing | ❌ | ✅ |
| Banyak Ingress → 1 ALB | ❌ | ✅ via IngressGroup |
| Production-ready | ❌ | ✅ |
Install LBC + IRSA
LBC butuh IAM permissions untuk panggil AWS API. Best practice-nya adalah IRSA (IAM Role for Service Account):
# modules/aws/aws-load-balancer-controller/main.tf (cuplikan)
# 1. Ambil OIDC issuer dari cluster
data "aws_eks_cluster" "this" { name = var.cluster_name }
# 2. Daftarkan OIDC issuer sebagai IAM OIDC provider
data "tls_certificate" "eks_oidc" {
url = data.aws_eks_cluster.this.identity[0].oidc[0].issuer
}
resource "aws_iam_openid_connect_provider" "eks" {
url = data.aws_eks_cluster.this.identity[0].oidc[0].issuer
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = [data.tls_certificate.eks_oidc.certificates[0].sha1_fingerprint]
}
# 3. IAM role dengan trust policy (hanya SA LBC yang boleh assume)
data "aws_iam_policy_document" "assume_role" {
statement {
effect = "Allow"
actions = ["sts:AssumeRoleWithWebIdentity"]
principals {
type = "Federated"
identifiers = [aws_iam_openid_connect_provider.eks.arn]
}
condition {
test = "StringEquals"
variable = "${replace(data.aws_eks_cluster.this.identity[0].oidc[0].issuer, "https://", "")}:sub"
values = ["system:serviceaccount:${var.namespace}:${var.service_account_name}"]
}
}
}
resource "aws_iam_role" "alb_controller" {
name = "${var.iam_role_name_prefix}-${var.cluster_name}"
assume_role_policy = data.aws_iam_policy_document.assume_role.json
}
# 4. Attach AWS-managed policy berisi permission LBC
resource "aws_iam_role_policy_attachment" "alb_controller" {
role = aws_iam_role.alb_controller.name
policy_arn = "arn:aws:iam::aws:policy/AWSLoadBalancerControllerIAMPolicy"
}
# 5. Install via Helm
resource "helm_release" "aws_load_balancer_controller" {
name = "aws-load-balancer-controller"
repository = "https://aws.github.io/eks-charts"
chart = "aws-load-balancer-controller"
namespace = var.namespace
set { name = "clusterName", value = var.cluster_name }
set { name = "region", value = var.aws_region }
set {
name = "serviceAccount.annotations.eks\\.amazonaws\\.com/role-arn"
value = aws_iam_role.alb_controller.arn # ← keajaiban IRSA
}
}
Mendefinisikan Ingress (di Helm chart)
Pendekatan paling bersih adalah menyertakan Ingress di dalam chart itu sendiri, di-toggle via values.yaml:
# charts/learn/values.yaml
ingress:
enabled: true
className: alb
hosts:
- host: ""
paths: [{ path: /, pathType: Prefix }]
alb:
scheme: internet-facing
targetType: ip
healthCheckPath: /
groupName: "" # Kosong = ALB sendiri per chart. Set nama sama di beberapa chart untuk share.
# charts/learn/templates/ingress.yaml
{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "learn.fullname" . }}
annotations:
kubernetes.io/ingress.class: alb
alb.ingress.kubernetes.io/scheme: {{ .Values.ingress.alb.scheme }}
alb.ingress.kubernetes.io/target-type: {{ .Values.ingress.alb.targetType }}
alb.ingress.kubernetes.io/healthcheck-path: {{ .Values.ingress.alb.healthCheckPath }}
{{- if .Values.ingress.alb.groupName }}
alb.ingress.kubernetes.io/group.name: {{ .Values.ingress.alb.groupName }}
{{- end }}
spec:
ingressClassName: {{ .Values.ingress.className }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "learn.fullname" $ }}
port: { number: $.Values.service.port }
{{- end }}
{{- end }}
{{- end }}
LBC watch Ingress ini, lihat annotation-nya, lalu provision ALB dengan konfigurasi yang cocok.
Urutan deploymentnya adalah sebagai berikut
# 1. VPC — paling cepat cd live/aws/dev/vpc && terragrunt init && terragrunt apply -auto-approve # 2. EKS — lambat (10-15 menit untuk control plane) cd ../eks && terragrunt init && terragrunt apply -auto-approve # 3. AWS Load Balancer Controller (sekali per cluster) cd ../alb-controller && terragrunt init && terragrunt apply -auto-approve # 4. Deploy app + Ingress + ALB cd ../learn && terragrunt init && terragrunt apply -auto-approve # 5. Ambil kubeconfig & verifikasi cd ../eks && terragrunt output -raw update_kubeconfig_command | bash kubectl get nodes # NAME STATUS ROLES AGE VERSION # ip-10-20-11-...compute.internal Ready <none> 5m v1.32.x-eks-yyyyy # ip-10-20-12-...compute.internal Ready <none> 5m v1.32.x-eks-yyyyy kubectl get pods,svc,ingress -n be # NAME READY STATUS RESTARTS AGE # pod/learn-7c8d4f9b9c-xxxxx 1/1 Running 0 1m # pod/learn-7c8d4f9b9c-yyyyy 1/1 Running 0 1m # # NAME TYPE CLUSTER-IP PORT(S) # service/learn ClusterIP 172.20.x.x 80/TCP # # NAME CLASS HOSTS ADDRESS # ingress/learn alb * k8s-be-learn-xxx.ap-southeast-3.elb.amazonaws.com curl http://k8s-be-learn-xxx.ap-southeast-3.elb.amazonaws.com

Aplikasi bisa diakses melalui web


Admin website igunawan.com, System Administrator, DevOps Engineer