Skip to main content
Version: 2.0-rc

Component Configuration

How to create and configure components for deployment in Civitas.

Creating/Updating components

Create a new component

make new-component

Update existing components to the latest template versions

make update-components

Folder Structure

A component includes:

components/<component>/
├── civitas-component.yaml # Component definition
├── charts.yaml # Helm chart sources and versions
├── default-environment.yaml.gotmpl # Calues, which can/should be set per environment
├── helmfile.yaml.gotmpl # Helmfile (auto-generated, usually no changes needed)
├── images.yaml # Container image definitions
├── values/ # Helm values per component part
│ ├── <part> # e.g., app, ui, worker
│ │ ├── base-values.yaml.gotmpl # Helm Values common to all environments
│ │ ├── development-values.yaml.gotmpl # Development-specific overrides
│ │ └── production-values.yaml.gotmpl # Production-specific overrides
│ └── ...
├── charts/ # Custom Helm charts (optional)
├── databases.yaml # Database configuration (optional)
├── secrets.yaml # Secret generation config (optional)
└── README.md # Component documentation

Configuring values

We work with Helm charts and therefore must primarily configure the components via Helm values. These values are set components/<component>/values/<part>/<profile>-values.yaml.gotmpl files.

Using Defaults

Some value settings are used in multiple components and have default values defined. These defaults can be found in the defaults/environments/.yaml.gotmpl files. For example for security there is security.yaml.gotmpl with securityDefaults object. You can access these defaults in your component values files like this:

securityContext:
{{ .Values.securityDefaults.securityContext | toYaml | nindent 2 }}

podSecurityContext:
{{ .Values.securityDefaults.podSecurityContext | toYaml | nindent 2 }}

Connect with Keycloak (OIDC/OAuth2)

You can configure other components to connect to Keycloak by constructing the Keycloak URL based on the configured subdomain and path prefix.

{{- $keycloakHost := printf "%s.%s" (default "" .Values.keycloak.app.subdomain) $global.domain | trimPrefix "." }}
{{- $keycloakPath := .Values.keycloak.app.pathPrefix -}}
{{- $keycloakUrl := printf "https://%s%s" $keycloakHost $keycloakPath -}}
{{- $realmName := $global.instanceSlug }}
authServerUrl: "{{ $keycloakUrl }}realms/{{ $realmName }}"

Registering a Keycloak Client

To register a Keycloak client for a component, create a keycloak-clients.yaml file at the component root (components/<my-component>/keycloak-clients.yaml). The Keycloak config job picks it up automatically.

Each top-level key is a Keycloak client ID. Any reference to this client elsewhere in the configuration — secrets, APISIX routes, etc. — must use the same value. At minimum, provide name, subdomain, and redirectPath:

# components/<my-component>/keycloak-clients.yaml
my-client:
name: My Component
description: My component's Keycloak client
subdomain: my-component
redirectPath: /callback
Client Roles

You can optionally define client roles alongside the client configuration. Client roles represent application-specific roles (e.g. admin, viewer) that are scoped to this client. Keycloak includes them in the token when a user authenticates against the client, and the application can then use those role claims to make authorization decisions.

Add a roles key to the client entry:

# components/<my-component>/keycloak-clients.yaml
my-client:
name: My Component
description: My component's Keycloak client
subdomain: my-component
redirectPath: /callback
roles:
- name: admin
description: Full administrative access
- name: viewer
description: Read-only access

The roles list is passed through verbatim as Keycloak client role definitions. Each entry must at minimum have a name field.

note

Roles are created during the Keycloak config job run. If you rename or remove a role, the old role is not automatically deleted from an existing Keycloak client — handle that manually if needed.

Connecting to Kafka Cluster

bootstrapServers: "{{ .Values.kafka.cluster.bootstrapService }}:{{ .Values.kafka.cluster.bootstrapPort }}"
warning

Currently, there is no standard way to configure Kafka authentication and TLS settings.

Connecting to Database

Create a file in components/<component>/databases.yaml with the following content:

keycloak:
name: 'keycloak'
embedded: true # set to false, if an external database is used
user: 'keycloak'
secret:
name: 'db-keycloak'
key: 'password'
generate: true # indicates that this value should be generated
host: 'postgres-cluster-rw' # set to external hostname, if an external database is used
port: 5432
warning

