Automation

Ansible in production: structure an operations repository before exposing it in AWX

Organize an Ansible repository used by AWX with bounded playbooks, reusable roles, separated inventories, readable variables, versioned collections and operations documentation.

20 May 2026 ansibleawxgitopsinventoryrolesoperations

AWX does not magically make an Ansible repository clean. It mainly makes its content more visible, easier to launch and sometimes easier to misuse. Before exposing playbooks through an interface, structure the repository as an operations tool: clear scopes, separated inventories, reusable roles, understandable variables and minimal documentation.

The scenario is simple: a team manages a Linux estate with a few playbooks already stored in Git. Runs still happen from administrator workstations. AWX should centralize recurring actions, not become the place where a missing repository structure is improvised.

Separate playbooks, roles and inventories

An operable repository should make it clear what is executable, what is reusable and what describes targets. Playbooks exposed in AWX should be few and action-oriented. Roles hold reusable logic. Inventories describe environments and groups.

text repository-layout.txt
ansible-operations/
playbooks/
  check-linux-baseline.yml
  check-disk-usage.yml
  restart-approved-service.yml
  patch-selected-package.yml
  apply-linux-baseline.yml
roles/
  linux_baseline/
  ssh_hardening/
  package_management/
  monitoring_agent/
inventories/
  preprod/hosts.yml
  production/hosts.yml
group_vars/
  all.yml
  production_web.yml
  production_tools.yml
collections/requirements.yml
README.md

This structure is not sophisticated, but it makes review possible. An AWX template points to an identifiable playbook. The playbook calls a role. The inventory limits the target. Variables are versioned.

Keep exposed playbooks short

A playbook used as an AWX job should be a controlled entry point. It should not contain all technical logic. It prepares context, validates parameters and calls the required roles. This reduces duplication and makes behavior easier to review.

yaml apply-linux-baseline.yml
---
- name: Apply Linux baseline
hosts: "{{ target_group }}"
become: true
gather_facts: true
pre_tasks:
  - name: Reject restricted groups
    ansible.builtin.fail:
      msg: "This playbook cannot target restricted hosts"
    when: target_group in ['restricted', 'production_databases']
roles:
  - role: linux_baseline
  - role: monitoring_agent
post_tasks:
  - name: Show kernel and distribution
    ansible.builtin.debug:
      msg: "{{ inventory_hostname }} {{ ansible_distribution }} {{ ansible_distribution_version }} kernel {{ ansible_kernel }}"

The role can evolve with its defaults and tests. The playbook remains readable for the team approving the AWX template.

Name inventories to prevent mistakes

Inventories should be explicit. A single hosts.yml file often ends up containing too many groups and too many special cases. Separating preproduction and production already avoids a class of errors. Separating sensitive groups prevents standard jobs from touching them accidentally.

yaml inventories/production/hosts.yml
all:
children:
  production_web:
    hosts:
      web01.example.local:
      web02.example.local:
  production_tools:
    hosts:
      tools01.example.local:
  production_databases:
    hosts:
      db01.example.local:
  restricted:
    children:
      production_databases:

In AWX, a maintenance template should point to the expected inventory. If an inventory can be selected at launch time, there should be a strong reason and playbook-side validation.

Version collections

A playbook that works today can change behavior after a collection update. For stable operations, collections must be declared and installed predictably. AWX can use execution environments, but the repository should still state its dependencies.

yaml collections/requirements.yml
---
collections:
- name: ansible.posix
  version: "1.5.4"
- name: community.general
  version: "8.6.0"

The exact versions depend on context, but the intent is clear: AWX execution should not depend on an implicit environment nobody can rebuild.

Document published jobs

The README does not need to be a full manual. It should describe exposed playbooks, their effects and limits. That lets a reviewer understand what an AWX button really does.

text README-jobs.txt
Job: check-disk-usage
Effect: read-only
Targets: preprod, production_web, production_tools
Credential: read-only

Job: restart-approved-service
Effect: restarts nginx, telegraf or chronyd
Targets: production_web, production_tools
Credential: maintenance
Validation: service_facts after restart

Job: apply-linux-baseline
Effect: applies linux_baseline and monitoring_agent roles
Targets: preproduction by default, production after approved change
Credential: restricted-admin

Plan AWX branches

A repository can be clean but risky if AWX tracks an unstable branch. A simple practice is to expose production from a stable branch or tag, while keeping work branches for tests. The exact Git workflow depends on the team, but it must be deliberate.

text branch-model.txt
main
Review and integration of approved changes

production
Branch followed by production AWX templates

feature/*
Development and tests on a separate AWX project

Tags
Possible rollback points for sensitive changes

Conclusion

Structuring an Ansible repository before AWX is not cosmetic. It is an operations requirement. Exposed playbooks must be short, roles reusable, inventories explicit, dependencies versioned and effects documented.

AWX then becomes a controlled executor of versioned code. Without that foundation, it becomes an interface that gives a sense of governance to automations that are still too free. The right order is simple: make the repository readable, bound the actions, test the roles, then publish the templates.