Ansible — Recap¶
What it is¶
Configuration management tool. SSHes into machines and runs tasks — installs packages, runs commands, writes files. You write playbooks in YAML describing what the machine should look like. Ansible makes it so.
Agentless — nothing installed on target machines. Just SSH and Python (Python must exist on the target, which it does on Ubuntu by default).
Idempotent by design — run the same playbook twice, second run changes nothing if everything is already in the correct state.
Where it runs¶
Your local machine, same as Terraform. Not on your VMs, not in your Swarm.
Your laptop
└── ansible-playbook playbook.yml
└── SSHes into manager → runs tasks
└── SSHes into worker → runs tasks
Installation¶
pip install ansible
# verify
ansible --version
Where it fits relative to Terraform¶
Terraform → provisions VMs, networking, Azure services, SP
outputs IPs and SP credentials
stops here
Ansible → reads Terraform outputs
SSHes into VMs
installs Docker, inits Swarm, joins worker, loads Docker secrets
stops here
GitHub Actions → docker stack deploy
Private key lives only in Ansible. Terraform never needs it.
Project structure¶
ansible/
inventory.yml # which machines, how to connect
playbook.yml # what to do on them
vars/
secrets.yml # sensitive values — can be encrypted with ansible-vault
Inventory¶
Tells Ansible which machines exist and how to connect to them. IPs come from Terraform outputs.
# inventory.yml
all:
children:
manager:
hosts:
aver-manager:
ansible_host: "{{ manager_ip }}"
ansible_user: azureuser
ansible_ssh_private_key_file: ~/.ssh/id_rsa
worker:
hosts:
aver-worker:
ansible_host: "{{ worker_ip }}"
ansible_user: azureuser
ansible_ssh_private_key_file: ~/.ssh/id_rsa
Playbook¶
Describes what to do on each group of machines. Tasks run in order, top to bottom.
# playbook.yml
# ── Manager ──────────────────────────────────────────────
- hosts: manager
become: true # sudo
tasks:
- name: Install Docker
apt:
name: docker.io
state: present
update_cache: yes
- name: Install Git
apt:
name: git
state: present
- name: Init Swarm
shell: >
docker swarm init
--advertise-addr {{ ansible_host }}
--default-addr-pool 10.20.0.0/16
--default-addr-pool-mask-length 24
register: swarm_init
ignore_errors: true # idempotent — won't fail if already initialised
- name: Get worker join token
shell: docker swarm join-token worker -q
register: join_token
- name: Load azure_tenant_id as Docker secret
shell: echo "{{ sp_tenant_id }}" | docker secret create azure_tenant_id -
ignore_errors: true # idempotent — won't fail if secret already exists
- name: Load azure_client_id as Docker secret
shell: echo "{{ sp_client_id }}" | docker secret create azure_client_id -
ignore_errors: true
- name: Load azure_client_secret as Docker secret
shell: echo "{{ sp_client_secret }}" | docker secret create azure_client_secret -
ignore_errors: true
# ── Worker ───────────────────────────────────────────────
- hosts: worker
become: true
tasks:
- name: Install Docker
apt:
name: docker.io
state: present
update_cache: yes
- name: Join Swarm
shell: >
docker swarm join
--token {{ hostvars['aver-manager']['join_token'].stdout }}
{{ hostvars['aver-manager']['ansible_host'] }}:2377
ignore_errors: true # idempotent — won't fail if already joined
Passing Terraform outputs into Ansible¶
# read from Terraform
MANAGER_IP=$(terraform output -raw manager_public_ip)
WORKER_IP=$(terraform output -raw worker_public_ip)
SP_TENANT=$(terraform output -raw sp_tenant_id)
SP_CLIENT=$(terraform output -raw sp_client_id)
SP_SECRET=$(terraform output -raw sp_client_secret)
# run Ansible with those values
ansible-playbook playbook.yml \
-i inventory.yml \
--extra-vars "manager_ip=$MANAGER_IP \
worker_ip=$WORKER_IP \
sp_tenant_id=$SP_TENANT \
sp_client_id=$SP_CLIENT \
sp_client_secret=$SP_SECRET"
Running it¶
ansible-playbook playbook.yml -i inventory.yml
You watch tasks execute in the terminal. Green = success, yellow = changed, red = failed.
Key concepts¶
become: true — run as sudo on the remote machine
register — capture the output of a task into a variable for use in later tasks. Used to capture the Swarm join token from the manager and pass it to the worker.
hostvars — access variables from a different host in the inventory. How the worker task reads the join token that was registered on the manager.
ignore_errors: true — don't fail the playbook if this task fails. Used for idempotency on tasks that error if already done (Swarm already init, secret already exists).
ansible-vault — encrypts sensitive files at rest in the repo. You'd encrypt vars/secrets.yml so SP credentials aren't plaintext in Git.
The full bootstrapping sequence¶
# 1. provision infrastructure
terraform apply
# 2. capture outputs and configure cluster
MANAGER_IP=$(terraform output -raw manager_public_ip)
WORKER_IP=$(terraform output -raw worker_public_ip)
SP_TENANT=$(terraform output -raw sp_tenant_id)
SP_CLIENT=$(terraform output -raw sp_client_id)
SP_SECRET=$(terraform output -raw sp_client_secret)
ansible-playbook playbook.yml \
-i inventory.yml \
--extra-vars "manager_ip=$MANAGER_IP worker_ip=$WORKER_IP \
sp_tenant_id=$SP_TENANT sp_client_id=$SP_CLIENT \
sp_client_secret=$SP_SECRET"
# 3. deploy the stack (manual first time, CI after)
docker stack deploy -c stack.yml aver
Full runnable code — complete Ansible setup¶
inventory.yml
all:
children:
manager:
hosts:
aver-manager:
ansible_host: "{{ manager_ip }}"
ansible_user: azureuser
ansible_ssh_private_key_file: ~/.ssh/id_rsa
worker:
hosts:
aver-worker:
ansible_host: "{{ worker_ip }}"
ansible_user: azureuser
ansible_ssh_private_key_file: ~/.ssh/id_rsa
playbook.yml
# ── Manager ──────────────────────────────────────────────
- hosts: manager
become: true
tasks:
- name: Update apt cache
apt:
update_cache: yes
- name: Install Docker
apt:
name: docker.io
state: present
- name: Install Git
apt:
name: git
state: present
- name: Add azureuser to docker group
user:
name: azureuser
groups: docker
append: yes
- name: Init Swarm
shell: >
docker swarm init
--advertise-addr {{ ansible_host }}
--default-addr-pool 10.20.0.0/16
--default-addr-pool-mask-length 24
register: swarm_init
ignore_errors: true
- name: Get worker join token
shell: docker swarm join-token worker -q
register: join_token
- name: Load azure_tenant_id as Docker secret
shell: |
echo "{{ sp_tenant_id }}" | docker secret create azure_tenant_id - 2>/dev/null || true
- name: Load azure_client_id as Docker secret
shell: |
echo "{{ sp_client_id }}" | docker secret create azure_client_id - 2>/dev/null || true
- name: Load azure_client_secret as Docker secret
shell: |
echo "{{ sp_client_secret }}" | docker secret create azure_client_secret - 2>/dev/null || true
# ── Worker ───────────────────────────────────────────────
- hosts: worker
become: true
tasks:
- name: Update apt cache
apt:
update_cache: yes
- name: Install Docker
apt:
name: docker.io
state: present
- name: Add azureuser to docker group
user:
name: azureuser
groups: docker
append: yes
- name: Join Swarm
shell: >
docker swarm join
--token {{ hostvars['aver-manager']['join_token'].stdout }}
{{ hostvars['aver-manager']['ansible_host'] }}:2377
ignore_errors: true
bootstrap.sh — ties Terraform outputs into Ansible
#!/bin/bash
set -e
echo "Reading Terraform outputs..."
MANAGER_IP=$(terraform output -raw manager_public_ip)
WORKER_IP=$(terraform output -raw worker_public_ip)
SP_TENANT=$(terraform output -raw sp_tenant_id)
SP_CLIENT=$(terraform output -raw sp_client_id)
SP_SECRET=$(terraform output -raw sp_client_secret)
echo "Manager IP: $MANAGER_IP"
echo "Worker IP: $WORKER_IP"
echo "Waiting for VMs to be ready..."
sleep 30
echo "Running Ansible..."
ansible-playbook playbook.yml \
-i inventory.yml \
--extra-vars "manager_ip=$MANAGER_IP \
worker_ip=$WORKER_IP \
sp_tenant_id=$SP_TENANT \
sp_client_id=$SP_CLIENT \
sp_client_secret=$SP_SECRET"
echo "Done. Cluster is ready."
echo "SSH into manager: ssh azureuser@$MANAGER_IP"
Full run from scratch
# 1. provision infrastructure
cd terraform-repo
az login
terraform init
terraform apply
# 2. configure cluster
cd ../ansible
chmod +x bootstrap.sh
./bootstrap.sh
# 3. deploy stack (first time manual, after this CI handles it)
ssh azureuser@<manager_ip>
docker stack deploy -c /path/to/stack.yml aver --with-registry-auth
Interview one liner¶
"I use Ansible for post-provisioning configuration — Docker install, Swarm init, joining worker nodes, and loading Azure Service Principal credentials as Docker secrets. It reads IPs and credentials directly from Terraform outputs. Agentless, idempotent, clean handoff from Terraform."