The data module
สิ่งที่จะสร้าง
หัวข้อที่มีชื่อว่า “สิ่งที่จะสร้าง”ShopMicro ต้องใช้ PostgreSQL database บนเครื่อง laptop เป็น container แต่บน production เป็น database แบบ managed — backup, patching และ failover ให้ cloud จัดการให้ — วางไว้แบบ private อยู่ภายใน network เดียวกับ cluster เข้าถึงได้จากข้างในเท่านั้น
บทนี้นิยาม interface ของ data module แล้วสร้าง implementation บน AWS RDS เหมือนกับ network, cluster และ iam คือมี interface เดียวกันบนทุก cloud ทำให้ unit ใน live/ ต่อสายแบบเดียวกันได้:
- in:
name,network_id,subnet_ids,db_name(defaultshopmicro) - out:
db_host,db_port,db_name,db_user,db_password(sensitive)
บทถัดไป managed Postgres implement interface เดียวกันนี้บน GCP Cloud SQL กับ Azure Database แล้วเปลี่ยน output ทั้งห้าตัวนั้นให้เป็น connection secret ที่ ShopMicro ใช้ ในบทนี้เราวางรูปร่างของ interface และสร้างเวอร์ชัน AWS โดยรับ dependency "network" เข้ามาเพื่อให้ database ไปลงใน private subnet
Managed Postgres แทน self-hosted เป็นตัวเลือกที่ตัดสินใจง่ายสำหรับ capstone ไม่มีใครได้เรียนรู้อะไรจากการรัน postgres StatefulSet ของตัวเองพร้อม backup ที่เขียนมือ การตัดสินใจที่น่าสนใจคือ วางไว้ที่ไหน และ password เดินทางยังไง
database อยู่ใน private subnet ที่ไม่มี public address ทางเดียวที่จะเข้าถึงได้คือจากภายใน network — pod ของ cluster นั่นคือ security boundary ที่คุณได้มาฟรีจาก network module และเป็นเหตุผลที่ module รับ network_id กับ subnet_ids แทนที่จะ provision subnet ของตัวเอง
password ถูก generate ขึ้นมา ไม่เคยมีคนพิมพ์เอง และถูกส่งออกมาเป็น sensitive output Terraform mark ไว้เพื่อให้ปิดบังจาก plan/apply log และ console ผู้ใช้เพียงตัวเดียวคือ ShopMicro unit ที่อ่านตรงจาก state เพื่อสร้าง Kubernetes Secret ไม่มี password ตัวไหนโผล่ในไฟล์ variables หรือใน commit เลย
ข้อดีข้อเสีย
หัวข้อที่มีชื่อว่า “ข้อดีข้อเสีย”Managed Postgres vs a self-hosted StatefulSet
- Pros: cloud จัดการ backup, minor-version patching, การขยาย storage และ failover ให้ ใช้ resource block เดียวแทนที่จะต้องมี operator, volume และ backup CronJob และนี่คือสิ่งที่คุณจะรันจริงบน production
- Cons: แพงกว่า pod คุณต้องเดินตาม version cadence ของ cloud และ managed offering ของแต่ละ cloud ก็มี quirk ของตัวเอง (parameter group, flag, SKU) — นั่นคือความแตกต่างต่อ cloud ที่บทถัดไปต้องรับมือพอดี
Generated password as sensitive output vs an externally-managed secret
- Pros:
random_password+ sensitive output นั้นครบในตัวเอง — ไม่มีคนเห็นเลย และ ShopMicro unit อ่านตรงจาก dependency ไม่มีขั้นตอนที่ต้องทำมือ - Cons: plaintext อยู่ใน Terraform state ดังนั้น state backend ต้อง ถูก encrypt และ lock (ซึ่งของเราเป็นแบบนั้น — S3 + encryption ในการตั้งค่า remote state) secrets manager แยกต่างหากคือขั้นถัดไป ซึ่งกล่าวถึงตอนท้ายคอร์ส
ติดตั้ง
หัวข้อที่มีชื่อว่า “ติดตั้ง”Implementation บน AWS คือ subnet group ครอบ private subnet, security group ที่ยอมรับเฉพาะ VPC, password ที่ generate ขึ้นมา และตัว RDS instance เอง
1. modules/aws/data/variables.tf
หัวข้อที่มีชื่อว่า “1. modules/aws/data/variables.tf”variable "name" { type = string }variable "network_id" { type = string } # the VPC idvariable "subnet_ids" { type = list(string) }
variable "db_name" { type = string default = "shopmicro"}2. modules/aws/data/main.tf
หัวข้อที่มีชื่อว่า “2. modules/aws/data/main.tf”data "aws_vpc" "this" { id = var.network_id}
resource "aws_db_subnet_group" "this" { name = "${var.name}-db" subnet_ids = var.subnet_ids}
resource "aws_security_group" "db" { name = "${var.name}-db" vpc_id = var.network_id
ingress { description = "PostgreSQL from within the VPC" from_port = 5432 to_port = 5432 protocol = "tcp" cidr_blocks = [data.aws_vpc.this.cidr_block] }}
resource "random_password" "db" { length = 24 special = false # keep it URL-safe for the connection string}
resource "aws_db_instance" "this" { identifier = var.name engine = "postgres" engine_version = "16" # prefix; RDS resolves the latest 16.x instance_class = "db.t4g.micro"
allocated_storage = 20 max_allocated_storage = 100 # storage autoscaling ceiling
db_name = var.db_name username = "shopmicro" password = random_password.db.result
db_subnet_group_name = aws_db_subnet_group.this.name vpc_security_group_ids = [aws_security_group.db.id] publicly_accessible = false
storage_encrypted = true skip_final_snapshot = true # a teaching platform, not prod data}publicly_accessible = false บวกกับ security group ที่จำกัดไว้ที่ VPC CIDR คือ boundary แบบ private-only engine_version = "16" เป็น prefix — RDS เลือก 16.x ล่าสุดที่มี ซึ่งไม่มีปัญหาเมื่อ auto_minor_version_upgrade ยังเปิดอยู่
3. modules/aws/data/outputs.tf
หัวข้อที่มีชื่อว่า “3. modules/aws/data/outputs.tf”interface output ทั้งห้าตัว โดย mark password เป็น sensitive
output "db_host" { value = aws_db_instance.this.address }output "db_port" { value = aws_db_instance.this.port }output "db_name" { value = aws_db_instance.this.db_name }output "db_user" { value = aws_db_instance.this.username }
output "db_password" { value = random_password.db.result sensitive = true}4. live/aws/data/terragrunt.hcl
หัวข้อที่มีชื่อว่า “4. live/aws/data/terragrunt.hcl”unit นี้ depend บน network แล้วส่ง private subnet เข้าไป — รูปร่างเหมือนกับที่ unit ของ gcp/azure จะทำในบทถัดไป
include "root" { path = find_in_parent_folders("root.hcl") }terraform { source = "../../../modules/aws/data" }
dependency "network" { config_path = "../network" }
inputs = { name = "clouddeploy" network_id = dependency.network.outputs.network_id subnet_ids = dependency.network.outputs.private_subnet_ids db_name = "shopmicro"}ตรวจสอบผล
หัวข้อที่มีชื่อว่า “ตรวจสอบผล”plan ก่อน — sensitive password ควรถูกปิดบังไว้แล้ว — จากนั้น apply แล้วอ่าน output
cd live/aws/dataterragrunt planPlan: 4 to add, 0 to change, 0 to destroy. # aws_db_instance.this will be created + password = (sensitive value) + address = (known after apply) ...terragrunt apply
# non-sensitive outputs print normallyterragrunt output db_host# => "clouddeploy.abc123xyz.us-east-1.rds.amazonaws.com"terragrunt output db_port# => 5432
# the password is redacted unless you explicitly ask for the raw valueterragrunt output db_password# => <sensitive>ยืนยันว่าเป็น private จริง — การเชื่อมต่อจากนอก VPC ต้อง fail และจาก pod ข้างในต้องสำเร็จ
# from your laptop (outside the VPC): should hang / refusepsql "postgres://shopmicro@$(terragrunt output -raw db_host):5432/shopmicro" -c '\l'# => timeout — there is no public route, by design
# from a pod inside the cluster: reachablekubectl run pg --rm -it --restart=Never --image=postgres:16 -- \ psql "postgres://shopmicro:$(terragrunt output -raw db_password)@$(terragrunt output -raw db_host):5432/shopmicro" -c '\conninfo'# => You are connected to database "shopmicro" ...คู่ inside-succeeds / outside-fails นั้นคือเรื่องราวด้าน security ทั้งหมดของ module นี้ จากนั้นรัน terragrunt run --all plan จาก live/aws/ เพื่อยืนยันว่า data เข้าที่หลัง network โดยไม่มี drift
ตรวจสอบความเข้าใจ:
- ทำไม module ถึงรับ
network_idและsubnet_idsเป็น input แทนที่จะสร้าง subnet ของตัวเอง? - การ mark
db_passwordเป็นsensitiveเปลี่ยนอะไรจริง ๆ — และ ไม่ ปกป้องอะไร (plaintext ยังอยู่ที่ไหน)? - setting สองตัวไหนที่ทำงานร่วมกันแล้วทำให้ database เข้าถึงได้จากภายใน network เท่านั้น?
skip_final_snapshot = trueใช้ได้ในบทนี้แต่อันตรายบน production เพราะอะไร และคุณจะเปลี่ยนอะไรสำหรับ data จริง?
เรานิยาม interface ของ data — name, network_id, subnet_ids, db_name เข้า; db_host, db_port, db_name, db_user, db_password (sensitive) ออก — แล้วสร้าง implementation บน AWS RDS: PostgreSQL instance ที่ private และ encrypt อยู่ใน private subnet ของ network โดยมี password ที่ generate ขึ้นมาส่งออกเป็น output ที่ถูกปิดบังเท่านั้น และ dependency "network" ที่วางไว้ให้ถูกที่
หนึ่ง cloud เสร็จ interface พิสูจน์แล้ว ถัดไป Managed Postgres → implement input และ output ทั้งห้าตัวเดียวกันบน GCP Cloud SQL และ Azure Database for PostgreSQL แล้วประกอบ output เหล่านั้นเป็น connection secret ที่ ShopMicro จะใช้