ConfigMap, Secret, RBAC và ServiceAccount

4 câu hỏi phỏng vấn~25 phútTừ khóa: ConfigMap, Secret, env, envFrom, volume mount, rollout restart, base64, encryption at rest, External Secrets, RBAC, Role, RoleBinding, ServiceAccount, namespace

Tiếp đi: ConfigMap, Secret và phân quyền.

Bài này nối thẳng với environment: của Docker Compose. Spring Boot không cần biết config đến từ Docker hay Kubernetes; nó chỉ cần biến môi trường tồn tại.

1. Bài toán

Docker Compose của ta từng viết:

environment:
  DB_URL: jdbc:postgresql://db:5432/shop
  DB_USERNAME: shop
  DB_PASSWORD: ...           # password nằm trong file

payment-service đọc:

spring:
  datasource:
    url: ${DB_URL}
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}
payment:
  timeout: ${PAYMENT_TIMEOUT:5000}

2. Vấn đề

3. Giải pháp

Deployment payment-service
      ├── ConfigMap payment-config   → config thường: DB_URL, PAYMENT_TIMEOUT
      └── Secret    payment-secret   → config nhạy cảm: DB_USERNAME, DB_PASSWORD

Hai cách đưa vào Pod:

ConfigMap / Secret
       ├── Environment variables   (env / envFrom)  → đọc 1 lần khi start
       └── Mounted files           (volume)         → file trong container, tự cập nhật

4. Code từng bước

Bước 1: ConfigMap

apiVersion: v1
kind: ConfigMap
metadata:
  name: payment-config
data:
  DB_URL: jdbc:postgresql://postgres:5432/shop
  PAYMENT_TIMEOUT: "5000"            # value luôn là string
  LOGGING_LEVEL_COM_SHOP: INFO

Bước 2: Secret, không commit giá trị thật

# CI/CD hoặc admin tạo từ biến môi trường / secret manager
kubectl create secret generic payment-secret \
  --from-literal=DB_USERNAME="$DB_USERNAME" \
  --from-literal=DB_PASSWORD="$DB_PASSWORD" \
  --dry-run=client -o yaml | kubectl apply -f -

Khi học có thể viết YAML với stringData (plain text, K8s tự encode), nhưng file đó không được vào Git:

apiVersion: v1
kind: Secret
metadata:
  name: payment-secret
type: Opaque
stringData:
  DB_USERNAME: shop
  DB_PASSWORD: change-me-local-only

Bước 3: Inject vào Deployment

containers:
  - name: payment
    image: myregistry/payment-service:1.0
    # Cách 1: từng key, nhìn là biết app dùng gì
    env:
      - name: DB_URL
        valueFrom:
          configMapKeyRef: { name: payment-config, key: DB_URL }
      - name: DB_PASSWORD
        valueFrom:
          secretKeyRef: { name: payment-secret, key: DB_PASSWORD }
    # Cách 2: import cả ConfigMap/Secret, ngắn gọn
    envFrom:
      - configMapRef: { name: payment-config }
      - secretRef:    { name: payment-secret }
    # Cách 3: mount file application.yml
    volumeMounts:
      - name: app-config
        mountPath: /config
        readOnly: true
volumes:
  - name: app-config
    configMap:
      name: payment-app-yml       # data: { application.yml: | ... }

Spring Boot tự đọc /config/application.yml nếu thêm SPRING_CONFIG_ADDITIONAL_LOCATION=/config/. Relaxed binding map PAYMENT_TIMEOUT vào payment.timeout, LOGGING_LEVEL_COM_SHOP vào logging.level.com.shop.

Bước 4: RBAC cho một Pod cần đọc ConfigMap

apiVersion: v1
kind: ServiceAccount
metadata:
  name: payment-sa
automountServiceAccountToken: false     # mặc định không mount token
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: config-reader
  namespace: shop
rules:
  - apiGroups: [""]
    resources: ["configmaps"]
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: payment-config-reader
  namespace: shop
subjects:
  - kind: ServiceAccount
    name: payment-sa
    namespace: shop
roleRef:
  kind: Role
  name: config-reader
  apiGroup: rbac.authorization.k8s.io
# Pod dùng ServiceAccount này, chỉ bật token khi app thật sự gọi K8s API
spec:
  serviceAccountName: payment-sa
  automountServiceAccountToken: true   # ví dụ dùng Spring Cloud Kubernetes reload

Test:

kubectl get cm,secret
kubectl get configmap payment-config -o yaml
kubectl exec deploy/payment-service -- printenv DB_URL
kubectl auth can-i list secrets --as=system:serviceaccount:shop:payment-sa -n shop
# no
kubectl auth can-i get configmaps --as=system:serviceaccount:shop:payment-sa -n shop
# yes

5. Cách hoạt động

