Files
development-gitops-argo-cd-…/references/helm-values.md
oleg-lukasonokandClaude Opus 5 50d746d03f Add gitops-argo-cd agent skill
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>
2026-07-29 23:48:32 +03:00

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:

  1. By Copier — substitutes {{ variable }} from the answer file, producing values.yaml.
  2. By Helm — ArgoCD passes that values.yaml to helm template at 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: Always for mutable tags (latest, environment-specific). IfNotPresent only for immutable digest-pinned images.
  • Private registries need imagePullSecrets referencing the pre-existing secret — commonly storage-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 livenessProbe and readinessProbe.
  • Prefer httpGet on /health where the app exposes it.
  • periodSeconds: 60 is the default; change only when startup or health cadence demands it.

Replicas

  • Default to 1 via 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

  1. Create it at cluster/resources/{{ '<NNN-name>' if <flag> }}/values.yaml.jinja — the expression identical to the cluster/argocd/ side.
  2. Open with {% macro render() %}.
  3. Write the values body, sourcing every variable through a .get() chain.
  4. Close with {% endmacro %} plus the stripping footer.
  5. Confirm every referenced variable is declared in copier.yaml.
  6. 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 global pattern and the standalone pattern in one file.
  • Forgetting imagePullSecrets for a private-registry image — pods stall on ImagePullBackOff.
  • Renaming a global sub-key to a synonym the chart does not read — the value silently becomes empty rather than erroring.