DEV Community

Cover image for Migrating Playbooks to Ansible Roles
Suraj P
Suraj P

Posted on

Migrating Playbooks to Ansible Roles

If you've worked with Ansible for a while, you've probably noticed that as your playbooks grow more complex, managing them can become a bit messy.

Why Migrate to Roles?

Migrating playbooks into roles is one way to clean things up, making them easier to reuse and maintain. Think of roles as building blocks. Instead of having all the tasks bundled together in a single playbook, we break them down into smaller, logical chunks. These chunks are easier to manage and can be reused across multiple projects.

Ansible Roles

Roles let us automatically load related vars, files, tasks, handlers, and other Ansible artifacts based on a known file structure. After the content is grouped into roles, they can easily be reused.

ansible-role/
├── tasks/
│   └── main.yml
├── handlers/
│   └── main.yml
├── templates/
│   └── conf.j2
├── files/
│   └── index.html
├── vars/
│   └── main.yml
├── defaults/
│   └── main.yml
└── meta/
    └── main.yml
Enter fullscreen mode Exit fullscreen mode

Structure of Ansible Roles

Ansible roles are organized in a specific directory structure, which typically includes the following parts:

Tasks
Contains the main tasks that the role will execute.
It typically has a main.yml file here that lists all the tasks in the role.

Handlers
Contains handler definitions that can be notified by tasks.
Handlers are typically used for actions that should only run when triggered (e.g., restarting a service).

Templates
Contains Jinja2 template files that can be dynamically rendered with variables. Useful for configuration files that need to be customized for each deployment.

Files
Contains static files that we want to copy to the target machine.
This can include scripts, configuration files, or any other files that need to be deployed.

Vars
Contains variable definitions that can be used within the role.
Variables defined here can be referenced in tasks, handlers, and templates.

Defaults
Contains default variable values that can be overridden by inventory/playbook vars when the role is called. These variables typically have lower precedence than those in vars.

Meta
Contains metadata about the role, such as dependencies on other roles. We can define required Ansible versions or other role dependencies here.

Tests
(Optional) Contains test playbooks and other files for testing the role. Useful for integration and acceptance testing.

Example Playbook: Configuring Nginx on Webservers

To demonstrate the migration, let's take a common scenario: setting up Nginx and hosting a static webpage. Here’s a simple playbook that installs Nginx, sets up its configuration, and ensures the service is running.

---
- hosts: webservers
  become: yes
  vars:
    root_directory: /var/www/html
    nginx_port: 80
    server_name: example.com

  tasks:
    - name: Install Nginx
      apt:
        name: nginx
        state: present

    - name: Ensure web root directory exists
      file:
        path: "{{ root_directory }}"
        state: directory

    - name: Copy index.html to web root
      copy:
        src: index.html
        dest: "{{ root_directory }}/index.html"

    - name: Template Nginx configuration file
      template:
        src: nginx.conf.j2
        dest: /etc/nginx/sites-available/default
      notify: restart nginx

    - name: Ensure Nginx is enabled and started
      service:
        name: nginx
        enabled: true
        state: started

  handlers:
    - name: restart nginx
      service:
        name: nginx
        state: restarted
Enter fullscreen mode Exit fullscreen mode

Now, let us try migrating this playbook to an Ansible role. Use the ansible-galaxy command to create a new role.

ansible-galaxy init nginx_webserver

This will create a file structure similar to this:

nginx_webserver/
├── defaults/
│   └── main.yml
├── files/
├── handlers/
│   └── main.yml
├── meta/
│   └── main.yml
├── tasks/
│   └── main.yml
├── templates/
└── vars/
    └── main.yml
Enter fullscreen mode Exit fullscreen mode
  • Create the tasks/main.yml file: This file will contain all the tasks from the playbook. Copy the tasks section from the playbook into this file. Organizing them into a tasks directory keeps the main playbook clean and focused, allowing us to manage each role independently.
# tasks/main.yaml
---
- name: Install Nginx
  apt:
    name: nginx
    state: present

- name: Ensure web root directory exists
  file:
    path: "{{ root_directory }}"
    state: directory

- name: Copy index.html to web root
  copy:
    src: index.html
    dest: "{{ root_directory }}/index.html"

- name: Template Nginx configuration file
  template:
    src: ../templates/nginx.conf.j2
    dest: /etc/nginx/sites-available/default
  notify: restart nginx

- name: Ensure Nginx is enabled and started
  service:
    name: nginx
    enabled: true
    state: started
Enter fullscreen mode Exit fullscreen mode
  • Create the handlers/main.yml file: Move the handler from the playbook to this file. Doing this improves readability, making it easier to track what services are being managed via handlers
# handlers/main.yaml
---
- name: restart nginx
  service:
    name: nginx
    state: restarted

Enter fullscreen mode Exit fullscreen mode
  • Create the templates/nginx.conf.j2 and files/index.html file: Doing this ensures that in the tasks, we don't need to set the path (absolute or relative) for the files, ansible reads it from them from the respective directories

Here is a sample Jinja template for the config

# templates/nginx.conf.j2
server {
    listen {{ nginx_port }};
    server_name {{ server_name }};

    root {{ root_directory }};
    index index.html;

    location / {
        proxy_pass http://localhost:3000;
        proxy_set_header Host $host;
    }
}
Enter fullscreen mode Exit fullscreen mode
  • Set the defaults and vars for the Role: Using a default file allows for easy customization without modifying the main task file, making it easier to adapt the role for different environments. But the vars file is intended for role-specific variables that we don’t want to be overridden. Here are some sample values.
# defaults/main.yml
nginx_port: 80                        # Default port for Nginx
root_directory: /var/www/html          # Default root directory 
Enter fullscreen mode Exit fullscreen mode
# vars/main.yaml
server_name:  example.com                 
Enter fullscreen mode Exit fullscreen mode

Updated Playbook

After migrating to a role, the playbook would look like this:

- hosts: webservers
  become: yes
  roles:
    - nginx_webserver
Enter fullscreen mode Exit fullscreen mode

This keeps the playbook concise and focused on high-level orchestration, while the role handles the implementation details.

To summarize

Here are a few reasons why roles are preferred over playbooks

Modularity: Roles encapsulate related tasks, making them easy to manage and understand.

Reusability: Each role can be reused across different playbooks, avoiding code duplication. This saves time and effort, especially in large projects.

Defaults and Overrides: Roles support defaults and can easily be overridden when included in a playbook, providing flexibility for different environments without altering core logic.

Scalability: Roles can be combined or expanded with additional features without cluttering a single playbook, making it easier to scale the automation.

By adopting a role-based structure, we create a more organized and maintainable Ansible codebase that reduces complexity as the projects evolve.

Top comments (0)