DevOps: implementing continuous deployment with Docker, Ansible and CircleCi

by | DevOps Expertise

Continuous deployment (CD) allows new functionalities developed by the teams to be delivered automatically as they are developed. It is often the last step in the continuous integration chain, and only occurs if the previous phases have been successfully completed (tests, build, etc.).

Automating the deployment saves valuable time in the application delivery chain. Another advantage is that every change is immediately available, for example on the test platform, which guarantees instant feedback. Bugs are detected as soon as possible, users can test as they go along and offer feedback continuously.

In this article we will see how to easily set up continuous deployment on a project hosted on Scaleway using CircleCI, Ansible and Docker.

All the files described are available on github, a link is provided for each of them.
Let'sgo!


Prerequisite

We will need a Scaleway account with a dedicated docker registry, which will allow us to push the docker images we build.

The tree structure of the different files we will refer to is as follows:

We will assume that the web service is already running on a staging server, on which docker and docker-compose are installed. A file docker-compose.yml file located on the server, in /etc/docker/, sets up the service. Here it is:

Docker-compose.yamlfile

version: "3.5
services:
  backend:
    image: rg.en-par.scw.cloud/thetribe/backend:latest
    restart: unless-stopped
    container_name: backend
    ports: - "8080:8080"
    environment: - NODE_ENV=staging

Let's go into some detail.

  • image describes the image that the backend service will use, which is what we will build below.
  • restart the policy of reviving the service
  • container_name parameter the name of the container
  • ports: 8080:8080 bind port 8080 of the container to port 8080 of the machine

As we have seen, the docker-compose is based on an image built by a Dockerfile which acts as a "recipe".

To build our image, we will perform the operation in two steps, creating a build image, then creating the final image. We will use docker's multi-stage build to build an optimised image.

The principle is to build the application in the first image, then to keep only the built application in the final image . The final image will then be free of all the dependencies necessary for the build and will therefore be lighter.

  1. Construction of the build image

Dockerfile

# ----- build image -----

FROM node:14.15.0-alpine AS build-env

WORKDIR /app

COPY . /app
RUN yarn install
RUN yarn build

RUN mkdir tmp \
&& cp -r build \
package.json \
tsconfig.json \
yarn.lock \
tmp

We copy the local sources in /app, then we run the installation commands yarn install & yarn build. Finish by copying the built application and its dependencies into a tmp folder.

  1. Construction of the final image
# ----- finale image -----

FROM node:14.15.0-alpine

WORKDIR /app
LABEL maintainer="Tommy Alexandre <tommy.alexandre@thetribe.io>"
COPY --from=build-env /app/node_modules /app/node_modules
COPY --from=build-env /app/tmp /app
EXPOSE 8080

USER node
ENTRYPOINT [ "yarn" ]
CMD [ "start" ]

We copy the tmp folder from the previous environment to /app, then we run the yarn start command to start the service.

That's it for building the image! We can now move on to setting up Circle.

Setting up Circle CI

We need to make some settings on Circle, in particular to retrieve the access keys that we will use for the deployment.

Adding the SSH key of the staging server to Circle

  • Go to project settings
  • SSH keys tab
  • Additional SSH keys
  • Add SSH key

Here we will add the SSH key to connect to the staging server. Let's get the fingerprint, we will use it later.

Addition of the Secret Key Scaleway

  • Go to project settings
  • Environment variables
  • Add Environment Variable

Add the SCW_SECRET_KEY variable corresponding to the scaleway secret key used to connect to the Scaleway docker registry.

