The ShopMicro Helm release
สิ่งที่จะสร้าง
หัวข้อที่มีชื่อว่า “สิ่งที่จะสร้าง”ทุกอย่างจนถึงตอนนี้เป็นเรื่องของ platform: network, managed Kubernetes cluster และ managed Postgres database บนแต่ละ cloud บทนี้วาง workload จริงลงไปบนฐานนั้น — ShopMicro ที่ deploy ลงทุก cluster ด้วย Terraform helm_release เดียว โดยต่อ database URL ตรงจาก output ของ data module
ชิ้นส่วนต่าง ๆ:
- สำเนา vendored ของ ShopMicro Helm chart ดึงจาก project #3 (
deploy/helm/shopmicro) เข้ามาที่charts/shopmicroที่ root ของ repo เพื่อให้ deploy ทำซ้ำได้และถูก pin ไว้แทนที่จะ fetch สด - module
modules/shopmicroที่เล็กและ cloud-neutral ซึ่งมีงานเดียวคือhelm_release— Terraform เดียวกันบนทั้งสาม cloud - Terragrunt unit
live/<cloud>/shopmicroที่ประกาศdependencyบนทั้งclusterและdata, generate providerhelm/kubernetesจาก output ของ cluster และสร้าง Postgres connection string จากdb_host,db_user,db_passwordและเพื่อน ๆ
ในตอนจบ terragrunt apply ใน live/aws/shopmicro (และพี่น้อง gcp/azure) จะ roll ShopMicro ลง cluster โดยคุยกับ managed database ส่วน ingress และ DNS มาใน บทถัดไป
ทำไมใช้ helm_release ของ Terraform แทน helm install ดิบใน CI? เพราะ workload ควรอยู่ใน state และ dependency graph เดียวกับ infrastructure ที่พึ่งพาอยู่ เมื่อ data module rotate password หรือ endpoint ของ cluster เปลี่ยน helm_release ที่อ่านค่าเหล่านั้นจาก dependency.*.outputs จะ re-render และ re-apply โดยไม่มีใครต้อง copy connection string ด้วยมือ chart ถูก managed ไม่ใช่ invoked
ทำไม vendor chart แทนที่จะอ้างอิง remote repository? chart ของ ShopMicro อยู่ใน repo อื่น การดึงสดทุกครั้งจะผูกทุก apply เข้ากับความพร้อมใช้ของ repo นั้นและ branch main การ vendor สำเนาที่รู้ว่าดีไว้ใต้ charts/shopmicro pin สิ่งที่เรา deploy ไว้เป๊ะ ให้เรา review upgrade เป็น diff ได้ และทำให้ terragrunt apply เป็น hermetic ส่วน trade-off — คุณต้อง re-vendor อย่างตั้งใจเพื่อรับ upstream fix — นั่นแหละคือประเด็น: upgrade เป็นเรื่องที่จงใจ
ทำไมต้องเป็น module ที่ cloud-neutral? ตัว helm_release เองไม่แคร์ว่าจะไปลงบน EKS, GKE หรือ AKS มีแค่ provider — วิธีที่ Terraform authenticate เข้า cluster — เท่านั้นที่เฉพาะ cloud และนั่นถูก generate ต่อ cloud ใน Terragrunt unit การให้ module cloud-neutral หมายความว่า chart ถูก deploy เหมือนกันทุกที่ นั่นคือสิ่งที่คำมั่นสัญญาแบบ “platform” ต้องการพอดี
ข้อดีข้อเสีย
หัวข้อที่มีชื่อว่า “ข้อดีข้อเสีย”helm_release (Terraform-managed) vs helm upgrade in a CI step
- Pros: state เดียว dependency graph เดียว DB URL ไหลอัตโนมัติจาก output ของ
datadrift บน chart value มองเห็นได้ในterragrunt planและ teardown คือdestroyที่สะอาด - Cons: ตอนนี้ Terraform เป็นเจ้าของ lifecycle ของ Helm ดังนั้น release ที่ค้าง (
pending-upgradeที่ติดอยู่) ต้องแกะผ่าน Terraform และ chart ที่ใหญ่มากทำให้ plan รก
Vendoring the chart vs referencing the upstream Helm repo by URL/version
- Pros: apply ที่ทำซ้ำได้ review ได้ และทำงาน offline ได้ มี source of truth เดียวเป๊ะสำหรับสิ่งที่ deploy
- Cons: คุณแบก chart ไว้ใน repo และต้อง re-vendor เพื่อรับ upstream change และสำเนาอาจ drift จาก upstream ถ้าคุณลืม
ติดตั้ง
หัวข้อที่มีชื่อว่า “ติดตั้ง”1. charts/shopmicro/ (vendor the chart)
หัวข้อที่มีชื่อว่า “1. charts/shopmicro/ (vendor the chart)”copy chart ของ ShopMicro ออกจาก project #3 เข้ามาใน repo นี้ จาก root ของ repo:
# One-time vendor of the ShopMicro Helm chart (pin a tag, not a moving branch)git clone --depth 1 --branch v1.0.0 \ https://github.com/avetavos/realworld-shopmicro /tmp/shopmicrocp -R /tmp/shopmicro/deploy/helm/shopmicro charts/shopmicrorm -rf /tmp/shopmicroตอนนี้คุณควรมี charts/shopmicro/Chart.yaml, values.yaml และ templates/ ที่ commit เข้า CloudDeploy repo แล้ว บันทึก upstream tag ไว้ใน comment หรือ CHANGELOG เพื่อให้การ re-vendor ครั้งถัดไปชัดเจน
2. modules/shopmicro/ (the cloud-neutral release)
หัวข้อที่มีชื่อว่า “2. modules/shopmicro/ (the cloud-neutral release)”module นี้ตั้งใจให้บาง: variable เข้า, helm_release เดียวออก module อ่าน chart จาก absolute path บน disk (คำนวณโดย Terragrunt unit) ดังนั้นจึงไม่มีอะไรถูก fetch ตอน apply
terraform { required_providers { helm = { source = "hashicorp/helm", version = "~> 3.0" } kubernetes = { source = "hashicorp/kubernetes", version = "~> 2.30" } }}variable "chart_path" { type = string } # absolute path to charts/shopmicrovariable "chart_version" { type = string } # the vendored chart's versionvariable "namespace" { type = string, default = "shopmicro" }variable "image_tag" { type = string, default = "latest" }variable "db_url" { type = string, sensitive = true } # postgres:// ... built from data outputsresource "helm_release" "shopmicro" { name = "shopmicro" namespace = var.namespace create_namespace = true
chart = var.chart_path version = var.chart_version
# helm provider v3: set / set_sensitive are lists of objects. set = [ { name = "image.tag", value = var.image_tag }, ]
set_sensitive = [ { name = "database.url", value = var.db_url }, ]}
output "namespace" { value = helm_release.shopmicro.namespace}key database.url ตรงนี้ตรงกับ chart value ของ ShopMicro — Postgres DSN เดียวที่ service ทั้งหลายอ่าน เพราะพา password มาด้วย ค่านี้จึงไปใน set_sensitive ไม่ใช่ set ธรรมดา
3. live/aws/shopmicro/terragrunt.hcl (wire the dependencies)
หัวข้อที่มีชื่อว่า “3. live/aws/shopmicro/terragrunt.hcl (wire the dependencies)”unit นี้เป็นที่ที่ความจริงเฉพาะ cloud ลงมา: จะ authenticate เข้า cluster ไหน และจะสร้าง DB URL จาก output ของ data อย่างไร
include "root" { path = find_in_parent_folders("root.hcl") }
terraform { source = "../../../modules/shopmicro" }
dependency "cluster" { config_path = "../cluster" mock_outputs = { cluster_name = "clouddeploy" cluster_endpoint = "https://localhost" cluster_ca = "" # base64 }}
dependency "data" { config_path = "../data" mock_outputs = { db_host = "localhost" db_port = 5432 db_name = "shopmicro" db_user = "shopmicro" db_password = "mock" }}
# Authenticate the helm/kubernetes providers to THIS cluster (EKS variant).generate "k8s_providers" { path = "k8s_providers.tf" if_exists = "overwrite_terragrunt" contents = <<EOFdata "aws_eks_cluster_auth" "this" { name = "${dependency.cluster.outputs.cluster_name}"}provider "helm" { kubernetes = { host = "${dependency.cluster.outputs.cluster_endpoint}" cluster_ca_certificate = base64decode("${dependency.cluster.outputs.cluster_ca}") token = data.aws_eks_cluster_auth.this.token }}provider "kubernetes" { host = "${dependency.cluster.outputs.cluster_endpoint}" cluster_ca_certificate = base64decode("${dependency.cluster.outputs.cluster_ca}") token = data.aws_eks_cluster_auth.this.token}EOF}
inputs = { chart_path = "${get_repo_root()}/charts/shopmicro" chart_version = "1.0.0" db_url = format( "postgres://%s:%s@%s:%s/%s", dependency.data.outputs.db_user, dependency.data.outputs.db_password, dependency.data.outputs.db_host, dependency.data.outputs.db_port, dependency.data.outputs.db_name, )}สองอย่างพิสูจน์คุณค่าตรงนี้ mock_outputs ให้ terragrunt plan รันได้ก่อน apply cluster และ data ครั้งแรก ดังนั้นทั้ง tree live/aws จึง plan เป็นหน่วยเดียวได้ และ get_repo_root() resolve chart ที่ vendor ไว้เป็น absolute path เพื่อให้ helm_release อ่านจาก disk แทน module cache ที่ copy มา
4. live/gcp/shopmicro/ และ live/azure/shopmicro/ (the same, one block different)
หัวข้อที่มีชื่อว่า “4. live/gcp/shopmicro/ และ live/azure/shopmicro/ (the same, one block different)”block dependency, inputs และการสร้าง db_url นั้น เหมือนกันเป๊ะ — นั่นคือผลตอบแทนของ module interface ที่สม่ำเสมอ มีแค่ auth ของ provider ที่ generate ขึ้นเท่านั้นที่ต่างกัน เพราะแต่ละ cloud แจก cluster token คนละวิธี:
# GCP (GKE): token from the google provider's client configdata "google_client_config" "this" {}provider "helm" { kubernetes = { host = "https://${dependency.cluster.outputs.cluster_endpoint}" cluster_ca_certificate = base64decode("${dependency.cluster.outputs.cluster_ca}") token = data.google_client_config.this.access_token }}# Azure (AKS): the cluster module exposes the admin kubeconfig bits directlyprovider "helm" { kubernetes = { host = "${dependency.cluster.outputs.cluster_endpoint}" cluster_ca_certificate = base64decode("${dependency.cluster.outputs.cluster_ca}") client_certificate = base64decode("${dependency.cluster.outputs.client_certificate}") client_key = base64decode("${dependency.cluster.outputs.client_key}") }}นี่คือภาษี multi-cloud ที่ซื่อตรงที่ architecture ตั้งชื่อไว้ตั้งแต่ต้น: สามวิธีในการได้ cluster credential Terragrunt กักไว้ในไฟล์ generate หนึ่งไฟล์ต่อ cloud แทนที่จะปล่อยให้รั่วเข้าไปใน module
ตรวจสอบผล
หัวข้อที่มีชื่อว่า “ตรวจสอบผล”init และ plan unit ของ AWS plan ควรแสดง helm_release.shopmicro หนึ่งตัวที่จะ add:
cd live/aws/shopmicroterragrunt planTerraform will perform the following actions: # helm_release.shopmicro will be created + resource "helm_release" "shopmicro" { + name = "shopmicro" + namespace = "shopmicro" + chart = "/…/charts/shopmicro" + version = "1.0.0" + create_namespace = true }Plan: 1 to add, 0 to change, 0 to destroy.apply แล้วยืนยัน release และ pod ด้วย kubectl (ใช้ kubeconfig ที่คุณสร้างใน module Managed Kubernetes):
terragrunt applyhelm list -n shopmicrokubectl get pods -n shopmicroNAME NAMESPACE REVISION STATUS CHART APP VERSIONshopmicro shopmicro 1 deployed shopmicro-1.0.0 1.0.0
NAME READY STATUS RESTARTS AGEshopmicro-gateway-6d8c... 1/1 Running 0 40sshopmicro-users-7b9f... 1/1 Running 0 40sshopmicro-orders-5c4d... 1/1 Running 0 40spod ที่ Running และอ่าน managed database ได้หมายความว่า workload ขึ้นแล้ว ถ้า service ตัวไหน CrashLoopBackOff ให้เช็ก log ตัวนั้นหา Postgres connection error ก่อน — นั่นเกือบทุกครั้งคือปัญหาการต่อสาย db_url ไม่ใช่ปัญหาของ chart:
kubectl logs -n shopmicro deploy/shopmicro-gatewayทำ terragrunt apply ซ้ำใน live/gcp/shopmicro และ live/azure/shopmicro เพื่อให้ได้ workload เดียวกันบนทั้งสาม cloud
ตรวจสอบความเข้าใจ:
- ทำไม
db_urlถึงถูกสร้างใน Terragrunt unit จากdependency.data.outputsแทนที่จะ hard-code เป็น chart value? - อะไรจะพังถ้าคุณอ้างอิง upstream chart repository ด้วย URL แทนที่จะ vendor ไว้ใต้
charts/shopmicro? - module
modules/shopmicroเป็น cloud-neutral แต่การ deploy ยังต่างกันต่อ cloud ความต่างนั้นอยู่ที่ไหน และทำไมถึงอยู่ตรงนั้น? mock_outputsบน dependencyclusterและdataให้คุณทำอะไรได้ที่ไม่งั้นคุณทำไม่ได้?
ตอนนี้ ShopMicro รันบนทุก cluster ในรูป helm_release ที่ Terraform managed: chart ที่ vendor ไว้, module ที่บางและ cloud-neutral และ unit live/<cloud>/shopmicro ที่ดึง cluster credential และ database URL ตรงจาก dependency cluster และ data ของตัวเอง workload อยู่ใน state และ dependency graph เดียวกับ infrastructure ที่อยู่ข้างใต้ — ดังนั้น password ที่ rotate หรือ cluster ที่ rebuild จะไหลผ่านมาในการ apply ครั้งถัดไป
pod อยู่ในสถานะ Running แล้ว แต่ยังไม่มีอะไรจากนอก cluster เข้าถึงได้ ถัดไป Ingress and per-cloud config → วาง ShopMicro ไว้หลัง ingress และ DNS บนแต่ละ cloud แล้วพิสูจน์ว่า app ตอบผ่าน HTTP ได้