Best Ansible Playbook Examples for Beginners

Best Ansible Playbook Examples for Beginners

Profile-Image
Bright SEO Tools in saas Published: Apr 04, 2026 | Updated: Apr 04, 2026 · 2 months ago
0:00

Best Ansible Playbook Examples for Beginners

Manual server configuration breaks the moment you need to manage more than three servers. You SSH into each server, run installation commands, edit configuration files, restart services, and document what you did (if you remember). Then a server fails, you provision a replacement, and you can't remember all the steps. Or worse, you remember most steps but miss one critical configuration, and the new server behaves differently from the old ones.

Ansible solves this by letting you write server configuration as YAML files called playbooks. Instead of SSHing into servers and running commands manually, you run one ansible-playbook command that connects to all servers simultaneously, executes your defined tasks, and ensures every server reaches the same configuration state. This article provides working playbook examples for common server management tasks, from basic package installation through complete application deployments.

You'll learn Ansible's core concepts (inventory, modules, tasks, playbooks), see practical examples you can run immediately, and understand the patterns that make playbooks maintainable as your infrastructure grows. These aren't minimal "hello world" examples—they're production-ready configurations handling real-world scenarios.

Why Ansible for Configuration Management

Configuration management tools solve the problem of keeping servers in a known, consistent state. Ansible's specific advantages come from its agentless architecture and push-based model. Unlike tools that require installing agents on every server, Ansible only needs SSH access. Your control machine (laptop or CI server) connects via SSH and executes configuration commands. No agents to update, no version drift between control plane and managed nodes.

The push model means you run playbooks when you want changes applied, not on a schedule. Chef and Puppet use pull models where agents check for configuration updates every 30 minutes. With Ansible, you make a configuration change, run the playbook, and see results immediately. For emergency fixes or one-off tasks, this directness eliminates waiting for the next check-in interval.

Ansible's language is YAML—declarative, human-readable, and version-controllable. You describe desired state (nginx installed, configuration file present, service running), and Ansible determines the steps needed. Running the same playbook twice produces the same result because Ansible only makes changes when current state differs from desired state. This idempotency means playbooks are safe to run repeatedly.

Key Insight: Ansible playbooks are documentation that executes itself. The YAML file describing your server configuration is both the documentation of how servers should be configured and the automation that configures them. This eliminates documentation drift—the code is the documentation.

Ansible Core Concepts

Ansible operates on four foundational concepts: inventory defines which servers to manage, modules are the actions Ansible can perform, tasks combine modules with parameters, and playbooks organize tasks into reusable automation.

Inventory: Defining Managed Hosts

# inventory.ini
[webservers]
web1.example.com
web2.example.com
web3.example.com

[databases]
db1.example.com
db2.example.com

[production:children]
webservers
databases

[all:vars]
ansible_user=ubuntu
ansible_ssh_private_key_file=~/.ssh/production.pem

Inventory files list servers organized into groups. The [webservers] group contains three web servers, [databases] contains two database servers. The [production:children] group combines both into a higher-level grouping. The [all:vars] section sets variables that apply to all hosts—SSH username and private key location.

Modules: Ansible's Building Blocks

Modules are Ansible's library of actions. The apt module installs packages on Debian/Ubuntu systems, the yum module handles Red Hat/CentOS, the file module manages files and directories, the service module controls systemd/init services. Ansible includes hundreds of built-in modules covering most infrastructure tasks:

Module Purpose Example Use
apt/yum Package management Install nginx, update all packages
file File/directory operations Create directories, set permissions
copy/template File deployment Deploy config files, app code
service/systemd Service management Start/stop/restart services
user/group User management Create users, manage SSH keys
git Git operations Clone repos, checkout branches

Example 1: Basic Web Server Setup

This playbook installs nginx, deploys a basic configuration, and ensures the service is running. It's the foundation for most web server automation—install packages, configure, enable service.

Complete Playbook

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

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

    - name: Install nginx
      apt:
        name: nginx
        state: present

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

    - name: Deploy nginx configuration
      copy:
        content: |
          server {
              listen 80;
              server_name {{ inventory_hostname }};

              location / {
                  root /var/www/myapp;
                  index index.html;
              }
          }
        dest: /etc/nginx/sites-available/myapp
        owner: root
        group: root
        mode: '0644'
      notify: Reload nginx

    - name: Enable site
      file:
        src: /etc/nginx/sites-available/myapp
        dest: /etc/nginx/sites-enabled/myapp
        state: link
      notify: Reload nginx

    - name: Deploy index.html
      copy:
        content: |
          
          
          {{ inventory_hostname }}
          
              

