Ansible Configuration Management 2026 — Automate Server Setup and Deployments
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
| Term | Meaning |
|---|---|
| Inventory | List of hosts to manage |
| Playbook | YAML file defining tasks to run on hosts |
| Task | Single action (install package, copy file, run command) |
| Role | Reusable, structured collection of tasks |
| Handler | Task triggered by notify — runs once at end of play |
| Module | Built-in action (apt, copy, service, template, k8s) |
| Vault | Encrypted 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 = sudoYour 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: 4Ansible 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_webserverGitHub 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-passUseful 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
--checkbefore 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_timeonapt— without it, Ansible updates the package cache on every run, adding minutes per host - Using
shell:when a module exists — prefernpm:,service:,user:,file:overshell:for idempotency and readability
Best Practices
- Use
--check --diffin CI on PRs to show what the playbook would change without executing anything - Pin collection and role versions in
requirements.ymlto 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: falseat the play level andbecome: trueonly 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 --diffmode 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
Related reading
Ansible Complete Guide — Agentless Configuration Management4 min readAI Tools for DevOps — Generate Dockerfiles, CI/CD Pipelines, and Kubernetes Manifests5 min readGitHub Actions — Complete CI/CD Guide 20266 min readGitLab CI/CD — Complete Pipeline Guide for 20265 min readJenkins Pipeline — Declarative CI/CD Syntax Complete Guide4 min readTerraform Complete Guide — Infrastructure as Code for Cloud Engineers5 min read