Ansible Complete Guide — Agentless Configuration Management

Sanjeev SharmaSanjeev Sharma
4 min read

Advertisement

Introduction

Why This Matters

Ansible is the most popular agentless configuration management tool, used by thousands of organizations to automate server provisioning, application deployment, and infrastructure configuration. Because it uses SSH and requires no agent installation on managed nodes, it is easy to adopt and integrates naturally into existing infrastructure. Its YAML-based playbooks are readable by non-developers, making it ideal for cross-functional DevOps teams.

Installation and Inventory

# Install Ansible (macOS)
pip3 install ansible
 
# Verify version
ansible --version
 
# Static inventory file
cat > inventory.ini << 'EOF'
[webservers]
web1.example.com ansible_user=ubuntu
web2.example.com ansible_user=ubuntu
 
[databases]
db1.example.com ansible_user=ubuntu ansible_port=2222
 
[production:children]
webservers
databases
 
[all:vars]
ansible_ssh_private_key_file=~/.ssh/prod_key
EOF
 
# Test connectivity
ansible all -i inventory.ini -m ping
 
# Run ad-hoc command
ansible webservers -i inventory.ini -m command -a "uptime"
ansible databases -i inventory.ini -m shell -a "df -h"

Dynamic Inventory (AWS)

# Install AWS collection
ansible-galaxy collection install amazon.aws
 
# aws_ec2 dynamic inventory plugin
cat > aws_ec2.yaml << 'EOF'
plugin: amazon.aws.aws_ec2
regions:
  - us-east-1
filters:
  tag:Environment: production
keyed_groups:
  - key: tags.Role
    prefix: role
EOF
 
ansible-inventory -i aws_ec2.yaml --list
ansible all -i aws_ec2.yaml -m ping

Playbooks

---
# deploy-app.yml
- name: Deploy Node.js application
  hosts: webservers
  become: true
  vars:
    app_version: "{{ lookup('env', 'APP_VERSION') | default('latest') }}"
    app_port: 3000
    app_dir: /opt/my-app
 
  tasks:
    - name: Install Node.js dependencies
      apt:
        name:
          - nodejs
          - npm
          - nginx
        state: present
        update_cache: true
 
    - name: Create app directory
      file:
        path: "{{ app_dir }}"
        state: directory
        owner: ubuntu
        mode: '0755'
 
    - name: Copy application files
      copy:
        src: dist/
        dest: "{{ app_dir }}/"
        owner: ubuntu
        mode: '0644'
      notify: restart app
 
    - name: Install npm dependencies
      npm:
        path: "{{ app_dir }}"
        state: present
 
    - name: Configure nginx
      template:
        src: templates/nginx.conf.j2
        dest: /etc/nginx/sites-available/my-app
        mode: '0644'
      notify: reload nginx
 
    - name: Enable nginx site
      file:
        src: /etc/nginx/sites-available/my-app
        dest: /etc/nginx/sites-enabled/my-app
        state: link
 
  handlers:
    - name: restart app
      systemd:
        name: my-app
        state: restarted
 
    - name: reload nginx
      service:
        name: nginx
        state: reloaded

Roles

Roles provide reusable, structured Ansible automation:

# Create role structure
ansible-galaxy init roles/webserver
 
# Directory structure
roles/webserver/
  tasks/main.yml
  handlers/main.yml
  templates/
  files/
  vars/main.yml
  defaults/main.yml
  meta/main.yml
# roles/webserver/tasks/main.yml
---
- name: Install web packages
  apt:
    name: "{{ webserver_packages }}"
    state: present
    update_cache: true
 
- name: Start and enable nginx
  systemd:
    name: nginx
    state: started
    enabled: true
 
- name: Deploy site configuration
  template:
    src: site.conf.j2
    dest: "/etc/nginx/sites-available/{{ site_name }}"
  notify: reload nginx
 
# roles/webserver/defaults/main.yml
---
webserver_packages:
  - nginx
  - curl
 
site_name: default
site_port: 80
# Using the role in a playbook
---
- hosts: webservers
  become: true
  roles:
    - role: webserver
      vars:
        site_name: my-app
        site_port: 8080

Variables and Vault

# group_vars/production/vars.yml
db_host: prod-db.example.com
db_port: 5432
app_env: production
 
# group_vars/production/vault.yml (encrypted)
# Create with: ansible-vault create group_vars/production/vault.yml
vault_db_password: "super-secret-password"
vault_api_key: "my-api-key"
# Encrypt a file
ansible-vault encrypt group_vars/production/vault.yml
 
# Edit encrypted file
ansible-vault edit group_vars/production/vault.yml
 
# Run playbook with vault password
ansible-playbook deploy.yml --ask-vault-pass
ansible-playbook deploy.yml --vault-password-file ~/.vault_pass

Common Mistakes

  • Not using become: true when tasks require root — tasks fail silently or with permission errors
  • Forgetting update_cache: true in apt tasks when installing packages on fresh hosts
  • Using command module instead of specific modules (apt, service) — loses idempotency
  • Storing secrets as plain text in group_vars — always encrypt sensitive data with ansible-vault
  • Not testing playbooks with --check (dry run) mode before running against production

Best Practices

  • Use ansible-lint in CI to catch style issues and potential bugs before they reach production
  • Prefer specific modules (apt, yum, copy, template) over command/shell for idempotency
  • Group hosts logically in inventory and use group_vars and host_vars for variable scoping
  • Write roles for reusable components; publish to Ansible Galaxy for sharing across teams
  • Always run ansible-playbook --check (dry-run) before applying to production
  • Pin collection versions in requirements.yml to ensure reproducible automation

Key Takeaways

  • Ansible uses SSH and Python — no agent required on managed nodes, making adoption very low friction
  • Inventory can be static (.ini/.yaml) or dynamic (cloud plugins for AWS, GCP, Azure)
  • Playbooks are idempotent by design — running the same playbook multiple times produces the same result
  • Handlers only execute once at the end of a play, even if notified multiple times — ideal for service restarts
  • Roles structure automation into reusable components with clear directory conventions
  • ansible-vault encrypts sensitive variables at rest while keeping them in version control
  • group_vars and host_vars allow scoping variables to inventory groups or individual hosts
  • The --check flag performs a dry run showing what would change without modifying any systems

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading