How to Manage Environment Variables in Helm Charts: A Comprehensive Guide

Jimin
12 min readMay 12, 2024

--

Helm is renowned not only as a package manager for Kuberentes but also for its robust templating and customization capabilities. These features allow dynamic generation of Kubernetes resource files based on variables and functions, enabling applications to be tailored to different environments without modifying the core chart files. This guide will explore how Helm manages environment variables, providing you with the tools to configure, customize, and optimize your deployments efficiently.

If you are new to setting up a Kubernetes environment or need a refresher, you might want to revisit my previous blog post: Setting Up a Local Kubernetes Development Environment with VirtualBox and Vagrant for AWS EKS. This will ensure you have the necessary foundation to effectively utilize Helm charts and manage Kubernetes applications.

Step 1: Creating a Helm Chart

To create a new Helm chart, you use the helm create command.

helm create my-chart

This command sets up a new chart directory with all the necessary files and directories with default settings:

my-chart/
├── Chart.yaml
├── charts
├── templates
│ ├── NOTES.txt
│ ├── _helpers.tpl
│ ├── deployment.yaml
│ ├── hpa.yaml
│ ├── ingress.yaml
│ ├── service.yaml
│ ├── serviceaccount.yaml
│ └── tests
│ └── test-connection.yaml
└── values.yaml

Step 2: Review and Update Chart.yaml

Chart.yaml contains metadata about your chart.Here’s an example of what it might look like:

apiVersion: v2
name: webapp
description: A Helm chart for deploying a basic Nginx web application
type: application
version: 0.1.0
appVersion: 1.21.0
  • apiVersion: Chart API version (v2 for Helm 3).
  • name: The name of your chart.
  • description: A single-line description of the chart.
  • type: Indicates whether the chart is an ‘application’ or a ‘library’. Application charts are standard charts that deploy resources, whereas library charts provide utilities or functions for other charts to use.
  • version: The version of the chart, following Semantic Versioning (SemVer). This is also a required field and is used when releasing or updating charts.
  • appVersion: The version of the application that is managed by the Helm chart. It’s optional and informational, specifying the version of the actual application like a Docker image version.

Step 3: Review and Update values.yaml

values.yaml specifies default configuration values for your chart. It’s a central place to customize the settings of your Kubernetes resources:

name: my-default-app
replicaCount: 1

image:
repository: nginx
tag: stable
pullPolicy: IfNotPresent

service:
type: ClusterIP
port: 80

resources:
limits:
cpu: "500m"
memory: "512Mi"
requests:
cpu: "250m"
memory: "256Mi"

path: /this/path:here

env:
LOG_LEVEL: debug
APP_MODE: "production"

fullnameTemplate: "{{ .Release.Name }}-{{ .Values.env.APP_MODE | lower }}"

configmaps:
- name: development
data:
db-host: dev.database.example.com
DB_USER: dev_user
DB_PASS: password123
- name: staging
data:
db-host: staging.database.example.com
DB_USER: staging_user
DB_PASS: password123
- name: production
data:
db-host: prod.database.example.com
DB_USER: prod_user
DB_PASS: password123

db:
password: secretPassword

Helm Template Syntax and Curly Braces {{ }}

In Helm templates, {{ }} are used to denote areas in YAML files where Go templating expressions are used. These expressions can include variables, functions, pipelines (chaining functions together), and control structures. Here’s what each part does:

Variables

Variables provide access to predefined or user-defined values. For example:

{{ .Release.Name }}

This accesses the release name of the Helm chart, a predefined Helm variable.

Functions

Functions in Helm are used to manipulate data. For example:

value: {{ .Values.path | quote }}

If .Values.path is /this/path:here, the quote function would render it safely as "/this/path:here".

Pipelines

Pipelines allow you to pass the result of one operation to the next operation. They are denoted by the | symbol.

{{ .Release.Name | upper | printf "Release: %s" }}

This converts the release name to uppercase and formats it with a prefix, resulting in something like "Release: MYAPP".

Control Structures

Helm also supports control structures like conditional logic and loops within the curly braces, enabling dynamic generation of the template based on conditions.

Conditionals: You can include or exclude parts of the template based on conditions.

{{ if eq .Values.env.APP_MODE "production" }}
- name: DATABASE_URL
value: "http://prod.database.example.com"
{{ end }}
  • This block will only be included if the environment variable in values.yaml is set to production.

Loops: Helm can iterate over data using loops. For instance:

