No description
  • Python 72.1%
  • Shell 27.9%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Frank Solli 31434a219a
All checks were successful
Generate README / generate-readme (push) Successful in 20s
docs: add Mermaid architecture diagram to README
2026-07-14 12:51:33 +02:00
.forgejo/workflows feat: replace Graphviz with Mermaid diagram (rendered natively by Forgejo/GitHub) 2026-07-13 23:28:28 +02:00
apps feat: add OpenBao-based pgEdge cert sync pipeline 2026-07-13 15:51:34 +02:00
bootstrap feat: replace standalone CNPG with pgEdge active-active Spock replication 2026-07-13 15:23:31 +02:00
docs feat: replace Graphviz with Mermaid diagram (rendered natively by Forgejo/GitHub) 2026-07-13 23:28:28 +02:00
manifests feat: wire app to CNPG secret for DB connection (pgEdge mode) 2026-07-13 17:06:43 +02:00
monitoring set slack channel to #varsel-pjd-test for test 2026-06-29 11:42:18 +02:00
scripts feat: replace Graphviz with Mermaid diagram (rendered natively by Forgejo/GitHub) 2026-07-13 23:28:28 +02:00
README.md docs: add Mermaid architecture diagram to README 2026-07-14 12:51:33 +02:00
README.md.j2 docs: add Mermaid architecture diagram to README 2026-07-14 12:51:33 +02:00

dd-mani-viti

Kubernetes manifests for Dokumentdeling REST API on the Viti platform.

Table of Contents

Architecture

flowchart TD
    subgraph External
        Client[Client / Helsenett]
        OpenBao[OpenBao Vault]
        S3[S3 Backup Storage]
    end

    subgraph Cluster["Kubernetes Cluster"]
        subgraph NS["Namespace: dokumentdeling"]
            GW[Gateway<br/>hnet-private VIP]
            HR[HTTPRoute]
            SVC[Service :8443]
            Deploy[Deployment<br/>dokumentdeling-api]
            CM[ConfigMap]
            SA[ServiceAccount]
            NP[NetworkPolicy]
            Cert[internal-cert<br/>cert-manager]
            SecretApp[dokumentdeling-secret]
            SecretIPAM[ipam-secret]

            subgraph DB["PostgreSQL (CNPG + pgEdge)"]
                PG[Cluster<br/>pgEdge Spock]
                PGSvc[Replication LB<br/>MetalLB]
                PGMon[PodMonitor]
                PGBackup[ScheduledBackup]
            end

            subgraph Secrets["OpenBao CSI Sync"]
                SPC_App[SecretProviderClass<br/>app secrets]
                SPC_S3[SecretProviderClass<br/>S3 creds]
                SPC_Certs[SecretProviderClass<br/>pgEdge certs]
            end

            MigJob[DB Migration Job]
        end
    end

    Client -->|HTTPS| GW
    GW --> HR --> SVC --> Deploy
    Deploy --- CM
    Deploy --- SA
    Deploy --- Cert
    Deploy --- SecretApp
    Deploy -.->|connects| PG
    MigJob -.->|migrates| PG

    OpenBao -->|CSI sync| SPC_App --> SecretApp
    OpenBao -->|CSI sync| SPC_S3 --> PGBackup
    OpenBao -->|CSI sync| SPC_Certs --> PG
    OpenBao -->|IPAM secret| SecretIPAM --> GW

    PG --> PGSvc
    PGSvc <-->|Spock replication| External
    PGBackup -->|WAL + base| S3
    PGMon -.->|metrics| PG

    NP -.->|controls| Deploy
    NP -.->|controls| PG

Repo Structure

dd-mani-viti/
├── apps/           ArgoCD App of Apps (parent application)
├── manifests/      Main API Helm chart
├── monitoring/     Prometheus probes, alerts and Alertmanager configs
└── scripts/        Operational scripts (cert push, README generation)

Manifests Reference

