Attribution

This Ansible 101 guide is based on our notes from the Getting started with Ansible video course by Linux TV.

We highly recommend this course as a great starting point for learning Ansible; it was instrumental in our understanding of the subject.

Overview

Ansible is an open-source automation tool that simplifies configuration management, application deployment, and task automation. This guide walks through setting up Ansible from scratch, starting with basic SSH configuration and progressing to advanced concepts like roles and templates.

Architecture Basics

  • Control Host: One machine with Ansible installed where we run our playbooks
  • Managed Nodes: Multiple servers that Ansible controls (no Ansible installation required on these)

SSH Setup and Configuration

Ansible communicates with managed nodes over SSH. Here’s how to set up secure, key-based authentication.

Step 1: Install OpenSSH

On the control host, ensure the OpenSSH client is installed (usually pre-installed on Linux). On each managed server, install the OpenSSH server:

# On the managed servers
sudo apt install openssh-server

Step 2: Accept Initial Host Keys

Connect to each server initially to accept their host keys:

# On the control host
ssh <username>@<server-ip>

Step 3: Create SSH Key Pair for Interactive Use

Generate a key pair with a passphrase for your normal user account:

# On the control host
# -t: type; -C: comment
ssh-keygen -t ed25519 -C "your comment here"

# You'll be prompted for:
# - File location (default: /home/<user>/.ssh/id_ed25519)
# - Passphrase (recommended for security)

Step 4: Distribute Your Public Key

Copy the public key to each managed server:

# On the control host
# -i: input file
ssh-copy-id -i ~/.ssh/id_ed25519.pub <server-ip>

Step 5: Create an Automation Key

Create a dedicated key for Ansible automation without a passphrase:

# On the control host
ssh-keygen -t ed25519 -C "ansible"

# Save to: /home/<username>/.ssh/ansible
# Leave passphrase empty (press Enter)

Step 6: Distribute the Automation Key

Copy the Ansible-specific key to all servers:

# On the control host
ssh-copy-id -i ~/.ssh/ansible.pub <server-ip>

# You'll be prompted for the password of the target user account

Step 7: Verify Key-Based Authentication

Test that the Ansible key works without requiring a passphrase:

# On the control host
ssh -i ~/.ssh/ansible <server-ip>

Step 8: Cache Your Interactive Key’s Passphrase

Use ssh-agent to cache your personal key’s passphrase for the current session:

# Start ssh-agent
eval $(ssh-agent)

# Verify it's running
ps aux | grep ssh-agent

# Add your key (you'll enter your passphrase once)
ssh-add

# Note: This is only cached for your current terminal session

Step 9: Create a Convenience Alias (Optional)

Add an alias to simplify starting ssh-agent:

# Add to ~/.bashrc for persistence
alias ssha='eval $(ssh-agent) && ssh-add'

# Reload your shell configuration
source ~/.bashrc

# Verify the alias
alias ssha

Setting Up Version Control

Tracking your Ansible configurations in Git provides version control and collaboration capabilities.

Install Git

# On the control host
sudo apt update
sudo apt install git

Configure Git

Set up your identity and create a repository:

  1. Create a GitHub account (if you don’t have one)
  2. Create a new repository for your Ansible project
  3. Add your SSH public key to your GitHub account
  4. Clone the repository to your control host:
# On the control host
git clone [email protected]:<username>/<repository>.git

Configure your local Git identity:

git config --global user.name "<your-name>"
git config --global user.email "<your-email>"

# Verify configuration
cat ~/.gitconfig

Make Your First Commit

# Check repository status
git status

# Review changes
git diff README.md

# Stage and commit
git add README.md
git commit -m "Initial commit: Add README"

# Push to remote
git push origin main

Running Ad-Hoc Commands

Ad-hoc commands let you execute simple tasks across your infrastructure without writing full playbooks.

Install Ansible

sudo apt update
sudo apt install ansible

Create an Inventory File

The inventory file lists your managed nodes:

# Navigate to your project directory
cd <github-ansible-project-path>

# Create inventory file
touch inventory

Example inventory (list IPs or hostnames):

192.168.1.1
192.168.1.2
192.168.1.3

Commit your inventory:

