Git, CI/CD & GitOpsLI · Ansible CIFormatAndValidate
ansible-lint and FQCN rules — fully qualified collection names as a convention
What you'll learn
- Explain why bare module names resolve differently across collections and the failure mode that causes
- Write a module reference using its fully qualified collection name (FQCN)
- Identify the ansible-lint rule families that enforce FQCN and run them as a PR gate
- Migrate an existing repository from bare names to FQCN using ansible-lint --fix
Prerequisites
Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x
A playbook written ten years ago that says copy: src: ... dest: ... would work on any reasonably modern Ansible because copy was always a builtin module and bare names resolved to builtins. A playbook written today that says copy works the same way for the same reason, but the assumption that bare names resolve to builtins is no longer safe: collections from ansible_collections/ directories, Galaxy-installed collections, and requirements.yml files all participate in module resolution, and a module named copy may now exist in more than one collection. The convention that closes this gap is the fully qualified collection name, FQCN, and ansible-lint enforces it.
Why bare names are ambiguous
Ansible’s module loader walks a configurable search path of collections. When a playbook task says copy:, the loader asks each collection in the path, in order, whether it ships a module called copy. The first collection that does wins. This works well when only one collection ships copy; it works dangerously when more than one does, because the resolution depends on the order, which depends on the install state, which depends on the runner.
flowchart LR
A[Task: copy] --> B[Module loader]
B --> C["ansible.builtin"]
C --> C1[Has copy? yes]
C1 --> D[Use ansible.builtin.copy]
D --> E[Apply]
The same task on a runner that has community.general installed before ansible.builtin in the search path, and where community.general happens to ship a copy module, would resolve differently. The playbook is unchanged; the behaviour depends on the runner. This is the failure mode that FQCN closes.
What an FQCN looks like
A fully qualified collection name has the form namespace.collection.module_name. Examples:
ansible.builtin.copy- thecopymodule from thebuiltincollection inside theansiblenamespace.ansible.builtin.file- thefilemodule, also fromansible.builtin.community.general.parted- thepartedmodule from thecommunity.generalcollection.community.docker.docker_container- thedocker_containermodule fromcommunity.docker.amazon.aws.ec2_instance- theec2_instancemodule from the cloud-specific AWS collection.
The FQCN names one module in one collection. There is no search path involved; the loader goes directly to that collection and asks for that module. If the collection is not installed, the loader raises an error at parse time rather than silently picking a different module.
The two namespaces every Ansible engineer encounters are:
ansible.builtin- the modules that ship with Ansible itself.copy,file,service,package,command,shell,template,lineinfile,assert,debug,set_fact, and the rest. These are always available and are the right FQCN for builtin modules.community.*- modules maintained by the Ansible community.community.general,community.docker,community.crypto,community.postgresql, and many more. These are installed viarequirements.ymloransible-galaxy collection install.
The ansible-lint fqcn rule family
ansible-lint ships a family of rules that flag bare module names and require FQCN. The named rules are:
fqcn[action-core]- tasks using a builtin module without FQCN.fqcn[action-community]- tasks using a community module without FQCN.fqcn[key-value]- key-value pairs (module options) using a bare plugin name where an FQCN is required.fqcn[module-core]- top-level module references outside tasks that are bare.fqcn[module-community]- same, for community modules.
In the default and stricter profiles, all four are enabled. Running ansible-lint on a playbook that uses bare names produces findings that name the module and the suggested FQCN. The fix is mechanical: replace copy: with ansible.builtin.copy:, replace parted: with community.general.parted:, and so on.
To see exactly which rules are enabled in the active config:
ansible-lint --rules
The output is the authoritative list of what the gate will flag.
Migrating an existing repository
The migration from bare names to FQCN is mechanical but voluminous. ansible-lint ships a fixer that rewrites bare module references to FQCN:
ansible-lint --fix
The fixer walks every playbook, role task, and handler, and rewrites bare names to the FQCN the rules resolve. For builtins, every copy: becomes ansible.builtin.copy:. For community modules, every parted: becomes community.general.parted:. The fixer is conservative: it does not invent FQCNs for modules it cannot resolve, and it leaves those modules for the engineer to address manually.
The production pattern for the migration:
- Run the gate in report-only mode first. Run
ansible-lintwithout--fixand capture the count of findings. This is the migration’s blast radius. - Run the fixer. Commit the result as one or more reviewable commits, not as a single “ansible-lint migration” commit that mixes hundreds of unrelated changes.
- Re-run the gate. Confirm zero findings; if any remain, address them manually.
- Enable the FQCN rules in CI. Only after the migration is complete should a red finding on FQCN be a merge blocker.
Doing the migration in the opposite order - enabling the gate first - produces a red CI badge on every existing PR until the migration is done. The gate that fails on legacy code is a gate the team learns to bypass.
What FQCN does not solve
FQCN closes the ambiguity in module resolution. It does not close:
- That the collection named by the FQCN is installed at apply time. A playbook that uses
community.docker.docker_containerbut does not declarecommunity.dockerincollections/requirements.ymlwill fail at parse time, but the failure is on the runner, not in CI, unless CI also runsansible-galaxy collection install -r collections/requirements.yml. - That the version of the installed collection matches the version the playbook was written against. The FQCN does not pin versions;
collections/requirements.ymldoes. - That two collections do not ship modules with overlapping names within their own FQCNs. This is a community convention problem, not an ansible-lint rule.
These are the gaps that collections/requirements.yml with pinned versions, plus a Molecule scenario that converges the role against an ephemeral host with the same requirements.yml, exists to close.
Production discipline
- Every module reference in every playbook, role, and handler uses FQCN. Bare names are not allowed in production repositories.
- The FQCN rule family is enabled in the active
.ansible-lintprofile. Disabling the rules requires a written exception in the PR. collections/requirements.ymlpins every collection the playbook references. A new FQCN in a task is a new entry in the requirements file in the same PR.ansible-lint --fixis a migration tool, not a permanent workflow. Once the repository is on FQCN, the fixer is run only when a new rule is adopted.# noqa: fqcn[...]requires a comment explaining the suppression. Bare suppressions do not survive code review.
Cross-course references
- Ansible for Production Sysadmins - Part VIII (Modules) covers how the module loader resolves a bare name; FQCN is the explicit alternative.
- Ansible for Production Sysadmins - Part XXVII (Collections) covers the
collections/requirements.ymlfile that pins the FQCNs to specific versions. - Ansible for Production Sysadmins - Part XXXVIII (GitCI) is the pipeline that runs the FQCN gate; this lesson is the rule that gate enforces.
Quiz
Knowledge check · 4 questions
Q1. A task says `copy: src: ... dest: ...`. A second collection, `community.example`, is installed on the runner and ships a module also named `copy`. What does the bare-name task resolve to?
Q2. FQCN is purely a stylistic convention and has no effect on which module Ansible executes.
Q3. Name the four ansible-lint rules in the fqcn rule family, and state the production-correct command for migrating an existing repository from bare names to FQCN.
Q4. Diagnose a production incident where bare module names resolved to the wrong module, and identify the gate that should have caught it before merge.
A team maintains a 200-task Ansible playbook with bare module names throughout. The runner used in CI installs `community.general` but the production runners do not. The playbook contains a task `parted: ...` that the team wrote assuming it was the `ansible.builtin` `parted` (which does not exist; `parted` lives in community.general). The playbook ran on CI without error because community.general was installed. On the production runner without community.general, the task resolves to nothing and the playbook fails at apply time. The team concludes that the playbook is correct and the runner is misconfigured.
Passing score: 75%. Answers are checked in this browser.