Skip to content

Kubernetes Deployment

This guide covers deploying FluxiQ PIX to a Kubernetes cluster for production workloads.

Prerequisites

  • Kubernetes cluster >= 1.28
  • kubectl configured with cluster access
  • Helm >= 3.14 (for dependency charts)
  • Container registry with built images
  • TLS certificates (ICP-Brasil) stored as Kubernetes Secrets

Namespace Setup

bash
# Create the PIX namespace
kubectl create namespace pix

# Set as default context
kubectl config set-context --current --namespace=pix

Secrets

Database Credentials

yaml
# k8s/secrets/database.yaml
apiVersion: v1
kind: Secret
metadata:
  name: pix-database
  namespace: pix
type: Opaque
stringData:
  DATABASE_URL: "postgres://pix_user:secure-password@pix-postgres:5432/pix_prod"

ICP-Brasil Certificates

bash
# Create secret from certificate files
kubectl create secret generic bacen-certs \
  --namespace pix \
  --from-file=client.pem=./certs/client.pem \
  --from-file=client_key.pem=./certs/client_key.pem \
  --from-file=ca_chain.pem=./certs/ca_chain.pem \
  --from-file=bacen_ca.pem=./certs/bacen_ca.pem

Application Secrets

yaml
# k8s/secrets/app.yaml
apiVersion: v1
kind: Secret
metadata:
  name: pix-app-secrets
  namespace: pix
type: Opaque
stringData:
  SECRET_KEY_BASE: "your-64-char-minimum-secret-key-base-here-for-production-use"
  ISPB: "12345678"

ConfigMap

yaml
# k8s/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: pix-config
  namespace: pix
data:
  DICT_BASE_URL: "https://dict.pi.rsfn.net.br:16522/api/v2"
  SPI_BASE_URL: "https://spi.pi.rsfn.net.br:16522"
  REDIS_URL: "redis://pix-redis-master:6379/0"
  NATS_URL: "nats://pix-nats:4222"
  CERT_PATH: "/certs/client.pem"
  KEY_PATH: "/certs/client_key.pem"
  CA_PATH: "/certs/ca_chain.pem"
  LOG_LEVEL: "info"
  LOG_FORMAT: "json"

Service Deployments

DICT Service

yaml
# k8s/dict-service.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: dict-service
  namespace: pix
  labels:
    app: dict-service
    tier: backend
spec:
  replicas: 3
  selector:
    matchLabels:
      app: dict-service
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    metadata:
      labels:
        app: dict-service
        tier: backend
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "9568"
    spec:
      serviceAccountName: pix-service
      securityContext:
        runAsNonRoot: true
        runAsUser: 1000
        fsGroup: 1000
      containers:
        - name: dict-service
          image: registry.example.com/pix/dict-service:1.0.0
          ports:
            - containerPort: 4001
              name: http
              protocol: TCP
            - containerPort: 9568
              name: metrics
              protocol: TCP
          envFrom:
            - configMapRef:
                name: pix-config
            - secretRef:
                name: pix-database
            - secretRef:
                name: pix-app-secrets
          env:
            - name: PHX_PORT
              value: "4001"
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: POD_IP
              valueFrom:
                fieldRef:
                  fieldPath: status.podIP
          volumeMounts:
            - name: certs
              mountPath: /certs
              readOnly: true
          resources:
            requests:
              cpu: "500m"
              memory: "512Mi"
            limits:
              cpu: "2000m"
              memory: "2Gi"
          livenessProbe:
            httpGet:
              path: /health
              port: http
            initialDelaySeconds: 15
            periodSeconds: 10
            timeoutSeconds: 5
            failureThreshold: 3
          readinessProbe:
            httpGet:
              path: /health/ready
              port: http
            initialDelaySeconds: 10
            periodSeconds: 5
            timeoutSeconds: 3
            failureThreshold: 3
          startupProbe:
            httpGet:
              path: /health
              port: http
            initialDelaySeconds: 5
            periodSeconds: 5
            failureThreshold: 30
      volumes:
        - name: certs
          secret:
            secretName: bacen-certs
            defaultMode: 0400
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app: dict-service
---
apiVersion: v1
kind: Service
metadata:
  name: dict-service
  namespace: pix
  labels:
    app: dict-service
