ai-superpower/.ai/instructions/skills/helm.instructions.md
moilanik 7ba8056ce9 docs(helm): add guidance for multiple files in ConfigMap data
New subsection "Multiple Files in ConfigMap data":
- start with inline range for few files (readable in template)
- shift to files/ + .AsConfig + tpl when >3 files (readability suffers)
- explains threshold for when to switch patterns
- shows both inline and file-based examples
- enables template variables in file-based approach
2026-03-10 10:58:04 +02:00

472 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Helm Chart Development Guidelines
---
## 🚨 Audit Checklist — Violations to Find When Reviewing
When asked to review or analyze a Helm project, **actively search for each of these**. Do not rely on a quick read — scan template files for the literal patterns below. A violation must be reported as an issue, not ignored.
### Hardcoded namespaces
Search all files under `templates/` for a literal `namespace:` key. Flag any value that is not `{{ .Release.Namespace }}` or a `required`/`.Values` expression.
```
grep -r "namespace:" templates/
```
Every hit that is not:
```yaml
namespace: {{ .Release.Namespace }}
namespace: {{ required "..." .Values.something }}
namespace: {{ .Values.something }}
```
is a violation.
### Required values with empty defaults
Search for `required` calls paired with a fallback default in values.yaml:
```
grep -r "required" templates/
```
For each hit, check if values.yaml contains an entry for that key. If it does with a non-empty or placeholder value, it is a violation — `required` values must have no entry in values.yaml.
### values.yaml keys not used in any template
```
grep -r "\.Values\." templates/
```
Compare the set of `.Values.*` references in templates against all keys defined in values.yaml. Keys in values.yaml that no template references are dead weight — flag them.
---
## <20> values.yaml Is Not Rendered
`values.yaml` is static YAML — it is **not** processed by Helm's template engine. Template expressions like `{{ .Release.Name }}` or `{{ .Values.foo }}` do not work in `values.yaml` and must never be placed there.
Consequences:
- Resource names passed to subcharts via values.yaml must be literal strings
- When a subchart needs to reference a resource created by the parent (e.g. a ConfigMap name), that name must be hardcoded in values.yaml and matched exactly in the parent template
- This is the correct pattern — not a violation
```yaml
# values.yaml — correct: fixed name because values.yaml cannot template
configMap:
name: custom-alloy-config # matches the name in templates/alloy-config.yaml
# templates/alloy-config.yaml — the parent creates the ConfigMap with this exact name
metadata:
name: custom-alloy-config
```
Document fixed names with a comment explaining why they are fixed. The audit checklist only flags hardcoded names in `templates/` files — never in `values.yaml`.
---
## <20>🔍 Before Creating Anything New
**ALWAYS search the workspace for existing implementations first.**
Before writing a new template, config, or resource:
1. Search the workspace for similar existing files
2. If found, migrate/adapt it — don't invent a new structure
3. Only create from scratch if nothing exists
---
## 🚫 Never Hardcode Namespace, Release Name, or Installation-Specific Values
**A Helm chart must be fully agnostic about where and how it is installed.**
This is a hard rule — no exceptions.
### Namespace
Never hardcode a namespace in any template. The namespace is set by the deployer at install time.
```yaml
# ❌ FORBIDDEN — always
metadata:
namespace: my-app
namespace: production
namespace: default
# ✅ Required — or omit namespace entirely and let Helm inject it
metadata:
namespace: {{ .Release.Namespace }}
```
If a resource must reference another namespace (e.g. a NetworkPolicy peer), expose it as a required value:
```yaml
metadata:
namespace: {{ required "global.targetNamespace is required — set the target namespace in values.<instance>.yaml" .Values.global.targetNamespace }}
```
### Release Name
Never hardcode a release name. Use `.Release.Name` to derive resource names:
```yaml
# ❌ FORBIDDEN
name: my-app-service
name: myapp-ingress
# ✅ Required
name: {{ include "mychart.fullname" . }}
# or
name: {{ .Release.Name }}-service
```
### Why — IaC Principle
The chart is infrastructure code. It must be deployable to any cluster, any namespace, any environment, by any deployer — without editing the chart itself. Installation-specific decisions belong exclusively in `values.<installation>.yaml` in the deployment repo.
**In practice:** In umbrella charts, you typically deploy each release to a different namespace, or at most two instances to separate clusters. You never deploy two instances of the same umbrella to the same cluster and namespace — that would be a misconfiguration. This constraint is not enforced by the chart; it is part of the deployment discipline.
```
❌ Wrong: chart decides where it lives
✅ Right: deployer decides where it lives — chart describes what it is
```
---
## 🏠 Resource Ownership
**Every Kubernetes resource must have exactly one owner. Never duplicate.**
- If a chart creates a resource (e.g. Traefik creates IngressClass), don't create it elsewhere
- Before adding a template for a resource, verify no other chart/subchart already manages it
```
❌ Wrong: subchart A creates IngressClass AND subchart B creates IngressClass
✅ Right: one chart owns IngressClass, others do not touch it
```
---
## 🧹 values.yaml — Keep It Short and Identical Across Installations
**values.yaml serves two purposes:**
1. **Static baseline** — identical for every installation. If a value varies between installations, it does not belong here.
2. **Umbrella glue** — wires subcharts into a coherent whole. Shared values like `global.domain` are defined once here and consumed by multiple subcharts, so the umbrella behaves as a single unit rather than a collection of independent charts.
Installation-specific values go exclusively in `values.<instance>.yaml` in the IaC/deployment repo.
**The goal is a short, minimal values.yaml.** The longer it gets, the harder it is to see what actually varies. When in doubt whether a value belongs in values.yaml, it probably doesn't.
**A value may only appear in values.yaml if:**
- At least one template references `.Values.<key>` — and
- It is either shared glue across subcharts, or overrides an upstream subchart default that would otherwise be wrong for all installations
**Forbidden:**
- Empty string placeholders (`key: ""`) — these are documentation pretending to be config; use README instead
- Defaults for values that are hardcoded in templates — they create a false impression the value is configurable
- Installation-specific data of any kind — hostnames, credentials, sizing, feature flags
- Required values (validated with `required`) — absence must trigger the error, not a silent empty default
**Two-file layering in IaC:**
| File | Location | Content |
|---|---|---|
| `values.yaml` | chart repo | minimal static baseline, same for all installations |
| `values.<instance>.yaml` | IaC/deployment repo | everything that varies per installation |
---
## ✅ required vs defaults
When a template needs a value, there are exactly two valid patterns:
**1. The value has a sensible default** — hardcode it directly in the template. Do not add it to values.yaml.
**2. The value must come from the deployer** — use `required` with a descriptive error message. Do not put any entry in values.yaml for this key.
`| default` in templates is forbidden. Adding defaults to values.yaml for values that rarely change is also forbidden — it makes values.yaml long and hard to maintain. Hardcoded defaults in templates are easy to find and easy to parameterize later if needed.
```yaml
# ✅ Sensible default — hardcoded in template, not in values.yaml
replicas: 1
# ✅ Must vary per installation — required, set in values.<installation>.yaml
host: {{ required "global.domain is required — set it in values.<installation>.yaml" .Values.global.domain }}
# ❌ Forbidden — default in values.yaml adds noise, bloats values.yaml
replicas: {{ .Values.replicas }} # values.yaml: replicas: 1
# ❌ Forbidden — hides missing config, fails later in an obscure way
host: {{ .Values.global.domain | default "" }}
replicas: {{ .Values.replicas | default 1 }}
```
The error message in `required` must tell the deployer exactly what to set and where. "X is required" alone is not enough — say what value is expected and in which file.
---
## 📦 Subchart Dependency Conditions
Use `condition:` in `Chart.yaml` to enable/disable components. Use flat booleans, not nested `.enabled`:
```yaml
# Chart.yaml
dependencies:
- name: keycloak
repository: "file://../components/keycloak"
version: "0.2.0"
condition: components.keycloak
# values (Good)
components:
keycloak: true
traefik: false
# values (Avoid)
keycloak:
enabled: true
```
---
## ⏱️ Hook Ordering
Resources that depend on other resources must use `helm.sh/hook` with `hook-weight` to ensure correct install order.
Established ordering convention:
- `hook-weight: "10"` — infrastructure prerequisites (e.g. ClusterIssuer, IngressClass)
- `hook-weight: "15"` — resources that depend on prerequisites (e.g. Certificate)
```yaml
metadata:
annotations:
"helm.sh/hook": post-install,post-upgrade
"helm.sh/hook-weight": "10"
```
---
## 📄 Configuration Files over values.yaml
**Prefer `files/` + ConfigMap over defining configuration in values.yaml.**
When a component needs configuration (CASC, realm config, ini files, etc.):
1. Put the config file in `files/` directory
2. Create a ConfigMap that reads it with `.AsConfig` + `tpl`
3. Use `{{ .Values.global.xxx }}` expressions inside the config file for dynamic values
```yaml
# templates/configmap.yaml
kind: ConfigMap
apiVersion: v1
metadata:
name: my-component-config
data: {{ (tpl (.Files.Glob "files/*").AsConfig . ) | nindent 2 }}
```
```yaml
# files/config.yaml — contains Helm template expressions
server:
url: https://{{ .Values.global.domain }}
env: {{ .Values.global.environment }}
```
**Why:** Config files are readable, diffable, and version-controlled as real files.
Large configs in values.yaml become unmaintainable and hard to review.
### Multiple Files in ConfigMap data
When a ConfigMap needs multiple files as separate keys (e.g. dashboards, rules):
**Start with inline range:**
```yaml
# Few files — use inline range
data:
{{- range $path, $_ := .Files.Glob "files/dashboards/*.json" }}
{{ base $path }}: {{ $.Files.Get $path | quote }}
{{- end }}
```
**Shift to files + tpl when:**
- You have many files (>3) — readability suffers in the template
- Files need Helm variables — `{{ .Values.xxx }}` expressions must be processed
When you cross this threshold, move the files to a directory and use `.AsConfig` + `tpl` to process them:
```yaml
# templates/configmap.yaml
kind: ConfigMap
apiVersion: v1
metadata:
name: dashboards
data: {{ (tpl (.Files.Glob "files/dashboards/*").AsConfig . ) | nindent 2 }}
```
```json
# files/dashboards/dashboard.json can now contain {{ .Values.xxx }} expressions
{
"namespace": "{{ .Release.Namespace }}",
"alertmanager": "{{ .Values.alertmanager.host }}"
}
```
**When `.AsConfig` + `tpl` is required:**
- Config file contains `{{ }}` template expressions → always use `.AsConfig` + `tpl`
- Config file contains special characters (`*`, `{`, `}`) → `.AsConfig` handles escaping safely
```yaml
# Never use plain Files.Get on files with template expressions or special chars:
{{ tpl (.Files.Get "files/config.json") . }} # ❌ breaks on * characters
# Always use AsConfig:
{{ tpl (.Files.Glob "files/config.json").AsConfig . }} # ✅
```
---
## 🔄 Dependency Caching
After editing any subchart template or values, cached charts in `charts/` will be stale:
```bash
rm -rf charts/ Chart.lock
helm dependency update .
```
Always rebuild before `helm template` testing when subchart files have changed.
---
## 🎂 Umbrella Chart Pattern
The standard structure is an umbrella chart that integrates multiple component charts.
```
my-umbrella/
├── Chart.yaml ← declares component charts as dependencies
├── values.yaml ← static baseline: shared across all installations
├── templates/ ← only cross-cutting resources that no subchart owns
└── charts/ ← built by helm dependency update
```
**Two-file values layering:**
| File | Where it lives | Purpose |
|---|---|---|
| `values.yaml` | umbrella chart repo | static baseline, same across all IAC installations |
| `values.<installation>.yaml` | IAC / deployment repo | installation-specific overrides and required parameters |
- `values.yaml` contains defaults valid for every installation; it never contains installation-specific data
- `values.<installation>.yaml` provides what that specific installation requires — credentials, hostnames, sizing, feature flags
- Required values (no sensible default) use `required` in templates and have **no entry** in `values.yaml` — their absence triggers a clear error at install time
---
## 🔗 Glue Configuration: Coordinating Subchart Dependencies
When a subchart cannot use Helm template expressions (e.g. reads `values.<key>` directly instead of templating), the parent chart may coordinate resource names as "glue configuration."
**When this pattern is appropriate:**
- Subchart hardcodes a config path or resource name lookup (cannot be changed)
- Parent chart must create that resource with the exact name the subchart expects
- This is documented in both parent and subchart values.yaml with clear reasoning
- There is no other way to make the subchart work
**Example: Tempo + MinIO**
Tempo chart cannot use `{{ .Release.Name }}` in values.yaml. It reads the MinIO hostname directly from config.
```yaml
# values.yaml — MinIO
minio:
fullnameOverride: "minio" # ← Forces service name to "minio"
# Documented trade-off: prevents multiple releases in same namespace
# but enables Tempo to connect
# values.yaml — Tempo
tempo:
storage:
s3:
endpoint: "http://minio:9000" # ← Hardcoded to match MinIO fullnameOverride
```
---
## ⚡ KISS — Start Minimal, Add Only When Asked
**Do not add knobs, flags, or options that arent needed right now.**
- Build the minimum that meets the stated functional requirement
- Do not anticipate future needs with extra values, conditions, or template logic
- Complexity is easy to add; it is hard to remove once deployed
- AI may **suggest** what might be needed later — but must not implement it without explicit instruction
```yaml
# ❌ Wrong: adding a flag “just in case”
features:
enableFutureThing: false # not needed yet, adding anyway
# ✅ Right: dont add it. If its needed, the developer will ask.
```
**When in doubt, leave it out.** A value that exists but is never used is noise. A template condition that is never toggled is dead code. Raise the question with the developer instead of silently implementing it.
### Hardcoded values in templates are the starting point
Not every value needs to be configurable. Making everything a parameter is an anti-pattern — it creates noise and makes charts harder to understand.
Start with hardcoded values in the template. When the developer asks for a value to be parameterized, move it to `values.<instance>.yaml` — not to `values.yaml`, which is shared across all installations.
```yaml
# ✅ Start here — hardcoded in template
resources:
requests:
cpu: "100m"
memory: "128Mi"
# ✅ When developer asks to parameterize — goes in values.<instance>.yaml, not values.yaml
resources:
requests:
cpu: {{ required "resources.requests.cpu is required — set it in values.<instance>.yaml" .Values.resources.requests.cpu }}
memory: {{ required "resources.requests.memory is required — set it in values.<instance>.yaml" .Values.resources.requests.memory }}
# ❌ Anti-pattern — parameterising everything preemptively
resources:
requests:
cpu: {{ .Values.resources.requests.cpu }} # nobody asked for this yet
memory: {{ .Values.resources.requests.memory }} # adds noise without benefit
```
---
---
## 🧪 Testing Templates
Always test with at least two different release names and namespaces. This verifies that resource references, names, and dependencies work correctly regardless of where the chart is deployed.
```bash
# Test 1: release=app1, namespace=default
helm template app1 . \
--namespace default \
--set global.key=value \
... \
2>&1 | grep "^kind:" | sort | uniq -c
# Test 2: release=app2, namespace=production
helm template app2 . \
--namespace production \
--set global.key=value \
... \
2>&1 | grep "^kind:" | sort | uniq -c
```
Verify:
- Expected resource kinds appear in both tests
- Resource names, labels, and selectors are correctly formed using `.Release.Name` or `.Release.Namespace`
- Manifest links between resources (e.g. Service → Deployment) use the correct names and namespaces
- No `Error:` lines
- `required` validation fires when values are missing