Multi-Tenant Operations — Logging & Cost Attribution
Tại sao operations là phần bị bỏ quên của multi-tenancy
Phần lớn tài liệu multi-tenancy dừng ở isolation (mạng, RBAC, security). Nhưng một cluster multi-tenant production còn hai bài toán vận hành sống còn mà nếu bỏ qua sẽ gây xung đột chính trị và thất thoát tài chính:
Logging isolation: Mỗi tenant cần thấy log của mình — và chỉ của mình. Một developer của team A không được đọc log team B (có thể chứa PII, secret rò rỉ, thông tin kinh doanh). Đồng thời, một số log cần route tới đích riêng (compliance bucket cho PCI, project riêng cho từng tenant).
Cost attribution: Khi 40 team dùng chung một cluster, ai trả tiền cho cái gì? Nếu không attribution được chi phí về tenant, bạn không thể showback/chargeback, không thể phát hiện team nào lãng phí, và FinOps không thể hoạt động. "Một hóa đơn cluster lớn không ai chịu trách nhiệm" là kết cục tệ nhất của soft multi-tenancy.
Hai bài toán này quyết định multi-tenancy có bền vững về tổ chức không, không chỉ về kỹ thuật.
Phần 1: Per-namespace logging
Internal model: log flow trên GKE
GKE tích hợp sẵn với Cloud Logging. Mọi container stdout/stderr được agent thu thập và gửi tới Cloud Logging với metadata phong phú, bao gồm resource.labels.namespace_name, pod_name, container_name, cluster_name. Chính metadata namespace này là chìa khóa cho isolation và routing per-tenant.
Luồng:
Container stdout/stderr
→ GKE logging agent (DaemonSet)
→ Cloud Logging (_Default bucket)
→ [Log Router / Sinks] → route theo filter
├── tenant-a sink → project/bucket của tenant A
├── pci sink → compliance bucket (retention dài)
└── _Default → log chungPer-namespace log routing bằng sinks
Cloud Logging Log Router cho phép tạo sink với filter định tuyến log tới đích khác nhau. Định tuyến theo namespace:
# Route log của namespace tenant-a sang project riêng của họ
gcloud logging sinks create tenant-a-sink \
logging.googleapis.com/projects/tenant-a-project/locations/global/buckets/tenant-a-logs \
--log-filter='resource.type="k8s_container"
AND resource.labels.namespace_name="tenant-a"' \
--project=platform-projectCác đích sink khả dụng: log bucket riêng (với retention/access control riêng), BigQuery dataset (phân tích), Pub/Sub (streaming sang hệ thống tenant), Cloud Storage (lưu trữ lâu dài/compliance), hoặc project khác hoàn toàn.
Pattern compliance: namespace pci-workloads có sink riêng route tới một bucket với retention 1 năm và CMEK encryption, tách khỏi log thường.
Isolation: tenant chỉ đọc log của mình
Routing thôi chưa đủ — phải chặn tenant đọc log nhau. Hai cách tiếp cận:
- Log Views với IAM: Tạo Log View giới hạn theo filter namespace, cấp tenant quyền
roles/logging.viewAccessorchỉ trên view của họ. Tenant query Cloud Logging chỉ thấy log namespace mình.
gcloud logging views create tenant-a-view \
--bucket=_Default --location=global \
--log-filter='resource.labels.namespace_name="tenant-a"'
# rồi cấp IAM logging.viewAccessor trên view này cho group tenant A- Sink tới project riêng của tenant: Log được route hẳn sang project mà chỉ tenant đó có quyền. Isolation mạnh nhất — log nằm trong ranh giới IAM của tenant.
Structured logging cho attribution chính xác
Để filter/route hiệu quả, khuyến khích tenant dùng structured logging (JSON) với field nhất quán (severity, tenant_id, trace). GKE parse JSON thành jsonPayload, cho phép filter tinh vi hơn chỉ theo namespace. Kết hợp với log-based metrics per-namespace để alert riêng từng tenant.
Phần 2: Cost attribution per-namespace
Internal model: GKE Cost Allocation
Theo tài liệu GKE Cost Allocation, khi bật, GKE thêm metadata Kubernetes vào billing export, cho phép phân rã chi phí cluster theo namespace/workload. Đây là khác biệt then chốt với "cluster usage metering" cũ — Cost Allocation tích hợp thẳng vào Cloud Billing.
Bật khi tạo/cập nhật cluster:
gcloud container clusters update my-cluster \
--enable-cost-allocation \
--region=us-central1Khi bật, billing export nhận các label:
k8s-namespace: namespace của Podgoog-k8s-cluster-name: tên clusterk8s-workload-type/k8s-workload-name: loại/tên workload- Pod labels (prefix
k8s-label/)
Hai namespace đặc biệt thu giữ overhead:
kube:system-overhead: tài nguyên của framework Kuberneteskube:unallocated: tài nguyên đã provision nhưng không được Pod nào request
⚠️ Giới hạn quan trọng: requested vs consumed
Theo tài liệu, "dữ liệu GKE cost allocation dựa trên resource requests, không phải resource consumed". Đây là điểm cực kỳ quan trọng cho cost modeling:
- Chi phí được tính theo những gì Pod request trong spec, KHÔNG theo CPU/memory thực dùng.
- Một tenant request 8 CPU nhưng chỉ dùng 1 CPU vẫn bị tính theo 8 CPU.
- Điều này đúng về mặt kinh tế — request chiếm capacity (scheduler reserve nó), nên tenant nên trả cho phần họ giữ chỗ. Nó cũng tạo động lực đúng: tenant tối ưu request để không lãng phí.
Hệ quả: namespace kube:unallocated cho thấy phần capacity bạn trả cho Google nhưng không tenant nào request — đây là "thuế" platform team gánh, hoặc dấu hiệu cần giảm node/cải thiện bin-packing.
Billing export tới BigQuery
Cost Allocation hoạt động qua Cloud Billing export tới BigQuery. Sau khi bật:
- GKE thêm line item chi tiết vào billing export.
- Dữ liệu có thể mất "tới ba ngày để xuất hiện" và không backfill lịch sử — bật sớm.
- Query phân rã chi phí theo namespace:
SELECT
labels.value AS namespace,
SUM(cost) AS total_cost
FROM `billing_project.billing_export.gcp_billing_export_v1_XXXX`,
UNNEST(labels) AS labels
WHERE labels.key = 'k8s-namespace'
AND usage_start_time >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY)
GROUP BY namespace
ORDER BY total_cost DESCShowback vs chargeback
- Showback: hiển thị cho mỗi team chi phí của họ (qua dashboard Looker Studio trên BigQuery), tạo nhận thức và áp lực tối ưu — nhưng không thực sự tính tiền.
- Chargeback: thực sự phân bổ chi phí vào ngân sách team, tính tiền nội bộ.
Cả hai dựa trên cùng dữ liệu Cost Allocation. Showback thường là bước đầu (ít ma sát chính trị), chargeback là đích đến cho FinOps trưởng thành.
Production architecture patterns
Pattern 1: Namespace = đơn vị attribution
Mỗi tenant = một (hoặc nhóm) namespace, là đơn vị cơ bản cho cả logging routing và cost attribution. Đây là lý do tổ chức namespace sạch (file 02) quan trọng cho operations, không chỉ isolation. Một namespace = một dòng trong báo cáo chi phí, một sink trong log router.
Pattern 2: Label-based attribution chéo namespace
Khi attribution cần chiều khác namespace (ví dụ theo cost-center, environment, product), bắt buộc Pod label nhất quán qua admission policy (Policy Controller), rồi attribution theo k8s-label/cost-center trong billing export. Một admission policy chặn Pod thiếu label bắt buộc đảm bảo 100% workload attribution được.
Pattern 3: FinOps dashboard + alerting
BigQuery billing export → Looker Studio dashboard per-team (showback) + budget alert per-namespace (Cloud Billing budgets không native theo namespace, nên dùng BigQuery scheduled query + alert khi chi phí namespace vượt ngưỡng). Phát hiện sớm team nào tăng chi phí bất thường.
Real-world scenario
Internal platform 40 team, FinOps chargeback: Platform team bật GKE Cost Allocation, export billing sang BigQuery. Một admission policy bắt buộc mọi Pod có label team và cost-center. Hàng tháng, scheduled query phân rã chi phí theo k8s-namespace + k8s-label/cost-center, gửi báo cáo chargeback cho từng team. Namespace kube:unallocated cho thấy 12% capacity không được request — platform team dùng số này để giảm node và cải thiện bin-packing, tiết kiệm chi phí chung. Về logging, mỗi team có Log View riêng (IAM giới hạn theo namespace), team xử lý dữ liệu thanh toán có sink riêng route log sang compliance bucket retention 1 năm. Kết quả: mỗi team thấy và trả cho phần của mình, không ai đọc được log của team khác, FinOps có dữ liệu để tối ưu.
Common mistakes / anti-patterns
Anti-pattern 1: Không attribution được chi phí (one big bill)
Vì sao xảy ra: Không bật Cost Allocation; cluster chỉ là một dòng chi phí khổng lồ.
Hệ quả ở scale: Không ai chịu trách nhiệm chi phí; team không có động lực tối ưu request (lãng phí tích lũy); platform team gánh toàn bộ và không lý giải được hóa đơn tăng. Soft multi-tenancy mất tính bền vững tài chính.
Cách phòng tránh: Bật GKE Cost Allocation ngay từ đầu (nó không backfill). Xây showback tối thiểu để tạo nhận thức.
Anti-pattern 2: Nhầm cost theo consumed thay vì requested
Vì sao xảy ra: Kỳ vọng chi phí phản ánh CPU thực dùng.
Hệ quả ở scale: Báo cáo chargeback gây tranh cãi ("tôi chỉ dùng 1 CPU sao tính 8?"). Hiểu sai dẫn đến quyết định tối ưu sai (tối ưu usage thay vì request).
Cách phòng tránh: Truyền đạt rõ: chi phí theo request vì request chiếm capacity. Động lực đúng là tenant giảm request thừa (right-sizing), không phải giảm usage. Dùng VPA recommendation để giúp tenant right-size request.
Anti-pattern 3: Tenant đọc được log nhau
Vì sao xảy ra: Cấp roles/logging.viewer ở cấp project cho mọi tenant; ai cũng query được mọi log.
Hệ quả ở scale: Rò rỉ PII/secret/thông tin kinh doanh giữa tenant qua log. Vi phạm compliance nếu log chứa dữ liệu nhạy cảm.
Cách phòng tránh: Dùng Log Views giới hạn theo namespace + IAM logging.viewAccessor trên view, hoặc sink tới project riêng của tenant. Không cấp logging.viewer project-wide cho tenant.
Anti-pattern 4: Bật Cost Allocation muộn rồi cần dữ liệu lịch sử
Vì sao xảy ra: Bật khi đã cần báo cáo, kỳ vọng có dữ liệu quá khứ.
Hệ quả ở scale: Cost Allocation không backfill — bạn chỉ có dữ liệu từ thời điểm bật, mất tháng đầu cho báo cáo.
Cách phòng tránh: Bật Cost Allocation và billing export BigQuery ngay khi tạo cluster, kể cả khi chưa cần báo cáo. Dữ liệu tích lũy sẵn cho khi cần.
Tổng kết chương
Multi-tenancy thành công không chỉ là isolation kỹ thuật mà là sự kết hợp đúng giữa: mô hình tenancy khớp với mức tin cậy (file 01), tổ chức namespace sạch (file 02), RBAC least-privilege (file 03), network default-deny (file 04), fair resource allocation (file 05), Pod security baseline (file 06), kernel isolation khi cần (file 07), hardware isolation cho compliance (file 08), và operations bền vững — logging isolation + cost attribution (file 09). Không lớp nào đủ một mình; sức mạnh đến từ defense-in-depth khớp với threat model thực tế của bạn.