Ansible Basics #3 - Playbook Cont'd

Last Edited: 12/4/2025

This blog post continues the discussion on features regarding playbook in Ansible.

DevOps

In the previous article, we covered the basic ad-hoc commands and playbooks for installing packages to all the managed nodes. However, there are more features for flexibly configuring servers that are often required in the real projects. Hence, in this article, we will continue the discussion on other features of Ansible playbooks.

Targetting Nodes & Tasks

In the previous article, we installed the same package on all nodes, but we often have different installation requirements for different servers, such as reverse proxies, web servers, database servers, and file servers. To run different commands for different sets of servers, we can group them in the inventory as shown below.

inventory
[web_servers]
253.78.154.178
84.246.7.193
 
[db_servers]
156.72.61.90

In the playbook, we can set the value of the hosts field to web_servers or db_servers to perform tasks on the specified nodes, such as installing Apache and PHP on web servers while installing PostgreSQL on the database servers. However, we might not want to perform those tasks simultaneously. To target the tasks to perform, we can add a tag field for each task, like apache and postgres, and run the ansible-playbook command with the --tags <tag> flag attached. For the tasks that need to be run regardless of tags specified, the special tag always can be utilized, and for the tasks that need to be run before all other tasks, we can use pre_tasks instead of tasks.

Files & Services

So far, we've been dealing with package managers and installing packages onto the nodes. However, we often need to copy local and remote files to the nodes, which we can do with the copy module, as shown below. We can specify the source file with src (the file name in the files directory in the same parent directory as the playbook), the destination with dest, and user and group permissions with other fields.

web.yaml
...
  # Copy local html
  - name: copy html file
    tags: apache
    copy:
      src: site.html
      dest: /var/www/html/index.html
      owner: root
      group: root
      mode: 0644
 
  # Install unzip & Install and unzip remote file
  - name: install unzip
    package: 
      name: unzip
 
  - name: install terraform
    unarchive:
      src: https://releases.hashicorp.com/terraform/0.12.28/terraform_0.12.28_linux_amd64.zip
      dest: /usr/local/bin
      remote_src: yes
      owner: root
      group: root
      mode: 0755
...

We can also install and unzip remote archives, such as Terraform (although Terraform is often required for a local workstation rather than a web server). Copying and installing files onto the managed nodes enables much more than just installing packages. In addition, some packages, like httpd, require systemctl to start the service on certain nodes, which can be achieved using the service module.

web.yaml
...
  - name: start httpd (CentOS)
    tags: apache
    service: 
      name: httpd
      state: started
      enabled: yes
    when: ansible_distribution == "CentOS"
 
  - name: change http config
    tags: apache
    lineinfile: 
      path: /etc/httpd/conf/httpd.conf
      regexp: '^ServerAdmin'
      line: ServerAdmin example@exmaple.com
    when: ansible_distribution == "CentOS"
    register: httpd
 
  - name: restart httpd upon config change
    tags: apache
    service:
      name: httpd
      state: restarted
    when: httpd.changed
...

The first task starts the httpd service and ensures that the service is enabled, even if the service is already started and somehow disabled. The second task uses the lineinfile module to apply changes to httpd.conf. The httpd variable is registered for the third task to be executed upon change in configuration, detectable with httpd.changed. It's important to note that httpd.changed returns true only when the immediately preceding task with httpd registered is performed. (If there's another task in between with httpd registered, and it didn't run, httpd.changed will evaluate to false.)

Users & Boostrapping

So far, we've been using --ask-become-pass and providing the sudo password in every command execution to run sudo commands on the managed nodes. However, we can avoid this flag by creating a new user for Ansible (that isn't the default user), registering an SSH key for the user, adding the user to the sudoers file, and adding remote_user = <usr_name> in ansible.cfg. We can do these processes manually for each node or utilize and bootstrap with an Ansible playbook.

boostrap.yml
- hosts: all
  become: true
 
  vars:
    ansible_manager_user: ansible_user
    ssh_public_key: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"
 
  tasks:
  - name: create ansible user
    tags: user
    user: 
      name: "{{ ansible_manager_user }}"
      group: "{{ 'wheel' if ansible_os_family == 'RedHat' else 'sudo' }}"
      create_home: true
      shell: /bin/bash
 
  - name: add ssh key for ansible
    tags: user
    authorized_key:
      user: "{{ ansible_manager_user }}"
      key: "{{ ssh_public_key }}"
 
  - name: add sudoer file for ansible
    tags: user
    copy:
      src: sudoer_ansible # 'ansible ALL=(ALL) NOPASSWD: ALL' in it
      dest: /etc/sudoers.d/{{ ansible_manager_user }}
      owner: root
      group: root
      mode: 0440
      validate: /usr/sbin/visudo -cf %s

The above bootstrap.yaml playbook utilizes the vars field, user, authorized_key, and copy modules to set up the user, its SSH key, and sudoer privileges. We can execute the playbook with flags like -u <initial_user>, --ask-pass, and --ask-become-pass, and edit ansible.cfg to set the default user to be ansible_user. After running this successfully, we can run future playbooks without --ask-become-pass and providing the sudo password.

Conclusion

In this article, we covered how we can target specific nodes and tasks, manage files and services, and set up users in Ansible playbooks. These capabilities enable much more flexible and convenient server configurations for real-world applications. However, the playbooks are getting longer and messier. Hence, in the next article, we're going to cover the additional features that we can utilize to cleanly organize the required components in Ansible.

Resources