git add inventory
git commit -m "Add initial inventory with three hosts"
git push origin main

Test Connectivity

Verify Ansible can reach your hosts:

# -i: inventory file; -m: module
ansible all --key-file ~/.ssh/ansible -i inventory -m ping

Create an Ansible Configuration File

Automate common options with ansible.cfg:

# Create config file
touch ansible.cfg

Example configuration:

[defaults]
inventory = inventory
private_key_file = ~/.ssh/ansible

Now you can run commands without specifying the inventory and key file:

ansible all -m ping

Useful Ad-Hoc Commands

List configured hosts:

ansible all --list-hosts

Gather system facts from all hosts:

ansible all -m gather_facts

Gather facts from specific hosts:

ansible all -m gather_facts --limit <server-ip>

Running Elevated Commands

Many administrative tasks require elevated privileges. Ansible uses privilege escalation (sudo by default) for these operations.

Update Package Cache (Will Fail Without Privileges)

# -a: arguments
ansible all -m apt -a update_cache=true

Update Cache With Privilege Escalation

# --become: use sudo; --ask-become-pass: prompt for password
ansible all -m apt -a update_cache=true --become --ask-become-pass

Install Packages

Install multiple packages:

# Install vim-nox
ansible all -m apt -a name=vim-nox --become --ask-become-pass

# Install tmux
ansible all -m apt -a name=tmux --become --ask-become-pass

Upgrade Specific Packages

# Multiple arguments require quotes
ansible all -m apt -a "name=snapd state=latest" --become --ask-become-pass

Full System Upgrade

# Equivalent to apt dist-upgrade
ansible all -m apt -a "upgrade=dist" --become --ask-become-pass

Writing Your First Playbook

Playbooks are YAML files that define automated tasks. They’re more powerful and reusable than ad-hoc commands.

Example: Install Apache

Create install_apache.yml:

---
- hosts: all
  become: true
  tasks:
  
    - name: Update repository index
      apt:
        update_cache: yes
  
    - name: Install apache2 package
      apt:
        name: apache2
        state: latest
    
    - name: Add PHP support for apache
      apt: 
        name: libapache2-mod-php
        state: latest

Example: Remove Apache

Create remove_apache.yml:

---
- hosts: all
  become: true
  tasks:
  
    - name: Remove apache2 package
      apt:
        name: apache2
        state: absent
    
    - name: Remove PHP support for apache
      apt: 
        name: libapache2-mod-php
        state: absent

Run Your Playbooks

ansible-playbook --ask-become-pass install_apache.yml
ansible-playbook --ask-become-pass remove_apache.yml

Using Conditionals (The WHEN Clause)

Conditionals let you run tasks only when specific criteria are met, such as differentiating between operating systems.

Check OS Information

cat /etc/os-release

OS-Specific Tasks

Modify install_apache.yml to handle both Ubuntu and CentOS:

---
- hosts: all
  become: true
  tasks:
  
    - name: Update repository index (Ubuntu)
      apt:
        update_cache: yes
      when: ansible_distribution == "Ubuntu"
      
    - name: Install apache2 package (Ubuntu)
      apt:
        name: apache2
        state: latest
      when: ansible_distribution == "Ubuntu"
    
    - name: Add PHP support (Ubuntu)
      apt: 
        name: libapache2-mod-php
        state: latest
      when: ansible_distribution == "Ubuntu"
      
    - name: Update repository index (CentOS)
      dnf:
        update_cache: yes
      when: ansible_distribution == "CentOS"
      
    - name: Install httpd package (CentOS)
      dnf:
        name: httpd
        state: latest
      when: ansible_distribution == "CentOS"
    
    - name: Add PHP support (CentOS)
      dnf: 
        name: php
        state: latest
      when: ansible_distribution == "CentOS"

Multiple Conditions

Match multiple distributions:

when: ansible_distribution in ["Debian", "Ubuntu"]

Combine conditions:

when: ansible_distribution == "CentOS" and ansible_distribution_version == "8.2"

Additional CentOS Configuration

CentOS requires additional steps to start the web server and configure the firewall:

systemctl start httpd
sudo firewall-cmd --add-port=80/tcp

Verify OS Facts

