100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Infrastructure as Code — Terraform & Ansible
25 minintermediate

Roles — Directory Layout, Defaults, Vars, Files and Meta

Ansible roles are the primary unit of reusable automation. A role is a structured directory tree that encapsulates a complete, self-contained piece of configuration work — a 'nginx' role configures Nginx, a 'postgresql' role installs and configures PostgreSQL, a 'hardening' role applies OS security baselines. Roles replace the flat, monolithic playbook structure (where all tasks for all components live in one file) with a component-oriented architecture where each role owns its domain completely: its tasks, its default variable values, its templates, its files, its handlers, and its dependency declarations. A 600-line playbook for a three-tier application becomes three focused roles of 200 lines each, independently testable, independently versionable, and reusable across different projects. The role structure is standardised by Ansible conventions — the directory layout is the same for every role, making it immediately navigable by any Ansible practitioner.

The distinction between role defaults and role vars is one of the most important (and most misunderstood) aspects of role design. Defaults ('defaults/main.yml') define the lowest-priority variable values — they are the factory settings that callers are explicitly expected to override. Vars ('vars/main.yml') define high-priority internal variables that are part of the role's implementation and are not intended to be overridden by callers. A correctly designed role puts all user-facing configuration in defaults/ (where callers can easily customise) and puts internal constants in vars/ (where callers should not need to touch them). This distinction enables roles to be opinionated about their internal implementation while remaining flexible about their external interface — exactly the same principle as well-designed Terraform modules with typed variables and hidden locals.

Analogy🏏Cricket
🏏 Think of it like cricket: Terraform Cloud is the ICC's centralised match management platform — instead of each national board (team) maintaining its own scoring system, umpire assignment software and results database (self-managed CI/CD + S3 backend), the ICC platform handles all of this centrally. When a board member proposes a rule change (pull request), the platform automatically simulates the match under the new rules (speculative plan on PR), shows the referees the impact (plan output in PR comment), and requires the match committee to approve (policy gates) before the rule takes effect. The audit log records every change, every approval, and who made each decision — providing the governance and traceability that serious tournament operations require.

The Standard Role Directory Layout

Ansible enforces a conventional directory structure for roles. Under the role's root directory: 'tasks/main.yml' is the entry point — where all tasks are defined or imported; 'defaults/main.yml' holds low-priority variable defaults that callers can override; 'vars/main.yml' holds high-priority internal variables; 'handlers/main.yml' defines handlers used by the role's tasks; 'templates/' holds Jinja2 templates; 'files/' holds static files that are copied to managed hosts; 'meta/main.yml' declares the role's author, license, minimum Ansible version, and inter-role dependencies (roles that must run before this one); 'tests/' holds test playbooks for the role; and 'README.md' documents the role. Not all directories are required — a simple role might only have tasks/, defaults/ and templates/. Creating roles with 'ansible-galaxy role init role_name' generates the full skeleton structure, which is the recommended starting point even for simple roles.

Analogy🏏Cricket
🏏 Think of it like cricket: The fixed role directory layout is the standardised kit bag every touring professional packs the same way — and the point is not tidiness, it is that anyone can find anything in anyone's bag without asking. Ansible's role loader is like ground staff who service every player's kit: because the layout is a convention the tooling itself enforces, the loader looks in EXACTLY one place for each thing — tasks/main.yml is the top compartment where the session plan always lives (the automatic entry point — no configuration says 'start here'; the convention IS the configuration), templates/ and files/ are the compartments for personalised versus standard-issue gear (and modules search them implicitly — a template task can say 'nginx.conf.j2' with no path, the way a player says 'my gloves' without saying which pocket), handlers/main.yml is where the standing conditional orders are filed, defaults/ holds the factory settings and vars/ the sealed internals, and meta/main.yml is the luggage tag declaring what this bag depends on (role dependencies, supported platforms). An engineer who has worked with one well-formed role can navigate any of the 30,000 on Galaxy in seconds — the entire ecosystem's interoperability rests on nothing more sophisticated than everyone packing the bag the same way.
bash
#!/bin/bash
# Create a production role with full structure
ansible-galaxy role init cricket_nginx