This key can be found on Scaleway(https://console.scaleway.com/project/credentials, API key)

As of now, Circle has all the necessary keys for deployment.

Implementation of continuous deployment

Continuous deployment will be fully implemented in the IC. The steps are as follows:

  • Building the application

We will build the image using the Dockerfile and push it to the docker registry.

  • Deployment on the test platform

We'll run an ansible script that will pull the new image to the staging server, then restart the services.

Let's go!

Building the application

In the Circle Ci configuration file (.circleci/config.yml), let's add the first step.

Let's start by describing the executors.

File config.yml

version: "2.1"
executors:
  node:
    docker:
      - image: circleci/node:14.16.0
  ubuntu:
    machine:
      image: ubuntu-2004:202010-01

Now let's add our job, build-docker-image.

jobs:
  build-docker-image:
    docker:
      - image: circleci/node:14.15.0
    working_directory: ~/project
    steps:
      - setup_remote_docker:
          version: 19.03.13
      - run: docker login rg.fr-par.scw.cloud/thetribe -u nologin -p $SCW_SECRET_KEY # ICI VALUER LE BON REGISTRY DOCKER
      - run:
          command: docker build --tag rg.fr-par.scw.cloud/thetribe/backend:latest latest . && docker push rg.fr-par.scw.cloud/thetribe/backend:latest
          no_output_timeout: 30m

This job, as the name suggests, will build the docker image of our service and push it to the scaleway registry.

Let's break it down a bit.

    docker:
      - image: circleci/node:14.15.0

Circle is told to use a circleci/node docker image (which is an official Circle image) to do the job.

    steps:
      - setup_remote_docker:
          version: 19.03.13

This first step, a bit special, allows to do docker in docker. As we told circle to perform the step in a docker image, and we want to run docker commands, we need to specify the setup_remote_docker instruction. (Be careful to replace the docker registry!)

- run: docker login rg.fr-par.scw.cloud/thetribe -u nologin -p $SCW_SECRET_KEY

This second step is not at all complicated. We run the docker login command with the scaleway key we defined above as a parameter. The $SCW_SECRET_KEY variable is known to Circle because we have added it to the project parameters.

- run:
          command: docker build --tag rg.fr-par.scw.cloud/thetribe/backend:latest latest . && docker push rg.fr-par.scw.cloud/thetribe/backend:latest

We build the docker image with the latest tag and push it to the docker registry.

Finally, we describe how the job is called in the Circle workflows.

workflows:
  version: "2"
  build:
    jobs:
      - build-docker-image:

It's all good!

At each commit, the build-docker-image job will be called, build and push a docker image to the scaleway registry.

Deployment on the test platform

Now we have to do the last step: make the staging server pull the new image to update the service . Let's set up the ansible deployment script.

We will need :

  • An inventory file
  • A playbook
  • From a "deployment" role

The inventory file(ansible/staging) :

Stagingfile

[staging:children]
web_server

[web_server]
163.172.214.134 ansible_user=root ansible_become=yes # ICI VALUER LA BONNE IP

This inventory file will simply define the ip of our staging server. The parameters ansible_user=root and ansible_become=yes specify that the user used will be root and that we allow the script to switch to root mode when it needs to.

The playbook(ansible/deployment.yaml) :

File deployment.yaml

---
- hosts: web_server
  roles:
      - role: deploy-docker
  vars:
      docker_compose_path: /etc/docker/
  vars_files:
      - vars/scaleway_credentials.yml

This playbook is also quite simple.

  • It defines the web_server host that we described in the inventory file
  • It specifies the deployer-docker role that we will write below
  • It defines a variable (the path of the docker-compose on the server), as well as a variable file containing the login information to the scaleway registry. For the moment, this file is empty. We will fill it in the Circle job.

The role(ansible/roles/deploy-docker/tasks/main.yaml) :

File main.yaml

---
- name: Log into scaleway docker registry
  command: docker login {{ scaleway_registry }} -u {{ scaleway_user }} -p {{ scaleway_token }}
  no_log: true

- name: Pull docker images
  command: docker-compose pull
  args:
    chdir: "{{ docker_compose_path }}"

- name: Log out of any docker registry
  command: docker logout
  no_log: true

- name: Deploy services
  command: docker-compose up -d
  args:
    chdir: "{{ docker_compose_path }}"

- name: Remove old image
  command: docker image prune -a -f

The role itself! That's where it all comes in 😉 

This role contains five steps:

  • The connection to the docker registry (uses the variables defined above)
docker login {{ scaleway_registry }} -u {{ scaleway_user }} -p {{ scaleway_token }}
  • The pull of the new image (with the path of the docker-compose as a variable)
docker-compose pull
  • Disconnecting the docker registry
docker logout
  • Deployment of the new service
docker-compose up -d
  • Deleting the old image
docker image prune -a -f

That's it for the Ansible part. Our role is ready to deploy a new image on our staging server.

Once the Ansible configuration is in place, all that remains is to add the deployment step in the CI (.circleci/config.yml) following the image construction step.

Let's create our new job:

deploy-staging:
    docker:
      - image: ansible/ansible:ubuntu1604
    working_directory: ~/project
    environment:
        ANSIBLE_HOST_KEY_CHECKING: false

Let's add the steps one by one.

Step 1:

    steps:
      - add_ssh_keys:
          fingerprints:
            - "91:76:19:c1:05:1b:09:71:03:56:31:de:a7:3b:c6:b1" # ICI VALUER LA BONNE FINGERPRINT

We add the fingerprint of the ssh key of our test server (the one we defined above) so that Circle can connect to the server.

Step 2:

      - run:
          name: Create var folder
          command: "mkdir ansible/vars"

This step will create the ansible/vars folder, in which we will push our Ansible configuration variables (the docker registry connection variables)

Steps 3 : 

      - run:
          name: Set scaleway registry
          command: 'echo -e "---\nscaleway_token: $SCW_SECRET_KEY \nscaleway_user: nologin \nscaleway_token: $SCW_SECRET_KEY " > ansible/vars/scaleway_credentials.yml'

We write the correct values to our configuration variables. The value of scaleway_token is taken directly from the environment variables we added to the Circle project configuration.

Step 4:

      - run:
          name: Install ansible
          command: python -m pip install pip==20.1.1 && python -m pip install ansible && python -m pip install jmespath

We install ansible

Step 5:

      - run:
          name: Deploy to staging
          command: cd ansible && ansible-playbook -i staging deployment.yml

We launch the deployment with the ansible-playbook command. The -i option is used to specify the inventory.

All we need to do is add our job to the list of workflows, and specify the requires parameter to say that it depends on the build-docker-image job.

workflows:
  version: "2"
  build:
    jobs:
      - build-docker-image:
      - deploy-staging:
          requires:
            - build-docker-image

That's it!

At the next commit, continuous integration will build the new image, push it to the docker registry, and trigger the pull and restart of the services on the test platform.

This is a good basis for implementing continuous deployment, and is already sufficient to save valuable deployment time each time a change is made to the project.

Of course, it is always possible to optimise things. One could for example provide a version management system, set up filter rules in the CI to deploy only when a merge on develop or on master, build a test infrastructure on the fly with Terraform... 

Everything is possible with DevOps

Tommy Alexandre

Tommy Alexandre

Lead developer @theTribe

Why don't we talk?