ansible all -m gather_facts --limit <server-ip> | grep ansible_distribution

Improving Your Playbook

As playbooks grow, consolidation improves maintainability.

Consolidated Package Installation

Combine related tasks:

---
- hosts: all
  become: true
  tasks:
      
    - name: Install apache and PHP (Ubuntu)
      apt:
        name: 
          - apache2
          - libapache2-mod-php
        state: latest
        update_cache: yes
      when: ansible_distribution == "Ubuntu"
      
    - name: Install apache and PHP (CentOS)
      dnf:
        name: 
          - httpd
          - php
        state: latest
        update_cache: yes
      when: ansible_distribution == "CentOS"

Using Variables for Package Names

Define package names per-host in your inventory:

# Inventory file
192.168.1.1 apache_package=apache2 php_package=libapache2-mod-php
192.168.1.2 apache_package=apache2 php_package=libapache2-mod-php
192.168.1.3 apache_package=httpd php_package=php

Then use the generic package module:

---
- hosts: all
  become: true
  tasks:
      
    - name: Install apache and PHP
      package:
        name: 
          - "{{ apache_package }}"
          - "{{ php_package }}"
        state: latest
        update_cache: yes

Targeting Specific Nodes

Organize your infrastructure into logical groups for targeted deployments.

Define Groups in Inventory

Update your inventory file with groups:

[web_servers]
192.168.1.1
192.168.1.3

[db_servers]
192.168.1.2

[file_servers]
192.168.1.4

Create a Site-Wide Playbook

Create site.yml targeting specific groups:

---
- hosts: all
  become: true
  pre_tasks:
  
    - name: Install updates (CentOS)
      dnf:
        update_only: yes
        update_cache: yes
      when: ansible_distribution == "CentOS"
      
    - name: Install updates (Ubuntu)
      apt:
        upgrade: dist
        update_cache: yes
      when: ansible_distribution == "Ubuntu"
      
- hosts: web_servers
  become: true
  tasks:
      
    - name: Install apache and PHP (Ubuntu)
      apt:
        name: 
          - apache2
          - libapache2-mod-php
        state: latest
      when: ansible_distribution == "Ubuntu"
      
    - name: Install apache and PHP (CentOS)
      dnf:
        name: 
          - httpd
          - php
        state: latest
      when: ansible_distribution == "CentOS"

- hosts: db_servers
  become: true
  tasks:
  
    - name: Install MariaDB (CentOS)
      dnf:
        name: mariadb
        state: latest
      when: ansible_distribution == "CentOS"
      
    - name: Install MariaDB (Ubuntu)
      apt:
        name: mariadb-server
        state: latest
      when: ansible_distribution == "Ubuntu"
      
- hosts: file_servers
  become: true
  tasks:
  
    - name: Install Samba
      package:
        name: samba
        state: latest

Note: pre_tasks run before the main tasks in each play. Ansible processes plays from top to bottom.

Using Tags

Tags let you run specific parts of a playbook without executing the entire file.

Add Tags to Tasks

Update site.yml with descriptive tags:

---
- hosts: all
  become: true
  pre_tasks:
  
    - name: Install updates (CentOS)
      tags: always
      dnf:
        update_only: yes
        update_cache: yes
      when: ansible_distribution == "CentOS"
      
    - name: Install updates (Ubuntu)
      tags: always
      apt:
        upgrade: dist
        update_cache: yes
      when: ansible_distribution == "Ubuntu"
      
- hosts: web_servers
  become: true
  tasks:
      
    - name: Install apache and PHP (Ubuntu)
      tags: apache,apache2,ubuntu
      apt:
        name: 
          - apache2
          - libapache2-mod-php
        state: latest
      when: ansible_distribution == "Ubuntu"
      
    - name: Install apache and PHP (CentOS)
      tags: apache,centos,httpd
      dnf:
        name: 
          - httpd
          - php
        state: latest
      when: ansible_distribution == "CentOS"

- hosts: db_servers
  become: true
  tasks:
  
    - name: Install MariaDB (CentOS)
      tags: centos,db,mariadb
      dnf:
        name: mariadb
        state: latest
      when: ansible_distribution == "CentOS"
      
    - name: Install MariaDB (Ubuntu)
      tags: db,mariadb,ubuntu
      apt:
        name: mariadb-server
        state: latest
      when: ansible_distribution == "Ubuntu"
      