spec:
  selector:
    app: dict-service
  ports:
    - name: http
      port: 4001
      targetPort: http
    - name: metrics
      port: 9568
      targetPort: metrics
  type: ClusterIP

SPI Service

yaml
# k8s/spi-service.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: spi-service
  namespace: pix
  labels:
    app: spi-service
    tier: backend
spec:
  replicas: 3
  selector:
    matchLabels:
      app: spi-service
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    metadata:
      labels:
        app: spi-service
        tier: backend
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "9568"
    spec:
      serviceAccountName: pix-service
      securityContext:
        runAsNonRoot: true
        runAsUser: 1000
        fsGroup: 1000
      containers:
        - name: spi-service
          image: registry.example.com/pix/spi-service:1.0.0
          ports:
            - containerPort: 4002
              name: http
            - containerPort: 9568
              name: metrics
          envFrom:
            - configMapRef:
                name: pix-config
            - secretRef:
                name: pix-database
            - secretRef:
                name: pix-app-secrets
          env:
            - name: PHX_PORT
              value: "4002"
          volumeMounts:
            - name: certs
              mountPath: /certs
              readOnly: true
          resources:
            requests:
              cpu: "1000m"
              memory: "1Gi"
            limits:
              cpu: "4000m"
              memory: "4Gi"
          livenessProbe:
            httpGet:
              path: /health
              port: http
            initialDelaySeconds: 15
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /health/ready
              port: http
            initialDelaySeconds: 10
            periodSeconds: 5
      volumes:
        - name: certs
          secret:
            secretName: bacen-certs
            defaultMode: 0400
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app: spi-service
---
apiVersion: v1
kind: Service
metadata:
  name: spi-service
  namespace: pix
spec:
  selector:
    app: spi-service
  ports:
    - name: http
      port: 4002
      targetPort: http
    - name: metrics
      port: 9568
      targetPort: metrics
  type: ClusterIP

Settlement Service

yaml
# k8s/settlement-service.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: settlement-service
  namespace: pix
  labels:
    app: settlement-service
    tier: backend
spec:
  replicas: 3
  selector:
    matchLabels:
      app: settlement-service
  template:
    metadata:
      labels:
        app: settlement-service
        tier: backend
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "9568"
    spec:
      serviceAccountName: pix-service
      securityContext:
        runAsNonRoot: true
        runAsUser: 1000
        fsGroup: 1000
      containers:
        - name: settlement-service
          image: registry.example.com/pix/settlement-service:1.0.0
          ports:
            - containerPort: 4003
              name: http
            - containerPort: 9568
              name: metrics
          envFrom:
            - configMapRef:
                name: pix-config
            - secretRef:
                name: pix-database
            - secretRef:
                name: pix-app-secrets
          env:
            - name: PHX_PORT
              value: "4003"
            - name: DICT_SERVICE_URL
              value: "http://dict-service:4001"
            - name: SPI_SERVICE_URL
              value: "http://spi-service:4002"
          volumeMounts:
            - name: certs
              mountPath: /certs
              readOnly: true
          resources:
            requests:
              cpu: "500m"
              memory: "512Mi"
            limits:
              cpu: "2000m"
              memory: "2Gi"
          livenessProbe:
            httpGet:
              path: /health
              port: http
            initialDelaySeconds: 15
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /health/ready
              port: http
            initialDelaySeconds: 10
            periodSeconds: 5
      volumes:
        - name: certs
          secret:
            secretName: bacen-certs
            defaultMode: 0400
---
apiVersion: v1
kind: Service
metadata:
  name: settlement-service
  namespace: pix
spec:
  selector:
    app: settlement-service
  ports:
    - name: http
      port: 4003
      targetPort: http
    - name: metrics
      port: 9568
      targetPort: metrics
  type: ClusterIP

Ingress

yaml
# k8s/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: pix-ingress
  namespace: pix
  annotations:
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.ingress.kubernetes.io/proxy-body-size: "10m"
    nginx.ingress.kubernetes.io/rate-limit: "100"
    nginx.ingress.kubernetes.io/rate-limit-window: "1m"
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - pix.example.com
        - admin.pix.example.com
        - docs.pix.example.com
      secretName: pix-tls
  rules:
    - host: pix.example.com
      http:
        paths:
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: settlement-service
                port:
                  number: 4003
          - path: /
            pathType: Prefix
            backend:
              service:
                name: user-portal
                port:
                  number: 8080
    - host: admin.pix.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: admin-portal
                port:
                  number: 8080
    - host: docs.pix.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: docs-site
                port:
                  number: 8080

