Stackvora.tech

Ansible: write your first playbook

Ansible playbooks are the cornerstone of automation for system administrators, network engineers, and developers alike. They define a set of tasks to be…

Ansible playbooks are the cornerstone of automation for system administrators, network engineers, and developers alike. They define a set of tasks to be executed on managed hosts, enabling idempotent configuration management, application deployment, and orchestration. This guide provides a foundational understanding of Ansible playbook structure, common modules, and best practices for authoring your first functional automation script.

At its core, an Ansible playbook is a YAML file containing one or more "plays," each targeting a specific group of hosts and defining a series of tasks to run on them. These tasks utilize Ansible modules to perform actions like installing packages, managing services, copying files, or executing shell commands.

Playbook Structure Fundamentals

An Ansible playbook always begins with a list of plays. Each play is a dictionary that includes parameters like hosts, become, and tasks. The basic structure looks like this:

---
- name: Web server setup and deployment
  hosts: webservers
  become: true
  vars:
    nginx_port: 80
    app_version: 1.0.0
  tasks:
    - name: Ensure Nginx is installed
      ansible.builtin.package:
        name: nginx
        state: present

    - name: Ensure Nginx service is running and enabled
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: true

    - name: Deploy custom Nginx configuration
      ansible.builtin.template:
        src: templates/nginx.conf.j2
        dest: /etc/nginx/nginx.conf
        owner: root
        group: root
        mode: '0644'
      notify: Restart Nginx

  handlers:
    - name: Restart Nginx
      ansible.builtin.service:
        name: nginx
        state: restarted

Key Components of a Play

  • ---: Marks the beginning of a YAML file.
  • name (optional, but highly recommended): A descriptive name for the play.
  • hosts: Specifies which hosts from your inventory file this play will target. Can be a group name (e.g., webservers), a single host (e.g., host1.example.com), or all.
  • become: A boolean (true/false) indicating whether Ansible should escalate privileges (e.g., use sudo) for tasks in this play. Defaults to false.
  • vars: A dictionary where you can define variables specific to this play.
  • tasks: A list of actions to be executed on the targeted hosts.
  • handlers: A list of tasks that are only run when explicitly notified by another task. Handlers are typically used for service restarts or reloads after configuration changes.

Inventory: Defining Your Targets

Before running a playbook, Ansible needs to know which hosts to connect to. This is defined in an inventory file, typically named inventory.ini or hosts. Inventory files can be static (INI or YAML) or dynamic (scripts that generate inventory on the fly).

[webservers]
web1.example.com
web2.example.com ansible_port=2222

[databases]
db1.example.com

[all:vars]
ansible_user=devops_user
ansible_ssh_private_key_file=~/.ssh/id_rsa

In this INI-formatted inventory:

  • [webservers] and [databases] define host groups.
  • Individual hostnames or IP addresses are listed under their respective groups.
  • Host-specific variables can be defined (e.g., ansible_port=2222 for web2.example.com).
  • [all:vars] defines variables that apply to all hosts in the inventory, such as the SSH user (ansible_user) and private key path (ansible_ssh_private_key_file).

Common Ansible Modules for Core Tasks

Ansible's power comes from its extensive collection of modules. Each module is designed to perform a specific function. Here are some fundamental modules you'll use frequently:

  • ansible.builtin.package: Manages packages (e.g., apt, yum, dnf, zypper).
    - name: Install Apache
          ansible.builtin.package:
            name: apache2
            state: present # present, absent, latest
  • ansible.builtin.service: Manages services (start, stop, restart, enable, disable).
    - name: Start and enable Apache service
          ansible.builtin.service:
            name: apache2
            state: started
            enabled: true
  • ansible.builtin.file: Manages files, directories, and symlinks (create, delete, change permissions/ownership).
    - name: Ensure /opt/app directory exists
          ansible.builtin.file:
            path: /opt/app
            state: directory
            mode: '0755'
            owner: appuser
            group: appuser
  • ansible.builtin.copy: Copies files from the control node to managed hosts.
    - name: Copy application binary
          ansible.builtin.copy:
            src: /path/to/local/app_binary
            dest: /usr/local/bin/my_app
            owner: root
            group: root
            mode: '0755'
  • ansible.builtin.template: Renders Jinja2 templates on the control node and copies the result to managed hosts. Ideal for configuration files with variables.
    - name: Deploy Nginx virtual host config
          ansible.builtin.template:
            src: templates/vhost.conf.j2
            dest: /etc/nginx/sites-available/{{ domain_name }}.conf
          notify: Reload Nginx
  • ansible.builtin.lineinfile: Ensures a particular line is present or absent in a file, or replaces a line matching a regular expression.
    - name: Ensure "PermitRootLogin no" is in sshd_config
          ansible.builtin.lineinfile:
            path: /etc/ssh/sshd_config
            regexp: '^#?PermitRootLogin'
            line: 'PermitRootLogin no'
            validate: '/usr/sbin/sshd -t' # Validate config before applying
          notify: Restart sshd
  • ansible.builtin.command / ansible.builtin.shell: Executes arbitrary commands. command is safer as it doesn't process shell features like piping or redirection; use shell when such features are required.
    - name: Check free disk space (command)
          ansible.builtin.command: df -h /
    
    - name: Run a complex script (shell)
          ansible.builtin.shell: |
            set -e
            if ! grep -q "my_setting" /etc/myconfig.conf; then
              echo "my_setting=value" >> /etc/myconfig.conf
            fi

Variables and Jinja2 Templating

Variables make playbooks dynamic and reusable. They can be defined in various places (inventory, play, role, command line). Ansible uses the Jinja2 templating language for processing variables within files. For example, if you have a variable nginx_port: 8080 defined, you can use it in a template (.j2 file) like this:

# templates/nginx.conf.j2
server {
    listen {{ nginx_port }};
    server_name {{ inventory_hostname }};
    root /var/www/html;
    index index.html;
    # ...
}

When the ansible.builtin.template module processes this, {{ nginx_port }} will be replaced with 8080, and {{ inventory_hostname }} with the actual hostname of the target machine.

Running Your Playbook

To execute a playbook, use the ansible-playbook command:

ansible-playbook -i inventory.ini my_first_playbook.yml

Common flags:

  • -i inventory.ini: Specifies the inventory file. Can also be a directory containing multiple inventory files.
  • -v, -vv, -vvv: Increase verbosity. Useful for debugging.
  • --check: Run in "check mode" (dry run) to see what changes would be made without actually making them.
  • --diff: When used with --check, shows the differences in files that would be changed.
  • --limit webservers: Restricts the playbook execution to a subset of hosts.
  • --syntax-check: Checks the playbook for syntax errors without executing it.
  • -e "nginx_port=80": Pass extra variables on the command line.

Idempotency: The Ansible Way

A core principle of Ansible is idempotency. This means that running a playbook multiple times should result in the same system state as running it once. If a resource (like a package or a service) is already in the desired state, Ansible should ideally do nothing, and report "ok" rather than "changed". Modules are designed to be idempotent; for example, installing a package that's already present will not re-install it, but merely confirm its presence.

Common Pitfalls

  • YAML Syntax Errors: YAML is whitespace-sensitive. Incorrect indentation is the most common cause of errors. Use a YAML linter if possible.
  • Privilege Escalation (become: true): Forgetting to specify become: true when tasks require root privileges, or not having the necessary sudo/su configuration on the managed hosts.
  • Inventory Issues: Incorrect hostnames, inaccessible hosts, or wrong SSH credentials (ansible_user, ansible_ssh_private_key_file). Always test connectivity first with ansible -i inventory.ini all -m ping.
  • Module vs. Shell: Using ansible.builtin.shell when a specific module (e.g., ansible.builtin.package, ansible.builtin.file) would be more appropriate and idempotent. Modules offer better error handling and idempotency guarantees.
  • Handler Not Notified: A handler will only run if a task explicitly notifies it, and that task reports a "changed" state. If a configuration file is unchanged, the corresponding service restart handler won't fire.
  • Path Issues: Always remember that paths in src parameters (for copy, template) are relative to the playbook directory on the control node, while paths in dest are on the managed host.

Back to the knowledge base · Ask the AI assistant