- hosts: file_servers
  become: true
  tasks:
  
    - name: Install Samba
      tags: samba
      package:
        name: samba
        state: latest

Run Specific Tags

List available tags:

ansible-playbook --list-tags site.yml

Run only CentOS-related tasks:

ansible-playbook --tags centos --ask-become-pass site.yml

Run multiple tags:

ansible-playbook --tags "apache,db" --ask-become-pass site.yml

Managing Files

Ansible can deploy configuration files and download remote assets.

Copy Local Files

Create a files directory for your assets:

mkdir files

Create a sample HTML file (files/default_site.html):

<html>
  <title>Web-site test</title>
  <body>
    <p>Ansible is awesome!</p>
  </body>
</html>

Add a task to copy the file:

- name: Copy default HTML file for site
  tags: apache,apache2,httpd
  copy:
    src: default_site.html
    dest: /var/www/html/index.html
    owner: root
    group: root
    mode: 0644

Download and Extract Remote Files

Install Terraform from a remote source:

- hosts: workstations
  become: true
  tasks:
  
    - name: Install unzip
      package:
        name: unzip
    
    - name: Install Terraform
      unarchive:
        src: https://releases.hashicorp.com/terraform/0.12.28/terraform_0.12.28_linux_amd64.zip
        dest: /usr/local/bin
        remote_src: yes
        mode: 0755
        owner: root
        group: root

Add workstations to your inventory and distribute the Ansible key:

# Add to inventory
[workstations]
192.168.1.5

# Copy SSH key
ssh-copy-id -i ~/.ssh/ansible.pub <workstation-ip>

Managing Services

Control system services (start, stop, enable, restart) using the service module.

Start and Enable Services

For CentOS, ensure Apache starts automatically:

- name: Start and enable httpd (CentOS)
  tags: apache,centos,httpd
  service:
    name: httpd
    state: started
    enabled: yes
  when: ansible_distribution == "CentOS"

Conditionally Restart Services

Restart a service only when configuration changes:

- name: Change admin email address
  tags: apache,centos,httpd
  lineinfile:
    path: /etc/httpd/conf/httpd.conf
    regexp: '^ServerAdmin'
    line: ServerAdmin [email protected]
  when: ansible_distribution == "CentOS"
  register: httpd_config

- name: Restart httpd if config changed
  tags: apache,centos,httpd
  service:
    name: httpd
    state: restarted
  when: httpd_config.changed

Adding Users and Bootstrapping

Create users, authorize SSH keys, and configure sudo access.

Create a User

Add a user across all hosts:

- hosts: all
  become: true
  tasks:
  
    - name: Create ben user
      tags: always
      user:
        name: ben
        groups: root

Add SSH Key and Sudo Access

Authorize the user’s key and configure passwordless sudo:

- name: Add SSH key for ben
  tags: always
  authorized_key:
    user: ben
    key: "<content-of-ansible.pub>"

- name: Add sudoers file for ben
  tags: always
  copy:
    src: sudoer_ben
    dest: /etc/sudoers.d/ben
    owner: root
    group: root
    mode: 0440

Create files/sudoer_ben:

ben ALL=(ALL) NOPASSWD: ALL

Test the Configuration

Verify passwordless login and sudo:

# Test SSH access
ssh -i ~/.ssh/ansible ben@<target-server-ip>

# Test sudo privileges
sudo apt update

Configure Ansible to Use the New User

Update ansible.cfg:

[defaults]
inventory = inventory
private_key_file = ~/.ssh/ansible
remote_user = ben

Now you can run playbooks without --ask-become-pass:

ansible-playbook site.yml

Separate Bootstrap Tasks

Create bootstrap.yml for initial server setup:

---
- hosts: all
  become: true
  pre_tasks:
  
    - name: Install updates (CentOS)
      tags: always
      dnf:
        update_only: yes
        update_cache: yes
      when: ansible_distribution == "CentOS"
      
    - name: Install updates (Ubuntu)
      tags: always
      apt:
        upgrade: dist
        update_cache: yes
      when: ansible_distribution == "Ubuntu"

