The with keyword in Helm templates doesn’t just limit variable scope; it fundamentally changes the context of your template execution, making variables relative to a specific piece of data.
Let’s see it in action. Imagine you have a values.yaml like this:
app:
name: my-awesome-app
replicas: 3
port: 8080
config:
database:
host: db.example.com
port: 5432
api:
timeout: 30s
And a deployment.yaml template:
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-{{ include "mychart.fullname" . }}
spec:
replicas: {{ .Values.app.replicas }}
selector:
matchLabels:
app: {{ include "mychart.name" . }}
template:
metadata:
labels:
app: {{ include "mychart.name" . }}
spec:
containers:
- name: {{ .Values.app.name }}
image: my-docker-repo/{{ .Values.app.name }}:latest
ports:
- containerPort: {{ .Values.app.port }}
env:
- name: APP_NAME
value: {{ .Values.app.name }}
- name: DB_HOST
value: {{ .Values.app.config.database.host }}
- name: DB_PORT
value: {{ .Values.app.config.database.port | quote }}
- name: API_TIMEOUT
value: {{ .Values.app.config.api.timeout }}
This works, but notice how repetitive {{ .Values.app... }} is. Now, let’s refactor with with:
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-{{ include "mychart.fullname" . }}
spec:
replicas: {{ .Values.app.replicas }}
selector:
matchLabels:
app: {{ include "mychart.name" . }}
template:
metadata:
labels:
app: {{ include "mychart.name" . }}
spec:
containers:
- name: {{ .Values.app.name }}
image: my-docker-repo/{{ .Values.app.name }}:latest
ports:
- containerPort: {{ .Values.app.port }}
env:
- name: APP_NAME
value: {{ .Values.app.name }}
{{- with .Values.app.config }}
- name: DB_HOST
value: {{ .database.host }}
- name: DB_PORT
value: {{ .database.port | quote }}
- name: API_TIMEOUT
value: {{ .api.timeout }}
{{- end }}
See how the {{- with .Values.app.config }} block changes things? Inside that block, . now refers to values.app.config. So, .database.host is equivalent to values.app.config.database.host from the outer scope. This makes your templates cleaner, especially when dealing with deeply nested structures.
The real power comes when you combine with with loops, or when you want to conditionally include sections based on the existence of a certain configuration.
Consider a scenario where you only want to expose a service if service.enabled is true and service.port is defined.
# values.yaml
service:
enabled: true
port: 80
targetPort: 8080
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
And your Kubernetes Service template:
{{- with .Values.service }}
{{- if .enabled }}
apiVersion: v1
kind: Service
metadata:
name: {{ include "mychart.fullname" $ }}-service
labels:
app: {{ include "mychart.name" $ }}
spec:
selector:
app: {{ include "mychart.name" $ }}
ports:
- protocol: TCP
port: {{ .port }}
targetPort: {{ .targetPort }}
{{- if .annotations }}
annotations:
{{ toYaml .annotations | nindent 4 }}
{{- end }}
---
{{- end }}
{{- end }}
Here, $ is used to refer back to the top-level scope (the entire Values object) when you need to access something outside the current with context, like include "mychart.fullname" $. Inside the with .Values.service block, . refers to values.service. So, .port is values.service.port, and .annotations is values.service.annotations. The {{- if .enabled }} check is now operating on values.service.enabled.
The with statement, when applied to a map or object, changes the root of the template’s data structure for the duration of that block. When applied to a list, it iterates over the list, and for each item, the . within the with block becomes that item. This is the mechanism behind using with for iterating over lists of configurations, like defining multiple ingress rules or environment variables from a list in your values.yaml.
Crucially, with doesn’t just prepend the parent path; it replaces the current . with the value of the expression. If you have {{ with .Values.app }} ... {{ .name }} ... {{ end }}, the .name inside refers to Values.app.name, not Values.app.name. This is why you often see {{ .Release.Name }} or {{ include "mychart.fullname" $ }} inside a with block when you need to refer to values outside the current context; the $ explicitly tells Helm to go back to the top-level scope.
A common pitfall is forgetting to use $ when you need to reference values from the root scope within a with block that has already changed the context. For example, if you have {{ with .Values.config }} ... {{ .Release.Name }} ... {{ end }}, the {{ .Release.Name }} inside will fail because .Release.Name isn’t defined within the config context. You’d need {{ with .Values.config }} ... {{ $.Release.Name }} ... {{ end }}.
The next step in mastering template logic is understanding how to conditionally render entire blocks of YAML based on complex expressions, often involving eq, and, and or operators within if statements.