Managed Postgres
สิ่งที่จะสร้าง
หัวข้อที่มีชื่อว่า “สิ่งที่จะสร้าง”data module พิสูจน์ interface บน AWS RDS แล้ว ตอนนี้เรา implement input และ output ทั้งห้าตัวเดียวกัน บน GCP Cloud SQL และ Azure Database for PostgreSQL แล้วทำสิ่งที่ทั้งหมดนี้มีไว้เพื่อ: เปลี่ยน db_host, db_port, db_name, db_user และ db_password ให้เป็น Kubernetes Secret ที่ ShopMicro อ่านเป็น DATABASE_URL
ผลตอบแทนของ interface ที่สม่ำเสมอเห็นได้ตรงนี้ unit ของ live/gcp/data/ กับ live/azure/data/ มีรูปร่างเหมือน AWS แบบ byte-for-byte — dependency "network" เดียวกัน input เดียวกัน — เพราะต่างกันแค่ implementation เท่านั้น แต่ความต่างนั้นก็เป็นเรื่องจริง: private connectivity คือ VPC security group บน AWS, private service access บน GCP และ delegated subnet บวก private DNS zone บน Azure การตั้งชื่อความต่างเหล่านั้นคือครึ่งหนึ่งของบทเรียน
นี่คือจุดที่ “หนึ่ง interface สาม cloud” พิสูจน์คุณค่าหรือพังทลาย ถ้า managed database แต่ละตัว expose output คนละชุด downstream consumer ทุกตัว — ShopMicro unit, connection secret, CI — จะต้องมี code path สามทาง ด้วยการบังคับให้ Cloud SQL กับ Azure Database emit db_host/db_port/db_name/db_user/db_password เป๊ะ ตัว consumer จึงยัง cloud-agnostic: สร้าง DATABASE_URL เดียวแบบเดียวกันไม่ว่า database จะรันอยู่ที่ไหนจริง ๆ
managed-DB layer ยังเป็นจุดที่ private-networking model ของแต่ละ cloud ต่างกันมากที่สุด จึงเป็นที่ที่ซื่อตรงที่สุดในการแสดงว่า “multi-cloud” ไม่ได้มาฟรี interface ซ่อนความต่างจาก consumer แต่ไม่ซ่อนจาก คุณ คนที่เขียน module นั่นคือ trade ที่ถูกต้อง — รับความซับซ้อนไว้ครั้งเดียวใน module เพื่อให้สิบห้าอย่างที่อยู่ downstream ไม่ต้องจ่ายราคานั้นทุกตัว
ข้อดีข้อเสีย
หัวข้อที่มีชื่อว่า “ข้อดีข้อเสีย”One interface hiding three private-networking models vs cloud-native modules
- Pros: consumer สร้าง connection string แบบเดียวกันทุกที่ การสลับ cloud ไม่แตะ ShopMicro และ diff ระหว่าง
live/gcp/dataกับlive/azure/dataคือ input ไม่ใช่ logic - Cons: interface เป็น lowest-common-denominator — surface Cloud SQL read replica หรือ Azure zone-redundant HA ไม่ได้โดยไม่ปล่อย output เฉพาะ cloud ออกมา เมื่อคุณต้องการสิ่งเหล่านั้น คุณขยาย interface อย่างตั้งใจ แทนที่จะ special-case consumer
Building the connection secret in Terraform vs letting the app assemble it
- Pros: Terraform ถือทุกชิ้นส่วนไว้ใน state อยู่แล้ว จึงเขียน
Secretเดียวที่มีDATABASE_URLพร้อมใช้ได้ ส่วน app แค่อ่าน env var แล้วไม่ต้องรู้เรื่อง host/port/credential เลย - Cons: วิธีนี้ผูก data unit เข้ากับ Kubernetes provider และรูปร่าง secret ที่ app คาดหวัง ทางเลือกอีกทาง — ยื่นค่าทั้งห้าให้ app แล้วให้ service ประกอบ URL เอง — ช่วย decouple แต่ก็ผลัก URL-encoding กับการจัดการ null เข้าไปในทุก service
ติดตั้ง
หัวข้อที่มีชื่อว่า “ติดตั้ง”1. modules/gcp/data/main.tf — Cloud SQL
หัวข้อที่มีชื่อว่า “1. modules/gcp/data/main.tf — Cloud SQL”Cloud SQL เข้าถึง VPC ผ่าน private service access: ช่วง IP ที่ reserve ไว้แล้ว peer เข้ากับ network ของคุณ แล้ว instance จะ bind เข้ากับช่วงนั้นด้วย ipv4_enabled = false
variable "name" { type = string }variable "network_id" { type = string } # the VPC self_link / idvariable "subnet_ids" { type = list(string) }variable "db_name" { type = string default = "shopmicro"}
# reserve a range and peer it for private service accessresource "google_compute_global_address" "psa" { name = "${var.name}-psa" purpose = "VPC_PEERING" address_type = "INTERNAL" prefix_length = 16 network = var.network_id}
resource "google_service_networking_connection" "psa" { network = var.network_id service = "servicenetworking.googleapis.com" reserved_peering_ranges = [google_compute_global_address.psa.name]}
resource "random_password" "db" { length = 24 special = false}
resource "google_sql_database_instance" "this" { name = var.name database_version = "POSTGRES_16" region = "us-central1" depends_on = [google_service_networking_connection.psa]
settings { tier = "db-custom-1-3840" # 1 vCPU / 3.75 GB
ip_configuration { ipv4_enabled = false private_network = var.network_id enable_private_path_for_google_cloud_services = true } }
deletion_protection = false # teaching platform}
resource "google_sql_database" "this" { name = var.db_name instance = google_sql_database_instance.this.name}
resource "google_sql_user" "this" { name = "shopmicro" instance = google_sql_database_instance.this.name password = random_password.db.result}
output "db_host" { value = google_sql_database_instance.this.private_ip_address }output "db_port" { value = 5432 }output "db_name" { value = google_sql_database.this.name }output "db_user" { value = google_sql_user.this.name }output "db_password" { value = random_password.db.result sensitive = true}สังเกตว่า db_port เป็นค่าคงที่ 5432 — Cloud SQL ไม่ expose attribute ของ port ดังนั้น module จึงจ่ายค่าคงที่ให้เพื่อรักษา interface ให้ครบ นั่นคือ interface ที่รับ quirk ของ cloud ไว้แทน consumer
2. modules/azure/data/main.tf — Azure Database for PostgreSQL
หัวข้อที่มีชื่อว่า “2. modules/azure/data/main.tf — Azure Database for PostgreSQL”Flexible Server ของ Azure ไปแบบ private ผ่าน delegated subnet บวก private DNS zone ที่ link เข้ากับ VNet
variable "name" { type = string }variable "network_id" { type = string } # the VNet idvariable "subnet_ids" { type = list(string) } # first is the delegated DB subnetvariable "db_name" { type = string default = "shopmicro"}variable "resource_group_name" { type = string default = "clouddeploy"}
data "azurerm_resource_group" "this" { name = var.resource_group_name}
resource "azurerm_private_dns_zone" "pg" { name = "${var.name}.postgres.database.azure.com" resource_group_name = data.azurerm_resource_group.this.name}
resource "azurerm_private_dns_zone_virtual_network_link" "pg" { name = "${var.name}-link" private_dns_zone_name = azurerm_private_dns_zone.pg.name resource_group_name = data.azurerm_resource_group.this.name virtual_network_id = var.network_id}
resource "random_password" "db" { length = 24 special = false}
resource "azurerm_postgresql_flexible_server" "this" { name = var.name resource_group_name = data.azurerm_resource_group.this.name location = data.azurerm_resource_group.this.location version = "16" delegated_subnet_id = var.subnet_ids[0] private_dns_zone_id = azurerm_private_dns_zone.pg.id public_network_access_enabled = false
administrator_login = "shopmicro" administrator_password = random_password.db.result
storage_mb = 32768 sku_name = "B_Standard_B1ms"
depends_on = [azurerm_private_dns_zone_virtual_network_link.pg]}
resource "azurerm_postgresql_flexible_server_database" "this" { name = var.db_name server_id = azurerm_postgresql_flexible_server.this.id charset = "UTF8" collation = "en_US.utf8"}
output "db_host" { value = azurerm_postgresql_flexible_server.this.fqdn }output "db_port" { value = 5432 }output "db_name" { value = azurerm_postgresql_flexible_server_database.this.name }output "db_user" { value = azurerm_postgresql_flexible_server.this.administrator_login }output "db_password" { value = random_password.db.result sensitive = true}สาม cloud สาม mechanism ของ private-networking — VPC security group (AWS), private service access peering (GCP), delegated subnet + private DNS zone (Azure) — และ output ชุดเดียวกันเป๊ะ
3. The connection secret ShopMicro consumes
หัวข้อที่มีชื่อว่า “3. The connection secret ShopMicro consumes”ตอนนี้ถึงจุดประสงค์ของ output ทั้งห้า: ประกอบทั้งห้าเป็น DATABASE_URL ใน Kubernetes Secret นี่คือชิ้นเล็ก ๆ ของ ShopMicro unit ที่แสดงตรงนี้เพราะเป็นปลายทางที่ output ของ data ป้อนเข้าไป และ cloud-agnostic — เหมือนกันบนทุก cloud เพราะ interface เหมือนกัน:
dependency "data" { config_path = "../data" }
resource "kubernetes_secret" "db" { metadata { name = "shopmicro-db" namespace = "shopmicro" }
data = { DATABASE_URL = format( "postgres://%s:%s@%s:%d/%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, ) }}เพราะ password เดินทางมาเป็น sensitive output จึงไหล dependency → secret โดยไม่เคยพิมพ์ลง log เลย ShopMicro mount DATABASE_URL เป็น env var แล้วไม่เคยรู้ว่าชี้ไปที่ database ของ cloud ไหน
ตรวจสอบผล
หัวข้อที่มีชื่อว่า “ตรวจสอบผล”apply data unit ของแต่ละ cloud แล้วยืนยันว่า output มีรูปร่างเดียวกัน จากนั้นยืนยันว่า secret ถูกสร้างขึ้นจริง
# GCPcd live/gcp/data && terragrunt applyterragrunt output db_host # => "10.42.0.3" (private IP, no public route)terragrunt output db_port # => 5432
# Azurecd ../../azure/data && terragrunt applyterragrunt output db_host # => "clouddeploy.postgres.database.azure.com"terragrunt output db_password # => <sensitive>ทุก cloud emit db_host/db_port/db_name/db_user/db_password — ความเหมือนนั้นคือสิ่งที่ทำให้ connection secret เป็น cloud-agnostic ตอนนี้ยืนยัน secret และการเชื่อมต่อจริงจากภายใน cluster:
kubectl -n shopmicro get secret shopmicro-db -o jsonpath='{.data.DATABASE_URL}' | base64 -d# => postgres://shopmicro:...@10.42.0.3:5432/shopmicro (host varies by cloud)
kubectl -n shopmicro run pg --rm -it --restart=Never --image=postgres:16 -- \ psql "$(kubectl -n shopmicro get secret shopmicro-db -o jsonpath='{.data.DATABASE_URL}' | base64 -d)" -c '\conninfo'# => You are connected to database "shopmicro" ...สุดท้าย terragrunt run --all plan ข้ามทั้งสาม tree ของ live/<cloud>/ ควรแสดง network → cluster → data ที่ converge ทุกที่ โดย data unit ต่างกันแค่ input เท่านั้น — นั่นคือหลักฐานว่า interface ยืนหยัดได้
ตรวจสอบความเข้าใจ:
- AWS, GCP และ Azure ทำให้ database เป็น private คนละแบบ บอก mechanism ของแต่ละตัว
- Cloud SQL ไม่มี attribute ของ port ดังนั้น module จึง return ค่าคงที่
5432ทำไมทำแบบนั้นใน module ถึงดีกว่าให้ consumer special-case GCP? - output ชุดเดียวกันเป๊ะให้ประโยชน์อะไรกับ code ของ connection-secret และจะพังตรงไหนถ้าคุณต้องการ Cloud SQL read replica?
- ไล่ตาม database password ตั้งแต่ตอนสร้างจนถึง env var ของ pod password มองเห็นเป็น plaintext ที่ step ไหนบ้าง และอะไรทำให้ step เหล่านั้นปลอดภัย?
เรา implement data interface อีกสองครั้ง — GCP Cloud SQL ผ่าน private service access และ Azure Database for PostgreSQL ผ่าน delegated subnet กับ private DNS zone — หลัง output ทั้งห้าตัวเป๊ะ ๆ ที่เวอร์ชัน AWS emit จากนั้นเราเปลี่ยน output เหล่านั้นเป็น DATABASE_URL Secret แบบ cloud-agnostic ที่พา password ที่ generate ขึ้นมาจาก state ไปยัง pod โดยไม่เคย log ออกมาเลย private-networking model ต่างกันจริงต่อ cloud interface รับความต่างนั้นไว้เพื่อให้ไม่มีอะไร downstream ต้องแคร์
ตอนนี้ platform มี network, cluster, identity และ data บนทั้งสาม cloud — ทุก dependency ที่ ShopMicro ต้องใช้ ถัดไป Deploying ShopMicro → วาง application ลงบนแต่ละ cluster ด้วย helm_release โดยต่อ database secret นี้และ keyless identity จาก IAM module เข้าไป