- hosts: all
  become: true
  tasks:
  
    - name: Create ben user
      tags: always
      user:
        name: ben
        groups: root
    
    - name: Add SSH key for ben
      tags: always
      authorized_key:
        user: ben
        key: "<content-of-ansible.pub>"
        
    - name: Add sudoers file for ben
      tags: always
      copy:
        src: sudoer_ben
        dest: /etc/sudoers.d/ben
        owner: root
        group: root
        mode: 0440

Simplify site.yml to remove user creation tasks, keeping only pre_tasks for updates.

Run bootstrap once, then use site.yml for ongoing management:

# Initial setup (requires password)
ansible-playbook --ask-become-pass bootstrap.yml

# Ongoing management (passwordless)
ansible-playbook site.yml

Organizing with Roles

Roles provide a standardized directory structure for organizing related tasks, files, and configurations.

Create Role Structure

Back up your current playbook and create role directories:

cp site.yml site_before_roles.yml
mkdir -p roles/{base,db_servers,file_servers,web_servers,workstations}/tasks

Delegate to Roles

Simplify site.yml to reference roles:

---
- hosts: all
  become: true
  pre_tasks:

    - name: Update repository index (CentOS)
      tags: always
      dnf:
        update_cache: yes
      changed_when: false
      when: ansible_distribution == "CentOS"

    - name: Update repository index (Ubuntu)
      tags: always
      apt:
        update_cache: yes
      changed_when: false
      when: ansible_distribution == "Ubuntu"

- hosts: all
  become: true
  roles:
    - base
  
- hosts: workstations
  become: true
  roles:
    - workstations

- hosts: web_servers
  become: true
  roles:
    - web_servers

- hosts: db_servers
  become: true
  roles:
    - db_servers

- hosts: file_servers
  become: true
  roles:
    - file_servers

Create Role Task Files

Base role (roles/base/tasks/main.yml):

- name: Add SSH key for ben
  authorized_key:
    user: ben
    key: "<ansible-pub-key>"

Database role (roles/db_servers/tasks/main.yml):

- name: Install MariaDB (CentOS)
  tags: centos,db,mariadb
  dnf:
    name: mariadb
    state: latest
  when: ansible_distribution == "CentOS"

- name: Install MariaDB (Ubuntu)
  tags: db,mariadb,ubuntu
  apt:
    name: mariadb-server
    state: latest
  when: ansible_distribution == "Ubuntu"

File server role (roles/file_servers/tasks/main.yml):

- name: Install Samba
  tags: samba
  package:
    name: samba
    state: latest

Workstation role (roles/workstations/tasks/main.yml):

- name: Install unzip
  package:
    name: unzip

- name: Install Terraform
  unarchive:
    src: https://releases.hashicorp.com/terraform/0.12.28/terraform_0.12.28_linux_amd64.zip
    dest: /usr/local/bin
    remote_src: yes
    mode: 0755
    owner: root
    group: root

Web server role (roles/web_servers/tasks/main.yml):

- name: Install httpd and PHP (CentOS)
  tags: apache,centos,httpd
  dnf:
    name:
      - httpd
      - php
    state: latest
  when: ansible_distribution == "CentOS"

- name: Start and enable httpd (CentOS)
  tags: apache,centos,httpd
  service:
    name: httpd
    state: started
    enabled: yes
  when: ansible_distribution == "CentOS"

- name: Install apache2 and PHP (Ubuntu)
  tags: apache,apache2,ubuntu
  apt:
    name:
      - apache2
      - libapache2-mod-php
    state: latest
  when: ansible_distribution == "Ubuntu"

- name: Change admin email address
  tags: apache,centos,httpd
  lineinfile:
    path: /etc/httpd/conf/httpd.conf
    regexp: '^ServerAdmin'
    line: ServerAdmin [email protected]
  when: ansible_distribution == "CentOS"
  register: httpd_config

- name: Restart httpd if config changed
  tags: apache,centos,httpd
  service:
    name: httpd
    state: restarted
  when: httpd_config.changed    

