Photo by Stanley Dai on Unsplash

Managing your infrastructure with Ansible and GitLab CI/CD

Leverage the power of CI/CD to run your playbooks

Tore Sæterdal
Published in
7 min readJul 6, 2020

--

Ansible is a very powerful and versatile tool for configuring servers and deploying applications to across your infrastructure. It is agent-less, using remote connection protocols such as SSH og WinRM to connect to targets.

Almost anyone can set up and configure a Ansible control node in a matter of minutes and get started automating. All you need is a *nix-style server, virtual machine or laptop and a SSH-keypair. But what happens when you need to collaborate with a larger team? How do you manage who runs what, and when, and further keep track of the output of deployments? And importantly with the rise of Infrastructure-as-Code, how to manage the versioning of your Ansible-code?

There are many ways to solve this. Red Hat provides a great solution with Ansible Tower, but for many small teams and organizations, Tower can introduce too much overhead in terms of cost and complexity. A small devops team who manages both development and operations of their own apps may not want or need the added responsibility of managing a Tower installation.

So why not run your playbooks the same way you already collaborate and build your applications, using continuous integration and either continuous delivery or continuous deployment (CI/CD) ? This article provides a suggested approach for managing and running Ansible using Gitlab CI/CD, deploying an example java web app along with Wildfly Application Server on EC2 virtual machines in AWS:

Happy Marriage?

Ansible — power in simplicity

Ansible work in somewhat the same way as shell scripting, you define a set of commands to run against your hosts, but does so in a simple and declarative way. Take the following bash script, which installs and starts the Apache webserver on Fedora flavour Linux hosts:

#!/bin/bash
yum install httpd -y
systemctl enable httpd --now

In Ansible, the above logic looks like this:

- name: Ensure httpd is installed 
package:
name: httpd
state: installed
- name: Ensure httpd service is started and enabled
service:
name: httpd
state: started
enabled: yes

What happens under the hood is that Ansible runs checks to get the state of the package and service, and only installs if the package is absent, or the service is any other state than started. The operation has idempotency, which most or all Ansible modules support natively.

The difference is that with bash, you imperatively describe the steps or commands in order to reach a desired outcome, while with Ansible you declarative describe the desired end state. Ansible hides all the scripting logic underneath a layer of abstraction, allowing users to write simple, human-readable playbooks that do complex operations.

Writing a winning play

For this article, we will set up the WildFly Application server on our hosts and deploy a simple Java webapplication to our environment. Lets begin with the playbook; An Ansible playbook is simply a YAML-file that describes a series of operations, or plays to run and which hosts to run against. A role is a logical structure of tasks and files, used to create reusable blocks of logic. We use a role for setting up Wildfly here:

The playbook ties together tasks and target hosts. Here, the Ansible tasks for setting up JBoss/Wildfly are grouped together in a role within the repo, to simplify readability of playbooks and enable reusability of code. Ansible Galaxy has tons of pre-written roles for configuring infrastructure, see https://galaxy.ansible.com/.

The last task copies the sample app from the repo out to the hosts, to the Wildfly deployments directory. In a real implementation, this would instead pull the artefact from wherever the application build process publishes the binaries, for instance a Maven repository, specified using the application version defined in the variables.

Ansible targets hosts using an inventory, which can represented in YAML like so:

In this example, we have 2 host groups, dev and test, and a single AWS EC2 instance defined in each. Further, both both environments will install the 20.0.0.Final version of Wildfly. Defining version variables like this allows a development team to test applications on different setups and environments before rolling to production. Production has no defined hosts here because lazyness.

Let’s go to pipeline-Town

The foundation of using Gitlab’s CI/CD feature is a simple YAML-file placed in the root of the project repository, .gitlab-ci.yml . This defines the stages of the pipeline, what scripts to run when, and what container-image you want to use for the job executions. For running the playbook and deploying our application along with the the application server in dev and test environments, we can use the following:

The file .gitlab-ci.yml can be adapted to fit with the desired build and deploy workflow for the team. The main parts here are:

  • stages: This is simple enough, that stages are part of the pipeline.
  • image: This tells the Gitlab Runner which docker image to use for running the scripts for the jobs. Here it is specified globally, but it can be set for each job individually. The image being pulled here is a simple Centos 7 base image with Ansible, openssh and pyYAML installed, and pushed to Gitlab Registry. The repo for building the image is here :https://gitlab.com/torese/docker-ansible
  • variables: Environment variables within the scope of the docker image. Here, we add some variables for Ansible such as auto-accepting host ssh certs and visibility.
  • .run_playbook: Again, specified globally as we´d like to use the same script for each job. This script is run in the shell of the docker image. The first block of lines in the script here pulls the private part of a ssh keypair from a Gitlab CI/CD variable and does some trimming to satisfy the ssh-clients, who is a picky eater. The variable SSH_PRIVATE_KEY is set in the project, under Settings -> CI/CD -> Variables. The last command line runs the playbook, specifying the inventory and additional settings.
  • deploy_dev/deploy_test: This is the job declarations, specifying additional variables such as the Ansible host group to target, the inventory. In this case the stage-job relationship is 1–1, but one can add more jobs within a stage, such as end-point testing, registering with monitoring and so on.

The pipeline can be adapted to fit the git workflow of your team, whether its feature-branch or GitFlow or something else.

deploy_production:
extends: .run_playbook
variables:
inventory: inventory
hosts: prod
stage: prod
only: master

This sample job declaration for production hosts uses the only-tag to lock the job for all other branches, meaning a developer can branch out, do stuff and see the changes in dev and test, but not have it run on production hosts until the feature-branch is merged back into master. The customisations available are vast.

Running a play

Now we are ready to deploy the app along with the infrastructure. But how? The pipeline can be triggered from CI/CD console of your Gitlab project, or simply by pushing a new commit. This triggers a new pipeline based on that commit and the stages defined in -gitlab-ci.yml:

Running pipeline

In this snippet, the pipeline has finished running the dev stage with no errors, promoting the pipeline to running the test stage.

In each of the jobs, the Ansible playbook is run on against the hosts:

Playbook run in CI/CD

In Ansible, an operation is only done when the state is not reached. Here, all tasks are changed, which we expected since this is a freshly provisioned EC2 instance. Immediately rerunning the pipeline should give green/ok on all tasks.

Lastly, we see that the Wildfly server is up and running on our environments

Wildfly server in on EC2

Conclusion

Using a layout like this can contribute to devops, where a dev team can manage their own infrastructure using IaC and Ansible, directly in Gitlab. This setup could also be extended using Terraform to provision the hosts prior to running the playbooks, all from within the project.

The repository for this example can be found at: https://gitlab.com/torese/ansible-gitops

Links:

https://docs.gitlab.com/ee/ci/

https://docs.ansible.com/

http://www.mastertheboss.com/howto/jboss-config/provisioning-wildfly-10-with-ansible

--

--