
Ansible, GitHub Workflows, and Docker: Automating Deployment Pipelines
Ansible, GitHub Workflows, and Docker: Automating Deployment Pipelines
I have been working on a project with a frontend, backend and database integration for work and I thought I would share how I set up the CI/CD integration from my machine, to GitHub to a virtual machine running Docker.
Docker
I have both my frontend and backend running on Docker. What is important from this step however is the port that you expose from your container. So whatever technology you're using, take note of the port.
EXPOSE 3030
Ansible
For Ansible, you need to create a playbook that you will use to run your container on the virtual host. Here is the basic structure:
- name: My app name
hosts: all #set your host from your inventory file
become: yes
vars:
image: "{{ lookup('env', 'IMAGE') }}"
container_name: "Loapi"
ghcr_username: "{{ lookup('env', 'GHCR_USERNAME' )}}"
ghcr_token: "{{ lookup('env', 'GHCR_TOKEN' )}}"
repository_name: "{{ lookup('env', 'REPOSITORY_NAME') }}"
tasks:
- name: Login to GHCR
community.docker.docker_login:
username: "{{ ghcr_username }}"
password: "{{ ghcr_token }}"
registry_url: ghcr.io
- name: Check if container exists
community.docker.docker_container_info:
name: "{{ container_name }}"
register: container_info
- name: Stop existing Docker Container
community.docker.docker_container:
name: "{{ container_name }}"
state: absent
when: container_info.exists and container_info.container.State.Running
- name: Remove old Docker Image
ansible.builtin.shell: |
docker images "{{ repository_name }}" -q | xargs -r docker rmi
ignore_errors: yes
- name: Pull the latest version of the image
community.docker.docker_image:
name: "{{ image }}"
source: pull
- name: Start the new Docker container
community.docker.docker_container:
name: api
image: "{{ image }}"
state: started
ports:
- "3030:3030"
env:
DATABASE_CONNECTION: "{{ lookup('env', 'DATABASE_CONNECTION') }}"
Here is a breakdown of the following playbook:
Login to GHCR
I am using GitHub registry to store my image, so at this step I use Docker to do the login, note that I am using the "community.docker" Ansible plugin, I will show you how to set this up in your workflow later.
Check if container exists
Because you may be pushing a new container with updated code, you may want to stop and remove the previous version. At this step I check whether there is a container of the specified "container_name" currently running and I save whether this is true or false in the "container_info" variable.
Stop and Remove the Docker Container
Depending on whether the "container_info" variable is true, I will set the state of any container with the name in "container_name" to absent, effectively stopping and removing it from the machine.
Remove Old Docker Image
This step is not absolutely necessary but it will help you save space on your host, because the images may be pretty large and they'll just keep on piling up in your host with every deployment. So it's really just to keep things efficient.
There is the "repository_name" variable which I use to find any image in the host with that particular repository name and remove it. You need to add the "ignore_errors: true" since if there is no image by that name, it will throw an error.
Pull the latest version of the image
This step will download the latest version of the image in your registry. For me, I am using the git SHA to tag my images so I pass in the repository name and the tag at once through the "image" variable.
Start the new Docker Container
Using the "image" variable, this step runs the container. Here is where the port you exposed in your application comes in handy because you are pointing one of your host's ports to that port that you specified in the Dockerfile. You can also use this point to pass in any application environment variable you may need.
After setting this up, you can place it somewhere in your repository, take note of the path where you store it since you'll be referencing it in your workflow.
GitHub Workflows
At this point you need to create your Docker image using GitHub workflow, push it to your registry of choice, set up Ansible and run the playbook.
Here is my workflow:
name: My Workflow Workflow
on:
push:
branches: [ main ]
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Check out Repository
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to the GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and Push Docker Image
uses: docker/build-push-action@v5
with:
file: ./location/of/my/Dockerfile
push: true
tags: ghcr.io/{org-name}/{repo-name}/{app-name}:${{ github.sha }}
deploy:
runs-on: ubuntu-latest
needs: build-and-push
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.12.1'
- name: Install Ansible and Docker modules
run: |
pip install ansible
ansible-galaxy collection install community.docker
- name: Create inventory from secret
run: echo "${{ secrets.HOST }}" > inventory
- name: Setup SSH Key
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/key.pem
chmod 600 ~/.ssh/key.pem
ssh-keyscan -H ${{ secrets.HOST }} >> ~/.ssh/known_hosts
echo "Host *\n IdentityFile ~/.ssh/key.pem" > ~/.ssh/config
- name: Run Ansible Playbook
run: ansible-playbook -vvv -i inventory ./path/to/my/playbook.yml --private-key ~/.ssh/key.pem -u ubuntu
env:
IMAGE: ghcr.io/{org-name}/{repo-name}/{app-name}:${{ github.sha }}
HOST: ${{ secrets.HOST }}
GHCR_USERNAME: ${{ secrets.GHCR_USERNAME }}
GHCR_TOKEN: ${{ secrets.GHCR_TOKEN }}
REPOSITORY_NAME: ghcr.io/{org-name}/{repo-name}/{app-name}
There are two jobs for my workflow:
- Build and push the image
- Deploy the image
Build and Push the Image
I have used buildx to build the Docker image as shown in step 2 & 3. Next would be logging into GHCR, which you can easily do using the github.actor as your username and secrets.GITHUB_TOKEN as your password.
Next you can see my push step where I just specify where my Dockerfile is. Also note that this is where I specify my repository_name. You can set this up any way you like but the path-like structure helps me keep things organized and the GitHub SHA is just best practice when using Docker and workflows.
Deploy the Image
This step needs the build and push one to complete successfully. First, it sets up Python because Ansible runs on Python.
Next, we set up Ansible by downloading it from pip. Take note of the step that downloads the "community.docker" plugin from Ansible Galaxy. This is integral and without it the playbook won't be able to work with Docker.
Next I use a GitHub secret to generate an inventory file, which only contains the IP address of my host. You can pass this in from a file in your repository but I find this more secure and dynamic.
The next step is to set up SSH for my workflow runner so that it can connect to my host. This involves creating a key.pem file with the key needed to connect to my host which I have in my GitHub secrets.
The next step is to run the playbook. This will also involve passing in the inventory file we just created, and any environment variable that our playbook needs.
Conclusion
That's it! There's a lot more you can do with these tools, for example:
- You can set up a workflow that runs periodically and uses Ansible to check on whether your Docker application is still running.
- You can set up a workflow that can be triggered manually to create and set up your host with all the prerequisites of this application (i.e., Docker, Nginx for the server, certbot for SSL). The same workflow can set the host variables where you can use them dynamically in the workflow that deploys the application.
Thanks for reading! I hope this helps you get started easily with these tools. Feel free to reach out with any questions or suggestions. How are you using these tools?