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

  1. You define desired state in playbooks or run ad-hoc commands
  2. Ansible connects to managed nodes via SSH
  3. Ansible copies module code to managed nodes
  4. Modules execute on managed nodes and return results
  5. Ansible removes the module code from managed nodes
  6. 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:

  1. ANSIBLE_CONFIG environment variable
  2. ansible.cfg in current directory
  3. ~/.ansible.cfg in home directory
  4. /etc/ansible/ansible.cfg system-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:

  1. Practice writing playbooks for your specific use cases
  2. Explore Ansible Galaxy for community roles
  3. Learn about Ansible Vault for secrets management
  4. Study advanced topics like dynamic inventory and custom modules
  5. 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.