Skip to main content
Version: 2.0-rc

Configuration

Repository Structure

civitas-core-deployment/
├── defaults/ # Default configuration (do not modify)
│ ├── environment/ # Global defaults, charts, images, secrets
│ │ ├── global.yaml # Master configuration file
│ │ └── *.yaml.gotmpl # Auto-aggregated component configs
│ └── deployment/ # Default deployment scaffolding
├── components/ # Component definitions (14 components)
│ ├── prepare/ # Cluster preparation hooks
│ ├── secrets/ # Secret generation
│ ├── postgres/ # PostgreSQL (CloudNativePG)
│ ├── etcd/ # ETCD key-value store
│ ├── kafka/ # Apache Kafka (Strimzi)
│ ├── keycloak/ # Identity & access management
│ ├── apisix/ # API gateway
│ ├── redpanda-connect/ # Data streaming pipelines
│ ├── portal/ # Web UI and backend API
│ ├── config-adapters/ # Configuration management
│ ├── opa/ # Open Policy Agent
│ └── authz-repo/ # Authorization repository
├── deployment/ # Your instance-specific configuration
│ ├── helmfile.yaml # Main entry point for Helmfile
│ └── environments/ # Environment-specific overrides
│ └── local/
├── dev-deployment/ # Local cluster setup scripts
├── helmfile-root.yaml.gotmpl # Root Helmfile template
└── helmfile-components.yaml.gotmpl # Component iteration template

Configuration Hierarchy

