Skip to main content
RunBook Academy

Git, CI/CD & GitOpsLI · Ansible CIFormatAndValidate

ansible-lint and FQCN rules — fully qualified collection names as a convention

Intermediate⏱ ~23 mingitansibleansible-lint

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

Not yet marked complete on this device.

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 - the copy module from the builtin collection inside the ansible namespace.
  • ansible.builtin.file - the file module, also from ansible.builtin.
  • community.general.parted - the parted module from the community.general collection.
  • community.docker.docker_container - the docker_container module from community.docker.
  • amazon.aws.ec2_instance - the ec2_instance module 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 via requirements.yml or ansible-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:

  1. Run the gate in report-only mode first. Run ansible-lint without --fix and capture the count of findings. This is the migration’s blast radius.
  2. 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.
  3. Re-run the gate. Confirm zero findings; if any remain, address them manually.
  4. 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_container but does not declare community.docker in collections/requirements.yml will fail at parse time, but the failure is on the runner, not in CI, unless CI also runs ansible-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.yml does.
  • 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

  1. Every module reference in every playbook, role, and handler uses FQCN. Bare names are not allowed in production repositories.
  2. The FQCN rule family is enabled in the active .ansible-lint profile. Disabling the rules requires a written exception in the PR.
  3. collections/requirements.yml pins every collection the playbook references. A new FQCN in a task is a new entry in the requirements file in the same PR.
  4. ansible-lint --fix is a migration tool, not a permanent workflow. Once the repository is on FQCN, the fixer is run only when a new rule is adopted.
  5. # 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.yml file 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

  1. 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?

  2. Q2. FQCN is purely a stylistic convention and has no effect on which module Ansible executes.

  3. 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.

  4. 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.