Config đi từ ConfigMap tới DataSource

ConfigMap / Secret (etcd)
      │ kubelet đọc khi tạo container
      ▼
Container ENV: DB_URL=..., DB_PASSWORD=...
      │
      ▼
java -jar app.jar → Spring Environment → ${DB_URL} → HikariDataSource

Sửa ConfigMap thì sao?

kubectl apply -f configmap.yaml   (PAYMENT_TIMEOUT 5000 → 3000)
   │
   ├── Dạng ENV:    process đã đọc env lúc start → KHÔNG đổi
   │                → cần: kubectl rollout restart deployment/payment-service
   │
   └── Dạng volume: kubelet cập nhật file sau ~1 phút (trừ khi dùng subPath)
                    → app phải tự đọc lại file (Spring Cloud Kubernetes reload,
                      @RefreshScope, hoặc tự watch file)

Cách hay dùng khi deploy bằng Helm: thêm checksum của ConfigMap vào annotation của Pod template, ConfigMap đổi thì template đổi, Deployment tự rolling update (bài 17.8).

Secret chỉ là base64

kubectl get secret payment-secret -o jsonpath='{.data.DB_PASSWORD}'
# Y2hhbmdlLW1lLWxvY2FsLW9ubHk=
echo Y2hhbmdlLW1lLWxvY2FsLW9ubHk= | base64 -d
# change-me-local-only

Base64 là encoding, không phải encryption. Ai có quyền get secret là đọc được. Bảo vệ thật nằm ở:

RBAC                → hạn chế ai được get/list secrets
Encryption at rest  → mã hóa Secret trong etcd (EncryptionConfiguration / KMS)
External manager    → Vault, AWS Secrets Manager, Azure Key Vault
                      + External Secrets Operator / CSI Secrets Store đồng bộ vào cluster
Git                 → không bao giờ commit Secret thật; nếu cần GitOps dùng Sealed Secrets / SOPS
Audit + rotation    → biết ai đọc, đổi định kỳ

Lưu ý: ai có quyền tạo Pod trong namespace là có thể mount mọi Secret của namespace đó, nên quyền create pods cũng nhạy cảm.

6. So sánh

env / envFromVolume mount
Cập nhật khi sửaKhông, cần restart PodCó, sau khoảng 1 phút
Hợp vớiGiá trị đơn, Spring relaxed bindingFile cấu hình, chứng chỉ, file lớn
Rủi ro lộDễ lộ qua /proc/<pid>/environ, log in env, crash dumpÍt hơn, Secret ở tmpfs
Role / RoleBindingClusterRole / ClusterRoleBinding
Phạm viMột namespaceToàn cluster (hoặc tái dùng ClusterRole trong RoleBinding)
Ví dụpayment-sa đọc ConfigMap trong shopIngress Controller đọc Ingress mọi namespace

7. Khi nào nên dùng / không nên dùng

Sửa ConfigMap xong apply rồi mà Spring Boot vẫn dùng giá trị cũ, mình làm sai chỗ nào nhỉ?

Bạn không sai, đó là hành vi đúng của biến môi trường. Biến môi trường được copy vào process một lần lúc container start. Sau đó sửa ConfigMap không chạm được vào process Java đang chạy.

Pod start → ConfigMap → ENV → java -jar app.jar
change ConfigMap ──X──► running Java ENV

Ba cách xử lý:

  1. Restart có kiểm soát (đơn giản, hay dùng nhất):
    kubectl rollout restart deployment/payment-service
    Rolling update nên không downtime.
  2. Tự động hóa restart: Helm checksum annotation (bài 17.8), hoặc công cụ như Reloader watch ConfigMap rồi restart Deployment.
  3. Mount dạng file + reload trong app: Spring Cloud Kubernetes Config Reload, hoặc @RefreshScope. Phức tạp hơn, và lúc reload các Pod có thể chạy config khác nhau vài chục giây.

Cách hay hơn nữa là coi ConfigMap là immutable: đặt tên có version (payment-config-v7), Deployment trỏ tên mới, rollback app là rollback luôn config.

Secret chỉ là base64 thì nó có an toàn không nhỉ?

Tự thân thì không. Nhưng Secret không vô dụng, nó cho bạn chỗ để gắn bảo vệ:

Mức production hay gặp:

AWS Secrets Manager / Vault  (nguồn sự thật, có rotation + audit)
        │ External Secrets Operator đồng bộ
        ▼
K8s Secret payment-secret (namespace shop, chỉ payment-sa và CI đọc được)
        │
        ▼
Pod payment-service

Câu nên nói khi phỏng vấn: Secret is base64-encoded, not encrypted. Real protection comes from RBAC, encryption at rest and an external secret manager.

Phỏng vấn hỏi về config, secret và RBAC trong Kubernetes thì trả lời sao?

Q1. ConfigMap khác Secret thế nào? Bạn đưa chúng vào Spring Boot ra sao?

🇻🇳 Tiếng Việt:

Cả hai đều là key-value tách config khỏi image. ConfigMap cho dữ liệu thường, Secret cho dữ liệu nhạy cảm, được phân quyền riêng và có thể mã hóa trong etcd. Em đưa vào Pod bằng env/envFrom để Spring Boot đọc qua relaxed binding, hoặc mount thành file khi cần application.yml hay chứng chỉ. Dạng env cần restart Pod khi đổi giá trị, dạng file thì kubelet cập nhật nhưng app phải tự reload.

🇬🇧 English:

Both are key-value objects that keep configuration out of the image. ConfigMaps hold regular data; Secrets hold sensitive data, with separate access control and optional encryption in etcd. I inject them as env/envFrom so Spring Boot reads them through relaxed binding, or mount them as files when I need an application.yml or certificates. Environment variables need a Pod restart to pick up changes; mounted files are updated by the kubelet but the app must reload them.

Q2. Kubernetes Secret có an toàn không? Bạn quản lý secret production thế nào?

🇻🇳 Tiếng Việt:

Mặc định Secret chỉ là base64, ai có quyền đọc là decode được, và nếu không bật encryption at rest thì nằm dạng gần như plain text trong etcd. Em giữ secret thật trong secret manager như Vault hoặc AWS Secrets Manager, đồng bộ vào cluster bằng External Secrets Operator, bật encryption at rest, giới hạn RBAC get secrets và không commit secret vào Git hay Helm values.

🇬🇧 English:

By default a Secret is just base64; anyone allowed to read it can decode it, and without encryption at rest it sits almost in plain text in etcd. I keep real secrets in a secret manager such as Vault or AWS Secrets Manager, sync them into the cluster with the External Secrets Operator, enable encryption at rest, restrict get secrets through RBAC, and never commit secrets to Git or Helm values.

Q3. RBAC trong Kubernetes hoạt động thế nào?

🇻🇳 Tiếng Việt:

RBAC gồm Role hoặc ClusterRole định nghĩa quyền, tức verb nào trên resource nào, và RoleBinding hoặc ClusterRoleBinding gán quyền đó cho subject là user, group hoặc ServiceAccount. Role giới hạn trong một namespace, ClusterRole áp dụng toàn cluster. Mặc định là deny, chỉ có quyền nào được cho. Em áp dụng least privilege và kiểm tra bằng kubectl auth can-i.

🇬🇧 English:

RBAC has Roles or ClusterRoles that define permissions, meaning which verbs on which resources, and RoleBindings or ClusterRoleBindings that grant them to subjects: users, groups or ServiceAccounts. A Role is namespaced; a ClusterRole is cluster-wide. Everything is denied unless explicitly allowed. I follow least privilege and verify it with kubectl auth can-i.

Q4. ServiceAccount dùng để làm gì?

🇻🇳 Tiếng Việt:

ServiceAccount là danh tính của Pod khi gọi Kubernetes API, khác với user account dành cho người. Token của nó được mount vào Pod và có thời hạn. Em tạo ServiceAccount riêng cho từng app, bind đúng quyền cần, và tắt automount token nếu app không gọi API. Trên cloud, ServiceAccount còn được map sang IAM role, như IRSA trên EKS hay Workload Identity, để Pod truy cập S3 hay Secrets Manager mà không cần access key.

🇬🇧 English:

A ServiceAccount is the identity a Pod uses to call the Kubernetes API, as opposed to user accounts for people. Its token is mounted into the Pod and is time-limited. I create one ServiceAccount per app, bind only the permissions it needs, and disable token automounting if the app doesn't call the API. On cloud platforms, a ServiceAccount can also map to an IAM role, such as IRSA on EKS or Workload Identity, so Pods can access S3 or Secrets Manager without static access keys.

Cách nhớ ngắn gọn


Tự kiểm tra

Biến PAYMENT_TIMEOUT trong ConfigMap map vào property Spring nào?

payment.timeout (relaxed binding).

Mount ConfigMap bằng subPath có tự cập nhật không?

Không. File mount qua subPath không được kubelet cập nhật.

Lệnh nào kiểm tra ServiceAccount có được list secrets không?

kubectl auth can-i list secrets --as=system:serviceaccount:<ns>:<sa> -n <ns>.

Vì sao quyền tạo Pod cũng nhạy cảm như quyền đọc Secret?

Người tạo được Pod có thể mount bất kỳ Secret nào trong namespace vào Pod rồi đọc.

Role và ClusterRole khác gì?

Role giới hạn trong một namespace; ClusterRole cho resource toàn cluster hoặc dùng lại ở nhiều namespace.