Ansible Basics #2 - Playbook

Last Edited: 11/16/2025

This blog post introduces the concept of playbook in Ansible.

DevOps

In the previous article, we covered the basic concepts and installation required to get started with Ansible. Hence, in this article, we will cover how to connect to managed nodes, run ad-hoc commands, and use playbooks for basic configuration.

Ad-Hoc Commands

Before running commands to apply configurations to managed nodes, we need to set up an inventory file on the control node. This file stores the IP addresses or hostnames of the nodes we are managing. To do this manually, we can create an inventory file in the local repository and simply list one IP address or hostname per row. (We can automatically generate or edit inventory files for better workflows.) We can confirm successful configurations by running the ad-hoc command ansible all --key-file <key_file_path> -i inventory -m ping, which uses the ping module to confirm SSH connection to the host(s).

ansible.cfg
[defaults]
inventory = inventory
private_key_file = <key_file_path>

However, running long commands like the above every time defeats the purpose of using a tool like Ansible. Hence, we can set up an ansible.cfg file in the local repository, where default parameters are stored and referenced. (We do have an ansible.cfg in the etc/ansible folder for global default settings, but it's generally recommended to set up a local ansible.cfg and configure local default settings to avoid unexpected behaviors.) After setting the configuration file, we can run the same ad-hoc command like ansible all -m ping. You can also use the gather_facts module to gather information about the servers, such as processor details, operating system, and environment variables. Use --limit <host> to extract information on a specific host, which can be useful for troubleshooting.

We can also start making changes to the servers using modules like apt, for example, ansible all -m apt -a "update_cache=true". However, the above command is likely to fail. This is because the ansible user on the managed nodes is not allowed to run the apt command without sudo. Therefore, we need to add --become and --ask-become-pass to provide the sudo password (which is assumed to be the same across the managed nodes) and run the apt command. The module can be used to install, uninstall, and upgrade packages depending on the arguments provided under the -a flag; explanations for these arguments are available in the Ansible documentation.

Playbook

Although ad-hoc commands can apply changes to all the servers at once and are convenient, we often need to run multiple commands to set up the servers, which might require many ad-hoc commands and make it difficult to keep track of the changes being made. The commands or modules required might also differ depending on the operating systems or Linux distributions, and dealing with different hosts is troublesome with ad-hoc commands. Hence, we often use a YAML file called a playbook to run multiple tasks.

install_apache.yaml
- hosts: all
  become: true
  tasks:
  - name: update repository index for Debian/Ubuntu
    apt:
        update_cache: yes
    when: ansible_distribution in ["Debian", "Ubuntu"]
 
  - name: install apache2 and php package for Ubuntu (22.0.4)
    apt:
        name: 
            - apache2
            - libapache2-mod-php
        state: latest
    when: ansible_distribution == "Ubuntu" and ansible_distribution_version = 22.0.4
 
  - name: install apache and php for CentOS
    dnf:
        name:
            - httpd
            - php
        state: latest
        update_cache: yes
    when: ansible_distribution == "CentOS"

The example playbook for installing Apache and PHP on Ubuntu and CentOS servers is shown above, demonstrating different functionalities of playbooks. The when statement is optional and is used to apply modules to different servers based on the fields in gather_facts, which provides access to operators like in and and, similar to Python. To apply changes based on the playbook, you can use ansible-playbook --ask-become-pass install_apache.yaml. After running the command, you should see that all the changes are successfully made. You can further rewrite the playbook to make it more concise, as shown below.

install_apache.yaml
- hosts: all
  become: true
  tasks:
  - name: install apache and php
    package:
        name:
            - "{{ apache_package }}"
            - "{{ php_package }}"
        state: latest
        update_cache: yes

The above utilizes the package module in Ansible, which runs the appropriate package manager depending on the operating system running on the server, and it also use templates for package names, which draw values from the inventory. Thus, the inventory must add key-value pairs like apache_package=httpd php_package=php for every host for this to work. It is debatable whether installing packages this way is better or not (personally, I prefer the first approach, which is more explicit and avoids hardcoding values in the inventory file), but they both demonstrate how convenient Ansible is in managing multiple nodes.

Conclusion

In this article, we covered the basics of ad-hoc commands and playbooks, which use modules to install different packages on the managed nodes. You can check the Ansible documentation cited below for details of the available modules. The article has already demonstrated the some convenience that Ansible offers in configuring multiple managed nodes with different operating systems and processors using just one or a few commands, but in the next article, we will cover more features that enable even more flexible configurations of managed nodes in Ansible.

Resources