Configuration and SecretsConfiguration and Secrets
Thuộc bộ tài liệu kiến thức theo Kubernetes Roadmap.
Tổng quan
Một ứng dụng không chỉ là image của nó. Cùng một container cần database URL, feature flag, log level, API key và mức tài nguyên khác nhau tùy theo nơi nó chạy. Nguyên tắc twelve-factor là tách cấu hình ra khỏi image và inject lúc runtime — để một artifact bất biến (immutable) được promote sạch sẽ từ dev sang staging rồi prod, chỉ khác nhau ở phần config gắn vào. Kubernetes cung cấp hai object hạng nhất cho việc này: ConfigMap cho cấu hình không nhạy cảm và Secret cho dữ liệu nhạy cảm, cả hai đều có thể inject dưới dạng environment variable hoặc file mount.
Bên cạnh việc inject, Kubernetes quản lý mức tài nguyên trên node mà một workload được phép tiêu thụ thông qua resource requests và limits. Requests là phần đặt trước (reservation) mà scheduler tôn trọng; limits là trần mà kubelet và kernel cưỡng chế. Kết hợp với QoS class, LimitRange và ResourceQuota, chúng quyết định Pod được đặt ở đâu, có bị throttle không, và ai bị evict trước khi node quá tải. Configuration và resource management là hai nửa của cùng một câu hỏi: workload này cần gì để chạy đúng và an toàn?
Trang này bao quát cả hai: cách mô hình hóa cấu hình và secret, vì sao Secret không phải là mã hóa mặc định, cách bật mã hóa at rest thật sự, cách downward API cung cấp metadata của Pod cho app, và cách requests/limits/QoS/quota định hình scheduling và eviction. Chủ đề xuyên suốt là các object này rẻ để tạo và dễ dùng sai — chi tiết (immutability, base64 vs mã hóa, ngữ nghĩa cập nhật env-vs-volume, các bậc QoS) mới là nơi sự cố production ẩn nấp.
Một mô hình tư duy
Hãy hình dung một Pod được lắp ráp từ ba đầu vào lúc admission: image (mã nguồn), config (ConfigMap, Secret, downward API — môi trường), và hợp đồng tài nguyên (requests và limits — ngân sách). Đổi image là đổi hành vi; đổi config là đổi môi trường; đổi hợp đồng là đổi nơi nó chạy và cách nó chết dưới áp lực. Giữ ba mối quan tâm này tách biệt — không bao giờ nhét config hay secret vào image, không bao giờ để trống hợp đồng tài nguyên — là điều làm cho workload Kubernetes có tính portable, tái lập được và dự đoán được.
Kiến thức nền tảng
ConfigMap vs Secret nhìn nhanh
| Khía cạnh | ConfigMap | Secret |
|---|---|---|
| Dữ liệu dự kiến | Config không nhạy cảm: URL, flag, tuning, cả file cấu hình | Dữ liệu nhạy cảm: password, token, khóa TLS, credential registry |
| Lưu trong etcd | Plaintext | Base64-encoded (KHÔNG mã hóa trừ khi bạn bật) |
| Giới hạn kích thước | ~1 MiB mỗi object | ~1 MiB mỗi object |
| Cách dùng | env var, envFrom, mount volume, command args | env var, envFrom, mount volume, imagePullSecrets |
| Có type | Không (data / binaryData) | Có (trường type: — Opaque, TLS, dockerconfigjson…) |
| Vệ sinh RBAC | Bình thường | Nên siết chặt; thường được audit riêng |
| Cập nhật khi mount | Có (volume tự cập nhật) | Có (volume tự cập nhật) |
| Cập nhật qua env var | Không (chốt lúc Pod khởi động) | Không (chốt lúc Pod khởi động) |
Bề mặt API gần như y hệt nhau — Secret về bản chất là “một ConfigMap được platform đối xử cẩn thận hơn”. Sự cẩn thận đó mới là điều quan trọng: siết RBAC, mã hóa at rest, và không in chúng ra log.
ConfigMap dưới dạng environment variable
Hai kiểu inject. Từng key riêng với valueFrom, hoặc cả ConfigMap với envFrom:
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
LOG_LEVEL: "info"
FEATURE_X_ENABLED: "true"
DATABASE_HOST: "postgres.default.svc.cluster.local"
---
# Trong Pod spec:
containers:
- name: app
image: ghcr.io/example/app:1.4.2
env:
- name: LOG_LEVEL # đổi tên / chọn từng key riêng
valueFrom:
configMapKeyRef: { name: app-config, key: LOG_LEVEL }
envFrom:
- configMapRef: { name: app-config } # inject mọi key giữ nguyên
prefix: APP_ # tùy chọn: APP_LOG_LEVEL, ...
Dùng valueFrom khi muốn đổi tên một key hoặc lấy một giá trị; dùng envFrom để inject cả map. Ngữ nghĩa then chốt: environment variable được phân giải một lần duy nhất, lúc container khởi động. Sửa ConfigMap sau đó không làm thay đổi env của container đang chạy — bạn phải tạo Pod mới (rollout) thì nó mới có hiệu lực. Điều này khiến rất nhiều người bất ngờ.
ConfigMap dưới dạng mount volume
Mount ConfigMap thành các file — lý tưởng cho file cấu hình (nginx.conf, application.yaml) mà app đọc từ đĩa:
volumes:
- name: config
configMap:
name: app-config
items: # tùy chọn: chiếu key cụ thể ra path cụ thể
- key: nginx.conf
path: nginx.conf
containers:
- name: app
volumeMounts:
- name: config
mountPath: /etc/app
readOnly: true
Mỗi key trở thành một file dưới mountPath với nội dung là giá trị. ConfigMap mount volume CÓ cập nhật tại chỗ — kubelet đồng bộ thay đổi (có độ trễ định kỳ, tới ~1 phút cộng cache TTL). Nhưng file thay đổi không đồng nghĩa app nhận ra: process của bạn phải watch file hoặc hỗ trợ tín hiệu reload. Mount kiểu subPath (mount một key duy nhất vào thư mục có sẵn) là ngoại lệ — nó không nhận cập nhật, một cái bẫy phổ biến.
Immutable ConfigMap và Secret
Đặt immutable: true sẽ đóng băng object: không thể cập nhật nữa, chỉ có thể xóa và tạo lại. Điều này có hai lợi ích — bảo vệ khỏi thay đổi vô tình lan tới mọi Pod tiêu thụ, và cải thiện hiệu năng cluster ở quy mô lớn vì kubelet ngừng watch các object immutable để tìm thay đổi, giảm tải apiserver. Kết hợp immutability với tên chứa content-hash (app-config-a1b2c3, như configMapGenerator của Kustomize làm) để một thay đổi config tạo ra object mới và một rollout tự nhiên.
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config-a1b2c3
immutable: true
data:
LOG_LEVEL: "info"
Các loại Secret
Secret mang một type gợi ý nội dung và, với các type dựng sẵn, cưỡng chế các key bắt buộc:
| Type | Key | Dùng cho |
|---|---|---|
Opaque | tùy ý | Mặc định; secret key/value chung |
kubernetes.io/tls | tls.crt, tls.key | Chứng chỉ TLS + private key (Ingress, mTLS) |
kubernetes.io/dockerconfigjson | .dockerconfigjson | Credential pull registry private (imagePullSecrets) |
kubernetes.io/basic-auth | username, password | Credential basic auth |
kubernetes.io/ssh-auth | ssh-privatekey | SSH private key |
kubernetes.io/service-account-token | dữ liệu token | Token ServiceAccount (phần lớn đã bị thay bởi projected token) |
bootstrap.kubernetes.io/token | token id/secret | Token bootstrap node |
Base64 là encoding, KHÔNG phải mã hóa
Đây là sự thật bị hiểu lầm nhiều nhất về Secret. Trường data được base64-encode, không phải mã hóa — bất kỳ ai kubectl get secret -o yaml được hay đọc được etcd đều giải mã trivial:
kubectl create secret generic db --from-literal=password='s3cr3t'
kubectl get secret db -o jsonpath='{.data.password}' | base64 -d # -> s3cr3t
Base64 chỉ tồn tại để dữ liệu binary sống sót qua truyền tải YAML/JSON. Nó cung cấp zero tính bảo mật. Bảo vệ thật đến từ ba kiểm soát trực giao: (1) RBAC để ít chủ thể đọc được Secret, (2) mã hóa at rest để etcd trên đĩa là ciphertext, và (3) không lộ Secret trong log, env dump hay Git. (Dùng stringData khi soạn để viết plaintext mà API server tự base64-encode cho bạn.)
Mã hóa at rest qua EncryptionConfiguration
Mặc định, Secret nằm trong etcd chỉ base64. Bật mã hóa at rest bằng cách truyền một EncryptionConfiguration vào flag --encryption-provider-config của API server:
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources: ["secrets", "configmaps"]
providers:
- kms: # TỐT NHẤT: envelope encryption qua external KMS (KMS v2)
apiVersion: v2
name: myKmsPlugin
endpoint: unix:///var/run/kmsplugin/socket.sock
- aescbc: # hoặc khóa AES local (key material nằm trên control plane)
keys:
- name: key1
secret: <base64-32-byte-key>
- identity: {} # dự phòng: không mã hóa (cần cho việc đọc khi migrate)
Thứ tự provider quan trọng: provider đầu tiên mã hóa các lần ghi mới; tất cả provider liệt kê đều được thử để giải mã. Provider identity nghĩa là plaintext — giữ nó cuối cùng (hoặc bỏ đi) khi đã migrate xong. Vì mã hóa diễn ra lúc ghi, Secret hiện có vẫn ở dạng cũ cho tới khi được ghi lại: sau khi bật, chạy kubectl get secrets -A -o json | kubectl replace -f - để buộc re-encrypt. Ưu tiên KMS v2 provider (envelope encryption dựa trên cloud KMS hoặc HSM) hơn khóa local tĩnh — nó giữ root key material khỏi control plane và hỗ trợ rotation.
env vs envFrom
env (với valueFrom) | envFrom | |
|---|---|---|
| Độ hạt | Một biến mỗi lần | Mọi key trong nguồn |
| Đổi tên | Có (name ≠ key nguồn) | Không (key thành tên biến, prefix tùy chọn) |
| Key thiếu | Lỗi nếu key vắng (trừ khi optional: true) | Bỏ qua âm thầm |
| Tên env không hợp lệ | N/A | Key không phải env identifier hợp lệ bị bỏ qua kèm event |
| Tốt nhất cho | Vài giá trị chính xác, trộn nguồn | Inject hàng loạt một config map bạn kiểm soát |
env còn cho phép tham chiếu env var khác bằng nội suy $(VAR) và lấy từ downward API — những khả năng envFrom không có.
Downward API
Downward API cung cấp metadata của Pod và container cho chính các container, không cần hardcode hay truy vấn API server. Hai cơ chế: fieldRef (trường Pod như name, namespace, IP, node) và resourceFieldRef (requests/limits của chính container).
env:
- name: POD_NAME
valueFrom: { fieldRef: { fieldPath: metadata.name } }
- name: POD_NAMESPACE
valueFrom: { fieldRef: { fieldPath: metadata.namespace } }
- name: NODE_NAME
valueFrom: { fieldRef: { fieldPath: spec.nodeName } }
- name: POD_IP
valueFrom: { fieldRef: { fieldPath: status.podIP } }
- name: MEMORY_LIMIT # cấp cho app limit của chính nó (sizing heap JVM/Go)
valueFrom:
resourceFieldRef: { containerName: app, resource: limits.memory, divisor: 1Mi }
volumes:
- name: podinfo # hoặc lộ label/annotation dưới dạng file
downwardAPI:
items:
- path: labels
fieldRef: { fieldPath: metadata.labels }
Dùng phổ biến: báo cho runtime ngôn ngữ biết memory limit thật của nó (để chương trình JVM/Go tính kích thước heap đúng thay vì thấy cả node), inject POD_NAME/NODE_NAME vào log và trace, và định danh từng Pod cho clustering. Label và annotation chỉ có qua dạng volume, không qua env var fieldRef.
Khái niệm chính
Resource requests và limits
Mỗi container có thể khai báo requests (mức tối thiểu được đảm bảo) và limits (trần cứng) cho CPU và memory:
resources:
requests:
cpu: "250m" # 0.25 vCPU core, được đảm bảo & dùng để scheduling
memory: "256Mi" # 256 mebibyte, đặt trước
limits:
cpu: "1" # throttle khi vượt 1 core
memory: "512Mi" # OOM-kill khi vượt 512Mi
Đơn vị CPU: 1 = một vCPU/core; 500m = 500 millicore = nửa core. CPU nén được (compressible) — vượt limit sẽ throttle container (nó chậm lại) chứ không kill. Đơn vị memory: Mi/Gi là nhị phân (1 Mi = 1024²), M/G là thập phân (1 M = 10⁶) — trộn lẫn chúng là một lỗi sizing kinh điển. Memory không nén được (incompressible) — vượt limit sẽ bị kernel OOM-kill và restart.
Hai núm vặn làm việc khác nhau: requests điều khiển scheduling (scheduler cộng requests, không phải limits hay usage thực, để quyết định Pod có vừa node không), còn limits điều khiển cưỡng chế runtime (throttle cho CPU, OOM-kill cho memory). Một Pod có thể request ít rồi burst lên tới limit, nhưng nó chỉ được đảm bảo phần requests.
QoS class
Kubernetes suy ra một Quality of Service class từ cách đặt requests và limits. Không thể đặt trực tiếp; nó quyết định thứ tự ưu tiên eviction khi node bị áp lực memory.
| QoS class | Điều kiện | Thứ tự eviction dưới áp lực node |
|---|---|---|
| Guaranteed | Mọi container đặt requests == limits cho cả CPU và memory | Evict cuối cùng — được bảo vệ cao nhất |
| Burstable | Ít nhất một container có request hoặc limit, nhưng chưa đủ Guaranteed | Evict ở giữa, kẻ vượt-request nhiều nhất trước |
| BestEffort | Không request/limit ở đâu cả | Evict đầu tiên — không được bảo vệ |
Pod Guaranteed an toàn nhất và được CPU độc quyền (với static CPU manager policy) — hãy dành cho workload nhạy latency, đã được đo đạc kỹ. BestEffort dành cho batch thực sự có thể vứt bỏ. Đa số workload thực là Burstable: đặt requests theo nhu cầu ổn định và limits theo trần burst an toàn. Dưới áp lực memory, kubelet xếp hạng ứng viên eviction theo QoS rồi theo usage tương đối so với requests — một Pod Burstable vượt request nhiều sẽ bị evict trước một Pod gần request của nó.
LimitRange
Một LimitRange đặt default và biên độ per-namespace cho container/Pod, để những Pod bỏ sót resource vẫn có giá trị hợp lý và không ai request lượng vô lý:
apiVersion: v1
kind: LimitRange
metadata: { name: defaults, namespace: team-a }
spec:
limits:
- type: Container
default: # áp dụng làm limits nếu container bỏ sót
cpu: "500m"
memory: "256Mi"
defaultRequest: # áp dụng làm requests nếu bỏ sót
cpu: "100m"
memory: "128Mi"
max: { cpu: "2", memory: "2Gi" } # từ chối bất cứ gì vượt trên
min: { cpu: "50m", memory: "64Mi" } # từ chối bất cứ gì dưới ngưỡng
Đây là cách bạn loại bỏ Pod BestEffort trong một namespace: defaultRequest đảm bảo mọi container có requests. LimitRange hoạt động lúc admission — Pod vi phạm bị từ chối thẳng.
ResourceQuota
Một ResourceQuota giới hạn tiêu thụ tổng hợp per-namespace — tổng CPU/memory request, số lượng object, storage. Đây là rào chắn multi-tenancy chính:
apiVersion: v1
kind: ResourceQuota
metadata: { name: team-a-quota, namespace: team-a }
spec:
hard:
requests.cpu: "10"
requests.memory: "20Gi"
limits.cpu: "20"
limits.memory: "40Gi"
pods: "50"
persistentvolumeclaims: "10"
count/secrets: "25"
Một quy tắc tinh tế nhưng quan trọng: một khi có ResourceQuota về CPU/memory trong namespace, mọi Pod phải khai báo requests/limits tương ứng hoặc bị từ chối. Đây là lý do quota và LimitRange thường được triển khai cùng nhau — LimitRange cung cấp default để developer không bị buộc phải annotate từng Pod.
External secrets: một điểm chỉ hướng
Secret native giải quyết lưu trữ và inject nhưng không giải quyết vòng đời: rotation, audit tập trung, và giữ plaintext khỏi Git cho GitOps. Ba cách tiếp cận phổ biến nằm ở tầng trên:
- External Secrets Operator (ESO) — một controller đồng bộ từ store bên ngoài (AWS Secrets Manager, Vault, GCP Secret Manager, Azure Key Vault) vào Secret native của Kubernetes qua CRD
ExternalSecret. Nguồn sự thật ở bên ngoài; ESO reconcile và rotate. - Sealed Secrets (Bitnami) — mã hóa một Secret phía client bằng public key của controller thành CRD
SealedSecretan toàn để commit lên Git. Chỉ controller trong cluster mới giải mã được. Tuyệt cho GitOps khi bạn muốn secret nằm trong repo nhưng đã mã hóa. - SOPS (Mozilla) + age/KMS — mã hóa các giá trị YAML tại chỗ; giải mã lúc apply (thường qua tích hợp SOPS của
kustomize-controllertrong Flux hoặc plugin Helm). Hạt mịn, hoạt động với bất kỳ KMS hay khóaagenào.
Nguyên tắc: ESO khi một secret manager thật là nguồn sự thật; Sealed Secrets hoặc SOPS khi Git phải là nguồn sự thật nhưng không được chứa plaintext.
Configuration reloading
Vì config inject qua env bị đóng băng lúc Pod khởi động và config mount volume cập nhật file nhưng không cập nhật process, thay đổi config cần một chiến lược để có hiệu lực:
- Roll Deployment — cách sạch nhất và audit tốt nhất. Đổi config, rồi trigger rollout để Pod mới nhận. Reloader (Stakater) tự động hóa việc này: annotate một Deployment và nó restart Pod khi ConfigMap/Secret được tham chiếu thay đổi.
- Content-hash tên — đặt tên ConfigMap/Secret theo hash nội dung (
configMapGeneratorcủa Kustomize làm vậy). Thay đổi nội dung tạo tên object mới, làm đổi Pod template, tự động trigger rollout. Ghép hoàn hảo vớiimmutable: true. - Reload trong app — app watch file mount (inotify) hoặc chấp nhận endpoint
SIGHUP/reload (nginx, Prometheus). Không restart, nhưng app phải hỗ trợ và mountsubPathsẽ không cập nhật.
Ví dụ hoàn chỉnh: Deployment tiêu thụ ConfigMap + Secret + resources
apiVersion: v1
kind: ConfigMap
metadata: { name: web-config }
data:
LOG_LEVEL: "info"
UPSTREAM_URL: "http://api.default.svc.cluster.local"
---
apiVersion: v1
kind: Secret
metadata: { name: web-secret }
type: Opaque
stringData: # soạn dạng plaintext; API server tự base64-encode
DB_PASSWORD: "s3cr3t-rotate-me"
---
apiVersion: apps/v1
kind: Deployment
metadata: { name: web }
spec:
replicas: 3
selector: { matchLabels: { app: web } }
template:
metadata: { labels: { app: web } }
spec:
containers:
- name: web
image: ghcr.io/example/web:1.4.2
envFrom:
- configMapRef: { name: web-config } # config không nhạy cảm hàng loạt
env:
- name: DB_PASSWORD # một giá trị nhạy cảm đơn lẻ
valueFrom:
secretKeyRef: { name: web-secret, key: DB_PASSWORD }
- name: POD_NAME
valueFrom: { fieldRef: { fieldPath: metadata.name } }
resources:
requests: { cpu: "250m", memory: "256Mi" }
limits: { cpu: "1", memory: "512Mi" } # QoS Burstable
volumeMounts:
- name: tls
mountPath: /etc/tls
readOnly: true
volumes:
- name: tls
secret:
secretName: web-tls # Secret kubernetes.io/tls mount thành file
Pod này là Burstable (requests < limits), lấy config hàng loạt qua envFrom, một secret qua secretKeyRef, tên của chính nó qua downward API, và chứng chỉ TLS dưới dạng file mount. Trong thực tế, password là ứng viên cho ESO/Sealed Secrets thay vì stringData plaintext.
Best Practices
- Không bao giờ nhét config hay secret vào image — inject lúc runtime qua ConfigMap/Secret để một image bất biến được promote nguyên vẹn qua các môi trường; build lại image cho mỗi môi trường phá hỏng tính tái lập và audit.
- Luôn đặt resource requests, và đặt dựa trên dữ liệu — requests là thứ scheduler dùng để đặt Pod; thiếu chúng thì scheduler mù, packing dẫn tới eviction, và Pod rơi xuống BestEffort. Dựa vào usage quan sát được (khuyến nghị VPA, metrics), không đoán mò.
- Đặt memory limit bằng requests cho workload quan trọng — memory không nén được; làm cho Pod nhạy latency thành Guaranteed để bảo vệ khỏi eviction. Với CPU, ưu tiên chỉ-requests hoặc limit rộng rãi vì throttle làm hại tail latency.
- Coi base64 là encoding, không phải bảo mật — đừng bao giờ cho rằng Secret được bảo vệ vì nó “trông có vẻ đã mã hóa”; siết RBAC, bật mã hóa at rest, và giữ Secret khỏi log và Git.
- Bật mã hóa at rest với KMS v2 provider — envelope encryption dựa trên cloud KMS/HSM giữ root key khỏi control plane và hỗ trợ rotation; re-encrypt Secret hiện có sau khi bật, và chỉ giữ
identitycuối cùng trong lúc migrate. - Siết chặt truy cập Secret bằng RBAC — cấp
get/listtrên Secret cho càng ít ServiceAccount và user càng tốt; một quyềnlist secretstrong namespace về cơ bản là trao mọi credential trong đó. - Ưu tiên Secret mount volume hơn env var cho dữ liệu nhạy cảm — env var rò rỉ vào process con, crash dump,
/proc, và log dump môi trường; file mount hẹp hơn và cập nhật tại chỗ. - Dùng workflow external secrets cho vòng đời — External Secrets Operator khi secret manager là nguồn sự thật, Sealed Secrets hoặc SOPS khi Git phải là, để plaintext không bao giờ vào repo và rotation được tập trung.
- Làm ConfigMap/Secret immutable và content-hash tên chúng — immutability ngăn thay đổi vô tình toàn fleet và giảm tải watch của apiserver; tên hash biến thay đổi config thành rollout tự động, truy vết được.
- Tự động hóa rollout khi config thay đổi — nhớ rằng env var bị đóng băng lúc Pod khởi động; dùng Reloader, tên hash, hoặc
kubectl rollout restarttường minh để thay đổi config thực sự đến được Pod đang chạy. - Triển khai LimitRange và ResourceQuota cùng nhau per-namespace — quota giới hạn blast radius của tenant trong khi LimitRange cung cấp default để developer không bị buộc annotate từng Pod và không Pod BestEffort nào lọt lưới.
- Chú ý đơn vị memory —
Mi/Gi(nhị phân) vsM/G(thập phân) chênh ~5–7%; chuẩn hóa vềMi/Giđể tránh limit thiếu hụt ngầm gây OOM-kill bất ngờ. - Cấp cho app limit của chính nó qua downward API — truyền memory/CPU limit
resourceFieldRefđể runtime (JVM, Go, Node) tính kích thước heap và pool theo container, không phải cả node — một nguyên nhân hàng đầu gây OOM-kill container. - Tránh mount
subPathcho config bạn kỳ vọng reload — file ConfigMap/Secret mountsubPathkhông nhận cập nhật trực tiếp; mount cả volume nếu app watch file để phát hiện thay đổi. - Giữ Secret và ConfigMap nhỏ và một mục đích — giới hạn ~1 MiB là trần, không phải mục tiêu; blob lớn phình etcd và env của mọi Pod, và một map chung khổng lồ ghép các app không liên quan.
- Rotate secret và thiết kế cho việc đó — giả định mọi credential sẽ được rotate; dùng external manager có rotation, tham chiếu secret theo tên để version mới lan tới, và không bao giờ coi secret đã commit là an toàn dù đã mã hóa.
Tài liệu tham khảo
- ConfigMaps
- Secrets
- Encrypting Confidential Data at Rest
- Using a KMS provider for data encryption
- Distribute Credentials Securely Using Secrets
- Expose Pod Information to Containers Through the Downward API
- Managing Resources for Containers (requests & limits)
- Pod Quality of Service Classes
- Resource Quotas · Limit Ranges
- Node-pressure Eviction
- External Secrets Operator · Sealed Secrets · SOPS
- Reloader (Stakater)
- roadmap.sh — Kubernetes
Part of the Kubernetes Roadmap knowledge base.
Overview
An application is more than its image. The same container needs different database URLs, feature flags, log levels, API keys, and resource envelopes depending on where it runs. The twelve-factor principle is to keep configuration out of the image and inject it at runtime — so one immutable artifact promotes cleanly from dev to staging to prod, differing only by the config attached to it. Kubernetes provides two first-class objects for this: ConfigMap for non-sensitive configuration and Secret for sensitive data, both injectable as environment variables or mounted files.
Alongside injection, Kubernetes governs how much of the node a workload may consume through resource requests and limits. Requests are a reservation the scheduler honors; limits are a ceiling the kubelet and kernel enforce. Together with QoS classes, LimitRange, and ResourceQuota, they decide where a Pod lands, whether it gets throttled, and who gets evicted first when a node runs hot. Configuration and resource management are two halves of the same question: what does this workload need to run correctly and safely?
This page covers both: how to model configuration and secrets, why Secrets are not encryption by default, how to turn on real encryption at rest, how the downward API exposes Pod metadata to the app, and how requests/limits/QoS/quotas shape scheduling and eviction. The recurring theme is that these objects are cheap to create and easy to misuse — the details (immutability, base64 vs. encryption, env-vs-volume update semantics, QoS tiers) are where production incidents hide.
A mental model
Think of a Pod as being assembled from three inputs at admission time: the image (the code), the config (ConfigMaps, Secrets, downward API — the environment), and the resource contract (requests and limits — the budget). Change the image and you change behavior; change the config and you change environment; change the contract and you change where it runs and how it dies under pressure. Keeping these three concerns separate — never baking config or secrets into images, never leaving the resource contract blank — is what makes Kubernetes workloads portable, reproducible, and predictable.
Fundamentals
ConfigMap vs. Secret at a glance
| Aspect | ConfigMap | Secret |
|---|---|---|
| Intended data | Non-sensitive config: URLs, flags, tuning, whole config files | Sensitive data: passwords, tokens, TLS keys, registry creds |
| Storage in etcd | Plaintext | Base64-encoded (NOT encrypted unless you enable it) |
| Size limit | ~1 MiB per object | ~1 MiB per object |
| Consumption | env vars, envFrom, mounted volume, command args | env vars, envFrom, mounted volume, imagePullSecrets |
| Typed | No (data / binaryData) | Yes (type: field — Opaque, TLS, dockerconfigjson, …) |
| RBAC hygiene | Normal | Should be tightly restricted; often audited separately |
| Update while mounted | Yes (volume auto-updates) | Yes (volume auto-updates) |
| Update via env var | No (captured at Pod start) | No (captured at Pod start) |
The API surface is nearly identical — a Secret is essentially “a ConfigMap the platform treats with more care.” The care is what matters: RBAC scoping, encryption at rest, and not printing them into logs.
ConfigMap as environment variables
Two injection styles. Individual keys with valueFrom, or the whole ConfigMap with envFrom:
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
LOG_LEVEL: "info"
FEATURE_X_ENABLED: "true"
DATABASE_HOST: "postgres.default.svc.cluster.local"
---
# In the Pod spec:
containers:
- name: app
image: ghcr.io/example/app:1.4.2
env:
- name: LOG_LEVEL # rename/select individual keys
valueFrom:
configMapKeyRef: { name: app-config, key: LOG_LEVEL }
envFrom:
- configMapRef: { name: app-config } # inject every key as-is
prefix: APP_ # optional: APP_LOG_LEVEL, ...
Use valueFrom when you want to rename a key or pull one value; use envFrom to inject the whole map. Critical semantics: environment variables are resolved once, when the container starts. Editing the ConfigMap afterward does not change the running container’s env — you must trigger a new Pod (rollout) for it to take effect. This surprises people constantly.
ConfigMap as a mounted volume
Mount the ConfigMap as files — ideal for config files (nginx.conf, application.yaml) an app reads from disk:
volumes:
- name: config
configMap:
name: app-config
items: # optional: project specific keys to specific paths
- key: nginx.conf
path: nginx.conf
containers:
- name: app
volumeMounts:
- name: config
mountPath: /etc/app
readOnly: true
Each key becomes a file under mountPath whose contents are the value. Volume-mounted ConfigMaps DO update in place — the kubelet syncs changes (with a periodic delay, up to ~1 minute plus cache TTL). But the file changing is not the app noticing: your process must watch the file or support a reload signal. subPath mounts (mounting a single key into an existing directory) are the exception — they do not receive updates, a common gotcha.
Immutable ConfigMaps and Secrets
Setting immutable: true freezes the object: it can no longer be updated, only deleted and recreated. This has two benefits — it protects against accidental changes that would ripple through every consuming Pod, and it improves cluster performance at scale because the kubelet stops watching immutable objects for changes, reducing apiserver load. Combine immutability with content-hashed names (app-config-a1b2c3, as Kustomize’s configMapGenerator does) so a config change produces a new object and a natural rollout.
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config-a1b2c3
immutable: true
data:
LOG_LEVEL: "info"
Secret types
Secrets carry a type that hints at their contents and, for built-in types, enforces required keys:
| Type | Keys | Use |
|---|---|---|
Opaque | arbitrary | Default; generic key/value secrets |
kubernetes.io/tls | tls.crt, tls.key | TLS certificate + private key (Ingress, mTLS) |
kubernetes.io/dockerconfigjson | .dockerconfigjson | Private registry pull credentials (imagePullSecrets) |
kubernetes.io/basic-auth | username, password | Basic auth credentials |
kubernetes.io/ssh-auth | ssh-privatekey | SSH private key |
kubernetes.io/service-account-token | token data | ServiceAccount token (largely superseded by projected tokens) |
bootstrap.kubernetes.io/token | token id/secret | Node bootstrap tokens |
Base64 is encoding, NOT encryption
This is the single most misunderstood fact about Secrets. The data field is base64-encoded, not encrypted — anyone who can kubectl get secret -o yaml or read etcd can trivially decode it:
kubectl create secret generic db --from-literal=password='s3cr3t'
kubectl get secret db -o jsonpath='{.data.password}' | base64 -d # -> s3cr3t
Base64 exists only so binary data survives YAML/JSON transport. It provides zero confidentiality. Real protection comes from three orthogonal controls: (1) RBAC so few subjects can read Secrets, (2) encryption at rest so etcd on disk is ciphertext, and (3) not exposing Secrets in logs, env dumps, or Git. (Use stringData when authoring to write plaintext that the API server base64-encodes for you.)
Encryption at rest via EncryptionConfiguration
By default, Secrets sit in etcd base64-only. Turn on encryption at rest by passing an EncryptionConfiguration to the API server’s --encryption-provider-config flag:
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources: ["secrets", "configmaps"]
providers:
- kms: # BEST: envelope encryption via external KMS (KMS v2)
apiVersion: v2
name: myKmsPlugin
endpoint: unix:///var/run/kmsplugin/socket.sock
- aescbc: # or a local AES key (key material lives on the control plane)
keys:
- name: key1
secret: <base64-32-byte-key>
- identity: {} # fallback: no encryption (needed for reads during migration)
Provider order matters: the first provider encrypts new writes; all listed providers are tried for decryption. The identity provider means plaintext — keep it last (or absent) once migrated. Because encryption happens on write, existing Secrets stay in their old form until rewritten: after enabling, run kubectl get secrets -A -o json | kubectl replace -f - to force re-encryption. Prefer the KMS v2 provider (envelope encryption backed by a cloud KMS or HSM) over static local keys — it keeps root key material off the control plane and supports rotation.
env vs. envFrom
env (with valueFrom) | envFrom | |
|---|---|---|
| Granularity | One variable at a time | Every key in the source |
| Renaming | Yes (name ≠ source key) | No (key becomes var name, optional prefix) |
| Missing key | Fails if key absent (unless optional: true) | Silently skips |
| Invalid env names | N/A | Keys that aren’t valid env identifiers are skipped with an event |
| Best for | A few precise values, mixing sources | Bulk-injecting a config map you control |
env also lets you reference other env vars with $(VAR) interpolation and pull from the downward API — capabilities envFrom lacks.
The downward API
The downward API exposes Pod and container metadata to the containers themselves, without hardcoding or querying the API server. Two mechanisms: fieldRef (Pod fields like name, namespace, IP, node) and resourceFieldRef (the container’s own requests/limits).
env:
- name: POD_NAME
valueFrom: { fieldRef: { fieldPath: metadata.name } }
- name: POD_NAMESPACE
valueFrom: { fieldRef: { fieldPath: metadata.namespace } }
- name: NODE_NAME
valueFrom: { fieldRef: { fieldPath: spec.nodeName } }
- name: POD_IP
valueFrom: { fieldRef: { fieldPath: status.podIP } }
- name: MEMORY_LIMIT # feed the app its own limit (JVM/Go heap sizing)
valueFrom:
resourceFieldRef: { containerName: app, resource: limits.memory, divisor: 1Mi }
volumes:
- name: podinfo # or expose labels/annotations as files
downwardAPI:
items:
- path: labels
fieldRef: { fieldPath: metadata.labels }
Common uses: telling a language runtime its real memory limit (so a JVM/Go program sizes its heap correctly instead of seeing the whole node), injecting POD_NAME/NODE_NAME into logs and traces, and per-Pod identity for clustering. Labels and annotations are only available via the volume form, not fieldRef env vars.
Key Concepts
Resource requests and limits
Every container can declare requests (guaranteed minimum) and limits (hard ceiling) for CPU and memory:
resources:
requests:
cpu: "250m" # 0.25 of a vCPU core, guaranteed & used for scheduling
memory: "256Mi" # 256 mebibytes, reserved
limits:
cpu: "1" # throttled above 1 core
memory: "512Mi" # OOM-killed above 512Mi
CPU units: 1 = one vCPU/core; 500m = 500 millicores = half a core. CPU is compressible — exceeding the limit throttles the container (it slows down) but does not kill it. Memory units: Mi/Gi are binary (1 Mi = 1024²), M/G are decimal (1 M = 10⁶) — mixing them is a classic sizing bug. Memory is incompressible — exceeding the limit gets the container OOM-killed by the kernel and restarted.
The two knobs do different jobs: requests drive scheduling (the scheduler sums requests, not limits or actual usage, to decide if a Pod fits a node), while limits drive runtime enforcement (throttling for CPU, OOM-kill for memory). A Pod can request little and burst up to its limit, but it is only guaranteed its requests.
QoS classes
Kubernetes derives a Quality of Service class from how requests and limits are set. It cannot be set directly; it determines eviction priority when a node is under memory pressure.
| QoS class | Condition | Eviction order under node pressure |
|---|---|---|
| Guaranteed | Every container sets requests == limits for both CPU and memory | Evicted last — highest protection |
| Burstable | At least one container has a request or limit, but not Guaranteed | Evicted in the middle, worst-offenders-over-request first |
| BestEffort | No requests or limits anywhere | Evicted first — no protection |
Guaranteed Pods are the safest and get exclusive CPU (with the static CPU manager policy) — reserve it for latency-critical, well-characterized workloads. BestEffort is for truly disposable batch work. Most real workloads are Burstable: set requests to the steady-state need and limits to a safe burst ceiling. Under memory pressure, the kubelet ranks eviction candidates by QoS and then by usage relative to requests — a Burstable Pod far over its request is evicted before one near it.
LimitRange
A LimitRange sets per-namespace defaults and bounds for containers/Pods, so Pods that omit resources still get sane values and nobody can request absurd amounts:
apiVersion: v1
kind: LimitRange
metadata: { name: defaults, namespace: team-a }
spec:
limits:
- type: Container
default: # applied as limits if the container omits them
cpu: "500m"
memory: "256Mi"
defaultRequest: # applied as requests if omitted
cpu: "100m"
memory: "128Mi"
max: { cpu: "2", memory: "2Gi" } # reject anything above
min: { cpu: "50m", memory: "64Mi" } # reject anything below
This is how you eliminate BestEffort Pods in a namespace: a defaultRequest guarantees every container gets requests. LimitRange operates at admission — violating Pods are rejected outright.
ResourceQuota
A ResourceQuota caps aggregate consumption per namespace — total CPU/memory requested, object counts, storage. It is the primary multi-tenancy guardrail:
apiVersion: v1
kind: ResourceQuota
metadata: { name: team-a-quota, namespace: team-a }
spec:
hard:
requests.cpu: "10"
requests.memory: "20Gi"
limits.cpu: "20"
limits.memory: "40Gi"
pods: "50"
persistentvolumeclaims: "10"
count/secrets: "25"
A subtle but important rule: once a ResourceQuota on CPU/memory exists in a namespace, every Pod must specify the corresponding requests/limits or it is rejected. This is why quotas and LimitRanges are usually deployed together — the LimitRange supplies defaults so developers aren’t forced to annotate every Pod.
External secrets: a pointer
Native Secrets solve storage and injection but not lifecycle: rotation, central auditing, and keeping plaintext out of Git for GitOps. Three common approaches sit on top:
- External Secrets Operator (ESO) — a controller that syncs from an external store (AWS Secrets Manager, Vault, GCP Secret Manager, Azure Key Vault) into native Kubernetes Secrets via an
ExternalSecretCRD. The source of truth stays external; ESO reconciles and rotates. - Sealed Secrets (Bitnami) — encrypt a Secret client-side with the controller’s public key into a
SealedSecretCRD that is safe to commit to Git. Only the in-cluster controller can decrypt it. Great for GitOps where you want the secret in the repo but encrypted. - SOPS (Mozilla) + age/KMS — encrypt YAML values in place; decrypt at apply time (often via Flux’s
kustomize-controllerSOPS integration or Helm plugins). Fine-grained, works with any KMS oragekey.
Rule of thumb: ESO when a real secret manager is the source of truth; Sealed Secrets or SOPS when Git must be the source of truth but must never hold plaintext.
Configuration reloading
Because env-injected config is frozen at Pod start and volume-mounted config updates the file but not the process, changing config needs a strategy to take effect:
- Roll the Deployment — the cleanest, most auditable approach. Change the config, then trigger a rollout so new Pods pick it up. Reloader (Stakater) automates this: annotate a Deployment and it restarts Pods when a referenced ConfigMap/Secret changes.
- Content-hash the name — name ConfigMaps/Secrets by a hash of their contents (Kustomize’s
configMapGeneratordoes this). A content change yields a new object name, which changes the Pod template, which triggers a rollout automatically. Pairs perfectly withimmutable: true. - In-app reload — the app watches the mounted file (inotify) or accepts a
SIGHUP/reload endpoint (nginx, Prometheus). No restart, but the app must support it andsubPathmounts won’t update.
Worked example: Deployment consuming ConfigMap + Secret + resources
apiVersion: v1
kind: ConfigMap
metadata: { name: web-config }
data:
LOG_LEVEL: "info"
UPSTREAM_URL: "http://api.default.svc.cluster.local"
---
apiVersion: v1
kind: Secret
metadata: { name: web-secret }
type: Opaque
stringData: # authored as plaintext; API server base64-encodes
DB_PASSWORD: "s3cr3t-rotate-me"
---
apiVersion: apps/v1
kind: Deployment
metadata: { name: web }
spec:
replicas: 3
selector: { matchLabels: { app: web } }
template:
metadata: { labels: { app: web } }
spec:
containers:
- name: web
image: ghcr.io/example/web:1.4.2
envFrom:
- configMapRef: { name: web-config } # bulk non-sensitive config
env:
- name: DB_PASSWORD # single sensitive value
valueFrom:
secretKeyRef: { name: web-secret, key: DB_PASSWORD }
- name: POD_NAME
valueFrom: { fieldRef: { fieldPath: metadata.name } }
resources:
requests: { cpu: "250m", memory: "256Mi" }
limits: { cpu: "1", memory: "512Mi" } # Burstable QoS
volumeMounts:
- name: tls
mountPath: /etc/tls
readOnly: true
volumes:
- name: tls
secret:
secretName: web-tls # a kubernetes.io/tls Secret mounted as files
This Pod is Burstable (requests < limits), pulls bulk config via envFrom, one secret via secretKeyRef, its own name via the downward API, and TLS certs as mounted files. The password is a candidate for ESO/Sealed Secrets rather than plaintext stringData in real life.
Best Practices
- Never bake config or secrets into the image — inject at runtime via ConfigMaps/Secrets so one immutable image promotes unchanged across environments; a rebuilt image per environment defeats reproducibility and auditing.
- Always set resource requests, and set them from data — requests are what the scheduler uses to place Pods; without them the scheduler is blind, packing leads to evictions, and the Pod falls to BestEffort. Base them on observed usage (VPA recommendations, metrics), not guesses.
- Set memory limits equal to requests for critical workloads — memory is incompressible; making a latency-sensitive Pod Guaranteed protects it from eviction. For CPU, prefer requests-only or generous limits since throttling hurts tail latency.
- Treat base64 as encoding, not security — never assume a Secret is protected because it “looks encrypted”; lock down RBAC, enable encryption at rest, and keep Secrets out of logs and Git.
- Enable encryption at rest with a KMS v2 provider — envelope encryption backed by a cloud KMS/HSM keeps root keys off the control plane and supports rotation; re-encrypt existing Secrets after enabling, and keep
identitylast only during migration. - Scope Secret access tightly with RBAC — grant
get/liston Secrets to as few ServiceAccounts and users as possible; alist secretsgrant in a namespace effectively hands over every credential in it. - Prefer mounted-volume Secrets over env vars for sensitive data — env vars leak into child processes, crash dumps,
/proc, and logging that dumps the environment; mounted files are narrower and update in place. - Use an external secrets workflow for lifecycle — External Secrets Operator when a secret manager is the source of truth, Sealed Secrets or SOPS when Git must be, so plaintext never lands in a repo and rotation is centralized.
- Make ConfigMaps/Secrets immutable and content-hash their names — immutability prevents accidental fleet-wide changes and cuts apiserver watch load; hashed names turn a config change into an automatic, traceable rollout.
- Automate config-change rollouts — remember env vars are frozen at Pod start; use Reloader, hashed names, or an explicit
kubectl rollout restartso config changes actually reach running Pods. - Deploy LimitRange and ResourceQuota together per namespace — the quota caps a tenant’s blast radius while the LimitRange supplies defaults so developers aren’t forced to annotate every Pod and no BestEffort Pods slip through.
- Watch your memory units —
Mi/Gi(binary) vsM/G(decimal) differ by ~5–7%; standardize onMi/Gito avoid subtly under-provisioned limits that cause surprise OOM-kills. - Feed the app its own limits via the downward API — pass
resourceFieldRefmemory/CPU limits so runtimes (JVM, Go, Node) size heaps and pools to the container, not the whole node — a top cause of container OOM-kills. - Avoid
subPathmounts for config you expect to reload —subPath-mounted ConfigMap/Secret files do not receive live updates; mount the whole volume if the app watches files for changes. - Keep Secrets and ConfigMaps small and single-purpose — the ~1 MiB object limit is a ceiling, not a target; large blobs bloat etcd and every Pod’s env, and one giant shared map couples unrelated apps.
- Rotate secrets and design for it — assume every credential will be rotated; use external managers with rotation, reference secrets by name so a new version flows through, and never treat a committed secret as safe even if encrypted.
References
- ConfigMaps
- Secrets
- Encrypting Confidential Data at Rest
- Using a KMS provider for data encryption
- Distribute Credentials Securely Using Secrets
- Expose Pod Information to Containers Through the Downward API
- Managing Resources for Containers (requests & limits)
- Pod Quality of Service Classes
- Resource Quotas · Limit Ranges
- Node-pressure Eviction
- External Secrets Operator · Sealed Secrets · SOPS
- Reloader (Stakater)
- roadmap.sh — Kubernetes