Día 57 - Variables de entorno en Pods (env + $(VAR) substitution)
Problema / Desafío
El equipo de Nautilus está armando una app de "greetings". Hay que crear un Pod que pruebe el patrón de variables de entorno:
- Pod:
print-envars-greeting - Container:
print-env-container, imagenbash - Env vars:
GREETING = "Welcome to"COMPANY = "DevOps"GROUP = "Ltd"- Command exacto:
["/bin/sh", "-c", 'echo "$(GREETING) $(COMPANY) $(GROUP)"'] restartPolicy: Never— para que el container termine y no entre en CrashLoopBackOff (elechocorre una vez y sale con exit 0)- Verificación:
kubectl logs -f print-envars-greetingdebe imprimirWelcome to DevOps Ltd
Conceptos clave
Variables de entorno en Pods — las 4 formas
K8s ofrece varias formas de inyectar variables de entorno en un container, cada una con su caso de uso:
| Forma | Sintaxis | Cuándo usarla |
|---|---|---|
| Hardcoded inline | env: [{name: KEY, value: "val"}] |
Valores triviales que no son sensibles ni cambian (este lab) |
| Desde ConfigMap | env: [{name: KEY, valueFrom: {configMapKeyRef: {...}}}] |
Configuración no sensible compartida entre varios pods |
| Desde Secret | env: [{name: KEY, valueFrom: {secretKeyRef: {...}}}] |
Passwords, API keys, tokens — encriptados at-rest (con KMS) |
| Downward API | env: [{name: POD_IP, valueFrom: {fieldRef: {fieldPath: status.podIP}}}] |
Metadata del pod (nombre, namespace, IP, labels, etc.) |
| Import masivo | envFrom: [{configMapRef: {name: ...}}] o secretRef |
Volcar TODO un ConfigMap/Secret como env vars (Twelve-Factor App) |
Más allá: ¿de dónde más pueden venir las variables?
- External Secrets Operator → puente entre K8s Secrets y backends externos (AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault, 1Password, etc.). El operator lee del backend y crea/actualiza Secrets nativos
- HashiCorp Vault Agent Injector → mutating webhook que inyecta un sidecar que monta credenciales en un emptyDir desde Vault
- CSI Secret Store → driver de volumen que monta secretos como archivos (no env vars), permite rotación sin reiniciar el pod
- Argo CD / Helm values → la config viene de un git repo o un release de Helm; al deploy se renderiza el manifest con las env vars correctas
$(VAR) substitution: ¿qué hace K8s, qué hace el shell?
La consigna usa paréntesis ($(GREETING)) en vez de llaves (${GREETING}). No es lo mismo — y entender quién expande cuál es clave:
| Sintaxis | Quién expande | Cuándo ocurre |
|---|---|---|
$(VAR) |
kubelet (Kubernetes) | Antes de pasar el command al container — al crear el proceso |
${VAR} o $VAR |
Shell del container | En runtime, cuando el shell ejecuta el comando |
Para este lab, K8s ve:
command: ["/bin/sh", "-c", 'echo "$(GREETING) $(COMPANY) $(GROUP)"']
env: [GREETING="Welcome to", COMPANY="DevOps", GROUP="Ltd"]
K8s reemplaza $(GREETING), $(COMPANY), $(GROUP) con sus valores antes de crear el container. El proceso recibe como argv:
El shell del container nunca ve las variables — solo el string final. Por eso ves Command: echo "$(GREETING) $(COMPANY) $(GROUP)" en el describe (es el spec original) pero el output es Welcome to DevOps Ltd (el resultado tras la substitution).
¿Cuál es la diferencia práctica? Si usaras
${GREETING}en el comando, K8s no lo expande, y depende del shell.shlo expande, así que también funcionaría. Pero si la imagen no tuviera shell (comodistrolessoscratch),${VAR}no se expande y queda literal.$(VAR)siempre funciona porque la expansión la hace kubelet, no el shell — es la forma "K8s-native".
restartPolicy — el campo MÁS confundido del Pod spec
Este es probablemente el error de indentación más común en K8s. restartPolicy es un campo del Pod, NO del container:
vs:
El problema: K8s NO da error si se pone mal indentado — simplemente lo ignora silenciosamente y aplica el default (Always). Entonces el container completó, K8s lo reinició, completó otra vez, K8s lo reinició, y eventualmente entró en CrashLoopBackOff aunque el exit code haya sido 0 siempre.
Los 3 valores válidos de restartPolicy
| Valor | Comportamiento | Cuándo usarlo |
|---|---|---|
Always |
Reinicia el container siempre que termine (success o failure) | Default. Apps de larga vida (web servers, daemons) |
OnFailure |
Reinicia solo si termina con exit code ≠ 0 | Jobs y CronJobs que pueden fallar y deben reintentar |
Never |
Nunca reinicia, sin importar el exit code | Tareas one-shot que terminan limpio (este lab) |
Trampa importante:
restartPolicysolo aplica a Pods stand-alone. Si el Pod es parte de un Deployment, el RS controller crea pods nuevos cuando los viejos mueren — elrestartPolicysolo afecta al container DENTRO del Pod, no al ciclo de vida del Pod en sí.
Excepción: restartPolicy a nivel container (K8s 1.28+)
Desde K8s 1.28 sí existe un restartPolicy por container, pero solo es válido para init containers: es lo que diferencia un init container clásico (restartPolicy no especificado) de un native sidecar (restartPolicy: Always) — ver Día 55. Para containers regulares se ignora completamente.
El símbolo del bug: Restart Count: 2 con Reason: Completed
State: Terminated
Reason: Completed
Exit Code: 0
Restart Count: 2
Events:
Warning BackOff 9s (x3 over 22s) kubelet Back-off restarting failed container
Esta combinación es paradójica: el container completó exitosamente (exit 0) pero K8s lo está reintentando como si fuera un fallo. Es el síntoma clásico del restartPolicy mal puesto:
- K8s no recibió
Never(porque lo puso indentado mal) → aplica defaultAlways - Container termina con exit 0 (todo bien)
Alwayssignifica "reiniciá pase lo que pase" → K8s reinicia- A los 2-3 restarts seguidos en poco tiempo, K8s entra en exponential back-off
- El evento
BackOffaparece a pesar de que el exit code es 0 — la "Falla" desde el punto de vista de K8s es "intenté reiniciar 3 veces seguido"
Pasos
- Escribir el manifest con la indentación correcta de
restartPolicy kubectl apply -f pod.ymlkubectl logs -f print-envars-greetingpara ver el output- Verificar el estado final con
describe— debe serCompletedconRestart Count: 0
Comandos / Código
Manifest CORREGIDO
apiVersion: v1
kind: Pod
metadata:
name: print-envars-greeting
spec:
restartPolicy: Never # ← a nivel Pod (NO dentro del container)
containers:
- name: print-env-container
image: bash
command: ["/bin/sh", "-c", 'echo "$(GREETING) $(COMPANY) $(GROUP)"']
env:
- name: GREETING
value: "Welcome to"
- name: COMPANY
value: "DevOps"
- name: GROUP
value: "Ltd"
Diferencia con el manifest original: moví
restartPolicy: Neverde dentro decontainers[0]aspec.restartPolicy. El YAML del lab original lo tenía dentro del container — K8s lo ignoró silenciosamente y aplicóAlways.
Aplicar
Ver el output (el test del ejercicio)
-f (follow) acá no hace nada práctico porque el container termina inmediatamente — el output se imprime una sola vez. En containers de larga vida -f es streaming continuo.
describe (con restartPolicy: Never correcto)
Con la indentación correcta, este sería el estado esperado:
State: Terminated
Reason: Completed
Exit Code: 0
Restart Count: 0 ← cero restarts (con Never K8s no reintenta)
Environment:
GREETING: Welcome to
COMPANY: DevOps
GROUP: Ltd
describe que viste en el lab (con el bug de indentación)
Como en tu YAML original restartPolicy quedó dentro del container, K8s aplicó Always:
State: Terminated
Reason: Completed
Exit Code: 0
Last State: Terminated
Reason: Completed
Exit Code: 0
Restart Count: 2 ← K8s reinició 2 veces aunque el container terminó OK
Events:
Normal Pulled (x3) kubelet Successfully pulled image "bash"
Normal Created (x3) kubelet Created container: print-env-container
Normal Started (x3) kubelet Started container print-env-container
Warning BackOff (x3) kubelet Back-off restarting failed container
La línea (x3) en los events es la huella de los reintentos. El container se ejecutó 3 veces (cada vez imprimió "Welcome to DevOps Ltd"), y K8s estaba a punto de hacer un 4to intento con exponential backoff cuando vimos el describe.
Por qué K8s NO te avisa de la mala indentación: YAML es un lenguaje permisivo, y la API de K8s acepta campos extra en muchos schemas. Existe un mecanismo (
--validate=strict) que rechaza campos desconocidos, perokubectl applypor default es laxo (--validate=warn) — sólo avisa por stderr y aplica igual. Para detectar este tipo de error en CI: usarkubectl apply --validate=strict -f pod.yml.
Cómo NO confundir env vars con substitution
Diferentes formas de pasar las mismas 3 vars
Forma 1 — Hardcoded (la del lab)
env:
- name: GREETING
value: "Welcome to"
- name: COMPANY
value: "DevOps"
- name: GROUP
value: "Ltd"
Forma 2 — Desde un ConfigMap
# Primero crear el CM
kubectl create configmap greeting-config \
--from-literal=GREETING="Welcome to" \
--from-literal=COMPANY=DevOps \
--from-literal=GROUP=Ltd
# En el pod
env:
- name: GREETING
valueFrom:
configMapKeyRef: { name: greeting-config, key: GREETING }
- name: COMPANY
valueFrom:
configMapKeyRef: { name: greeting-config, key: COMPANY }
- name: GROUP
valueFrom:
configMapKeyRef: { name: greeting-config, key: GROUP }
Forma 3 — Import masivo con envFrom
K8s toma TODAS las keys del ConfigMap y las inyecta como env vars con el mismo nombre. Más limpio con 5+ vars. Ojo: si dos ConfigMaps tienen la misma key, K8s da un evento InvalidVariableNames y skipea las que colisionan.
Forma 4 — Downward API (metadata del pod)
env:
- name: MY_POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: MY_POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: MY_NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
Útil para apps que necesitan saber su propia identidad (logging, distributed tracing).
Troubleshooting
| Problema | Causa y solución |
|---|---|
Pod en CrashLoopBackOff aunque el container termina con exit 0 |
restartPolicy está mal indentado (dentro del container) o no se especificó. Moverlo a spec.restartPolicy: Never (o OnFailure) |
kubectl logs -f muestra el output pero el pod queda Running y reinicia |
Mismo: restartPolicy no es Never. Con Never el pod queda en Completed y no reinicia |
echo "$(GREETING) $(COMPANY)" imprime literalmente "$(GREETING) $(COMPANY)" |
La env var no existe (typo en name) o se está usando una sintaxis no soportada. K8s solo expande $(VAR), no $VAR |
kubectl describe pod ... Environment: está vacío |
Bloque env: mal indentado (probablemente fuera del container). Verificar con kubectl get pod <name> -o yaml |
Necesito una env var con el carácter $ literal |
Escapar con $$ en el value. value: "price $$10" → price $10 |
| Quiero referenciar una env var dentro de otra (chained) | K8s expande $(VAR1) solo si VAR1 fue declarada antes en el mismo env:. El orden de la lista importa |
Pod entra RunContainerError o CreateContainerConfigError |
La env var referencia un ConfigMap/Secret que no existe. kubectl describe pod muestra el detalle en Events |