{{ range .Values.items }} 
- name: {{ .name }}
value: {{ .value }}
{{ end }}
  • This will create multiple lines in the manifest, one for each item in the .Values.items list.

Explanation of {{- and -}}

You might also notice that sometimes the curly braces have a dash inside them like {{- or -}}. These dashes control whitespace:

  • {{- removes all whitespace (including newlines and spaces) on the left side up to the curly brace.
  • -}} removes all whitespace on the right side up to the curly brace.

This is particularly useful for maintaining clean, readable YAML files while still controlling where the line breaks and spaces appear, ensuring that the YAML syntax is correct.

  • For example, without dashes:
name:
{{ .Release.Name }}

This might unintentionally include leading spaces before the value if not aligned correctly.

  • Example with dashes:
name: {{- .Release.Name -}}

This ensures no unwanted spaces around the release name, regardless of how the template is indented.

Step 4: Basic Handling of Environment Variables

Direct Value Assignment

The simplest way to set environment variables in Helm is by directly referencing values from the values.yaml file. This method is straightforward and is commonly used for static configurations that do not change across different deployments.

env:
- name: LOG_LEVEL
value: {{ .Values.env.LOG_LEVEL }}

Utilizing Helm’s Built-in Objects (Chart and Release)

You can leverage the metadata defined in Chart.yaml within your Helm templates to inject this data directly into your Kubernetes manifests. This is useful for labeling, annotations, and even setting environment variables that reflect your chart’s metadata.

Components of the .Release Object

In Helm, the .Release object is a crucial part of the templating system, offering metadata about the release of a Helm chart. It is automatically populated by Helm during the rendering process of templates and is primarily used to ensure that multiple installations of the same chart can coexist in a cluster without conflicts. Here are the key components of the .Release object and how they are typically used:

  • .Release.Name: The name of the release. This is usually set during the installation or upgrade of a chart (e.g., helm install my-release). It allows you to identify different releases of the same chart within a cluster.
  • .Release.Namespace: The Kubernetes namespace in which the release is deployed. This is important for managing access and resources scope within a cluster.
  • .Release.IsUpgrade and .Release.IsInstall: Boolean values that indicate whether the current operation is an upgrade or an installation. These can drive conditional logic in templates to handle different operations distinctly.
  • .Release.Revision: Represents the revision number of the release. This number increments with each upgrade or rollback, allowing for tracking changes and versions deployed over time.
  • .Release.Service: The service that is rendering the current template. In almost all cases, this value is "Helm".

Usage in a Helm Template

Here’s how you might reference these values in a Helm template for a Kubernetes Deployment:

File: templates/deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-webapp
labels:
app: {{ .Release.Name }}-webapp
chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app: {{ .Release.Name }}-webapp
template:
metadata:
labels:
app: {{ .Release.Name }}-webapp
chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
spec:
containers:
- name: nginx
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
resources:
limits:
cpu: {{ .Values.resources.limits.cpu }}
memory: {{ .Values.resources.limits.memory }}
requests:
cpu: {{ .Values.resources.requests.cpu }}
memory: {{ .Values.resources.requests.memory }}
env:
- name: LOG_LEVEL
value: {{ .Values.env.LOG_LEVEL }}
- name: CHART_NAME
value: "{{ .Chart.Name }}"
- name: CHART_VERSION
value: "{{ .Chart.Version }}"

In Helm templates, .Values is used to reference values that are specified in your values.yaml file or passed to Helm during installation or updates via command-line arguments. For example, if your values.yaml has a key called name, you can access it in your templates using .Values.name.

The .Chart object, on the other hand, is used to access metadata defined in your Chart.yaml file. This includes the name of the chart, the version, and other details. For example, .Chart.Name retrieves the name of the chart as defined in Chart.yaml.

Step 5: Advanced Templating with Helpers and tpl

In this step, we enhance our Helm chart by introducing more complex templating techniques using the _helpers.tpl file and the tpl function. These tools allow us to reuse code and dynamically generate content based on our deployment context and configuration values.

File: templates/_helpers.tpl

{{/* "webapp.fullname" constructs the full application name dynamically. */}}
{{- define "webapp.fullname" -}}
{{ tpl .Values.fullnameTemplate . }}
{{- end }}

