Continuous Delivery on Kubernetes with Database using ArgoCD and Liquibase

Continuous Delivery on Kubernetes with Database using ArgoCD and Liquibase

In this article, you will learn how to design a continuous delivery process on Kubernetes with ArgoCD and Liquibase. We will consider the application that connects to a database and update the schema on a new release. How to do it properly for a cloud-native application? Moreover, how to do it properly on Kubernetes?

Fortunately, there are two types of tools that perfectly fit the described process. Firstly, we need a tool that allows us to easily deploy applications in multiple environments. That’s what we may achieve on Kubernetes with ArgoCD. In the next step, we need a tool that automatically updates a database schema on demand. There are many tools for that. Since we need something lightweight and easy to containerize my choice fell on Liquibase. It is not my first article about Liquibase on Kubernetes. You can also read more about the blue-green deployment approach with Liquibase here.

However, in this article, we will focus on a little bit different problem. First, let’s describe it.

Introduction

I think that one of the biggest challenges around continuous delivery is integration to the databases. Consequently, we should treat this integration the same as a standard configuration. It’s time to treat database code like application code. Otherwise, our CI/CD process fails at the database.

Usually, when we consider the CI/CD process for the application we have multiple target environments. According to the cloud-native patterns, each application has its own separated database. Moreover, it is unique per each environment. So each time, we run our delivery pipeline, we have to update the database instance on the particular environment. We should do it just before running a new version application (e.g. with the blue-green approach).

Here’s the visualization of our scenario. We are releasing the Spring Boot application in three different environments. Those environments are just different namespaces on Kubernetes: dev, test and prod. The whole process is managed by ArgoCD and Liquibase. In the dev environment, we don’t use any migration tool. Let’s say we leave it to developers. Our mechanism is active for the test and prod namespaces.

argocd-liquibase-arch

Modern Java frameworks like Spring Boot offer built-in integration with Liquibase. In that concept, we just need to create a Liquibase changelog and set its location in Spring configuration. Our framework is running such a script on application startup. Since it is a very useful approach in development, I would not recommend it for production deployment. Especially if you deploy your application on Kubernetes. Why? You will find a detailed explanation in the article I have already mentioned in the first paragraph.

Source Code

If you would like to try it by yourself, you may always take a look at my source code. In order to do that you need to clone my GitHub repository. Then you should just follow my instructions 🙂

Also, one thing before we start. Here I described the whole process of building and deploying applications on Kubernetes with ArgoCD and Tekton. Typically there is a continuous integration phase, which is realized with Tekton. In this phase, we are just building and pushing the image. We are not releasing changes to the database. Especially, that we have multiple environments.

The picture visible below illustrates our approach in that context. ArgoCD synchronizes configuration and applies the changes into the Kubernetes cluster and a database using Liquibase. It doesn’t matter if a database is running on Kubernetes or not. However, in our case, we assume it is deployed on the namespace as the application.

argocd-liquibase-pipeline

Docker Image with Liquibase

There is an official Liquibase image on Docker Hub. We need to execute the update command using this image. To do that I prepared a custom image based on the official Liquibase image. You can see the Dockerfile below. But you can as well pull the image I published in my Docker registry docker.io/piomin/liquibase:latest.

FROM liquibase/liquibase
ENV URL=jdbc:postgresql://postgresql:5432/test
ENV USERNAME=postgres
ENV PASSWORD=postgres
ENV CHANGELOGFILE=changelog.xml
CMD ["sh", "-c", "docker-entrypoint.sh --url=${URL} --username=${USERNAME} --password=${PASSWORD} --classpath=/liquibase/changelog --changeLogFile=${CHANGELOGFILE} update"]

We will run that image as the init container inside the pod with our application. Thanks to that approach we can be sure that it updates database schema just before starting the container with the application.

Sample Spring Boot application

We will use one of my sample Spring Boot applications in this exercise. You may find it in my GitHub repository here. It connects to the PostgreSQL database:

spring:
  application:
    name: person-service
  datasource:
    url: jdbc:postgresql://person-db:5432/${DATABASE_NAME}
    username: ${DATABASE_USER}
    password: ${DATABASE_PASSWORD}

You can clone the application source code by yourself. But you can as well pull the ready image located here: quay.io/pminkows/person-app. The application is compiled with Java 17 and uses Spring Data JPA as an ORM layer to integrate with the database.

<properties>
  <java.version>17</java.version>
</properties>
<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
  </dependency>
  <dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <scope>runtime</scope>
  </dependency>
</dependencies>

Use Liquibase with ArgoCD and Kustomize

In our scenario, the Liquibase init container should be included in the Deployment only for test and prod namespaces. On the dev environment, the pod should just contain a single container with the Spring Boot application. In order to implement this behavior with ArgoCD, we may use Kustomize. Kustomize has the concepts of bases and overlays. A base is a directory with a kustomization.yaml, which contains a set of resources and associated customization. Thanks to overlays, we may include additional elements into the base manifests. So, here’s the structure of our configuration repository in GitHub:

