Consolidates the five cogarchhubgitops-* source skills into one skill with scope routing: ArgoCD Applications, Copier templates, Helm values, environment answer files, and the end-to-end resource-addition walkthrough. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
7.2 KiB
Helm Values Templates
Everything under cluster/resources/**/values.yaml.jinja: image wiring, the global block,
per-service sections, environment variables, probes, and replica counts.
Read this when creating or editing a values template, or when rendered values.yaml produces
invalid YAML or unexpected Helm behaviour.
1. Dual-layer rendering
Every values.yaml.jinja is rendered twice:
- By Copier — substitutes
{{ variable }}from the answer file, producingvalues.yaml. - By Helm — ArgoCD passes that
values.yamltohelm templateat sync time.
The file must therefore be valid Jinja2 and produce valid Helm values afterwards. A mistake in layer 1 fails the render; a mistake in layer 2 fails the sync, potentially hours later. Validate both (section 6).
2. Two categories of values file
| Category | Shape | Used for |
|---|---|---|
| Operator / config resources | Simple YAML, minimal templating | CRD instances — OperatorGroup, Subscription, ClusterSecretStore |
| Application workloads | Full Helm values with global + per-service sections |
Platform services, MCP servers, notification, integration |
Both use the same render() macro pattern. Do not mix the global shape and the standalone
shape inside one file — pick the one matching the chart and follow it consistently.
3. Minimal structure (every file)
{% macro render() %}
# --- values content here ---
key: {{ domain.get('sub', {}).get('field', '') }}
{% endmacro %}
{%- set result = render() -%}
{%- for line in result.split('\n') -%}
{% if line | trim | length != 0 -%}
{{ line }}
{% endif -%}
{%- endfor %}
4. Shared-chart workload structure
Services that share one Helm release read cluster-wide settings from a global block:
global:
is_internal: "{{ applications.get('global', {}).get('is_internal', '') }}"
ca_env: {{ applications.get('global', {}).get('ca_env', '') }}
ca_secret_name: {{ applications.get('global', {}).get('ca_secret_name', '') }}
host: {{ applications.get('global', {}).get('host', '') }}
ca_host: {{ applications.get('global', {}).get('ca_host', '') }}
suffix_url: {{ applications.get('global', {}).get('suffix_url', '') }}
maintenance: "{{ applications.get('global', {}).get('maintenance', 'false') }}"
node_selector_app: "{{ applications.get('global', {}).get('node_selector_app', '') }}"
mongo:
host: {{ applications.get('global', {}).get('mongo', {}).get('host', '') }}
port: {{ applications.get('global', {}).get('mongo', {}).get('port', '') }}
user: {{ applications.get('global', {}).get('mongo', {}).get('user', '') }}
network:
routes: []
<service-name>:
enabled: true
image:
name: {{ applications.get('<service-name>', {}).get('image', {}).get('address', '') }}
tag: {{ applications.get('<service-name>', {}).get('image', {}).get('tag', '') }}
replicas: {{ applications.get('<service-name>', {}).get('replicas', 1) }}
global goes at the top of the file. All global sub-keys use the established names — do
not introduce synonyms (hostname for host, env for ca_env); the chart templates read
the exact keys.
5. Standalone workload structure
Charts that do not share a global block use the standard Helm shape:
replicaCount: 1
image:
repository: {{ domain.get('service', {}).get('image', {}).get('address', '') }}
tag: {{ domain.get('service', {}).get('image', {}).get('tag', '') }}
pullPolicy: Always
imagePullSecrets:
- name: "storage-images"
service:
type: ClusterIP
port: 80
livenessProbe:
httpGet:
path: /health
port: <app-port>
periodSeconds: 60
readinessProbe:
httpGet:
path: /health
port: <app-port>
periodSeconds: 60
env:
targetPort: <app-port>
<env_var>: {{ domain.get('service', {}).get('env', {}).get('<env_var>', '') }}
6. Design rules
Images
- Always wire address and tag from Copier variables; never hardcode image coordinates.
- Use
.get()with an empty-string default for every image field. pullPolicy: Alwaysfor mutable tags (latest, environment-specific).IfNotPresentonly for immutable digest-pinned images.- Private registries need
imagePullSecretsreferencing the pre-existing secret — commonlystorage-images.
Booleans
Booleans passed into global (is_internal, maintenance, enable_instana) are quoted
strings because the chart templates treat them as strings. This is deliberate and differs
from Copier feature flags, which are real YAML booleans. Keep the two straight: flags gate
rendering, these gate chart behaviour.
Environment variables
- Map every env var from a Copier variable via
.get(); never hardcode. - Group them under an
env:key in the service's section. - Document which env vars are required, never their values — secrets come from an external secret store, not from a values file.
Probes
- Every application workload configures both
livenessProbeandreadinessProbe. - Prefer
httpGeton/healthwhere the app exposes it. periodSeconds: 60is the default; change only when startup or health cadence demands it.
Replicas
- Default to
1via the Copier variable:replicas: {{ domain.get('svc', {}).get('replicas', 1) }}. - Never hardcode — environments must scale independently.
Blank-line stripping
- Wrap the full body in
{% macro render() %}…{% endmacro %}. - Always append the four-line stripping footer.
- Do not rely on
-whitespace control inside the macro body for the same purpose.
7. Adding a new values file
- Create it at
cluster/resources/{{ '<NNN-name>' if <flag> }}/values.yaml.jinja— the expression identical to thecluster/argocd/side. - Open with
{% macro render() %}. - Write the values body, sourcing every variable through a
.get()chain. - Close with
{% endmacro %}plus the stripping footer. - Confirm every referenced variable is declared in
copier.yaml. - Render and validate.
A starting template is in assets/example-cluster-resource/resources/values.yaml.jinja.
8. Validation
# Render (Jinja2 layer)
copier copy . /tmp/gitops-render --overwrite --defaults
# Parse the rendered values (YAML layer)
python3 -c "import yaml; yaml.safe_load(open('/tmp/gitops-render/cluster/resources/<NNN-name>/values.yaml'))"
# Lint against the chart (Helm layer)
helm lint <path-to-chart> -f /tmp/gitops-render/cluster/resources/<NNN-name>/values.yaml
# See what Helm will actually produce
helm template <release> <path-to-chart> \
-f /tmp/gitops-render/cluster/resources/<NNN-name>/values.yaml
9. Mistakes to avoid
- Omitting the
render()wrapper — blank lines from unexpanded blocks produce invalid YAML. - Hardcoding image tags, so every environment deploys the same version.
- Putting secrets in a values file instead of an ExternalSecret / ClusterSecretStore.
- Quoting numerics (replicas, ports) unless the chart explicitly expects a string.
- Mixing the
globalpattern and the standalone pattern in one file. - Forgetting
imagePullSecretsfor a private-registry image — pods stall onImagePullBackOff. - Renaming a
globalsub-key to a synonym the chart does not read — the value silently becomes empty rather than erroring.