Skip to content

Ansible Overview

Introduction

Ansible is an open-source automation platform that enables infrastructure as code, configuration management, application deployment, and orchestration without requiring any agent software on managed nodes. It uses a declarative, human-readable YAML syntax called Playbooks to describe the desired state of systems, making it one of the most accessible automation tools in modern DevOps. Understanding Ansible is essential for anyone working with cloud infrastructure, CI/CD pipelines, or large-scale system administration.

Core Concepts

What Makes Ansible Different

Ansible stands apart from tools like Puppet, Chef, and SaltStack due to three fundamental design decisions:

  • Agentless Architecture: Ansible communicates with managed nodes over SSH (Linux) or WinRM (Windows). There is no daemon to install, no database to maintain, and no additional attack surface on your servers.
  • Push-Based Model: The control node pushes configurations to managed nodes on demand, rather than nodes polling a central server on a schedule.
  • Idempotency: Running the same Ansible playbook multiple times produces the same result. If a system is already in the desired state, Ansible makes no changes.

Architecture Overview

Ansible's architecture is intentionally simple. The control node is the machine where Ansible is installed and from which all automation is driven. Managed nodes (also called hosts) are the target systems. The control node reads an inventory to discover managed nodes, executes modules on those nodes, and organizes work through playbooks.

Key Terminology

TermDescription
Control NodeThe machine where Ansible is installed and playbooks are executed from
Managed NodeAny server or device managed by Ansible (no agent required)
InventoryA file or script that defines hosts and groups of hosts
ModuleA unit of work Ansible can execute (e.g., apt, copy, service)
TaskA single call to a module with specific arguments
PlayA mapping of hosts to a set of tasks
PlaybookA YAML file containing one or more plays
RoleA structured, reusable collection of tasks, files, templates, and variables
HandlerA task that runs only when notified by another task
FactsSystem information gathered automatically from managed nodes
VaultAnsible's encryption mechanism for sensitive data

Inventory

The inventory tells Ansible which machines to manage. It can be a simple INI or YAML file, or a dynamic script that queries cloud APIs.

Static Inventory (INI Format)

ini
# inventory/hosts.ini

[webservers]
web1.example.com ansible_host=10.0.1.10
web2.example.com ansible_host=10.0.1.11

[dbservers]
db1.example.com ansible_host=10.0.2.10
db2.example.com ansible_host=10.0.2.11

[loadbalancers]
lb1.example.com ansible_host=10.0.0.10

[production:children]
webservers
dbservers
loadbalancers

[production:vars]
ansible_user=deploy
ansible_ssh_private_key_file=~/.ssh/prod_key

Static Inventory (YAML Format)

yaml
# inventory/hosts.yml
all:
  children:
    production:
      children:
        webservers:
          hosts:
            web1.example.com:
              ansible_host: 10.0.1.10
            web2.example.com:
              ansible_host: 10.0.1.11
        dbservers:
          hosts:
            db1.example.com:
              ansible_host: 10.0.2.10
        loadbalancers:
          hosts:
            lb1.example.com:
              ansible_host: 10.0.0.10
      vars:
        ansible_user: deploy
        ansible_ssh_private_key_file: ~/.ssh/prod_key

Playbooks

Playbooks are the heart of Ansible. They declare the desired state of your infrastructure in YAML.

Basic Playbook Structure

yaml
# site.yml — Full stack deployment
---
- name: Configure web servers
  hosts: webservers
  become: yes
  vars:
    http_port: 80
    app_version: "2.4.1"

  tasks:
    - name: Install Nginx
      apt:
        name: nginx
        state: present
        update_cache: yes

    - name: Deploy application configuration
      template:
        src: templates/nginx.conf.j2
        dest: /etc/nginx/sites-available/default
        owner: root
        group: root
        mode: '0644'
      notify: Restart Nginx

    - name: Ensure Nginx is running and enabled
      service:
        name: nginx
        state: started
        enabled: yes

  handlers:
    - name: Restart Nginx
      service:
        name: nginx
        state: restarted

Playbook Execution Flow

Multi-Play Playbook