manifests/templates/ — all Kubernetes resources deployed by the API chart
File What it does Key tunables
namespace.yaml Creates the dokumentdeling namespace -
serviceaccount.yaml dokumentdeling-sa SA for pods -
configmap.yaml Static env vars (ASPNETCORE_ENVIRONMENT, ASPNETCORE_URLS) -
deployment.yaml Main API deployment replicaCount, api.image, api.envVariables
service.yaml ClusterIP service on port 8443 api.service.port
httproute.yaml Gateway API route for external traffic api.hosts, gateway.ipZone, gateway.clusterIssuer
networkpolicy.yaml Egress/ingress firewall rules Allows DNS, all external, and pgEdge on 5432
internal-cert.yaml Self-signed TLS cert for pod-to-pod HTTPS internalSsl.commonName, internalSsl.names
openbao-kubernetes-auth.yaml SA + Secret + ClusterRoleBinding for OpenBao auth -
openbao-secrets.yaml SecretProviderClass: syncs app secrets from OpenBao openbao.kubernetesMountPath
database-migration-job.yaml Runs DB migrations before deploy (wave -1) databaseMigration.enabled
job-reader-role.yaml RBAC Role to read Job status -
job-reader-rolebinding.yaml Binds job-reader to the SA -
PostgreSQL
postgres.yaml CNPG Cluster (only when role != pgedge) postgres.instances, postgres.storage.size
pg-replication-svc.yaml MetalLB LoadBalancer for Spock cross-cluster traffic postgres.pgedge.loadBalancerIP
pg-podmonitor.yaml Prometheus PodMonitor for CNPG metrics postgres.monitoring.enabled
pg-scheduled-backup.yaml Daily S3 backup via Barman (when backup enabled) postgres.backup.bucket, postgres.backup.endpointURL
openbao-pg-s3.yaml SecretProviderClass for S3 backup credentials postgres.backup.secretPath
openbao-pgedge-certs.yaml SecretProviderClass: syncs pgEdge TLS certs from OpenBao (non-TRD only) -
pgedge-cert-sync-job.yaml Job that triggers CSI cert sync (non-TRD only) -
manifests/values — key values you might want to tune
Value Default Dev override What it controls
replicaCount 2 3 Number of API pods
postgres.role primary pgedge DB mode: standalone or active-active
postgres.pgedge.localNode n1 set per cluster Which pgEdge node this cluster runs
postgres.storage.size 10Gi 5Gi PVC size for PostgreSQL
postgres.monitoring.enabled true true Enable PodMonitor for Prometheus
databaseMigration.enabled true false Run migration Job on sync
openbao.kubernetesMountPath kubernetes per cluster OpenBao auth mount for this cluster
gateway.ipamSecret "" dokumentdeling-ipam-secret Multi-cluster VIP failover
api.image - sha256 digest Container image for the API
api.hosts [] [viti-api.dev.pjd.nhn.no] External hostnames

Environments

Env Hosts Clusters Failover Auto-sync
dev viti-api.dev.pjd.nhn.no d-osl-dd-viti, d-trd-dd-viti IPAM shared VIP yes
test viti-api.pjd.test.nhn.no t-trd-dd-viti - yes
qa viti-api.pjd.qa.nhn.no q-trd-dd-viti - yes
prod api.pjd.nhn.no p-dd-000 (Tanzu) - no

Cluster details

Cluster Location Vitistack ID OpenBao auth mount pgEdge node MetalLB IP
d-osl-dd-viti OSL vitistack-dokdel-xb9h dokumentdeling-auth-mount-osl n2 10.204.153.179
d-trd-dd-viti TRD vitistack-dokdeling-qr46 dokumentdeling-auth-mount n1 10.204.153.180
prod PROD `` `` - -
qa QA `` `` - -
test TEST `` `` - -

PostgreSQL (pgEdge Active-Active)

The database layer uses pgEdge with Spock logical replication for active-active multi-master across clusters. Each cluster runs an independent CNPG-managed PostgreSQL instance that replicates bidirectionally.

Architecture

