Automate Server Setup with an Ansible Playbook
Install packages, create users, deploy config files, and run commands across one or many servers — repeatably, from a single YAML file.
The problem with setting up servers manually is that the second server never quite matches the first, and six months later no one remembers what was installed or why. Ansible solves this by describing the desired state in a YAML file and making the server match it. Running the playbook twice leaves the server in the same state as running it once.
How Ansible works
Ansible connects to servers over SSH, pushes small Python scripts, runs them, and removes them. There is no agent to install on the managed servers, no daemon to keep running. Your workstation (or CI server) is the only machine that needs Ansible.
Install Ansible
pip install ansible # recommended: use a virtualenv
# or
sudo apt install ansible # Debian / Ubuntu system package
brew install ansible # macOS
The inventory
Ansible needs to know which servers to manage. Create an inventory.ini:
[webservers]
web1 ansible_host=192.168.1.10
web2 ansible_host=192.168.1.11
[databases]
db1 ansible_host=192.168.1.20
[all:vars]
ansible_user=deploy
ansible_ssh_private_key_file=~/.ssh/id_ed25519
Test connectivity:
ansible all -i inventory.ini -m ping
A green pong means Ansible can reach and authenticate to every host. Fix SSH issues here before writing any playbooks.
A minimal playbook
# setup.yml
---
- name: Configure webservers
hosts: webservers
become: true # run tasks as root (via sudo)
tasks:
- name: Update package cache
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
- name: Install required packages
ansible.builtin.apt:
name:
- nginx
- git
- fail2ban
state: present
- name: Ensure nginx is running and enabled
ansible.builtin.service:
name: nginx
state: started
enabled: true
Run it:
ansible-playbook -i inventory.ini setup.yml
Ansible runs each task in order on every host in the target group. Tasks are idempotent — "install nginx" means "ensure nginx is installed," not "run apt-get install nginx every time." If nginx is already installed, the task reports ok and moves on.
Variables
Hardcoding values into playbooks makes them brittle. Use variables instead:
# setup.yml
---
- name: Configure webservers
hosts: webservers
become: true
vars:
app_user: deploy
app_dir: /var/www/app
nginx_port: 80
tasks:
- name: Create application directory
ansible.builtin.file:
path: "{{ app_dir }}"
state: directory
owner: "{{ app_user }}"
mode: "0755"
Variables defined in vars: are available with {{ variable_name }} anywhere in the playbook. You can also put variables in separate files:
vars_files:
- vars/main.yml
Or override them at the command line:
ansible-playbook -i inventory.ini setup.yml -e "app_user=www-data"
Deploying a config file with a template
Ansible's template module renders a Jinja2 template and copies the result to the server:
# templates/nginx.conf.j2
server {
listen {{ nginx_port }};
server_name {{ server_name }};
root {{ app_dir }};
index index.html;
}
- name: Deploy nginx config
ansible.builtin.template:
src: templates/nginx.conf.j2
dest: /etc/nginx/sites-available/app.conf
owner: root
mode: "0644"
notify: Reload nginx
handlers:
- name: Reload nginx
ansible.builtin.service:
name: nginx
state: reloaded
Handlers run at the end of a play, but only if the task that notify-ed them reported a change. If the config file was already correct and unchanged, nginx is not reloaded. If the file changed, nginx is reloaded once — even if ten tasks all notified the same handler.
Check mode: dry run
See what Ansible would change without making any changes:
ansible-playbook -i inventory.ini setup.yml --check
Tasks that would make changes report changed. Tasks that find the system already in the desired state report ok. Useful for verifying a playbook before running it on a production server.
A complete example: user, packages, firewall
---
- name: Baseline server setup
hosts: all
become: true
vars:
deploy_user: deploy
packages:
- git
- curl
- ufw
- fail2ban
tasks:
- name: Create deploy user
ansible.builtin.user:
name: "{{ deploy_user }}"
shell: /bin/bash
groups: sudo
append: true
- name: Install packages
ansible.builtin.apt:
name: "{{ packages }}"
state: present
update_cache: true
- name: Allow SSH through firewall
community.general.ufw:
rule: allow
name: OpenSSH
- name: Enable firewall
community.general.ufw:
state: enabled
policy: deny
- name: Enable fail2ban
ansible.builtin.service:
name: fail2ban
state: started
enabled: true
Run this against a fresh server and you have a consistent, documented baseline in under two minutes. Run it again a month later and Ansible confirms everything is still as expected — or corrects any drift.
SysEmperor