0

[Microservices] Config & Secrets trên Kubernetes – Từ ConfigMap đến External Secrets, Vault và GitOps

1. Vấn đề thật sự không phải là file .env

Nhiều bài viết mở đầu bằng cảnh: 20 microservice, 20 file .env nằm trong repo, đổi mật khẩu DB phải sửa, commit, push 20 lần. Thực tế, team nào làm nghiêm túc cũng không commit secret vào git. Vấn đề thật nằm ở chỗ khác:

  • Phân tán và lệch cấu hình (drift): staging đúng, production thiếu một biến, không ai biết vì cấu hình nằm rải rác ở CI/CD variables, server, Slack và đầu người.
  • Xoay vòng secret (rotation) đau đớn: đổi mật khẩu DB đồng nghĩa với việc phải đồng bộ nhiều nơi và deploy lại nhiều service.
  • Không có audit: ai đổi giá trị nào, lúc nào, vì sao? Nếu trả lời là "không biết" thì đó là rủi ro vận hành lẫn tuân thủ.
  • Không có rollback: đổi cấu hình sai thì quay lại giá trị cũ bằng cách nào?
  • Secret nằm sai chỗ: trong image, trong log, trong lịch sử git, trong biến môi trường của CI.

Vì vậy mục tiêu của "cấu hình tập trung" không phải là một cái tên công cụ, mà là 4 thuộc tính: một nguồn sự thật (single source of truth), có phân quyền, có lịch sử thay đổi, và có thể xoay vòng secret mà không đau.

Trên Kubernetes, bạn đã có sẵn một nửa bộ công cụ. Phần còn lại là chọn thêm đúng mảnh ghép.


2. Bước 0: Phân loại cấu hình trước khi chọn công cụ

Đừng bắt đầu bằng "dùng Vault hay Consul?". Hãy bắt đầu bằng việc phân loại từng giá trị cấu hình theo 4 câu hỏi:

Câu hỏi Ví dụ
Nhạy cảm đến mức nào? Mật khẩu DB, private key (secret) vs timeout, URL nội bộ (config thường)
Đổi thường xuyên không? Hầu như không bao giờ vs mỗi ngày
Có cần áp dụng nóng không? Feature flag, tỉ lệ giảm giá vs connection string
Ai được phép sửa? Dev, SRE, hay cả bộ phận kinh doanh

Từ đó ra 4 nhóm điển hình:

  1. Cấu hình tĩnh, không nhạy cảm (port, URL service nội bộ, log level mặc định): để trong ConfigMap, quản lý bằng Git.
  2. Secrets (mật khẩu, token, key): không để trong Git ở dạng rõ. Dùng secret manager.
  3. Feature flag và tham số nghiệp vụ đổi nóng (bật tắt tính năng, tỉ lệ khuyến mãi): nên dùng công cụ feature flag riêng, không nhồi vào ConfigMap hay secret store.
  4. Cấu hình theo môi trường (dev, staging, prod): quản lý bằng overlay (Helm values, Kustomize), không copy-paste file.

Nguyên tắc thứ nhất của 12-factor app vẫn đúng: cấu hình nằm ngoài code, ứng dụng nhận qua môi trường. Kubernetes chỉ thay cái cách "nhận".


3. Lớp nền tảng: ConfigMap và Secret của Kubernetes

3.1 ConfigMap

ConfigMap chứa cấu hình không nhạy cảm dạng key-value hoặc cả file.

apiVersion: v1
kind: ConfigMap
metadata:
  name: ticket-service-config
data:
  PAYMENT_TIMEOUT_MS: "3000"
  application.yaml: |
    app:
      retry:
        max-attempts: 3

Có hai cách đưa vào Pod, và chúng cư xử khác nhau khi cấu hình thay đổi:

Cách Cập nhật khi ConfigMap đổi?
Biến môi trường (env, envFrom) Không. Phải restart Pod mới thấy giá trị mới.
Mount thành file (volume) Có, nhưng chậm (kubelet đồng bộ định kỳ, thường vài chục giây đến khoảng một phút).
Mount bằng subPath Không nhận cập nhật.

Đây là bẫy kinh điển: sửa ConfigMap xong, tưởng đã áp dụng, nhưng Pod vẫn chạy giá trị cũ vì dùng envFrom.

3.2 Secret: không phải là mã hóa

Kubernetes Secret về hình thức giống ConfigMap, nhưng có vài điểm riêng (không in ra khi describe, có thể bật mã hóa lưu trữ, RBAC tách riêng). Tuy nhiên:

Giá trị trong Secret chỉ được mã hóa base64, mà base64 không phải là mã hóa. Ai có quyền get secret là đọc được ngay.

