Skip to content
Jul 7 / Greg

Building a Custom Execution Environment with Claude Code and a Private Automation Hub


I’ve been building Execution Environments (EEs) for Ansible Automation Platform for a while now, but this time I wanted to see how far I could get by handing the whole thing to Claude Code and just reacting to what came back. I’m using the Ansible Automation Platform version 2.6 for this demo, and I’m putting certified and validated content into the custom EE. I’ve noticed that about 75% of the time with a custom EE I run into issues that takes me a few iterations to fix, so I wanted to farm out the work to AI.

I have a blog post on syncing certified/verified collections into Private Automation Hub here.

The Starting Point

This is the execution-environment.yml file I use with ansible-builder:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
---
version: 3
 
images:
  base_image:
    name: registry.redhat.io/ansible-automation-platform-26/ee-minimal-rhel9:latest
 
dependencies:
  system:
    - python3
    - python3-pip
    - python3-setuptools
    - python3-wheel
    - python3-devel
    - gcc
    - krb5-devel
    - krb5-libs
    - systemd-devel
 
  galaxy:
    collections:
      - name: ansible.platform
        version: "2.6.20260504"        # match your AAP 2.6 target
      - name: ansible.hub
        version: ">=1.0.0"
      - name: ansible.controller
        version: ">=4.7.0"      # 4.7.0+ for AAP 2.6
      - name: ansible.eda
        version: ">=2.5.0"
      - name: infra.aap_configuration
 
  python:
    - python-dateutil>=2.7.0
    - awxkit
    - pyyaml>=6.0.1
 
options:
  package_manager_path: /usr/bin/microdnf
 
additional_build_steps:
  prepend_galaxy:
    - ENV ANSIBLE_GALAXY_SERVER_LIST=rh_certified,validated
    - ENV ANSIBLE_GALAXY_SERVER_RH_CERTIFIED_URL=https://aap.example.com/api/galaxy/content/rh-certified/
    - ENV ANSIBLE_GALAXY_SERVER_VALIDATED_URL=https://aap.example.com/api/galaxy/content/validated/
    - ARG ANSIBLE_GALAXY_SERVER_RH_CERTIFIED_TOKEN
    - ENV ANSIBLE_GALAXY_SERVER_RH_CERTIFIED_TOKEN=$ANSIBLE_GALAXY_SERVER_RH_CERTIFIED_TOKEN
    - ARG ANSIBLE_GALAXY_SERVER_VALIDATED_TOKEN
    - ENV ANSIBLE_GALAXY_SERVER_VALIDATED_TOKEN=$ANSIBLE_GALAXY_SERVER_VALIDATED_TOKEN
    - ENV ANSIBLE_GALAXY_DISABLE_GPG_VERIFY=1

The magic happens at the end where I’m setting up the environment variables to grab the tokens for my certified and validated repos. The old way was to hard code this in an ansible.cfg file, but you really don’t want to pass that to your AI, so doing it this way keeps things clean.

Notice the split: the URLs are plain ENV values baked right into the YAML, because they’re not sensitive. The tokens are declared as ARG first, then re-exported as ENV of the same name because ansible-galaxy only reads environment variables, not build args directly. The actual token value never touches this file; it only gets supplied at build time via --build-arg.

This matters for another reason too: ansible-builder generates a multi-stage Containerfile. The stage that installs collections (galaxy) is discarded the final image only does a plain filesystem COPY --from=galaxy /usr/share/ansible /usr/share/ansible, not a full FROM galaxy. Docker/Podman ENV/ARG values are stage-scoped, so the token doesn’t end up as an inspectable environment variable in the shipped image. It does still exist transiently in that stage’s build cache layer, which is worth knowing if your threat model cares about that.

Problem 1: TLS Certificate Verification

Once auth was wired up, the build got further but hit an SSL error reaching the PAH:

1
CERTIFICATE_VERIFY_FAILED: unable to get local issuer certificate

My PAH uses an internal CA that isn’t in the trust store of the minimal RHEL9 build image. The “proper” fix is importing the CA cert into the image’s trust store before the collection install step. The fast fix appropriate for an internal, already-authenticated private Hub is telling `ansible-galaxy` to skip cert validation for that call. Ansible Builder already exposes a hook for this via the ANSIBLE_GALAXY_CLI_COLLECTION_OPTS build-arg, which gets appended straight onto the ansible-galaxy collection install command:

1
2
3
ansible-builder build --tag infra-cac \
  --build-arg ANSIBLE_GALAXY_CLI_COLLECTION_OPTS="--ignore-certs" \
  ...

Keeping your token secure

Claude refused to build with the token exposed to it…which is what you would want, but I still need it to interact with ansible-builder. At several points the build needed a live Automation Hub token passed via --build-arg.

The trick is a tiny wrapper script that sources a local, gitignored secrets file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/usr/bin/bash
set -euo pipefail
 
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SECRETS_FILE="$SCRIPT_DIR/.secrets/hub-tokens.env"
 
if [ ! -f "$SECRETS_FILE" ]; then
    echo "Missing $SECRETS_FILE" >&2
    exit 1
fi
 
source "$SECRETS_FILE"
 
cd "$SCRIPT_DIR"
ansible-builder build --tag infra-cac \
  --build-arg ANSIBLE_GALAXY_SERVER_RH_CERTIFIED_TOKEN="$ANSIBLE_GALAXY_SERVER_RH_CERTIFIED_TOKEN" \
  --build-arg ANSIBLE_GALAXY_SERVER_VALIDATED_TOKEN="$ANSIBLE_GALAXY_SERVER_VALIDATED_TOKEN" \
  --build-arg ANSIBLE_GALAXY_CLI_COLLECTION_OPTS="--ignore-certs"

And the secrets file itself, created by hand not by the AI, and never pasted into the chat with restrictive permissions:

1
2
3
4
umask 077 && cat > .secrets/hub-tokens.env <<'EOF'
export ANSIBLE_GALAXY_SERVER_RH_CERTIFIED_TOKEN='your-token-here'
export ANSIBLE_GALAXY_SERVER_VALIDATED_TOKEN='your-token-here'
EOF

From that point on, all I had to tell Claude was “run build.sh” it never saw, typed, or logged the actual secret. The file path is the only thing that ever appears in a command. Add .secrets/ to .gitignore and you’ve got a repeatable, safe build process that an AI agent can drive end-to-end.

Cleaning Up After Yourself

Every failed build attempt leaves behind a dangling, untagged image layer. After a dozen or so iterations chasing the errors above, podman image list was full of <none><none> entries eating disk space. Since these aren’t referenced by any tagged image, they’re safe to clear in one shot:

1
podman image prune -f

Conclusion

I can now quickly build my execution-environment.yml file, and hand it off to claude code build VM to just crank away and iterate until it can successfully build the EE…what a time to be alive LOL.

If you have any questions, comments, or modifications, drop me a line.
Good luck and happy EE creation.

Leave a Comment

 

*