Configuration
Overview
Configuration migrates from Docker Compose .env files to Kubernetes ConfigMaps and Secrets. In staging, secrets are created manually via create-secrets.sh. In production, Sealed Secrets provides Git-safe encryption.
ConfigMap Organization
Each service gets its own ConfigMap with non-sensitive configuration. ConfigMaps are organized per overlay (staging vs production) via Kustomize.
Shared ConfigMap
Values shared across all backend services. Note the namespace differences for service discovery and data layer access.
apiVersion: v1
kind: ConfigMap
metadata:
name: nx-shared-config
namespace: nx-backend
data:
NODE_ENV: staging
# PostgreSQL via PgBouncer — nx-persistent namespace
APP_ENV_POSTGRES_HOST: nx-pgbouncer.nx-persistent.svc.cluster.local
APP_ENV_POSTGRES_PORT: "5432"
APP_ENV_POSTGRES_USERNAME: nx_seller_operator
APP_ENV_POSTGRES_DATABASE: nx_seller_core
# Redis — cluster mode, 3 nodes (nx-broker namespace)
APP_ENV_CACHE_REDIS_MODE: cluster
APP_ENV_CACHE_REDIS_CLUSTER_NODES: "nx-redis-0.nx-redis-headless.nx-broker.svc.cluster.local:6379,nx-redis-1.nx-redis-headless.nx-broker.svc.cluster.local:6379,nx-redis-2.nx-redis-headless.nx-broker.svc.cluster.local:6379"
# Kafka — CLIENT listener with SASL/SCRAM-SHA-512 (nx-broker namespace)
APP_ENV_KAFKA_BROKERS: nx-kafka-0.nx-kafka-headless.nx-broker.svc.cluster.local:9092,nx-kafka-1.nx-kafka-headless.nx-broker.svc.cluster.local:9092,nx-kafka-2.nx-kafka-headless.nx-broker.svc.cluster.local:9092
APP_ENV_KAFKA_SASL_ENABLE: "true"
APP_ENV_KAFKA_SASL_MECHANISM: SCRAM-SHA-512
APP_ENV_KAFKA_SASL_USERNAME: nx.staging
# APP_ENV_KAFKA_SASL_PASSWORD is in nx-shared-secret
# Service discovery — nx-backend namespace
APP_ENV_IDENTITY_SERVICE_BASE_URL: http://nx-identity.nx-backend.svc.cluster.local:3000/v1/api/identity
APP_ENV_COMMERCE_SERVICE_BASE_URL: http://nx-commerce.nx-backend.svc.cluster.local:3000/v1/api/commerce
APP_ENV_PRICING_SERVICE_BASE_URL: http://nx-pricing.nx-backend.svc.cluster.local:3000/v1/api/pricing
# Domains — staging
APP_DOMAIN: sgw.staging.bana.com.vn
HOOK_DOMAIN: hook.staging.bana.com.vnapiVersion: v1
kind: ConfigMap
metadata:
name: nx-shared-config
namespace: nx-backend
data:
NODE_ENV: production
LOG_LEVEL: info
# Database — nx-persistent namespace
DB_HOST: nx-postgresql.nx-persistent.svc.cluster.local
DB_PORT: "5432"
DB_NAME: nx_seller
# Redis — nx-broker namespace
REDIS_HOST: nx-redis.nx-broker.svc.cluster.local
REDIS_PORT: "6379"
# Kafka — nx-broker headless service DNS
KAFKA_BROKERS: nx-kafka-0.nx-kafka-headless.nx-broker.svc.cluster.local:29092,nx-kafka-1.nx-kafka-headless.nx-broker.svc.cluster.local:29092,nx-kafka-2.nx-kafka-headless.nx-broker.svc.cluster.local:29092
# Typesense — nx-search namespace
TYPESENSE_HOST: nx-typesense.nx-search.svc.cluster.local
TYPESENSE_PORT: "8108"
# Service discovery — nx-backend namespace
IDENTITY_URL: http://nx-identity.nx-backend.svc.cluster.local:3000
COMMERCE_URL: http://nx-commerce.nx-backend.svc.cluster.local:3000
SALE_URL: http://nx-sale.nx-backend.svc.cluster.local:3000
SIGNAL_URL: http://nx-signal.nx-backend.svc.cluster.local:3000
PAYMENT_URL: http://nx-payment-api.nx-backend.svc.cluster.local:3000
# Domains — production (TBD)
APP_DOMAIN: TBD
HOOK_DOMAIN: TBD
# OpenTelemetry — production only
OTEL_EXPORTER_OTLP_ENDPOINT: http://nx-otel-collector.nx-watcher.svc.cluster.local:4317
OTEL_SERVICE_NAME: banaStaging vs Production ConfigMap Differences
| Key | Staging | Production |
|---|---|---|
NODE_ENV | staging | production |
APP_ENV_POSTGRES_HOST | nx-pgbouncer.nx-persistent... (PgBouncer) | nx-pgbouncer.nx-persistent... (PgBouncer) |
APP_ENV_CACHE_REDIS_MODE | cluster | cluster |
APP_ENV_KAFKA_SASL_USERNAME | nx.staging | nx.production |
APP_DOMAIN | sgw.staging.bana.com.vn | TBD |
HOOK_DOMAIN | hook.staging.bana.com.vn | TBD |
OTEL_EXPORTER_OTLP_ENDPOINT | Not set | http://nx-otel-collector.nx-watcher.svc.cluster.local:4317 |
TIP
Service discovery URLs (IDENTITY_URL, COMMERCE_URL, etc.) are the same across environments because they use cluster-internal DNS within nx-backend. Only external-facing domains differ.
Per-Service ConfigMaps
identity-specific config
# identity-specific config
apiVersion: v1
kind: ConfigMap
metadata:
name: nx-identity-config
namespace: nx-backend
data:
APP_NAME: identity
APP_PORT: "3000"
APP_BASE_PATH: /v1/api/identity
APP_ROLE: issuer
# JWKS settings
JWKS_ROTATION_INTERVAL: "86400"
TOKEN_ACCESS_EXPIRY: "3600"
TOKEN_REFRESH_EXPIRY: "604800"payment-api specific config
# payment-api specific config
apiVersion: v1
kind: ConfigMap
metadata:
name: nx-payment-api-config
namespace: nx-backend
data:
APP_NAME: payment
APP_PORT: "3000"
APP_BASE_PATH: /v1/api/payment
APP_MODE: apipayment-worker specific config
# payment-worker specific config
apiVersion: v1
kind: ConfigMap
metadata:
name: nx-payment-worker-config
namespace: nx-backend
data:
APP_NAME: payment
APP_ENV_MQ_PAY_MODE: workerDocker Compose to K8s DNS Mapping
Docker Compose (.env) | K8s ConfigMap | Notes |
|---|---|---|
DB_HOST=nx-postgresql | APP_ENV_POSTGRES_HOST=nx-pgbouncer.nx-persistent.svc.cluster.local | PgBouncer pooler, cross-namespace requires FQDN |
REDIS_HOST=nx-redis | APP_ENV_CACHE_REDIS_CLUSTER_NODES=nx-redis-{0,1,2}.nx-redis-headless.nx-broker... | Cluster mode, 3 StatefulSet pods in nx-broker |
KAFKA_BROKERS=nx-kafka-1:29092,... | APP_ENV_KAFKA_BROKERS=nx-kafka-0.nx-kafka-headless.nx-broker...:9092,... | CLIENT listener port 9092 with SASL in nx-broker |
IDENTITY_URL=http://dev-nx-identity:3000 | APP_ENV_IDENTITY_SERVICE_BASE_URL=http://nx-identity.nx-backend.svc.cluster.local:3000/v1/api/identity | Same namespace (nx-backend), short form OK too |
TIP
Within the same namespace, short DNS names work: nx-identity:3000. Cross-namespace requires the full \<svc\>.\<ns\>.svc.cluster.local form. All backend services are in nx-backend; frontend services stay in nx-app. Data services are split across nx-persistent, nx-broker, and nx-search.
Secrets Management
Staging: Manual Creation via create-secrets.sh
In staging, secrets are created manually using the create-secrets.sh script in infrastructure/deployments/staging/manifests/02-secrets/. Template YAML files define the structure; the script prompts for actual values and creates K8s secrets directly.
# Run from the staging manifests directory
cd infrastructure/deployments/staging/manifests/02-secrets/
./create-secrets.shProduction: Sealed Secrets
Sealed Secrets encrypts secrets client-side so they can be stored in Git (production only).
Workflow (Production)
# 1. Create a regular secret YAML (DO NOT commit this)
cat <<EOF > secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: nx-identity-secret
namespace: nx-backend
type: Opaque
stringData:
DB_PASSWORD: "actual-password"
JWT_SECRET: "actual-jwt-secret"
JWKS_PRIVATE_KEY: "actual-private-key"
EOF
# 2. Seal it (encrypts with cluster's public key)
kubeseal --format yaml < secret.yaml > sealed-secret.yaml
# 3. Commit the sealed version
git add sealed-secret.yaml
rm secret.yaml # Never commit the plain versionSecret Organization
| Secret | Namespace | Contents |
|---|---|---|
nx-shared-secret | nx-backend | APP_ENV_POSTGRES_PASSWORD, APP_ENV_CACHE_REDIS_PASSWORD, APP_ENV_KAFKA_SASL_PASSWORD |
nx-identity-secret | nx-backend | ES256 keys, mail credentials, SMS API keys |
nx-payment-secret | nx-backend | Payment DB & Redis credentials |
nx-commerce-secret | nx-backend | S3 credentials, VNPAY TVAN API key, Typesense key |
nx-sale-secret | nx-backend | Basic auth credentials |
nx-ledger-secret | nx-backend | Encryption key, S3 credentials |
nx-postgresql-superuser-secret | nx-persistent | Postgres superuser credentials |
nx-postgresql-app-secret | nx-persistent | App user (nx_seller_operator) credentials |
nx-postgresql-replication-secret | nx-persistent | Streaming replication credentials |
nx-redis-secret | nx-broker | REDIS_PASSWORD |
nx-kafka-jaas | nx-broker | Kafka SASL/SCRAM JAAS config |
nx-kafka-scram-secret | nx-broker | SCRAM username/password |
nx-typesense-secret | nx-search | TYPESENSE_API_KEY |
staging-bana-tls | nx-internal | TLS cert and key |
bcr-registry | nx-backend / nx-app / nx-internal | Container registry pull credentials |
INFO
The bcr-registry is an imagePullSecret required for pulling images from bcr.bana.com.vn private registry. It must be created in nx-backend, nx-app, and nx-internal and referenced in all Deployment specs or the namespace default ServiceAccount.
Pod Configuration
Pods reference both shared and service-specific configs:
Pods reference both shared and service-specific configs:
spec:
imagePullSecrets:
- name: bcr-registry
containers:
- name: identity
envFrom:
# Shared config (DB_HOST, REDIS_HOST, etc.)
- configMapRef:
name: nx-shared-config
# Service-specific config
- configMapRef:
name: nx-identity-config
# Shared secrets (DB_PASSWORD, REDIS_PASSWORD)
- secretRef:
name: nx-shared-secret
# Service-specific secrets
- secretRef:
name: nx-identity-secret
env:
# Downward API for Snowflake ID
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.nameConfiguration Hierarchy
Pod Environment
├── nx-shared-config (ConfigMap) — shared across all services
├── nx-<service>-config (ConfigMap) — service-specific overrides
├── nx-shared-secret (Secret) — shared credentials
└── nx-<service>-secret (Secret) — service-specific credentialsLater entries override earlier ones when keys conflict. This allows:
nx-shared-configsetsDB_HOSTfor all servicesnx-identity-configcan override any shared key if needed
Namespace Reference
| Namespace | Purpose | ConfigMap/Secret scope |
|---|---|---|
nx-internal | nginx-ingress, Traefik, cert-manager, API Portal | TLS certs, gateway configs, registry creds |
nx-backend | Backend services | Backend configs, backend secrets, registry creds |
nx-app | Frontend apps | Frontend configs, registry creds |
nx-persistent | PostgreSQL | Database credentials |
nx-broker | Redis, Kafka | Broker credentials |
nx-search | Typesense | Search credentials |
nx-watcher | Observability stack | Monitoring configs |
Environment Validation
Each service validates its environment at startup using Zod schemas (IGNIS pattern). If required vars are missing, the pod fails fast with a clear error in logs.
// packages/identity/src/config/environment.ts
const envSchema = z.object({
APP_ENV_POSTGRES_HOST: z.string(),
APP_ENV_POSTGRES_PORT: z.coerce.number(),
APP_ENV_POSTGRES_DATABASE: z.string(),
APP_ENV_POSTGRES_USERNAME: z.string(),
// ... all required vars
});This is the same validation used in Docker Compose — no changes needed.