{{/* "webapp.labels" provides standard Kubernetes labels for resources. */}}
{{- define "webapp.labels" -}}
app.kubernetes.io/name: {{ include "webapp.fullname" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.Version }}
environment: {{ default "production" .Values.env.APP_MODE | upper }}
{{- end }}

{{/* "webapp.configMessage" creates a message about the current deployment context. */}}
{{- define "webapp.configMessage" -}}
Configuration loaded for {{ .Release.Name }} in the {{ .Release.Namespace }} namespace.
{{- end }}
  1. webapp.fullname
    - Constructs the application’s full name dynamically using the tpl function, which evaluates expressions from values.yaml.
    - Usage: {{ include "webapp.fullname" . }}
  2. webapp.labels
    - Sets standard Kubernetes labels for resources, incorporating the application name, instance, version, and deployment environment.
    - Usage: {{ include "webapp.labels" . }}
  3. webapp.configMessage
    - Generates a context-specific message indicating the deployment’s release name and namespace, useful for logs or application metadata.
    - Usage: {{ include "webapp.configMessage" . }}

Usage in a Helm Template

Here’s how you might reference these values in a Helm template for a Kubernetes Deployment:

File: templates/deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
# Use a named template to define the full name of the deployment. This helps maintain consistency across multiple resources.
name: {{ include "webapp.fullname" . }}
labels:
# Apply standard labels from the defined helper to ensure uniformity and to support network policies or service discovery.
{{- include "webapp.labels" . | nindent 4 }}
spec:
# Set the number of replicas as specified in the values.yaml, allowing easy scaling.
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app: {{ .Values.name }}
template:
metadata:
labels:
app: {{ .Values.name }}
spec:
containers:
- name: nginx
# Pull the image specified in values.yaml, using the repository and tag fields.
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
ports:
# Container port that Nginx listens on. This must match the service specification.
- containerPort: 80
env:
# Set environment variables directly from values.yaml and from the dynamic message defined in _helpers.tpl.
- name: LOG_LEVEL
value: {{ .Values.env.LOG_LEVEL }}
- name: CONFIG_MESSAGE
# The tpl function renders the configMessage template with the current context, showing how to inject dynamic release data.
value: "{{ include "webapp.configMessage" . | tpl }}"

Step 6: Use of Secrets and ConfigMaps

What is a ConfigMap?

A ConfigMap is a Kubernetes resource used to store non-confidential data in key-value pairs. ConfigMaps allow you to separate your configuration artifacts from your application code, which is a best practice in software development and deployment. This separation allows for more streamlined application management and configuration.

File: templates/configmap.yaml

{{- range .Values.configmaps }}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .name }}
data:
{{- range $key, $val := .data }}
{{ $key }}: {{ $val | quote }}
{{- end }}
---
{{- end }}

What is a Secret?

A Secret is a Kubernetes resource that is similar to ConfigMaps but designed specifically for handling sensitive information. Secrets are used to store and manage access to tokens, passwords, keys, OAuth tokens, ssh keys, etc., ensuring sensitive data is kept secure.

File: templates/secrets.yaml

apiVersion: v1
kind: Secret
metadata:
# The name of the secret is derived from a helper template, maintaining naming consistency.
name: {{ include "webapp.fullname" . }}
type: Opaque
data:
# Encode the database password from values.yaml as a base64 string for security.
db-password: {{ .Values.db.password | b64enc }}