graph LR
n2["d-osl-dd-viti<br/>dokumentdeling-n2<br/>LB: 10.204.153.179"]
n1["d-trd-dd-viti<br/>dokumentdeling-n1<br/>LB: 10.204.153.180"]
n1 <-->|"Spock replication"| n2

Cluster Details

Property OSL (n2) TRD (n1)
CNPG Cluster dokumentdeling-n2 dokumentdeling-n1
Spock node n2 n1
MetalLB IP 10.204.153.179 10.204.153.180
Image ncr.sky.nhn.no/ghcr/pgedge/pgedge-postgres:16-spock5-standard same
Storage 5Gi (cephfs) same
Cert source cert-manager OpenBao
Extensions Spock 5.x + Snowflake same

Connection Details

Applications connect using the CNPG-generated secret:

Cluster Secret Name Service (rw) Database User
OSL dokumentdeling-n2-app dokumentdeling-n2-rw.dokumentdeling:5432 app app
TRD dokumentdeling-n1-app dokumentdeling-n1-rw.dokumentdeling:5432 app app

The secret contains keys: host, port, dbname, user, password, uri, jdbc-uri.

How the app gets its connection string

In pgEdge mode (dev), the deployment builds ConnectionStrings__DefaultConnection directly from the CNPG secret -- no manual connection string in OpenBao needed:

ConnectionStrings__DefaultConnection = "Host=$(DB_HOST);Port=5432;Database=app;Username=$(DB_USER);Password=$(DB_PASSWORD)"
Env var Source
DB_HOST (OSL) dokumentdeling-n2-apphost
DB_USER (OSL) dokumentdeling-n2-appusername
DB_PASSWORD (OSL) dokumentdeling-n2-apppassword
DB_HOST (TRD) dokumentdeling-n1-apphost
DB_USER (TRD) dokumentdeling-n1-appusername
DB_PASSWORD (TRD) dokumentdeling-n1-apppassword

In non-pgEdge envs (test/qa/prod), ConnectionStrings__DefaultConnection is fetched from OpenBao (kv-v2-dokumentdeling/data/shared) as before.

Cross-Cluster Networking

Clusters have overlapping pod CIDRs so Cilium ClusterMesh cannot be used. Instead, MetalLB LoadBalancer services with routable IPs in the 10.204.153.0/24 range provide connectivity for Spock replication on port 5432.

TLS Certificates

pgEdge uses mutual TLS for replication. The first cluster (OSL) provisions certificates via cert-manager, then they are pushed to OpenBao and synced to other clusters via the CSI Secrets Store driver.

Secret Purpose
client-ca-key-pair Client CA (shared across all clusters)
admin-client-cert Admin client cert
app-client-cert App client cert
pgedge-client-cert pgEdge replication client cert
streaming-replica-client-cert Streaming replica cert

OpenBao path: kv-v2-dokumentdeling/data/pgedge-certs/dokumentdeling

Pushing certs to OpenBao (one-time after first cluster deploy)

export BAO_ADDR=https://openbao.test.nhn.no
export BAO_NAMESPACE=PJD
bao login -method=oidc
./scripts/push-pgedge-certs-to-openbao.sh

Wiring Spock Replication

After all clusters are deployed, create Spock nodes and subscriptions.

Step 1: Create Spock nodes
# OSL (n2)
kubectl --context d-osl-dd-viti -n dokumentdeling exec dokumentdeling-n2-1 -c postgres -- \
  psql -U postgres -d app -c "SELECT spock.node_create('n2', 'host=10.204.153.179 port=5432 dbname=app user=pgedge sslmode=require sslcert=/projected/pgedge/certificates/tls.crt sslkey=/projected/pgedge/certificates/tls.key');"
# TRD (n1)
kubectl --context d-trd-dd-viti -n dokumentdeling exec dokumentdeling-n1-1 -c postgres -- \
  psql -U postgres -d app -c "SELECT spock.node_create('n1', 'host=10.204.153.180 port=5432 dbname=app user=pgedge sslmode=require sslcert=/projected/pgedge/certificates/tls.crt sslkey=/projected/pgedge/certificates/tls.key');"