Server: {{ inventory_hostname }}

Configured with Ansible

dest: /var/www/myapp/index.html owner: www-data group: www-data mode: '0644' - name: Ensure nginx is running service: name: nginx state: started enabled: yes handlers: - name: Reload nginx service: name: nginx state: reloaded

Running the Playbook

# Check syntax
ansible-playbook --syntax-check webserver.yml

# Run in check mode (dry run, no changes)
ansible-playbook --check webserver.yml -i inventory.ini

# Execute the playbook
ansible-playbook webserver.yml -i inventory.ini

# Output shows task execution status
PLAY [Configure web servers] ***********************************

TASK [Update apt cache] ****************************************
ok: [web1.example.com]
ok: [web2.example.com]

TASK [Install nginx] *******************************************
changed: [web1.example.com]
changed: [web2.example.com]

PLAY RECAP *****************************************************
web1.example.com : ok=8 changed=6
web2.example.com : ok=8 changed=6

The become: yes directive runs tasks with sudo privileges (required for package installation and system configuration). The notify directive triggers handlers only when the task makes changes—nginx reloads only if configuration changes, not on every playbook run. This efficiency prevents unnecessary service restarts.

Example 2: User and SSH Key Management

Managing user accounts and SSH keys manually across multiple servers is tedious and error-prone. This playbook creates user accounts, deploys SSH public keys, and configures sudo access—common tasks when onboarding new team members or provisioning new servers.

# users.yml
---
- name: Manage user accounts
  hosts: all
  become: yes

  vars:
    users:
      - name: alice
        ssh_key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQ... [email protected]"
        sudo: yes
      - name: bob
        ssh_key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDn... [email protected]"
        sudo: no

  tasks:
    - name: Create user accounts
      user:
        name: "{{ item.name }}"
        state: present
        shell: /bin/bash
        create_home: yes
      loop: "{{ users }}"

    - name: Deploy SSH authorized keys
      authorized_key:
        user: "{{ item.name }}"
        state: present
        key: "{{ item.ssh_key }}"
        exclusive: yes
      loop: "{{ users }}"

    - name: Configure sudo access
      copy:
        content: "{{ item.name }} ALL=(ALL) NOPASSWD:ALL"
        dest: "/etc/sudoers.d/{{ item.name }}"
        mode: '0440'
        validate: 'visudo -cf %s'
      loop: "{{ users }}"
      when: item.sudo

    - name: Create .ssh directory
      file:
        path: "/home/{{ item.name }}/.ssh"
        state: directory
        owner: "{{ item.name }}"
        group: "{{ item.name }}"
        mode: '0700'
      loop: "{{ users }}"

    - name: Disable password authentication
      lineinfile:
        path: /etc/ssh/sshd_config
        regexp: '^PasswordAuthentication'
        line: 'PasswordAuthentication no'
        state: present
      notify: Restart sshd

  handlers:
    - name: Restart sshd
      service:
        name: sshd
        state: restarted

The vars section defines users as a list of dictionaries, each containing name, ssh_key, and sudo status. The loop: "{{ users }}" directive iterates tasks over this list, creating accounts and deploying keys for each user. The when: item.sudo conditional runs the sudo configuration task only for users where sudo is true. The validate parameter in the sudoers copy task prevents invalid sudoers files from being deployed—Ansible tests the file with visudo before committing it.

Pro Tip: Use the exclusive: yes parameter in authorized_key to ensure only the specified keys are present. This prevents forgotten keys from lingering after users leave, a common security oversight when managing SSH access manually.

Example 3: Database Installation and Configuration

Database setup involves package installation, service configuration, security hardening, and initial database creation. This playbook handles PostgreSQL installation with proper security settings—changing default passwords, restricting network access, and configuring authentication.