Cần làm tối thiểu:

  • Bật encryption at rest cho etcd. Trên EKS, dùng envelope encryption với khóa KMS.
  • RBAC chặt: chỉ ServiceAccount cần thiết mới được get Secret; hạn chế list và watch diện rộng.
  • Tách namespace theo team hoặc môi trường để giảm bán kính ảnh hưởng.
  • Ưu tiên mount secret thành file thay vì biến môi trường: biến môi trường dễ bị lộ qua crash dump, /proc, log debug hoặc công cụ chẩn đoán.

3.3 Ép rollout khi cấu hình đổi

Vì env không tự cập nhật, cần cơ chế để Pod khởi động lại khi cấu hình đổi. Hai cách phổ biến:

Cách 1: Helm checksum annotation. Mỗi khi nội dung ConfigMap đổi, hash đổi, Deployment template đổi và K8s rollout:

# deployment.yaml (Helm)
spec:
  template:
    metadata:
      annotations:
        checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}

Cách 2: Reloader (stakater/Reloader). Controller theo dõi ConfigMap và Secret, tự rollout workload có annotation:

metadata:
  annotations:
    reloader.stakater.com/auto: "true"

Rollout kiểu này an toàn hơn "hot reload" vì tận dụng rolling update, readiness probe và có thể rollback bằng kubectl rollout undo.


4. Vì sao Secret của K8s chưa đủ?

Bài toán lớn nhất khi dùng Secret trong K8s là GitOps. Nếu bạn quản lý hạ tầng bằng Git (ArgoCD, Flux), mọi manifest nằm trong repo, vậy Secret nằm ở đâu? Commit file Secret base64 lên Git là tự sát.

Có 4 hướng giải quyết chính:

Giải pháp Ý tưởng Phù hợp khi
Sealed Secrets Mã hóa Secret bằng public key của cluster, commit bản mã hóa lên Git; controller trong cluster giải mã Team nhỏ, một cluster, muốn đơn giản, không phụ thuộc dịch vụ ngoài
SOPS (kèm age hoặc KMS) Mã hóa giá trị trong file YAML, giải mã khi deploy (ArgoCD/Flux hỗ trợ qua plugin) Muốn secret vẫn nằm trong Git, quen với workflow file
External Secrets Operator (ESO) Secret nằm ở secret manager bên ngoài (AWS Secrets Manager, Vault, GCP...), ESO đồng bộ vào K8s Secret Đã dùng cloud, cần rotation và audit từ nguồn chuẩn
Secrets Store CSI Driver / Vault Agent / Vault Secrets Operator Mount trực tiếp secret từ nguồn ngoài vào Pod, có thể không tạo K8s Secret Yêu cầu bảo mật cao, không muốn secret nằm trong etcd

Với đa số team chạy trên cloud, bộ đôi ESO + secret manager của cloud (hoặc Vault) là điểm cân bằng tốt nhất giữa độ an toàn và công sức vận hành. Phần tiếp theo đi sâu vào nó.


5. Thực chiến: External Secrets Operator + AWS Secrets Manager trên EKS

5.1 Kiến trúc

AWS Secrets Manager (nguồn sự thật, có audit qua CloudTrail)
        │  (ESO đọc định kỳ, xác thực bằng IAM)
        ▼
External Secrets Operator (trong cluster)
        │  (tạo/cập nhật)
        ▼
Kubernetes Secret  ──mount──▶  Pod (ứng dụng)

Ưu điểm quan trọng: Pod chỉ đọc Secret có sẵn trong cluster, không gọi ra ngoài lúc khởi động. Nếu Secrets Manager hay ESO tạm thời gặp sự cố, Secret đã đồng bộ vẫn còn đó và Pod mới vẫn lên được. Đây là điểm khác biệt lớn so với mô hình "service gọi thẳng config server lúc boot".

5.2 Giải quyết "secret zero": làm sao ESO xác thực với AWS?

Bài toán gà và trứng: để lấy secret, bạn cần credential, vậy credential đầu tiên ở đâu ra? Trên EKS, câu trả lời là không dùng access key tĩnh, mà gắn IAM Role cho ServiceAccount:

  • IRSA (IAM Roles for Service Accounts): dựa trên OIDC provider của cluster.
  • EKS Pod Identity: cơ chế mới hơn, cấu hình đơn giản hơn.

Cả hai đều cấp credential tạm thời cho Pod dựa trên danh tính ServiceAccount, nên không có secret nào phải "rải" trước. Role chỉ nên có quyền secretsmanager:GetSecretValue trên đúng các secret cần thiết.

