Ansible Introduction: First Steps to Infrastructure Automation
Introduction
Ansible has revolutionized infrastructure automation by providing a simple, agentless, and powerful approach to configuration management and orchestration. Unlike traditional configuration management tools that require agents on managed nodes, Ansible uses SSH for Linux/Unix systems and WinRM for Windows, making it incredibly easy to get started with infrastructure-as-code practices.
In today's DevOps landscape, manual server configuration is not only time-consuming but also error-prone. Ansible solves this problem by allowing you to define your infrastructure state in simple YAML files called playbooks. Whether you're managing 5 servers or 5,000, Ansible enables you to automate repetitive tasks, ensure consistency across your infrastructure, and implement infrastructure-as-code principles.
This comprehensive guide will walk you through everything you need to know to get started with Ansible, from installation to writing your first automation tasks. By the end of this tutorial, you'll understand the core concepts of Ansible and be able to automate common server administration tasks.
Why Choose Ansible for Infrastructure Automation?
Key Advantages
Agentless Architecture: Ansible doesn't require any agents or additional software on managed nodes. It uses standard SSH for communication, reducing overhead and security concerns.
Simple Learning Curve: Written in YAML, Ansible playbooks are human-readable and easy to understand, even for those new to automation.
Idempotent Operations: Ansible ensures that running the same playbook multiple times produces the same result without causing unintended changes.
Extensive Module Library: With thousands of built-in modules, Ansible can manage virtually any system, application, or cloud platform.
Push-Based Model: Unlike pull-based tools, Ansible pushes configurations from a central control node, giving you immediate feedback and control.
Prerequisites
Before diving into Ansible, ensure you have the following:
- Control Node: A Linux/Unix system (Ubuntu 20.04+, Debian 10+, CentOS 7+, Rocky Linux 8+, or macOS) where Ansible will be installed
- Managed Nodes: One or more servers that you want to manage with Ansible
- SSH Access: SSH key-based authentication configured between control node and managed nodes
- Python: Python 3.6+ on the control node (Python 2.7+ or 3.5+ on managed nodes)
- Sudo/Root Access: Administrative privileges on managed nodes
- Basic Linux Knowledge: Understanding of command line, SSH, and basic system administration
Network Requirements
- Control node must have network connectivity to all managed nodes
- SSH port (default 22) must be accessible on managed nodes
- Managed nodes should allow the Ansible user to execute commands via SSH
Installation and Setup
Installing Ansible on Ubuntu/Debian
The recommended way to install Ansible on Debian-based systems is through the official PPA:
# Update package index
sudo apt update
# Install software-properties-common for add-apt-repository
sudo apt install -y software-properties-common
# Add Ansible PPA repository
sudo add-apt-repository --yes --update ppa:ansible/ansible
# Install Ansible
sudo apt install -y ansible
# Verify installation
ansible --version
Expected output:
ansible [core 2.15.x]
config file = /etc/ansible/ansible.cfg
configured module search path = ['/home/user/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
ansible python module location = /usr/lib/python3/dist-packages/ansible
ansible collection location = /home/user/.ansible/collections:/usr/share/ansible/collections
executable location = /usr/bin/ansible
python version = 3.10.x
Installing Ansible on CentOS/Rocky Linux
For RHEL-based systems, use the EPEL repository:
# Install EPEL repository
sudo dnf install -y epel-release
# Install Ansible
sudo dnf install -y ansible
# Verify installation
ansible --version
Installing Ansible with pip (Alternative Method)
For more control over the version or when you need the latest release:
# Install pip if not already installed
sudo apt install -y python3-pip # Ubuntu/Debian
# OR
sudo dnf install -y python3-pip # CentOS/Rocky
# Upgrade pip
python3 -m pip install --upgrade pip
# Install Ansible
python3 -m pip install --user ansible
# Add to PATH if needed (add to ~/.bashrc or ~/.zshrc)
export PATH="$PATH:$HOME/.local/bin"
# Verify installation
ansible --version
Setting Up SSH Key Authentication
Before using Ansible, configure SSH key-based authentication:
# Generate SSH key pair (if you don't have one)
ssh-keygen -t ed25519 -C "ansible-automation" -f ~/.ssh/ansible_key
# Copy public key to managed nodes
ssh-copy-id -i ~/.ssh/ansible_key.pub user@managed-node-1
ssh-copy-id -i ~/.ssh/ansible_key.pub user@managed-node-2
ssh-copy-id -i ~/.ssh/ansible_key.pub user@managed-node-3
# Test SSH connection
ssh -i ~/.ssh/ansible_key user@managed-node-1
For automated key distribution across multiple servers:
#!/bin/bash
# distribute-keys.sh
ANSIBLE_KEY="$HOME/.ssh/ansible_key.pub"
MANAGED_NODES="192.168.1.10 192.168.1.11 192.168.1.12"
SSH_USER="admin"
for node in $MANAGED_NODES; do
echo "Copying key to $node..."
ssh-copy-id -i $ANSIBLE_KEY ${SSH_USER}@${node}
done
Core Concepts and Architecture
Ansible Components
Control Node: The machine where Ansible is installed and from which you run commands and playbooks.
Managed Nodes: The servers or devices you manage with Ansible. Also called "hosts."
Inventory: A file defining the managed nodes, organized into groups. Can be static (INI or YAML) or dynamic (scripts that query external sources).
Modules: Reusable units of code that Ansible executes on managed nodes. Examples include apt, yum, copy, service, etc.
Tasks: Individual actions that Ansible performs using modules.
Playbooks: YAML files containing one or more plays, which define tasks to execute on specified hosts.
Plays: Map managed node groups to tasks.
Roles: Structured way to organize playbooks, variables, files, and templates into reusable components.
Facts: System information gathered automatically from managed nodes.
How Ansible Works
- You define desired state in playbooks or run ad-hoc commands
- Ansible connects to managed nodes via SSH
- Ansible copies module code to managed nodes
- Modules execute on managed nodes and return results
- Ansible removes the module code from managed nodes
- Results are displayed on the control node
Setting Up Your First Inventory
The inventory file is where you define your managed nodes. Create your first inventory:
Static Inventory (INI Format)
# Create inventory directory
mkdir -p ~/ansible-projects/inventory
# Create inventory file
cat > ~/ansible-projects/inventory/hosts << 'EOF'
# Web Servers
[webservers]
web1.example.com ansible_host=192.168.1.10
web2.example.com ansible_host=192.168.1.11
web3.example.com ansible_host=192.168.1.12
# Database Servers
[databases]
db1.example.com ansible_host=192.168.1.20
db2.example.com ansible_host=192.168.1.21
# Load Balancers
[loadbalancers]
lb1.example.com ansible_host=192.168.1.30
# Group of groups
[production:children]
webservers
databases
loadbalancers
# Variables for all hosts
[all:vars]
ansible_user=admin
ansible_ssh_private_key_file=~/.ssh/ansible_key
ansible_python_interpreter=/usr/bin/python3
EOF
Static Inventory (YAML Format)
# ~/ansible-projects/inventory/hosts.yml
all:
children:
webservers:
hosts:
web1.example.com:
ansible_host: 192.168.1.10
web2.example.com:
ansible_host: 192.168.1.11
web3.example.com:
ansible_host: 192.168.1.12
databases:
hosts:
db1.example.com:
ansible_host: 192.168.1.20
db2.example.com:
ansible_host: 192.168.1.21
loadbalancers:
hosts:
lb1.example.com:
ansible_host: 192.168.1.30
production:
children:
webservers:
databases:
loadbalancers:
vars:
ansible_user: admin
ansible_ssh_private_key_file: ~/.ssh/ansible_key
ansible_python_interpreter: /usr/bin/python3
Testing Inventory Connectivity
Verify that Ansible can connect to your managed nodes:
# Ping all hosts
ansible all -i ~/ansible-projects/inventory/hosts -m ping
# Ping specific group
ansible webservers -i ~/ansible-projects/inventory/hosts -m ping
# List all hosts in inventory
ansible all -i ~/ansible-projects/inventory/hosts --list-hosts
# Show inventory structure
ansible-inventory -i ~/ansible-projects/inventory/hosts --graph
Expected output for ping:
web1.example.com | SUCCESS => {
"changed": false,
"ping": "pong"
}
web2.example.com | SUCCESS => {
"changed": false,
"ping": "pong"
}
Running Ad-Hoc Commands
Ad-hoc commands are useful for quick, one-time tasks without writing a playbook.
Basic Syntax
ansible <hosts> -i <inventory> -m <module> -a "<arguments>"
Practical Examples
Check disk space on all servers:
ansible all -i inventory/hosts -m shell -a "df -h"
Update package cache on Ubuntu servers:
ansible webservers -i inventory/hosts -m apt -a "update_cache=yes" --become
Install a package:
ansible databases -i inventory/hosts -m apt -a "name=postgresql state=present" --become
Restart a service:
ansible webservers -i inventory/hosts -m systemd -a "name=nginx state=restarted" --become
Copy a file to all servers:
ansible all -i inventory/hosts -m copy -a "src=/local/file.txt dest=/tmp/file.txt mode=0644"
Create a user:
ansible all -i inventory/hosts -m user -a "name=deploy state=present shell=/bin/bash" --become
Gather system facts:
ansible web1.example.com -i inventory/hosts -m setup
Check uptime:
ansible all -i inventory/hosts -m command -a "uptime"
Configuration Management
Ansible Configuration File
Ansible looks for configuration files in this order:
ANSIBLE_CONFIGenvironment variableansible.cfgin current directory~/.ansible.cfgin home directory/etc/ansible/ansible.cfgsystem-wide
Create a project-specific configuration:
# ~/ansible-projects/ansible.cfg
[defaults]
# Inventory file location
inventory = ./inventory/hosts
# Don't check host keys (for testing only)
host_key_checking = False
# Number of parallel processes
forks = 10
# Timeout for connections
timeout = 30
# Default user for SSH connections
remote_user = admin
# Private key file
private_key_file = ~/.ssh/ansible_key
# Disable cowsay (optional)
nocows = 1
# Gathering facts (implicit, explicit, smart)
gathering = smart
# Fact caching
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts
fact_caching_timeout = 3600
# Logging
log_path = ./ansible.log
# Roles path
roles_path = ./roles
[privilege_escalation]
# Become settings
become = False
become_method = sudo
become_user = root
become_ask_pass = False
[ssh_connection]
# SSH settings
pipelining = True
control_path = /tmp/ansible-ssh-%%h-%%p-%%r
Environment Variables
Common Ansible environment variables:
# Set inventory location
export ANSIBLE_INVENTORY=~/ansible-projects/inventory/hosts
# Set configuration file
export ANSIBLE_CONFIG=~/ansible-projects/ansible.cfg
# Increase verbosity
export ANSIBLE_VERBOSITY=2
# Set SSH arguments
export ANSIBLE_SSH_ARGS="-o ControlMaster=auto -o ControlPersist=60s"
# Disable host key checking
export ANSIBLE_HOST_KEY_CHECKING=False
Writing Your First Playbook
Playbooks are the heart of Ansible automation. Let's create a practical playbook:
Basic Web Server Setup Playbook
# ~/ansible-projects/webserver-setup.yml
---
- name: Configure web servers with Nginx
hosts: webservers
become: yes
tasks:
- name: Update apt cache
apt:
update_cache: yes
cache_valid_time: 3600
when: ansible_os_family == "Debian"
- name: Install Nginx
apt:
name: nginx
state: present
when: ansible_os_family == "Debian"
- name: Install Nginx on RHEL
yum:
name: nginx
state: present
when: ansible_os_family == "RedHat"
- name: Ensure Nginx is started and enabled
systemd:
name: nginx
state: started
enabled: yes
- name: Copy custom index.html
copy:
content: |
<!DOCTYPE html>
<html>
<head><title>Welcome to {{ inventory_hostname }}</title></head>
<body>
<h1>Server: {{ inventory_hostname }}</h1>
<p>Configured by Ansible</p>
</body>
</html>
dest: /var/www/html/index.html
mode: '0644'
- name: Configure firewall for HTTP
ufw:
rule: allow
port: '80'
proto: tcp
when: ansible_os_family == "Debian"
Run the playbook:
cd ~/ansible-projects
ansible-playbook webserver-setup.yml
Multi-Play Playbook
# ~/ansible-projects/full-stack-setup.yml
---
- name: Configure web servers
hosts: webservers
become: yes
tasks:
- name: Install and configure Nginx
apt:
name: nginx
state: present
update_cache: yes
- name: Start Nginx
systemd:
name: nginx
state: started
enabled: yes
- name: Configure database servers
hosts: databases
become: yes
tasks:
- name: Install PostgreSQL
apt:
name:
- postgresql
- postgresql-contrib
- python3-psycopg2
state: present
update_cache: yes
- name: Ensure PostgreSQL is running
systemd:
name: postgresql
state: started
enabled: yes
- name: Configure all servers
hosts: all
become: yes
tasks:
- name: Create monitoring user
user:
name: monitoring
state: present
shell: /bin/bash
create_home: yes
- name: Install security updates
apt:
upgrade: safe
update_cache: yes
when: ansible_os_family == "Debian"
Practical Examples
Example 1: System Hardening Playbook
# ~/ansible-projects/security-hardening.yml
---
- name: Basic security hardening
hosts: all
become: yes
tasks:
- name: Update all packages
apt:
upgrade: dist
update_cache: yes
autoremove: yes
when: ansible_os_family == "Debian"
- name: Install security packages
apt:
name:
- ufw
- fail2ban
- unattended-upgrades
state: present
- name: Configure UFW default policies
ufw:
direction: "{{ item.direction }}"
policy: "{{ item.policy }}"
loop:
- { direction: 'incoming', policy: 'deny' }
- { direction: 'outgoing', policy: 'allow' }
- name: Allow SSH through firewall
ufw:
rule: allow
port: '22'
proto: tcp
- name: Enable UFW
ufw:
state: enabled
- name: Configure automatic security updates
copy:
content: |
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Download-Upgradeable-Packages "1";
APT::Periodic::AutocleanInterval "7";
APT::Periodic::Unattended-Upgrade "1";
dest: /etc/apt/apt.conf.d/20auto-upgrades
mode: '0644'
- name: Disable root login via SSH
lineinfile:
path: /etc/ssh/sshd_config
regexp: '^PermitRootLogin'
line: 'PermitRootLogin no'
state: present
notify: restart ssh
handlers:
- name: restart ssh
systemd:
name: sshd
state: restarted
Example 2: User Management Playbook
# ~/ansible-projects/user-management.yml
---
- name: Manage users across servers
hosts: all
become: yes
vars:
admin_users:
- username: alice
groups: sudo
ssh_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... [email protected]"
- username: bob
groups: sudo
ssh_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... [email protected]"
removed_users:
- charlie
- david
tasks:
- name: Create admin users
user:
name: "{{ item.username }}"
groups: "{{ item.groups }}"
append: yes
shell: /bin/bash
create_home: yes
state: present
loop: "{{ admin_users }}"
- name: Add SSH keys for admin users
authorized_key:
user: "{{ item.username }}"
key: "{{ item.ssh_key }}"
state: present
loop: "{{ admin_users }}"
- name: Remove old users
user:
name: "{{ item }}"
state: absent
remove: yes
loop: "{{ removed_users }}"
- name: Configure sudo without password for admins
lineinfile:
path: /etc/sudoers.d/admins
line: "%sudo ALL=(ALL) NOPASSWD: ALL"
create: yes
mode: '0440'
validate: 'visudo -cf %s'
Example 3: Monitoring Setup Playbook
# ~/ansible-projects/monitoring-setup.yml
---
- name: Install Node Exporter for Prometheus
hosts: all
become: yes
vars:
node_exporter_version: "1.7.0"
node_exporter_user: node_exporter
tasks:
- name: Create node_exporter user
user:
name: "{{ node_exporter_user }}"
system: yes
shell: /bin/false
create_home: no
- name: Download Node Exporter
get_url:
url: "https://github.com/prometheus/node_exporter/releases/download/v{{ node_exporter_version }}/node_exporter-{{ node_exporter_version }}.linux-amd64.tar.gz"
dest: "/tmp/node_exporter.tar.gz"
- name: Extract Node Exporter
unarchive:
src: "/tmp/node_exporter.tar.gz"
dest: "/tmp"
remote_src: yes
- name: Copy Node Exporter binary
copy:
src: "/tmp/node_exporter-{{ node_exporter_version }}.linux-amd64/node_exporter"
dest: "/usr/local/bin/node_exporter"
mode: '0755'
remote_src: yes
- name: Create systemd service file
copy:
content: |
[Unit]
Description=Node Exporter
After=network.target
[Service]
User={{ node_exporter_user }}
Group={{ node_exporter_user }}
Type=simple
ExecStart=/usr/local/bin/node_exporter
[Install]
WantedBy=multi-user.target
dest: /etc/systemd/system/node_exporter.service
mode: '0644'
- name: Start and enable Node Exporter
systemd:
name: node_exporter
state: started
enabled: yes
daemon_reload: yes
- name: Allow Node Exporter through firewall
ufw:
rule: allow
port: '9100'
proto: tcp
when: ansible_os_family == "Debian"
Best Practices
1. Idempotency
Always write playbooks that are idempotent - they should produce the same result regardless of how many times they're run:
# Good - idempotent
- name: Ensure nginx is installed
apt:
name: nginx
state: present
# Avoid - not idempotent
- name: Install nginx
shell: apt-get install -y nginx
2. Use Modules Over Shell Commands
Prefer built-in modules over shell/command modules:
# Good
- name: Create directory
file:
path: /opt/myapp
state: directory
mode: '0755'
# Avoid
- name: Create directory
shell: mkdir -p /opt/myapp && chmod 755 /opt/myapp
3. Organize with Roles
For complex projects, use roles for better organization:
# Create role structure
ansible-galaxy init webserver
# Directory structure
roles/
webserver/
tasks/
main.yml
handlers/
main.yml
templates/
nginx.conf.j2
files/
vars/
main.yml
defaults/
main.yml
4. Use Variables Wisely
Keep sensitive data in encrypted vaults:
# Create encrypted file
ansible-vault create secrets.yml
# Edit encrypted file
ansible-vault edit secrets.yml
# Run playbook with vault
ansible-playbook site.yml --ask-vault-pass
5. Test Before Deploying
Always use check mode to test changes:
# Dry run - don't make actual changes
ansible-playbook site.yml --check
# Show differences
ansible-playbook site.yml --check --diff
6. Use Tags
Tag tasks for selective execution:
tasks:
- name: Install packages
apt:
name: nginx
state: present
tags:
- packages
- nginx
- name: Configure firewall
ufw:
rule: allow
port: 80
tags:
- security
- firewall
Run specific tags:
ansible-playbook site.yml --tags "packages"
ansible-playbook site.yml --skip-tags "security"
7. Version Control
Always keep your Ansible code in version control:
cd ~/ansible-projects
git init
git add .
git commit -m "Initial Ansible project setup"
Create a .gitignore:
# .gitignore
*.retry
*.log
.vault_pass
ansible_facts/
8. Documentation
Document your playbooks with comments:
---
# Playbook: webserver-setup.yml
# Purpose: Configure Nginx web servers with SSL
# Author: DevOps Team
# Last Updated: 2024-01-15
- name: Configure web servers
hosts: webservers
become: yes
# This playbook installs and configures Nginx with Let's Encrypt SSL
tasks:
- name: Install Nginx
# Using apt module for idempotency
apt:
name: nginx
state: present
Troubleshooting
Common Issues and Solutions
Issue: SSH Connection Failure
# Debug SSH connection
ansible all -m ping -vvv
# Test SSH manually
ssh -i ~/.ssh/ansible_key user@host
# Check SSH configuration
ansible all -m setup -a "filter=ansible_ssh*"
Issue: Permission Denied
# Use become for privilege escalation
ansible-playbook site.yml --become --ask-become-pass
# Check sudo configuration on managed node
ansible host -m shell -a "sudo -l" --become
Issue: Module Not Found
# List installed modules
ansible-doc -l
# Check Python version
ansible all -m shell -a "python3 --version"
# Install required Python packages
ansible all -m pip -a "name=requests state=present"
Issue: Slow Playbook Execution
# Disable fact gathering if not needed
- hosts: all
gather_facts: no
# Or use smart gathering
gathering = smart # in ansible.cfg
# Enable pipelining
pipelining = True # in ansible.cfg [ssh_connection]
Issue: Playbook Syntax Errors
# Validate playbook syntax
ansible-playbook site.yml --syntax-check
# Use yamllint
pip install yamllint
yamllint playbook.yml
Debugging Techniques
Increase Verbosity:
ansible-playbook site.yml -v # verbose
ansible-playbook site.yml -vv # more verbose
ansible-playbook site.yml -vvv # debug
ansible-playbook site.yml -vvvv # connection debug
Debug Module:
- name: Show variable value
debug:
var: ansible_distribution
- name: Show message
debug:
msg: "The hostname is {{ inventory_hostname }}"
Pause Execution:
- name: Pause for verification
pause:
prompt: "Check the server before continuing"
minutes: 5
Register Variables:
- name: Run command
shell: uptime
register: uptime_result
- name: Show result
debug:
var: uptime_result.stdout
Conclusion
Ansible is a powerful tool for infrastructure automation that simplifies configuration management, application deployment, and orchestration. This guide has covered the fundamental concepts, from installation and setup to writing your first playbooks and implementing best practices.
Key takeaways from this introduction:
- Ansible's agentless architecture makes it easy to deploy and manage
- YAML-based playbooks are human-readable and version-controllable
- The extensive module library covers most automation needs
- Ad-hoc commands are useful for quick tasks
- Playbooks enable complex, reusable automation workflows
- Following best practices ensures maintainable, reliable automation
As you continue your Ansible journey, focus on building a library of reusable roles, implementing proper testing, and gradually automating more of your infrastructure. The initial investment in learning Ansible will pay dividends in reduced manual work, improved consistency, and faster deployments.
Next steps:
- Practice writing playbooks for your specific use cases
- Explore Ansible Galaxy for community roles
- Learn about Ansible Vault for secrets management
- Study advanced topics like dynamic inventory and custom modules
- Implement CI/CD for your Ansible code
Remember that infrastructure-as-code is a journey, not a destination. Start small, automate incrementally, and continuously refine your playbooks based on lessons learned. With Ansible in your toolkit, you're well-equipped to tackle modern infrastructure challenges efficiently and reliably.


