Chapter 2

Configuring applications

Commands, args, env, ConfigMaps, Secrets, operators (External Secrets / HyperShift), Downward API.

2.5 hoursKubernetes Foundations

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.

Interactive — configuring shopbotClick a stage

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-banner

1. 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.

Interactive — Dockerfile ↔ Pod fields (shopbot)Click a column

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.js

Quote 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-web

List 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 manifest

Shell 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.

Same shopbot Pod YAML, different settings per environment
  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.yaml
apiVersion: 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: true

If 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_   # optional

Keys 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 vs Secret fields
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-key
env:
  - name: LLM_VENDOR_TOKEN
    valueFrom:
      secretKeyRef:
        name: shopbot-llm-creds
        key: vendor_token

# Whole Secret:
envFrom:
  - secretRef:
      name: shopbot-llm-creds

Typed 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/tlstls.crt + tls.key for the edge route in front of shopbot.
  • kubernetes.io/dockerconfigjson — registry auth so the node can pull acme/shopbot from 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-chat
apiVersion: 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_token

Why 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.

Fleet sketch (management vs spoke)
  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.

Interactive — operator path for shopbot credentialsClick a stage

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_token

App 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.

Interactive — shopbot metadata into the containerClick a field to toggle injection

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.name

Not 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.hostIP

Other 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: 1k

The 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 command and args without 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 secret on 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.