Automated provisioning and deployments with Ansible, Docker and Bitbucket pipelines.
By: Stivi Ndoni
Tags: Bitbucket Pipelines, Docker Swarm, Nginx, Docker, Ansible

Project structure

Creating the express app

We will start by installing express, nodemon, and dotenv.

yarn add express nodemon dotenv

Add the following script into package.json

"start": "nodemon src/"

This will run the express server.

In the src/index.js file we will create a simple server:

Create the docker image to build the express app

Create the NGINX docker image

NGINX config file:

NGINX will use the least connections algorithm to route the requests between node app replicas.

Docker compose files

For each environment we will create 3 separate docker-compose.yml files which will be sent in the related server and run the services.

The env variables in the below snippets are provided from Ansible during the process.

Staging:

Sandbox:

Production:

As we see the production compose file is slightly different. The deploy property is used from docker swarm to create the stack with the given replicas and the other configurations like restart policy etc…

Time to dive into Ansible scripts

We will use Ansible to install Docker and configure the servers so we can run the app.

Ansible flow:

SSH into server.

Install Docker.

Install Docker compose.

Copy the directory that contains the docker-compose.yml file into the server.

Initialise the docker services defined in the compose file.

One thing we have to do before starting the whole process is to create 3 EC2 instances in AWS with the same secret key.

We will server side encrypt the key with SSE-C method and store it in S3.

The key will be used by Ansible later to ssh into servers.

Ansible folder structure:

The main scripts or as Ansible calls them — "Playbooks", are deploy.production.yml, deploy.sandbox.yml, and deploy.staging.yml. This playbooks contain plays which define a series of tasks that will be executed in the assigned hosts.

The hosts are defined in the hosts inventory file:

Lets take a look at deploy.staging.yml:

It will target the staging servers, and execute the roles and tasks as are defined. Roles are groups of tasks, organised in separate folders so you can use them in different plays, also it makes the script more readable. The common role for all servers is the Docker and Docker compose installation, so we have made a role for that:

Every time this role is executed Ansible checks the state of the application in the server, if something has changed it will update it according to the state, else it will pass the task and execute the other one.

Other common tasks shared across the plays is the role copy_compose_src

which copies the directory that contains the docker compose file in the server, because that is all that we need to spin up the services.

For staging and sandbox the deployment logic is the same. We have a role deploy_staging_sandbox which pulls the latest images from Docker hub builds up the containers again.

In production the deployment logic defers a little bit, because we will use Docker swarm to manage our running containers, like creating multiple replicas, container restart policies if any of them is down, based on the config that we provide in the compose file.

The deploy.production.yml file is almost the same as staging and sandbox except the last task that is deploy_prod_stack and the initialisation of Docker swarm:

We also pass some other variables that we read from the repository variables like PROXY_IMAGE_NAME which is the name of our NGINX image.

Docker swarm initialisation role:

- name: Init swarm

docker_swarm:

state: present

Deployment role:

We use docker stack to use our compose file and spin up the containers in the swarm.

Wiring all it up with Bitbucket pipelines

So we are in the last step, wiring it all together. With a single push in the respective branches like, develop, sandbox, and master it will trigger a pipeline execution which does the following steps:

Builds the express app image (nginx image also for production) and pushes it in Docker hub.

Gets the decrypted secret key from aws S3 bucket.

from Executes the Ansible play book that manages all the server stuff that we said above.

In the pipeline script below you will see the whole process and commands that are used to execute playbooks etc…

Bonus

I have also created a simple docker image with Ansible installed in it so it can be used directly by our pipeline:

FROM ubuntu RUN apt update RUN apt install software-properties-common --yes

RUN apt-add-repository --yes --update ppa:ansible/ansible

RUN apt install ansible --yes

Hope you enjoyed this article & see you in the next one :D