# database.yml
---
- name: Configure PostgreSQL database servers
  hosts: databases
  become: yes

  vars:
    postgres_version: "14"
    postgres_password: "{{ vault_postgres_password }}"
    database_name: "production_db"
    database_user: "app_user"
    database_password: "{{ vault_database_password }}"

  tasks:
    - name: Install PostgreSQL
      apt:
        name:
          - postgresql-{{ postgres_version }}
          - postgresql-contrib
          - python3-psycopg2
        state: present
        update_cache: yes

    - name: Ensure PostgreSQL is running
      service:
        name: postgresql
        state: started
        enabled: yes

    - name: Configure PostgreSQL to listen on all interfaces
      lineinfile:
        path: /etc/postgresql/{{ postgres_version }}/main/postgresql.conf
        regexp: '^#?listen_addresses'
        line: "listen_addresses = '*'"
        state: present
      notify: Restart PostgreSQL

    - name: Configure client authentication
      blockinfile:
        path: /etc/postgresql/{{ postgres_version }}/main/pg_hba.conf
        block: |
          # Application server access
          host    {{ database_name }}    {{ database_user }}    10.0.0.0/8    md5
        marker: "# {mark} ANSIBLE MANAGED BLOCK"
      notify: Restart PostgreSQL

    - name: Set postgres user password
      postgresql_user:
        name: postgres
        password: "{{ postgres_password }}"
        encrypted: yes
      become_user: postgres

    - name: Create application database
      postgresql_db:
        name: "{{ database_name }}"
        encoding: UTF8
        lc_collate: en_US.UTF-8
        lc_ctype: en_US.UTF-8
      become_user: postgres

    - name: Create application user
      postgresql_user:
        name: "{{ database_user }}"
        password: "{{ database_password }}"
        encrypted: yes
        db: "{{ database_name }}"
        priv: ALL
      become_user: postgres

    - name: Grant schema privileges
      postgresql_privs:
        database: "{{ database_name }}"
        type: schema
        objs: public
        privs: ALL
        role: "{{ database_user }}"
      become_user: postgres

  handlers:
    - name: Restart PostgreSQL
      service:
        name: postgresql
        state: restarted

Managing Secrets with Ansible Vault

# Create encrypted vars file
ansible-vault create vars/secrets.yml

# Enter vault password, then edit file
---
vault_postgres_password: "secure-random-password-here"
vault_database_password: "another-secure-password"

# Run playbook with vault
ansible-playbook database.yml -i inventory.ini --ask-vault-pass

# Or use vault password file
echo "your-vault-password" > .vault_pass
ansible-playbook database.yml -i inventory.ini --vault-password-file .vault_pass

Ansible Vault encrypts sensitive variables, preventing passwords from existing in plaintext in your playbooks or version control. The vault_postgres_password variable references an encrypted value that Ansible decrypts at runtime. This pattern lets you safely commit playbooks to git without exposing credentials.

Example 4: Application Deployment with Git

Application deployment typically involves pulling code from git, installing dependencies, configuring environment variables, and restarting the application service. This playbook handles a Node.js application deployment with all these steps.

# deploy.yml
---
- name: Deploy Node.js application
  hosts: webservers
  become: yes

  vars:
    app_name: "myapp"
    app_user: "appuser"
    app_directory: "/opt/{{ app_name }}"
    git_repo: "https://github.com/company/myapp.git"
    git_branch: "main"
    node_version: "20"

  tasks:
    - name: Install Node.js
      shell: |
        curl -fsSL https://deb.nodesource.com/setup_{{ node_version }}.x | bash -
        apt-get install -y nodejs
      args:
        creates: /usr/bin/node

    - name: Create application user
      user:
        name: "{{ app_user }}"
        system: yes
        shell: /bin/bash
        home: "{{ app_directory }}"

    - name: Create application directory
      file:
        path: "{{ app_directory }}"
        state: directory
        owner: "{{ app_user }}"
        group: "{{ app_user }}"
        mode: '0755'

    - name: Deploy application from Git
      git:
        repo: "{{ git_repo }}"
        dest: "{{ app_directory }}"
        version: "{{ git_branch }}"
        force: yes
      become_user: "{{ app_user }}"
      notify: Restart application

    - name: Install Node.js dependencies
      npm:
        path: "{{ app_directory }}"
        production: yes
      become_user: "{{ app_user }}"
      notify: Restart application

    - name: Create environment file
      copy:
        content: |
          NODE_ENV=production
          PORT=3000
          DATABASE_URL={{ database_url }}
        dest: "{{ app_directory }}/.env"
        owner: "{{ app_user }}"
        group: "{{ app_user }}"
        mode: '0600'
      notify: Restart application

    - name: Create systemd service file
      copy:
        content: |
          [Unit]
          Description={{ app_name }} application
          After=network.target

          [Service]
          Type=simple
          User={{ app_user }}
          WorkingDirectory={{ app_directory }}
          ExecStart=/usr/bin/node {{ app_directory }}/server.js
          Restart=always
          RestartSec=10
          StandardOutput=journal
          StandardError=journal
          EnvironmentFile={{ app_directory }}/.env

          [Install]
          WantedBy=multi-user.target
        dest: "/etc/systemd/system/{{ app_name }}.service"
        mode: '0644'
      notify:
        - Reload systemd
        - Restart application

    - name: Enable and start application service
      service:
        name: "{{ app_name }}"
        enabled: yes
        state: started

  handlers:
    - name: Reload systemd
      systemd:
        daemon_reload: yes

    - name: Restart application
      service:
        name: "{{ app_name }}"
        state: restarted