Using Secrets and ConfigMaps in deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "webapp.fullname" . }}
labels:
{{- include "webapp.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app: {{ include "webapp.fullname" . }}
template:
metadata:
labels:
app: {{ include "webapp.fullname" . }}
spec:
containers:
- name: {{ .Values.name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- containerPort: {{ .Values.service.port }}
resources:
requests:
cpu: {{ .Values.resources.requests.cpu }}
memory: {{ .Values.resources.requests.memory }}
limits:
cpu: {{ .Values.resources.limits.cpu }}
memory: {{ .Values.resources.limits.memory }}
env:
- name: LOG_LEVEL
value: {{ .Values.env.LOG_LEVEL }}
- name: CONFIG_MESSAGE
value: {{ include "webapp.configMessage" . }}
- name: DB_HOST
valueFrom:
configMapKeyRef:
name: {{ .Values.env.APP_MODE | lower }}
key: db-host
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: {{ include "webapp.fullname" . }}
key: db-password

Step 7: Preliminary Checks Before Installing Your Helm Chart

Running Helm Lint

The helm lint command is used to validate that your chart follows best practices and that all templates are correctly formatted without any obvious errors.

# Navigate to your Helm chart directory
cd my-chart

# Run Helm Lint
helm lint .

This command will analyze your Helm chart files and report any warnings or errors it finds, which could potentially prevent your chart from working as expected.

Performing a Dry-Run Installation

A dry-run installation is a great way to see what resources Helm would create when you run the helm install command, without actually applying any changes to your Kubernetes cluster.

# Run a Dry-Run Installation:
helm install --debug --dry-run my-release .
  • --debug: Provides additional output for the installation process, which can be helpful for debugging.
  • --dry-run: Tells Helm to simulate an installation and output the Kubernetes manifests that would be generated based on the current state of the chart and values.

This step allows you to visually verify the configurations, particularly focusing on aspects like environment variables, volume mounts, and other resource specifications to ensure they align with your expectations.

Step 8: Deploying and Verifying Your Helm Chart

After performing the necessary preliminary checks with helm lint and a dry-run installation, you are ready to deploy your Helm chart to the Kubernetes cluster and verify that everything is correctly set up.

Simplifying the Chart Structure

Before deployment, ensure that your chart directory includes only the necessary files:

my-chart/
├── Chart.yaml
├── charts
├── templates
│ ├── _helpers.tpl
│ ├── configmap.yaml
│ ├── deployment.yaml
│ └── secrets.yaml
└── values.yaml

Remove any files not directly used by your deployment to avoid potential errors.

Deploying the Chart

To deploy your Helm chart, use the helm install command followed by a release name. Here’s how you can do it:

helm install my-release .
  • my-release is the name you're assigning to that particular deployment (or "release") of your Helm chart.
  • .Release.Name within any of your Helm templates will be substituted with "my-release" when Helm processes the template during deployment.

This command deploys your Helm chart to the Kubernetes cluster configured in your kubeconfig file, creating a new release named my-release.

Verifying the Deployment

Throughout this guide, we explored several environment variables and how Helm facilitates their dynamic management in Kubernetes deployments. Here’s a quick overview of the key environment variables we’ve covered:

  • Check Deployment Details

To ensure that your deployment has been successfully applied, you can use kubectl to inspect the deployment. Start by listing the deployments to confirm the presence and status of your release:

kubectl get deployments

Once you’ve confirmed that your deployment is listed, you can dive deeper into the specifics of your deployment named my-release-production:

kubectl get deployment my-release-production -o yaml

This will output the deployment details in YAML format, allowing you to inspect the configurations.

  • Verify Environment Variables:

Throughout this guide, we explored several environment variables and how Helm facilitates their dynamic management in Kubernetes deployments. To verify these settings, specifically focus on how they are calculated and injected into the deployment, for example:

  1. LOG_LEVEL: Used to control the logging level of the application, making it a crucial variable for debugging and monitoring in different environments.
  2. APP_MODE: Indicates the mode in which the application is running (e.g., production, development). This variable helps configure the application appropriately for the environment it's deployed in.
  3. CONFIG_MESSAGE: A dynamic message generated using Helm’s tpl function, which can include details like the release name and the environment mode. This showcases Helm’s ability to inject complex, computed values into your deployments.
  4. DB_HOST and DB_PASSWORD: Examples of how Helm can securely inject external data from Kubernetes ConfigMaps and Secrets into your application, ensuring that sensitive data like database credentials are handled securely and contextually.

Step 9: Troubleshooting Deployment Issues

If the application is not performing as expected, you may need to check logs and other details:

Checking Logs:

# List all pods to identify the pod
kubectl get pods
# Output might look like this
NAME READY STATUS RESTARTS AGE
my-release-production-785c87ffc8-jq5h4 1/1 Running 0 10m

# kubectl logs -f <pod-name>
kubectl logs -f my-release-production-785c87ffc8-jq5h4
  • This command fetches the logs from the specified pod, which can help identify any runtime issues.

Helm Status:

helm status my-release
  • Get a snapshot of your release’s current status.

Helm History:

helm history my-release
  • Review the revision history to see changes or perform rollbacks.

Step 10: Cleaning Up

To delete your release and remove all associated Kubernetes resources:

helm delete my-release

This command ensures all resources created by the release are cleaned up from the cluster.

--

--

Jimin
Jimin

Written by Jimin

DevOps engineer and tech enthusiast. Sharing tech insights to simplify the complex. Let's connect on LinkedIn! https://www.linkedin.com/in/byun-jimin/