pwshub.com

How to Run Database Migrations in Kubernetes – Different Approaches with Examples

How to Run Database Migrations in Kubernetes – Different Approaches with Examples

In the era of Microservices and Kubernetes, managing database migrations has become more complex than ever. Traditional methods of running migrations during application startup are no longer sufficient.

This article explores various approaches to handling database migrations in a Kubernetes environment, with a focus on Go tooling. You'll get the most out of this article if you already have some experience with Go, Kubernetes, and relational databases.

Table of Contents

  • The challenge of migrations in Kubernetes

  • Popular migration tools for Golang

  • Run migrations inside the application

  • Run migrations in initContainers

  • Run migrations as a Kubernetes Job

  • Helm hooks

  • Best practices for Kubernetes migrations

  • Conclusion

  • Resources

The Challenge of Migrations in Kubernetes

Kubernetes introduces new challenges for database migrations:

  • Multiple replicas starting simultaneously. These can span the same migration twice which may introduce some database locks.

  • Separation of concerns between application and migration logic. This means it’s good to be able to run or rollback migrations without redeploying your application.

As I mentioned in another post, there are a few different tools you can use to manage your migrations. They are quite similar, so I personally don’t have a strong preference between once or another. I just wanted to provide a few options so you know what the popular tools are.

  1. golang-migrate
  • Widely used and supports numerous databases.

  • Simple CLI and API.

  • Supports various migration sources (local files, S3, Google Storage).

  1. goose
  • Supports main SQL databases.

  • Allows migrations written in Go for complex scenarios.

  • Flexible versioning schemas.

  1. atlas
  • Powerful database schema management tool.

  • Supports declarative and versioned migrations.

  • Offers integrity checks and migration linting.

  • Provides GitHub Actions and Terraform provider.

Run Migrations Inside the Application

A naïve implementation would be to run the code of the migration directly inside your main function before you start your server.

Example using golang-migrate:

package main
import (
    "database/sql"
    "fmt"
    "log"
    "net/http"
    "github.com/golang-migrate/migrate/v4"
    "github.com/golang-migrate/migrate/v4/database/postgres"
    _ "github.com/golang-migrate/migrate/v4/source/file"
    _ "github.com/lib/pq"
)
func main() {
    // Database connection parameters
    url := "postgres://user:pass@localhost:5432/dbname"
    // Connect to the database
    db, err := sql.Open("postgres", url)
    if err != nil {
        log.Fatalf("could not connect to database: %v", err)
    }
    defer db.Close()
    // Run migrations
    if err := runMigrations(db); err != nil {
        log.Fatalf("could not run migrations: %v", err)
    }
    // Run the application, for example start the server
    if err := http.ListenAndServe(":8080", nil); err != nil {
        log.Fatalf("server failed to start: %v", err)
    }
}
func runMigrations(db *sql.DB) error {
    driver, err := postgres.WithInstance(db, &postgres.Config{})
    if err != nil {
        return fmt.Errorf("could not create database driver: %w", err)
    }
    m, err := migrate.NewWithDatabaseInstance(
        "file://migrations", // Path to your migration files
        "postgres",          // Database type
        driver,
    )
    if err != nil {
        return fmt.Errorf("could not create migrate instance: %w", err)
    }
    if err := m.Up(); err != nil && err != migrate.ErrNoChange {
        return fmt.Errorf("could not run migrations: %w", err)
    }
    log.Println("migrations completed successfully")
    return nil
}

However, these could cause different issues like your migrations being slow and Kubernetes considering that the pod didn’t start successfully and therefore killing it. You could run those migrations in a Go routine, but how do you handle failures then?

In cases when multiple pods are created at the same time, you would have a potential concurrency problem.

It also means your migrations need to be inside your Docker image.

Even with its downsides, this approach might work well for quick and stable database changes and small projects.

Run Migrations in initContainers

By using initContainers in your Kubernetes Deployment, it will run the migration before the main application container starts. This is a good first solution for when scaling is not a problem yet.

If the initContainer fails, the blue/green deployment from Kubernetes won’t go further and your previous pods stay where they are. This prevents having a newer version of the code without the planned migration.

Example:

initContainers:
  - name: migrations
    image: migrate/migrate:latest
    command: ['/migrate']
    args: ['-source', 'file:///migrations', '-database','postgres://user:pass@db:5432/dbname', 'up']

This approach might work well for quick and stable database changes for deployments with a single Pod. And it already separates the application and migration layers.

You could create a Kubernetes Job that runs your migrations, and trigger that job during the deployment process before rolling out the application.

Example:

apiVersion: batch/v1
kind: Job
metadata:
  name: db-migrate
