DEV Community

devtocash
devtocash

Posted on • Originally published at devtocash.com

Ansible Automation: Complete Linux Server Management Tutorial

💡 Originally published on devtocash.com — where this guide stays updated. I write hands-on DevOps/SRE deep-dives there weekly.

Introduction

SSH-ing into five servers to run the same commands is tedious. SSH-ing into fifty is impossible. Ansible solves this: define your server state in YAML, and Ansible applies it across your entire fleet—idempotently, in parallel.

Unlike Puppet or Chef, Ansible is agentless. It connects over SSH, runs commands, and cleans up. No daemons, no agent updates, no certificate management.

This tutorial takes you from zero to managing production Linux servers with Ansible. You'll write playbooks, organize roles, and automate common tasks like web server setup, database configuration, and monitoring agent deployment.

Installation and Basics

Install Ansible on your control machine (your laptop or a bastion host):

# Ubuntu/Debian
sudo apt install ansible

# macOS
brew install ansible

# Verify
ansible --version
Enter fullscreen mode Exit fullscreen mode

Define your inventory—the list of servers to manage:

# inventory.ini
[webservers]
web01 ansible_host=10.0.1.10 ansible_user=deploy
web02 ansible_host=10.0.1.11 ansible_user=deploy

[databases]
db01 ansible_host=10.0.2.10 ansible_user=deploy

[all:vars]
ansible_python_interpreter=/usr/bin/python3
Enter fullscreen mode Exit fullscreen mode

Test connectivity:

ansible all -i inventory.ini -m ping
# web01 | SUCCESS => {"changed": false, "ping": "pong"}
Enter fullscreen mode Exit fullscreen mode

Ad-Hoc Commands

Before writing playbooks, learn ad-hoc commands—quick, one-line operations:

# Check disk usage on all servers
ansible all -i inventory.ini -m shell -a "df -h /"

# Install a package
ansible webservers -i inventory.ini -m apt -a "name=nginx state=present" --become

# Restart a service
ansible webservers -i inventory.ini -m systemd -a "name=nginx state=restarted" --become
Enter fullscreen mode Exit fullscreen mode

The --become flag runs commands with sudo.

Writing Your First Playbook

Playbooks are YAML files that define a series of tasks:

# setup-web-server.yml
---
- name: Configure web servers
  hosts: webservers
  become: yes

  vars:
    nginx_port: 80
    app_domain: example.com

  tasks:
    - name: Update apt cache
      apt:
        update_cache: yes
        cache_valid_time: 3600

    - name: Install required packages
      apt:
        name:
          - nginx
          - certbot
          - python3-certbot-nginx
        state: present

    - name: Create web root directory
      file:
        path: /var/www/{{ app_domain }}
        state: directory
        owner: www-data
        group: www-data
        mode: '0755'

    - name: Deploy Nginx configuration
      template:
        src: nginx.conf.j2
        dest: /etc/nginx/sites-available/{{ app_domain }}
      notify: restart nginx

    - name: Enable site
      file:
        src: /etc/nginx/sites-available/{{ app_domain }}
        dest: /etc/nginx/sites-enabled/{{ app_domain }}
        state: link
      notify: restart nginx

  handlers:
    - name: restart nginx
      systemd:
        name: nginx
        state: restarted
Enter fullscreen mode Exit fullscreen mode

Run it:

ansible-playbook -i inventory.ini setup-web-server.yml
Enter fullscreen mode Exit fullscreen mode

Variables and Templates

Variables make playbooks reusable. Define them in multiple places with clear precedence:

# group_vars/webservers.yml
app_domain: example.com
nginx_worker_connections: 4096
enable_ssl: true
Enter fullscreen mode Exit fullscreen mode

Templates use Jinja2 syntax:

# templates/nginx.conf.j2
server {
    listen {{ nginx_port }};
    server_name {{ app_domain }};

    root /var/www/{{ app_domain }};
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }

    {% if enable_ssl %}
    listen 443 ssl;
    ssl_certificate /etc/letsencrypt/live/{{ app_domain }}/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/{{ app_domain }}/privkey.pem;
    {% endif %}
}
Enter fullscreen mode Exit fullscreen mode

Handlers

Handlers run only when notified—perfect for restarting services after config changes. They run once at the end of the play, even if notified multiple times.

Organizing with Roles

As your automation grows, split logic into roles:

ansible-galaxy init nginx
# Creates:
# nginx/
#   tasks/main.yml
#   handlers/main.yml
#   templates/
#   vars/main.yml
#   defaults/main.yml
#   meta/main.yml
Enter fullscreen mode Exit fullscreen mode

Role structure:

# roles/nginx/tasks/main.yml
---
- name: Install Nginx
  apt:
    name: nginx
    state: present

