Skip to main content
RunBook Academy

AnsibleXXVII · Collections, Galaxy and Dependency TrustCollections and dependency trust

requirements.yml and version pinning

Intermediate⏱ ~21 minansible-galaxy

What you'll learn

  • Write a requirements.yml for collections and roles with exact version pins
  • Use the version specifier syntax ansible-galaxy actually accepts, and recognise the syntax it rejects
  • Explain why an unpinned dependency makes today and tomorrow different changes
  • Find the transitive dependencies a pin does not cover, and pin those too

Prerequisites

Verified against ansible-core 2.21.x · ansible (community package) 14.x · Python (controller) 3.12+ · ansible-lint 26.x · Molecule 26.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-11

Not yet marked complete on this device.

Version pinning gets filed under tidiness, alongside consistent indentation and sorted lists. It is not tidiness. It is the difference between an automation repository that describes a change and one that describes an intention to run whatever is current.

Here is the whole argument in one sentence. Your repository has not changed since Tuesday. Your run on Thursday does something different. That is what an unpinned dependency buys you, and no amount of review discipline on your own code prevents it, because the change was not in your code.

The file

Read-only / Saferequirements.yml
---
collections:
- name: community.docker
  version: '4.7.0'
- name: ansible.posix
  version: '3.0.0'
- name: community.general
  version: '12.4.0'

roles:
- name: geerlingguy.postgresql
  version: '3.5.2'

Two top-level keys, both optional. collections: and roles: are resolved by different machinery — roles predate collections and use a different index — so a file with both is normal and each half is installed independently.

Configuration changeinstall from the file
$ ansible-galaxy collection install -r requirements.yml -p ./collections
Starting galaxy collection install process
Process install dependency map
Starting collection install process
Downloading https://galaxy.ansible.com/api/v3/plugin/ansible/content/published/collections/artifacts/community-docker-4.7.0.tar.gz to /home/opsuser/.ansible/tmp/ansible-local-1601101soz9l_f/tmpx0j7iwi0/community-docker-4.7.0-zbsncrpu
Installing 'community.docker:4.7.0' to '/home/opsuser/project/collections/ansible_collections/community/docker'
community.docker:4.7.0 was installed successfully
Downloading https://galaxy.ansible.com/api/v3/plugin/ansible/content/published/collections/artifacts/community-library_inventory_filtering_v1-1.1.5.tar.gz to /home/opsuser/.ansible/tmp/ansible-local-1601101soz9l_f/tmpx0j7iwi0/community-library_inventory_filtering_v1-1.1.5-rnua6vrl
Installing 'community.library_inventory_filtering_v1:1.1.5' to '/home/opsuser/project/collections/ansible_collections/community/library_inventory_filtering_v1'
community.library_inventory_filtering_v1:1.1.5 was installed successfully

Read that output again. You asked for one collection and two were installed. The second is a transitive dependency, and it is the subject of the second half of this lesson.

The specifier syntax, verified

The version key accepts exact versions and comma-separated range expressions. An exact pin:

Read-only / Safethe pin this course teaches
collections:
- name: community.docker
  version: '4.7.0'

A range resolves at install time to whatever is newest inside it:

Configuration changea range, resolved today
$ ansible-galaxy collection install -r req.yml -p ./gx --no-deps
Downloading https://galaxy.ansible.com/api/v3/plugin/ansible/content/published/collections/artifacts/community-crypto-2.26.9.tar.gz to /home/opsuser/.ansible/tmp/ansible-local-182176gyjuqyyk/tmpf9q18xw4/community-crypto-2.26.9-mk4xhvbf
Installing 'community.crypto:2.26.9' to '/home/opsuser/project/gx/ansible_collections/community/crypto'
community.crypto:2.26.9 was installed successfully

The file said '>=2.0.0,<3.0.0'. What landed was 2.26.9 — because that is the newest 2.x today. Run the same file next quarter and you get 2.27.something, from the same unchanged commit. The range is a policy; the pin is a fact.

Not every syntax you have met elsewhere is accepted. The npm-style caret is not:

Read-only / Safea specifier that is rejected
$ ansible-galaxy collection install -r req-caret.yml -p ./gx3
[ERROR]: Non integer values in LooseVersion ('^2.0.0')
Starting galaxy collection install process
Process install dependency map

Use >=, >, <=, <, !=, ==, *, and commas to combine them. '*' means “any”, which is also what you get when you omit version entirely — and it is the state this lesson exists to argue against.

The flags that matter

FlagWhat it doesWhen you want it
-r requirements.ymlInstall everything in the fileAlways, in a repository
-p ./collectionsInstall to this path instead of the first configured oneAlways, on a shared controller
-U, --upgradeUpgrade installed collections, and their dependencies unless --no-depsDeliberately, as its own reviewed change
-n, --no-depsDo not install declared dependenciesBuilding an offline bundle, or when you pin the dependencies yourself
--preConsider pre-release versions, which are ignored by defaultTesting an upcoming release, never in production
-f, --forceOverwrite an existing collectionRepairing a partial install
--offlineInstall from local tarballs without contacting any serverAir-gapped install; lesson 7

The pin that does not hold

Back to the two collections from one request. Where did the second one come from? From inside the first one’s manifest:

Read-only / Safethe dependency declaration
$ python3 -c "import json; m=json.load(open('collections/ansible_collections/community/docker/MANIFEST.json')); print(m['collection_info']['dependencies'])"
{'community.library_inventory_filtering_v1': '>=1.0.0'}

>=1.0.0. Unbounded. You pinned community.docker to exactly 4.7.0, and the collection you pinned brings in a second collection with a range that will resolve to whatever is newest whenever the install runs.

Your requirements.yml is pinned. Your controller is not.

Finding what you actually installed

The fix is mechanical. Install once, list what landed, and pin all of it:

Read-only / Safethe real installed set
$ ansible-galaxy collection list -p ./collections --format json
{"/home/opsuser/project/collections/ansible_collections": {"community.docker": {"version": "4.7.0"}, "community.library_inventory_filtering_v1": {"version": "1.1.5"}}}

Then write every line of that into requirements.yml with an exact version, and install with --no-deps so the resolver takes your list rather than re-deriving one:

Read-only / Saferequirements.yml, fully pinned
---
# Direct dependencies.
collections:
- name: community.docker
  version: '4.7.0'

# Transitive: pulled in by community.docker, which declares it as
# '>=1.0.0'. Pinned here so a rebuild reproduces this controller
# rather than today's newest.
- name: community.library_inventory_filtering_v1
  version: '1.1.5'
Configuration changeinstall exactly this list and nothing else
ansible-galaxy collection install -r requirements.yml -p ./collections --no-deps

The comment is not decoration. In eighteen months somebody will find a collection in that file that no playbook imports and delete it, and the comment is the only thing that stops them.

Knowledge check

Knowledge check · 4 questions

  1. Q1. requirements.yml pins community.docker to exactly 4.7.0. A controller rebuilt from that file six months later behaves differently. What is the most likely cause?

  2. Q2. Which statements about ansible-galaxy collection install are accurate? Select all that apply.

  3. Q3. version: '>=2.0.0,<3.0.0' in requirements.yml produces a reproducible install.

  4. Q4. What is the reviewable way to move a collection from 4.7.0 to 4.8.0 on a production controller?

Passing score: 75%. Answers are checked in this browser.