Layers in templates

When a package pulls in layers, every layer’s values.yaml is deep-merged with the parent’s into a single flat values map. Both the parent’s templates and the layers’ templates read that same map at ${values.*} — there is no per-layer namespace. What the parent controls is which values win, using the layers.<name> block in its own values.yaml.

For how you declare and compose layers, see the Layers guide.

How layer values reach the merged map

A parent that pulls in a redis layer:

Layer — redis/values.yaml:

password: from-layer
port: 6379

Parent — my-app/hull.yaml:

apiVersion: hull/v1
name: my-app
version: 1.0.0
layers:
  - name: redis
    source: ../redis

Parent — my-app/values.yaml:

replicas: 3
layers:
  redis:
    password: from-parent

After the merge, every template sees:

replicas: 3
password: from-parent   # parent's layers.redis.password overrode the layer
port: 6379              # from the layer

Both my-app/templates/* and redis/templates/* resolve ${values.password} to from-parent, ${values.port} to 6379, and ${values.replicas} to 3. The layers key is consumed by the merger and is not present in the values templates see.

Precedence: deeper layers merge first, shallower layers next, and the parent last (highest). A parent key therefore wins over a layer key of the same name.

Overriding a layer’s values

Put overrides for a layer under layers.<name> in the parent’s values.yaml; the merger applies them to that layer’s values before flattening. In the example above, layers.redis.password replaced the layer’s password.

Forcing precedence with !

To make a key win regardless of merge order — even over a profile that would otherwise re-override it — prefix it with !:

# parent values.yaml
layers:
  redis:
    "!password": forced-by-parent

The ! is stripped during merge and the value is applied at the highest precedence. Use it sparingly.

What does not exist

  • No ${layer.name} / ${.Layer.Path} namespace exposing the current layer’s identity to its templates.
  • No ${.Layers.<name>.<key>} accessor to reach a layer’s pre-merge values. Read ${values.<merged-key>} after the merge.
  • No per-layer value scoping at render time. Every layer’s keys land in the same flat map.

If a layer needs keys that won’t collide with siblings, namespace them inside the layer’s own values (redis: { password: ... }, read as ${values.redis.password}) and override via layers.redis.redis.password. This is a YAML convention, not an engine feature.

The global convention

Cluster-wide settings conventionally live under values.global:

# parent values.yaml
global:
  imageRegistry: registry.example.com

Because everything merges flat, values.global is visible everywhere — layer templates read ${values.global.imageRegistry} with no special scoping.

A complete example

Layer — redis/templates/serviceaccount.yaml:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: ${release.name}-redis
  labels:
    app.kubernetes.io/name: redis
    app.kubernetes.io/instance: ${release.name}

Parent — my-app/templates/deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: ${release.name}
spec:
  replicas: ${values.replicas}
  template:
    spec:
      containers:
        - name: app
          image: ${values.global.imageRegistry}/${values.image.name}:${values.image.tag}
          env:
            - name: REDIS_PASSWORD
              value: ${values.password}

Parent — my-app/values.yaml:

replicas: 3
global:
  imageRegistry: registry.example.com
image:
  name: my-app
  tag: 1.4.2
layers:
  redis:
    password: hunter2

Output of hull template ./my-app --release-name hello:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello
spec:
  replicas: 3
  template:
    spec:
      containers:
        - name: app
          image: registry.example.com/my-app:1.4.2
          env:
            - name: REDIS_PASSWORD
              value: hunter2
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: hello-redis
  labels:
    app.kubernetes.io/name: redis
    app.kubernetes.io/instance: hello

Library layers

A package with type: library in its hull.yaml can’t be installed standalone. A library ships partials in _helpers.yaml that consuming packages $include, so every consumer gets the same shape:

# org-base/hull.yaml
apiVersion: hull/v1
name: org-base
version: 1.0.0
type: library
# org-base/templates/_helpers.yaml
common.labels:
  app.kubernetes.io/name: ${package.name}
  app.kubernetes.io/instance: ${release.name}
  app.kubernetes.io/managed-by: hull

Consumers add it as a layer and include the partial:

# consumer hull.yaml
layers:
  - name: org-base
    source: oci://ghcr.io/example/org-base
    version: ^1.0.0
# consumer template
metadata:
  labels:
    $include: common.labels

See also