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
472 lines
16 KiB
Markdown
472 lines
16 KiB
Markdown
# 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 aren’t 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: don’t add it. If it’s 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
|
||
|