1. Introduction
Helm is an indispensable tool for managing Kubernetes applications. It simplifies deploying and managing complex applications through a packaging format known as charts.
In this tutorial, we’ll explore flow control, a crucial aspect of working with Helm charts. First, we’ll recap the Helm templating engine. Then, we’ll discuss how to effectively control the flow of logic in our Helm templates, a key to creating flexible and powerful Kubernetes deployments. Let’s get started!
2. Basics of Helm Templates
Helm charts are packages composed of files that describe a related set of Kubernetes resources. A chart defines everything from a simple pod to a complex application with multiple components like databases, caches, and web servers. These charts harness the power of Kubernetes resources to work seamlessly together.
Additionally, Helm uses a combination of Kubernetes YAML files and a powerful templating engine based on the Go programming language. This engine allows us to parameterize aspects of our Kubernetes configurations using values defined in a separate file, typically named values.yaml. This separation of configuration from code enables us to customize deployments without altering our application’s core definition.
Let’s see a quick example:
apiVersion: v1
kind: Service
metadata:
name: {{ .Values.serviceName }}
spec:
type: {{ .Values.serviceType }}
ports:
- port: {{ .Values.port }}
Here, serviceName, serviceType, and port are placeholders that get their actual values from the values.yaml file, which might look like:
serviceName: my-service
serviceType: ClusterIP
port: 80
With the values in this file, we can reuse the same chart for multiple purposes, which could be anything from resource limits to enabling or turning off features. This flexibility is one of the reasons why Helm is so powerful in managing complex applications.
3. Understanding Flow Control in Helm
Flow control in Helm templates is fundamental for writing dynamic Kubernetes configurations.
Let’s examine how Helm flow control can improve our Kubernetes management.
3.1. The Role of Go Templating
Flow control allows us to specify conditions under which certain parts of our Helm chart are included or excluded from the final Kubernetes manifest file. This capability is crucial for creating versatile applications that adapt to different deployment environments or configurations without requiring separate charts for each scenario.
Helm utilizes Go templating for its template processing, which introduces several flow control structures, such as if, else, and range, similar to those we use in traditional programming languages.
3.2. if-else Statements
if-else statements are conditional logic to check if a given condition is true and then execute specific parts of the template:
{{ if .Values.enableFeatureX }}
apiVersion: v1
kind: ConfigMap
metadata:
name: feature-x-config
data:
settings: "true"
{{ end }}
This snippet will only include the ConfigMap if enableFeatureX is set to true in values.yaml.
3.3. range Loops
range loops are useful for iterating over lists and creating multiple resources based on the values of the list:
{{ range .Values.users }}
apiVersion: v1
kind: User
metadata:
name: {{ .name }}
{{ end }}
Here, the snippet will create a User resource for each entry in the users list in values.yaml.
With these structures, we can craft responsive Kubernetes deployments that adjust based on varying inputs.
4. Common Flow Control Patterns
In our journey with Helm, we’ll find that certain flow control patterns recur frequently. These patterns concern resource conditional inclusion and configuration modification based on the deployment context.
Let’s see some of these common patterns.
4.1. Conditional Resource Deployment
We might want to deploy certain resources only in specific environments, such as debugging tools in development but not in production:
{{ if eq .Values.environment "development" }}
apiVersion: v1
kind: Pod
metadata:
name: debug-tools
spec:
containers:
- name: debug
image: debug-toolkit
{{ end }}
This comes in handy in our multistage development environment.
4.2. Configuration Adjustment
We can also adjust resource limits based on the environment to ensure that production deployments have higher resources than staging:
resources:
limits:
cpu: {{ if eq .Values.environment "production" }} "2" {{ else }} "1" {{ end }}
memory: {{ if eq .Values.environment "production" }} "2048Mi" {{ else }} "1024Mi" {{ end }}
In short, by using these patterns, we can tailor our Helm charts to react dynamically to the values.yaml configuration, making our charts both more powerful and versatile.
5. The “Greater Than” Issue in Helm
Many Helm users encounter a common challenge when using relational operators such as “greater than” (gt) in templates. The Helm templating engine, built on Go’s templating system, supports these operators but occasionally runs into type issues due to how it interprets data from YAML files.
5.1. Understanding the Error
Let’s say that we want to adjust Kubernetes deployment parameters based on the number of replicas specified in our values.yaml file.
To do this, we might try to implement this with an if statement that checks if the replicaCount is greater than a certain value:
rollingUpdate:
maxSurge: 1
{{ if gt .Values.replicaCount 2 }}
maxUnavailable: 0
{{ else }}
maxUnavailable: 1
{{ end }}
Here, the goal is to have 0 unavailable pods if the replicaCount is greater than 2. However, users often receive an error like “error calling gt: incompatible types for comparison” — primarily because YAML interprets numbers as integers or floats based on their appearance. Additionally, Go’s templating engine is strictly typed.
5.2. Solving the Type Issue
To resolve these type-related issues, we need to ensure consistent typing in our comparisons.
One way to address this is by explicitly defining the type in the YAML file or ensuring the comparison is against the same type:
rollingUpdate:
maxSurge: 1
{{ if gt .Values.replicaCount 2.0 }}
maxUnavailable: 0
{{ else }}
maxUnavailable: 1
{{ end }}
In this corrected example, comparing against 2.0 (a float) helps avoid type mismatches if Values.replicaCount is interpreted as a float.
Alternatively, Helm also supports type casting within templates, which we can use for more explicit control:
{{ if gt (toFloat .Values.replicaCount) 2.0 }}
This approach ensures that no matter how we define replicaCount in values.yaml, it treats it as a float during the comparison, thus avoiding type errors and making the template more robust.
However, if we’re certain that the values should logically be integers (such as replicaCount), casting to an integer might be more appropriate:
{{ if gt (int .Values.replicaCount) 2 }}
This uses Helm’s int function to treat Values.replicaCount as an integer during comparison.
Notably, we must be mindful that the integer casting method truncates any fractional parts, which could lead to unintended outcomes or errors if fractional numbers are valid inputs.
6. Advanced Usage of Flow Control
As we become more comfortable with the basics of flow control in Helm, we can implement more advanced techniques to manage complex scenarios and logic within our charts.
Let’s see some more complex examples.
6.1. Nesting Conditions
Sometimes, a single layer of conditional logic isn’t enough.
However, nesting conditions allow for more detailed logic paths that can handle multiple layers of decision-making:
{{ if eq .Values.environment "production" }}
# Only include this block for production deployments
{{ if gt .Values.replicaCount 5 }}
resources:
limits:
cpu: "500m"
memory: "2000Mi"
{{ else }}
resources:
limits:
cpu: "250m"
memory: "1000Mi"
{{ end }}
{{ end }}
In this example, it adjusts resource limits not only based on the environment but also based on the number of replicas.
6.2. Using range Loops With Conditions
Combining range loops with conditions can dynamically generate Kubernetes resources based on lists defined in values.yaml, with additional logic applied to each item.
Let’s see an illustration:
{{ range .Values.services }}
{{ if .enabled }}
apiVersion: v1
kind: Service
metadata:
name: {{ .name }}
spec:
ports:
- port: {{ .port }}
{{ end }}
{{ end }}
This pattern allows for the dynamic creation of resources where we can toggle each service on or off individually, demonstrating the powerful flexibility of Helm’s templating engine for real-world applications.
6.3. Conditional Inclusion of Subcharts
Helm charts can include other charts, a feature known as subcharts. These are useful for managing dependencies or modularizing components.
However, we can also control the inclusion of subcharts based on conditions derived from the values.yaml file:
# In the parent chart's values.yaml
subchart1:
enabled: true
# In the parent chart's requirements.yaml or Chart.yaml
dependencies:
- name: subchart1
condition: subchart1.enabled
This setup lets us dynamically include or exclude subcharts based on custom conditions, allowing extensive customization of complex deployments through simple configuration changes.
6.4. Dynamic Resource Names
Sometimes, we might need resource names to be dynamic, especially when we deploy multiple instances of a chart within the same namespace, to avoid clashes:
kind: Service
apiVersion: v1
metadata:
name: {{ printf "%s-%s" .Release.Name .Values.customSuffix | trunc 63 | trimSuffix "-" }}
This template uses Helm’s built-in functions to construct a dynamic name based on the release name and a custom suffix the user provides. It also truncates the name to ensure it doesn’t exceed Kubernetes’ maximum length for names.
6.5. Using lookup to Conditionally Create Resources
Helm 3 introduced the lookup function that can be particularly useful for creating resources conditionally based on the existence of other resources:
{{- $existingSecret := lookup "v1" "Secret" .Release.Namespace "my-secret" }}
{{- if not $existingSecret }}
apiVersion: v1
kind: Secret
metadata:
name: my-secret
data:
password: {{ .Values.password | b64enc }}
{{- end }}
In this example, it only creates a secret if it doesn’t already exist in the namespace. This prevents accidental overwrites and ensures that manual configurations are respected.
6.6. Manipulating Lists and Dictionaries With range and if
We can leverage Helm’s range and conditional checks to manipulate and iterate over lists and dictionaries.
This provides powerful ways to generate Kubernetes resources or configuration blocks dynamically:
# Assuming .Values.backends is a list of dictionaries
{{- range .Values.backends }}
apiVersion: v1
kind: Pod
metadata:
name: {{ .name }}
spec:
containers:
- name: {{ .containerName }}
image: {{ .image }}
{{- if eq .environment "production" }}
resources:
limits:
memory: "512Mi"
cpu: "1"
{{- end }}
{{- end }}
This snippet creates a set of pods based on a list of backends, with each backend potentially having a different container configuration. It also adjusts resource limits conditionally for production environments.
6.7. Dynamic Volume Mounts Based on Environment
Sometimes, we might need to adjust volume mounts in our deployments based on the deployment environment, such as production vs. staging.
We can also handle this dynamically with a combination of range and conditional checks:
spec:
containers:
- name: my-app
image: my-app-image
{{- if eq .Values.environment "production" }}
volumeMounts:
- name: prod-secrets
mountPath: /etc/secrets
readOnly: true
{{- else }}
volumeMounts:
- name: dev-secrets
mountPath: /etc/secrets
readOnly: true
{{- end }}
volumes:
- name: prod-secrets
secret:
secretName: prod-secrets
- name: dev-secrets
secret:
secretName: dev-secrets
This configuration ensures that the correct secrets are mounted depending on whether the deployment targets production or a development environment.
6.8. Conditional Annotations for Resource Optimization
We use annotations in Kubernetes to control behavior in managed environments, such as those involving Istio or other service meshes.
In Helm, we can also dynamically add annotations to our resources based on conditions:
apiVersion: v1
kind: Service
metadata:
name: {{ .Release.Name }}-service
annotations:
{{- if .Values.istio.enabled }}
"sidecar.istio.io/inject": "true"
{{- end }}
spec:
ports:
- port: 80
In this example, the Istio sidecar injection is conditionally applied based on whether we enable Istio in values.yaml. This allows for flexibility in deploying applications in environments with different configurations and requirements.
6.9. Handling Optional Configurations
In many scenarios, we might need to handle optional configurations that should only be applied based on certain conditions.
For instance, we may want to optionally configure a proxy for our application only if certain values are provided:
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-proxy-config
data:
PROXY_URL: "{{ .Values.proxy.url | default "not_set" }}"
{{- if .Values.proxy.enabled }}
USE_PROXY: "true"
{{- else }}
USE_PROXY: "false"
{{- end }}
This technique ensures that our application can seamlessly adapt to environments with and without proxy configurations.
Ultimately, by leveraging these advanced flow control techniques, we can significantly enhance the dynamic nature and responsiveness of our Helm charts.
7. Conclusion
In this article, we explored the depths of Helm’s templating engine, focusing particularly on flow control. Each example illustrated how we can leverage Helm’s powerful templating features to create flexible, dynamic Kubernetes deployments.
Starting from basic conditional statements to more complex scenarios involving nested conditions and dynamic resource generation, we covered various techniques that we can apply to make our Helm charts more dynamic and responsive to different deployment environments.