Links#
https://kubernetes.io/docs/concepts/configuration/secret/
https://docs.docker.com/compose/how-tos/environment-variables/
https://docs.aws.amazon.com/eks/latest/userguide/manage-secrets.html
https://cloud.google.com/secret-manager/docs/secret-manager-managed-csi-component
https://learn.microsoft.com/en-us/azure/aks/csi-secrets-store-driver
https://external-secrets.io/latest/
1. Important Points#
configuration goal:
same image
different runtime config
no secret in git
no secret baked into image
fail fast when config missing
rule:
non-secret -> env / ConfigMap
secret -> Secret / external secret manager / mounted file
2. Application Config Loader#
import "dotenv/config";
import { existsSync, readFileSync } from "node:fs";
import { z } from "zod";
const configSchema = z.object({
NODE_ENV: z.enum(["local", "dev", "test", "staging", "prod"]).default("local"),
PORT: z.coerce.number().int().positive().default(3000),
LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32)
});
function readSecret(name) {
const filePath = process.env[`${name}_FILE`];
if (filePath !== undefined && existsSync(filePath)) {
return readFileSync(filePath, "utf8").trim();
}
return process.env[name];
}
const parsed = configSchema.safeParse({
...process.env,
JWT_SECRET: readSecret("JWT_SECRET")
});
if (!parsed.success) {
console.error(parsed.error.flatten().fieldErrors);
process.exit(1);
}
export const config = parsed.data;
supports:
JWT_SECRET
JWT_SECRET_FILE
3. Local#
NODE_ENV=local
PORT=3000
LOG_LEVEL=debug
DATABASE_URL=postgres://order:password@localhost:5432/order
JWT_SECRET=change-me-change-me-change-me-change-me
4. Docker#
docker run --rm \
-p 3000:3000 \
--env-file .env.local \
order-api:local
docker run --rm \
-p 3000:3000 \
-e JWT_SECRET_FILE=/run/secrets/jwt_secret \
-v "$PWD/secrets/jwt_secret:/run/secrets/jwt_secret:ro" \
--env-file .env.local \
order-api:local
5. Kubernetes#
apiVersion: v1
kind: ConfigMap
metadata:
name: order-api-config
data:
NODE_ENV: prod
PORT: "3000"
LOG_LEVEL: info
DATABASE_URL: postgres://order:password@postgres:5432/order
apiVersion: v1
kind: Secret
metadata:
name: order-api-secret
type: Opaque
stringData:
JWT_SECRET: change-me-change-me-change-me-change-me
envFrom:
- configMapRef:
name: order-api-config
- secretRef:
name: order-api-secret
6. AWS / EKS#
options:
Kubernetes Secret
AWS Secrets Manager + Secrets Store CSI Driver
External Secrets Operator
app reads Secrets Manager directly
recommendation:
use IRSA / Pod Identity
grant least privilege
7. GCP / GKE#
options:
Kubernetes Secret
Google Secret Manager CSI
External Secrets Operator
app reads Secret Manager directly
recommendation:
use Workload Identity
avoid static service account keys
8. Azure / AKS#
options:
Kubernetes Secret
Azure Key Vault CSI Driver
External Secrets Operator
app reads Key Vault directly
recommendation:
use managed identity / workload identity
9. Alibaba Cloud / ACK#
options:
Kubernetes Secret
KMS / Secret Manager integration
External Secrets Operator when provider support is available
app reads KMS Secrets Manager directly
10. Vault#
options:
Vault Agent Injector
Vault Secrets Operator
External Secrets Operator
app reads Vault directly
11. Decision#
| Runtime |
Simple |
Better For Production |
| Local |
.env.local |
local secret manager |
| Docker |
--env-file |
mounted secret |
| K8S |
ConfigMap + Secret |
CSI / External Secrets |
| EKS |
Kubernetes Secret |
AWS Secrets Manager |
| GKE |
Kubernetes Secret |
Google Secret Manager |
| AKS |
Kubernetes Secret |
Azure Key Vault |
| ACK |
Kubernetes Secret |
KMS integration |
| Vault |
mounted file |
Vault Agent / VSO |
12. Production Checklist#
application:
validate config at startup
support env and *_FILE
fail fast on missing secret
do not log secret values
container:
no .env in image
runtime injection only
kubernetes:
ConfigMap for non-secret
Secret/external secret for sensitive values
restrict RBAC