Anatomy of a service
A service is made up of several parts:
-
Operations, including scheduled maintenance
This page gives an overview of all of them and links to the more detailed documentation for each topic.
Provisioning
Provisioning is handled by the upstream Helm chart, the prima materia. It sits outside the framework and remains the service maintainer’s responsibility.
Everything around provisioning is where the framework comes in. It provides helper controllers that let you declare platform-agnostic supporting manifests, covering backups, networking, TLS, ingress and more. Service maintainers don’t need to worry about whether a Kubernetes cluster uses Ingress or the Gateway API, for example. They state the intent through the framework’s helper custom resources (CRs) and the rest is handled for them.
Transmuter CLI
Transmuter is the core tool a service maintainer works with. It helps you bootstrap, validate and extend services.
transmuter transmute \
--name my-service \ (1)
--prima-materia-url oci://... \ (2)
--prima-materia-version x.x.x (3)
| 1 | The name of the new service. The command creates a folder with this name. |
| 2 | OCI URL to the upstream Helm chart. Only charts hosted via OCI are supported at the moment. |
| 3 | Version of the upstream chart. There is no floating tag, so an exact version is required. |
This creates a new reagent, the chart for your service.
The specified upstream chart is added to the dependency list in Chart.yaml.
The new chart also ships a pre-seeded values.yaml that wires up the framework’s helper controllers.
The values.yaml file
Each service is just a Helm chart. The framework builds around Helm and adds revisions and pre-wired helpers that turn a plain chart into a proper managed service.
# Abridged for brevity
backup:
# Whether the service is backed up at all.
enabled: false
network:
# Allow access from all namespaces.
allowAllNamespaces: false
credentials:
# Secret name which will contain the service's connection details.
# Defaults to releaseName-credentials.
targetSecret: ""
# The service's values go here
service: {}
values.yaml defines the service’s CRD.
Every value in the file becomes a field in that CRD, so it pays to understand the structure and the default values.
These are only defaults.
A user will be able to change these values per instance.
Hints
Every value in values.yaml turns into a field of the CRD, and the type of the field is guessed from the default value.
Guessing only goes so far.
An empty list has nothing to guess from, and not every value should be settable by a user in the first place.
Hints are how you correct that.
A hint is a key that sits next to the value it describes, with the same name and a # in front.
# starts a comment in YAML, so the key has to be quoted.
Pure schema hints without a corresponding key are supported.
Descriptions can be passed by using the description field of the hint.
Using the description field can be useful to have full control over formatting and whitespace.
'#replicas':
type: integer
replicas:
Hints never reach the service. They are read while the CRD is generated and dropped afterwards, so the upstream chart never sees them.
Descriptions
The comment above a key becomes the description of the field in the CRD.
This needs no hint, it happens for every key that has a comment.
Users read those descriptions with kubectl explain, so it’s worth writing them.
# The hostname the service is reachable under.
# Leave empty to disable ingress.
ingressHostname: ""
Hiding values
export: false keeps a value out of the CRD.
The value still applies, it just stops being something a user can set.
This is how you pin things that must not be changed per instance, like the backup annotations further down this page.
Hiding is inherited.
A hint on an object hides everything below it, and an export: true deeper down brings the field and all its fields below back.
'#internal':
export: false (1)
internal:
a:
b: "" (2)
c:
'#d':
export: true (3)
d: ""
| 1 | Hides internal and everything under it. |
| 2 | Not settable, inherited from internal. |
| 3 | Settable again, the closest hint wins. |
If nothing under an object ends up exported, the object disappears from the CRD as well.
Types
A value the generator can’t read a type from is skipped, with a warning during generation.
That happens for empty values and for null.
Give those a type hint.
Allowed types are boolean, integer, number and string.
'#logLevel':
type: string
logLevel:
'#logLevel':
type: integer
description: |
The log level of the service. Higher levels suppress more logs.
Restricting values
enum limits a field to a fixed set of values.
The API server rejects anything else, so users get the error when they apply the claim instead of when the service fails to start.
'#logLevel':
type: string
enum: [debug, info, warn, error]
logLevel:
Empty lists and objects
An empty list or object carries no type either.
items and properties describe what goes inside.
Use items for repeated things: the entries of a list, or the values of a map with keys you don’t know upfront.
The '#' key describes a scalar entry.
'#tags':
items:
'#':
type: string
tags: [] (1)
'#labels':
items:
'#':
type: string
labels: {} (2)
| 1 | A list of strings. |
| 2 | A map with free-form keys and string values. |
For entries that are objects, describe their fields instead of using '#'.
'#ports':
items:
# The name of the port.
'#name':
type: string
'#port':
type: integer
ports: []
Use properties when the object has a fixed set of fields rather than free-form keys.
'#resources':
properties:
'#cpu':
type: string
'#memory':
type: string
resources: {}
The shorter way to describe entries is to write an example entry instead of hints, which is often easier to read.
'#ports':
items:
# The name of the port.
name: !!str
port: 0
ports: []
Backups
This section configures the service’s backup. The default backup is based on K8up and creates two things:
-
A bucket through COSI (Container Object Storage Interface), so the backup has a target
-
A K8up
Schedulethat runs the actual backup
By default, every PVC in the instance namespace is backed up. That is a good starting point, but copying files off a running application can result in a corrupted backup. Most applications ship a backup command that produces a consistent dump instead.
For application-aware backups, the service pods need annotations:
template:
metadata:
labels:
app: mariadb
annotations:
k8up.io/backupcommand: mysqldump -uroot -psecure --all-databases
The command’s output is streamed to the backup target and stored on S3.
Set k8up.io/backup: "false" on a PVC to skip it, so data the backup command already covers isn’t backed up a second time through its PVC.
The annotations should be fed into the service via available values for the upstream chart.
Since it’s not desired that the users changes them, hide them with export: false.
For more advanced setups, see the K8up docs.
Network
This section makes sure the service is reachable from the namespace where the claim was created.
The default values expose fields that let additional namespaces reach the service. The defaults are usually fine as they are.
Credentials
A service is only usable if the end user gets credentials for it.
The credentials section lets them pick the name of the secret those credentials are written to.
If left empty, the name defaults to the release name with a -credentials suffix.
To configure the fields and sources of the secret, adjust templates/credentials.yaml.
The file contains examples to start from.
Service
The service key holds the values for the upstream chart itself.
The prima materia is pulled in as a chart dependency under the alias service, so everything the upstream chart expects at the top level is nested under this key.
Whatever you set here becomes the default configuration for every instance, and users can override it in their claim.
Operations
Some services need operations that can be triggered on demand, for example:
-
Restart the service
-
Trigger a backup
-
Run a command in a pod
The framework calls these rituals.
They are not configured through values.yaml, they live in templates/rituals.
The template ships a few no-op rituals there.
They are placeholders: adapt each one to your service.
maintenance.yaml defines a special ritual that runs during the service’s scheduled maintenance.
It can carry any housekeeping a service needs.
For example regularly running VACUUM on a PostgreSQL instance.