Terraform Kubernetes Cluster Deployment: Provision EKS และ Deploy Workload

การใช้ Terraform จัดการ Kubernetes cluster ถือเป็น pattern มาตรฐานในองค์กรที่ต้องการ Infrastructure as Code แบบเต็มรูปแบบ — ตั้งแต่การ provision managed cluster (EKS, GKE, AKS) การติดตั้ง add-on สำคัญ ไปจนถึง deploy application resource ลงใน cluster ด้วย Kubernetes provider โดยตรง

บทความนี้อธิบายโครงสร้างโปรเจกต์ในการ deploy K8s cluster หลาย layer, การใช้ hashicorp/kubernetes provider + helm provider, การจัดการ state ระหว่าง cluster และ workload, รวมถึงข้อควรระวังที่ทีม platform engineering พบบ่อย

สถาปัตยกรรมแบบ Multi-Layer

โปรเจกต์ Kubernetes ใน Terraform นิยมแยก root module เป็นอย่างน้อย 3 layer เพื่อลด blast radius และทำให้ state file ไม่ใหญ่เกินไป

  • Layer 1 — Network: VPC, subnet, security group, route table
  • Layer 2 — Cluster: EKS/GKE/AKS control plane + node group
  • Layer 3 — Workload: Namespace, Helm release, Ingress, Service, ConfigMap

แต่ละ layer มี state file แยกกัน layer ถัดไปอ่านค่าจาก layer ก่อนหน้าผ่าน terraform_remote_state data source ทำให้สามารถ apply แยกกันได้และปลอดภัยกว่าการรวมเป็น state เดียว

สร้าง EKS Cluster

ตัวอย่างใช้ module official terraform-aws-modules/eks/aws ซึ่งรวม IAM role, OIDC, node group, security group ไว้พร้อม

module "eks" {
  source  = "terraform-aws-modules/eks/aws"
  version = "~> 20.0"

  cluster_name    = "prod-cluster"
  cluster_version = "1.29"

  vpc_id     = data.terraform_remote_state.network.outputs.vpc_id
  subnet_ids = data.terraform_remote_state.network.outputs.private_subnets

  eks_managed_node_groups = {
    default = {
      instance_types = ["t3.medium"]
      min_size       = 2
      max_size       = 6
      desired_size   = 3
    }
  }

  enable_irsa = true
}

flag enable_irsa = true สร้าง OIDC provider สำหรับ IAM Roles for Service Accounts — pod สามารถ assume IAM role ตาม namespace/SA ได้โดยไม่ต้องใส่ static credential

Kubernetes Provider

หลัง cluster พร้อม ให้ตั้งค่า provider kubernetes โดย authenticate ผ่าน exec plugin ของ AWS CLI — วิธีนี้ปลอดภัยกว่าการ hardcode token ลง state

provider "kubernetes" {
  host                   = module.eks.cluster_endpoint
  cluster_ca_certificate = base64decode(module.eks.cluster_certificate_authority_data)

  exec {
    api_version = "client.authentication.k8s.io/v1beta1"
    command     = "aws"
    args        = ["eks", "get-token", "--cluster-name", module.eks.cluster_name]
  }
}

การใช้ exec ให้ AWS CLI จัดการ token rotation อัตโนมัติ — ไม่ต้องอัปเดต provider config เมื่อ token หมดอายุ

จัดการ Namespace และ Resource

resource "kubernetes_namespace" "app" {
  metadata {
    name = "production"
    labels = {
      environment = "prod"
      managed-by  = "terraform"
    }
  }
}

resource "kubernetes_config_map" "app_config" {
  metadata {
    name      = "app-config"
    namespace = kubernetes_namespace.app.metadata[0].name
  }
  data = {
    LOG_LEVEL = "info"
    REGION    = "ap-southeast-1"
  }
}

แนะนำให้จัดการ resource ระดับ cluster (namespace, CRD, RBAC, storage class) ด้วย Terraform ส่วน application deployment ให้ใช้เครื่องมือ GitOps เช่น Argo CD หรือ Flux ซึ่งเหมาะกับ rolling update และ rollback กว่า

Helm Provider

การติดตั้ง chart platform สำคัญ เช่น ingress-nginx, cert-manager, metrics-server ผ่าน helm provider เป็นทางเลือกที่สะดวกและทำให้ dependency ระหว่าง infrastructure กับ platform service ชัดเจน

