EKS, GKE, AKS
สิ่งที่จะสร้าง
หัวข้อที่มีชื่อว่า “สิ่งที่จะสร้าง”EKS implementation พิสูจน์ว่า cluster interface ทำงานได้บน cloud เดียว ตอนนี้เรา implement อีกสองครั้ง — GKE (modules/gcp/cluster) และ AKS (modules/azure/cluster) — หลัง output cluster_name, cluster_endpoint, cluster_ca เดียวกัน แล้วทำสิ่งที่ output สามตัวนั้นมีอยู่เพื่อการนั้น: สร้าง kubeconfig และ config kubernetes กับ helm provider เพื่อให้ platform layer (Datadog, Keycloak, GrowthBook) deploy ลงบน cluster ตัวใดตัวหนึ่งในสามได้ด้วย code ที่เหมือนกัน
เมื่อเราทำเสร็จ live/gcp/cluster และ live/azure/cluster จะพก inputs block เดียวกัน กับ live/aws/cluster — node_size แบบ abstract, network id จาก dependency — และ provider configuration ที่เปลี่ยน cluster output ให้เป็น helm_release ที่ทำงานได้จะเป็นรูปทรงเดียวกันบนทุก cloud
managed-Kubernetes service ทั้งสามแยกทางกันมากกว่า network — ในเรื่อง node pool, ในเรื่อง identity, และโดยเฉพาะใน วิธี authenticate เข้า API server:
- EKS — auth ผ่าน exec plugin:
aws eks get-token - GKE — auth ผ่าน OAuth token อายุสั้นจาก
google_client_config - AKS — auth ผ่าน
kube_configของ cluster (certificate) หรือ exec plugin (kubelogin) สำหรับ AAD cluster
แต่ input ของแต่ละ auth method นั้นคือค่าสามตัวเดียวกัน: ชื่อ cluster, endpoint, และ CA นั่นคือเหตุผลที่ interface หยุดที่ output สามตัวนั้น — เป็นตัวส่วนร่วมของ “ฉันจะคุยกับ cluster นี้ยังไง” และ provider block ของแต่ละ cloud ประกอบขึ้นต่างกัน คง interface ไว้ที่สาม output แล้ว platform layer ข้างบนไม่เคยรู้ว่าตัวเองอยู่บน cloud ไหน
ข้อดีข้อเสีย
หัวข้อที่มีชื่อว่า “ข้อดีข้อเสีย”Auth ของ provider จาก cluster_endpoint + cluster_ca + exec/token plugin vs. เขียน kubeconfig file ลง disk
- Pros: ไม่มี kubeconfig file ที่เป็นความลับตกลงบน disk หรือใน state; provider ดึง token สดทุกครั้งที่รัน; output สามตัวเดียวกัน config provider บนทั้งสาม cloud
- Cons: เครื่องที่รัน Terragrunt ต้องติดตั้ง cloud CLI (หรือ
kubelogin) เพื่อให้ exec plugin ทำงาน ใน CI นั่นคือ setup step ที่ชัดเจน — ซึ่งเราทำให้มองเห็นได้แทนที่จะสมมติเอา
จัดการ node pool เป็น resource แยก (GKE) vs. inline ใน cluster (AKS)
- Pros:
google_container_node_poolแยกของ GKE ให้คุณเปลี่ยน node ได้โดยไม่ต้องสร้าง control plane ใหม่ — pattern ที่แนะนำdefault_node_poolแบบ inline ที่ AKS บังคับให้มี คง cluster ขั้นต่ำไว้ใน resource เดียว - Cons: สอง cloud ไม่เห็นตรงกันเรื่องรูปทรง ดังนั้น code ของ module ต่างกัน interface ซ่อนความต่างนั้นไว้: ทั้งคู่ยังรับ
node_count/node_sizeและปล่อย output สามตัวเดียวกัน
ติดตั้ง
หัวข้อที่มีชื่อว่า “ติดตั้ง”1. modules/gcp/cluster/ — GKE
หัวข้อที่มีชื่อว่า “1. modules/gcp/cluster/ — GKE”variables.tf และ node_size map เดียวกัน (map เข้ากับ GCP machine type), VPC-native cluster ที่อ้างอิง secondary range ของ network ด้วยชื่อ (pods, services), และ node pool ที่จัดการแยก
locals { machine_type = { small = "e2-standard-2" medium = "e2-standard-4" large = "e2-standard-8" }[var.node_size]}
resource "google_container_cluster" "this" { name = var.name location = "us-central1" network = var.network_id subnetwork = var.subnet_ids[0]
# Manage nodes via a separate pool, not the default one. remove_default_node_pool = true initial_node_count = 1
ip_allocation_policy { cluster_secondary_range_name = "pods" services_secondary_range_name = "services" }}
resource "google_container_node_pool" "this" { name = "${var.name}-default" cluster = google_container_cluster.this.id node_count = var.node_count
node_config { machine_type = local.machine_type oauth_scopes = ["https://www.googleapis.com/auth/cloud-platform"] }}endpoint ของ GKE กลับมาโดยไม่มี scheme และ CA อยู่ใต้ master_auth outputs.tf normalize ให้เข้า interface:
output "cluster_name" { value = google_container_cluster.this.name}
output "cluster_endpoint" { value = "https://${google_container_cluster.this.endpoint}"}
output "cluster_ca" { value = google_container_cluster.this.master_auth[0].cluster_ca_certificate}2. modules/azure/cluster/ — AKS
หัวข้อที่มีชื่อว่า “2. modules/azure/cluster/ — AKS”AKS บังคับให้มี default_node_pool แบบ inline และ managed identity cluster ลงจอดใน resource group ที่ derive จาก network node_size map เข้ากับ Azure VM size
locals { vm_size = { small = "Standard_D2s_v3" medium = "Standard_D4s_v3" large = "Standard_D8s_v3" }[var.node_size]}
resource "azurerm_kubernetes_cluster" "this" { name = var.name location = "eastus" resource_group_name = "${var.name}-rg" # created by the network module dns_prefix = var.name
default_node_pool { name = "default" node_count = var.node_count vm_size = local.vm_size vnet_subnet_id = var.subnet_ids[0] }
identity { type = "SystemAssigned" }}AKS expose ทุกอย่างผ่าน kube_config outputs.tf:
output "cluster_name" { value = azurerm_kubernetes_cluster.this.name}
output "cluster_endpoint" { value = azurerm_kubernetes_cluster.this.kube_config[0].host}
output "cluster_ca" { value = azurerm_kubernetes_cluster.this.kube_config[0].cluster_ca_certificate}สาม cloud, resource ที่ต่างกันมากสามตัว — ชื่อ output ชุดเดียวที่เหมือนกัน
3. The live/ units — same inputs, different source
หัวข้อที่มีชื่อว่า “3. The live/ units — same inputs, different source”live/gcp/cluster/terragrunt.hcl (Azure เหมือนกันเว้นแต่ source):
include "root" { path = find_in_parent_folders("root.hcl")}
terraform { source = "../../../modules/gcp/cluster"}
dependency "network" { config_path = "../network"
mock_outputs = { network_id = "projects/p/global/networks/mock" private_subnet_ids = ["projects/p/regions/us-central1/subnetworks/mock"] } mock_outputs_allowed_terraform_commands = ["plan", "validate"]}
inputs = { name = "clouddeploy" network_id = dependency.network.outputs.network_id subnet_ids = dependency.network.outputs.private_subnet_ids node_count = 2 node_size = "small"}inputs block นั้นคือตัว AWS จากบทที่แล้วแบบตัวอักษรต่อตัวอักษร interface ทำให้ได้อย่างนั้น
4. Building a kubeconfig / the kubernetes + helm providers
หัวข้อที่มีชื่อว่า “4. Building a kubeconfig / the kubernetes + helm providers”นี่คือสิ่งที่ output สามตัว มีไว้เพื่อ platform layer (สร้างใน module ทีหลัง) รัน Helm ผ่าน Terraform helm provider ซึ่งต้องการ kubernetes provider ที่ config จาก cluster configuration นั้นต่างกันแค่ auth plugin ต่อ cloud — endpoint และ CA เป็น cluster_endpoint กับ base64decode(cluster_ca) เสมอ
AWS — exec aws eks get-token:
provider "kubernetes" { host = var.cluster_endpoint cluster_ca_certificate = base64decode(var.cluster_ca) exec { api_version = "client.authentication.k8s.io/v1beta1" command = "aws" args = ["eks", "get-token", "--cluster-name", var.cluster_name] }}
provider "helm" { kubernetes { host = var.cluster_endpoint cluster_ca_certificate = base64decode(var.cluster_ca) exec { api_version = "client.authentication.k8s.io/v1beta1" command = "aws" args = ["eks", "get-token", "--cluster-name", var.cluster_name] } }}GCP — token อายุสั้นจาก google_client_config:
data "google_client_config" "default" {}
provider "kubernetes" { host = var.cluster_endpoint cluster_ca_certificate = base64decode(var.cluster_ca) token = data.google_client_config.default.access_token}Azure — exec kubelogin (หรือ kube_config แบบ cert-based ของ cluster):
provider "kubernetes" { host = var.cluster_endpoint cluster_ca_certificate = base64decode(var.cluster_ca) exec { api_version = "client.authentication.k8s.io/v1beta1" command = "kubelogin" args = ["get-token", "--server-id", var.cluster_name] }}ดูสิ่งที่คงที่ข้ามทั้งสาม: host = var.cluster_endpoint, cluster_ca_certificate = base64decode(var.cluster_ca) มีแค่ stanza ของ auth ที่เปลี่ยน — และเปลี่ยน เพราะ cloud authenticate ต่างกัน นั่นคือความต่างจริงที่ interface กลบไม่ได้และไม่ควรพยายาม ทุกอย่างที่ helm_release ทำเหนือบรรทัดนี้เหมือนกันหมด
ตรวจสอบผล
หัวข้อที่มีชื่อว่า “ตรวจสอบผล”Plan cluster แต่ละตัวจาก tree live/ ของแต่ละตัว — dependency resolve กับ mock network output บน plan ที่สะอาด:
terragrunt run --all plan --working-dir live/gcpterragrunt run --all plan --working-dir live/azureยืนยันว่าทั้งสาม cloud ปล่อยชื่อ output สามตัวเดียวกัน — interface ยึดได้ข้าม EKS, GKE, และ AKS:
for c in aws gcp azure; do echo "== $c ==" terragrunt output -json --working-dir live/$c/cluster | jq 'keys'done== aws ==[ "cluster_ca", "cluster_endpoint", "cluster_name" ]== gcp ==[ "cluster_ca", "cluster_endpoint", "cluster_name" ]== azure ==[ "cluster_ca", "cluster_endpoint", "cluster_name" ]ทีนี้บทพิสูจน์จริง — apply cluster, generate kubeconfig, แล้วเข้าถึงด้วย kubectl บน GCP:
cd live/gcp/cluster && terragrunt applygcloud container clusters get-credentials clouddeploy --region us-central1kubectl get nodesNAME STATUS ROLES AGE VERSIONgke-clouddeploy-default-abc1-... Ready <none> 2m v1.33.xgke-clouddeploy-default-abc1-... Ready <none> 2m v1.33.xnode Ready ที่เข้าถึงผ่าน kubeconfig ที่สร้างจาก cluster_endpoint และ cluster_ca — output สองตัวเดียวกัน บนคนละ cloud platform layer ตอนนี้ Helm-deploy ลงบน cluster ตัวใดในสามได้เหมือนกัน
ตรวจสอบความเข้าใจ:
- provider block ทั้งสามใช้
hostและcluster_ca_certificateร่วมกัน แต่ต่างกันที่ stanza เดียว stanza ไหน และทำไมความต่างนั้นถึงเป็นสิ่งเดียวที่ interface ตั้งใจปฏิเสธที่จะซ่อน? endpointดิบของ GKE ไม่มี scheme และ AKS expose host ไว้ใต้kube_configการ normalize นั้นเกิดที่ไหนเพื่อให้ consumer เห็นcluster_endpointแบบเดียวกัน?- GKE จัดการ node pool เป็น resource แยก ส่วน AKS บังคับให้เป็น inline
clusterinterface กันความต่างเชิงโครงสร้างนั้นไม่ให้ไปถึงlive/unit ได้อย่างไร? - CI runner fail ตอน config
helmprovider สำหรับ EKS ด้วย “aws: command not found” จาก exec block อธิบายสาเหตุรากและวิธีแก้
เรา implement cluster interface ครั้งที่สองและสาม — VPC-native GKE cluster พร้อม node pool ที่จัดการแยก และ AKS cluster พร้อม default pool แบบ inline และ managed identity — หลัง output cluster_name, cluster_endpoint, cluster_ca เดียวกัน แล้วเราเปลี่ยน output สามตัวนั้นให้เป็น kubeconfig และเป็น kubernetes/helm provider configuration ที่มีความต่างต่อ cloud แค่ auth plugin live/gcp/cluster และ live/azure/cluster unit ใช้ inputs เดียวกันเป๊ะกับ unit ของ AWS และ Terragrunt จัดลำดับ network-แล้ว-cluster บนทั้งสาม
network และ cluster เสร็จบนทุก cloud และ Helm เข้าถึงแต่ละตัวได้แล้ว แต่ cluster ที่รัน workload ได้ยังต้อง ให้ cloud access แก่ workload พวกนั้น — โดยไม่มี static key นั่นคือ layer ถัดไป ที่ cloud ทั้งสามแยกทางกันหนักที่สุด: Identity & IAM →