Ansible Configuration Management 2026 — Automate Server Setup and Deployments

Sanjeev SharmaSanjeev Sharma
7 min read

Advertisement

Introduction

Why This Matters

Ansible automates everything from bare-metal server setup to Kubernetes deployments using YAML playbooks that are human-readable and idempotent. Unlike Terraform which provisions infrastructure, Ansible configures it — installing software, managing services, deploying applications. In 2026, it remains the most pragmatic configuration management tool for teams that need results without a steep learning curve.

Core Concepts

TermMeaning
InventoryList of hosts to manage
PlaybookYAML file defining tasks to run on hosts
TaskSingle action (install package, copy file, run command)
RoleReusable, structured collection of tasks
HandlerTask triggered by notify — runs once at end of play
ModuleBuilt-in action (apt, copy, service, template, k8s)
VaultEncrypted secrets store built into Ansible

Inventory and Configuration

# inventory/hosts.ini
[webservers]
web1.myapp.com ansible_user=ubuntu
web2.myapp.com ansible_user=ubuntu
 
[databases]
db1.myapp.com ansible_user=ubuntu ansible_port=2222
 
[production:children]
webservers
databases
 
[all:vars]
ansible_python_interpreter=/usr/bin/python3
ansible_ssh_common_args='-o StrictHostKeyChecking=no'
# ansible.cfg
[defaults]
inventory          = ./inventory
remote_user        = ubuntu
private_key_file   = ~/.ssh/id_rsa
host_key_checking  = False
stdout_callback    = yaml
gathering          = smart
fact_caching       = jsonfile
fact_caching_connection = /tmp/ansible-facts
fact_caching_timeout = 86400
 
[privilege_escalation]
become       = True
become_method = sudo

Your First Playbook

