Software & Configuration

Ansible

Automate management of multiple VPS with Ansible, installation, inventory, playbooks and practical examples for server configuration

Ansible enables management of dozens of VPS simultaneously using YAML playbooks. It is agentless, using only SSH, with nothing to install on target servers. Ideal for infrastructure automation, configuration management, and deployment workflows.


Installation on Control Machine

Ubuntu / Debian

apt install ansible -y
ansible --version

CentOS / Rocky / AlmaLinux

dnf install ansible -y
ansible --version

Via pip (Latest Version)

pip install ansible --break-system-packages
ansible --version

Inventory: Define Your Servers

Create an inventory file (inventory.ini):

[webservers]
web1 ansible_host=192.168.1.10 ansible_user=root
web2 ansible_host=192.168.1.11 ansible_user=ubuntu ansible_become=yes

[dbservers]
db1 ansible_host=192.168.1.20 ansible_user=ubuntu ansible_become=yes

[all:vars]
ansible_ssh_private_key_file=~/.ssh/id_rsa
ansible_python_interpreter=/usr/bin/python3

Variables Explained

  • ansible_host: Server IP or hostname
  • ansible_user: SSH user
  • ansible_become=yes: Use sudo for commands
  • ansible_ssh_private_key_file: Path to SSH key

Test Connectivity

Test all servers:

ansible all -i inventory.ini -m ping

Run a command on all webservers:

ansible webservers -i inventory.ini -m command -a "uptime"

First Playbook: Install Nginx

Create install-nginx.yml:

---
- name: Install and start Nginx
  hosts: webservers
  become: yes
  tasks:
    - name: Update apt cache
      apt:
        update_cache: yes
      when: ansible_os_family == "Debian"

    - name: Install Nginx
      package:
        name: nginx
        state: present

    - name: Start and enable Nginx
      service:
        name: nginx
        state: started
        enabled: yes

    - name: Check Nginx status
      command: systemctl status nginx
      register: nginx_status

    - name: Show status
      debug:
        var: nginx_status.stdout_lines

Run the playbook:

ansible-playbook -i inventory.ini install-nginx.yml

Common Ansible Modules

Package Management

- name: Install packages
  package:
    name: "{{ item }}"
    state: present
  loop:
    - curl
    - wget
    - git
    - htop

Copy Files

- name: Copy config file
  copy:
    src: /local/path/nginx.conf
    dest: /etc/nginx/nginx.conf
    backup: yes

User Management

- name: Create user
  user:
    name: deploy
    shell: /bin/bash
    home: /home/deploy
    createhome: yes
    groups: sudo

- name: Set SSH key
  authorized_key:
    user: deploy
    state: present
    key: "{{ lookup('file', '/local/ssh/id_rsa.pub') }}"

Services

- name: Start service
  service:
    name: nginx
    state: started
    enabled: yes

- name: Restart on config change
  service:
    name: nginx
    state: restarted
  when: config_changed

Run Shell Commands

- name: Run custom script
  shell: |
    #!/bin/bash
    echo "Deploying..."
    ./deploy.sh
  args:
    executable: /bin/bash

Templates with Jinja2

Create templates/nginx.conf.j2:

server {
    listen {{ nginx_port }};
    server_name {{ server_hostname }};

    location / {
        proxy_pass http://{{ backend_server }};
    }
}

Use in playbook:

- name: Deploy Nginx config
  template:
    src: nginx.conf.j2
    dest: /etc/nginx/sites-available/default
    owner: root
    group: root
    mode: '0644'
  vars:
    nginx_port: 80
    server_hostname: example.com
    backend_server: 127.0.0.1:3000
  notify: Restart Nginx

handlers:
  - name: Restart Nginx
    service:
      name: nginx
      state: restarted

Handlers and Notifications

Handlers run only when notified:

- name: Update Nginx config
  copy:
    src: nginx.conf
    dest: /etc/nginx/nginx.conf
  notify: Restart Nginx

handlers:
  - name: Restart Nginx
    service:
      name: nginx
      state: restarted

Variables and Facts

Define Variables

