Kubernetes .env to ConfigMap & Secret

Paste a .env file and get a ConfigMap and a Secret, with the credentials separated out and the keys that will not survive envFrom named. It runs in this tab, which is the only sane place to paste a file full of passwords.

Paste below, or drop a file anywhere on this panel

Or drop a file anywhere on this panel. Nothing is uploaded: the analysis runs in this tab.

The answer appears here

Paste on the left and press Convert. Nothing leaves this tab.

Examples

Real input you can load into the tool above. Each one shows a different thing going wrong, because that is what the tool is for.

A credential in .env

A value that must become a Secret rather than a ConfigMap, detected by name

DB_PASSWORD=hunter2
DEBUG=true
API_URL=https://api.example.com

Values needing care

Quotes, spaces and a dollar sign, none of which Kubernetes interpolates

GREETING="hello world"
PATH_PREFIX=/api/v1
COST=$100
EMPTY=

Common mistakes

These are the ones that fail silently. The config is accepted, nothing raises an error, and the consequence arrives later.

  1. Putting a credential in a ConfigMap

    ConfigMaps are not access-controlled the way Secrets are, are readable by anyone with get in the namespace, and appear in kubectl describe output in full.

    Instead:Split the file: non-secret values to a ConfigMap, credentials to a Secret.

  2. Expecting .env quoting rules to carry over

    Docker Compose, dotenv libraries and shells all differ on quotes, escapes and interpolation. Kubernetes does none of it: the value is literal.

    Instead:Check any value containing quotes, spaces or a dollar sign after conversion.

  3. Assuming a ConfigMap change reaches the pod

    Values injected with envFrom are read at container start. Editing the ConfigMap does nothing until the pod restarts, unlike a mounted volume which updates eventually.

    Instead:Restart the workload, or mount as a volume if live updates matter.

What it catches

The conversion is four lines of sed. What is not is everything that goes wrong after the ConfigMap is applied and appears to have worked.

The keys envFrom throws away without telling you

envFrom turns every key in a ConfigMap into an environment variable, and a variable name has to match [A-Za-z_][A-Za-z0-9_]*. A key with a dot or a dash in it does not, so the kubelet skips it, writes an InvalidVariableNames event nobody reads, and starts the container anyway. The application sees the variable as unset and falls back to a default. Spring and Symfony .env files are full of dotted keys.

Credentials separated from configuration

Keys and values that look like credentials go into a Secret and the rest into a ConfigMap, with the reason for each move reported. A ConfigMap is readable by anything with get on ConfigMaps in the namespace and is dumped by default in most debug tooling, so the split is worth making at conversion time rather than never.

The values that make the apply fail

ConfigMap and Secret data are both string maps. PORT=8080 written unquoted becomes a YAML integer and the API server rejects the object with a Go unmarshalling error that never names the key. So does DEBUG=true, and so does anything YAML reads as a boolean, which includes no, off and y. Every value in the output is quoted, so the whole class is gone.

dotenv quoting, done the way dotenv does it

Escape sequences are interpreted inside double quotes and not inside single ones, which is what makes single quotes right for Windows paths. An unquoted hash starts a comment and a quoted one does not. Multi-line quoted values, which is how a PEM key ends up in a .env, are read to their closing quote and emitted as a block scalar.

What happens to these values once they are in the cluster

A .env file is read by the application at startup. A ConfigMap is read by the kubelet, at container start, and the difference in when and how often matters more than the difference in format.

envFrom takes everything, env takes one and can rename it

envFrom is the direct equivalent of an .env file and is what most people want. It is also where the silent key-skipping happens. Referencing a key explicitly with configMapKeyRef has no naming restriction at all, so it is the escape hatch for a key you cannot rename.

envFrom:                      # every key, names unchanged
  - configMapRef:
      name: app-config
  - secretRef:
      name: app-secrets

env:                          # one key, renamed on the way
  - name: DATABASE_URL
    valueFrom:
      configMapKeyRef:
        name: app-config
        key: spring.datasource.url   # dots are fine here

Changing a ConfigMap does not restart anything

This is the single most surprising part. A ConfigMap consumed through env or envFrom is read once, when the container starts. Edit it afterwards and every running pod keeps the old values indefinitely, because nothing triggers a restart. Mounted as a volume it does update in place, after a delay of up to the kubelet sync period, but the process still has to notice the file changed.

kubectl edit configmap app-config     # takes effect: never

kubectl rollout restart deploy/api    # takes effect: now

# or make the change force a rollout, by putting a hash
# of the config in the pod template annotations

A Secret is RBAC, not cryptography

Secret data is base64, which is an encoding. Anyone who can read the Secret can read the value, and by default that is anyone with get on Secrets in the namespace, plus every ServiceAccount token that grants it. What actually protects a Secret is RBAC, and encryption at rest if the cluster has it turned on. Neither of those is a property of the file this tool gives you.

# what is in the manifest either way:
stringData:
  DB_PASSWORD: hunter2        <- plain text

data:
  DB_PASSWORD: aHVudGVyMg==   <- also plain text,
                                 with extra steps

# so do not commit either. Sealed Secrets, the External
# Secrets Operator and SOPS all exist for this.

Kubernetes expands $(VAR) inside env values

Before the container starts, Kubernetes substitutes $(OTHER_VAR) in env values using the other variables in the same container. A password containing that sequence is rewritten, and if the name does not resolve the text is left alone, so the same value behaves differently depending on what else happens to be set.

PASSWORD=abc$(def)ghi

  ->  if DEF is set:    abc<value of DEF>ghi
  ->  if it is not:     abc$(def)ghi

escape it as $$(, or mount the value as a file,
where no substitution happens at all