Ansible: Configuration Management Automation

Ansible has become the de facto standard for configuration management and automation in modern infrastructure. Unlike Puppet and Chef, which require agents on managed nodes, Ansible operates...

Key Insights

  • Ansible’s agentless architecture using SSH eliminates the need for installing agents on managed nodes, making it simpler to deploy and maintain than Puppet or Chef
  • Idempotency ensures playbooks can run repeatedly without unintended side effects, allowing safe automation of infrastructure changes in production environments
  • Roles provide a structured way to organize automation code for reusability, while Ansible Vault secures sensitive data like passwords and API keys directly in your version control system

Introduction to Ansible

Ansible has become the de facto standard for configuration management and automation in modern infrastructure. Unlike Puppet and Chef, which require agents on managed nodes, Ansible operates agentlessly over SSH. This architectural choice eliminates deployment overhead and reduces the attack surface of your infrastructure.

Where Puppet and Chef follow a pull-based model with agents checking in to a master server, Ansible uses push-based execution from a control node. This makes Ansible’s learning curve gentler and its operational model more straightforward. Salt offers similar agentless capabilities, but Ansible’s YAML-based playbooks are more readable than Salt’s Python-centric approach.

Ansible excels at three primary use cases: configuration management (ensuring systems are in desired states), application deployment (orchestrating multi-tier application rollouts), and task automation (executing ad-hoc commands across infrastructure). Its declarative syntax lets you describe what you want, not how to achieve it.

Core Concepts and Architecture

Understanding Ansible’s components is essential before writing automation code. The control node is where Ansible runs—typically your laptop or a CI/CD server. It requires Python and the Ansible package. Managed nodes are the servers you’re automating, requiring only Python and SSH access.

Inventory files define your infrastructure. They list hosts and organize them into groups. Here’s a simple INI-format inventory:

[webservers]
web1.example.com
web2.example.com

[databases]
db1.example.com
db2.example.com

[production:children]
webservers
databases

The same inventory in YAML format:

all:
  children:
    webservers:
      hosts:
        web1.example.com:
        web2.example.com:
    databases:
      hosts:
        db1.example.com:
        db2.example.com:
    production:
      children:
        webservers:
        databases:

Playbooks are YAML files containing automation instructions. Modules are the units of work—Ansible ships with thousands covering everything from package installation to cloud resource provisioning. Roles package related tasks, variables, and files into reusable components.

Ansible’s idempotency is critical. Run a playbook once or a hundred times—the result is identical. Modules check current state before making changes. If a package is already installed, Ansible skips installation. This property makes automation safe and predictable.

Writing Your First Playbook

Playbooks consist of one or more plays, each targeting specific hosts and containing tasks. Here’s a complete playbook to deploy nginx:

---
- name: Configure web servers
  hosts: webservers
  become: yes
  vars:
    nginx_port: 80
    server_name: example.com
  
  tasks:
    - name: Install nginx
      apt:
        name: nginx
        state: present
        update_cache: yes
      
    - name: Copy nginx configuration
      template:
        src: templates/nginx.conf.j2
        dest: /etc/nginx/sites-available/default
      notify: Restart nginx
    
    - name: Ensure nginx is running
      service:
        name: nginx
        state: started
        enabled: yes
  
  handlers:
    - name: Restart nginx
      service:
        name: nginx
        state: restarted

The template file templates/nginx.conf.j2 uses Jinja2 syntax:

server {
    listen {{ nginx_port }};
    server_name {{ server_name }};
    
    location / {
        root /var/www/html;
        index index.html;
    }
}

Handlers run only when notified and only once per playbook execution, regardless of how many tasks notify them. This prevents unnecessary service restarts.

Variables can come from multiple sources: playbook vars, inventory, command-line arguments, or discovered facts. Facts are system information Ansible gathers automatically:

- name: Display OS information
  debug:
    msg: "Running on {{ ansible_distribution }} {{ ansible_distribution_version }}"

Conditionals enable environment-specific logic:

- name: Install package manager specific packages
  apt:
    name: python3-pip
    state: present
  when: ansible_os_family == "Debian"

- name: Install package on RedHat systems
  yum:
    name: python3-pip
    state: present
  when: ansible_os_family == "RedHat"

Inventory Management and Host Patterns

Static inventories work for small, stable environments. For dynamic infrastructure, use inventory scripts or plugins. Here’s a comprehensive static inventory:

