Ansible is an agentless configuration management tool that runs over SSH. Installing the same packages on 100 servers, distributing config files, starting services — all with a single command. It needs Python, but that's already installed on target servers.
Installation
# Ubuntu/Debian
sudo apt install -y ansible
# Or with pip
pipx install ansible
ansible --version
# Having an SSH key on target servers is all you need
ssh-copy-id root@web-1.example.com
Inventory
# inventory.ini
[web]
web-1.example.com
web-2.example.com
web-3.example.com ansible_host=1.2.3.4
[db]
db-1.example.com
db-2.example.com
[production:children]
web
db
[web:vars]
ansible_user=deploy
ansible_ssh_private_key_file=~/.ssh/deploy_key
# inventory.yml (alternative)
all:
children:
web:
hosts:
web-1.example.com:
web-2.example.com:
vars:
ansible_user: deploy
db:
hosts:
db-1.example.com:
Ad-Hoc Commands
# Ping all servers
ansible all -i inventory.ini -m ping
# Run a command
ansible web -i inventory.ini -m shell -a 'uptime'
# Install a package
ansible web -i inventory.ini -m apt -a 'name=htop state=present' --become
# Copy a file
ansible web -i inventory.ini -m copy -a 'src=./nginx.conf dest=/etc/nginx/nginx.conf'
Playbook — Basic Example
# site.yml
- name: Harden web servers
hosts: web
become: true
vars:
ssh_port: 22022
tasks:
- name: Apt update
apt:
update_cache: yes
upgrade: safe
cache_valid_time: 3600
- name: Required packages
apt:
name:
- fail2ban
- ufw
- nginx
- htop
state: present
- name: Change SSH port
lineinfile:
path: /etc/ssh/sshd_config
regexp: '^#?Port '
line: 'Port {{ ssh_port }}'
notify: restart sshd
- name: UFW rules
ufw:
rule: allow
port: '{{ item }}'
loop: ['{{ ssh_port }}', '80', '443']
- name: UFW enable
ufw:
state: enabled
policy: deny
handlers:
- name: restart sshd
service: { name: ssh, state: restarted }
# Run it
ansible-playbook -i inventory.ini site.yml
# Dry run
ansible-playbook -i inventory.ini site.yml --check --diff
# Single host only
ansible-playbook -i inventory.ini site.yml --limit web-1.example.com
# Resume from a specific task
ansible-playbook -i inventory.ini site.yml --start-at-task='SSH port'
Roles — Modular Structure
# Scaffold a role
ansible-galaxy init roles/nginx
# Structure
roles/nginx/
├── tasks/main.yml # main tasks
├── handlers/main.yml # service restarts etc.
├── templates/ # jinja2 templates
├── files/ # static files
├── vars/main.yml # role variables
└── defaults/main.yml # overridable defaults
# roles/nginx/tasks/main.yml
- name: Install Nginx
apt: { name: nginx, state: present }
- name: Config template
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
validate: 'nginx -t -c %s'
notify: reload nginx
- name: Service enabled
service:
name: nginx
state: started
enabled: yes
# roles/nginx/handlers/main.yml
- name: reload nginx
service: { name: nginx, state: reloaded }
# Main playbook
- hosts: web
become: true
roles:
- nginx
Jinja2 Templates
# roles/nginx/templates/nginx.conf.j2
worker_processes {{ ansible_processor_vcpus }};
worker_connections {{ worker_connections | default(1024) }};
{% for server in groups['backend'] %}
upstream backend {
server {{ hostvars[server].ansible_host }}:3000;
}
{% endfor %}
server {
listen 443 ssl;
server_name {{ server_name }};
ssl_certificate /etc/letsencrypt/live/{{ server_name }}/fullchain.pem;
}
Ansible Vault
# Encrypted secrets file
ansible-vault create secrets.yml
# editor opens, with content like:
# db_password: supersecret
# api_key: xyz
ansible-vault edit secrets.yml
ansible-vault encrypt existing.yml
ansible-vault decrypt encrypted.yml
# Use in a playbook
ansible-playbook site.yml --ask-vault-pass
ansible-playbook site.yml --vault-password-file=.vault_pass
Real World: Full Deploy Playbook
- hosts: web
become: true
vars_files:
- secrets.yml
tasks:
- name: Create app user
user:
name: app
home: /var/www/app
shell: /bin/bash
- name: Clone/update repo
git:
repo: 'git@github.com:user/app.git'
dest: /var/www/app
version: main
accept_hostkey: yes
become_user: app
notify: restart app
- name: NPM install
npm:
path: /var/www/app
production: yes
state: present
become_user: app
- name: Env file
template:
src: env.j2
dest: /var/www/app/.env
owner: app
mode: '0600'
- name: Run with PM2
command: pm2 startOrReload ecosystem.config.js
args: { chdir: /var/www/app }
become_user: app
handlers:
- name: restart app
command: pm2 restart ecosystem.config.js
become_user: app
Conclusion
Ansible is worth its weight in gold for any system with 5+ servers. It runs wherever SSH and Python exist, needs no agent and has a gentle learning curve. Terraform provisions infrastructure; Ansible configures that infrastructure. Together they form the two pillars of modern IaC.
Reach out to KEYDAL for multi-server management, playbook design and migration automation. Contact us