Configuration values are resolved in the following order (lowest to highest precedence):

  1. defaults/environment/global.yaml - Global defaults (domain, profile, components list)
  2. defaults/environment/*.yaml.gotmpl - Aggregated component configs (charts, images, databases, secrets)
  3. components/<name>/default-environment.yaml.gotmpl - Per-component defaults (namespaces, feature flags)
  4. components/<name>/values/<part>/base-values.yaml.gotmpl - Base Helm values
  5. components/<name>/values/<part>/<profile>-values.yaml.gotmpl - Profile-specific values (development or production)
  6. deployment/environments/<env>/global.yaml.gotmpl - Your environment-specific overrides (highest precedence)

You should only modify files in the deployment/ directory. The defaults/ and components/ directories contain the upstream configuration and should not be changed for a specific deployment instance.

Setting up the Deployment Directory

The deployment/ directory is your instance-specific configuration. It is not tracked by the civitas-core-deployment repository, so you can track your deployment configurations separately. Create your deployment directory by copying the default deployment scaffolding:

cp -r defaults/deployment deployment
tip

The deployment directory is in .gitignore of the CIVITAS/CORE v2 deployment repository. Therefore, it can be used as git repository without affecting the main repository. This is very handy for operators to track their own deployments with git.

A typical directory structure looks like this:

civitas-core-deployment/
├── ...
└── deployment
├── environments
│ ├── local
│ │ └── global.yaml.gotmpl # Local environment overrides
│ └── production
│ └── global.yaml.gotmpl # Production environment overrides
└── helmfile.yaml # Main Helmfile entry point (do not modify)

Creating Environments

An environment is a complete installation of the whole platform.

Environments are used for two main purposes:

  1. Deploy different stages of the platform like testing, staging, or production
  2. Deploy different installations for different clients like client-a, client-b
tip

It is possible to combine both purposes and have deployments for multiple clients and for each client multiple stages. For this organize environments in subfolders like city-a/production and city-a/testing. The same names (with /) must be used in the helmfile.yaml file.

To create a new environment:

  1. Create a folder inside deployment/environments/ with the name of the environment
  2. Add an empty global.yaml.gotmpl file inside the environment folder
  3. Add it to the helmfile.yaml in the environments section with empty values
  4. Add it to the helmfiles section in the values list under environments
---
environments:
# add here
city-a:
values: [ ]
---
helmfiles:
- path: "../helmfile-root.yaml.gotmpl"
values:
- environments:
- city-a # add here

Configuring the Environment

Most configuration options have sensible defaults and only need to be adjusted in special cases. Therefore, in most cases, it is enough to adjust only a few global settings. Everything you don't overwrite in the environment config files will be taken from the default values defined in defaults/environment/.

Start with the defaults/environment/global.yaml file and copy any values you need to overwrite into the global.yaml.gotmpl file of your environment. Especially the global.instanceSlug must be set to the namespace name, that was created for this deployment.

info

Both single-namespace and multi-namespace deployment models are supported.

  • global.singleNamespace: true keeps all components in one namespace.
  • global.singleNamespace: false derives per-component namespaces (for example dev-postgres, dev-kafka).

See Multi-Namespace Configuration below for how to enable it and reference components across namespaces, or Multi-Namespace Support for help choosing a model.

info

The profile setting controls environment-specific behavior: development (default):

  • Lower resource requests and limits
  • Debug-level logging
  • Self-signed TLS certificates
  • Single replicas
  • Relaxed security policies

production:

  • Higher resource requests and limits
  • Info/warn-level logging
  • Production TLS certificates (e.g. Let's Encrypt)
  • Multiple replicas for high availability
  • Strict security policies and network policies
  • Prometheus metrics enabled
tip

If you want to use linkerd without the patchNamespaces feature, you can manually patch the namespaces with the following command:

kubectl annotate namespace <namespace> linkerd.io/inject=enabled --overwrite

Exclude Components

If you don't want to deploy all components, you can copy the components list from defaults/environment/global.yaml into your environment global.yaml.gotmpl file and remove the components you don't want to deploy.

warning

Many components depend on each other, so make sure to check the dependencies before removing components.

Multi-Namespace Configuration

By default all components run in one namespace (global.singleNamespace: true). Set global.singleNamespace: false to give each component its own namespace — recommended for staging and production. See Multi-Namespace Support for help choosing a model.

How Namespaces Are Created

You never name namespaces yourself. They are derived from global.instanceSlug and the component name:

ModeNamespace patternExample (instanceSlug: dev)
singleNamespace: true<instanceSlug>dev
singleNamespace: false<instanceSlug>-<component>dev-postgres, dev-keycloak, dev-apisix
# global.yaml.gotmpl
# `instanceSlug` plus the component suffix must stay within the 63-character Kubernetes name limit, so keep `instanceSlug` short.
global:
instanceSlug: dev
singleNamespace: false
createNamespaces: true # let Helmfile create the derived namespaces (default)
tip

Preview the resulting namespaces before deploying:

helmfile -f deployment/helmfile.yaml template -e production | grep -E '^\s*namespace:'

Overriding a Component's Namespace

warning

This feature is not yet tested.

You can pin a single component part to a custom namespace by setting its namespace value in the environment file. Only that part moves; all other components keep their derived namespaces.

# kafka.yaml.gotmpl
kafka:
operator:
namespace: shared-kafka-operator

Referencing Components Across Namespaces

When components talk to each other, never hard-code a namespace. Always reference the component by name so the reference keeps working in both single- and multi-namespace mode (and even if a component is moved). There are four places where you do this.

Secrets — componentNamespaces

A generated secret can be copied into several components' namespaces so every consumer reads the same credentials. List the target components (not literal namespaces):

portal:
backend:
db-portal:
username: portal
password:
length: 32
generate: true
componentNamespaces:
- portal # consumer
- postgres # database that owns the credential

The secret is generated once and created in each listed component's namespace (dev-portal and dev-postgres).

note

Use the component key as it appears in the values — usually identical to the component name (portal, postgres, keycloak); multi-word components use camelCase, e.g. configAdapters.

info

Keycloak client secrets are generated automatically for every non-public client and distributed to the keycloak namespace plus the owning component's namespace. You do not configure this yourself.

Network Policies — componentNamespace

Inside a network policy peer (from / to), reference another component with componentNamespace. It is resolved to the correct namespace selector automatically:

portal-backend:
podSelector:
app.kubernetes.io/name: portal
app.kubernetes.io/component: backend
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app.kubernetes.io/name: apisix
componentNamespace: apisix # resolves to dev-apisix

The policy stays correct in single-namespace mode (collapses to the shared namespace) and in multi-namespace mode (targets dev-apisix).

note

Network policy peers accept the component's directory name, including the kebab-case form (e.g. config-adapters). For all-namespaces behaviour use a plain namespaceSelector: {}.

APISIX Routes & Plugins

Use the right field for the right job:

FieldUse forExample
subDomainExternal access via the gatewaysubDomain: portalportal.<global.domain>
upstreamInternal cross-namespace backendportal-portal-backend.<portal-namespace>.svc.cluster.local:8080
hostAvoid for cross-namespace targetssee warning below

External hosts come from subDomain + global.domain and are namespace-independent. Internal upstreams and plugin endpoints must use a namespace-aware cluster DNS name; the shipped routes and plugins already do this via the civitas.namespace helper:

upstream: portal-portal-backend.{{ include "civitas.namespace" (dict "global" .Values.global "suffix" "portal") }}.svc.cluster.local:8080
warning

Avoid the route host field for cross-namespace targets: APISIX appends the release namespace (.<release-namespace>.svc.cluster.local) to a bare host, which breaks when the target lives elsewhere. Use a full upstream (with explicit namespace) for internal traffic or subDomain for external routing.

Service Hosts in Values (Databases, etc.)

Internal service URLs follow the pattern <service>.<namespace>.svc.cluster.local. The defaults resolve the namespace for you (PostgreSQL, Kafka bootstrap, Keycloak, APISIX admin, …). Database hosts, for example, combine the database's hostServiceName with the resolved postgres namespace:

# databases.yaml defines the service name only:
portal:
hostServiceName: 'postgres-cluster-rw'
port: 5432

# The component values build the full host using the postgres namespace:
host: {{ .Values.databases.portal.hostServiceName }}.{{ .Values.postgres.cluster.namespace }}.svc.cluster.local
# -> postgres-cluster-rw.dev-postgres.svc.cluster.local

If you override a hostname yourself, always use the fully qualified svc.cluster.local name with the correct namespace.

Configuration Reference

The following tables list all configurable parameters. Parameters are set in your environment's global.yaml.gotmpl file unless noted otherwise.

Global Parameters

ParameterDescriptionDefaultValid Values
global.domainDNS domain for all servicescivitas.testAny valid domain
global.instanceSlugUnique identifier for this deployment (used as namespace)devLowercase alphanumeric, max 63 chars
global.profileDeployment profile controlling resources and replicasdevelopmentdevelopment, production
global.createNamespacesWhether Helmfile should create namespaces automaticallytruetrue, false
global.singleNamespaceAll components in a single namespace vs. separate namespacestruetrue, false

Service Mesh

ParameterDescriptionDefaultValid Values
global.serviceMesh.enableEnable service mesh integrationtruetrue, false
global.serviceMesh.typeService mesh typelinkerdlinkerd
global.serviceMesh.patchNamespacesAuto-inject sidecars into namespacestruetrue, false

Ingress

ParameterDescriptionDefaultValid Values
global.ingress.clusterIssuercert-manager ClusterIssuer for TLS certificatesselfsigned-caAny installed ClusterIssuer (e.g. letsencrypt-prod)
global.ingress.ingressClassIngress controller classnginxAny installed IngressClass (e.g. nginx, traefik)

Storage

ParameterDescriptionDefaultValid Values
global.storage.storageClass.rwoStorageClass for ReadWriteOnce volumes (databases)'' (cluster default)Any available StorageClass
global.storage.storageClass.rwxStorageClass for ReadWriteMany volumes (currently unused)'' (cluster default)Any available StorageClass
global.storage.storageClass.locStorageClass for local storage'' (cluster default)Any available StorageClass

Metrics

ParameterDescriptionDefaultValid Values
global.metrics.enabledEnable Prometheus metrics scraping across componentsfalsetrue, false

Components List

ParameterDescriptionDefault
componentsOrdered list of components to deploy (order matters for dependencies)See below

Component-Specific Parameters

Component-specific settings are configured in .yaml.gotmpl files (e.g. <component-name>.yaml.gotmpl) files within your environment folder.

Overriding Helm Values Directly

Any Helm value of a component can be overridden via rawValues in the environment file:

# Example: Override keycloak replica count
keycloak:
app:
rawValues:
replicaCount: 3

# Example: Override PostgreSQL max_connections
postgres:
cluster:
rawValues:
cluster:
postgresql:
parameters:
max_connections: "1000"
warning

Use rawValues with care. These values bypass the configuration hierarchy and may break synchronization between components.

Helm Defaults

Global Helm behavior is configured in defaults/helm-defaults.yaml:

ParameterDescriptionDefault
helmDefaults.waitWait for resources to be readytrue
helmDefaults.waitForJobsWait for jobs to completetrue
helmDefaults.timeoutTimeout for each release in seconds300

Configuring other default values

Some default files in the defaults/environment/ directory don't have values set, but only Go template code.

images:
{{ include "civitas.configFiles" (dict "components" .Values.components "file" "images.yaml") }}

These are values that are collected from all components and merged into one file. You can see the values in the respective components/<component>/<filename>.yaml files. They can be adjusted in the environment by overwriting the values with the top-level key in any of the environment .yaml.gotmpl files.

e.g. deployment/environments/local/images.yaml.gotmpl:

images:
keycloak:
app:
tag: "15.0.2"
# ...

Using external components

Postgres Databases

External postgres databases can be used instead of the CloudNativePG operator. To do this, you can remove the postgres component from the components list and add the connection details of your external database in the environment config file. For this create a file inside the environment folder named databases.yaml.gotmpl and for every databases.yaml file inside the components/<component>/ folders copy everything, set embedded: false, set secret.generate: false and add the connection details of your external database. The secrets must be created manually in the cluster before deploying the platform.

Example for the portal component:

# databases.yaml.gotmpl
portal:
name: 'portal'
embedded: false
user: 'portal'
secret:
name: 'db-portal'
key: 'password'
generate: false
host: 'external-db-host.example.com'
port: 5432

Be sure to create a authz-readonly-user user with read-only access to the portal database.

The following databases with extensions are expected to be provided by an external database:

Database NameRequired Extensions
portal
frostpostgis, postgis_topology, fuzzystrmatch, postgis_tiger_geocoder
keycloak

Kafka

warning

To be documented...