Ansible: Handling Multiple Hosts via SSH

PROBLEM

To run Ansible playbook in multiple hosts via SSH.

SOLUTION

Configuring SSH environment

Ensure SSH keypair exists on the current machine (ex: ~/.ssh/id_rsa for private key and ~/.ssh/id_rsa.pub for public key). If you do not have one, create one:

ssh-keygen

Copy the public key (ex: ~/.ssh/id_rsa.pub) to each remote host’s ~/.ssh/authorized_keys. If this file doesn’t exist, create it.

Ensure the current machine’s .ssh/ directory and file have correct permission.

chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys

In the current machine’s /etc/hosts, add all remote hosts.

127.0.0.1      localhost  # current machine
192.168.1.100  donkeykong # remote host 1
192.168.1.200  supermario # remote host 2

In each remote host, enable the remote login and grant yourself the access to this service.

Enabling Remote Login on Mac

Test SSH connection to remote host to ensure they work first before working on Ansible playbook.

ssh user@donkeykong
ssh user@supermario

Creating Ansible Playbook

Create ansible.cfg and define the location of inventory file.

[defaults]
inventory = inventory.yml

Create inventory.yml and define both localhost and remote hosts.

all:
  hosts:
    localhost:
      ansible_connection: local
    donkeykong:
      ansible_user: user
      ansible_ssh_private_key_file: ~/.ssh/id_rsa
    supermario:
      ansible_user: user
      ansible_ssh_private_key_file: ~/.ssh/id_rsa

Run a test to ensure the connection to remote hosts are successful.

ansible all -i inventory.yml -m ping

If successful, the output looks something like this:

localhost | SUCCESS => {
    "changed": false,
    "ping": "pong"
}
donkeykong | SUCCESS => {
    "changed": false,
    "ping": "pong"
}
supermario | SUCCESS => {
    "changed": false,
    "ping": "pong"
}

Create main.yml with a very simple task.

- name: all-hosts
  hosts: all
  tasks:
    - name: Capture current dir
      shell: pwd
      register: output

    - name: Display output
      debug: msg='{{ output.stdout }}'

Run the playbook.

ansible-playbook main.yml

If successful, the output looks something like this:

PLAY [all-hosts] *******************************************************************************************************

TASK [Gathering Facts] *************************************************************************************************
ok: [localhost]
ok: [donkeykong]
ok: [supermario]

TASK [Capture current dir] *********************************************************************************************
changed: [localhost]
changed: [donkeykong]
changed: [supermario]

TASK [Display output] **************************************************************************************************
ok: [localhost] => {
    "msg": "/Users/user/myshittycode"
}
ok: [donkeykong] => {
    "msg": "/Users/user"
}
ok: [supermario] => {
    "msg": "/Users/user"
}

PLAY RECAP *************************************************************************************************************
donkeykong                 : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
localhost                  : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0  
supermario                 : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

Controlling the Hosts

Sometimes, you want finer controls on what tasks to be ran in certain hosts.

To run in just one host (ex: donkeykong):

- name: one-host
  hosts: donkeykong
  tasks:
    - ...

To run in all remote hosts except localhost:

- name: all-hosts-except-localhost
  hosts: all:!localhost
  tasks:
    - ...

Docker: Executing Startup Script When Running Container Interactively

PROBLEM

When running the Docker container interactively (ex: docker run –rm -it myimage), you want to run a startup script every time.

SOLUTION

For Ubuntu, Debian and Centos images, write the startup script to /root/.bashrc:

# UBUNTU
FROM ubuntu:latest
RUN echo "echo 'Welcome!'" >> /root/.bashrc
WORKDIR /home

# DEBIAN
FROM debian:latest
RUN echo "echo 'Welcome!'" >> /root/.bashrc
WORKDIR /home

# CENTOS
FROM centos:latest
RUN echo "echo 'Welcome!'" >> /root/.bashrc
WORKDIR /home

For Alpine image, it’s a little different because it uses Ash shell. Besides writing the startup script to /root/.profile, you also need to set that path to an environment variable called ENV:

FROM alpine:latest
ENV ENV=/root/.profile
RUN echo "echo 'Welcome!'" > $ENV
WORKDIR /home

Git: Querying Tags Without Cloning the Repository

PROBLEM

A typical way to get a list of tags from a repository is to clone it before running git tag:-

git clone git@ssh.dev.azure.com:v3/test/my-shitty-repo
cd my-shitty-repo
git -c 'versionsort.suffix=-' tag --sort='v:refname'

# output
1.0.0-b20200317174203
1.0.0
1.0.1-b20200318174753
1.0.1-b20200318174841
1.0.1-b20200407185909
1.0.1
1.0.2-b20200413205910
1.0.2

versionsort.suffix=- ensures 1.0.0-XXXXXX comes after 1.0.0.

To retrieve the latest tag:-

git clone git@ssh.dev.azure.com:v3/test/my-shitty-repo
cd my-shitty-repo
git -c 'versionsort.suffix=-' tag --sort='v:refname' |
tail -n1

# output
1.0.2

While it works, it requires us to clone the repository first, and if we want to retrieve tags from multiple repositories, we are quickly filling our hard drive space.

SOLUTION

Git has a way to perform a remote query through git ls-remote.

To perform the same task without cloning the repository, we can run this:-

git -c 'versionsort.suffix=-' ls-remote \
--tags \
--sort='v:refname' \
git@ssh.dev.azure.com:v3/test/my-shitty-repo

# output
b90df3d12413db22d051db1f7c7286cdd2f00b66	refs/tags/1.0.0-b20200317174203
e355a58829a2d2895ab2d7610fad1ab26dc0c1e7	refs/tags/1.0.0
345153c39a588a6ab2782772ee9dcf9f9123efa9	refs/tags/1.0.1-b20200318174753
efc40f0bd68bb8c7920be7700cab81db0e6bdf83	refs/tags/1.0.1-b20200318174841
efc40f0bd68bb8c7920be7700cab81db0e6bdf83	refs/tags/1.0.1-b20200407185909
c5ed5fe30cba621f40daa542c0613fa2c1c1a47d	refs/tags/1.0.1
7205ada5d8bd4318f82e58e8752ba651211f9d82	refs/tags/1.0.2-b20200413205910
6ba62a0f06f831812cbb13a6d1e83602ffe9e8d3	refs/tags/1.0.2

To retrieve the latest tag:-

git -c 'versionsort.suffix=-' ls-remote \
--tags \
--sort='v:refname' \
git@ssh.dev.azure.com:v3/test/my-shitty-repo |
tail -n1 |
sed -E 's|.*refs/tags/(.+)|\1|'

# output
1.0.2