Skip to content

Ansible - Technology Guide

This guide explains what Ansible is, how it works, and how it is used to automate node setup and application deployment in this homelab. No prior Ansible experience required.


What is Ansible?

Ansible is an open-source configuration management and automation tool. It allows you to define what you want a server to look like in simple YAML files (called playbooks), and Ansible will make it so.

What makes Ansible different from other tools:

  • Agentless - no software needs to be installed on the target servers
  • Uses SSH - connects via standard SSH (or Tailscale SSH in this homelab)
  • Idempotent - running the same playbook multiple times has the same result (safe to re-run)
  • Declarative - you describe the desired state, not the steps to get there

Without Ansible:

# Manually SSH to every node and run:
ssh ubuntu@k3s-server.tailnet.ts.net
curl -sfL https://get.k3s.io | sh -  # What flags? I forgot...
# Then repeat for k3s-agent-1, k3s-agent-2
# What if one node is already configured and the other isn't?

With Ansible:

# One command, handles everything, safe to re-run:
ansible-playbook playbooks/deploy_k3s.yml

References:


Key Concepts

Inventory

An inventory is a file that lists the servers Ansible will manage. For this homelab, GitHub Actions workflows generate the inventory dynamically from workflow inputs:

# Example inventory generated by the k3s workflow
all:
  children:
    k3s:
      hosts:
        k3s-server:
          ansible_host: k3s-server.tailnet.ts.net
          ansible_user: ubuntu
          ansible_ssh_common_args: "-o StrictHostKeyChecking=yes"
      vars:
        ansible_python_interpreter: /usr/bin/python3

Playbook

A playbook is a YAML file that describes automation tasks. Each playbook:

  • Targets specific hosts (from the inventory)
  • Runs a list of tasks in order
  • Can include variables, conditionals, loops, and error handling
# Example structure of a playbook
- name: Deploy k3s server
  hosts: k3s # Which hosts to target
  become: true # Run as root (sudo)

  pre_tasks:
    - name: Update apt cache
      ansible.builtin.apt:
        update_cache: true
        cache_valid_time: 3600 # Don't update if done in the last hour

  tasks:
    - name: Install k3s
      ansible.builtin.shell:
        cmd: "curl -sfL https://get.k3s.io | sh -s - server"

Module

An Ansible module is a reusable unit of automation. Modules handle specific tasks:

Module What It Does
ansible.builtin.apt Install/remove packages on Debian/Ubuntu
ansible.builtin.shell Run shell commands
ansible.builtin.copy Copy files to servers
ansible.builtin.template Copy Jinja2 template files
ansible.builtin.service Start/stop/enable systemd services
ansible.builtin.file Create/delete files and directories
ansible.builtin.get_url Download files from URLs
ansible.builtin.assert Validate conditions (fail with message if not met)

FQCN (Fully Qualified Collection Name)

This homelab uses the full module names like ansible.builtin.apt instead of just apt. This is best practice as it avoids ambiguity and works correctly with all Ansible versions.

Idempotency

An idempotent operation produces the same result whether run once or many times. Most Ansible modules are idempotent:

  • ansible.builtin.apt: name=curl state=present - installs curl if not installed, does nothing if already installed
  • ansible.builtin.service: name=k3s state=started - starts k3s if not running, does nothing if already running

Ansible Playbooks in This Homelab

All playbooks are in ansible/playbooks/.


Running Ansible Manually

While GitHub Actions is the recommended path, you can run playbooks manually:

# Install Ansible
pip install ansible

# Ensure you're on the tailnet
tailscale up

# Create a minimal inventory file
cat > /tmp/inventory.yml <<EOF
all:
  children:
    k3s:
      hosts:
        k3s-server:
          ansible_host: k3s-server.tailnet.ts.net
          ansible_user: ubuntu
EOF

# Dry-run (check mode - no changes made)
ansible-playbook playbooks/deploy_k3s.yml \
  -i /tmp/inventory.yml \
  --check

# Actually run it
ansible-playbook playbooks/deploy_k3s.yml \
  -i /tmp/inventory.yml

# Run with increased verbosity for debugging
ansible-playbook playbooks/deploy_k3s.yml \
  -i /tmp/inventory.yml \
  -v    # -v, -vv, or -vvv for increasing detail

# Run only specific tasks (by tags, if defined)
ansible-playbook playbooks/deploy_k3s.yml \
  -i /tmp/inventory.yml \
  --tags "install"

Common Troubleshooting

SSH connection refused

# Check Tailscale is running on the target node
tailscale ping k3s-server

# Check if Tailscale SSH is enabled
tailscale status | grep k3s-server

Task fails with "Permission denied"

The task needs become: true (sudo). Check if the playbook or task has become: true.

Playbook fails but the server is in a weird state

Most Ansible modules are idempotent - you can re-run the playbook and it will pick up where it left off without breaking things that already succeeded.

"No module named 'ansible'"

Ansible is not installed:

pip install ansible
# or
pip3 install ansible

Known hosts verification fails

If a VM was recreated (new host key), remove the old entry:

ssh-keygen -R k3s-server.tailnet.ts.net

Or repopulate the host key with ssh-keyscan:

ssh-keyscan k3s-server.tailnet.ts.net >> ~/.ssh/known_hosts