yaml
# deploy-stack.yml
---
- name: Update database servers
  hosts: dbservers
  become: yes
  tasks:
    - name: Install PostgreSQL 15
      apt:
        name:
          - postgresql-15
          - postgresql-client-15
        state: present

    - name: Configure PostgreSQL to listen on all interfaces
      lineinfile:
        path: /etc/postgresql/15/main/postgresql.conf
        regexp: "^#?listen_addresses"
        line: "listen_addresses = '*'"
      notify: Restart PostgreSQL

    - name: Allow application subnet in pg_hba
      lineinfile:
        path: /etc/postgresql/15/main/pg_hba.conf
        line: "host all all 10.0.1.0/24 md5"
      notify: Restart PostgreSQL

  handlers:
    - name: Restart PostgreSQL
      service:
        name: postgresql
        state: restarted

- name: Deploy application to web servers
  hosts: webservers
  become: yes
  serial: 1  # Rolling deployment — one server at a time
  vars:
    app_repo: "https://github.com/company/webapp.git"
    app_branch: "release/v2.4"

  tasks:
    - name: Pull latest application code
      git:
        repo: "{{ app_repo }}"
        dest: /opt/webapp
        version: "{{ app_branch }}"
        force: yes

    - name: Install application dependencies
      pip:
        requirements: /opt/webapp/requirements.txt
        virtualenv: /opt/webapp/venv

    - name: Restart application service
      systemd:
        name: webapp
        state: restarted
        daemon_reload: yes

    - name: Wait for application to become healthy
      uri:
        url: "http://localhost:8080/health"
        status_code: 200
      register: health_check
      until: health_check.status == 200
      retries: 10
      delay: 5

Modules

Modules are the building blocks of Ansible. Each module performs a specific action. Ansible ships with thousands of modules organized into collections.

Common Module Examples

yaml
# File and directory management
- name: Create application directory
  file:
    path: /opt/myapp
    state: directory
    owner: appuser
    group: appuser
    mode: '0755'

# Copy files with validation
- name: Deploy sudoers file
  copy:
    src: files/custom-sudoers
    dest: /etc/sudoers.d/custom
    validate: visudo -cf %s
    mode: '0440'

# Template with Jinja2
- name: Generate application config
  template:
    src: templates/app.conf.j2
    dest: /etc/myapp/app.conf
    backup: yes

# User management
- name: Create application user
  user:
    name: appuser
    shell: /bin/bash
    groups: [docker, sudo]
    append: yes
    create_home: yes

# Execute commands with guards
- name: Initialize database
  command: /opt/myapp/init-db.sh
  args:
    creates: /opt/myapp/.db-initialized

Roles

Roles provide a structured way to organize playbooks into reusable components. A role encapsulates tasks, variables, files, templates, and handlers under a standardized directory layout.

Role Directory Structure

roles/
└── webserver/
    ├── defaults/
    │   └── main.yml       # Default variables (lowest precedence)
    ├── vars/
    │   └── main.yml       # Role variables (higher precedence)
    ├── tasks/
    │   └── main.yml       # Main task list
    ├── handlers/
    │   └── main.yml       # Handler definitions
    ├── templates/
    │   └── nginx.conf.j2  # Jinja2 templates
    ├── files/
    │   └── index.html     # Static files
    ├── meta/
    │   └── main.yml       # Role dependencies
    └── README.md

Role Task File

yaml
# roles/webserver/tasks/main.yml
---
- name: Install Nginx
  apt:
    name: nginx
    state: present
    update_cache: yes
  tags: [install]

- name: Create web root directory
  file:
    path: "{{ web_root }}"
    state: directory
    owner: www-data
    mode: '0755'

- name: Deploy Nginx configuration
  template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
    validate: nginx -t -c %s
  notify: Reload Nginx

- name: Deploy site configuration
  template:
    src: site.conf.j2
    dest: "/etc/nginx/sites-available/{{ site_name }}.conf"
  notify: Reload Nginx

- name: Enable site
  file:
    src: "/etc/nginx/sites-available/{{ site_name }}.conf"
    dest: "/etc/nginx/sites-enabled/{{ site_name }}.conf"
    state: link
  notify: Reload Nginx

- name: Ensure Nginx is started and enabled
  service:
    name: nginx
    state: started
    enabled: yes
yaml
# roles/webserver/defaults/main.yml
---
web_root: /var/www/html
site_name: default
http_port: 80
worker_processes: auto
worker_connections: 1024
yaml
# Using roles in a playbook
---
- name: Configure full stack
  hosts: webservers
  become: yes
  roles:
    - common
    - role: webserver
      vars:
        http_port: 8080
        site_name: myapp
    - role: monitoring
      tags: [monitoring]

Variables and Templating

