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

214 lines
7.2 KiB
Markdown

# 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)
```jinja
{% 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:
```yaml
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:
```yaml
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
```bash
# 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.