Modify the values and secrets passed to your service
Your service takes two kinds of runtime input besides its code: deploy-time values (non-secret knobs — resources, environment variables, application config) and secrets (credentials, tokens, keys). This guide gives practical examples of modifying both. None of them require rebuilding an image.
The split matters: values are versioned in your repository and reviewed like
code; secrets live in
Vault and never touch git.
Never put a secret in spec.values — including
appConfig.
Modifying values
Everything under spec.values in your Fairway manifest becomes the default
content of the generated chart’s values.yaml, and every environment can
override it at deploy time. (For the generator-time vs deploy-time distinction,
see the
Fairway service-owner guide.)
Change a default for every environment
Edit spec.values in fairway.yaml — for example, adding an environment
variable and raising the memory limit:
# fairway.yaml
spec:
values:
environment:
HTTP_CLIENT_TIMEOUT: "10s"
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 500m
memory: 512MiThe next chart release carries the new defaults to every environment that deploys it. Validate before pushing:
fairway validate fairway.yamlOverride locally (Caproni)
Your Caproni values file overrides just the keys you need — Helm deep-merges maps, so a partial tree is enough:
# caproni/<service>-values.yaml
resources:
limits:
memory: 1Gi # profiling locally; don't change the shipped default
appConfig:
data:
feature_flags:
endpoint: http://flipt:8080 # local flag backendRedeploy (caproni up) to re-render; restart the pod to reload application
config.
Override at deploy time (operators)
The published chart is a plain OCI Helm artifact, so any environment’s operator can override values at install time:
helm install my-service \
oci://registry.gitlab.com/<group>/<project>/chart --version 1.4.0 \
--values production-overrides.yamlEach
platform binding has its own surface for
supplying these overrides — Caproni uses the values file above, and on Cells the
Configuration COM module materialises them from the tenant model — but they all
converge on the same values.yaml schema.
Modifying secrets
Secrets are stored in Vault and synchronised into your workload’s runtime; your repository only ever names them:
flowchart LR
vault["Vault<br/>runway/env/<env>/service/<id>/<name>"] --> sync["Sync<br/>(GKE: External Secrets Operator<br/>EKS: provisioner → AWS Secrets Manager)"]
sync --> ksecret["Kubernetes Secret"]
ksecret --> container["Container<br/>(env vars via secretEnvFrom,<br/>files via secretVolumes)"]
Prerequisite: managing secrets needs Vault access — an Okta access request plus a Vault policy MR. The Okta step can take around two weeks, so start early. The Runway secrets-management guide is the authoritative walkthrough; the examples below connect it to the Theseus workflow.
Add a secret
In Vault, navigate to
runway/env/<environment>/service/<runway_service_id>(environment isstagingorproduction) and create a secret — for exampleenv-vars— with one key per value you need:API_KEY: "your-api-key-here" DATABASE_PASSWORD: "your-database-password"Keys become environment-variable names, so keep them uppercase with underscores.
Declare the secret in your Runway workload config and expose it as environment variables:
# .runway/<runway_service_id>/gke-service.yaml <runway_service_id>: workloadSecrets: - name: env-vars secretEnvFrom: - env-vars
Every key in env-vars arrives in the container as an environment variable. On
GKE the sync is immediate (External Secrets Operator reads Vault directly); on
EKS it waits for the provisioner’s Terraform cycle.
Mount a file-based secret
For credentials that must be files — service accounts, certificates — use
secretVolumes instead:
# .runway/<runway_service_id>/gke-service.yaml
<runway_service_id>:
workloadSecrets:
- name: gcp-service-account
secretVolumes:
- name: gcp-service-account
path: /secrets/gcp
spec:
environment:
GOOGLE_APPLICATION_CREDENTIALS: /secrets/gcp/credentials.jsonEach key in the Vault secret becomes a read-only file under the mount path —
here, a credentials.json key appears at /secrets/gcp/credentials.json.
Rotate a secret
In Vault, open the secret and Create new version with the updated values — the path and your repository configuration don’t change. The new version syncs on the same schedule as above (immediately on GKE, next provisioner cycle on EKS).
If your service reads a mounted secret file, it can pick up rotation without a restart using LabKit’s rotating file provider:
p := secret.NewRotatingFileProvider("/secrets/db", "password", time.Minute)
if err := p.Start(ctx); err != nil {
return err
}
defer p.Shutdown(ctx) //nolint:errcheck
go func() {
for event := range p.OnRotate() {
if event.Err != nil {
logger.Warn("rotation poll error", labkitlog.Error(event.Err))
continue
}
pool.Reconnect(event.NewValue.Value())
}
}()Environment-variable secrets are read at startup, so a rotation there needs a pod restart to take effect.
Provide a secret in local development (Caproni)
Locally there is no Vault sync — you create the Kubernetes Secret yourself and
reference it the same way. This is exactly what the
exemplar’s lifecycle hook does for
DATABASE_URL:
kubectl -n <namespace> create secret generic my-service-secrets \
--from-literal=DATABASE_URL="postgres://app:…@deps-postgresql:5432/app" \
--dry-run=client -o yaml | kubectl apply -f -# caproni/<service>-values.yaml
secretEnvFrom:
- my-service-secretsThe container sees the same environment variables it would receive in production, so no code changes between environments.
Consume secrets with LabKit v2/secret
Keep secret access behind
LabKit v2/secret
rather than raw os.Getenv, so the backend can change without touching
application code:
// Default client reads environment variables (secretEnvFrom).
secrets := secret.New()
dbURL, err := secrets.Get(ctx, "DATABASE_URL")
if err != nil {
return fmt.Errorf("read DATABASE_URL: %w", err)
}
db, err := postgres.NewWithConfig(&postgres.Config{DSN: dbURL.Value()})For volume-mounted secrets (secretVolumes), swap the provider — the calling
code stays identical:
secrets := secret.NewWithConfig(&secret.Config{
Provider: secret.NewFileProvider("/secrets/gcp"),
})Values are wrapped in a Secret type whose String method redacts, so an
accidental %v in a log line prints a placeholder, never the credential.
Related reading
- Runway secrets management — the authoritative Vault workflow, including access setup
- Pass application configuration via Fairway — for non-secret application config
- Fairway service-owner guide — the full
spec.valuesreference, includingsecretEnvFromandsecretVolumesin the generated chart - LabKit
v2/secret— providers, rotation, and redaction guarantees - Go service template exemplar — the local-dev secret pattern as working code