5.3 Manifest mẫu

Khai báo nơi lấy secret (ClusterSecretStore):

apiVersion: external-secrets.io/v1beta1   # tùy phiên bản ESO, có thể là v1
kind: ClusterSecretStore
metadata:
  name: aws-secrets-manager
spec:
  provider:
    aws:
      service: SecretsManager
      region: ap-southeast-1
      auth:
        jwt:
          serviceAccountRef:
            name: external-secrets-sa      # ServiceAccount đã gắn IAM Role (IRSA)
            namespace: external-secrets

Khai báo secret cần đồng bộ (ExternalSecret):

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: ticket-service-db
  namespace: ticketing
spec:
  refreshInterval: 15m                     # kiểm tra lại nguồn mỗi 15 phút
  secretStoreRef:
    name: aws-secrets-manager
    kind: ClusterSecretStore
  target:
    name: ticket-service-db                # tên K8s Secret được tạo ra
    creationPolicy: Owner
  data:
    - secretKey: DB_PASSWORD
      remoteRef:
        key: prod/ticketing/db
        property: password

Manifest này an toàn để commit lên Git vì nó chỉ chứa tham chiếu, không chứa giá trị secret. Đây là chìa khóa để GitOps và secret chung sống hòa bình.

5.4 Quy trình đổi mật khẩu DB lúc này

  1. Security yêu cầu đổi mật khẩu. SRE cập nhật giá trị trong Secrets Manager (hoặc bật rotation tự động).
  2. Sau tối đa refreshInterval, ESO cập nhật K8s Secret.
  3. Reloader phát hiện Secret đổi và rollout các Deployment liên quan theo kiểu rolling update.
  4. Toàn bộ thao tác có dấu vết trong CloudTrail; không file nào bị sửa tay, không có CI/CD nào chạy 20 lần.

Lưu ý thực tế: trong lúc đổi mật khẩu, cần chiến lược để không có khoảng "nửa cũ nửa mới". Cách phổ biến là rotation hai pha (tạo user hoặc mật khẩu mới song song, chuyển dần, rồi mới thu hồi cái cũ).


6. Khi nào cần đến Vault và Dynamic Secrets?

Secrets Manager + ESO đủ cho phần lớn trường hợp. Hãy cân nhắc HashiCorp Vault khi:

  • Cần Dynamic Secrets: Vault tạo credential DB riêng cho từng Pod hoặc từng phiên, có TTL ngắn, tự thu hồi khi hết hạn.
  • Chạy đa nền tảng (K8s lẫn VM, nhiều cloud) và muốn một chỗ quản lý chung.
  • Cần các tính năng nâng cao như PKI nội bộ, encryption-as-a-service, chính sách chi tiết.

6.1 Kubernetes auth method

Vault giải bài toán secret zero bằng cách xác thực dựa trên ServiceAccount token của Pod: Pod đưa token cho Vault, Vault hỏi lại Kubernetes API để xác minh, rồi cấp Vault token theo policy gắn với role tương ứng. Pod không cần mang theo bất kỳ credential tĩnh nào.

6.2 Ba cách đưa secret từ Vault vào Pod

Cách Đặc điểm
Vault Agent Injector (sidecar) Sidecar tự xác thực, lấy secret, ghi ra file trong Pod, gia hạn lease
Vault Secrets Operator Đồng bộ secret từ Vault thành K8s Secret, tương tự ESO nhưng của HashiCorp
ESO với provider Vault Dùng chung một cơ chế với các nguồn khác

6.3 Cái giá của Dynamic Secrets mà ít bài nhắc đến

Credential có TTL một giờ nghe rất hay, nhưng ứng dụng phải sống chung được với việc credential đổi liên tục:

  • Phải gia hạn lease hoặc lấy credential mới trước khi hết hạn.
  • Connection pool phải làm mới kết nối. Ví dụ với HikariCP, đặt maxLifetime ngắn hơn TTL của credential để kết nối cũ được thay trước khi bị thu hồi. Nếu không, đến giờ hết hạn bạn sẽ thấy lỗi kết nối hàng loạt, đúng kiểu sự cố mà ta muốn tránh.
  • Vault trở thành dependency quan trọng: cần chạy HA (Raft 3 hoặc 5 node), cấu hình auto-unseal (qua KMS), có backup và quy trình nâng cấp.

Tóm lại: Dynamic Secrets mạnh về bảo mật nhưng đổi lại chi phí vận hành và độ phức tạp. Đừng bật chỉ vì nghe "xịn".


7. Hot reload ở tầng ứng dụng: nên hay không?

