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), orall.become: A boolean (true/false) indicating whether Ansible should escalate privileges (e.g., usesudo) for tasks in this play. Defaults tofalse.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=2222forweb2.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, latestansible.builtin.service: Manages services (start, stop, restart, enable, disable).- name: Start and enable Apache service ansible.builtin.service: name: apache2 state: started enabled: trueansible.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: appuseransible.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 Nginxansible.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 sshdansible.builtin.command/ansible.builtin.shell: Executes arbitrary commands.commandis safer as it doesn't process shell features like piping or redirection; useshellwhen 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 specifybecome: truewhen tasks require root privileges, or not having the necessarysudo/suconfiguration 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 withansible -i inventory.ini all -m ping. - Module vs. Shell: Using
ansible.builtin.shellwhen 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
srcparameters (forcopy,template) are relative to the playbook directory on the control node, while paths indestare on the managed host.