Skip to main content

Deploying schema migrations to Kubernetes with Helm

Helm is a popular package manager for Kubernetes that allows developers to package applications into distributable modules called Charts that can be installed, upgraded, uninstalled, and more against a Kubernetes cluster.

Helm is commonly used by software projects as a means for distributing software in a way that will be simple for developers to manage on their clusters. For example, Bitnami maintains hundreds of charts for easily installing many popular applications, such as MySQL, Apache Kafka and others on Kubernetes.

In addition, many teams (Ariga among them) use Helm as a way to package internal applications for deployment on Kubernetes.

In this guide, we demonstrate how schema migrations can be integrated into Helm charts in such a way that satisfies the principles for deploying schema migrations which we described in the introduction.

Prerequisites to the guide:

  1. A migrations docker image
  2. A Helm chart defining your application.

Using Helm lifecycle hooks

To satisfy the principle of having migrations run before the new application version starts, as well as ensure that only one migration job runs concurrently, we use Helm's pre-upgrade hooks feature.

Helm pre-upgrade hooks are chart hooks that:

Executes on an upgrade request after templates are rendered, but before any resources are updated

To use a pre-upgrade hook to run migrations with Atlas as part of our chart definition, we create a template for a Kubernetes Job and annotate it with the relevant Helm hook annotations.

apiVersion: batch/v1
kind: Job
# job name should include a unix timestamp to make sure it's unique
name: "{{ .Release.Name }}-migrate-{{ now | unixEpoch }}"
labels: "{{ .Chart.Name }}-{{ .Chart.Version }}"
"": pre-install,pre-upgrade
"": before-hook-creation,hook-succeeded
name: "{{ .Release.Name }}-create-tables"
labels: {{ .Release.Service | quote }} {{ .Release.Name | quote }} "{{ .Chart.Name }}-{{ .Chart.Version }}"
restartPolicy: Never
- name: {{ .Values.imagePullSecret }}
- name: atlas-migrate
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
- migrate
- apply
- -u
- {{ .Values.dburl }}
- --dir
- file:///src/

Be sure to pass the following values:

  • imagePullSecret - secret containing credentials to a private repository. If you are hosting on, see this guide.
  • image.repository: the container repository where you pushed your migration image to.
  • image.tag: the tag of the latest migration image.
  • dburl: the URL of the database which you want to apply migrations to.

Notice the annotations block at the top of the file. This block contains two important attributes:

  1. "": pre-install,pre-upgrade: configures this job to run as a pre-install hook and as a pre-upgrade hook.
  2. "": before-hook-creation,hook-succeeded: sets the following deletion behavior for the jobs created by the hook:
  • before-hook-creation: Delete the previous resource before a new hook is launched (default)
  • hook-succeeded: Delete the resource after the hook is successfully executed. This combination ensures that on the happy path jobs are cleaned after finishing and that in case a job fails, it remains on the cluster for its operators to debug. In addition, it ensures that when you retry a job, its past invocations are also cleaned up.