Ansible is an open-source software provisioning, configuration management, and application deployment tool. It uses declarative language to describe system configuration, making it easy to use.

Ansible uses a simple syntax written in YAML called playbooks. It is agentless, meaning it doesn't require any software installed on remote nodes to perform its tasks. Instead, it uses SSH to execute commands and copy resources to remote machines. Ansible is also idempotent, ensuring that the same playbook can be run multiple times without changing the final state beyond the first run. It's widely used for IT automation tasks.

Preparing the Ansible Dev Environment

Before getting started, we need to install Ansible and its dependencies on our server. As Ansible is based on Python, the latest versions are available on the PIP package repository. Note that the package repository of our host system may not have an up-to-date package, so pip is the recommended platform for getting the latest versions.

To install Ansible, simply run,

pip install ansible

If you don't have pip in your system, then simply run the following:

Debian/Ubuntu

apt install python3-pip

CentOs/RHEL

yum install epel-release
yum install python3-pip

Note: Use sudo for package installation if the install fails with a lack of privilege/permission errors.

Windows Systems

# Download the get-pip.py script
Invoke-WebRequest -Uri https://bootstrap.pypa.io/get-pip.py -OutFile get-pip.py

# Install pip
python3 get-pip.py

Provisioning the Server

We will now install Docker and Start up a Docker Stack with Ansible.

ℹ️
To ensure we are on the same page, I'll be using a CentOS 8 Stream based VM as my target server; please do the same to keep things consistent. The playbook I provided has been written accordingly!

Place the configuration files provided below in the same directory. Name them according to their codeblock captions.

All the required configuration is down in the Jinja Template for Ansible. This makes it easy to generate the playbook with the necessary variables placed into it in its final form. The passwords can be encrypted using ansible-vault which is the recommended way to do so.

---
- hosts: all
  become: yes
  vars:
    docker_users:
      - "{{ ansible_user }}"

  tasks:
    - name: Install required packages
      dnf:
        name:
          - dnf-utils
          - device-mapper-persistent-data
          - lvm2
          - python3-pip
        state: latest
        update_cache: true

    - name: Install Docker SDK for Python
      pip:
        name: 
          - docker
        state: present

    - name: Add Docker repository
      get_url:
        url: https://download.docker.com/linux/centos/docker-ce.repo
        dest: /etc/yum.repos.d/docker-ce.repo

    - name: Install Docker
      dnf:
        name: 
          - docker-ce
        state: latest

    - name: Install Docker Compose
      get_url:
        url: https://github.com/docker/compose/releases/download/v2.27.0/docker-compose-linux-x86_64
        dest: /usr/local/bin/docker-compose
        mode: 'u+x,g+x'

    - name: Add users to docker group
      user:
        name: "{{ ansible_user }}"
        groups: docker
        append: yes
      with_items: "{{ docker_users }}"

    - name: Start Docker service
      systemd:
        name: docker
        state: started
        enabled: yes

    - name: Init Swarm
      community.docker.docker_swarm:
        state: present
        advertise_addr: "{{ ansible_default_ipv4.address }}"
    
    - name: Login to Docker Registry
      docker_login:
        username: "{{ docker_username }}"
        password: "{{ docker_password }}"
        registry_url: "{{ docker_registry_url }}"
        reauthorize: yes

    - name: Transfer Docker Compose file to remote host
      copy:
        src: docker-compose.yaml
        dest: /tmp/docker-compose.yaml
    
    - name: Deploy Swarm
      community.docker.docker_stack:
        state: present
        name: mystack
        compose: /tmp/docker-compose.yaml

playbook.yaml.j2

This generate_playbook.yaml file is used to generate the main playbook from the playbook.yaml.j2 template file above.

---
- name: Generate Playbook
  hosts: all
  connection: local
  gather_facts: true

  tasks:
    - name: Include Variables
      include_vars:
        file: variables.yaml
      
    - name: Include Docker Credentials
      include_vars:
        file: docker_credentials.txt
    
    - name: Generate playbook
      template:
        src: playbook.yaml.j2
        dest: playbook.yaml
      run_once: true

generate_playbook.yaml

This file contains credentials to the docker registry. Although it's not required for public packages in dockerhub, it has been used here to encourage the use of a private registry for hosting docker images.

docker_username: asd
docker_password: asd

docker-credentials.txt

Run ansible-vault encrypt docker_credentials.txt, you will be asked to enter a password which will be used to encrypt the password file. Once completed the username/passwords will be replaced with the encrypted form of the same.

This is the main docker-compose file, which will be used to create the docker stack containing the nginx web service.

version: '3.9'
services:
  web:
    image: nginx:22-alpine
    ports:
      - "80:80"
    networks:
      - nginx

networks:
  nginx:
    name: nginx

docker-compose.yaml

The hosts.ini file consists of the server host IP address and username with which to access the server.

[all]
192.168.2.22 ansible_user=root

hosts.ini

The variables.yaml file is used to offload all values that require frequent changes

docker_registry_url: my-private.registry.example.com
ansible_user: root
docker_users:
  - "{{ ansible_user }}"

variables.yaml

The password used to encrypt the vault is saved as vault_password.txt. Instead of providing the password on every run, we can point Ansible to this file to ensure faster execution of playbooks.

asd-asd

vault_password.txt

Once all the files are available and placed in the directory, we can run the following command to set up our server and start the docker swarm stack.

ansible-playbook --vault-password-file vault_password.txt generate_playbook.yaml -i hosts.ini

This will generate the playbook file according to the Jinja template and the provided variables. Remember that the provided passwords will also be available in the playbook file in cleartext. Don't commit this file to version control.

ansible-playbook playbook.yaml -i hosts.ini

Finally, now the server will be provisioned according to the instructions we provided in the playbook.

If you SSH into the server, you should see that docker has been installed, the nginx container has been deployed, and the necessary packages, like pip, etc, are also available in the server.

Conclusion

In this way we learned about Ansible, provisioned a server with Ansible and deployed a Docker Swarm Stack with Ansible.

Thank you for reading, I periodically try to update my articles to ensure legibility.