Currently multi namespaces do not yet work. Namespace of the cluster is not known, when rendering this.

You can access the credentials and connection details via the .Values.databases.<component> object like this:

database:
vendor: postgres
hostname: {{ .Values.databases.keycloak.host }}
port: {{ .Values.databases.keycloak.port }}
database: {{ .Values.databases.keycloak.name }}
username: {{ .Values.databases.keycloak.user }}
existingSecret: {{ .Values.databases.keycloak.secret.name }}
existingSecretKey: "password"

Setting Docker Images

To adjust/set where a docker image is pulled from and which version is used, create a file in components/<component>/images.yaml with content like this:

keycloak:
app:
repository: 'quay.io/keycloak/keycloak'
tag: '26.4.0'
configCli:
repository: 'docker.io/adorsys/keycloak-config-cli'
tag: '6.4.0-26'

You can access these values via the .Values.images.<component>.<imageKey> object like this:

image:
repository: {{ .Values.images.keycloak.app.repository }}
tag: {{ .Values.images.keycloak.app.tag }}

Generating Secrets

For generating and using secrets in a component, create a file in components/<component>/secrets.yaml with content like this:

---
keycloak:
app:
keycloak-admin-user: # name of the secret in kubernetes
password: # key in the secret
length: 16
generate: true # indicates that this value should be generated
warning

Currently, you have to ensure that the secret name and key match what the component expects or is set in the values files.

Exposing UIs and APIs

In the components/<component>/default-environment.yaml.gotmpl file add two new values under the part which is exposed:

kafka:
ui:
enabled: true
namespace: {{ include "civitas.namespace" (dict "global" .Values.global "suffix" "kafka") }}
# subdomain to use for accessing the UI. Can be null/empty to use the base domain with path
# pathPrefix is expected to start and end with /
subdomain: kafka-ui
pathPrefix: /

You can access these values via the .Values.<component>.<part> object like this:

ingress:
enabled: {{ $global.ingress.enabled }}
annotations:
cert-manager.io/cluster-issuer: {{ $global.ingress.clusterIssuer }}
{{- $host := printf "%s.%s" (default "" $this.subdomain) $global.domain | trimPrefix "." }}
host: {{ $host }}
path: '{{ $this.pathPrefix }}'
ingressClassName: {{ $global.ingress.ingressClass }}

Configuring Resources

For now: Set resource requests and limits directly in the <part>-values.yaml.gotmpl file of the component with sensible defaults. Defaults should be appropriate for a development and test environment and not much more.

Creating custom Helm charts

When creating a helm chart ensure that all deployment requirements can be fulfilled either by setting directly or via values. The helm chart should be opinionated, since only we use it in this specific context. It is important to follow standards across this project and helm charts in general how which values are set if possible. This way we can define global defaults and use them across multiple components.

The following (incomplete) list shows some of the values which should be supported in the helm chart:

  • In general
    • labels/annotations
  • Deployment/StatefulSet
    • resource requests/limits
    • securityContext/podSecurityContext
    • liveness/readiness probes (can be set directly)
    • image repository/tag/pullPolicy
    • rollingUpdate
    • replicas
    • podDisruptionBudget
    • autoscaling
    • importing self-signed TLS certificates (if applicable)
  • Service
    • type/port named (can be set directly)
  • ServiceAccount
    • create/name/annotations
  • Ingress
    • enabled
    • host/path
    • ingressClassName/annotations
    • tls

Setting other values

Here some guidelines which should help setting other values:

  • Prefer sensible defaults directly in the <part>-values.yaml.gotmpl file over setting many values in the default-environment.yaml.gotmpl These values can be overridden in the environment via <component>.<part>.rawValues if needed. We want to avoid too many values being set in the environment files to keep them clean and easy to read.
  • Security relevant values must be secure by default. E.g. if TLS can be enabled/disabled, it must be enabled by default.
  • Use the global values for settings which are relevant for multiple components.

Overriding a Component's Namespace

In multi-namespace mode each component's namespace is derived automatically from global.instanceSlug. 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.

warning

This feature is not yet tested.

# 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.