Ansible uses Jinja2 for templating. Variables can be defined at many levels with a well-defined precedence order.

Variable Precedence (Lowest to Highest)

Jinja2 Template Example

jinja2
{# templates/nginx.conf.j2 #}
worker_processes {{ worker_processes }};

events {
    worker_connections {{ worker_connections }};
}

http {
    upstream app_backend {
    {% for host in groups['webservers'] %}
        server {{ hostvars[host]['ansible_host'] }}:{{ app_port | default(8080) }};
    {% endfor %}
    }

    server {
        listen {{ http_port }};
        server_name {{ server_name | default('_') }};

        location / {
            proxy_pass http://app_backend;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
        }

    {% if enable_ssl | default(false) %}
        listen 443 ssl;
        ssl_certificate {{ ssl_cert_path }};
        ssl_certificate_key {{ ssl_key_path }};
    {% endif %}
    }
}

Conditionals and Loops

yaml
# Using conditionals
- name: Install packages based on OS family
  apt:
    name: "{{ item }}"
    state: present
  loop:
    - curl
    - wget
    - jq
  when: ansible_os_family == "Debian"

- name: Install packages on RedHat
  yum:
    name: "{{ item }}"
    state: present
  loop:
    - curl
    - wget
    - jq
  when: ansible_os_family == "RedHat"

# Using registered variables
- name: Check if application is installed
  stat:
    path: /opt/myapp/bin/myapp
  register: app_binary

- name: Install application
  shell: /tmp/install.sh
  when: not app_binary.stat.exists

# Looping over dictionaries
- name: Create application users
  user:
    name: "{{ item.key }}"
    groups: "{{ item.value.groups }}"
    shell: "{{ item.value.shell | default('/bin/bash') }}"
  loop: "{{ users | dict2items }}"
  vars:
    users:
      alice:
        groups: [sudo, docker]
      bob:
        groups: [docker]
        shell: /bin/zsh

Ansible Vault

Vault encrypts sensitive data (passwords, API keys, certificates) so they can be safely stored in version control.

yaml
# group_vars/production/vault.yml (encrypted)
# Created with: ansible-vault encrypt group_vars/production/vault.yml
vault_db_password: "s3cureP@ssw0rd!"
vault_api_key: "ak-12345-abcde-67890"
vault_ssl_private_key: |
  -----BEGIN RSA PRIVATE KEY-----
  MIIEpAIBAAKCAQEA...
  -----END RSA PRIVATE KEY-----

# group_vars/production/vars.yml (plaintext, references vault vars)
db_password: "{{ vault_db_password }}"
api_key: "{{ vault_api_key }}"

Ad-Hoc Commands

Before writing playbooks, Ansible supports quick one-off commands:

bash
# Ping all hosts
ansible all -i inventory/hosts.ini -m ping

# Check disk space on web servers
ansible webservers -m shell -a "df -h /"

# Install a package on database servers
ansible dbservers -m apt -a "name=htop state=present" --become

# Copy a file to all production servers
ansible production -m copy -a "src=./motd dest=/etc/motd"

# Restart a service
ansible webservers -m service -a "name=nginx state=restarted" --become

# Gather facts from a specific host
ansible web1.example.com -m setup

Ansible Galaxy and Collections

Ansible Galaxy is the community hub for sharing roles and collections. Collections bundle modules, plugins, and roles from a specific vendor or domain.

yaml
# requirements.yml — install dependencies before running playbooks
---
collections:
  - name: amazon.aws
    version: ">=6.0.0"
  - name: community.postgresql
    version: ">=3.0.0"
  - name: community.docker
    version: ">=3.0.0"

roles:
  - name: geerlingguy.docker
    version: "6.1.0"
  - name: geerlingguy.certbot
    version: "5.1.0"
bash
# Install collections and roles
ansible-galaxy install -r requirements.yml
ansible-galaxy collection install amazon.aws
ansible-galaxy role install geerlingguy.docker

Ansible with AWS

Ansible integrates deeply with AWS through the amazon.aws and community.aws collections.

yaml
# provision-aws.yml — Provision EC2 instances
---
- name: Provision AWS infrastructure
  hosts: localhost
  connection: local
  gather_facts: false

  vars:
    region: us-east-1
    instance_type: t3.medium
    ami_id: ami-0c55b159cbfafe1f0
    key_name: deploy-key

  tasks:
    - name: Create VPC
      amazon.aws.ec2_vpc_net:
        name: app-vpc
        cidr_block: 10.0.0.0/16
        region: "{{ region }}"
        tags:
          Environment: production
      register: vpc

    - name: Create subnet
      amazon.aws.ec2_vpc_subnet:
        vpc_id: "{{ vpc.vpc.id }}"
        cidr: 10.0.1.0/24
        az: "{{ region }}a"
        tags:
          Name: app-subnet
      register: subnet

    - name: Create security group
      amazon.aws.ec2_security_group:
        name: app-sg
        description: Application security group
        vpc_id: "{{ vpc.vpc.id }}"
        rules:
          - proto: tcp
            ports: [22, 80, 443]
            cidr_ip: 0.0.0.0/0
      register: security_group

    - name: Launch EC2 instances
      amazon.aws.ec2_instance:
        name: "web-{{ item }}"
        instance_type: "{{ instance_type }}"
        image_id: "{{ ami_id }}"
        key_name: "{{ key_name }}"
        vpc_subnet_id: "{{ subnet.subnet.id }}"
        security_group: "{{ security_group.group_id }}"
        network:
          assign_public_ip: true
        wait: yes
        tags:
          Role: webserver
      loop: [1, 2, 3]
      register: ec2_instances

    - name: Add instances to inventory
      add_host:
        name: "{{ item.instances[0].public_ip_address }}"
        groups: webservers
      loop: "{{ ec2_instances.results }}"

Execution Strategies and Performance

yaml
# Performance tuning in ansible.cfg
[defaults]
forks = 20                    # Parallel host connections
gathering = smart             # Cache facts, don't re-gather
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts
fact_caching_timeout = 3600

[ssh_connection]
pipelining = True             # Reduces SSH operations
ssh_args = -o ControlMaster=auto -o ControlPersist=60s

Error Handling and Debugging

yaml
# Error handling patterns
- name: Attempt risky operation
  block:
    - name: Try deploying new version
      shell: /opt/app/deploy.sh --version={{ new_version }}

    - name: Verify deployment
      uri:
        url: http://localhost:8080/health
        status_code: 200
      register: health
      retries: 5
      delay: 3
      until: health.status == 200

  rescue:
    - name: Rollback to previous version
      shell: /opt/app/deploy.sh --version={{ old_version }}

    - name: Notify team of failure
      slack:
        token: "{{ slack_token }}"
        channel: '#deployments'
        msg: "Deployment of {{ new_version }} FAILED. Rolled back."

  always:
    - name: Write deployment log
      lineinfile:
        path: /var/log/deployments.log
        line: "{{ ansible_date_time.iso8601 }} - version={{ new_version }} - status={{ 'failed' if ansible_failed_task is defined else 'success' }}"
        create: yes

Ansible vs Other Tools

FeatureAnsiblePuppetChefTerraform
Primary UseConfig mgmt + orchestrationConfig mgmtConfig mgmtInfrastructure provisioning
AgentNoYesYesNo
LanguageYAMLPuppet DSLRuby DSLHCL
ModelPushPullPullPush
StateStatelessStateful (PuppetDB)Stateful (Chef Server)Stateful (tfstate)
Learning CurveLowMediumHighMedium

Best Practices

  1. Use roles for reusability: Break playbooks into roles that encapsulate a single responsibility—one role per application or service component.
  2. Keep secrets in Vault: Never store passwords, API keys, or certificates in plaintext. Use ansible-vault and prefix encrypted variables with vault_.
  3. Use meaningful names: Every task should have a descriptive name field that explains what and why, not just how.
  4. Leverage tags: Tag tasks and roles so you can selectively run subsets of your playbook with --tags or --skip-tags.
  5. Test with check mode: Always run ansible-playbook --check --diff before applying changes to production to preview what will change.
  6. Pin versions: Pin collection, role, and package versions in requirements.yml and playbooks to ensure reproducible deployments.
  7. Use serial for rolling deployments: When updating web servers behind a load balancer, use serial: 1 or serial: "25%" to avoid downtime.
  8. Prefer modules over shell/command: Modules are idempotent by design; shell and command are not. Use creates, removes, or changed_when if you must use shell.
  9. Organize variables by environment: Use group_vars/ and host_vars/ directories to separate environment-specific configuration from shared defaults.
  10. Implement CI/CD for playbooks: Lint playbooks with ansible-lint, test with Molecule, and run in CI before applying to production.