The git module clones the repository and checks out the specified branch. The force: yes parameter discards local changes (important when deploying over previous versions). The npm module installs dependencies, with production: yes omitting devDependencies. Handlers ensure the application restarts only when code, dependencies, or configuration change—not on every playbook run.

Warning: The git module with force: yes overwrites local modifications without backup. For production deployments, use a more conservative approach: deploy to a releases directory with timestamps, symlink the current release, and keep previous releases for quick rollback.

Example 5: Firewall Configuration with UFW

Firewall rules protect servers from unauthorized access. This playbook configures UFW (Uncomplicated Firewall) with rules specific to each server role—web servers allow HTTP/HTTPS, database servers allow PostgreSQL from application servers only.

# firewall.yml
---
- name: Configure firewall on all servers
  hosts: all
  become: yes

  tasks:
    - name: Install UFW
      apt:
        name: ufw
        state: present

    - name: Set default incoming policy to deny
      ufw:
        direction: incoming
        policy: deny

    - name: Set default outgoing policy to allow
      ufw:
        direction: outgoing
        policy: allow

    - name: Allow SSH from anywhere
      ufw:
        rule: allow
        port: '22'
        proto: tcp
        comment: 'SSH access'

    - name: Enable UFW
      ufw:
        state: enabled

- name: Configure web server firewall rules
  hosts: webservers
  become: yes

  tasks:
    - name: Allow HTTP
      ufw:
        rule: allow
        port: '80'
        proto: tcp
        comment: 'HTTP access'

    - name: Allow HTTPS
      ufw:
        rule: allow
        port: '443'
        proto: tcp
        comment: 'HTTPS access'

- name: Configure database server firewall rules
  hosts: databases
  become: yes

  tasks:
    - name: Allow PostgreSQL from web servers
      ufw:
        rule: allow
        port: '5432'
        proto: tcp
        from_ip: "{{ hostvars[item]['ansible_default_ipv4']['address'] }}"
        comment: "PostgreSQL from {{ item }}"
      loop: "{{ groups['webservers'] }}"

    - name: Deny PostgreSQL from everywhere else
      ufw:
        rule: deny
        port: '5432'
        proto: tcp

This playbook uses multiple plays (the "---" separated sections). The first play runs against all hosts, setting baseline firewall rules. Subsequent plays add role-specific rules. The database firewall configuration demonstrates Ansible's host introspection—it queries IP addresses of web servers using hostvars and creates allow rules for each one. This dynamic rule generation prevents hardcoding IP addresses in playbooks.

Example 6: System Updates and Patching

Regular system updates are critical for security but disruptive if done manually. This playbook updates packages, handles kernel updates requiring reboots, and can be scheduled via cron for automated patching.

# system-update.yml
---
- name: System updates and patching
  hosts: all
  become: yes
  serial: 1

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

    - name: Upgrade all packages
      apt:
        upgrade: dist
        autoremove: yes
        autoclean: yes
      register: upgrade_result

    - name: Check if reboot is required
      stat:
        path: /var/run/reboot-required
      register: reboot_required

    - name: Display packages that were upgraded
      debug:
        msg: "{{ upgrade_result.stdout_lines }}"
      when: upgrade_result.changed

    - name: Reboot server if required
      reboot:
        msg: "Rebooting for kernel updates"
        connect_timeout: 5
        reboot_timeout: 300
        pre_reboot_delay: 0
        post_reboot_delay: 30
        test_command: uptime
      when: reboot_required.stat.exists

    - name: Wait for server to come back online
      wait_for_connection:
        connect_timeout: 20
        sleep: 5
        delay: 5
        timeout: 300
      when: reboot_required.stat.exists

    - name: Verify services are running
      service:
        name: "{{ item }}"
        state: started
      loop:
        - nginx
        - postgresql
      when: reboot_required.stat.exists
      ignore_errors: yes

