News
🛠️ DevOps Tutorials Automate Server Setup with an Ansible Playbook

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.