Here’s our base Deployment file. It’s pretty simple. The only thing we need to do is to inject database credentials from secrets:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample-spring-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sample-spring
  template:
    metadata:
      labels:
        app: sample-spring
    spec:
      containers:
        - name: sample-spring
          image: quay.io/pminkows/person-app:1.0
          ports:
            - containerPort: 8080
          env:
            - name: DATABASE_USER
              valueFrom:
                secretKeyRef:
                  name: postgres
                  key: database-user
            - name: DATABASE_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: postgres
                  key: database-password
            - name: DATABASE_NAME
              valueFrom:
                secretKeyRef:
                  name: postgres
                  key: database-name

We also have the Liquibase changeLog.sql file in the base directory. We should place it inside Kubernetes ConfigMap:

apiVersion: v1
kind: ConfigMap
metadata:
  name: changelog-cm
data:
  changeLog.sql: |
    --liquibase formatted sql
    --changeset piomin:1
    create table person (
      id serial primary key,
      name varchar(255),
      gender varchar(255),
      age int,
      externalId int
    );
    insert into person(name, age, gender) values('John Smith', 25, 'MALE');
    insert into person(name, age, gender) values('Paul Walker', 65, 'MALE');
    insert into person(name, age, gender) values('Lewis Hamilton', 35, 'MALE');
    insert into person(name, age, gender) values('Veronica Jones', 20, 'FEMALE');
    insert into person(name, age, gender) values('Anne Brown', 60, 'FEMALE');
    insert into person(name, age, gender) values('Felicia Scott', 45, 'FEMALE');

Also, let’s take look at the base/kustomization.yaml file:

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - deployment.yaml
  - changelog.yaml

In the overlays/liquibase/liquibase-container.yaml file we are defining the init container that should be included in our base Deployment. There are four parameters available to override. Therefore, we will set the address of a target database, username, password, and location of the Liquibase changelog file. The changeLog.sql file is available to the container as a mounted volume under location /liquibase/changelog. Of course, I’m using the image described in the Docker Image with Liquibase section.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample-spring-deployment
spec:
  template:
    spec:
      initContainers:
        - name: liquibase
          image: docker.io/piomin/liquibase:latest
          env:
            - name: URL
              value: jdbc:postgresql://person-db:5432/sampledb
            - name: USERNAME
              valueFrom:
                secretKeyRef:
                  name: postgres
                  key: database-user
            - name: PASSWORD
              valueFrom:
                secretKeyRef:
                  name: postgres
                  key: database-password
            - name: CHANGELOGFILE
              value: changeLog.sql
          volumeMounts:
            - mountPath: /liquibase/changelog
              name: changelog
      volumes:
        - name: changelog
          configMap:
            name: changelog-cm

And last manifest in the repository. Here’s the overlay kustomization.yaml file. It uses the whole structure from the base catalog and includes the init container to the application Deployment:

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
bases:
  - ../../base
patchesStrategicMerge:
  - liquibase-container.yaml

Create ArgoCD applications

Since our configuration is ready, we may proceed to the last step. Let’s create ArgoCD applications responsible for synchronization between Git repository and both Kubernetes cluster and a target database. In order to create the ArgoCD application, we need to apply the following manifest. It refers to the Kustomize overlay defined in the /overlays/liquibase directory. The declaration for test or prod environment looks as shown below:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: liquibase-prod
spec:
  destination:
    namespace: prod
    server: 'https://kubernetes.default.svc'
  project: default
  source:
    path: overlays/liquibase
    repoURL: 'https://github.com/piomin/sample-argocd-liquibase-kustomize.git'
    targetRevision: HEAD

On the other hand, the dev environment doesn’t require an init container with Liquibase. Therefore, it uses the base directory:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: liquibase-dev
spec:
  destination:
    namespace: dev
    server: 'https://kubernetes.default.svc'
  project: default
  source:
    path: base
    repoURL: 'https://github.com/piomin/sample-argocd-liquibase-kustomize.git'
    targetRevision: HEAD

Since ArgoCD supports Kustomize, we just need to create applications. Alternatively, we could have created them using ArgoCD UI. Finally, here’s a list of ArgoCD applications responsible for applying changes to the Postgres database. Here’s the UI view for all the environments after synchronization (Sync button).

We can also verify the logs printed by the Liquibase init container after ArgoCD synchronization:

We may also verify a list of running pods in one of the target namespace, e.g. prod.

$ kubectl get pod                                               
NAME                                        READY   STATUS    RESTARTS   AGE
person-db-1-vn8kw                           1/1     Running   0          82m
sample-spring-deployment-7695d64c54-9hf75   1/1     Running   0          7m16s

And also print out Liquibase logs using kubectl:

$ kubectl logs sample-spring-deployment-7695d64c54-9hf75 -c liquibase

4 COMMENTS

comments user
kishore

how to modify the changeset here? Suppose i have a new table

    comments user
    piotr.minkowski

    You can add many changesets in the same file. Just add such a comment `–changeset piomin:2` similarly as there is `–changeset piomin:1` and place your new table sql below

      comments user
      kishore

      i’m getting checksum error when i modify a changeset, should i not modify at all? What is the good practice?

        comments user
        piotr.minkowski

        Yes, you cannot modify it after adding applying on the database. You should prepare a new changeset version with a proper SQL

Leave a Reply