# playbooks/setup-webserver.yml
---
- name: Configure web server
  hosts: webservers
  become: true
 
  vars:
    node_version: "20"
    app_user: "nodeapp"
    app_dir: "/var/www/app"
 
  tasks:
    - name: Update apt cache and install packages
      apt:
        update_cache: yes
        cache_valid_time: 3600
        name:
          - git
          - curl
          - nginx
          - certbot
          - python3-certbot-nginx
        state: present
 
    - name: Create app user
      user:
        name: "{{ app_user }}"
        shell: /bin/bash
        system: yes
        create_home: yes
 
    - name: Create app directory
      file:
        path: "{{ app_dir }}"
        state: directory
        owner: "{{ app_user }}"
        group: "{{ app_user }}"
        mode: "0755"
 
    - name: Deploy Nginx config from template
      template:
        src: templates/nginx.conf.j2
        dest: /etc/nginx/sites-available/app
        mode: "0644"
      notify: reload nginx
 
    - name: Enable Nginx site
      file:
        src: /etc/nginx/sites-available/app
        dest: /etc/nginx/sites-enabled/app
        state: link
      notify: reload nginx
 
    - name: Remove default Nginx site
      file:
        path: /etc/nginx/sites-enabled/default
        state: absent
      notify: reload nginx
 
    - name: Ensure Nginx is running and enabled
      service:
        name: nginx
        state: started
        enabled: yes
 
  handlers:
    - name: reload nginx
      service:
        name: nginx
        state: reloaded
{# templates/nginx.conf.j2 #}
server {
    listen 80;
    server_name {{ ansible_fqdn }};
 
    location / {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_cache_bypass $http_upgrade;
    }
}

Roles: Reusable Automation

ansible-galaxy role init roles/nodejs-app
# Creates: tasks/, handlers/, templates/, files/, vars/, defaults/, meta/
# roles/nodejs-app/tasks/main.yml
---
- name: Install PM2 globally
  npm:
    name: pm2
    global: yes
    state: present
 
- name: Sync application code
  synchronize:
    src: "{{ local_app_path }}/"
    dest: "{{ app_dir }}/"
    rsync_opts:
      - "--exclude=node_modules"
      - "--exclude=.git"
      - "--exclude=.env"
  become_user: "{{ app_user }}"
 
- name: Install npm dependencies
  npm:
    path: "{{ app_dir }}"
    production: yes
  become_user: "{{ app_user }}"
 
- name: Copy environment file
  template:
    src: env.j2
    dest: "{{ app_dir }}/.env"
    owner: "{{ app_user }}"
    mode: "0600"
 
- name: Deploy with PM2
  command: pm2 reload ecosystem.config.js --update-env
  args:
    chdir: "{{ app_dir }}"
  become_user: "{{ app_user }}"
 
- name: Save PM2 process list
  command: pm2 save
  become_user: "{{ app_user }}"
# roles/nodejs-app/defaults/main.yml
---
app_user: nodeapp
app_dir: /var/www/app
node_env: production
app_port: 3000
pm2_instances: max
# playbooks/deploy.yml — Use the role
---
- name: Deploy application
  hosts: webservers
  become: true
 
  vars:
    node_env: production
    database_url: "{{ vault_database_url }}"
 
  roles:
    - role: nodejs-app
      vars:
        pm2_instances: 4

Ansible Vault: Encrypted Secrets

# Encrypt a single variable
ansible-vault encrypt_string 'postgresql://...' --name 'vault_database_url'
# Paste the output into your group_vars file
 
# Encrypt an entire file
ansible-vault encrypt group_vars/production/secrets.yml
 
# Edit encrypted file
ansible-vault edit group_vars/production/secrets.yml
 
# Run playbook with vault password
ansible-playbook deploy.yml --ask-vault-pass
 
# CI/CD: use a password file
echo "$ANSIBLE_VAULT_PASSWORD" > .vault-pass
chmod 600 .vault-pass
ansible-playbook deploy.yml --vault-password-file .vault-pass
rm .vault-pass
# group_vars/production/secrets.yml (encrypted)
---
vault_database_url: "postgresql://user:pass@db.host/myapp"
vault_redis_url: "redis://host:6379"
vault_jwt_secret: "super-secret-key"

Dynamic Inventory from AWS

# inventory/aws_ec2.yml
plugin: amazon.aws.aws_ec2
regions:
  - us-east-1
  - us-west-2
 
filters:
  tag:Environment: production
  instance-state-name: running
 
keyed_groups:
  - key: tags.Role
    prefix: role
  - key: placement.availability_zone
    prefix: az
 
compose:
  ansible_host: public_ip_address
  ansible_user: "'ubuntu'"
# Install AWS collection
ansible-galaxy collection install amazon.aws
 
# Test dynamic inventory
ansible-inventory -i inventory/aws_ec2.yml --list
 
# Run against only webserver-tagged instances
ansible-playbook -i inventory/aws_ec2.yml deploy.yml \
  --limit role_webserver

GitHub Actions CI/CD Integration

# .github/workflows/ansible-deploy.yml
name: Ansible Deploy
 
on:
  push:
    branches: [main]
 
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
 
      - name: Install Ansible and AWS SDK
        run: pip install ansible boto3
 
      - name: Install Ansible collections
        run: ansible-galaxy collection install -r requirements.yml
 
      - name: Configure SSH key
        run: |
          mkdir -p ~/.ssh
          echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
          chmod 600 ~/.ssh/id_rsa
 
      - name: Deploy
        env:
          ANSIBLE_HOST_KEY_CHECKING: "False"
        run: |
          echo "${{ secrets.ANSIBLE_VAULT_PASSWORD }}" > .vault-pass
          ansible-playbook \
            -i inventory/aws_ec2.yml \
            playbooks/deploy.yml \
            --vault-password-file .vault-pass \
            -e "image_tag=${{ github.sha }}"
          rm .vault-pass

Useful Ansible Commands

# Dry run — show what would change without changing anything
ansible-playbook deploy.yml --check --diff
 
# Limit to specific hosts
ansible-playbook deploy.yml --limit web1.example.com
 
# Run only tagged tasks
ansible-playbook deploy.yml --tags "nginx,app"
 
# Skip specific tags
ansible-playbook deploy.yml --skip-tags "packages"
 
# Increase verbosity for debugging
ansible-playbook deploy.yml -vvv
 
# Ad-hoc commands
ansible webservers -m service -a "name=nginx state=restarted" -b
ansible all -m shell -a "df -h"
ansible databases -m setup -a "filter=ansible_memtotal_mb"

Common Mistakes

  • Not using --check before applying — always dry-run against production to see exactly what will change
  • Non-idempotent tasks — every task should produce the same result whether run once or ten times; avoid shell: commands that create duplicates
  • Storing secrets in plaintext — always encrypt secrets with Ansible Vault before committing to the repository
  • No cache_valid_time on apt — without it, Ansible updates the package cache on every run, adding minutes per host
  • Using shell: when a module exists — prefer npm:, service:, user:, file: over shell: for idempotency and readability

Best Practices

  • Use --check --diff in CI on PRs to show what the playbook would change without executing anything
  • Pin collection and role versions in requirements.yml to prevent unexpected behavior from upstream updates
  • Use handlers for service restarts — they run only once at the end of a play even if notified multiple times
  • Use become: false at the play level and become: true only on specific tasks that genuinely need sudo
  • Playbooks double as runbooks — write task names as sentences so they read like documentation

Key Takeaways

  • Ansible is agentless — it connects over SSH and runs Python modules remotely; no daemon or agent installation required
  • Idempotency is the core principle — run a playbook 100 times and the result is always the same
  • Roles separate concerns: tasks, templates, handlers, and defaults live in predictable directories that any engineer can navigate
  • Ansible Vault encrypts secrets at the variable or file level, making it safe to commit secrets.yml to version control
  • Dynamic inventory from AWS EC2 eliminates manual host lists — targeting instances by tags is accurate and always current
  • Handlers run exactly once at the end of a play regardless of how many tasks notify them — prevents double restarts
  • --check --diff mode shows the exact changes a playbook would make without applying them — essential for production safety
  • Playbooks written with clear task names are self-documenting runbooks that new engineers can read and understand immediately

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading