Ansible 101: Modularization & Debugging
Tags: ansible, devops, docker
Published: 2024-01-31 12:51:20

### Overview

When playbooks grow in size, they become challenging to manage. Modularization helps by separating logic from data, making playbooks cleaner and more maintainable. As you introduce these abstractions, it's crucial to integrate robust error handling and debugging techniques to ensure reliability.

### Roles


![Diagram showing roles directory structure](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ulv3pgy1l6slft4hfmia.png)



Roles in Ansible are units of organization that allow you to group related tasks, variables, files, and templates. They make your playbooks modular. Here’s how to create and use them:

* **Creating a Role:** Organize your playbook into a directory structure with separate files for tasks, handlers, defaults, vars, files, and templates.
    
* **Using Roles:** Incorporate a role into your playbook with the `roles` property or the `include_role` module for dynamic inclusion.
    

```yaml
- hosts: all
  roles:
    - role: my_custom_role

```

### Error Handling & Debugging

**Error Handling**

Ansible provides ways to define custom failure conditions and manage the playbook execution flow when encountering errors.

* **Block and Rescue:** Use `block` to group tasks and `rescue` to define remediation steps if any task in the block fails.
    

```yaml
tasks:
  - block:
      - name: Attempt to do something
        command: /bin/false
    rescue:
      - name: This will run on failure
        command: /bin/true
```

**Debugger**

Ansible’s debugger lets you interactively troubleshoot tasks that fail during playbook execution. It can be invoked by adding `debugger` to your playbook. Use one of the following values to control when the debugger is activated:

* `always`: always activate debugger
    
* `never`: never activate debugger
    
* `on_failed`: activate only on task failure
    
* `on_unreachable`: activate when a host is unreachable
    
* `on_skipped`: activate when a task is skipped
    

```yaml
- name: Install NGINX Web Server
  hosts: webservers
  tasks:
   - name: Install NGINX
     ansible.builtin.apt:
      name: httpd
      state: present
     debugger: on_failed

```

Core commands when working with the debugger include:

* `print`: display task details
    
* `task.args['arg-name'] = 'updated-val'`: update arg value
    
* `redo`: rerun the task
    
* `quit`: exit the debugger
    

## Practice

Our aim is to modularize the process of installing NGINX and updating the homepage. We also want to handle an error caused by incorrect package name and dynamically fix it.

1. Clone the repo
    

```bash
git clone https://github.com/perplexedyawdie/ansible-learn.git
```

2\. Spin up the environment using docker-compose

```bash
docker compose up -d --build
```

3\. SSH into the Ansible server

```bash
ssh -o StrictHostKeyChecking=no -o NoHostAuthenticationForLocalhost=yes root@localhost -p 2200# password: test123
```

4\. Change directory to `ansible_learn`

```bash
cd ansible_learn
```

### Roles & Modularization

5\. Create the roles directory with the relevant folders. For this task, we'll only require `tasks` , `templates` and `handlers`

```bash
cd ansible_learn

mkdir -p roles/nginx/tasks roles/nginx/templates roles/nginx/handlers

```

6\. Create the template that will generate the homepage.

Filename: `index.html.j2`

Location: `ansible_learn/roles/nginx/templates`

```Html
<html>
<head>
  <title>Welcome to {{ ansible_facts['os_family'] }}</title>
</head>
<body>
  <h1>Server running on {{ ansible_facts['distribution'] }}</h1>
</body>
</html>

```

7\. Create the tasks that will install NGINX, generate the homepage, and copy it to the server.

* Filename: `main.yaml`
    
* Location: `ansible_learn/roles/nginx/tasks`
    

```yaml
- name: Install NGINX for Debian-based systems   
  ansible.builtin.apt:
   name: nginx
   state: present
- name: Create Homepage with Jinja2 Template for NGINX
  ansible.builtin.template:
   src: index.html.j2
   dest: /var/www/html/index.html
   mode: '644'
  notify: restart nginx
   
```

8\. Create the handler that will restart NGINX upon update.

* Filename: `main.yaml`
    
* Location: `ansible_learn/roles/nginx/handlers`
    

```yaml
- name: Restart NGINX
  listen: "restart nginx"
  ansible.builtin.service:
   name: nginx
   state: restarted
```

9\. Create the playbook.

* Filename: `nginx_setup.yaml`
    
* Location: `ansible_learn`
    

```yaml
- name: Install NGINX
  hosts: all
  roles:
   - role: nginx    

```

10\. Run the linter

```bash
ansible-lint nginx_setup.yaml

```

11\. Execute the playbook.

```bash
ansible-playbook --key-file /root/.ssh/id_rsa_ansible -u root -i inventory.yaml nginx_setup.yaml

```

12\. Confirm deployment by visiting [http://localhost:2203](http://localhost:2203/) in your browser.

### Error Handling

The aim is to use `block` and `rescue` to print a custom message on error.

  
13\. Create a new playbook that should install Apache but use the wrong package name then add block & rescue properties.Filename: `error_test.yaml`Location: `ansible_learn`

```yaml
- name: Install Apache on Ubuntu
  hosts: all
  tasks:
    - name: Install Apache for Debian-based systems
      block:
        - name: Installer
          ansible.builtin.apt:
            name: httpd
            state: present
      rescue:
        - name: Installer errors
          ansible.builtin.debug:
            msg: "Error installing Apache on {{ ansible_facts['distribution'] }}"
```

14\. Run the linter

```bash
ansible-lint error_test.yaml

```

15\. Execute the playbook

```bash
ansible-playbook --key-file /root/.ssh/id_rsa_ansible -u root -i inventory.yaml error_test.yaml
```

### Debugging

The aim is to use the `debugger` and dynamically fix an error in a task.

  
16\. Update the `error_test.yaml` playbook and add the `debugger` property

```yaml
- name: Install Apache on Ubuntu
  hosts: all
  debugger: on_failed  
  tasks:
    - name: Install Apache for Debian-based systems
      block:
        - name: Installer
          ansible.builtin.apt:
            name: httpd
            state: present
      rescue:
        - name: Installer errors
          ansible.builtin.debug:
            msg: "Error installing Apache on {{ ansible_facts['distribution'] }}"
```

17\. Run the linter

```bash
ansible-lint error_test.yaml

```

18\. Execute the playbook

```bash
ansible-playbook --key-file /root/.ssh/id_rsa_ansible -u root -i inventory.yaml error_test.yaml

```

19\. In the interactive debugger, run the following commands to update the package name

```bash
# display all args in the failed task, we are interested in the name, since that contains the name of the package
p task.args

# update the package name. When installing Apache on Ubuntu, we use apache2
task.args['name'] = 'apache2'

# rerun the task
redo

# exit the debugger
quit
```

20\. Confirm successful installation of Apache

```bash
ssh -i /root/.ssh/id_rsa_ansible root@server1 apache2 -V

```

## Recap

Great effort! We learned how to create playbooks that are easier to extend and maintain along with how to handle errors and debug tasks to ensure reliability. For the final tutorial, we'll look at how to safely store sensitive data using Ansible Vault. Til then, take care! 👋