The serial: 1 directive processes hosts one at a time rather than all simultaneously. This prevents updating all servers at once (which would cause downtime). The playbook updates packages, checks if a reboot is required (/var/run/reboot-required file exists on Ubuntu after kernel updates), reboots if needed, waits for the server to come back, and verifies critical services restarted. This automation makes patching routine rather than an emergency fire drill.

Playbook Organization Patterns

As playbooks grow, organizing them properly prevents the single-file maintenance nightmare. The standard pattern uses roles—reusable components containing tasks, handlers, files, templates, and variables organized in a consistent directory structure.

Converting Playbooks to Roles

# Directory structure
roles/
├── nginx/
│   ├── tasks/
│   │   └── main.yml
│   ├── handlers/
│   │   └── main.yml
│   ├── templates/
│   │   └── nginx.conf.j2
│   ├── files/
│   │   └── index.html
│   └── defaults/
│       └── main.yml
├── postgresql/
│   ├── tasks/
│   │   └── main.yml
│   ├── handlers/
│   │   └── main.yml
│   └── vars/
│       └── main.yml
└── common/
    ├── tasks/
    │   └── main.yml
    └── handlers/
        └── main.yml

# Main playbook using roles
# site.yml
---
- name: Configure all servers
  hosts: all
  become: yes
  roles:
    - common

- name: Configure web servers
  hosts: webservers
  become: yes
  roles:
    - nginx

- name: Configure database servers
  hosts: databases
  become: yes
  roles:
    - postgresql

Roles encapsulate related tasks, files, and variables. The nginx role contains all nginx-specific configuration, making it reusable across multiple playbooks. Variables in roles/nginx/defaults/main.yml set defaults that can be overridden per environment. Templates in roles/nginx/templates/ use Jinja2 for dynamic configuration generation.

Testing Playbooks

Running playbooks against production without testing is how infrastructure incidents happen. Ansible provides several testing mechanisms: check mode (dry run), syntax checking, molecule (automated testing framework), and staging environments.

Testing Workflow

# 1. Syntax check
ansible-playbook --syntax-check playbook.yml

# 2. Check mode (shows what would change, no modifications)
ansible-playbook --check playbook.yml -i inventory.ini

# 3. Diff mode (shows exact changes to files)
ansible-playbook --check --diff playbook.yml -i inventory.ini

# 4. Run against staging environment first
ansible-playbook playbook.yml -i staging-inventory.ini

# 5. After validation, run against production
ansible-playbook playbook.yml -i production-inventory.ini

Check mode simulates execution, showing which tasks would change without actually making changes. This catches obvious errors but isn't perfect—modules that depend on earlier tasks might show incorrect results because the earlier tasks didn't actually execute. The diff mode shows line-by-line changes to configuration files before applying them, useful for reviewing exactly what will change.

Pro Tip: Maintain separate inventory files for staging and production with identical structure but different hosts. Run playbooks against staging first to catch issues before they reach production. This staging-first workflow catches environment-specific problems that check mode misses.

Common Ansible Patterns

Certain patterns appear repeatedly in production playbooks. These solve common problems around conditionals, error handling, and dynamic configuration.

Conditional Execution

# Run task only on Ubuntu
- name: Install package
  apt:
    name: nginx
  when: ansible_distribution == "Ubuntu"

# Run task only in production
- name: Enable monitoring
  service:
    name: datadog-agent
    state: started
  when: environment == "production"

# Combined conditions
- name: Configure SSL
  template:
    src: ssl.conf.j2
    dest: /etc/nginx/ssl.conf
  when:
    - environment == "production"
    - enable_ssl is defined
    - enable_ssl | bool

Error Handling

# Ignore errors and continue
- name: Try to stop service (might not exist)
  service:
    name: old-service
    state: stopped
  ignore_errors: yes

# Fail if condition not met
- name: Check disk space
  shell: df -h / | awk 'NR==2 {print $5}' | sed 's/%//'
  register: disk_usage
  failed_when: disk_usage.stdout | int > 90

# Retry tasks that might fail temporarily
- name: Download large file
  get_url:
    url: https://example.com/largefile.tar.gz
    dest: /tmp/largefile.tar.gz
  retries: 3
  delay: 10

