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

16 KiB
Raw Blame History

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:

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.


<EFBFBD> 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
# 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.


<EFBFBD>🔍 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.

# ❌ 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:

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:

# ❌ 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.

# ✅ 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:

# 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)
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
# templates/configmap.yaml
kind: ConfigMap
apiVersion: v1
metadata:
  name: my-component-config
data: {{ (tpl (.Files.Glob "files/*").AsConfig . ) | nindent 2 }}
# 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:

# 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:

# templates/configmap.yaml
kind: ConfigMap
apiVersion: v1
metadata:
  name: dashboards
data: {{ (tpl (.Files.Glob "files/dashboards/*").AsConfig . ) | nindent 2 }}
# 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
# 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:

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.

# 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
# ❌ 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.

# ✅ 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.

# 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