Developer Tools AWS

ARA Records Ansible on AWS User Guide

| Product: ARA Records Ansible

Overview

This guide covers the deployment and configuration of ARA Records Ansible on AWS using the cloudimg AWS Marketplace AMI.

ARA is the popular open source reporting tool for Ansible. You add a callback plugin to your Ansible configuration, run your playbooks exactly as you do today, and ARA records every play, task, host and result into a database and renders it in a web interface. Instead of scrolling back through terminal output that nobody kept, you get a durable, searchable record of what ran, where, how long it took, and precisely which task failed on which host.

The cloudimg image ships ARA unmodified from the upstream release. The Django application runs under gunicorn as an unprivileged service account bound to the loopback interface, with nginx published on port 80 in front of it. The application, its Python virtual environment and its recording database live on a dedicated EBS volume mounted at /opt/ara, which you can resize independently of the operating system disk. Ansible and the ARA callback are installed on the appliance itself, so it records runs out of the box, and any remote control node can be pointed at it just as easily. Backed by 24/7 cloudimg support.

ARA is licensed under the GNU General Public License v3. Ansible is a registered trademark of Red Hat, Inc. This image is produced by cloudimg and is not affiliated with, endorsed by, or sponsored by the ARA project or Red Hat. It ships the upstream self-hosted software, unmodified.

What is included:

  • ARA Records Ansible 1.8.0 in a dedicated Python virtual environment at /opt/ara/venv
  • ansible-core and the ARA callback plugin preinstalled and wired up in /etc/ara/ansible.cfg
  • gunicorn on loopback 127.0.0.1:8000 behind an nginx reverse proxy on port 80
  • A self-contained database at /opt/ara/server/ansible.sqlite, the upstream default for a single-node deployment
  • A dedicated 20 GiB gp3 data volume mounted at /opt/ara by filesystem UUID and captured into the AMI
  • Authentication required for both reading and writing the whole interface and the whole API
  • A first-boot service that generates a fresh application secret key and a unique administrator password per instance

Why the authentication default matters

This is the single most important thing to understand about the image. A stock ARA server defaults to unauthenticated read and unauthenticated write (READ_LOGIN_REQUIRED and WRITE_LOGIN_REQUIRED both default to false). On a public network that means anyone who can reach port 80 can read every recorded playbook, every host name and every task result, and can also write records into your history.

This image turns both on in /opt/ara/server/settings.yaml, so the reporting interface and the REST API are gated behind HTTP authentication from the very first request. The only endpoint deliberately left open is /healthcheck/, which reports up or down and exposes no data, so it can be used as a load balancer target group probe.

Connecting to your instance

Connect over SSH on port 22 using the key pair you selected at launch. The login user depends on the operating system variant you launched:

OS variant SSH login user Example
Ubuntu 24.04 LTS ubuntu ssh -i your-key.pem ubuntu@<public-ip>

Replace <public-ip> with the public IPv4 address shown for your instance in the EC2 console.

The security group you attach needs port 22 for SSH and port 80 for the reporting interface. Port 443 is worth opening too if you intend to terminate TLS on the instance, as described at the end of this guide. Port 8000 is deliberately not exposed: gunicorn binds the loopback interface only, and nothing outside the instance can reach it.

What happens on first boot

Every instance launched from this image runs a one-shot service, ara-firstboot.service, before the application starts. It:

  1. Generates a fresh Django SECRET_KEY unique to that instance and writes it to /opt/ara/ara.env.
  2. Applies the database migrations against the empty database shipped in the image, creating a fresh schema with zero user accounts.
  3. Creates a single administrator, ara, with a random password unique to that instance.
  4. Resolves the instance's public address from the EC2 instance metadata service and records it in the credentials file and the login banner.
  5. Writes its sentinel, at which point systemd starts ara-web.service and nginx.service.

The application services are gated on that sentinel, so there is no window in which the appliance serves traffic with a placeholder secret or without an administrator. Confirm first boot completed:

ls -l /var/lib/cloudimg/ara-firstboot.done
systemctl is-active ara-web.service nginx.service
-rw-r--r-- 1 root root 0 Jul 25 23:37 /var/lib/cloudimg/ara-firstboot.done
active
active

