Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The Full Stack, One Deployment

Vault to sidecar to template to file to a live process — every hop on one page, as the manifests you would actually apply. Each piece has its own chapter; production is all of them at once, and the order they come up in.

Vault (secret/myapp)                        the values that move
  → injected agent, kubernetes auth        no distributed secret
  → minijinja template (ConfigMap)         the store's shape → the app's
  → /config/rendered.yaml (tmpfs)          off the node's disk
  → the app's own watcher                  live without a restart

0. The one-time pieces

$ helm install dynamic-config oci://ghcr.io/dynamic-config-rs/charts/dynamic-config \
    --namespace dynamic-config --create-namespace

$ vault auth enable kubernetes
$ vault write auth/kubernetes/config kubernetes_host=https://kubernetes.default.svc
$ vault write auth/kubernetes/role/billing \
    bound_service_account_names=billing \
    bound_service_account_namespaces=shop \
    policies=billing-read ttl=1h
$ kubectl -n shop create configmap vault-ca --from-file=ca.crt=./internal-ca.pem

1. The template — the store's shape becomes the app's

apiVersion: v1
kind: ConfigMap
metadata:
  name: billing-template
  namespace: shop
data:
  template: |
    db:
      host: {{ db.host }}
      pool_size: {{ db.pool_size | default(8) }}
    features:
      cache: {{ features.cache | default(false) }}

Re-read every render, so editing the ConfigMap is itself a live change — no rollout. The Rendering chapter owns the semantics; the one rule worth restating is that a template failure leaves the previous rendered file in place, which is last-known-good at the file layer.

2. The workload — annotations are the whole integration

apiVersion: apps/v1
kind: Deployment
metadata:
  name: billing
  namespace: shop
spec:
  replicas: 3
  selector: { matchLabels: { app: billing } }
  template:
    metadata:
      labels: { app: billing }
      annotations:
        dynamic-config.rs/inject: "true"
        dynamic-config.rs/source: "vault"
        dynamic-config.rs/endpoint: "https://vault.vault.svc:8200"
        dynamic-config.rs/key: "secret/myapp"
        dynamic-config.rs/auth: "kubernetes"
        dynamic-config.rs/auth-role: "billing"
        dynamic-config.rs/ca-configmap: "vault-ca"
        dynamic-config.rs/template-configmap: "billing-template"
        dynamic-config.rs/path: "/config/rendered.yaml"
        dynamic-config.rs/native-sidecar: "true"
        dynamic-config.rs/watch-seconds: "30"
    spec:
      serviceAccountName: billing
      containers:
        - name: app
          image: myapp:1
          # /config arrives injected: tmpfs, shared with the agent.
          readinessProbe:
            httpGet: { path: /readyz, port: 8080 }

No secret in the manifest, no volume stanza to write, no sidecar to maintain: the webhook injects the agent, the agent logs in with the pod's own service account, and the token never exists as a Kubernetes Secret. native-sidecar: "true" makes the agent an init container with restartPolicy: Always, so a Job with these annotations still finishes.

3. The app — the last hop is an ordinary file

The agent's output is a file, so the app's side is the engine's ordinary file story — any of the three languages, here Rust:

#[dynamic_config]
#[derive(Deserialize)]
struct Db {
    host: String,
    pool_size: u32,
}

Db::builder("db").file("/config/rendered.yaml").init()?;
Db::builder("db")
    .file("/config/rendered.yaml")
    .watch(Duration::from_millis(500))?
    .detach();

The write is atomic (rename), so the watcher never reads half a render — the same guarantee the kubelet's ..data swap gives a mounted ConfigMap, held at both layers.

What moves without a rollout, and what does not

ChangeTakes effect
the secret in Vaultnext watch-seconds tick → render → app's watcher
the template ConfigMapnext render (kubelet sync + render tick)
an annotationrollout — injection happens at pod creation
the chart's own valueshelm upgrade, webhook restart, no app rollout

The annotation row is the one that surprises: annotations are read by the webhook when the pod is admitted, so changing them is a Deployment edit and rolls pods — which is also why it is safe, because every replica converges through the same admission path.

Watching it work

$ kubectl -n shop logs deploy/billing -c dynamic-config-agent --tail=5
$ kubectl -n shop exec deploy/billing -c app -- cat /config/rendered.yaml
$ vault kv put secret/myapp db='{"host": "db-2.internal", "pool_size": 16}'
# … within watch-seconds, the same two commands show the new values,
# and the app's /readyz generation has moved. No pod restarted.