Downward API
A ConfigMap and a Secret are for what you know in advance. But there is data your application needs that nobody can write in a manifest, because it does not exist until the Pod starts: which node it is going to run on, which IP the CNI gave it, what the specific replica running that code is called.
The Downward API is the mechanism to bring that data down into the container. It is not a new object: it is one more variant of the valueFrom you already used with the ConfigMap and the Secret.
In this lesson you are going to set up the tienda's api with two replicas and give it three things only the cluster knows: its identity, its own resource limits and its labels. And at the end you will check for yourself which of those routes refreshes live and which does not, which is the difference that causes the most grief. Work from the dev-machine tab.
Step 1: The Pod's identity
Create deployment-api.yaml:
cat << 'EOF' > deployment-api.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 2
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: ghcr.io/iximiuz/labs/nginx:alpine
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
EOF
The YAML, explained in questions and answers
Why a Deployment and not a bare Pod?
Because with a single replica this does not show. A Deployment spreads two Pods with different names and, very probably, on different nodes: that is where POD_NAME stops being decoration and starts answering the question of which replica wrote a log line.
Which fields can I ask for with fieldRef?
The Pod's own: metadata.name, metadata.namespace, metadata.uid, spec.nodeName, spec.serviceAccountName, status.podIP and status.hostIP. It is not the complete list of the object, it is the list of what the kubelet knows for certain at the moment it creates the container.
Couldn't I write the name by hand and save myself this?
In a bare Pod, yes. In a Deployment it does not exist: the ReplicaSet composes the name with a random suffix, and you do not know it until the Pod exists. That is exactly the gap the Downward API fills.
Apply it and look at the two replicas:
kubectl apply -f deployment-api.yaml
kubectl get pods -l app=api -o wide
Now ask each one who it is. Notice the single quotes: the one that has to expand $POD_NAME is the shell inside the container, not yours. The line it prints is in Spanish (va en means "runs on", con IP means "with IP").
for p in $(kubectl get pods -l app=api -o name); do
kubectl exec $p -- sh -c 'echo "$POD_NAME va en $NODE_NAME con IP $POD_IP"'
done
Step 2: Its own limits
Now the part that most often saves an application from an OOMKilled. Add a resources block and three more variables to the container. The complete container looks like this:
containers:
- name: api
image: ghcr.io/iximiuz/labs/nginx:alpine
resources:
requests:
memory: 128Mi
cpu: 250m
limits:
memory: 256Mi
cpu: 500m
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: MEMORIA_MAXIMA_MB
valueFrom:
resourceFieldRef:
containerName: api
resource: limits.memory
divisor: 1Mi
- name: MEMORIA_SOLICITADA_MB
valueFrom:
resourceFieldRef:
containerName: api
resource: requests.memory
divisor: 1Mi
- name: CPU_MAXIMA_MILI
valueFrom:
resourceFieldRef:
containerName: api
resource: limits.cpu
divisor: 1m
The variable names are Spanish, like the rest of the tienda's identifiers: MEMORIA_MAXIMA_MB is the memory limit in MB, MEMORIA_SOLICITADA_MB the memory request, and CPU_MAXIMA_MILI the CPU limit in millicores.
The YAML, explained in questions and answers
What does the divisor do?
It decides in which unit the number arrives. The Downward API delivers bytes and cores, and almost no runtime wants that: with divisor: 1Mi the limit's 256Mi arrive as 256, and with divisor: 1m the CPU's 500m arrive as 500. The result is always rounded up to the next integer.
Why would I want my own memory limit?
To size itself. A JVM that does not know how much memory it has sizes its heap by what it sees of the node, not by its cgroup, and ends up asking for more than the limit allows: the result is an OOMKilled that does not look like a configuration error. The same happens to a Node runtime with --max-old-space-size. Reading the limit from here instead of writing it by hand in two places is what keeps the manifest and the application from contradicting each other.
And if the container does not declare limits?
Then it does not fail: it gives you back what the node can offer. It is the mirage of this section, because the number arrives, the application believes it and sizes itself for a whole machine that is not its own.
Is containerName mandatory?
In an environment variable, no, the container that declares it is assumed. But write it: in a Pod with a sidecar it lets you ask for another container's resources, and without it you never know which one you are reading.
Apply it. Since the template changes, the Deployment does a rollout: the previous Pods disappear and two others are created. The echo prints in Spanish: MB pedidos means "MB requested", MB de techo means "MB ceiling".
kubectl apply -f deployment-api.yaml
kubectl rollout status deploy/api
kubectl exec deploy/api -- sh -c 'echo "$MEMORIA_SOLICITADA_MB MB pedidos, $MEMORIA_MAXIMA_MB MB de techo, $CPU_MAXIMA_MILI milicores"'
Step 3: The labels, through both routes
The labels are missing, and they are a case apart: they do not fit in an environment variable as they are, because they are a map that can change. That is what the volume variant is for.
You are going to mount the labels as a file and, on top of that, expose one of them as a variable. You will compare the two in a moment.
First, add the version label to the Pod template. Watch out for where: in template.metadata.labels, not in selector.matchLabels.
selector:
matchLabels:
app: api # the selector stays as it is
template:
metadata:
labels:
app: api
version: "1" # <- new
Then, one more variable in the env list:
- name: VERSION_ENV
valueFrom:
fieldRef:
fieldPath: metadata.labels['version']
And the volume, with its volumeMounts in the container and its volumes at Pod level, the two-halves pattern from the previous lesson:
volumeMounts:
- name: podinfo
mountPath: /etc/podinfo
readOnly: true
volumes:
- name: podinfo
downwardAPI:
items:
- path: labels
fieldRef:
fieldPath: metadata.labels
- path: annotations
fieldRef:
fieldPath: metadata.annotations
The YAML, explained in questions and answers
Why do the labels go in a volume and the name does not?
For the same reason a ConfigMap can be mounted: the volume can refresh and an environment variable cannot. The Pod's name never changes while the Pod exists, so a variable is enough for it. Labels and annotations do change during the Pod's life, and that is why they take this form.
Then why VERSION_ENV as well?
For the experiment in the next step. In a real manifest you would have one of the two, not both.
What does the labels file look like?
One line per label, in the format key="value". You will also see the pod-template-hash the ReplicaSet sets, which you did not write.
And if I want just one specific label in the volume?
There you can use metadata.labels['version'] as fieldPath, and mount it as a one-line file. Plain metadata.labels, the whole map, only works in a volume: in an environment variable the API rejects it.
Apply it. It is the third rollout of the lesson, and that is no accident: every change to the container's configuration means new Pods.
kubectl apply -f deployment-api.yaml
kubectl rollout status deploy/api
kubectl exec deploy/api -- cat /etc/podinfo/labels
Step 4: Which of the two finds out
Here is the real reason for the lesson. Take one of the replicas and change its version label live:
POD=$(kubectl get pods -l app=api -o jsonpath='{.items[0].metadata.name}')
kubectl label pod $POD version=2 --overwrite
The Pod stays alive: version is not in the Deployment's selector, so the ReplicaSet takes no notice and nobody recreates anything. Now ask the same thing through both routes:
kubectl exec $POD -- cat /etc/podinfo/labels
kubectl exec $POD -- sh -c 'echo $VERSION_ENV'
The first time, the file may still say version="1". It is not broken: the kubelet refreshes these volumes in its sync cycle, which usually resolves in a few seconds but can stretch to a minute. Repeat the cat until it changes.
When the file changes, the variable will still say 1. And it will keep saying so until the container is recreated, no matter that the label is already a different one.
Why this matters more than it seems
What you just checked with a label holds just the same for resources. MEMORIA_MAXIMA_MB was resolved when the container was created, so if the VPA resizes the Pod in place, your application keeps working with the number it read at startup, which no longer matches its cgroup.
The practical rule is short: what you read from an environment variable is what there was when the container started. If you need a piece of data that can change while the Pod is running, either you mount it as a file and read it again, or you assume a restart is needed.
Summary
- The Downward API brings down into the container what only the cluster knows: identity, node, IP and resources. It is one more
valueFrom, not a new object. fieldRefgives Pod fields;resourceFieldRefgives its requests and limits, withdivisorto choose the unit.- Without
limits,resourceFieldRefreturns what the node can offer. The number arrives all the same, and it is a lie for your container. - Labels and annotations are mounted as a volume, and there they do refresh. Environment variables freeze when the container is created.
- That is why an application that sizes itself reads its limit at startup, and why an in-place resize goes unnoticed by it.
- Previous lesson
- Challenge: pull an image from a private registry
- Next lesson
- Persistent storage