This guide wires GitLab into AWX so that inventories, playbooks, roles and collections all live in versioned, reviewable Git repos.

Git becomes the single source of truth: AWX just mirrors it.


The model: one mechanism, two consumers

A Project in AWX = a Git repo, cloned and kept in sync.

Its a live link, not a one-time import.

Basically, we create one project per repo (some hold playbooks, some hold inventories) and link everything together to make it run.

flowchart LR
    subgraph GL["🦊 GitLab"]
        PB["playbooks/linux-hardening<br/>linux-hardening.yml<br/>+ collections/requirements.yml"]
        INV["inventories/prod<br/>openstack/openstack.yml + group_vars/"]
    end
    subgraph AWX["πŸŽ›οΈ AWX"]
        ProjPB["πŸ“š Project (playbook)"]
        ProjINV["πŸ“š Project (inventories)"]
        Src["πŸ”— Inventory Source"]
        Inv["πŸ“‹ Inventory"]
        JT["▢️ Job Template<br/>Project + Playbook + Inventory"]
    end
    Target["πŸ–₯️ target hosts"]
    PB -- "git clone" --> ProjPB
    INV -- "git clone" --> ProjINV
    ProjINV --> Src
    Src -- "sync = import" --> Inv
    ProjPB --> JT
    Inv --> JT
    JT -- "run via execution node" --> Target
  • Job Templates pick a playbook from inside a Project
  • Inventory Sources pick an inventory file from a Project
  • roles / collections come in through requirements.yml, automatically on Project sync.

INFO

A green Sync Status: Success on an inventory, or a Job Template that just has a playbook dropdown, both mean the same thing underneath: a Project (Git) behind it.


1. GitLab: the repos

First we have to create the Groups:

Inventories group: contains one project per environment (prod, test, dev…), and every project holds its inventory file + group_vars/ together:

inventories/prod          # inventories is the group, prod is the project (repo)
└── openstack/            # openstack is just a folder (one per machine type)
    β”œβ”€β”€ openstack.yml      # the inventory file
    └── group_vars/
        └── all.yml        # the group_vars
inventories/test
└── openstack/
...

IMPORTANT

Keep group_vars/ in the same folder as the inventory file, that’s how Ansible (and AWX’s import) auto-loads them. The group a playbook targets (e.g. hardened_servers) and its variables also live here, in the inventory β€” not in the playbook repo.

Playbooks group: contains one project per automation (LinuxHardening, UpgradeHost, JoinAD…). Each repo is flat: the playbook, its templates/, and its requirements.yml files sit at the repo root, for example:

playbooks/linux-hardening   # playbooks is the group, linux-hardening is the project (repo)
β”œβ”€β”€ linux-hardening.yml      # the playbook
β”œβ”€β”€ collections/
β”‚   └── requirements.yml     # external collections
└── templates/               # jinja2 templates the playbook uses
playbooks/upgrade-host
β”œβ”€β”€ upgrade.yml
β”œβ”€β”€ roles/requirements.yml   # external roles (only if the playbook uses any)
└── collections/requirements.yml
...

requirements.yml must be at the repo root

On Project sync, AWX runs ansible-galaxy install reading only the root-level roles/requirements.yml and collections/requirements.yml.

Files in subfolders are not picked up: that’s why each automation is its own repo with the requirements at its root (the playbook itself can sit in a subfolder, the requirements can’t).


2. GitLab: read-only SSH access (one credential for all repos)

AWX only needs to clone.

To do that, we can use a read-only service account with a dedicated SSH key, member of every group: this way one credential can clone every repo.

Procedure:

  1. Bot user: Admin β†’ Users β†’ New user β†’ svc-awx.
  2. Read access: each group (inventories, Playbooks…) β†’ Manage β†’ Members β†’ Invite β†’ svc-awx β†’ role Reporter.
  3. Create a SSH key:
    ssh-keygen -t ed25519 -f svc-awx -C svc-awx -N ""
  4. Admin β†’ Users β†’ svc-awx β†’ Impersonate β†’ Preferences β†’ SSH Keys β†’ paste svc-awx.pub β†’ Stop impersonation.

