Trên Cloud VPS, mỗi site Laravel có một file .env nằm cạnh code, được pipeline dựng bằng một loạt lệnh sed. Khi Website AI chuyển sang một image dùng chung cho mọi site, file đó không thể nằm trong image nữa. Kubernetes tách cấu hình ra hai loại object: ConfigMap cho giá trị thông thường và Secret cho thông tin nhạy cảm. Bài này trình bày cách chia .env của Laravel vào hai nơi, cách nạp vào pod bằng envFrom hoặc mount thành file, vì sao Secret chỉ là base64 chứ không phải mã hoá, phân quyền RBAC, và cái bẫy kinh điển: sửa ConfigMap xong mà pod vẫn chạy cấu hình cũ.
1. Từ file .env đến ConfigMap
Pipeline deploy trên Cloud VPS trong bài deploy tự động với GitLab Runner và SSH tạo .env bằng cách copy .env.example rồi sed từng dòng: DB_HOST, DB_DATABASE, DB_PASSWORD, APP_URL… Cách này gắn cấu hình vào đĩa của một máy cụ thể. Như đã nói ở bài Docker hoá Laravel, image Website AI không được chứa .env, và cấu hình phải đi vào container dưới dạng biến môi trường lúc chạy.
ConfigMap là object lưu các cặp key-value dạng chuỗi, nằm trong namespace của site. Có thể tạo ConfigMap từ YAML, hoặc sinh nhanh từ một file định dạng giống .env:
kubectl create configmap site-a-example-com-config \
--from-env-file=site-a.env -n site-a-example-com \
--dry-run=client -o yaml > configmap.yamlCờ --dry-run=client -o yaml chỉ in ra YAML để xem lại và lưu vào repo, chưa gửi lên cluster. Tài liệu đầy đủ ở trang ConfigMaps trên kubernetes.io.
2. Chia .env của Laravel: ConfigMap hay Secret?
File .env.example của một ứng dụng Laravel như core Website AI thường có vài chục biến. Một quy tắc chia đơn giản và dễ kiểm tra: tên biến có PASSWORD, KEY, TOKEN, SECRET thì vào Secret, còn lại vào ConfigMap.
| Nhóm | Ví dụ biến | Đặt ở |
|---|---|---|
| Ứng dụng | APP_NAME, APP_ENV, APP_DEBUG, APP_URL, LOG_CHANNEL | ConfigMap |
| Kết nối database | DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME | ConfigMap |
| Driver | CACHE_DRIVER, SESSION_DRIVER, QUEUE_CONNECTION, FILESYSTEM_DISK | ConfigMap |
| Mail không nhạy cảm | MAIL_HOST, MAIL_PORT, MAIL_USERNAME | ConfigMap |
| Bí mật | APP_KEY, DB_PASSWORD, MAIL_PASSWORD, REDIS_PASSWORD, API_TOKEN | Secret |
Dòng MAIL_PASSWORD đáng được nhấn mạnh. Trong .env, nó nằm ngay dưới MAIL_HOST, MAIL_PORT, MAIL_USERNAME. Khi chia file bằng tay hoặc viết template sinh cấu hình, rất dễ chép nguyên khối MAIL_* vào ConfigMap cho gọn. Kết quả là mật khẩu email của site nằm trong ConfigMap, nơi mà quyền đọc thường được cấp rộng rãi cho dev. Hãy ghi rủi ro này vào checklist ngay từ đầu: mọi biến bí mật phải được rà lại khi review template.
Mẫu ConfigMap cho một site (giá trị là ví dụ):
apiVersion: v1
kind: ConfigMap
metadata:
name: site-a-example-com-config
namespace: site-a-example-com
data:
APP_NAME: "site-a"
APP_ENV: "production"
APP_DEBUG: "false"
APP_URL: "https://site-a.example.com"
LOG_CHANNEL: "stderr"
DB_CONNECTION: "mysql"
DB_HOST: "<db-host>"
DB_PORT: "3306"
DB_DATABASE: "site_a"
DB_USERNAME: "site_a"
CACHE_DRIVER: "file"
SESSION_DRIVER: "file"
QUEUE_CONNECTION: "database"Mọi giá trị trong data của ConfigMap phải là chuỗi. Viết DB_PORT: 3306 không có ngoặc kép sẽ bị API server từ chối vì YAML hiểu đó là số. LOG_CHANNEL: stderr dùng kênh có sẵn trong cấu hình logging của Laravel, đẩy log ra stderr để kubectl logs đọc được.
3. Secret chỉ là base64, không phải mã hoá
Secret trông giống ConfigMap, chỉ khác là giá trị trong data được mã base64. Nhiều người nhìn chuỗi base64 và nghĩ dữ liệu đã được mã hoá. Không phải vậy:
$ echo -n 'mat-khau-vi-du' | base64
bWF0LWtoYXUtdmktZHU=
$ echo 'bWF0LWtoYXUtdmktZHU=' | base64 -d
mat-khau-vi-duBase64 chỉ là cách biểu diễn để chứa được cả dữ liệu nhị phân như file chứng chỉ. Ai có quyền get Secret là đọc được giá trị gốc bằng một lệnh. Theo tài liệu Secret của Kubernetes, mặc định Secret được lưu trong etcd ở dạng không mã hoá, trừ khi cluster bật mã hoá lúc lưu (encryption at rest). Trên cluster managed, việc này do nhà cung cấp quyết định, cần kiểm tra trong tài liệu của họ.
Vậy Secret hơn ConfigMap ở đâu? Ở chỗ Kubernetes và các công cụ xung quanh đối xử với nó khác: có thể phân quyền đọc Secret riêng, Secret không bị in ra trong kubectl describe, kubelet giữ Secret trong bộ nhớ tạm thay vì ghi xuống đĩa node. Tách đúng loại là điều kiện để các lớp bảo vệ này có tác dụng. Khi viết Secret bằng YAML, dùng stringData để khỏi tự mã base64:
apiVersion: v1
kind: Secret
metadata:
name: site-a-example-com-secret
namespace: site-a-example-com
type: Opaque
stringData:
APP_KEY: "<...>"
DB_PASSWORD: "<...>"
MAIL_PASSWORD: "<...>"
API_TOKEN: "<...>"File này chứa bí mật thật, nên không được commit thẳng vào Git ở dạng thô. Cách quản lý Secret trong repo (Sealed Secrets, SOPS hoặc biến bí mật của CI) sẽ được bàn khi xây quy trình deploy.
4. Nạp ConfigMap và Secret vào pod
Cách 1: envFrom, nạp toàn bộ thành biến môi trường
Đây là cách tự nhiên nhất với Laravel, vì framework ưu tiên biến môi trường thật hơn file .env:
containers:
- name: web
image: <registry>/websiteai:<tag>
envFrom:
- configMapRef:
name: site-a-example-com-config
- secretRef:
name: site-a-example-com-secretNếu một key xuất hiện ở cả hai nguồn, nguồn khai báo sau thắng. Biến khai báo riêng trong env lại thắng cả envFrom. Muốn lấy lẻ một key, dùng env[].valueFrom.configMapKeyRef hoặc secretKeyRef.
Cách 2: mount thành file
ConfigMap và Secret cũng có thể mount thành file trong container. Ví dụ một ConfigMap chứa đoạn php.ini riêng cho site:
volumeMounts:
- name: php-ini
mountPath: /usr/local/etc/php/conf.d/zz-site.ini
subPath: zz-site.ini
volumes:
- name: php-ini
configMap:
name: site-a-example-com-phpCũng có thể mount nguyên một file .env từ Secret vào thư mục code. Cách này giúp chuyển từ VPS sang nhanh, nhưng trộn lẫn cấu hình thường và bí mật vào một chỗ. Khuyến nghị: dùng envFrom cho biến Laravel và chỉ mount file cho những thứ vốn là file, như php.ini.
Cẩn thận với config:cache
Nếu image chạy php artisan config:cache lúc build, Laravel ghi giá trị cấu hình tại thời điểm build vào bootstrap/cache/config.php, và biến môi trường truyền vào lúc chạy sẽ bị bỏ qua. Muốn cache cấu hình, hãy chạy lệnh đó khi container khởi động, sau khi biến môi trường đã có mặt.
5. Pod không tự nhận ConfigMap mới
Đây là điều làm nhiều người bất ngờ nhất. Sửa ConfigMap bằng kubectl edit hay kubectl apply xong, site vẫn chạy với giá trị cũ:
- Biến môi trường được đọc một lần khi container khởi động. Cấu hình đổi sau đó không ảnh hưởng tới tiến trình đang chạy. PHP-FPM, queue worker vẫn giữ giá trị cũ.
- File mount từ ConfigMap (không dùng
subPath) được kubelet cập nhật sau một khoảng trễ, có thể tới khoảng một phút hoặc hơn. File mount bằngsubPaththì không bao giờ được cập nhật.
Deployment cũng không biết cấu hình đã đổi, vì phần template của nó không đổi. Có ba cách xử lý:
- Khởi động lại thủ công:
kubectl rollout restart deployment/site-a-example-com -n site-a-example-com. Pod được thay cuốn chiếu, pod mới đọc ConfigMap mới. - Gắn mã băm cấu hình vào pod template: thêm một annotation chứa hash nội dung cấu hình. Cấu hình đổi thì hash đổi, template đổi, Deployment tự rolling update. Công cụ sinh YAML nào cũng làm được việc này, nên đưa nó vào yêu cầu bắt buộc của quy trình deploy.
- ConfigMap bất biến có phiên bản: đặt tên kèm phiên bản (
site-a-config-v2), đánh dấuimmutable: true, rồi sửa Deployment trỏ sang tên mới. Đổi tham chiếu là đổi template, rollout diễn ra tự nhiên.
6. RBAC: ai được đọc ConfigMap, ai được đọc Secret
RBAC cho phép cấp quyền theo namespace. Một Role cho dev xem được pod, log, ConfigMap nhưng không xem được Secret:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: dev-read
namespace: site-a-example-com
rules:
- apiGroups: [""]
resources: ["pods", "pods/log", "configmaps", "services"]
verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch"]Ba lỗ hổng phân quyền cần nhớ: quyền list Secret trả về luôn nội dung, không chỉ tên; ai được tạo pod trong namespace thì có thể mount bất kỳ Secret nào của namespace đó; và quyền pods/exec cho phép vào container đọc biến môi trường, tức là đọc được giá trị lấy từ Secret. Đó cũng là lý do bí mật đặt nhầm vào ConfigMap trở nên nguy hiểm: Role "chỉ đọc" ở trên đã đủ để thấy nó.
7. Lỗi thường gặp với ConfigMap và Secret
CreateContainerConfigError: pod tham chiếu tới ConfigMap hoặc Secret chưa tồn tại, hoặc thiếu key. Tạo cấu hình trước Deployment, hoặc đánh dấuoptional: truenếu thật sự không bắt buộc.- Giá trị số không có ngoặc kép trong ConfigMap: API server báo lỗi kiểu dữ liệu.
- Thừa ký tự xuống dòng:
echo 'x' | base64mã cả ký tự xuống dòng ở cuối, khác vớiecho -n. Mật khẩu database thừa một ký tự là lỗi đăng nhập khó tìm. Tạo Secret bằng--from-filecũng gặp chuyện tương tự nếu file kết thúc bằng dòng trống. - Quá lớn: mỗi object loại này giới hạn 1 MiB. Đừng nhét file lớn vào đây.
- Sửa rồi mà không thấy tác dụng: xem lại mục 5.
8. Đánh đổi và khuyến nghị
Tách cấu hình khỏi image là bước bắt buộc để một image phục vụ được mọi site Website AI, và hai loại object này làm việc đó gọn hơn nhiều so với sed vào .env. Cái giá là phải kỷ luật: chia đúng loại, không đặt bí mật vào ConfigMap, nhớ rằng base64 không phải mã hoá, và luôn có cơ chế khởi động lại pod khi cấu hình đổi. Nếu bỏ qua điểm cuối, sẽ có ngày đổi máy database cho một site mà pod vẫn kết nối vào máy cũ. Bộ khung Namespace, Deployment, Service từ bài Pod, Deployment, Service và Namespace giờ đã có thêm cấu hình.
Bài tiếp theo
Volume, PersistentVolume, PVC và StorageClass trên Kubernetes. File upload, theme và plugin của từng site sẽ nằm ở đâu khi container chỉ là tạm thời.