# Review the generated structure
tree cricket_nginx/
# cricket_nginx/
# ├── defaults/
# │   └── main.yml      # Overridable defaults
# ├── files/            # Static files for copy module
# ├── handlers/
# │   └── main.yml      # Handlers triggered by notify
# ├── meta/
# │   └── main.yml      # Role metadata and dependencies
# ├── README.md
# ├── tasks/
# │   └── main.yml      # All tasks (entry point)
# ├── templates/        # Jinja2 templates
# ├── tests/
# │   ├── inventory
# │   └── test.yml
# └── vars/
#     └── main.yml      # Internal vars (high priority)

# ── defaults/main.yml — user-facing configuration ────────────────────────────
cat > cricket_nginx/defaults/main.yml << 'DEFAULTS'
---
# Nginx service configuration — callers should override these
nginx_port: 80
nginx_ssl_port: 443
nginx_worker_processes: auto
nginx_worker_connections: 1024
nginx_keepalive_timeout: 65
nginx_client_max_body_size: 50m
nginx_server_name: "{{ inventory_hostname }}"

# SSL settings
nginx_ssl_enabled: false
nginx_ssl_cert_path: /etc/ssl/certs
nginx_ssl_key_path: /etc/ssl/private

# Application upstream
nginx_upstream_host: 127.0.0.1
nginx_upstream_port: 8080
nginx_upstream_keepalive: 32

# Logging
nginx_access_log: /var/log/nginx/access.log
nginx_error_log: /var/log/nginx/error.log
nginx_log_level: warn
DEFAULTS

# ── vars/main.yml — internal implementation constants ────────────────────────
cat > cricket_nginx/vars/main.yml << 'VARS'
---
# Internal variables — not intended for caller override
nginx_config_dir: /etc/nginx
nginx_sites_dir: /etc/nginx/conf.d
nginx_service_name: nginx
nginx_packages:
  RedHat: nginx
  Debian: nginx
VARS

# ── meta/main.yml — dependencies and metadata ────────────────────────────────
cat > cricket_nginx/meta/main.yml << 'META'
---
galaxy_info:
  author: nagarajarao
  description: Nginx reverse proxy for Cricket Analytics Platform
  company: Sri Hayavadhana Info-Tech
  license: MIT
  min_ansible_version: '2.15'
  platforms:
    - name: Ubuntu
      versions: ['22.04', '24.04']
    - name: Amazon
      versions: ['2023']
  categories:
    - web

dependencies:
  # This role requires firewall to be configured before Nginx starts
  - role: cricket_firewall
    vars:
      firewall_allowed_ports:
        - { port: 80, proto: tcp }
        - { port: 443, proto: tcp }
META
yaml
# tasks/main.yml — Role task entry point
---
- name: Include OS-family-specific variables
  ansible.builtin.include_vars:
    file: '{{ ansible_os_family }}.yml'
  failed_when: false  # Skip if no OS-specific vars file exists

- name: Install Nginx packages
  ansible.builtin.package:
    name: '{{ nginx_packages[ansible_os_family] | default("nginx") }}'
    state: present
  tags: [nginx, packages]

- name: Create Nginx configuration directories
  ansible.builtin.file:
    path: '{{ item }}'
    state: directory
    owner: root
    group: root
    mode: '0755'
  loop:
    - '{{ nginx_config_dir }}'
    - '{{ nginx_sites_dir }}'
  tags: [nginx]

- name: Deploy main Nginx configuration
  ansible.builtin.template:
    src: nginx.conf.j2
    dest: '{{ nginx_config_dir }}/nginx.conf'
    mode: '0644'
    validate: 'nginx -t -c %s'
    backup: true
  notify: Reload Nginx
  tags: [nginx]

- name: Deploy virtual host configuration
  ansible.builtin.template:
    src: vhost.conf.j2
    dest: '{{ nginx_sites_dir }}/cricket-api.conf'
    mode: '0644'
  notify: Reload Nginx
  tags: [nginx]

- name: Enable and start Nginx
  ansible.builtin.service:
    name: '{{ nginx_service_name }}'
    enabled: true
    state: started
  tags: [nginx]

# handlers/main.yml
# - name: Reload Nginx
#   ansible.builtin.service:
#     name: '{{ nginx_service_name }}'
#     state: reloaded

# Using the role in a playbook:
# - name: Configure API servers
#   hosts: api_servers
#   roles:
#     - role: cricket_nginx
#       vars:
#         nginx_upstream_port: 8080
#         nginx_ssl_enabled: true
#         nginx_server_name: api.cricket-analytics.io
Lesson 22 of 33
0% complete