all:
  vars:
    ansible_user: deploy
    ansible_python_interpreter: /usr/bin/python3
  children:
    production:
      children:
        webservers:
          hosts:
            web1.prod.example.com:
              ansible_host: 10.0.1.10
            web2.prod.example.com:
              ansible_host: 10.0.1.11
          vars:
            nginx_worker_processes: 4
        databases:
          hosts:
            db1.prod.example.com:
              ansible_host: 10.0.2.10
          vars:
            postgresql_max_connections: 200
    staging:
      children:
        webservers:
          hosts:
            web1.staging.example.com:

For AWS environments, use the EC2 dynamic inventory plugin. Configure it in aws_ec2.yml:

plugin: aws_ec2
regions:
  - us-east-1
  - us-west-2
filters:
  tag:Environment: production
keyed_groups:
  - key: tags.Role
    prefix: role
  - key: placement.region
    prefix: region
hostnames:
  - tag:Name

Enable it in ansible.cfg:

[inventory]
enable_plugins = aws_ec2

Roles and Reusability

Roles enforce a standard directory structure that promotes reusability. Here’s a complete role for a web application:

roles/webapp/
├── tasks/
   └── main.yml
├── handlers/
   └── main.yml
├── templates/
   ├── app.conf.j2
   └── systemd.service.j2
├── files/
   └── app.tar.gz
├── vars/
   └── main.yml
├── defaults/
   └── main.yml
└── meta/
    └── main.yml

The tasks/main.yml:

---
- name: Create application user
  user:
    name: "{{ app_user }}"
    system: yes
    shell: /bin/false

- name: Create application directory
  file:
    path: "{{ app_path }}"
    state: directory
    owner: "{{ app_user }}"
    mode: '0755'

- name: Extract application
  unarchive:
    src: app.tar.gz
    dest: "{{ app_path }}"
    owner: "{{ app_user }}"

- name: Configure systemd service
  template:
    src: systemd.service.j2
    dest: "/etc/systemd/system/{{ app_name }}.service"
  notify: Restart application

Use roles in playbooks:

---
- name: Deploy web application
  hosts: webservers
  roles:
    - role: webapp
      vars:
        app_name: myapp
        app_user: myapp
        app_path: /opt/myapp

Ansible Galaxy hosts thousands of community roles. Install them with:

ansible-galaxy install geerlingguy.nginx

Advanced Features

Ansible Vault encrypts sensitive data. Create an encrypted file:

ansible-vault create secrets.yml

Store credentials:

db_password: supersecret
api_key: abc123xyz

Reference encrypted variables in playbooks:

- name: Configure database
  postgresql_db:
    name: myapp
    password: "{{ db_password }}"

Run playbooks with:

ansible-playbook site.yml --ask-vault-pass

Tags enable selective execution:

- name: Install packages
  apt:
    name: "{{ item }}"
  loop:
    - nginx
    - postgresql
  tags: packages

- name: Configure application
  template:
    src: app.conf.j2
    dest: /etc/app/config.yml
  tags: config

Run only configuration tasks:

ansible-playbook site.yml --tags config

Error handling provides control over failure behavior:

- name: Attempt optional configuration
  command: /opt/configure.sh
  ignore_errors: yes

- name: Check service status
  command: systemctl is-active myapp
  register: service_status
  failed_when: service_status.rc > 1

Best Practices and Production Tips

Structure projects for maintainability:

ansible/
├── ansible.cfg
├── inventories/
   ├── production/
      ├── hosts.yml
      └── group_vars/
          └── all.yml
   └── staging/
       ├── hosts.yml
       └── group_vars/
           └── all.yml
├── roles/
   ├── common/
   ├── webserver/
   └── database/
├── playbooks/
   ├── site.yml
   ├── webservers.yml
   └── databases.yml
└── files/

The ansible.cfg:

[defaults]
inventory = inventories/production
roles_path = roles
host_key_checking = False
retry_files_enabled = False
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts
fact_caching_timeout = 3600

[ssh_connection]
pipelining = True
control_path = /tmp/ansible-ssh-%%h-%%p-%%r

Test playbooks with ansible-lint:

ansible-lint playbooks/site.yml

Use Molecule for role testing:

molecule init role my-role
molecule test

In CI/CD pipelines, use check mode for validation:

ansible-playbook site.yml --check --diff

Enable pipelining and fact caching for performance. Limit concurrent forks based on your control node’s capacity. Use serial for rolling updates:

- name: Rolling update
  hosts: webservers
  serial: 2
  tasks:
    - name: Update application
      # tasks here

Version control everything except secrets. Use vault files for credentials and store the vault password in your CI/CD system’s secret management. Keep playbooks idempotent and test thoroughly in staging before production runs.

Ansible transforms infrastructure management from manual, error-prone processes into reliable, repeatable automation. Master these fundamentals, follow best practices, and you’ll build infrastructure as code that scales with your organization.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.