Bài kiểu cũ thường quảng bá "đổi cấu hình không cần restart" như siêu năng lực. Thực tế cần cân nhắc:

Vấn đề của hot reload:

  • Không phải mọi cấu hình đều reload nóng được (DB URL, kích thước pool, cổng lắng nghe).
  • Các instance nhận giá trị mới không cùng lúc, tạo ra khoảng thời gian không nhất quán.
  • Đổi thẳng trên UI mà không có review, versioning, rollback là mở cửa cho sự cố do thao tác tay.

Khuyến nghị:

  • Cấu hình hạ tầng và secret: đi qua rollout (Reloader hoặc checksum). Tận dụng rolling update, readiness probe, rollback.
  • Tham số nghiệp vụ và feature flag cần đổi nóng: dùng công cụ feature flag (Unleash, Flagsmith, hoặc chuẩn OpenFeature với flagd). Chúng có sẵn phân quyền, audit, bật theo tỉ lệ, kill switch, những thứ ConfigMap không có.

7.1 Nếu vẫn muốn reload nóng với Spring Boot

Cách A: mount file + configtree (đơn giản nhất cho secret):

# application.yaml
spring:
  config:
    import: "optional:configtree:/etc/secrets/"

Mỗi file trong thư mục /etc/secrets/ thành một property (tên file là key). Secret từ ESO mount vào đây là Spring đọc được.

Cách B: Spring Cloud Kubernetes đọc trực tiếp ConfigMap và Secret qua Kubernetes API, hỗ trợ reload:

spring:
  config:
    import: "kubernetes:"
  cloud:
    kubernetes:
      reload:
        enabled: true
        mode: event          # event (watch API) hoặc polling
        strategy: refresh    # refresh | restart_context | shutdown

Cần cấp RBAC cho ServiceAccount của Pod để đọc ConfigMap và Secret. Bean cần reload nóng nên dùng @ConfigurationProperties hoặc @RefreshScope.

Cách C: Spring Cloud Config Server (dùng Git làm backend): hợp khi bạn muốn cấu hình có lịch sử Git, và có thể kết hợp Spring Cloud Bus để refresh. Cái giá là thêm một service phải vận hành, và ngoài hệ sinh thái Spring thì kém lợi thế.

Lưu ý: tùy phiên bản Spring Boot và Spring Cloud, tên thuộc tính có thể khác nhau chút. Hãy đối chiếu tài liệu chính thức của đúng phiên bản bạn dùng.


8. Consul có còn chỗ đứng không?

Có, nhưng hẹp hơn so với thời trước khi Kubernetes thống trị. Consul hợp khi:

  • Bạn chạy hỗn hợp VM và K8s, hoặc đang trong giai đoạn di cư (migration) và cần một nơi dùng chung.
  • Bạn cần thêm service discovery và service mesh ngoài K8s.

Nếu toàn bộ workload đã ở trong K8s thì service discovery có sẵn (Service, DNS), cấu hình có ConfigMap và Secret. Dựng thêm Consul chỉ để lưu key-value thường là thêm một hệ thống phải vận hành mà lợi ích không tương xứng.


9. Ghép lại: luồng GitOps hoàn chỉnh

Git repo (manifest, Helm values, ExternalSecret – KHÔNG chứa giá trị secret)
   │  Pull Request → review → merge
   ▼
ArgoCD / Flux  ──đồng bộ──▶  Cluster
                                 ├─ ConfigMap (cấu hình thường, theo từng môi trường)
                                 ├─ ExternalSecret ──ESO──▶ Secret ◀── Secrets Manager / Vault
                                 └─ Deployment (+ Reloader hoặc checksum để rollout khi đổi)

Những gì bạn có được:

  • Audit: thay đổi cấu hình = commit trong Git; thay đổi secret = log ở secret manager.
  • Review: cấu hình đi qua Pull Request như code.
  • Rollback: git revert hoặc kubectl rollout undo.
  • Nhất quán giữa môi trường: cùng một chart, khác values-<env>.yaml.
  • Không ai phải biết mật khẩu production ngoài hệ thống.

10. Checklist bảo mật

  • [ ] Bật mã hóa etcd (EKS: KMS envelope encryption).
  • [ ] RBAC theo nguyên tắc tối thiểu quyền; hạn chế quyền get/list Secret.
  • [ ] IAM Role gắn ServiceAccount (IRSA hoặc Pod Identity), không dùng access key tĩnh.
  • [ ] Policy trên secret manager chỉ cho đúng role, đúng secret.
  • [ ] Không in secret ra log; mask trong log khi cần.
  • [ ] Ưu tiên mount file thay vì biến môi trường cho secret.
  • [ ] Quét secret trong repo và CI (gitleaks, trufflehog) cùng pre-commit hook.
  • [ ] Có quy trình xoay vòng định kỳ và diễn tập khi bị lộ.
  • [ ] Dùng NetworkPolicy hạn chế Pod nào được gọi tới Vault hoặc endpoint secret.
  • [ ] Backup và kế hoạch khôi phục cho secret manager tự vận hành (Vault).

