Continuous Integration with Docker

Maintenance is one of the indisputable costs in software development projects. The code base is updated with multiple commits day after day. We cannot promise that if the code is still behaving as same as before the previous commit. If an issue is discovered today which is introduced two weeks ago, it can be too late to fix it or the cost of fixing could be much higher compared to fixing the issues right after the issues are introduced. Finding the issues as fast as possible can be done by proper testing in Continuous Integration (CI). This concept is a development practice which states that the code should be tested and verified with each commit from each developer from the project.

In this article, I am going to implement a Continuous Integration pipeline using Containerization. By packaging the application together with its dependencies, containerization helps smooth delivery of the software in an isolated environment. In this post we will use Docker for containerization and Gitlab-CI for Continuous Integration.

First, I am going to build a Fibonacci calculator using Flask. I am creating a function which calculates the nth Fibonacci number. Additionally, I created an endpoint which allows user to GET the index or POST a form. Here is how app.py looks like:

The project includes a requirements.txt file with all the dependencies required for the project to be run.

Testing is one of the crucial part of software engineering projects to make sure everything is healthy and works fine. It is time to write unit tests! I am using pytest for my unit tests. Naming conventions are important for pytest, we need to start naming the python test file with ‘test_’. I called my file test_app.py. The clean way of using fixtures with pytest is the reason why I chose it.

@pytest.mark.parametrize  is a fixture in pytest which we can use to pass multiple parameters for the same test. I need to define what the input and the expected output is, like below.

assert n keywords checks if the given operation is correct between the result of the application and the expected value.

To execute the unit tests, in the project root folder we run the pytest command with the following result:

In the result we can see that the same scenario is executed with 3 different input/ouput pairs.

It is time to dockerize the application!

Docker is containerization platform which packages the application code with its dependencies. So, the application can run anywhere independently of environment. I need to tell Docker what to install, where the starting point of my application is located and what the environment variables required by the app are. To pass all this information to Docker, we need to create a Dockerfile. A Dockerfile contains the recipe of your application.

FROM: what is the base image

RUN: to run bash commands

COPY: copy from to target

WORKDIR: set the given directory as a current directory

ENV: set an environment variable

CMD: Application start point

My Docker file is below:

Now, I can build my docker image from the Dockerfile to see if it works. Go to the project directory.

cd dockerci

docker build -t calculator .

To list all available docker images:

docker images or docker image ls

I see my image is built successfully, now I can run my container to run the application.

docker run -d -p 5000:5000 calculator

To list all docker containers:

docker ps

Perfect, I see my docker container is created and running successfully.

Note: If you cannot see your docker container when you do docker ps, run docker ps -a to see containers that are not running. Then you can docker logs <CONTAINER_ID> to investigate what is going on.

My container is running successfully, now I can go http://localhost:5000 to see my application and play with it.

Now that Docker is set up, it is time to jump into CI. I will use Gitlab-CI because it is very easy to integrate with your application. There are two ways to implement Gitlab-CI, first you can use the Gitlab native servers to build and run your application, which means you basically don’t need anything but a pipeline. This pipeline will be executed on the Gitlab servers. Alternatively, you can setup Gitlab runner on your own server, then the pipeline operations will be executed there. For the current project, I will use Gitlab-CI servers so I don’t need to setup Gitlab Runner.

Let’s create the .gitlab-ci.yml file.

We can run multiple stages in an order using stages like below:

Here we have 3 stages unittest, build and push. First the unit test stage will be executed, then the build stage and finally the push stage. If the unit test stage fails, the pipeline will stop and next steps will not be executed.

To do this, I need to write what commands need to be executed. The first stage is unit test and I define it with stage: unittest, and the script phase will execute the commands in order. To execute the pip command I started with base image image: python:3.6

Together with build and push my gitlab-ci.yml file looks like this:

Now my pipeline is ready!

Whenever I push a new change this pipeline will be automatically trigggered by Gitlab-CI. I can clearly see which steps are being executed currently or which are already done.

My pipeline is successfully executed and the status is passed!

Here is my Youtube Channel which I explain all the steps in detail. : https://www.youtube.com/playlist?list=PLSt3GXI7vWE9iCnPgkIdPnmina9EEoGb4

Here is the gitlab project link: https://gitlab.com/selin-gungor/astrea/tree/master

Thanks for reading. See you in my next post! 🙂

Selin Gungor