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:
- Create a GitHub account (if you don’t have one)
- Create a new repository for your Ansible project
- Add your SSH public key to your GitHub account
- 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.