Skip to content

Helm Values

The Helm values of a module on Dedicated come from two places:

  1. Your service repository. When you release, Runway merges the values files in your Runway directory into the module’s default values, and stores them in the Module Catalog entry for that release.
  2. The Tenant Model. When Runway deploys the module to a tenant, it merges the tenant’s values_override.helm.values over the default values from the catalog entry.

Runway then expands templates in the merged values, so that one set of default values can work for every tenant.

The Helm values themselves are documented in the Fairway values reference.

Runway reads these files from your Runway directory (.runway by default), and merges them in this order:

Order Source Description
1 Fairway Fairway’s built-in defaults.
2 fairway.yaml spec.values from the Fairway manifest.
3 values.yaml Values shared by all runtimes. Optional.
4 values-dedicated.yaml Values that only apply to Dedicated. Optional.
5 spec.extraValueFiles in the module manifest Extra values files, in the order they are listed.

Put values that apply to every runtime in values.yaml, and values that only make sense on Dedicated, such as templates, in values-dedicated.yaml.

spec.extraValueFiles in the module manifest lists more values files, relative to the Runway directory:

.runway/module.yaml
apiVersion: runway/v3
kind: RunwayManifest
metadata:
name: example-service-dedicated
spec:
extraValueFiles:
- values-common.yaml
- "values-{{ .Runtime }}-features.yaml"

File names are Go text/template templates, with these fields:

Field Value on Dedicated Description
.Runtime dedicated The runtime: gke, eks, or dedicated.
.ServiceID The Runway service ID metadata.name of the module manifest, for example example-service-dedicated.
.Environment dedicated The environment, such as staging or production on GKE.

A file whose name contains a template is skipped if it doesn’t exist. A file with a literal name, such as values-common.yaml, must exist.

Each layer is merged over the result of the previous layers, the same way Helm merges values files:

  • Objects (maps) are merged key by key, recursively. Keys that only exist in the earlier layer are kept.
  • Scalars (strings, numbers, booleans) are replaced.
  • Lists are replaced as a whole. They are not concatenated, and their entries are not merged.

This also applies to appConfig.data, which is free-form.

values.yaml
environment:
LOG_LEVEL: info
REGION: us-east-1
secretEnvFrom:
- shared-secrets
- extra-secrets
values-dedicated.yaml
environment:
LOG_LEVEL: warn
secretEnvFrom:
- dedicated-secrets
Result
environment:
LOG_LEVEL: warn
REGION: us-east-1
secretEnvFrom:
- dedicated-secrets

See Values files in the Helm documentation for details.

A tenant can override any of the default values with values_override.helm.values in its Tenant Model. Runway merges them over the default values from the catalog entry when it deploys the module, with the same merge semantics.

Tenant Model
{
"modular_features": {
"modules": {
"example-service-dedicated": {
"enabled": true,
"version": "v1.25.0",
"values_override": {
"helm": {
"values": {
"environment": { "LOG_LEVEL": "debug" }
}
}
}
}
}
}
}

Overrides are an escape hatch for a single tenant, for example to raise the log level while debugging. They are not how a module is configured: values that every tenant needs belong in your service repository, so that they are released, reviewed, and tested with the module. If a value only differs between tenants in a way that a template can express, use a template instead of an override.

Runway owns some values, such as the secretRef of the S3 bucket, the PostgreSQL instance, and the Redis instance. It sets them after the merge, and warns if it overwrites a value from the default values or from the Tenant Model.

appConfig.data is your service’s own configuration. Fairway doesn’t interpret it: it can be any tree of objects, lists, and scalars, in whatever structure your service reads. Set it in your values files like any other value:

values.yaml
appConfig:
data:
log:
level: info
upstreams:
- name: registry
timeout: 5s

Your service finds appConfig.data as a YAML file at /etc/<name>/config.yaml, and the environment variable CONFIG_DIR points at the directory, /etc/<name>. <name> is metadata.name of the Fairway manifest, not the Runway service ID. For the example manifest, example-frontend, your service reads:

/etc/example-frontend/config.yaml
log:
level: info
upstreams:
- name: registry
timeout: 5s