Step 2: Create subscriptions (each node subscribes to every other)
# OSL subscribes to TRD
kubectl --context d-osl-dd-viti -n dokumentdeling exec dokumentdeling-n2-1 -c postgres -- \
  psql -U postgres -d app -c "SELECT spock.sub_create('sub_n1_n2', 'host=10.204.153.180 port=5432 dbname=app user=pgedge sslmode=require sslcert=/projected/pgedge/certificates/tls.crt sslkey=/projected/pgedge/certificates/tls.key', '{default,default_insert_only,ddl_sql}', synchronize_data := true);"
# TRD subscribes to OSL
kubectl --context d-trd-dd-viti -n dokumentdeling exec dokumentdeling-n1-1 -c postgres -- \
  psql -U postgres -d app -c "SELECT spock.sub_create('sub_n2_n1', 'host=10.204.153.179 port=5432 dbname=app user=pgedge sslmode=require sslcert=/projected/pgedge/certificates/tls.crt sslkey=/projected/pgedge/certificates/tls.key', '{default,default_insert_only,ddl_sql}', synchronize_data := true);"
Verify replication status
# OSL
kubectl --context d-osl-dd-viti -n dokumentdeling exec dokumentdeling-n2-1 -c postgres -- \
  psql -U postgres -d app -c "SELECT * FROM spock.sub_show_status();"
# TRD
kubectl --context d-trd-dd-viti -n dokumentdeling exec dokumentdeling-n1-1 -c postgres -- \
  psql -U postgres -d app -c "SELECT * FROM spock.sub_show_status();"

All subscriptions should show status = replicating.

Managed Roles

Role Capabilities
app Default application user (owner of app database)
admin Superuser for administration
pgedge Superuser + replication (used by Spock)

Bootstrapping a Cluster

Each cluster has a bootstrap manifest in bootstrap/:

bootstrap/
├── dev-osl.yaml
├── dev-trd.yaml
├── prod.yaml
├── qa.yaml
├── test.yaml
Apply bootstrap per cluster
# OSL (d-osl-dd-viti)
kubectl apply -f bootstrap/dev-osl.yaml --context d-osl-dd-viti
# TRD (d-trd-dd-viti)
kubectl apply -f bootstrap/dev-trd.yaml --context d-trd-dd-viti
# PROD (prod)
kubectl apply -f bootstrap/prod.yaml --context prod
# QA (qa)
kubectl apply -f bootstrap/qa.yaml --context qa
# TEST (test)
kubectl apply -f bootstrap/test.yaml --context test

Cluster-specific parameters

Each bootstrap file sets these Helm parameters to tell the apps which cluster they're on:

d-osl-dd-viti (OSL)
Parameter Value
vitistackName vitistack-dokdel-xb9h
clusterName d-osl-dd-viti
openbaoMountPath dokumentdeling-auth-mount-osl
postgresSite cluster-osl
autoSync true
d-trd-dd-viti (TRD)
Parameter Value
vitistackName vitistack-dokdeling-qr46
clusterName d-trd-dd-viti
openbaoMountPath dokumentdeling-auth-mount
postgresSite cluster-trd
autoSync true
prod (PROD)
Parameter Value
vitistackName ``
clusterName prod
openbaoMountPath ``
postgresSite ``
autoSync true
qa (QA)
Parameter Value
vitistackName ``
clusterName qa
openbaoMountPath ``
postgresSite ``
autoSync true
test (TEST)
Parameter Value
vitistackName ``
clusterName test
openbaoMountPath ``
postgresSite ``
autoSync true

ArgoCD then manages child applications:

  • dokumentdeling-api (from manifests/)
  • dokumentdeling-monitoring (from monitoring/)
  • dokumentdeling-pgedge (pgEdge Helm chart from pgedge.github.io)
  • nhn-appmarket (CSI driver, OpenBao provider, k8sapibridge, CNPG operator)