Horizontal Pod Autoscaler

yaml
# k8s/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: dict-service-hpa
  namespace: pix
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: dict-service
  minReplicas: 3
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70
    - type: Resource
      resource:
        name: memory
        target:
          type: Utilization
          averageUtilization: 80
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 60
      policies:
        - type: Pods
          value: 2
          periodSeconds: 60
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
        - type: Pods
          value: 1
          periodSeconds: 120
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: spi-service-hpa
  namespace: pix
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: spi-service
  minReplicas: 3
  maxReplicas: 15
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 65
    - type: Resource
      resource:
        name: memory
        target:
          type: Utilization
          averageUtilization: 75
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 30
      policies:
        - type: Pods
          value: 3
          periodSeconds: 30
    scaleDown:
      stabilizationWindowSeconds: 300
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: settlement-service-hpa
  namespace: pix
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: settlement-service
  minReplicas: 3
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70

Pod Disruption Budgets

Ensure availability during cluster maintenance:

yaml
# k8s/pdb.yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: dict-service-pdb
  namespace: pix
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: dict-service
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: spi-service-pdb
  namespace: pix
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: spi-service
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: settlement-service-pdb
  namespace: pix
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: settlement-service

Infrastructure Dependencies

PostgreSQL (via Helm)

bash
helm repo add bitnami https://charts.bitnami.com/bitnami

helm install pix-postgres bitnami/postgresql \
  --namespace pix \
  --set auth.postgresPassword=secure-password \
  --set auth.database=pix_prod \
  --set primary.resources.requests.cpu=1000m \
  --set primary.resources.requests.memory=2Gi \
  --set primary.persistence.size=100Gi \
  --set metrics.enabled=true

Redis (via Helm)

bash
helm install pix-redis bitnami/redis \
  --namespace pix \
  --set architecture=standalone \
  --set auth.enabled=false \
  --set master.resources.requests.cpu=500m \
  --set master.resources.requests.memory=1Gi \
  --set master.persistence.size=10Gi \
  --set metrics.enabled=true

NATS (via Helm)

bash
helm repo add nats https://nats-io.github.io/k8s/helm/charts/

helm install pix-nats nats/nats \
  --namespace pix \
  --set config.jetstream.enabled=true \
  --set config.jetstream.memStorage.size=1Gi \
  --set config.jetstream.fileStorage.size=10Gi

Monitoring

ServiceMonitor for Prometheus

yaml
# k8s/monitoring/service-monitor.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: pix-services
  namespace: pix
  labels:
    release: prometheus
spec:
  selector:
    matchLabels:
      tier: backend
  endpoints:
    - port: metrics
      interval: 15s
      path: /metrics

Grafana Dashboard

Import the PIX dashboard from k8s/monitoring/grafana-dashboard.json which includes:

  • Transaction throughput (TPS) per service
  • P50/P95/P99 latency histograms
  • Error rates by endpoint
  • Settlement position over time
  • Pod resource utilization
  • BACEN API latency and error rates

Deployment Commands

bash
# Apply all manifests
kubectl apply -f k8s/secrets/
kubectl apply -f k8s/configmap.yaml
kubectl apply -f k8s/dict-service.yaml
kubectl apply -f k8s/spi-service.yaml
kubectl apply -f k8s/settlement-service.yaml
kubectl apply -f k8s/ingress.yaml
kubectl apply -f k8s/hpa.yaml
kubectl apply -f k8s/pdb.yaml

# Check rollout status
kubectl rollout status deployment/dict-service -n pix
kubectl rollout status deployment/spi-service -n pix
kubectl rollout status deployment/settlement-service -n pix

# Verify pods are running
kubectl get pods -n pix -l tier=backend

# Run database migrations (one-time job)
kubectl create job --from=cronjob/pix-migrate pix-migrate-manual -n pix

Rolling Updates

bash
# Update a service image
kubectl set image deployment/dict-service \
  dict-service=registry.example.com/pix/dict-service:1.1.0 \
  -n pix

# Monitor the rollout
kubectl rollout status deployment/dict-service -n pix

# Rollback if needed
kubectl rollout undo deployment/dict-service -n pix

FluxiQ PIX - Plataforma Brasileira de Pagamento Instantaneo