Retrieving your administrator credential

The credential is written to a root-only file, /root/ara-credentials.txt. Read the non-secret fields to confirm it was generated:

sudo grep -E '^ara\.(url|username)=' /root/ara-credentials.txt
sudo grep -c '^ara.password=' /root/ara-credentials.txt
ara.url=http://35.172.114.84/
ara.username=ara
1

To read the password itself, print just that one field:

Purpose Command
Show the administrator password sudo grep '^ara.password=' /root/ara-credentials.txt \| cut -d= -f2-
Show the whole credentials file sudo cat /root/ara-credentials.txt

The username is ara. Treat this password as you would any production administrator credential. Because the image ships with zero accounts and the password is minted per instance, no two instances launched from this AMI ever share a credential.

Checking the service is healthy

nginx serves an unauthenticated health endpoint. It returns 200 as soon as the stack is serving, which makes it a ready-made health check for an Application Load Balancer target group:

curl -s -o /dev/null -w 'health: HTTP %{http_code}\n' http://127.0.0.1/healthcheck/
health: HTTP 200

Confirm the two published listeners are exactly what you expect. nginx is on port 80 on all interfaces; gunicorn is on port 8000 on loopback only:

sudo ss -tlnp | grep -E ':(80|8000)\b'
LISTEN 0  2048  127.0.0.1:8000  0.0.0.0:*  users:(("gunicorn",pid=3559,fd=3),("gunicorn",pid=3557,fd=3),("gunicorn",pid=3540,fd=3))
LISTEN 0   511    0.0.0.0:80    0.0.0.0:*  users:(("nginx",pid=3549,fd=5),("nginx",pid=3547,fd=5),("nginx",pid=3545,fd=5))
LISTEN 0   511       [::]:80       [::]:*  users:(("nginx",pid=3549,fd=6),("nginx",pid=3547,fd=6),("nginx",pid=3545,fd=6))

Proving authentication is enforced

Both the reporting interface and the REST API must refuse an unauthenticated request. This block asserts that, and fails if either endpoint is ever open:

UI=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1/)
API=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1/api/v1/playbooks)
echo "unauthenticated UI:  HTTP $UI"
echo "unauthenticated API: HTTP $API"
if [ "$UI" = "401" ] && [ "$API" = "401" ]; then
  echo "both refused without credentials - correct"
else
  echo "UNEXPECTED: an endpoint answered without authentication"; exit 1
fi
unauthenticated UI:  HTTP 401
unauthenticated API: HTTP 401
both refused without credentials - correct

Now confirm the generated credential is accepted. This reads the password into a shell variable so it is never printed:

PW=$(sudo grep '^ara.password=' /root/ara-credentials.txt | cut -d= -f2-)
curl -s -o /dev/null -w 'authenticated UI: HTTP %{http_code}\n' -u "ara:$PW" http://127.0.0.1/
authenticated UI: HTTP 200

Signing in to the reporting interface

Open http://<public-ip>/ in a browser. Because both read and write require authentication, the browser prompts for HTTP credentials before anything renders. Enter the username ara and the password from the credentials file.

The landing page is the playbook report list: one row per recorded run, with its status, start time, duration, the Ansible version and controller that ran it, and counts of hosts, tasks and results. Every column is a link that filters the list.

The ARA playbook report list, showing recorded runs with status, duration, controller and result counts

Click a run to open its report. This is where ARA earns its keep: the failed run below shows immediately which task failed, on which host, with the full result. The Summary panel gives the play, task, result, host and file counts, and Versions records the exact Ansible, ARA and Python versions that produced the run.

An ARA playbook report for a failed run, showing the failed task, host and result detail

The Tasks view searches and filters task results across every recorded run at once, by action, task name, path, status, or whether the result carried a deprecation, exception or warning.

The ARA task results view with its search and filter panel open

Recording your first playbook

The appliance ships ansible-core, the ARA callback and a small demo playbook, so you can prove the whole pipeline end to end without configuring anything. The image also includes a helper that does exactly that and checks the result:

