Ansible Configuration Management 2026: Automate Everything from Servers to Kubernetes
Advertisement
Ansible 2026: Infrastructure as Code Without the Code
Ansible automates everything from bare metal configuration to Kubernetes deployments. It requires no agents, uses SSH, and its playbooks read like documentation. In 2026, it remains the most pragmatic configuration management tool for teams that need results fast.
- Installation and Basic Concepts
- Your First Playbook
- Roles: Reusable, Structured Automation
- Ansible Vault: Encrypted Secrets
- Dynamic Inventory from AWS
- Kubernetes Module: Deploy to K8s with Ansible
- CI/CD Integration: GitHub Actions
- Useful Ansible Commands
Installation and Basic Concepts
# Install Ansible
pip install ansible --break-system-packages
# Or via package manager:
sudo apt install ansible # Debian/Ubuntu
# Verify
ansible --version
# Quick test: ping all hosts
ansible all -i "server1.com,server2.com," -m ping \
--user ubuntu --private-key ~/.ssh/id_rsa
# inventory/hosts.ini — Static inventory
[webservers]
web1.webcoderspeed.com ansible_user=ubuntu
web2.webcoderspeed.com ansible_user=ubuntu
[databases]
db1.webcoderspeed.com ansible_user=ubuntu ansible_port=2222
[staging]
staging.webcoderspeed.com ansible_user=ubuntu
[production:children]
webservers
databases
[all:vars]
ansible_python_interpreter=/usr/bin/python3
ansible_ssh_common_args='-o StrictHostKeyChecking=no'
# ansible.cfg — Project configuration
[defaults]
inventory = ./inventory
remote_user = ubuntu
private_key_file = ~/.ssh/id_rsa
host_key_checking = False
retry_files_enabled = 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"
nginx_port: 80
tasks:
- name: Update apt cache
apt:
update_cache: yes
cache_valid_time: 3600
- name: Install required packages
apt:
name:
- git
- curl
- nginx
- certbot
- python3-certbot-nginx
state: present
- name: Install Node.js via nvm
shell: |
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
export NVM_DIR="$HOME/.nvm"
source "$NVM_DIR/nvm.sh"
nvm install {{ node_version }}
nvm use {{ node_version }}
nvm alias default {{ node_version }}
args:
creates: "/root/.nvm/versions/node/v{{ node_version }}" # Skip if exists (idempotent)
- 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: Copy Nginx config
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
- 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 {{ nginx_port }};
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, Structured Automation
Roles are the right way to organize larger automation:
# Create role structure
ansible-galaxy role init roles/nodejs-app
# Generated structure:
# roles/nodejs-app/
# ├── tasks/
# │ └── main.yml
# ├── handlers/
# │ └── main.yml
# ├── templates/
# ├── files/
# ├── vars/
# │ └── main.yml
# ├── defaults/
# │ └── main.yml
# └── meta/
# └── main.yml
# roles/nodejs-app/tasks/main.yml
---
- name: Install PM2 globally
npm:
name: pm2
global: yes
state: present
- name: Copy 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: Copy PM2 ecosystem config
template:
src: ecosystem.config.js.j2
dest: "{{ app_dir }}/ecosystem.config.js"
owner: "{{ app_user }}"
- name: Start or reload app with PM2
command: pm2 reload ecosystem.config.js --update-env
args:
chdir: "{{ app_dir }}"
become_user: "{{ app_user }}"
register: pm2_result
failed_when: pm2_result.rc != 0
- name: Save PM2 process list
command: pm2 save
become_user: "{{ app_user }}"
- name: Setup PM2 startup
shell: pm2 startup | tail -1 | bash
when: setup_pm2_startup | default(true)
# roles/nodejs-app/defaults/main.yml
---
app_user: nodeapp
app_dir: /var/www/app
node_env: production
app_port: 3000
pm2_instances: max
setup_pm2_startup: true
local_app_path: "{{ playbook_dir }}/../"
# playbooks/deploy.yml — Use the role
---
- name: Deploy application
hosts: webservers
become: true
vars:
node_env: production
app_port: 3000
database_url: "{{ vault_database_url }}" # From Ansible Vault
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'
# Outputs:
# vault_database_url: !vault |
# $ANSIBLE_VAULT;1.1;AES256
# ...
# Encrypt a whole 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
# Or use a password file (for CI/CD)
echo "my-vault-password" > .vault-pass
chmod 600 .vault-pass
ansible-playbook deploy.yml --vault-password-file .vault-pass
# group_vars/production/secrets.yml (encrypted with vault)
---
vault_database_url: "postgresql://user:pass@db.host/myapp"
vault_redis_url: "redis://host:6379"
vault_jwt_secret: "super-secret-jwt-key"
vault_slack_webhook: "https://hooks.slack.com/services/..."
Dynamic Inventory from AWS
# Install AWS collection
ansible-galaxy collection install amazon.aws
# Configure dynamic inventory
# 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'"
# Test dynamic inventory
ansible-inventory -i inventory/aws_ec2.yml --list
ansible-inventory -i inventory/aws_ec2.yml --graph
# Use with playbooks
ansible-playbook -i inventory/aws_ec2.yml deploy.yml \
--limit role_webserver # Only hosts with tag Role=webserver
Kubernetes Module: Deploy to K8s with Ansible
# playbooks/k8s-deploy.yml
---
- name: Deploy to Kubernetes
hosts: localhost
gather_facts: false
collections:
- kubernetes.core
vars:
kubeconfig: "~/.kube/config"
namespace: production
image_tag: "{{ lookup('env', 'IMAGE_TAG') }}"
tasks:
- name: Create namespace
k8s:
api_version: v1
kind: Namespace
name: "{{ namespace }}"
state: present
- name: Create secret from vault variables
k8s:
definition:
apiVersion: v1
kind: Secret
metadata:
name: app-secrets
namespace: "{{ namespace }}"
type: Opaque
stringData:
DATABASE_URL: "{{ vault_database_url }}"
REDIS_URL: "{{ vault_redis_url }}"
state: present
- name: Deploy application
k8s:
definition: "{{ lookup('template', 'k8s/deployment.yml.j2') }}"
state: present
wait: yes
wait_timeout: 120
- name: Wait for rollout
k8s_rollout_info:
name: myapp
namespace: "{{ namespace }}"
kind: Deployment
register: rollout
until: rollout.conditions | selectattr('type', 'eq', 'Available') | list | length > 0
retries: 20
delay: 10
CI/CD Integration: GitHub Actions
# .github/workflows/ansible-deploy.yml
name: Ansible Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install Ansible
run: pip install ansible boto3
- name: Install Ansible collections
run: ansible-galaxy collection install -r requirements.yml
- name: Setup SSH key
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
- name: Run Ansible playbook
env:
ANSIBLE_HOST_KEY_CHECKING: "False"
VAULT_PASSWORD: ${{ secrets.ANSIBLE_VAULT_PASSWORD }}
run: |
echo "$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 (check mode) — no changes made
ansible-playbook deploy.yml --check
# Show diff of what would change
ansible-playbook deploy.yml --check --diff
# Limit to specific hosts
ansible-playbook deploy.yml --limit web1.example.com
# Run specific tags only
ansible-playbook deploy.yml --tags "nginx,app"
# Skip specific tags
ansible-playbook deploy.yml --skip-tags "packages"
# Run step by step (interactive)
ansible-playbook deploy.yml --step
# Increase verbosity
ansible-playbook deploy.yml -v # verbose
ansible-playbook deploy.yml -vvv # connection debug
ansible-playbook deploy.yml -vvvv # full debug
# 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"
Ansible's power is in its simplicity. Playbooks double as documentation. When a new engineer joins the team, reading the playbooks tells them exactly what every server is running and why. That clarity is worth more than any technical feature.
Advertisement