---
- hosts: all
  vars:
    app_port: 3000
    app_user: deploy
  tasks:
    - name: Create app directory
      file:
        path: /opt/{{ app_name }}
        state: directory
        owner: "{{ app_user }}"

Register Variables

- name: Check if file exists
  stat:
    path: /etc/app.conf
  register: config_file

- name: Only run if file exists
  command: cat /etc/app.conf
  when: config_file.stat.exists

Use Facts

- name: Display system info
  debug:
    msg: |
      Hostname: {{ ansible_hostname }}
      OS: {{ ansible_os_family }}
      IP: {{ ansible_default_ipv4.address }}
      Memory: {{ ansible_memtotal_mb }} MB

Ansible Vault: Encrypt Secrets

Create encrypted file:

ansible-vault create secrets.yml

Edit encrypted file:

ansible-vault edit secrets.yml

Encrypt existing file:

ansible-vault encrypt vars/db_password.yml

Run playbook with vault:

ansible-playbook -i inventory.ini playbook.yml --ask-vault-pass

Or use vault password file:

ansible-playbook -i inventory.ini playbook.yml --vault-password-file ~/.vault_pass

Ansible Galaxy: Reusable Roles

Search community roles:

ansible-galaxy search docker

Install a role:

ansible-galaxy install geerlingguy.docker

Use in playbook:

---
- hosts: all
  roles:
    - geerlingguy.docker
    - geerlingguy.nginx

Create requirements.yml:

---
roles:
  - name: geerlingguy.docker
    version: 7.0.0
  - name: geerlingguy.nginx
    version: 4.0.0

Install all:

ansible-galaxy install -r requirements.yml

Complete Hardening Playbook Example

---
- name: Base server hardening
  hosts: all
  become: yes
  vars:
    ssh_port: 2222
    disable_root_login: yes
    firewall_allowed_ports:
      - 22
      - 80
      - 443
  tasks:
    - name: Update all packages
      apt:
        update_cache: yes
        upgrade: dist
      when: ansible_os_family == "Debian"

    - name: Install security tools
      package:
        name: "{{ item }}"
        state: present
      loop:
        - fail2ban
        - ufw
        - curl
        - git

    - name: Configure SSH security
      lineinfile:
        path: /etc/ssh/sshd_config
        regexp: "{{ item.regexp }}"
        line: "{{ item.line }}"
        state: present
      loop:
        - regexp: "^#?PermitRootLogin"
          line: "PermitRootLogin no"
        - regexp: "^#?PasswordAuthentication"
          line: "PasswordAuthentication no"
        - regexp: "^#?PubkeyAuthentication"
          line: "PubkeyAuthentication yes"
      notify: Restart SSH

    - name: Configure UFW firewall
      ufw:
        rule: allow
        port: "{{ item }}"
        proto: tcp
      loop: "{{ firewall_allowed_ports }}"

    - name: Enable UFW
      ufw:
        state: enabled
        policy: deny
        direction: incoming

    - name: Start fail2ban
      service:
        name: fail2ban
        state: started
        enabled: yes

    - name: Create deploy user
      user:
        name: deploy
        shell: /bin/bash
        home: /home/deploy
        createhome: yes
        groups: sudo

  handlers:
    - name: Restart SSH
      service:
        name: sshd
        state: restarted

Run with check mode (dry-run):

ansible-playbook -i inventory.ini hardening.yml --check --diff

Apply for real:

ansible-playbook -i inventory.ini hardening.yml

Dry Run and Diff

Always test before applying:

# Check mode (no changes)
ansible-playbook playbook.yml --check

# Show what would change
ansible-playbook playbook.yml --diff

# Both
ansible-playbook playbook.yml --check --diff

Running Playbooks Verbosely

# More verbose output
ansible-playbook playbook.yml -v

# Very verbose
ansible-playbook playbook.yml -vv

# Debug everything
ansible-playbook playbook.yml -vvv

Ansible is the industry standard for managing fleets of VPS. With playbooks, you can apply security updates, deploy applications, or configure 50 servers in minutes instead of hours.

Always test playbooks with --check and --diff before running in production. A syntax error or logic bug can impact all servers in your inventory. Keep vault password safe and never commit it to version control.

On this page