Use Ansible Collections Directly from Source During Development¶
I've recently been working on writing an Ansible collection from scratch. The collection ships everything in a single repo – the collection itself, example playbooks, roles, and supporting content. As it matured I started running it through Ansible Automation Platform (AAP) alongside local development, and that's when a friction point I hadn't anticipated started to slow me down.
I'd been using a Makefile to build and install the collection locally before testing it. Every change - a tweak to a module, an update to a role - meant stopping, rebuilding, reinstalling, then running the playbook. It was interrupting the flow constantly. But I couldn't just rip that out, because I also needed AAP to be able to pull the collection directly from the repo without a manual build step. I needed a setup that worked for both, without breaking either.
When you use AAP with a project repo that also contains the collection you're developing, you end up with a constraint that makes local development awkward. This post explains the problem and how to solve it.
The AAP Setup¶
AAP is designed to pull directly from a source control repo and run playbooks from it. How it resolves collections depends on whether you're using a custom Execution Environment (EE) or not.
With a custom EE, the collection is baked into the EE image at build time. AAP uses whatever version is in the image and the project repo plays no role in collection resolution.
Without a custom EE (or during early development before one exists), you can add a collections/requirements.yml to the repo that points AAP at the project root:
Before each job run, AAP processes this file and installs the collection from the repo root into its execution environment automatically. No build step, no separate publish step; AAP just uses the source directly.
The Local Development Problem¶
Locally, Ansible doesn't process collections/requirements.yml automatically. It searches a list of paths (the collections_path) for a directory tree that looks like:
To satisfy that, you have to manually run ansible-galaxy collection install to copy your source files into that path. Change a module? Copy again. Change a role? Copy again. It breaks the tight feedback loop you want when developing.
The goal is to make local development work the same way AAP does in the no-EE case: using the source directly, with no intermediate step.
The Fix: Symlink + Collections Path¶
Instead of copying, you can create a symlink that points the expected install location back at the live source. Ansible follows the symlink and reads your files directly. No copy needed.
The one gotcha is that the symlink can't live inside the repo without creating a circular reference. The solution is to put it in your home directory:
mkdir -p ~/.ansible/dev_collections/ansible_collections/tenthirtyam
ln -sfn /path/to/your/repo ~/.ansible/dev_collections/ansible_collections/tenthirtyam/example
Then tell Ansible to look there by setting collections_path in ansible.cfg:
Now every edit you make is instantly live - Ansible reads straight from your working tree, exactly the same way AAP does in the no-EE case. And because the collections/requirements.yml in the repo is untouched, AAP continues to work exactly as before.
Automating It with a Makefile¶
If your project already uses a Makefile, you can wrap the setup into dev and dev-clean targets so the whole thing is a single command. Here's a minimal standalone example you can drop in:
COLLECTION := tenthirtyam.example
PROJECT_PATH := .
COLLECTION_PATH := $(PROJECT_PATH)/collections/ansible_collections/$(subst .,/,$(COLLECTION))
NAMESPACE_PATH := $(patsubst %/,%,$(dir $(COLLECTION_PATH)))
DEV_COLLECTIONS_ROOT := $(HOME)/.ansible/dev_collections
DEV_COLLECTION_PATH := $(DEV_COLLECTIONS_ROOT)/ansible_collections/$(subst .,/,$(COLLECTION))
DEV_NAMESPACE_PATH := $(patsubst %/,%,$(dir $(DEV_COLLECTION_PATH)))
.PHONY: dev dev-clean
dev:
@echo "→ Setting up development environment for $(COLLECTION)..."
@mkdir -p $(DEV_NAMESPACE_PATH)
@rm -rf $(DEV_COLLECTION_PATH)
@ln -sfn $(abspath $(PROJECT_PATH)) $(DEV_COLLECTION_PATH)
@[ -L "$(COLLECTION_PATH)" ] && rm -f "$(COLLECTION_PATH)" || true
@rmdir "$(NAMESPACE_PATH)" 2>/dev/null || true
@rmdir "$(PROJECT_PATH)/collections/ansible_collections" 2>/dev/null || true
@printf '[defaults]\ncollections_path = ~/.ansible/dev_collections:~/.ansible/collections\n' > ansible.cfg
@printf 'export ANSIBLE_COLLECTIONS_PATH=~/.ansible/dev_collections:~/.ansible/collections\n' > dev-env.sh
@echo "✓ Development environment ready."
@echo " Symlink: $(DEV_COLLECTION_PATH) -> $(abspath $(PROJECT_PATH))"
@echo " Edit files directly - no rebuild or reinstall needed."
@echo ""
@echo " If ansible.cfg is ignored (world-writable directory warning), run:"
@echo " source dev-env.sh"
dev-clean:
@echo "→ Removing development environment..."
@rm -rf $(DEV_COLLECTION_PATH)
@rmdir $(DEV_NAMESPACE_PATH) 2>/dev/null || true
@rmdir $(DEV_COLLECTIONS_ROOT)/ansible_collections 2>/dev/null || true
@rmdir $(DEV_COLLECTIONS_ROOT) 2>/dev/null || true
@rm -f ansible.cfg dev-env.sh
@echo "✓ Development environment removed."
Running make dev creates the symlink, writes ansible.cfg, and generates a dev-env.sh for WSL users. Running make dev-clean tears it all back down.
Add both ansible.cfg and dev-env.sh to your .gitignore - they're machine-specific and shouldn't be committed.
WSL / World-Writable Directories¶
On Windows Subsystem for Linux, drives are mounted with 777 permissions. Ansible treats world-writable directories as untrusted and silently ignores ansible.cfg with this warning:
[WARNING]: Ansible is being run in a world writable directory,
ignoring it as an ansible.cfg source.
The workaround is to export ANSIBLE_COLLECTIONS_PATH as an environment variable instead. Ansible always honors env vars regardless of directory permissions:
The dev target generates a dev-env.sh you can source for this. Add the export to your ~/.bashrc or ~/.zshrc to make it permanent.
Summary¶
| Scenario | How the collection is found |
|---|---|
| AAP with custom EE | Baked into the EE image at build time |
| AAP without custom EE | collections/requirements.yml → installed from repo root at runtime |
| Local (macOS / Linux) | ansible.cfg → collections_path → symlink → repo root |
| Local (WSL / Windows) | ANSIBLE_COLLECTIONS_PATH env var → symlink → repo root |
The end result for the no-EE cases is the same: Ansible is reading the collection straight from the source repo, with no build or copy step in between.