Read the file from $CONFIG_DIR/config.yaml rather than hard-coding the path. In Go, LabKit’s v2/config package loads and validates the file against your configuration’s protobuf message:

loader, err := config.New()
if err != nil {
return err
}
var cfg configv1.Config
if err := loader.Load(filepath.Join(os.Getenv("CONFIG_DIR"), "config.yaml"), &cfg); err != nil {
return err
}

LabKit doesn’t read CONFIG_DIR itself, so your service builds the path. The same protobuf message can also validate appConfig when Fairway builds the chart.

  • Changes restart the pods. The chart adds a checksum/config annotation to the pods, so a change to appConfig rolls out new pods. Your service only needs to read the file at startup.
  • CONFIG_DIR is reserved. Don’t set it in environment when appConfig is set: Fairway rejects the values. Likewise, don’t set the pod annotation checksum/config.
  • CronJobs don’t get it. Only the pods of your service’s Deployment have the file, not the pods of cronJobs.

The Fairway service owner guide describes appConfig in detail, including how to validate it against your service’s schema.

String values in appConfig.data can be templates, which Runway expands for each tenant. Use them for values such as the URL of the tenant’s GitLab instance:

values-dedicated.yaml
appConfig:
data:
gitlab:
url: "https://{% .ManagedDomain %}"

Runway expands the templates before Helm renders the chart, so config.yaml holds the expanded value, for example url: https://example.gitlab-dedicated.com. A tenant can also override appConfig.data in the Tenant Model, but use templates for anything that every tenant needs.

Some values differ between tenants in a predictable way. For example, a service that validates GitLab OIDC tokens needs the URL of the tenant’s GitLab instance. Instead of a Tenant Model override for each tenant, set the value to a template in values-dedicated.yaml:

values-dedicated.yaml
appConfig:
data:
auth:
issuer:
# Must equal the `issuer` in https://<gitlab-host>/.well-known/openid-configuration.
url: "https://{% .ManagedDomain %}"
keys_url: "https://{% .ManagedDomain %}/oauth/discovery/keys"
environment:
GITLAB_URL: "https://{% .ManagedDomain %}/"

On a tenant whose managed_domain is example.gitlab-dedicated.com, the module is deployed with:

appConfig:
data:
auth:
issuer:
url: "https://example.gitlab-dedicated.com"
keys_url: "https://example.gitlab-dedicated.com/oauth/discovery/keys"
environment:
GITLAB_URL: "https://example.gitlab-dedicated.com/"

Templates are Go text/template templates. Runway expands them when it deploys the module, after it has merged the Tenant Model overrides. The catalog entry stores the templates, not the expanded values.

Field Expanded
appConfig.data Every string value, at any depth, including strings in lists. Keys are not expanded.
environment Every value. Variable names are not expanded.

Templates in any other field are not expanded, and are passed to the Helm chart as they are.

Field Type Description
.ManagedDomain string Domain of the tenant’s GitLab instance, for example example.gitlab-dedicated.com, from managed_domain in the Tenant Model. Never empty.
  • Template delimiters are {% and %}. Runway uses them to tell its own templates apart from Helm templates, which use {{ and }}. Runway leaves anything in {{ }}, such as {{ .Release.Name }}, unchanged. {% .ChartVersion %} in chartRef uses the same delimiters.
  • Quote templated values in YAML. A value that starts with {%, such as {% .ManagedDomain %}, is not a valid plain YAML scalar unless it is quoted.
  • Only strings can be templates. Numbers, booleans, and other non-string scalars can’t be templated. A template always expands to a string, even if the result looks like a number.
  • Only the fields in Template data exist. An unknown field, such as {% .Domain %}, fails the deployment, as does a template that doesn’t parse. The error names the value, for example values.appConfig.data.auth.issuer.url.
  • Release-time checks see the template, not the expanded value. If your service validates appConfig against a schema when the chart is built, the templated value must pass it as written, for example a field with a URL pattern must accept https://{% .ManagedDomain %}.
  • Strings without template actions are unchanged. There is no escaping to worry about unless a value contains {%. To produce a literal {%, write {% "{%" %}.