We will use a running example: shopbot, a customer-support chatbot for an online store. The image ships with defaults—listen port, welcome banner, process entrypoint. Staging wants a different port and banner; production needs an LLM API token that must not live in a public Git repo.
Below we configure that Pod—literals in the manifest first, then ConfigMaps and Secrets, then identity metadata via the Downward API.
Learning goals
- Override the container command and arguments from the Pod manifest.
- Set environment variables from literals, references, ConfigMaps, and Secrets.
- Keep environment-specific settings outside the Pod YAML.
- Choose Secret types for tokens, TLS, and private image pulls—and know their limits.
- See how platform operators (External Secrets on OpenShift, HyperShift spokes) reduce hand-rolled Secrets.
- Expose Pod name, IPs, and resource limits without duplicating them by hand.
The big picture
Think of a school science kit. The sealed box is the image. The lab bench is the Pod. The worksheet you fill before the timer starts—temperature, gas mixture, student ID—is configuration. Same kit; different worksheet.
Pod manifest
command, args, and env live here. You can hardcode values or point at ConfigMaps and Secrets by name.
containers:
- name: shopbot
image: acme/shopbot:2.1
args: ["--http-port", "4480"]
env:
- name: WELCOME_BANNER
valueFrom:
configMapKeyRef:
name: shopbot-settings
key: welcome-banner1. Command, arguments, and environment variables
Image defaults vs Pod fields
Dockerfiles usually place the program in ENTRYPOINT and default arguments in CMD. Kubernetes mirrors that split with command and args. At start time the two lists are concatenated, just as Docker concatenates ENTRYPOINT and CMD.
Image defaults: process is node server.js, port 4400. Change dials in the Pod without rebuilding.
Like a lab apparatus with a dial: the hardware stays put; you turn the dial. Override settings in the Pod without rebuilding the image.
# Override only arguments (common case)
containers:
- name: shopbot
image: acme/shopbot:2.1
args: ["--http-port", "4480"]
# Override the program itself (less common)
command:
- node
- --inspect
- server.jsQuote values YAML might treat as numbers or booleans: "4480", "yes", "on".
Literal environment variables
Each container has its own env list. There is no Pod-wide environment inherited by every container.
env:
- name: WELCOME_BANNER
value: "Hi — I can help with orders and returns."
- name: BOT_INSTANCE
value: shopbot-webList variables declared on the Pod with kubectl set env pod <name> --list. Inspect the live process with kubectl exec <name> -- env.
References with $(VAR_NAME)
Inside the same manifest you may compose values with $(VAR_NAME). The named variable must appear earlier in that manifest. Variables defined only in the image are not expanded. Unresolved references remain literal text. Write $$(VAR_NAME) when you want a literal dollar sign.
env:
- name: BOT_INSTANCE
value: shopbot-web
- name: WELCOME_BANNER
value: "You are chatting with $(BOT_INSTANCE)."
args:
- --http-port
- $(HTTP_PORT)
# HTTP_PORT must also be defined under env in this manifestShell expansion for container-local variables
To expand variables that exist only inside the container (for example HOSTNAME), run a shell and use shell syntax $VAR or curly-brace form—not Kubernetes’ $(VAR).
command:
- sh
- -c
- 'echo "Hostname is $HOSTNAME"; sleep infinity'Hostname and subdomain
By default the Pod hostname matches the Pod name. Set spec.hostname and optionally spec.subdomain to shape an internal FQDN of the form <hostname>.<subdomain>.<namespace>.svc.<cluster-domain>. Making that name resolvable via DNS is a networking topic—Services and DNS come later in this book when we wire Pods for discovery.
2. ConfigMaps — settings outside the Pod
Embedding every setting in the Pod YAML forces a different Pod file per environment. A ConfigMap stores key–value pairs in a separate object. Pods reference it by name, so the same Pod manifest can run in development and production while each cluster keeps its own ConfigMap values.
staging production
┌────────────────┐ ┌────────────────┐
│ Pod shopbot-web│ │ Pod shopbot-web│
│ → shopbot- │ │ → shopbot- │
│ settings │ │ settings │
└───────┬────────┘ └───────┬────────┘
│ │
▼ ▼
┌────────────────┐ ┌────────────────┐
│ ConfigMap │ │ ConfigMap │
│ banner=try us │ │ banner=live ok │
└────────────────┘ └────────────────┘Same idea as one exam paper and a different answer key per classroom.
Creating ConfigMaps
# Literal
kubectl create configmap shopbot-settings \
--from-literal welcome-banner="Thanks for shopping with Acme."
# File (key = filename by default)
kubectl create configmap shopbot-settings --from-file=shopbot.toml
# KEY=value lines
kubectl create configmap shopbot-settings --from-env-file=shopbot.env
# Generate YAML for Git without applying
kubectl create configmap shopbot-settings \
--from-file=shopbot.toml \
--dry-run=client -o yaml > cm-shopbot-settings.yamlapiVersion: v1
kind: ConfigMap
metadata:
name: shopbot-settings
data:
welcome-banner: Thanks for shopping with Acme.Non-UTF-8 content belongs under binaryData (Base64 in YAML). Keys may use letters, digits, dashes, underscores, and dots.
Inject one key
env:
- name: WELCOME_BANNER
valueFrom:
configMapKeyRef:
name: shopbot-settings
key: welcome-banner
optional: trueIf the ConfigMap or key is missing and the reference is not optional, that container is blocked from starting. Other containers in the Pod may still run.
Inject every key with envFrom
envFrom:
- configMapRef:
name: shopbot-settings
optional: true
# prefix: BOT_ # optionalKeys become environment variable names as-is. Explicit env entries override envFrom. When several ConfigMaps list the same key, the last one wins.
Updates and immutability
Editing a ConfigMap does not rewrite environment variables inside containers that are already running. A restart loads the new values. That can leave a mix of old and new settings across replicas.
For stable settings, mark the object immutable with immutable: true. The API then rejects changes to data and binaryData. Ship a new ConfigMap name when you need a new generation of Pods.
3. Secrets — sensitive data
Shopbot needs more than a welcome banner. The LLM vendor token, the TLS material on the edge route, and the pull credential for a private image registry all belong in Secrets—not ConfigMaps. The API shape is similar (key–value bags), but the cluster treats Secret bytes more carefully: only nodes that run a consumer Pod receive them, and kubelet keeps that material in memory rather than parking it on disk.
ConfigMap Secret ───────── ────── data ←plain→ stringData (write-only) binaryData ←base64→ data immutable immutable (no type) type (Opaque, tls, …)
Base64 in a Secret manifest is encoding for YAML/JSON—not encryption. Anyone with get secrets can decode the payload. Encryption-at-rest in etcd, tight RBAC, and (better) an external vault are what actually raise the bar.
Opaque Secret for the LLM token
kubectl create secret generic shopbot-llm-creds \
--from-literal vendor_token="sk-live-not-a-real-key"
# Or plain text in YAML (keep this out of public Git)
apiVersion: v1
kind: Secret
metadata:
name: shopbot-llm-creds
namespace: retail-chat
type: Opaque
stringData:
vendor_token: sk-live-not-a-real-keyenv:
- name: LLM_VENDOR_TOKEN
valueFrom:
secretKeyRef:
name: shopbot-llm-creds
key: vendor_token
# Whole Secret:
envFrom:
- secretRef:
name: shopbot-llm-credsTyped Secrets you will actually use
type is not decoration—built-in types enforce expected keys so controllers and kubelet know how to consume them.
- Opaque — arbitrary keys (shopbot’s vendor token).
- kubernetes.io/tls —
tls.crt+tls.keyfor the edge route in front of shopbot. - kubernetes.io/dockerconfigjson — registry auth so the node can pull
acme/shopbotfrom a private Quay (or similar).
# TLS for the shopbot edge route
kubectl create secret tls shopbot-edge-tls \
--cert=shopbot.acme.example.crt \
--key=shopbot.acme.example.key \
-n retail-chat
# Pull from a private registry
kubectl create secret docker-registry shopbot-quay-pull \
--docker-server=quay.acme.example \
--docker-username=robot-shopbot \
--docker-password=… \
-n retail-chatapiVersion: v1
kind: Pod
metadata:
name: shopbot-web
namespace: retail-chat
spec:
imagePullSecrets:
- name: shopbot-quay-pull
containers:
- name: shopbot
image: quay.acme.example/acme/shopbot:2.1
env:
- name: LLM_VENDOR_TOKEN
valueFrom:
secretKeyRef:
name: shopbot-llm-creds
key: vendor_tokenWhy env vars leak secrets
Crash reports, startup dumps, and anything that prints process.env will cheerfully include your vendor token. Child processes inherit that whole environment too. If the credential must stay quiet, mount it as a file in the container filesystem—volumes handle that pattern; we stay on env injection here.
What Secrets do not buy you
- Values in manifests are encoded, not encrypted.
- etcd may store Secrets in cleartext unless encryption-at-rest is enabled.
- Weak RBAC can expose Secrets through the API.
- Native Secrets do not rotate themselves—you (or an operator) must.
3b. Operator view — stop hand-rolling every Secret
On a laptop cluster, kubectl create secret is fine. On a fleet—say HyperShift hosted control planes with bare-metal worker spokes for retail stores—copy-pasting tokens into every namespace does not scale and does not audit well. Platform teams lean on operators so app manifests only consume Secrets; something else writes them.
HyperShift + bare-metal spokes (mental model)
HyperShift keeps the control plane as a hosted cluster while workers can sit on bare metal in a store or DC. From shopbot’s point of view the API is still Kubernetes: Pods, ConfigMaps, Secrets. From the operator’s point of view you want one vault path and many spokes—each spoke gets a synced shopbot-llm-creds in retail-chat without a human SSHing to the metal.
Management / hosting plane
┌─────────────────────────────┐
│ HyperShift hosted CPs │
│ Operators, GitOps, policies │
└──────────────┬──────────────┘
│ provision / sync
▼
Bare-metal spoke (store / DC)
┌─────────────────────────────┐
│ namespace retail-chat │
│ Secret shopbot-llm-creds │← synced
│ Pod shopbot-web │← consumes
└─────────────────────────────┘External Secrets Operator on OpenShift
Red Hat ships an External Secrets Operator for OpenShift (based on the upstream External Secrets project). It is generally available as a day-2 operator from the Red Hat catalog for OpenShift Container Platform 4.20 and later. You install it via OLM; then declare a SecretStore (how to reach Vault, cloud SM, Conjur, …) and an ExternalSecret (which remote keys become which in-cluster Secret).
Alongside it, OpenShift’s secrets portfolio also includes cert-manager (certificate lifecycle) and the Secrets Store CSI Driver operator (mount vault material as ephemeral files without necessarily persisting a Secret in etcd). Pick the tool that matches the threat model: ESO when you still want a normal Secret object for secretKeyRef; SSCSI when you want file-only, cluster-unaware credentials.
ExternalSecret CR
On OpenShift 4.20+, the External Secrets Operator (Red Hat catalog) watches this CR and talks to the vault. Developers do not create the Secret by hand.
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: shopbot-llm-sync
namespace: retail-chat
spec:
refreshInterval: 1h
secretStoreRef:
name: acme-vault-store
kind: SecretStore
target:
name: shopbot-llm-creds
data:
- secretKey: vendor_token
remoteRef:
key: retail/shopbot
property: vendor_tokenApp teams keep referencing shopbot-llm-creds. SecOps rotates vendor_token in the vault; the operator refreshes the Secret on the interval you set. That is the gap native Kubernetes Secrets leave open—no built-in rotation—filled by platform automation rather than another paragraph of Pod YAML.
4. Downward API — metadata without duplication
Some facts appear only after scheduling: Pod IP, node name, UID. Others already live in the Pod object: CPU and memory limits. The Downward API injects selected fields into environment variables (or files) so you do not copy them by hand—handy when shopbot stamps replies with which replica answered.
If the length is already stamped on the rod, read the stamp—don’t remeasure with another ruler. The Downward API is that stamp.
Pod object
kind: Pod
metadata:
spec:
status:
resources.limits:
Stamp replies with which replica answered. Available as env or as a file via a downwardAPI volume.
Container · environment
SHOPBOT_POD=shopbot-web
SHOPBOT_ADDR=10.42.1.88
Manifest snippet for “metadata.name”
- name: SHOPBOT_POD
valueFrom:
fieldRef:
fieldPath: metadata.nameNot a REST call — Kubernetes copies selected Pod fields into env vars (or files) when the container starts.
Identity fields
env:
- name: SHOPBOT_POD
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: SHOPBOT_ADDR
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: CLUSTER_NODE
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: CLUSTER_NODE_IP
valueFrom:
fieldRef:
fieldPath: status.hostIPOther useful paths include metadata.namespace, metadata.uid, metadata.labels['key'], metadata.annotations['key'], and spec.serviceAccountName. Some collections (all labels or all annotations) are available only as files, not as environment variables.
Resource limits
env:
- name: CPU_BUDGET
valueFrom:
resourceFieldRef:
resource: limits.cpu
- name: MEMORY_BUDGET_KB
valueFrom:
resourceFieldRef:
resource: limits.memory
divisor: 1kThe optional divisor selects units (1, 1k, 1Ki, 1m for milli-CPU, and so on). Use containerName when one container must read another container’s limits in the same Pod.
Mini-lab
kubectl create configmap shopbot-settings \
--from-literal WELCOME_BANNER="Thanks for shopping with Acme."
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: shopbot-probe
spec:
containers:
- name: shopbot
image: alpine:3.20
command: ["sh", "-c", "env; sleep 3600"]
envFrom:
- configMapRef:
name: shopbot-settings
env:
- name: SHOPBOT_POD
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: SHOPBOT_ADDR
valueFrom:
fieldRef:
fieldPath: status.podIP
EOF
kubectl exec shopbot-probe -- env | grep -E 'WELCOME_BANNER|SHOPBOT_'Checklist
- I can override
commandandargswithout rebuilding an image. - I know when
$(VAR)resolves and when it does not. - I can move environment-specific strings into a ConfigMap.
- I keep credentials in Secrets and avoid dumping them through environment logs.
- I know when to push Secret creation to an operator (e.g. External Secrets on OpenShift) instead of
kubectl create secreton every spoke. - I can inject Pod name, IPs, and resource limits with the Downward API.
Where this leaves you
You can now shape shopbot’s process and feed it settings without rebuilding the image. On a real fleet, let operators own Secret materialization; keep Pod YAML as a consumer. Durable disk for transcripts is the next concern—PersistentVolumeClaims and StorageClasses. After that, mounting ConfigMaps and Secrets as files sits beside that data path.