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
Related guides: What is DNS, settings · Domain names & WHOIS lookup · Hosting types guide · Nginx configuration · Plesk panel guide
# 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
Modern Web Hosting and Server Infrastructure
A performant web hosting service rests on three infrastructure decisions: NVMe SSD disks (4-6× IOPS over SATA SSD), LiteSpeed Web Server or Nginx + LSCache (9× request capacity over Apache) and CloudLinux + Imunify360 isolation. The hosting provider's control panel (cPanel, Plesk, DirectAdmin), daily backup policy, data center location and support response time make a big difference too. Turkish locations give low latency to local visitors, while Hetzner Frankfurt or OVH Roubaix suit global traffic. As your site grows, transitioning from shared hosting to VPS to dedicated server scales CPU/RAM/disk to your needs.
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