AWX

Last modified by Tomas Terälä on 2025/05/15 07:56

AWX is the upstream for AAP or Ansible Automation Platform, previously known as Ansible Tower. AWX can be viewed as a GUI and tracker for running Ansible playbooks, and it's greatest strengths are for creating self-service tools and for making sure only the latest ansible-playbooks from git are run. It is also helpful when creating multiple sequential playbooks with workflows.

General knowledge about creating and running Ansible playbooks is required.

Basic usage

In AWX, separation of resources is done by creating an Organization for each group/project/team that requires access to all parts of the Organization. In addition, a Team which links to an iam-group is created.

Inside an Organization, the most used resources are Credentials, Hosts, Inventories, Projects and Templates.

A Host is a singular target for an ansible playbook, Inventories are groupings of hosts. Projects define the connection to a version control system. Templates are singular playbooks, or in the case of Workflow Templates, they are a graph of Templates. Workflow templates can be thought of as "If playbook 1 succeeds, run playbook 2. If playbook 1 fails, run playbook 3" etc.

Patterns for creating workflows

  1. Simple playbooks
  2. Separate naming for passed variables
  3. Consistent directory structure
  4. When using custom execution environments, keep the files in the same repo
  5. Keep naming as similar as possible between playbooks in git and Templates in AWX

It is recommended to create simple playbooks with only a few steps, and then to have the playbook fail in a consistent matter. An example of this is checking if a resource exists (or if a server is online), then passing information to the next playbook with ansible.builtin.set_stats. If the playbook is simple, it is easy to debug and then conditional cases can be handled by checking if the playbook fails or succeeds. Making playbooks simple also allows them to be reused in other workflows.
 

Another good practice is to use a separate naming convention for variables passed from other playbooks. One example is to add stats_ at the start of all passed variables.

A helpful video explaining how set_stats works: https://www.youtube.com/watch?v=NTDFCO7DAr0

The third recommended practice is to create a cohesive directory structure. This will help when trying to add the playbooks in the AWX GUI.

Click here to expand...

├── execution-environments
│   └── context
│       └── _build
├── playbooks
│   ├── alerts
│   ├── api_key_functions
│   ├── emails
│   ├── groups_and_teams

Custom execution environments remove the "it worked on my machine" part of most ansible issues. However since they are another learning step, keeping them close to your playbooks lessens the work when something inevitably breaks or someone wants to add a new module. Intructions can be found in AWX docs

The final recommended pattern is keeping names between AWX and your repository as close as possible. This lesson was learned the hardway.

Examples

Checking if a resource from the API exists

