Saturday, July 14, 2018

Liquibase migration with Docker containers

Motivation

I was looking for a Docker image to execute liquibase migrations on a MySQL database and I´ve found Kilna´s project. Although it is a very nice and simple to use image, I´d like to try something different.

I´d like to have an image that could be used by testers, front-end, developers, operators, etc, who doesn´t have to know Java or Maven and doesn´t care in which repositories the migration files are.

Base Images

To support the creation of the liquibase image, I´ve created two other images that serves as base for it. They can be found in my Github repository: https://github.com/mroger/liquibase-docker.

One is liquibase-docker/ubuntu-jdk8, an image with Ubuntu version 16.04 and JDK 8. Also installs vim for administrative and tests tasks.

To build this image, go to the ubuntu-jdk8 folder and execute the command:

$ docker build -t ubuntu-jdk8:latest .

Here is the Dockerfile:


The other is liquibase-docker/ubuntu-jdk8-dev an image that adds Maven and Git to the ubuntu-jdk8 image.

To build this image, go to the ubuntu-jdk8-dev folder and execute the command:

docker build -t ubuntu-jdk8-dev:latest .

Here is the Dockerfile:



Liquibase image

And finally, we are going to see the Dockerfile that is reponsible for making the image for executing liquibase migrations over a maven project.

The project can be found in my Github repository:

https://github.com/mroger/ubuntu-jdk8-liquibase

Here is the Dockerfile:


Below we can see what this Dockerfile does.

Line1:
We declare that we want to create this image having the image we built previously as the base image: ubuntu-jdk8-dev:latest.

Lines 3 through 7:
Here we define environment variables for project directory and database parameters, assigning default values to them. We´ll see how to override these environment variables in the next section.
As this project folder contains the liquibase project, we configure it as the LIQUIBASE_PROJECT_DIR.

Line 9:
In this line we add the content of the project directory to a folder inside the container. This way the running container will be able to access the liquibase project folder.

Line 11:
In this line we establish the directory where the next commands will be issued: in liquibase project folder.

Lines 13 through 18:
We execute the maven commands that will resolve all dependencies and plugins and burn them into the image so the container won´t need to download them every time it´s executed.
The liquibase:update goal needs a MySQL server up and running for it to connect to.

Lines 20 and 22:
These commands will copy the entrypoint script to the container and turn it into an executable.

Lines 24 and 25:
Those are commands that will be issued every time the container is executed.

As said earlier, to build this image, we need a MySQL server running, so liquibase:update can be executed without error:

$ docker run -itd -e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=TIMESHEET --name db -p 3306:3306 mysql:5.7.22

This will start a MySQL container that is need by the liquibase build so the liquibase dependencies and plugins are downloaded without error.

The environment parameter tells MySQL container to create a database if it´s not already created: TIMESHEET. It´s the same name used in the driver URL defined in line 5 of the Dockerfile.

And now we can build the liquibase image:

$ docker build -t ubuntu-jdk8-liquibase:latest --network="host" .

The --network="host" parameter is needed so the container´s network has access to the host´s network and, in our case, the Git command has access to the internet.

We can see the maven dependencies and plugins being downloaded. They will be part of the image.

After the download of the maven artifacts,  the liquibase migration starts and some SQL commands are issued against the running database. Using a MySQL client, we can see that some tables were created in the TIMESHEET database, some for the application and some for liquibase control.

Entrypoint script

The entrypoint script is executed every time the container is executed as opposed to the RUN commands above that are executed during the build of the image.

Here is the entrypoint script:

This script´s job is to update the liquibase project from the Git repository and execute the liquibase migration itself.

To run this container, you must run or start the MySQL container as seen in the last section and issue the command:

$ docker run -itd --name ubuntu-jdk8-liquibase --link db:localhost ubuntu-jdk8-liquibase:latest

Running the command below, we can see that no more dependencies are donwloaded because they were already downloaded to the image during its build phase.

$ docker logs -ft <container-id>

The --link parameter makes que MySQL port accessible to the liquibase container.

Docker-compose

With a docker-compose.yml file we can build all the images and start the MySQL server and migration process, all at once.

Here is the docker-compose.yml file:

This docker-compose file defines the services for mysql and liquibase and its dependencies.

To build and start the containers, you can issue the command:

$ docker-compose up -d

It has one problem, though: the liquibase depends on mysql image but it doesn´t mean that the liquibase container waits until the mysql server is available. It will probably fail on the first attempt, because the mysql server is not ready yet. But on the next and subsequent executions the liquibase container will successfuly start and update the mysql database.

This issue is addressed in the Docker documentation: https://docs.docker.com/compose/startup-order/

To stop the containers, you can execute the command:

$ docker-compose down

This will stop and remove the containers, which also means that all changes in database will be lost. This can be addressed configuring a volume pointing for the database files.

Summary

In this post we saw how to build images having others as base. We also saw how to build an liquibase image and run its container that connects and issue update commands to a MySQL server.

And filnally we saw how to create a docker compose file to wire the containers together.