11. Rủi ro và điểm chết tập trung: nhìn lại cho đúng

Nỗi lo "config server chết thì toàn hệ thống không lên được" có thật, nhưng mức độ phụ thuộc vào kiến trúc:

Mô hình Nếu nguồn cấu hình chết...
Service gọi thẳng config server lúc boot Pod mới không khởi động được → cần HA và fallback
ESO đồng bộ vào K8s Secret Pod mới vẫn lên bình thường với Secret đã có; chỉ không cập nhật được giá trị mới
Vault Agent sidecar Pod mới cần Vault lúc khởi tạo; secret đang chạy vẫn dùng được đến khi hết lease

Nghĩa là mô hình "đồng bộ về cluster" (ESO) có khả năng chịu lỗi tốt hơn nhờ tách việc lấy secret khỏi đường khởi động của ứng dụng.

Một số nguyên tắc giảm rủi ro chung:

  • Chạy secret manager tự vận hành ở chế độ HA, có auto-unseal và giám sát.
  • Đặt cảnh báo khi ExternalSecret ở trạng thái lỗi đồng bộ (ESO có metric và condition).
  • Tránh cơ chế fallback bằng file .env.backup trên đĩa. Nó đưa secret cũ trở lại đĩa và gây câu hỏi "khóa giải mã để ở đâu". Dùng cache của Vault Agent hoặc Secret đã đồng bộ trong cluster là lựa chọn gọn hơn.
  • Đừng để hệ thống "đẹp" mà không diễn tập: định kỳ thử kịch bản nguồn secret ngừng hoạt động.

12. Chọn gì theo quy mô?

Bối cảnh Gợi ý
Startup, 1 cluster, vài service ConfigMap + Secret, bật mã hóa etcd, Sealed Secrets hoặc SOPS, Reloader
Chạy trên AWS EKS, nhiều service, có GitOps ConfigMap qua Helm/Kustomize + ESO + AWS Secrets Manager (IRSA/Pod Identity) + Reloader
Cần credential động, đa cloud hoặc đa nền tảng Vault (HA) + Vault Secrets Operator hoặc Agent; feature flag riêng
Hệ sinh thái Spring, cần cấu hình có lịch sử Git Spring Cloud Config hoặc Spring Cloud Kubernetes, kết hợp một trong các cách trên cho secret
Đang hỗn hợp VM và K8s Consul hoặc Vault dùng chung, dần chuyển sang cách native của K8s

Lời khuyên chung: bắt đầu đơn giản, nâng cấp khi có nhu cầu thật. Rất nhiều team đã dựng Vault cluster rồi mới nhận ra họ chỉ cần Secrets Manager và ESO.


13. Tổng kết

  1. Vấn đề cốt lõi là phân tán, thiếu audit, khó xoay vòng, không phải bản thân file .env.
  2. Phân loại cấu hình (thường, secret, feature flag, theo môi trường) trước khi chọn công cụ.
  3. K8s ConfigMap và Secret là nền tảng, nhưng cần nhớ: env không tự cập nhật, Secret chỉ là base64.
  4. Giải bài toán GitOps cho secret bằng ESO (hoặc Sealed Secrets, SOPS); trên cloud thì ESO + secret manager là điểm cân bằng tốt.
  5. Vault và Dynamic Secrets rất mạnh nhưng có cái giá vận hành; chỉ dùng khi lợi ích rõ ràng.
  6. Rollout (Reloader, checksum) an toàn hơn hot reload cho cấu hình hạ tầng; tham số nghiệp vụ đổi nóng nên dùng feature flag.
  7. Mô hình "đồng bộ về cluster" giúp giảm điểm chết tập trung so với việc ứng dụng gọi thẳng config server lúc boot.

Ở bài tiếp theo (Bài 14), chúng ta sẽ làm chủ Monitoring & Alerting với Prometheus và Grafana, để biết hệ thống có vấn đề trước khi người dùng báo lỗi.

Câu hỏi cho bạn đọc: Team bạn đang dùng cách nào: Secret thuần, Sealed Secrets, ESO, hay Vault? Đã bao giờ phải xoay vòng mật khẩu production khẩn cấp chưa, và quy trình có suôn sẻ không? Chia sẻ ở phần bình luận nhé!


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.