spec:
  template:
    spec:
      containers:
      - name: migrate
        image: your-migration-image:latest
        command: ['/app/migrate']

You can also combine it with initContainers, making sure that the pod starts only when the job is successful.

initContainers:
  - name: migrations-wait
    image: ghcr.io/groundnuty/k8s-wait-for:v2.0
    args:
      - "job"
      - "my-migration-job"

This approach can solve the problems related to multiple replicas mentioned above.

Helm Hooks

If you use Helm, it has hooks that you can use for running migrations during chart installation/upgrade. You just define a pre-install or pre-upgrade hook in your Helm chart.

pre-install-hook.yaml:

apiVersion: batch/v1
kind: Job
metadata:
  name: {{ include "mychart.fullname" . }}-migrations
  annotations:
    "helm.sh/hook": pre-install,pre-upgrade
    "helm.sh/hook-weight": "-5"
    "helm.sh/hook-delete-policy": hook-succeeded
spec:
  template:
    spec:
      containers:
        - name: migrations
          image: your-migrations-image:tag
          command: ["./run-migrations.sh"]

In this example, the pre-install hook executes after templates are rendered, but before any resources are created in Kubernetes.

This of course works only when you use Helm, meaning you need to find something else if you decide not to use Helm.

Best Practices for Kubernetes Migrations

Decouple migrations from application code:

  1. Create a separate Docker image for migrations. This ensures that migration logic is encapsulated and doesn't interfere with the application codebase.

  2. Use tools like Atlas to manage migrations independently. Tools like Atlas provide features for automating migration processes, scheduling, and rollback.

Use version control for migrations:

  1. Store migration files in your Git repository. This ensures a complete history of migration changes, making it easier to track and revert changes.

  2. Use sequential or timestamp-based versioning. Sequential versioning guarantees the correct order of migrations which is very important for relational databases.

Ensure idempotent migrations:

  1. Ensure migrations can be run multiple times without side effects. Idempotent migrations prevent accidental data corruption or inconsistencies if a migration is run multiple times.

Have a rollback strategy

  1. Implement and test rollback procedures for each migration. Having a rollback strategy ensures that you can revert changes if a migration fails or causes unexpected issues.

Perform monitoring and logging

  1. Use tools like Atlas Cloud for visibility into migration history. Atlas Cloud provides detailed logs and history of migrations, making it easy to track changes and troubleshoot issues.

Conclusion

Managing database migrations in a Kubernetes environment requires careful planning and execution.

By leveraging tools like golang-migrate, goose, or atlas, and following best practices, you can create robust, scalable, and maintainable migration strategies.

Remember to decouple migrations from application code, use version control, and implement proper monitoring to ensure smooth database evolution in your Kubernetes-based architecture.

Resources

Source: freecodecamp.org

Related stories
3 weeks ago - Different programming languages have their own ways of working with relational databases and SQL. Ruby on Rails has its Active Record, Python has SQLAlchemy, Typescript has Drizzle, and so on. Go is a language with quite a diverse...
1 month ago - API monitoring is the process of tracking and analyzing the performance, availability, and functionality of application programming interfaces (APIs) to ensure they function correctly and efficiently. It helps detect issues like slow...
1 week ago - Docker hosting platform provides cloud infrastructure for users to deploy and manage Docker containers. This type of platform simplifies container scaling and offers an optimized environment for users to manage their containerized...
13 hours ago - VPS (Virtual Private Server) hosting helps businesses host websites, eCommerce, and digital services, as it provides a scalable yet affordable hosting option. It utilizes virtualization technology to break physical servers into multiple...
1 month ago - Spring Boot is an open-source micro framework maintained by a company called Pivotal. It provides Java developers with a platform to get started with an auto configurable production-grade Spring application. With it, developers can get...
Other stories
2 hours ago - Data visualization tools let you turn raw numbers into visuals — so you have some guidance when making design choices. I talk more on this in today's blog. The post Using data visualization tools as a UX/UI designer appeared first on...
2 hours ago - So, you’re a JavaScript developer? Nice to hear — what do you think this code returns? And yeah, it’s a […] The post Six things you may not know about JavaScript appeared first on LogRocket Blog.
2 hours ago - Try supporting NPS with CES, which helps you uncover where customers struggle, and CSAT, which focuses on product satisfaction. The post Why CES will give you more insights than CSAT and NPS appeared first on LogRocket Blog.
2 hours ago - IdPs (aka Identity providers) are crucial in the modern digital world. Learn what they are and what they do.
4 hours ago - Mobile app development has evolved tremendously, and creating a robust, full-featured app today involves mastering both the front-end and back-end. If you're looking to build something practical, like an e-commerce platform, and want to...