Besides the key we just created for syncing projects and inventories, every target needs:

  • A key for AWX to reach the targets via SSH (you select it later as a Machine credential when running the playbook): we set it up here. Recap: the public half goes in every target’s ~/.ssh/authorized_keys; the private half goes into the AWX Machine credential: AWX injects it into whichever execution node runs the job (it’s never stored on the nodes).
  • Every target allowing :22 from the execution node’s IP as source.

3. AWX: Source Control credential (SSH)

Resources β†’ Credentials β†’ Add

  • Credential Type: Source Control

  • SCM Private Key: the private svc-awx key

  • Leave Username / Password / Passphrase empty (the user comes from the git@ URL).


4. AWX: one Project per repo

As I said, we need to create a Project for each repo: same steps, different URL.

Resources β†’ Projects β†’ Add:

FieldPlaybookInventories
NameLinuxHardeningInventories
Source Control TypeGitGit
Source Control URLgit@gitlab.yourdomain.com:playbooks/linux-hardening.gitgit@gitlab.yourdomain.com:inventories/prod.git
Source Control Credentialsvc-awxsvc-awx
Optionsβœ… Update Revision on Launchβœ… Update Revision on Launch

For the Source Control URL, copy the exact SSH URL from the GitLab repo’s Code β†’ Clone with SSH.

Save each, and wait for Successful. On the playbook project sync, check the log shows ansible-galaxy installing your collections: that confirms the root-level collections/requirements.yml was picked up.

Now AWX can see your inventories and playbooks… Let’s put them together!

  • Inventories β†’ an Inventory Source
  • Playbooks β†’ a Job Template

Inventories β†’ Inventory + Source

Resources β†’ Inventories β†’ Add β†’ Inventory β†’ Name β†’ Save (this is just an empty container for now).

Now open it: Sources tab (appears only after saving) β†’ Add:

FieldValue
SourceSourced from a Project
ProjectInventories
Inventory fileopenstack/openstack.yml (in this example)
Optionsβœ… Update on launch Β· βœ… Overwrite Β· βœ… Overwrite variables

Save β†’ Sync.

NOTE

Variables from group_vars/all.yml are imported as inventory-level variables (Inventory β†’ Variables), not onto each host β€” but they still apply to every host at runtime.


Playbooks β†’ Job Template

This is the main use of a Project.

Resources β†’ Templates β†’ Add β†’ Job Template:

FieldValue
Namee.g. LinuxHardening
Job TypeCheck for a dry-run, then Run
Inventorythe inventory that holds the target group
ProjectLinuxHardening
Playbooklinux-hardening.yml (dropdown; subfolders are listed too)
Execution Environmentleave default (it’s an image, not a machine)
Credentialsthe Machine credential for the targets
Instance Groupsexecution-vms (the execution node that SSHes to the target)

Save β†’ Launch.

Execution Environment β‰  Execution Node

Two similarly-named things:

  • Execution Environment = the container image the job runs in β†’ leave it on the default (awx-ee). Don’t pick β€œControl Plane Execution Environment” (that’s the internal image for control-plane tasks).
  • Execution Node = the machine (VM) that runs the job and opens the SSH to the target β†’ you choose it via Instance Groups β†’ execution-vms.

TIP

Run a dry-run first: set Job Type β†’ Check (+ Show Changes for the diff), read the diff, then switch to Run. Essential for anything touching SSH / PAM / firewall.


Scaling out

Git stays the single source of truth, AWX mirrors it:

  • Playbooks group β†’ one repo per automation β†’ one AWX Project each β†’ its Job Template(s)
  • inventories group β†’ one repo per environment β†’ AWX Project β†’ many Inventory Sources (one per type: openstack/, windows/…)
  • roles / collections β†’ requirements.yml at each repo root β†’ installed automatically on sync