Skip to content

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.

yaml
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.vn
yaml
apiVersion: 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: bana

Staging vs Production ConfigMap Differences

KeyStagingProduction
NODE_ENVstagingproduction
APP_ENV_POSTGRES_HOSTnx-pgbouncer.nx-persistent... (PgBouncer)nx-pgbouncer.nx-persistent... (PgBouncer)
APP_ENV_CACHE_REDIS_MODEclustercluster
APP_ENV_KAFKA_SASL_USERNAMEnx.stagingnx.production
APP_DOMAINsgw.staging.bana.com.vnTBD
HOOK_DOMAINhook.staging.bana.com.vnTBD
OTEL_EXPORTER_OTLP_ENDPOINTNot sethttp://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
yaml
# 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
yaml
# 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: api
payment-worker specific config
yaml
# 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: worker

Docker Compose to K8s DNS Mapping

Docker Compose (.env)K8s ConfigMapNotes
DB_HOST=nx-postgresqlAPP_ENV_POSTGRES_HOST=nx-pgbouncer.nx-persistent.svc.cluster.localPgBouncer pooler, cross-namespace requires FQDN
REDIS_HOST=nx-redisAPP_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:3000APP_ENV_IDENTITY_SERVICE_BASE_URL=http://nx-identity.nx-backend.svc.cluster.local:3000/v1/api/identitySame 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.

bash
# Run from the staging manifests directory
cd infrastructure/deployments/staging/manifests/02-secrets/
./create-secrets.sh

Production: Sealed Secrets

Sealed Secrets encrypts secrets client-side so they can be stored in Git (production only).

Workflow (Production)

bash
# 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 version

Secret Organization

SecretNamespaceContents
nx-shared-secretnx-backendAPP_ENV_POSTGRES_PASSWORD, APP_ENV_CACHE_REDIS_PASSWORD, APP_ENV_KAFKA_SASL_PASSWORD
nx-identity-secretnx-backendES256 keys, mail credentials, SMS API keys
nx-payment-secretnx-backendPayment DB & Redis credentials
nx-commerce-secretnx-backendS3 credentials, VNPAY TVAN API key, Typesense key
nx-sale-secretnx-backendBasic auth credentials
nx-ledger-secretnx-backendEncryption key, S3 credentials
nx-postgresql-superuser-secretnx-persistentPostgres superuser credentials
nx-postgresql-app-secretnx-persistentApp user (nx_seller_operator) credentials
nx-postgresql-replication-secretnx-persistentStreaming replication credentials
nx-redis-secretnx-brokerREDIS_PASSWORD
nx-kafka-jaasnx-brokerKafka SASL/SCRAM JAAS config
nx-kafka-scram-secretnx-brokerSCRAM username/password
nx-typesense-secretnx-searchTYPESENSE_API_KEY
staging-bana-tlsnx-internalTLS cert and key
bcr-registrynx-backend / nx-app / nx-internalContainer 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:
yaml
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.name

Configuration 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 credentials

Later entries override earlier ones when keys conflict. This allows:

  • nx-shared-config sets DB_HOST for all services
  • nx-identity-config can override any shared key if needed

Namespace Reference

NamespacePurposeConfigMap/Secret scope
nx-internalnginx-ingress, Traefik, cert-manager, API PortalTLS certs, gateway configs, registry creds
nx-backendBackend servicesBackend configs, backend secrets, registry creds
nx-appFrontend appsFrontend configs, registry creds
nx-persistentPostgreSQLDatabase credentials
nx-brokerRedis, KafkaBroker credentials
nx-searchTypesenseSearch credentials
nx-watcherObservability stackMonitoring 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.

typescript
// 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.

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.