Rendering Templates Locally

Helm template commands
# API manifests (dev)
helm template dokumentdeling manifests/ \
  -f manifests/values.yaml \
  -f manifests/values.dev.yaml

# Apps chart (per cluster)

# OSL
helm template dokumentdeling-apps apps/ \
  -f apps/values.yaml \
  -f apps/values.dev.yaml \
  --set postgresSite=cluster-osl \
  --set openbaoMountPath=dokumentdeling-auth-mount-osl

# TRD
helm template dokumentdeling-apps apps/ \
  -f apps/values.yaml \
  -f apps/values.dev.yaml \
  --set postgresSite=cluster-trd \
  --set openbaoMountPath=dokumentdeling-auth-mount

# PROD
helm template dokumentdeling-apps apps/ \
  -f apps/values.yaml \
  -f apps/values.prod.yaml \
  --set postgresSite= \
  --set openbaoMountPath=

# QA
helm template dokumentdeling-apps apps/ \
  -f apps/values.yaml \
  -f apps/values.qa.yaml \
  --set postgresSite= \
  --set openbaoMountPath=

# TEST
helm template dokumentdeling-apps apps/ \
  -f apps/values.yaml \
  -f apps/values.test.yaml \
  --set postgresSite= \
  --set openbaoMountPath=

Secrets Management

OpenBao (CSI Secrets Store)

Env OpenBao Server Namespace Auth Mounts KV Path
dev openbao.test.nhn.no PJD dokumentdeling-auth-mount-osl, dokumentdeling-auth-mount kv-v2-dokumentdeling/data/shared
test/qa openbao.test.nhn.no PJD kubernetes kv-v2-dokumentdeling/data/shared
prod openbao.nhn.no PJD kubernetes kv-v2-dokumentdeling/data/shared

Secrets synced to Kubernetes Secrets

K8s Secret Name Keys Purpose
dokumentdeling-secret All app secrets envFrom in deployment
dokumentdeling-api-slack-alertmanagerconfig-secret apiSecret, apiUrl Alertmanager Slack bot
dokumentdeling-ipam-secret secret IPAM shared secret for failover
admin-client-cert tls.crt, tls.key, ca.crt pgEdge admin cert (non-primary clusters)
client-ca-key-pair tls.crt, tls.key, ca.crt pgEdge shared CA (non-primary clusters)
pgedge-client-cert tls.crt, tls.key, ca.crt pgEdge replication cert (non-primary clusters)

CNPG-managed secrets (auto-generated per cluster)

Secret Keys Purpose
dokumentdeling-n2-app host, port, dbname, user, password, uri, jdbc-uri OSL DB credentials
dokumentdeling-n1-app host, port, dbname, user, password, uri, jdbc-uri TRD DB credentials

ArgoCD Sync Waves

Wave Resources
-4 OpenBao pgEdge certs SecretProviderClass (non-primary clusters)
-3 pgEdge cert-sync Job (non-primary clusters)
-2 Namespace, ServiceAccount, Secrets, Service, Cert, RBAC, ConfigMap, NetworkPolicy, LoadBalancer
-1 Database migration Job
0 Deployment, HTTPRoutes, PodMonitor

Monitoring & Alerting

The monitoring/ chart installs:

  • Blackbox Probe -- HTTP health check against /health
  • PrometheusRule with alerts: SlowHttp, HttpFailure, ProbeFailed, SSLCertExpiringSoon
  • AlertmanagerConfig (OS Dashboard) -- webhook to NHN OS Dashboard
  • AlertmanagerConfig (Slack) -- posts to Slack via bot token
  • PodMonitor for CNPG PostgreSQL metrics

Network Access

The API is published on Helsenett private (ipam.vitistack.io/zone: hnet-private) with security policy expose-nhn-office-client. The NetworkPolicy allows:

  • Ingress on port 8443
  • Egress to DNS (kube-system), all external IPs, and PostgreSQL (port 5432)