Dynamic Variables with Templates

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

    {% if enable_ssl %}
    listen 443 ssl;
    ssl_certificate {{ ssl_cert_path }};
    ssl_certificate_key {{ ssl_key_path }};
    {% endif %}

    location / {
        proxy_pass http://{{ backend_host }}:{{ backend_port }};
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

# Using the template
- name: Deploy nginx configuration
  template:
    src: nginx.conf.j2
    dest: /etc/nginx/sites-available/myapp
  notify: Reload nginx

FAQ

What's the difference between Ansible and Terraform?

Terraform provisions infrastructure (VPCs, instances, databases), while Ansible configures software on that infrastructure (install packages, deploy apps, configure services). They're complementary—use Terraform to create servers, then Ansible to configure them. Some overlap exists (Terraform can run provisioners, Ansible has cloud modules), but each tool excels at its primary purpose.

Do I need to install Ansible on every server?

No. Ansible is agentless and only needs SSH access to managed servers. Install Ansible on one control machine (your laptop, a CI server), and it connects to managed servers via SSH. The managed servers only need Python (pre-installed on most Linux distributions) and SSH server running.

How do I handle secrets in Ansible?

Use Ansible Vault to encrypt sensitive variables (passwords, API keys). Create encrypted variable files with ansible-vault create, then reference those variables in playbooks. Run playbooks with --ask-vault-pass or --vault-password-file to decrypt at runtime. Never commit plaintext secrets to version control.

Can Ansible manage Windows servers?

Yes, through WinRM instead of SSH. Install pywinrm on the control machine, configure WinRM on Windows servers, and use Windows-specific modules (win_package, win_service, win_file). Most core Ansible concepts apply, but module names differ because Windows APIs differ from Linux.

What's the difference between copy and template modules?

The copy module deploys files as-is without modification. The template module processes files through Jinja2 templating engine, replacing variables with actual values. Use copy for static files (images, binaries), template for configuration files that need variable substitution.

How do I run playbooks automatically on a schedule?

Add ansible-playbook commands to cron on your control machine. For example, daily system updates: 0 2 * * * /usr/bin/ansible-playbook /path/to/system-update.yml -i inventory.ini. For complex scheduling, use a CI/CD system (Jenkins, GitLab CI) that can run playbooks on schedules or git pushes.

Should I use handlers or notified tasks?

Handlers are deferred actions that run once at the end of a play, even if multiple tasks trigger them. This prevents unnecessary service restarts (five tasks modify nginx config, nginx restarts once, not five times). Use handlers for service restarts, reloads, and other actions that should happen only if something changed.

How do I debug playbook failures?

Add -v, -vv, or -vvv flags for increasing verbosity levels. Use the debug module to print variable values. Run playbooks with --start-at-task to resume from a specific task after fixing issues. Check ansible.log (if logging is enabled) for detailed execution information including SSH commands and module output.

Can multiple people run playbooks simultaneously?

Yes, but be cautious. Ansible doesn't lock hosts—two people running playbooks against the same hosts simultaneously can cause race conditions or configuration conflicts. For production, run playbooks through CI/CD pipelines that serialize execution, or use Ansible Tower/AWX which provides execution locking and scheduling.

How do I roll back changes if a playbook breaks something?

Ansible doesn't have built-in rollback. Your rollback is running the previous version of your playbook (via git revert). This is why version controlling playbooks is critical. For application deployments, implement blue-green or symlink-based patterns where you deploy to a new directory and switch atomically, keeping the old version for quick rollback.

Conclusion

Ansible playbooks transform manual server configuration into automated, reproducible processes. The examples in this article—web server setup, user management, database installation, application deployment, firewall configuration, and system updates—cover the most common infrastructure automation needs. Each playbook is idempotent (safe to run multiple times), maintainable (organized clearly with roles), and production-ready (handles errors and notifications properly).

Start with one manual task you do frequently—deploying application code, adding user accounts, updating packages—and convert it to an Ansible playbook. Test it in a safe environment, refine it based on what breaks, then expand to more complex scenarios. The investment in learning Ansible syntax pays off quickly because playbooks become both your automation and your documentation.

The goal isn't to automate everything immediately. It's to stop manually SSHing into servers for routine tasks. Every manual change you make is a potential inconsistency waiting to cause production issues. Playbooks prevent that by ensuring every server reaches the same state every time.


Share on Social Media: