# 🔐 ConfigMap và Secret: biến môi trường, volume mount và cạm bẫy

## Mục tiêu bài học

- Tạo được ConfigMap và Secret từ literal, từ file và từ cả thư mục bằng `kubectl`.
- Đưa cấu hình vào Pod qua hai đường: biến môi trường (`env`, `envFrom`) và volume mount, biết khi nào dùng đường nào.
- Nhận diện các cạm bẫy kinh điển: secret lộ trong manifest hoặc Git, base64 bị nhầm là mã hoá, sửa cấu hình mà app không reload.
- Kiểm tra được một Pod đang ăn cấu hình từ object nào và debug lỗi thiếu key, thiếu object.

## Ôn lại nhanh (2 phút)

Ở Module 1 bạn đã biết Pod là đơn vị nhỏ nhất, Deployment quản ReplicaSet và rollout theo template. Điểm mấu chốt: **image là bất biến**, nên mọi thứ thay đổi theo môi trường (dev, staging, prod) phải nằm ngoài image. Service và Ingress lo phần "ai gọi tới Pod", còn bài này lo phần "Pod đọc cấu hình ở đâu ra".

## Lý thuyết

### 1. ConfigMap: cấu hình không bí mật

ConfigMap là API object chứa các cặp key-value dạng text. Nó **không** mã hoá gì cả, ai có quyền `get` object là đọc được toàn bộ.

```bash
kubectl create configmap app-config \
  --from-literal=LOG_LEVEL=info \
  --from-literal=APP_MODE=production

kubectl create configmap nginx-conf --from-file=default.conf=./nginx/default.conf
```

Với `--from-file`, tên file trở thành key; nếu muốn key khác thì viết `key=đường/dẫn/file`.

### 2. Secret: cũng là key-value, nhưng có ý nghĩa bảo mật

Secret có cấu trúc gần giống ConfigMap, thêm `type` (`Opaque`, `kubernetes.io/tls`, `kubernetes.io/dockerconfigjson`) và được các cơ chế như RBAC, encryption at rest trong etcd, audit log đối xử riêng.

> [!WARNING]
> Secret lưu giá trị dưới dạng {{c1::base64}}, nhưng base64 chỉ là encoding chứ không phải {{c2::mã hoá}}. Bất kỳ ai đọc được object đều decode ra plaintext trong một giây. Đừng commit file Secret YAML lên Git.

### 3. Hai con đường đưa cấu hình vào Pod

```mermaid
flowchart LR
    CM["📦<br/>ConfigMap / Secret"] --> ENV["🌱<br/>env / envFrom"]
    CM --> VOL["📁<br/>volume mount"]
    ENV --> P1["Pod<br/>giá trị chốt lúc container start"]
    VOL --> P2["Pod<br/>file được đồng bộ lại khi object đổi"]
```

**Đường 1: biến môi trường.** Dùng `envFrom` cho toàn bộ key, hoặc `env.valueFrom` cho từng key cụ thể (bắt buộc khi key có dấu chấm, dấu gạch ngang, vì `envFrom` sẽ bỏ qua key không hợp lệ với tên biến môi trường).

**Đường 2: volume mount.** Mỗi key thành một file trong thư mục mount. Cách này hợp với file cấu hình (nginx.conf, application.yaml) và là cách **duy nhất** để app thấy thay đổi mà không cần restart.

| Tiêu chí | env / envFrom | volume mount |
| --- | --- | --- |
| Định dạng phù hợp | chuỗi ngắn | file cấu hình dài |
| Tự cập nhật khi sửa object | Không | Có, sau độ trễ đồng bộ của kubelet |
| Dùng `subPath` | không áp dụng | **Mất tính năng tự cập nhật** |
| Lưu trên đĩa node | có | Secret dùng tmpfs (RAM) |

Biến môi trường từ ConfigMap {{c3::không tự cập nhật}} khi ConfigMap thay đổi, bạn phải `kubectl rollout restart`. Volume mount thì có đồng bộ lại, **trừ khi** dùng {{c4::subPath}}.

### 4. Vài thuộc tính đáng nhớ

- `optional: true`: cho phép Pod vẫn chạy khi thiếu object hoặc thiếu key. Mặc định thiếu là Pod kẹt ở `CreateContainerConfigError` (env) hoặc `FailedMount`/`Pending` (volume).
- `immutable: true`: chặn sửa/xoá, giảm tải cho API server và kubelet khi cluster lớn. Đặt `immutable: {{c5::true}}` nghĩa là muốn đổi thì phải tạo object mới.
- `defaultMode`: với Secret nên đặt `0400` thay vì mặc định `0644`.

## Ví dụ có hướng dẫn

**Ví dụ 1: ConfigMap làm biến môi trường**

```yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  LOG_LEVEL: info
  APP_MODE: production
---
apiVersion: v1
kind: Pod
metadata:
  name: api
spec:
  containers:
    - name: api
      image: nginx:1.27
      envFrom:
        - configMapRef:
            name: app-config
```

Kiểm tra: `kubectl exec api -- env | grep -E 'LOG_LEVEL|APP_MODE'`.
Sửa `LOG_LEVEL` thành `debug`, vào Pod xem lại: vẫn là `info`. Đây chính là cạm bẫy số một.

**Ví dụ 2: Mount file cấu hình và quan sát reload**

```yaml
spec:
  containers:
    - name: web
      image: nginx:1.27
      volumeMounts:
        - name: nginx-conf
          mountPath: /etc/nginx/conf.d
          readOnly: true
  volumes:
    - name: nginx-conf
      configMap:
        name: nginx-conf
```

Sau khi `kubectl edit configmap nginx-conf`, chờ khoảng 30 tới 60 giây rồi `kubectl exec web -- cat /etc/nginx/conf.d/default.conf`, nội dung đã đổi. Nhưng nginx thì chưa nạp lại: app vẫn phải tự reload (`nginx -s reload`) hoặc bạn phải restart. Volume cập nhật file, **không** restart tiến trình giúp bạn.

**Ví dụ 3: Secret mount an toàn hơn**

```yaml
  volumes:
    - name: db-secret
      secret:
        secretName: db-secret
        defaultMode: 0400
```

File nằm trong tmpfs, quyền chỉ đọc cho owner, không ghi xuống đĩa node.

## Thực hành có hướng dẫn

**Bài 1: Tạo cấu hình từ file**

1. Tạo `app.properties` với 2 dòng `retries=3` và `timeout=30s`.
2. `kubectl create configmap app-props --from-file=app.properties`
3. `kubectl get configmap app-props -o yaml` và xác nhận key là `app.properties`.

<details><summary>Gợi ý đáp án</summary>

Toàn bộ nội dung file nằm trong một key duy nhất là tên file. Nếu muốn tách từng key, dùng `--from-env-file=app.properties` cho định dạng `KEY=value`.
</details>

**Bài 2: Cố tình gây lỗi thiếu object**

1. Tạo Pod có `envFrom.configMapRef.name: khong-ton-tai`.
2. `kubectl get pod` và `kubectl describe pod` để xem lỗi.
3. Thêm `optional: true` và quan sát Pod chạy bình thường nhưng không có biến nào.

<details><summary>Gợi ý đáp án</summary>

Pod kẹt ở `CreateContainerConfigError`. Đây là lỗi bạn sẽ gặp rất nhiều khi deploy, nên nhớ nó nằm ở phần `Events`, không phải trong log container (container chưa hề start).
</details>

**Bài 3: So sánh env và volume**

1. Deploy một Pod vừa dùng `envFrom` vừa mount cùng một ConfigMap.
2. Sửa một value trong ConfigMap, chờ 1 phút.
3. So sánh `env` và nội dung file trong thư mục mount.

<details><summary>Gợi ý đáp án</summary>

Env giữ giá trị cũ, file đã đổi. Đây là lý do nhiều team chọn volume mount cho cấu hình cần thay đổi nóng, và chọn env cho cấu hình ngắn, ít đổi.
</details>

## Góc chuyên gia

- **Đừng commit Secret YAML.** Dùng `kubectl create secret --dry-run=client -o yaml` để sinh nhanh, nhưng file sinh ra chỉ để dùng tại chỗ. Lên production thì cân nhắc External Secrets hoặc Sealed Secrets.
- **`subPath` là bẫy im lặng.** Rất nhiều người dùng `subPath` để mount một file đơn lẻ rồi ngồi đợi file đổi. Nó không bao giờ đổi. Muốn mount một file thì mount cả thư mục.
- **Mount đè thư mục.** Mount vào `/app/config` sẽ thay thế toàn bộ nội dung thư mục đó trong container, kể cả file có sẵn trong image. Kiểm tra lại đường dẫn trước khi mount.
- **Đổi tên object thay vì sửa.** Với cấu hình quan trọng, tạo ConfigMap mới (`app-config-v2`) rồi trỏ Deployment sang. Rollback đơn giản hơn nhiều so với sửa tại chỗ.
- **Luôn đặt `readOnly: true`** cho volume cấu hình, tránh app ghi ngược vào file gốc trong container.
- Quy tắc nhanh: **ngắn và tĩnh thì env, dài và cần đổi nóng thì volume.**

## Tóm tắt & bước tiếp theo

### Bộ thẻ ôn tập

```flashcard
[
  { "front": "ConfigMap lưu gì?", "back": "Cấu hình dạng key-value không mã hoá, tách khỏi image." },
  { "front": "Secret khác ConfigMap ở điểm nào?", "back": "Có type, lưu base64, mount dạng tmpfs, được RBAC và encryption at rest đối xử riêng." },
  { "front": "base64 có phải mã hoá không?", "back": "Không, chỉ là encoding; ai đọc được object là decode ra plaintext." },
  { "front": "Hai cách đưa ConfigMap vào Pod?", "back": "env/envFrom hoặc mount volume." },
  { "front": "env có tự cập nhật khi ConfigMap đổi không?", "back": "Không, giá trị chốt lúc container start; phải rollout restart." },
  { "front": "Volume mount có tự cập nhật không?", "back": "Có, kubelet đồng bộ lại sau một khoảng trễ, trừ khi dùng subPath." },
  { "front": "subPath có cập nhật khi ConfigMap đổi không?", "back": "Không bao giờ, phải tạo lại Pod." },
  { "front": "Thiếu ConfigMap bắt buộc thì Pod báo gì?", "back": "CreateContainerConfigError với env, FailedMount/Pending với volume." },
  { "front": "Làm sao cho phép thiếu ConfigMap hoặc key?", "back": "Đặt optional: true." },
  { "front": "defaultMode mặc định của Secret volume là gì?", "back": "0644; nên đặt 0400 cho file secret." },
  { "front": "immutable: true có tác dụng gì?", "back": "Chặn sửa/xoá, giảm tải API server và kubelet; muốn đổi phải tạo object mới." },
  { "front": "Mount volume vào thư mục có sẵn thì sao?", "back": "Ghi đè toàn bộ nội dung thư mục đó trong container." },
  { "front": "Key có dấu chấm hoặc gạch ngang dùng envFrom được không?", "back": "Không, envFrom bỏ qua key không hợp lệ; dùng env.valueFrom riêng." }
]
```

### Tóm tắt

ConfigMap và Secret chỉ là hai object chứa dữ liệu. Giá trị thật nằm ở cách bạn đưa chúng vào Pod: env thì đơn giản nhưng đóng băng, volume thì linh hoạt nhưng app phải tự đọc lại file. Và nhớ: Secret mount dưới dạng volume được lưu trong {{c6::tmpfs}} (RAM) trên node, không ghi xuống đĩa.

### Bài tiếp theo

Cấu hình thì đã tách khỏi image, nhưng dữ liệu thật của ứng dụng (database, upload) thì chưa. Bài sau: **Volume, PersistentVolume, PersistentVolumeClaim và StorageClass**, nơi chúng ta giải bài toán dữ liệu sống lâu hơn Pod.