- name: Configure Nginx
  template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
  notify: restart nginx

- name: Ensure Nginx is running
  systemd:
    name: nginx
    state: started
    enabled: yes
Enter fullscreen mode Exit fullscreen mode

Use roles in your playbook:

---
- name: Production servers
  hosts: all
  become: yes
  roles:
    - common
    - nginx
    - node_exporter
Enter fullscreen mode Exit fullscreen mode

Real-World Example: Full Stack Setup

Here's a playbook that sets up a complete web application stack:

# site.yml
---
- name: Database tier
  hosts: databases
  become: yes
  roles:
    - role: postgresql
      vars:
        postgres_version: 16
        databases:
          - name: myapp
            user: myapp
            password: "{{ vault_db_password }}"

- name: Application tier
  hosts: webservers
  become: yes
  roles:
    - common
    - nginx
    - nodejs
  tasks:
    - name: Deploy application
      git:
        repo: https://github.com/your-org/myapp.git
        dest: /opt/myapp
        version: "{{ app_version }}"

    - name: Install dependencies
      npm:
        path: /opt/myapp
        production: yes

    - name: Start application with PM2
      command: pm2 start /opt/myapp/server.js --name myapp
Enter fullscreen mode Exit fullscreen mode

Manage secrets with Ansible Vault:

ansible-vault create group_vars/all/vault.yml
# Enter and confirm password, then add:
# vault_db_password: superSecret123

ansible-playbook site.yml -i inventory.ini --ask-vault-pass
Enter fullscreen mode Exit fullscreen mode

For CI/CD, store the vault password in your pipeline secrets and use --vault-password-file.

Roles: The Building Blocks of Ansible

Raw playbooks work for simple tasks. For real infrastructure management, use Ansible Roles to organize your automation into reusable components.

Role Directory Structure

ansible/
  roles/
    webserver/
      tasks/main.yml          # Main task list
      handlers/main.yml       # Restart/reload handlers
      templates/nginx.conf.j2 # Jinja2 templates
      vars/main.yml           # Role variables (high priority)
      defaults/main.yml       # Default variables (low priority)
      meta/main.yml           # Role dependencies
Enter fullscreen mode Exit fullscreen mode

Writing a Role

# roles/webserver/tasks/main.yml
---
- name: Install Nginx
  apt:
    name: nginx
    state: present
    update_cache: yes
  become: yes

- name: Deploy Nginx config
  template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
    owner: root
    group: root
    mode: '0644'
  notify: restart nginx
  become: yes
Enter fullscreen mode Exit fullscreen mode
# roles/webserver/handlers/main.yml
---
- name: restart nginx
  service:
    name: nginx
    state: restarted
  become: yes
Enter fullscreen mode Exit fullscreen mode

Using Roles

# site.yml
---
- hosts: all
  roles:
    - common

- hosts: webservers
  roles:
    - webserver
  vars:
    nginx_port: 443
Enter fullscreen mode Exit fullscreen mode

Run:

ansible-playbook -i inventory/production site.yml
Enter fullscreen mode Exit fullscreen mode

Advanced Playbook Patterns

Idempotent User Management

- name: Manage system users
  user:
    name: "{{ item.name }}"
    state: "{{ item.state | default('present') }}"
    groups: "{{ item.groups | default([]) }}"
    shell: "{{ item.shell | default('/bin/bash') }}"
  loop: "{{ system_users }}"
  become: yes
Enter fullscreen mode Exit fullscreen mode

With inventory data:

# group_vars/all.yml
system_users:
  - name: alice
    groups: ['sudo', 'docker']
  - name: bob
    groups: ['dev']
  - name: deploy-bot
    state: absent
Enter fullscreen mode Exit fullscreen mode

Rolling Updates with Serial

- name: Rolling Nginx update
  hosts: webservers
  serial: 2
  max_fail_percentage: 25
  tasks:
    - name: Deploy new config
      template:
        src: nginx.conf.j2
        dest: /etc/nginx/nginx.conf
      notify: restart nginx

    - name: Wait for health check
      uri:
        url: "https://{{ inventory_hostname }}/health"
        status_code: 200
      register: result
      until: result.status == 200
      retries: 10
      delay: 5
Enter fullscreen mode Exit fullscreen mode

Integrating with AWX (Ansible Tower)

AWX provides a web UI, RBAC, and job scheduling:

version: '3.8'
services:
  awx-web:
    image: ansible/awx:latest
    ports:
      - "80:80"
    depends_on:
      - postgres
      - redis
  postgres:
    image: postgres:15
    environment:
      POSTGRES_DB: awx
      POSTGRES_USER: awx
  redis:
    image: redis:7
Enter fullscreen mode Exit fullscreen mode

AWX Job Templates make playbooks reusable with different inventories and credentials:

