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>
214 lines
7.2 KiB
Markdown
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.
|