- name: Copy HTML file for site
  tags: apache,apache2,httpd
  copy:
    src: default_site.html
    dest: /var/www/html/index.html
    owner: root
    group: root
    mode: 0644

Move web server files to the role:

mkdir roles/web_servers/files
cp files/default_site.html roles/web_servers/files/

Run the refactored playbook:

ansible-playbook site.yml

Host Variables and Handlers

Use variables to customize behavior per-host and handlers to respond to changes.

Define Host Variables

Create a directory for host-specific variables:

mkdir host_vars

Create a variable file for each host (filename must match inventory entry):

# host_vars/192.168.1.1.yml
apache_package_name: apache2
apache_service: apache2
php_package_name: libapache2-mod-php

Copy and customize for other hosts:

# host_vars/192.168.1.3.yml (CentOS)
apache_package_name: httpd
apache_service: httpd
php_package_name: php

Update Web Server Role

Use variables in roles/web_servers/tasks/main.yml:

- name: Install apache and PHP packages
  tags: apache,httpd,php
  package:
    name:
      - "{{ apache_package_name }}"
      - "{{ php_package_name }}"
    state: latest

- name: Start and enable apache service
  tags: apache,httpd
  service:
    name: "{{ apache_service }}"
    state: started
    enabled: yes

- name: Change admin email address
  tags: apache,centos,httpd
  lineinfile:
    path: /etc/httpd/conf/httpd.conf
    regexp: '^ServerAdmin'
    line: ServerAdmin [email protected]
  when: ansible_distribution == "CentOS"
  notify: restart_apache

- name: Copy HTML file for site
  tags: apache,apache2,httpd
  copy:
    src: default_site.html
    dest: /var/www/html/index.html
    owner: root
    group: root
    mode: 0644

Create a Handler

Create roles/web_servers/handlers/main.yml:

- name: restart_apache
  service:
    name: "{{ apache_service }}"
    state: restarted

Key advantage: Handlers run only once at the end of the play, even if notified multiple times. This is more efficient than using register with when: changed for service restarts.

Working with Templates

Templates let you deploy configuration files with customized content per-host using Jinja2 syntax.

Gather Source Configuration Files

Copy SSH configuration files from your servers as templates:

# Create templates directory
mkdir roles/base/templates

# Copy from Ubuntu server
cp /etc/ssh/sshd_config roles/base/templates/sshd_config_ubuntu.j2

# Copy from CentOS server (temporarily make it readable)
scp <centos-ip>:/etc/ssh/sshd_config roles/base/templates/sshd_config_centos.j2

Make Configurations Dynamic

Edit both template files and add a variable for allowed users:

# In both sshd_config_ubuntu.j2 and sshd_config_centos.j2
AllowUsers {{ ssh_users }}

Define Template Variables

Update host variable files:

# Ubuntu hosts
apache_package_name: apache2
apache_service: apache2
php_package_name: libapache2-mod-php
ssh_users: "ben ansible"
ssh_template_file: sshd_config_ubuntu.j2

# CentOS hosts
apache_package_name: httpd
apache_service: httpd
php_package_name: php
ssh_users: "ben ansible"
ssh_template_file: sshd_config_centos.j2

Deploy Templates

Update roles/base/tasks/main.yml:

- name: Add SSH key for ben
  authorized_key:
    user: ben
    key: "<ansible-pub-key>"

- name: Generate sshd_config from template
  tags: ssh
  template:
    src: "{{ ssh_template_file }}"
    dest: /etc/ssh/sshd_config
    owner: root
    group: root
    mode: 0644
  notify: restart_sshd

Create the handler (roles/base/handlers/main.yml):

- name: restart_sshd
  service:
    name: sshd
    state: restarted

Deploy the configuration:

ansible-playbook site.yml

Summary

We’ve covered the complete Ansible workflow from initial SSH setup through advanced patterns like roles and templates.

Key takeaways:

  • Inventory files define your infrastructure
  • Playbooks declare desired state in YAML
  • Modules (like apt, copy, template) perform the actual work
  • Roles organize your automation into reusable components
  • Variables and templates customize behavior per-host
  • Handlers efficiently respond to configuration changes

This foundation prepares you to manage infrastructure at scale with Ansible.