Helm #
When we start running larger applications on Kubernetes, we’ll realize that deploying complex third-party applications isn’t a simple task. Imagine having to install a monitoring stack like Prometheus and Grafana, or a network ingress controller like Nginx Ingress. These applications need dozens of interrelated YAML manifest objects: Deployment, Service, ConfigMap, Secret, ServiceAccount, ClusterRole, and even Custom Resource Definitions (CRDs). Writing and maintaining hundreds of these YAML manifests manually for every environment is an enormous time waste.
This is where Helm comes in as the savior. Helm is Kubernetes’ official package manager, acting like apt on Ubuntu or pip in Python. With Helm, all those complex manifests are packaged into one modular unit called a Chart. Ansible fully supports Helm integration through dedicated modules in the kubernetes.core collection. By combining Ansible and Helm, we can manage installations, upgrades, configuration customization (values), and even automatic rollbacks of third-party applications declaratively and in a standardized way. This article will thoroughly discuss how we manage Helm in Kubernetes using Ansible.
Basic Helm Concepts: Chart, Release, Repository, and Values #
Before diving into Ansible playbook automation, it’s very important for us to understand the key terms used in the Helm ecosystem:
Key Helm Concepts:
- Chart: A Kubernetes application package containing YAML manifest templates and configuration metadata files.
- Release: An instance of a Chart running inside the Kubernetes cluster. One Chart can be deployed multiple times producing several Releases (e.g. db-dev and db-prod).
- Repository (Repo): An online repository where ready-to-download Charts are stored and shared.
- Values: Configuration parameters used to customize a Chart to our needs (like replica count, IP addresses, or memory limits).
WHEN DO WE NEED HELM?
✓ We want to install popular third-party applications (like Nginx Ingress, Cert-Manager, Postgres, Keycloak) that already have official Charts.
✓ We want to make our internal application deployments modular and easily customizable across environments through a single parameter file.
✓ We want to manage application release versions with instant rollback capability to previous versions on failure.
WHEN IS IT BETTER TO WRITE MANUAL MANIFESTS:
✗ For our own very simple internal microservices with no plans to share with outside teams (writing pure k8s YAML in Ansible is more practical).
How Ansible and Helm Integration Works #
Using Ansible to control Helm provides huge advantages over just writing wrapper shell scripts like helm upgrade --install.
flowchart TD
A["Ansible Playbook Execution"] --> B["kubernetes.core.helm Module"]
B -->|"Reads Kubeconfig & Helm Binary"| C["Kubernetes API Gateway"]
C -->|"Deploy / Upgrade / Rollback"| D["Helm Release in the K8s Cluster"]When we use the kubernetes.core.helm module, Ansible will:
- Verify whether the Helm release already exists in the cluster.
- Perform a diff between the current values configuration and the new configuration we declare in the playbook.
- Only send the necessary changes to the Kubernetes API Server (maintaining idempotency).
- Provide an error handling flow if the deployment process gets stuck, with the ability to trigger automatic rollback.
Step 1: Installing the Helm Binary Using Ansible #
For Ansible’s Helm module to run, the machine where the task executes (can be localhost or a target server) must already have the helm binary file installed. We can write a simple Ansible task to download the official installer script and install Helm automatically and idempotently.
# playbooks/install-helm.yml
---
- name: Helm CLI Installation on the Target Host
hosts: control_plane
become: true
tasks:
- name: Check whether Helm is already installed
stat:
path: /usr/local/bin/helm
register: helm_binary_check
- name: Download the official Helm installer script
get_url:
url: https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
dest: /tmp/get-helm-3.sh
mode: '0700'
when: not helm_binary_check.stat.exists
- name: Run the Helm installer
command: /tmp/get-helm-3.sh
when: not helm_binary_check.stat.exists
changed_when: true
- name: Verify the installation and display the Helm version
command: /usr/local/bin/helm version --short
register: helm_version_output
changed_when: false
- name: Display the Helm version output
debug:
msg: "Helm successfully installed: {{ helm_version_output.stdout }}"
Step 2: Managing Helm Repositories #
Before we can install Chart packages, we must add the Chart creator’s repository address to our local repository list. The kubernetes.core.helm_repository module lets us manage this list declaratively.
# playbooks/deploy-helm-apps.yml
---
- name: Manage Helm and Deploy Charts
hosts: localhost
connection: local
gather_facts: false
vars:
k8s_kubeconfig: "{{ playbook_dir }}/kubeconfig/admin.conf"
tasks:
- name: Add the required third-party Helm repositories
kubernetes.core.helm_repository:
name: "{{ item.name }}"
repo_url: "{{ item.url }}"
state: present
loop:
- { name: "ingress-nginx", url: "https://kubernetes.github.io/ingress-nginx" }
- { name: "bitnami", url: "https://charts.bitnami.com/bitnami" }
- { name: "prometheus-community", url: "https://prometheus-community.github.io/helm-charts" }
- name: Update the local Chart database (helm repo update)
command: helm repo update
changed_when: false
Step 3: Deploying a Helm Chart with Inline Values Customization #
After the repositories are ready, we can deploy our chosen Chart. In the example below, we’ll install the Nginx Ingress Controller into a dedicated namespace called ingress-nginx.
[!TIP] In production environments, always specify the Chart version specifically using the
chart_versionparameter. If we don’t set the version, Helm automatically takes the latest available version. This risks breaking the system (breaking change) when we re-run the playbook in the future.
- name: Deploy the Nginx Ingress Controller idempotently
kubernetes.core.helm:
kubeconfig: "{{ k8s_kubeconfig }}"
name: my-nginx-ingress
chart_ref: ingress-nginx/ingress-nginx
chart_version: "4.9.0" # Locking the Chart version for stability
release_namespace: ingress-nginx
create_namespace: true
state: present
# Customizing the default parameter values (values.yaml) inline
values:
controller:
replicaCount: 2
service:
type: LoadBalancer
resources:
requests:
cpu: "100m"
memory: "120Mi"
limits:
cpu: "300m"
memory: "256Mi"
Step 4: Managing Sensitive Variables with Ansible Vault #
Many third-party application Charts need sensitive data like admin passwords, encryption keys, or database integration tokens. We must not write these sensitive parameters openly in playbook code.
We must use Ansible Vault to encrypt those passwords, then forward them dynamically to the values block in the Helm module. We must also include the no_log: true parameter on that task to hide sensitive parameters from terminal logging systems.
# ANTI-PATTERN: Writing raw passwords in the playbook values block
# This password will be exposed on CI/CD console screens and other log monitoring systems
- name: Deploy the Database with an exposed password (DON'T DO THIS)
kubernetes.core.helm:
kubeconfig: "{{ k8s_kubeconfig }}"
name: bad-db
chart_ref: bitnami/postgresql
state: present
values:
auth:
postgresPassword: "secret-password-123" # ✗ Credential leak danger
# CORRECT: Fetching the encrypted password from Ansible Vault and hiding the task log
# Credentials are safely encrypted in Git and terminal logs are protected from leaks
- name: Deploy the PostgreSQL Database with Credential Protection (Recommended System)
kubernetes.core.helm:
kubeconfig: "{{ k8s_kubeconfig }}"
name: prod-database
chart_ref: bitnami/postgresql
chart_version: "13.4.0"
release_namespace: database
create_namespace: true
state: present
values:
auth:
database: "production_db"
username: "app_user"
postgresPassword: "{{ vault_postgresql_admin_password }}" # ✓ Fetched from Vault
password: "{{ vault_postgresql_user_password }}" # ✓ Fetched from Vault
primary:
persistence:
size: "20Gi"
# Hide the task output from the console terminal
no_log: true
Step 5: Using External Values Files and Multi-Values #
For applications requiring very large and detailed configuration (for example the Prometheus Stack monitoring suite), writing all parameters inline in the playbook makes the playbook code very long and hard to read.
The best way to solve this problem is separating the configuration into one or several external YAML files, then calling them using the values_files parameter. We can also combine a base configuration file with an environment-specific configuration file (override pattern).
- name: Deploy the Prometheus Monitoring Stack with External Values Files
kubernetes.core.helm:
kubeconfig: "{{ k8s_kubeconfig }}"
name: monitoring-stack
chart_ref: prometheus-community/kube-prometheus-stack
chart_version: "55.5.0"
release_namespace: monitoring
create_namespace: true
state: present
values_files:
# Global base configuration file for the entire cluster
- "{{ playbook_dir }}/helm-values/prometheus-base-values.yml"
# Production environment-specific configuration file (overriding base parameters)
- "{{ playbook_dir }}/helm-values/prometheus-prod-overrides.yml"
Example contents of the prometheus-prod-overrides.yml file:
# helm-values/prometheus-prod-overrides.yml
grafana:
enabled: true
adminPassword: "{{ vault_grafana_admin_password }}" # Dynamic Ansible variables can still be rendered inside values files!
persistence:
enabled: true
size: 10Gi
prometheus:
prometheusSpec:
retention: 14d
storageSpec:
volumeClaimTemplate:
spec:
resources:
requests:
storage: 50Gi
Helm Lifecycle: Upgrade, Wait, and Automatic Rollback #
By default, if a Helm release already exists in the cluster, the kubernetes.core.helm module performs an upgrade if it detects changes in the Chart version or values configuration.
However, there’s a common problem where the Kubernetes API reports the upgrade process was successfully accepted, even though the new Pods on the ground crash during boot. To ensure our deployment is truly production-ready, we must set the following parameters:
wait: true: Forces Ansible to block the execution process and wait until all Pods, Services, and Ingresses under that Chart are in an active and healthy state before considering the task successful.wait_timeout: The maximum wait time limit (e.g. “10m”). If exceeded, the process is considered failed.atomic: true: Very important in production. If combined withwait: true, this parameter guarantees that if the upgrade process fails (e.g. new Pods crash), Helm automatically triggers a clean instant rollback to the previous stable release version.
- name: Upgrade Nginx Ingress with an Atomic Rollback Strategy
kubernetes.core.helm:
kubeconfig: "{{ k8s_kubeconfig }}"
name: my-nginx-ingress
chart_ref: ingress-nginx/ingress-nginx
chart_version: "4.10.0" # Raising the Chart version
release_namespace: ingress-nginx
state: present
wait: true # Wait until all new pods are ready
wait_timeout: "10m" # 10 minute timeout
atomic: true # Automatic rollback to the previous version on failure!
values:
controller:
replicaCount: 3 # Increasing replicas to 3
After the release is updated, we can monitor its status using the kubernetes.core.helm_info module to ensure the final system state.
- name: Fetch status information from the Helm release
kubernetes.core.helm_info:
kubeconfig: "{{ k8s_kubeconfig }}"
name: my-nginx-ingress
release_namespace: ingress-nginx
register: ingress_release_info
- name: Display the current Helm release status
debug:
msg:
- "Release Name: {{ ingress_release_info.status.name }}"
- "Release Status: {{ ingress_release_info.status.status }}"
- "Application Version: {{ ingress_release_info.status.app_version }}"
when: ingress_release_info.status is defined
Removing Releases (State Absent) #
If we want to clean up the cluster from applications no longer in use, we can remove them cleanly by setting the state: absent parameter. This command automatically removes all Kubernetes resources ever created by that Chart.
- name: Remove the old database Helm release from the cluster
kubernetes.core.helm:
kubeconfig: "{{ k8s_kubeconfig }}"
name: legacy-database
release_namespace: database
state: absent
Summary #
- Helm acts as Kubernetes’ official package manager that wraps complex YAML manifests into one standardized Chart unit.
- Use the
kubernetes.core.helmmodule declaratively to automate the install, upgrade, and uninstall flow of third-party applications idempotently.- Always lock the Chart version (
chart_version) in production playbooks to avoid unwanted automatic updates that risk breaking compatibility.- Integrate Ansible Vault to store database credentials and Chart API keys, and include the
no_log: trueparameter to prevent secret data from leaking to terminal logs.- Use the
values_filesparameter to separate very large parameter configurations (like monitoring stacks) into external YAML files so playbook code stays clean and maintainable.- Set the
wait: trueandatomic: trueparameters when upgrading to trigger an automatic rollback process to the previous stable version if the new release fails to boot.- Leverage the
kubernetes.core.helm_infomodule to verify the final operational status of application releases after deployment.