provider "helm" {
  kubernetes {
    host                   = module.eks.cluster_endpoint
    cluster_ca_certificate = base64decode(module.eks.cluster_certificate_authority_data)
    exec {
      api_version = "client.authentication.k8s.io/v1beta1"
      command     = "aws"
      args        = ["eks", "get-token", "--cluster-name", module.eks.cluster_name]
    }
  }
}

resource "helm_release" "ingress_nginx" {
  name       = "ingress-nginx"
  repository = "https://kubernetes.github.io/ingress-nginx"
  chart      = "ingress-nginx"
  namespace  = "ingress-nginx"
  version    = "4.10.0"

  create_namespace = true

  values = [yamlencode({
    controller = {
      service = {
        annotations = {
          "service.beta.kubernetes.io/aws-load-balancer-type" = "nlb"
        }
      }
    }
  })]
}

ค่า values รับได้ทั้ง string YAML ตรง ๆ หรือใช้ yamlencode() แปลง HCL map เป็น YAML — วิธีหลังอ่านง่ายกว่าและสามารถ interpolate ค่าได้

IRSA และ Service Account

สำหรับ workload ที่ต้องการเข้าถึง AWS service (S3, DynamoDB, SQS) ให้สร้าง IAM role + service account mapping เอาไว้ล่วงหน้า

module "app_irsa" {
  source  = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks"
  version = "~> 5.0"

  role_name = "app-s3-reader"
  role_policy_arns = {
    s3 = "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
  }

  oidc_providers = {
    main = {
      provider_arn               = module.eks.oidc_provider_arn
      namespace_service_accounts = ["production:app"]
    }
  }
}

resource "kubernetes_service_account" "app" {
  metadata {
    name      = "app"
    namespace = "production"
    annotations = {
      "eks.amazonaws.com/role-arn" = module.app_irsa.iam_role_arn
    }
  }
}

การจัดการ State และ Lock

  • ใช้ S3 backend + DynamoDB lock แยกไฟล์ state ของแต่ละ layer — cluster/workload/network คนละ file
  • ห้าม import resource ระดับ workload ที่ deploy ผ่าน Argo CD เข้ามาใน Terraform state — จะเกิด drift ตลอด
  • ต้อง lifecycle ignore_changes สำหรับ node group desired_size หาก cluster autoscaler ปรับค่าเองได้ ไม่เช่นนั้น Terraform จะ revert ทุกครั้ง

CI/CD Pipeline สำหรับ Cluster

  • แยก pipeline: infra-network → infra-cluster → infra-platform (Helm add-on) → app (GitOps)
  • ใช้ OIDC federation ให้ GitHub Actions assume role ตรง ๆ โดยไม่ต้องเก็บ AWS static credential
  • รัน terraform plan อัตโนมัติบน PR, รัน apply หลัง merge main โดยให้มนุษย์ approve ก่อน
  • เปิดใช้ drift detection รายสัปดาห์ — ใช้ terraform plan เทียบ live state

ข้อควรระวัง

  • Terraform kubernetes provider ไม่เหมาะกับการ deploy application ที่เปลี่ยนบ่อย — ใช้ GitOps tool แทน
  • การอัปเดต cluster version ควรทำเป็นขั้น minor (1.28 → 1.29) ห้ามข้าม version
  • หากลบ module EKS จะลบทุก resource ใน cluster → ตั้ง prevent_destroy = true ที่ lifecycle ของ cluster
  • ควร backup etcd (ผ่าน AWS managed backup ของ EKS) หรือใช้ Velero สำหรับ workload backup
  • เก็บ kubeconfig ที่ใช้ interact กับ cluster ไว้อย่างปลอดภัย อย่า commit ลง git

สรุป

Terraform เป็นเครื่องมือที่เหมาะกับการ provision Kubernetes cluster และจัดการ platform-level resource ได้อย่างมีประสิทธิภาพ — โดยเฉพาะเมื่อแยก layer ชัดเจนระหว่าง network, cluster และ platform add-on ทำให้ blast radius เล็กลง และลดโอกาสที่ apply เดียวจะกระทบ resource หลายระดับ

อย่างไรก็ดี หน้าที่ของ Terraform ควรหยุดที่ระดับ platform service ส่วน application deployment ให้ใช้ GitOps tool (Argo CD, Flux) ซึ่งออกแบบมาสำหรับ workflow นี้โดยเฉพาะ — การแยก concern แบบนี้ทำให้ทีม platform และทีม application engineer ทำงานขนานกันได้โดยไม่รบกวน state ของกันและกัน