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.

Automate with Ansible

Reach out to KEYDAL for multi-server management, playbook design and migration automation. Contact us

WhatsApp