sudo /usr/local/sbin/ara-record-check.sh
OK: authenticated record round-trip verified - recorded playbook id=11 (before=5 after=6), unauth=401, read-back=200

That helper confirms an unauthenticated read is refused, runs a real playbook through the ARA callback with the per-instance credential, then queries the API to confirm the run was recorded and reads it back by id.

The configuration that makes local runs record themselves lives in /etc/ara/ansible.cfg:

sudo grep -vE '^\s*#|^\s*$' /etc/ara/ansible.cfg
[defaults]
callback_plugins = /opt/ara/venv/lib/python3.12/site-packages/ara/plugins/callback
action_plugins = /opt/ara/venv/lib/python3.12/site-packages/ara/plugins/action
lookup_plugins = /opt/ara/venv/lib/python3.12/site-packages/ara/plugins/lookup
callbacks_enabled = ara_default
host_key_checking = False
interpreter_python = auto_silent
[ara]
api_client = http
api_server = http://127.0.0.1

Querying the REST API

Everything in the interface is also available over the REST API, which is browsable in a web browser and returns JSON to a client that asks for it. List the recorded runs:

PW=$(sudo grep '^ara.password=' /root/ara-credentials.txt | cut -d= -f2-)
curl -s -u "ara:$PW" -H 'Accept: application/json' 'http://127.0.0.1/api/v1/playbooks?limit=5' \
  | jq -r '"recorded playbooks: \(.count)", (.results[] | "  #\(.id)  \(.status)  \(.duration)  \(.path)")'
recorded playbooks: 6
  #11  completed  00:00:09.979665  /opt/ara/demo/record-demo.yml
  #10  completed  00:00:17.995068  /root/playbooks/patch-baseline.yml
  #9  completed  00:00:19.565372  /root/playbooks/deploy-web-tier.yml
  #8  failed  00:00:13.630470  /root/playbooks/nightly-backup.yml
  #7  completed  00:00:20.014845  /root/playbooks/deploy-web-tier.yml

Browsing to /api/v1/playbooks in a signed-in browser gives the same data through the browsable API, which is a fast way to explore the available endpoints and filters:

The browsable ARA REST API returning the recorded playbook list as JSON

Useful endpoints: /api/v1/playbooks, /api/v1/plays, /api/v1/tasks, /api/v1/hosts, /api/v1/results and /api/v1/records. Each supports filtering by the fields shown in the interface, plus order and limit.

Pointing your own Ansible at the recorder

Recording runs from the appliance itself proves the stack works, but the real deployment is your existing control nodes and CI runners reporting into it. On each control node, install the ARA client and export four environment variables. Nothing else changes about how you run playbooks.

pip install "ara[server]==1.8.0"
export ANSIBLE_CALLBACK_PLUGINS="$(python3 -m ara.setup.callback_plugins)"
export ARA_API_CLIENT=http
export ARA_API_SERVER=http://<public-ip>
export ARA_API_USERNAME=ara
export ARA_API_PASSWORD=your-administrator-password
ansible-playbook site.yml

Only the client is needed on a control node, so pip install ara is enough if you do not want the server extras there. Set the same four variables in your CI job environment and every pipeline run reports itself into the appliance automatically.

To make it permanent for a control node, put the equivalent into that machine's ansible.cfg instead:

[defaults]
callback_plugins = /path/to/ara/plugins/callback
callbacks_enabled = ara_default

[ara]
api_client = http
api_server = http://<public-ip>
api_username = ara
api_password = your-administrator-password

Because writing requires authentication, a control node that has not been given the credential cannot inject records into your history.

Adding more users

Every account is a Django user, so give each person or CI system its own rather than sharing the bootstrap administrator. Create one interactively:

sudo /usr/local/sbin/ara-manage createsuperuser --username alice --email alice@example.com
# enter <new-password> when prompted, then confirm it

The same wrapper runs any ARA server management command with the instance environment already loaded, for example sudo /usr/local/sbin/ara-manage changepassword ara to rotate the bootstrap credential once you have signed in.

Where things live

