Beyond ConfigMaps and Secrets, Kubernetes provides ways to inject Pod-specific runtime information into containers — the Pod's name, namespace, IP, node, resource limits, and labels. This is the Downward API: a way for Pods to learn about themselves without calling the API server.
1. All Environment Variable Sources
A container's env vars can come from five sources:
| Source | Field | Use Case |
|---|---|---|
| Static value | env.value | Hardcoded config, simple defaults |
| ConfigMap | env.valueFrom.configMapKeyRef | Non-sensitive config |
| Secret | env.valueFrom.secretKeyRef | Passwords, tokens |
| Downward API (field) | env.valueFrom.fieldRef | Pod metadata (name, namespace, IP, node) |
| Downward API (resource) | env.valueFrom.resourceFieldRef | Container resource limits/requests |
Complete Example: All Sources in One Pod
spec:
containers:
- name: app
image: myapp:latest
env:
# 1. Static value
- name: APP_ENV
value: production
# 2. From ConfigMap
- name: LOG_LEVEL
valueFrom:
configMapKeyRef:
name: app-config
key: LOG_LEVEL
# 3. From Secret
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-creds
key: password
# 4. Downward API — Pod field
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
# 5. Downward API — Resource field
- name: MEMORY_LIMIT
valueFrom:
resourceFieldRef:
containerName: app
resource: limits.memory
Dependent Environment Variables
env:
- name: DB_HOST
value: postgres.default.svc
- name: DB_PORT
value: "5432"
- name: DATABASE_URL
value: "postgresql://$(DB_HOST):$(DB_PORT)/mydb" # ← variable interpolation!
# K8s resolves $(VAR_NAME) from previously-defined env vars
# Order matters: referenced vars must be defined above
$(VAR_NAME) expansion: Kubernetes expands $(VAR) references using previously defined env vars (in order). This is done at Pod creation time, not shell expansion. Use $$(VAR) to escape if you need a literal $(VAR) in the value.
2. Downward API — fieldRef
Inject Pod metadata into the container without calling the API server:
Available Fields
| fieldPath | Value Example | Use Case |
|---|---|---|
metadata.name | web-abc123 | Logging, tracing (identify Pod) |
metadata.namespace | production | Multi-tenant apps that need to know their namespace |
metadata.uid | a1b2c3d4-... | Unique correlation IDs |
metadata.labels['key'] | v2 | Version-aware behavior |
metadata.annotations['key'] | team-platform | Injected metadata from CI/CD |
spec.nodeName | worker-3 | Node-aware routing, logging |
spec.serviceAccountName | web-sa | Identity-aware behavior |
status.podIP | 10.244.1.5 | Self-registration with service discovery |
status.hostIP | 192.168.1.10 | Node-level metrics endpoint |
env:
- name: MY_POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: MY_POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: MY_NODE
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: MY_VERSION
valueFrom:
fieldRef:
fieldPath: metadata.labels['version']
resourceFieldRef — Container Resources
env:
- name: MEMORY_LIMIT
valueFrom:
resourceFieldRef:
containerName: app # which container (required in multi-container)
resource: limits.memory # in bytes by default
divisor: "1Mi" # convert to MiB (optional)
- name: CPU_REQUEST
valueFrom:
resourceFieldRef:
resource: requests.cpu
divisor: "1m" # in millicores
| resource | Returns |
|---|---|
requests.cpu | CPU requests (default: cores as decimal) |
limits.cpu | CPU limits |
requests.memory | Memory requests (default: bytes) |
limits.memory | Memory limits |
-Xmx to match the container memory limit. Use resourceFieldRef to inject the limit, then set -Xmx to ~75% of it. This prevents OOMKill from the JVM allocating more than the cgroup allows.
3. Downward API via Volume
Labels and annotations can change after Pod creation (unlike most metadata). To see live updates, use a downwardAPI volume instead of env vars:
spec:
containers:
- name: app
volumeMounts:
- name: pod-info
mountPath: /etc/podinfo
readOnly: true
volumes:
- name: pod-info
downwardAPI:
items:
- path: "labels" # → /etc/podinfo/labels
fieldRef:
fieldPath: metadata.labels
- path: "annotations" # → /etc/podinfo/annotations
fieldRef:
fieldPath: metadata.annotations
- path: "cpu_limit" # → /etc/podinfo/cpu_limit
resourceFieldRef:
containerName: app
resource: limits.cpu
divisor: "1m"
# Inside the container: cat /etc/podinfo/labels # app="web" # version="v2" # environment="production" cat /etc/podinfo/cpu_limit # 500
Env Var vs Volume: When to Use Each
| Method | Updates Live? | Best For |
|---|---|---|
Env var (fieldRef) | ❌ No (fixed at start) | Immutable info: Pod name, namespace, node, IP |
Volume (downwardAPI) | ✅ Yes (labels/annotations change) | Labels, annotations that may be updated post-creation |
4. Practical Production Uses
| Pattern | Implementation |
|---|---|
| Structured logging | Inject POD_NAME, NAMESPACE, NODE as env vars → include in every log line |
| JVM memory tuning | Inject limits.memory → set -Xmx to 75% of limit |
| Prometheus metrics | Inject POD_NAME → use as instance label in metrics |
| Service registration | Inject status.podIP → register with Consul/etcd |
| Canary detection | Mount labels as volume → sidecar reads version label to adjust behavior |
| Cost allocation | Inject metadata.labels['team'] → tag cloud metrics by team |
POD_NAME and NAMESPACE as env vars. Without them, correlating logs and metrics to a specific Pod instance is painful. Most logging frameworks (Fluentd, structured logging) use these to enrich log entries automatically.
Summary
| Concept | Key Point |
|---|---|
| 5 env var sources | Static, ConfigMap, Secret, fieldRef, resourceFieldRef |
$(VAR) expansion | Reference earlier env vars — order matters |
| fieldRef | Pod metadata: name, namespace, IP, node, labels, annotations |
| resourceFieldRef | Container CPU/memory requests and limits |
| downwardAPI volume | Files that auto-update when labels/annotations change |
| Env vars are fixed | Set at container start, never change during lifetime |
📝 Quiz: Environment Variables & the Downward API
Q1: How does a container learn its own Pod IP without calling the Kubernetes API?
fieldRef:env:
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIPThe kubelet injects this at Pod start — no API server call needed.
Q2: You define DATABASE_URL: "postgres://$(DB_HOST):$(DB_PORT)/mydb". DB_PORT is defined AFTER DATABASE_URL in the env list. What value does DATABASE_URL get?
DATABASE_URL will be postgres://<DB_HOST_value>:$(DB_PORT)/mydb. $(DB_HOST) is expanded (defined earlier), but $(DB_PORT) is left as a literal string because it hasn't been defined yet. Order matters — referenced vars must be defined above.Q3: A label on a running Pod is changed via kubectl label pod web version=v3. An env var uses fieldRef: metadata.labels['version']. Does the env var update?
downwardAPI volume mount instead — the file will be updated when the label changes.Q4: A Java app is OOMKilled. Its container has limits.memory: 512Mi. How do you set JVM heap correctly using the Downward API?
- name: MEM_LIMIT
valueFrom:
resourceFieldRef:
resource: limits.memory
divisor: "1Mi"Then in the container command:
-Xmx$(echo "$MEM_LIMIT * 75 / 100" | bc)m or use a modern JVM with -XX:MaxRAMPercentage=75.0 which reads cgroup limits directly.Q5: What's the difference between fieldRef and resourceFieldRef?
fieldRef: Exposes Pod-level metadata — name, namespace, UID, IP, node name, labels, annotations. These are the same for all containers in the Pod.resourceFieldRef: Exposes container-level resource settings — CPU/memory requests and limits. These can differ between containers in the same Pod (requires containerName in multi-container Pods).Q6: You need to pass ALL of a Pod's labels to a container. Can you do this with env vars?
fieldRef for labels requires a specific key: metadata.labels['key']. To expose ALL labels (without knowing them in advance), use a downwardAPI volume with fieldRef: metadata.labels. This creates a file containing all labels as key=value pairs.