Three-cloud networks
สิ่งที่จะสร้าง
หัวข้อที่มีชื่อว่า “สิ่งที่จะสร้าง”บทที่แล้วเรานิยาม network interface — รับ name, cidr เข้า; คืน network_id, private_subnet_ids, public_subnet_ids ออก — แล้ว implement ครั้งเดียวเป็น AWS VPC ตอนนี้เรา implement อีกสองครั้ง: GCP VPC (modules/gcp/network) และ Azure VNet (modules/azure/network) หลัง ห้าชื่อเดียวกัน แล้วตั้ง unit live/gcp/network และ live/azure/network ขึ้นมา
บททดสอบทั้งหมดของ interface คือสิ่งนี้: เมื่อเราทำเสร็จ terragrunt.hcl unit ทั้งสามต่างกันแค่ที่ path ของ source เท่านั้น ความจริงที่เฉพาะต่อ cloud ข้างใน — secondary IP range ของ GCP สำหรับ VPC-native GKE, VNet ของ Azure ที่ scope อยู่กับ resource group พร้อม subnet แบบ inline — ยังคงอยู่หลัง interface ทั้งหมด
multi-cloud จัดการได้ก็ต่อเมื่อ ความต่าง ระหว่าง cloud ถูกกักไว้ network ต่างกันจริง:
- AWS — VPC พร้อม
aws_subnetresource แบบชัดเจน subnet id เป็น handle ระดับ first-class - GCP — global VPC พร้อม regional subnetwork; VPC-native cluster ต้องมี secondary IP range (alias) สำหรับ pod และ service ที่ประกาศไว้บน subnet
- Azure — VNet ที่ scope อยู่กับ resource group พร้อม subnet และ
address_spaceแทน CIDR
ถ้าความต่างพวกนั้นไปถึง cluster module เราจะมี cluster module สามตัว แต่ความต่างไม่ไปถึงตรงนั้น เพราะ network module แต่ละตัวดูดซับรูปทรงของ cloud ตัวเองไว้ และปล่อย output สามตัวเดียวกันออกมา interface คือ firewall: ความประหลาดของ cloud หยุดที่ขอบเขต module นั่นคือคำกล่าวจาก architecture ที่ทำให้เป็นรูปธรรม
ข้อดีข้อเสีย
หัวข้อที่มีชื่อว่า “ข้อดีข้อเสีย”Interface ร่วมชุดเดียวข้ามสาม cloud vs. module ที่ทำเฉพาะต่อ cloud
- Pros: consumer ทุกตัวต่อสาย dependency เหมือนกัน; คนที่เข้าใจ
live/aws/networkเข้าใจ unit ของ GCP และ Azure ทันที;clusterlayer เขียนครั้งเดียว - Cons: interface คือตัวส่วนร่วมน้อยที่สุด secondary range ของ GCP และ resource group ของ Azure ไม่ map เข้ากับ output ตัวไหน — เป็น implementation detail ที่ interface ตั้งใจแสดงออกไม่ได้ เมื่อ feature ของ cloud ต้องโผล่ขึ้นมาจริง คุณ extend interface บนทั้งสามตัว ไม่ใช่ตัวเดียว
Secondary range ของ GCP ที่ประกาศใน module vs. ปล่อยให้ GKE auto-create subnet
- Pros: การประกาศ
secondary_ip_rangeblock เองหมายความว่า CIDR ของ pod และ service ชัดเจน review ได้ และเสถียรข้ามการ apply — ไม่มี range เซอร์ไพรส์โผล่ใน plan - Cons: ต่อสาย resource มากกว่าทางแบบ auto-create และ cluster module ต้องรู้ ชื่อ range เพื่ออ้างอิง เราคงชื่อพวกนั้นให้เป็น convention (
pods,services) เพื่อให้ coupling คาดเดาได้
ติดตั้ง
หัวข้อที่มีชื่อว่า “ติดตั้ง”1. modules/gcp/network/ — the GCP VPC
หัวข้อที่มีชื่อว่า “1. modules/gcp/network/ — the GCP VPC”variables.tf เดียวกับ AWS (name, cidr) implementation คือ custom-mode VPC บวก subnetwork หนึ่งตัวที่พก secondary range สองตัว — alias ที่ GKE consume สำหรับ pod และ service
resource "google_compute_network" "this" { name = var.name auto_create_subnetworks = false}
resource "google_compute_subnetwork" "private" { name = "${var.name}-private" ip_cidr_range = cidrsubnet(var.cidr, 4, 0) region = "us-central1" network = google_compute_network.this.id
# VPC-native GKE reads these by name for pod + service IPs. secondary_ip_range { range_name = "pods" ip_cidr_range = cidrsubnet(var.cidr, 4, 1) } secondary_ip_range { range_name = "services" ip_cidr_range = cidrsubnet(var.cidr, 8, 32) }
private_ip_google_access = true}
# Cloud Router + NAT so private nodes can reach the internet for images.resource "google_compute_router" "this" { name = "${var.name}-router" region = "us-central1" network = google_compute_network.this.id}
resource "google_compute_router_nat" "this" { name = "${var.name}-nat" router = google_compute_router.this.name region = "us-central1" nat_ip_allocate_option = "AUTO_ONLY" source_subnetwork_ip_ranges_to_nat = "ALL_SUBNETWORKS_ALL_IP_RANGES"}GCP ไม่มี object “public subnet” แยกต่างหาก — node ได้ public IP หรือออกไปข้างนอกผ่าน Cloud NAT เราจึงทำตาม interface อย่างซื่อสัตย์: private_subnet_ids คือ subnetwork จริง และ public_subnet_ids คืน id ของ subnetwork เดียวกันนั้น (public exposure บน GCP เป็นเรื่องของ load balancer และ config ของ node ไม่ใช่ subnet ที่แยกออกมา) outputs.tf:
output "network_id" { value = google_compute_network.this.id}
output "private_subnet_ids" { value = [google_compute_subnetwork.private.id]}
output "public_subnet_ids" { # GCP exposes services via load balancers, not a separate subnet. value = [google_compute_subnetwork.private.id]}ความไม่สมมาตรนั้น — GCP พับ public/private ให้อยู่ใน subnetwork เดียว — คือชนิดของความต่างระหว่าง cloud ที่ interface ถูกออกแบบมาให้กลืนพอดี consumer ยังได้ list สองตัวภายใต้ชื่อเดียวกัน
2. modules/azure/network/ — the Azure VNet
หัวข้อที่มีชื่อว่า “2. modules/azure/network/ — the Azure VNet”Azure scope ทุกอย่างเข้ากับ resource group และ azurerm_virtual_network รับ address_space พร้อม subnet block แบบ inline interface name/cidr เดียวกัน
resource "azurerm_resource_group" "this" { name = "${var.name}-rg" location = "eastus"}
resource "azurerm_virtual_network" "this" { name = var.name resource_group_name = azurerm_resource_group.this.name location = azurerm_resource_group.this.location address_space = [var.cidr]}
resource "azurerm_subnet" "private" { name = "${var.name}-private" resource_group_name = azurerm_resource_group.this.name virtual_network_name = azurerm_virtual_network.this.name address_prefixes = [cidrsubnet(var.cidr, 4, 0)]}
resource "azurerm_subnet" "public" { name = "${var.name}-public" resource_group_name = azurerm_resource_group.this.name virtual_network_name = azurerm_virtual_network.this.name address_prefixes = [cidrsubnet(var.cidr, 4, 1)]}VNet id คือ network_id ของ Azure และ subnet แต่ละตัว expose id AKS ต้องการ resource group ในภายหลังด้วย แต่ interface network ยังคงอยู่ที่สาม output — cluster module derive สิ่งที่ต้องการจาก network_id outputs.tf:
output "network_id" { value = azurerm_virtual_network.this.id}
output "private_subnet_ids" { value = [azurerm_subnet.private.id]}
output "public_subnet_ids" { value = [azurerm_subnet.public.id]}3. The live/ units — identical but for source
หัวข้อที่มีชื่อว่า “3. The live/ units — identical but for source”นี่คือผลตอบแทน วางเทียบกัน live/gcp/network/terragrunt.hcl:
include "root" { path = find_in_parent_folders("root.hcl")}
terraform { source = "../../../modules/gcp/network"}
inputs = { name = "clouddeploy" cidr = "10.10.0.0/16"}live/azure/network/terragrunt.hcl:
include "root" { path = find_in_parent_folders("root.hcl")}
terraform { source = "../../../modules/azure/network"}
inputs = { name = "clouddeploy" cidr = "10.20.0.0/16"}เทียบพวกนี้กับ live/aws/network จากบทที่แล้ว: structure เดียวกัน input สองตัวเดียวกัน source ต่างกัน และ cidr ที่ไม่ทับกันต่อ cloud root.hcl แต่ละตัวจัดหา backend ของ cloud ตัวเอง (gcs, azurerm) และ provider generate block ดังนั้น unit ยังบางแบบนี้ นั่น คือชัยชนะของ multi-cloud — ไม่ใช่ว่า cloud เหมือนกัน แต่คือ interface ทำให้ดูเหมือนกันสำหรับทุกอย่างที่อยู่ข้างบน
ตรวจสอบผล
หัวข้อที่มีชื่อว่า “ตรวจสอบผล”Plan network ของแต่ละ cloud จาก tree live/ ของแต่ละตัว:
cd live/gcp/network && terragrunt init && terragrunt plancd ../../azure/network && terragrunt init && terragrunt planแต่ละตัวควรรายงาน plan แบบ create-only เล็ก ๆ — VPC/VNet, subnet ของตัวเอง และ NAT/router (GCP) — โดยไม่มี error ในการ resolve module source
ทีนี้บททดสอบจริง: ยืนยันว่าทั้งสาม cloud ปล่อย ชื่อ output ชุดเดียวกัน จาก repo root:
terragrunt output -json --working-dir live/aws/network | jq 'keys'terragrunt output -json --working-dir live/gcp/network | jq 'keys'terragrunt output -json --working-dir live/azure/network | jq 'keys'ทุกตัวพิมพ์ชุดที่เหมือนกันเป๊ะ — interface ยึดได้ข้ามสาม cloud:
[ "network_id", "private_subnet_ids", "public_subnet_ids"]สุดท้าย plan networking layer ทั้งหมดข้าม cloud ในทีเดียวเพื่อยืนยันว่า tree ต่อสายกัน:
terragrunt run --all plan --working-dir live/gcpตรวจสอบความเข้าใจ:
live/*/networkunit ทั้งสามต่างกันในสองจุดพอดี จุดไหนสองจุด และทำไมการคงทุกอย่างที่เหลือให้เหมือนกันถึงสำคัญกว่า code ที่ประหยัดได้?public_subnet_idsของ GCP คืน id เดียวกับprivate_subnet_idsนั่นเป็นการโกงหรือเป็นการอ่าน interface ที่ถูกต้อง? ปกป้องคำตอบของคุณ- Azure ต้องมี resource group ที่ AWS และ GCP ไม่ต้อง ทำไมจึงไม่รั่วเข้า
networkinterface — และไป consume ที่ไหนแทน? - เพื่อนร่วมทีมอยากให้ CIDR ของ pod/service config ได้ต่อ cloud อธิบายว่าทำไมคุณถึงเพิ่ม input นั้นเข้าทั้งสาม module พร้อมกัน แทนที่จะเฉพาะของ GCP
เรา implement network interface อีกสองครั้ง — GCP VPC พร้อม secondary range แบบ VPC-native, Azure VNet ที่ scope อยู่กับ resource group — หลัง output สามตัวเดียวกับที่ AWS module ปล่อย live/gcp/network และ live/azure/network unit เป็นรูปทรงเดียวกับ live/aws/network แบบบรรทัดต่อบรรทัด ต่างกันแค่ source และ cidr เพราะ Terragrunt และ interface ที่คั่นอยู่ตรงกลางดูดซับความต่างของ cloud ทุกอย่าง
network เสร็จแล้วและ output เสถียรข้ามทั้งสาม cloud นั่นคือสิ่งที่ layer ถัดไปต้องการเป๊ะ: Managed Kubernetes → consume network_id และ private_subnet_ids เพื่อวาง cluster — และก็เหมือน network คือ interface หนึ่งกับ implementation สาม