Path Contents
/opt/ara/venv Python virtual environment with ARA, ansible-core and gunicorn
/opt/ara/server/ansible.sqlite The recording database
/opt/ara/server/settings.yaml Server settings, including the two authentication requirements
/opt/ara/ara.env Per-instance environment, including the generated secret key
/etc/ara/ansible.cfg Ansible configuration that records the appliance's own runs
/etc/nginx/sites-available/cloudimg-ara The reverse proxy site
/root/ara-credentials.txt The per-instance administrator credential, mode 0600

Everything under /opt/ara is on the dedicated data volume:

findmnt -no SOURCE,TARGET,FSTYPE,SIZE /opt/ara
df -h /opt/ara | tail -1
/dev/nvme1n1 /opt/ara ext4   19.5G
/dev/nvme1n1     20G  164M   19G   1% /opt/ara

Confirm the versions shipped:

sudo /opt/ara/venv/bin/python -c 'from ara.setup import ara_version; print("ara", ara_version)'
sudo /opt/ara/venv/bin/ansible --version | head -1
ara 1.8.0
ansible [core 2.21.2]

Backing up the recorded history

The database is a single file, so a consistent backup is one command. Take it with the database's own online backup so a running server cannot produce a torn copy:

BK=$(mktemp -d)
sudo sqlite3 /opt/ara/server/ansible.sqlite ".backup '$BK/ara-backup.sqlite'"
ls -lh "$BK/ara-backup.sqlite"
sudo rm -rf "$BK"

Copy the resulting file to S3 on a schedule for off-instance retention. Because the whole data tier is on its own EBS volume, an EBS snapshot of that volume is the other good option and captures the application and the database together.

Resizing the data volume

The recording database grows with every playbook run, so plan to grow the data volume rather than the operating system disk. Modify the volume in the EC2 console or with aws ec2 modify-volume, then grow the filesystem in place with no downtime:

lsblk
sudo resize2fs /dev/nvme1n1
df -h /opt/ara

There is no partition table on the data volume, so resize2fs alone is enough; the device name may differ on your instance, so take it from lsblk.

Enabling HTTPS

The image serves plain HTTP on port 80 so it works the moment it launches. For anything beyond evaluation, terminate TLS. Two supported routes:

Application Load Balancer. Put the instance in a target group on port 80, attach an ACM certificate to an HTTPS listener, and let the load balancer terminate. The /healthcheck/ endpoint is the target group health check. This needs no change on the instance.

Let's Encrypt on the instance. Point a DNS A record at the instance, open port 443 in the security group, then:

sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d <your-domain> --agree-tos -m you@example.com --redirect

certbot rewrites the nginx site in place and installs a renewal timer. Once TLS is in place, update ARA_API_SERVER on your control nodes to the https:// URL so the credential is no longer sent in the clear.

Troubleshooting

Symptom Check
Browser prompts repeatedly for credentials The password is case-sensitive and 24 characters. Re-read it with sudo grep '^ara.password=' /root/ara-credentials.txt \| cut -d= -f2-.
502 Bad Gateway from nginx gunicorn is not running. systemctl status ara-web.service and journalctl -u ara-web.service -n 50.
Nothing recorded from a remote control node Confirm ARA_API_CLIENT=http is set (the default is offline, which writes locally instead), that ARA_API_SERVER is reachable on port 80 from that node, and that the credential is correct. Writing is authenticated, so a wrong password silently produces no records.
First boot appears not to have run systemctl status ara-firstboot.service and journalctl -u ara-firstboot.service. The sentinel is /var/lib/cloudimg/ara-firstboot.done.
/healthcheck/ is 200 but the interface is not That endpoint is intentionally unauthenticated and only reports liveness. A 401 on / is the correct, secure response, not a fault.

Support

cloudimg provides 24/7 technical support for this product with a guaranteed 24-hour response SLA. Our engineers help with deployment, credential rotation and user management, connecting your control nodes and CI runners, callback troubleshooting, DNS and TLS, data volume resizing, backups, retention and upgrades.

Email support@cloudimg.co.uk. Billing enquiries are handled through the same address.