Field Value
Name Deploy Web Servers
Inventory Production
Project ansible-infra
Playbook site.yml
Limit webservers

Schedule via cron, trigger via webhook on git push, or run on-demand via the API.

Ansible vs Other Tools

Feature Ansible Puppet Chef SaltStack
Architecture Agentless (SSH) Agent-based Agent-based Hybrid
Language YAML Ruby DSL Ruby DSL YAML/Python
Learning curve Low Medium High Medium
Idempotent Yes Yes Yes Yes
Cloud modules Extensive Moderate Moderate Good
Community Largest Large Medium Medium

Ansible's agentless architecture is its killer feature. No agents to install, update, or monitor. If SSH works, Ansible works. This alone saves countless hours of agent management across fleets of thousands of servers.

Dynamic Inventory for Cloud Environments

Static inventory files don't scale across hundreds of servers. Use dynamic inventory scripts to pull host data from cloud APIs:

ansible-inventory -i aws_ec2.yaml --graph
ansible-playbook -i aws_ec2.yaml site.yml
Enter fullscreen mode Exit fullscreen mode
# aws_ec2.yaml
plugin: amazon.aws.aws_ec2
regions:
  - us-east-1
  - us-west-2
keyed_groups:
  - key: tags.Environment
    prefix: env
  - key: tags.Role
    prefix: role
hostnames:
  - dns-name
  - private-dns-name
compose:
  ansible_host: private_ip_address
Enter fullscreen mode Exit fullscreen mode

Now target groups dynamically across regions:

# Target all production web servers in both regions
ansible-playbook -i aws_ec2.yaml site.yml --limit 'env_production:&role_webserver'
Enter fullscreen mode Exit fullscreen mode

Ansible Pull for Edge Devices

For IoT devices, edge nodes, or firewalled networks where push-based SSH doesn't work, use ansible-pull:

# Run on each node via cron job
ansible-pull -U https://github.com/company/ansible-infra.git \
    -i localhost, \
    site.yml
Enter fullscreen mode Exit fullscreen mode

The node pulls the latest playbook from git and applies itself. This gives centralized control on devices that can't be reached via SSH.

Applying Ansible to Linux Server Management

Base Server Setup Playbook

- name: Base server configuration
  hosts: all
  become: yes
  tasks:
    - name: Set timezone
      timezone:
        name: UTC

    - name: Install common packages
      apt:
        name:
          - htop
          - fail2ban
          - ufw
          - unattended-upgrades
        state: present

    - name: Configure firewall
      ufw:
        rule: allow
        port: "{{ item }}"
        proto: tcp
      loop: [22, 80, 443]

    - name: Harden SSH config
      lineinfile:
        path: /etc/ssh/sshd_config
        regexp: "^{{ item.key }}"
        line: "{{ item.key }} {{ item.value }}"
      loop:
        - {key: "PermitRootLogin", value: "no"}
        - {key: "MaxAuthTries", value: "3"}
      notify: restart sshd
Enter fullscreen mode Exit fullscreen mode

Multi-Tier Deployment Example

A complete deployment covering load balancers, app servers, and databases:

- hosts: loadbalancers
  roles:
    - haproxy
  vars:
    haproxy_backend_servers: "{{ groups.appservers }}"

- hosts: appservers
  roles:
    - nginx
    - nodejs-app
  vars:
    app_env: production
    app_port: 3000
  serial: 3

- hosts: databases
  roles:
    - postgresql
  vars:
    pg_version: 16
    pg_max_connections: 200
Enter fullscreen mode Exit fullscreen mode

Run the full stack:

ansible-playbook deploy.yml -i production -v
Enter fullscreen mode Exit fullscreen mode

Ansible handles the ordering: load balancers first, then app servers (rolling, 3 at a time), then databases.

Ansible Best Practices Cheat Sheet

Practice Why It Matters
Use roles for everything Reusability and readability
Keep playbooks short Playbooks should orchestrate roles, not contain tasks
Use --check before --diff Dry-run mode catches errors before they hit servers
Pin Ansible and collection versions Prevents breaking changes in CI/CD
Use ansible-lint in CI Enforces best practices automatically
Structure inventory by environment Prevents accidental production changes
Use serial for rolling updates Zero-downtime deployments

Related Reading

Conclusion

Ansible turns server management from a manual, error-prone process into reproducible code. Start with ad-hoc commands, graduate to playbooks, then organize with roles as complexity grows.

The real power isn't in the tool—it's in the mindset. Every server configuration becomes version-controlled, testable, and repeatable. No more "it works on my machine" because every machine is built the same way.


📌 Read the latest version of this guide — plus the full library of DevOps, SRE, Kubernetes, observability & cloud-cost guides — on devtocash.com.

Top comments (0)