devops

Ansible Automation: Complete Linux Server Management Tutorial

Automate Linux server configuration with Ansible. Playbooks, roles, variables, handlers, and real-world examples for web servers, databases, and monitoring.

July 1, 2026·9 min read·
#ansible#automation#linux#devops#configuration-management

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

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

Test connectivity:

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

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

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

Run it:

ansible-playbook -i inventory.ini setup-web-server.yml

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

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 %}
}

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

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

Use roles in your playbook:

---
- name: Production servers
  hosts: all
  become: yes
  roles:
    - common
    - nginx
    - node_exporter

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

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

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

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
# roles/webserver/handlers/main.yml
---
- name: restart nginx
  service:
    name: nginx
    state: restarted
  become: yes

Using Roles

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

- hosts: webservers
  roles:
    - webserver
  vars:
    nginx_port: 443

Run:

ansible-playbook -i inventory/production site.yml

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

With inventory data:

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

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

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

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

FieldValue
NameDeploy Web Servers
InventoryProduction
Projectansible-infra
Playbooksite.yml
Limitwebservers

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

Ansible vs Other Tools

FeatureAnsiblePuppetChefSaltStack
ArchitectureAgentless (SSH)Agent-basedAgent-basedHybrid
LanguageYAMLRuby DSLRuby DSLYAML/Python
Learning curveLowMediumHighMedium
IdempotentYesYesYesYes
Cloud modulesExtensiveModerateModerateGood
CommunityLargestLargeMediumMedium

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
# 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

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'

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

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

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

Run the full stack:

ansible-playbook deploy.yml -i production -v

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

Ansible Best Practices Cheat Sheet

PracticeWhy It Matters
Use roles for everythingReusability and readability
Keep playbooks shortPlaybooks should orchestrate roles, not contain tasks
Use --check before --diffDry-run mode catches errors before they hit servers
Pin Ansible and collection versionsPrevents breaking changes in CI/CD
Use ansible-lint in CIEnforces best practices automatically
Structure inventory by environmentPrevents accidental production changes
Use serial for rolling updatesZero-downtime deployments

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.

#ansible#automation#linux#devops#configuration-management
D
DevToCashAuthor

Senior DevOps/SRE Engineer · 10+ years · Professional Trader (IDX, Crypto, US Equities)

I write about real infrastructure patterns and trading strategies I use in production and in live markets. No courses, no affiliate hype — just documentation of what actually works.

More about me →