This playbook calls an API, then checks if the requested object (in this case a Team with name is found from the APIs result. If it doesn't, the playbook will fail. If

Click here to expand...

-
- name: Check if team exists in Dependency-Track
  hosts: localhost
  gather_facts: true
  tasks:
    - name: Include encrypted variables
      ansible.builtin.include_vars:
        file: "../{{ vault_file }}"
      when: prod is defined

    - name: Include test variables
      ansible.builtin.include_vars:
        file: test-vars.yaml
      when: prod is not defined

    - name: Get all teams
      ansible.builtin.uri:
        url: "{{ dependency_track_url }}/api/v1/team"
        method: GET
        headers:
          X-Api-Key: "{{ api_key }}"
          Content-Type: "application/json"
      register: all_teams

    - name: Print all teams
      ansible.builtin.debug:
        var: all_teams

    - name: Check if the team exists and extract variables
      ansible.builtin.set_fact:
        team_uuid: "{{ item.uuid }}"
        mapped_oidc_groups: "{{ item.mappedOidcGroups }}"
        api_keys: "{{ item.apiKeys }}"
      loop: "{{ all_teams.json }}"
      when: item.name == grp_group

    - name: Fail if team does not exist
      ansible.builtin.fail:
        msg: "The team '{{ grp_group }}' does not exist in the system."
      when: team_uuid is not defined

    - name: Display the UUID if the team exists
      ansible.builtin.debug:
        msg: "The UUID for the team '{{ grp_group }}' is '{{ team_uuid }}'"
      when: team_uuid is defined

    - name: Pass team_uuid forward in workflow
      ansible.builtin.set_stats:
        data:
          stats_team_uuid: "{{ team_uuid }}"
          stats_mapped_oidc_groups: "{{ mapped_oidc_groups }}"
          stats_api_keys: "{{ api_keys }}"

Sending an email using ansible

An email can be used to notify a user using a self-service tool of the success/failure of a Workflow template or even a single step, possibly with information on what the issue was. These can be also used to let admins know.

The playbook below is triggered when the previous playbook has failed. It then makes sure that there is an email address, composes the email body and then sends the email to the recipient.

Click here to expand...

- name: No permission emails
  hosts: localhost
  gather_facts: false
  tasks:
    - name: Include encrypted variables
      ansible.builtin.include_vars:
        file: "../{{ vault_file }}"
      when: prod is defined

    - name: Include test variables
      ansible.builtin.include_vars:
        file: test-vars.yaml
      when: prod is not defined

    - name: Ensure mail_to is set correctly
      ansible.builtin.set_fact:
        mail_to: "{{ awx_user_email }}"
      when: mail_to is not defined or mail_to | trim == ""

    - name: Compose email; team exists.
      ansible.builtin.set_fact:
        user_email_body: >-
          (This is an automatic response)

          Hello,

          you tried to create the team {{ grp_group }} in Dependency Track, but the name already exists. Please try again with another name.

          If you have any questions, please email {{ admin_email }}.

    - name: Sending an e-mail using the remote machine to the user
      community.general.mail:
        host: smtp.helsinki.fi
        port: 25
        sender: noreply@helsinki.fi
        to: "{{ mail_to }}"
        subject: Dependency track error information
        body: "{{ user_email_body }}"

Webhooks

Workflow templates and job templates can be launched either from the AWX UI or by using webhooks. The most simple way is to start editing the Template in the UI and Enabling the webhook from the bottom of the page. Even when using curl, gitlab webhooks are the suggested way of using webhooks, since they don't require requesting a local account from the AWX admins. There are however some differences in using the webhooks, more info here
1744295241433-563.png

Credentials

The main types of crendentials for AWX are:

  • Vault
    • used for authentication to a local ansible-vault file. Using vault identifier is preferred, but not necessary
  • Machine
    • contains info for authenticating into target hosts. Username/password/ssh-key etc
  • Source control
    • Used to authenticate to git-provider to sync all playbooks in a Project.
  • Microsoft Azure Key Vault
    • Used to establish a link to an Azure Key Vault. Usage info in AWX/AAP
    • After establishing the link, secret values from Azure Key Vault can be dynamically retrieved to other kinds of Crendentials.

Using ansible-vault

The preferred directory structure is to have the ansible-vault file inside the top-most directory, with the directory only containing ansible-vault files, a README.md and directories for playbooks. Then something like this can be added to the start of each ansible-playbook. This allows the user to start playbooks both in AWX and locally, without having to specify extra variables for local testing. Making vault_file a variable also allows seamlessly changing between test and prod files in workflows.

    - name: Include encrypted variables
     ansible.builtin.include_vars:
       file: "../{{ vault_file }}"
     when: prod is defined

    - name: Include test variables
     ansible.builtin.include_vars:
       file: test-vars.yaml
     when: prod is not defined

An example about using vault identifier can be found here: https://medium.com/t%C3%BCrk-telekom-bulut-teknolojileri/ansible-vault-with-awx-80b603617798

Debugging AWX issues

How do I pass a value to multiple playbooks in a Workflow?

Either add it to the Workflow Template in the variables section OR create a forced survey that needs to be submitted before the playbook can be run.

What values was my playbook ran with/what did it pass forward?

Either go from Templates -> Template XXX -> Jobs -> 123 -Template XXX or directly from Jobs -> 123 -Template XXX and check the Details tab. Variables are what the playbook was given as startup variables, Artifacts are what the playbook saved/passed forward.

AWX Job showing variables and artifacts

Failure Explanation: Previous Task Failed: {"job_type": "project_update", "job_name": "XXXX", "job_id": "1234"}

This error is caused by AWX losing access to the git repository. By checking the Job logs, prior to this Template running, a sync job for the repo is attempted but it is unsuccesfull. By checking the Details-tab of the Project sync, we can check the used credential in Machine Credential.

Why can't person X access Template/Host/Organization

By default, when an Organization is created only a single iam-group will be given full access to an Organization. Other groups need to be added to the group in IAM, or they can be requested to be added by the AWX admins. This needs to be added to SAML settings, which is why regular users need to request for groups to be added. There is a WIP for making this a self-service tool inside AWX.

Below is the JSON that maps users belonging to the group hy-employees to a Team called HY employees in the Default organization. If the iam-group is specific to your organization, request for the team to only be added to your Org. If you suspect others might need it, request for the Team to be created in the Default org.

    {
     "team": "hy-employees",
     "team_alias": "HY employees",
     "organization": "Default"
    },