Skip to content
CloudOps
Newsletter
All guides
AI for Ansible By James Joyner IV · · 11 min read

Migrating Ansible Modules to FQCN Before a Core Upgrade With AI

Use AI to safely migrate short-name Ansible modules to FQCN before an ansible-core upgrade, pin collections, and verify with ansible-lint and syntax-check.

  • #ansible
  • #ai
  • #fqcn
  • #ansible-lint
  • #automation

The warning showed up on a Tuesday, buried in a thousand lines of green: [DEPRECATION WARNING]: Collection community.general v8.x will no longer support short module name 'mysql_user'. I was mid-deploy on the OpenStack control plane, and the play still worked, so I ignored it like every other engineer on the team had for a year. Then the platform group announced an ansible-core 2.18 bump for the shared runners, and I realized our 200-odd roles were leaning hard on short module names that the new core was about to stop resolving the way we expected. That’s the trap with FQCN migrations: nothing is broken today, and everything is quietly fragile tomorrow. AI turned a multi-day grind into an afternoon of review — but only because I refused to let it merge anything I hadn’t read.

Why Short Names Are a Time Bomb

When you write apt: or mysql_user:, Ansible resolves that short name through a search path defined by collections keywords, the ansible.builtin namespace, and whatever’s installed. For a long time that worked because the resolution order was forgiving. The problem is that it’s implicit. If two installed collections both ship a mysql_user module, the one that wins depends on install order and configuration you probably didn’t audit. That’s silent module shadowing, and it’s the single scariest thing about deferring this work.

Here’s the concrete risk. You think mysql_user means community.mysql.mysql_user. But community.general historically shipped database modules too, and on a runner where it loads first, your “create a read-only reporting user” task can silently bind to a different module with different parameter handling. No error. No warning that matters. Just a user created with subtly wrong privileges, discovered three weeks later by your security scanner.

Fully qualified collection names kill this ambiguity dead. community.mysql.mysql_user resolves to exactly one thing, forever, regardless of install order, core version, or what some other role pulled into the environment. Before a core upgrade — where deprecated short-name routing is exactly the kind of thing that gets tightened — FQCN isn’t cleanup, it’s insurance.

Let AI Do the Decoding, Not the Deciding

The hard part of this migration isn’t mechanical find-and-replace. It’s the mapping: which collection does each short name actually belong to in your environment? user is ansible.builtin.user, but firewalld is ansible.posix.firewalld and pacman is community.general.pacman. Get the namespace wrong and you’ve traded one silent failure for another.

This is where AI earns its seat. I dumped a representative role and asked for the mapping with a hard constraint: don’t guess.

You are helping migrate Ansible tasks to fully qualified collection names (FQCN) ahead of an ansible-core 2.18 upgrade. For each task below, give me the FQCN. Rules: only use namespaces from ansible.builtin, ansible.posix, community.general, and community.mysql. If a module could plausibly live in more than one collection, flag it and tell me which ansible-doc -l command I should run to confirm — do not pick one silently. Output a table: short_name | proposed_fqcn | confidence | verify_command.

The “flag ambiguity, don’t pick silently” instruction is the whole game. AI is genuinely good at the unambiguous 80% — copy, template, service, user — and it’s fast at producing the verify command for the other 20%. What it must never do is make the shadowing decision for you. That’s a human call backed by ansible-doc.

The Before and After

Here’s a typical pre-migration task block. Looks fine. Runs fine. Routes implicitly.

- name: Configure database tier
  hosts: db
  become: true
  tasks:
    - name: Install MariaDB
      apt:
        name: mariadb-server
        state: present

    - name: Ensure firewalld allows mysql
      firewalld:
        service: mysql
        permanent: true
        state: enabled
        immediate: true

    - name: Create reporting user
      mysql_user:
        name: reporting
        priv: "analytics.*:SELECT"
        state: present

And the FQCN version, where every module names its home explicitly:

- name: Configure database tier
  hosts: db
  become: true
  tasks:
    - name: Install MariaDB
      ansible.builtin.apt:
        name: mariadb-server
        state: present

    - name: Ensure firewalld allows mysql
      ansible.posix.firewalld:
        service: mysql
        permanent: true
        state: enabled
        immediate: true

    - name: Create reporting user
      community.mysql.mysql_user:
        name: reporting
        priv: "analytics.*:SELECT"
        state: present

Three modules, three different collections. That mysql_user line is exactly the kind of task that would have shadowed silently. Now it can’t.

Pin Your Collections or the Migration Is a Lie

FQCN only buys you safety if the named collections are actually present and at known versions. An explicit community.mysql.mysql_user reference fails loudly if the collection is missing — which is good — but you don’t want that failure showing up at deploy time. Declare and pin your dependencies in a requirements.yml:

---
collections:
  - name: ansible.posix
    version: ">=1.5.4,<2.0.0"
  - name: community.general
    version: ">=8.0.0,<9.0.0"
  - name: community.mysql
    version: ">=3.9.0,<4.0.0"

Install them into a project-local path so you’re testing against the same versions CI uses:

ansible-galaxy collection install -r requirements.yml -p ./collections

Pinning with bounded ranges matters more during a core upgrade than usual. A major collection bump can move or remove modules, and you do not want your FQCN migration and a collection major to land in the same change. Freeze the collections, migrate to FQCN, verify, then tackle collection majors as a separate, reviewable step.

Make ansible-lint Enforce It

You don’t want to police FQCN by hand across 200 roles, and you definitely don’t want it creeping back in on the next PR. ansible-lint ships fqcn rules that catch every short name. Turn them into hard failures:

# .ansible-lint
profile: production

enable_list:
  - fqcn[action-core]
  - fqcn[action]
  - fqcn[canonical]

exclude_paths:
  - collections/

Then run it across the repo:

ansible-lint --offline -p roles/ playbooks/

The fqcn[canonical] rule is the sharp one — it flags cases where you used a qualified name but not the canonical one (for example, an old redirect path that still resolves but shouldn’t be trusted across a core bump). That’s the rule that catches mistakes AI is most likely to make, which is why I let the linter, not the model, have the final word. If you’re newer to wrangling lint output, I wrote up the broader workflow in taming ansible-lint with AI — the same human-verifies-the-machine loop applies here.

Verify Twice: Syntax-Check and Lint

Linting proves your style is right. It does not prove your plays still parse and resolve under the new core. For that, run --syntax-check against the actual ansible-core you’re upgrading to, ideally in a throwaway venv:

python3 -m venv /tmp/core218 && . /tmp/core218/bin/activate
pip install 'ansible-core>=2.18,<2.19'
ansible-galaxy collection install -r requirements.yml -p ./collections

for pb in playbooks/*.yml; do
  echo "== $pb =="
  ansible-playbook --syntax-check "$pb" || exit 1
done

--syntax-check loads the play, resolves module names through the real plugin loader, and fails if an FQCN points at a module that isn’t installed or doesn’t exist under that core. It’s the cheapest possible proof that your migration resolves cleanly before a single task touches a host. Pair it with a --check dry run on a canary host for the modules that support check mode, and you’ve covered both “does it parse” and “does it intend the right thing.”

My loop ends up being: AI proposes the mapping, I confirm every ambiguous one with ansible-doc, I apply the edits, ansible-lint enforces the style, and --syntax-check against the target core proves resolution. Four gates, and AI only owns the first one.

What Stays In Human Hands

The pattern that keeps this safe is boring on purpose. AI is a fast, tireless decoder of which collection a module belongs to and a decent drafter of the bulk edits. It is not the thing that decides how a shadowed mysql_user should resolve, and it never merges without the linter and syntax-check agreeing. Every place where ambiguity could bite — the exact cases where short names were dangerous — is exactly where I make the model stop and hand me a verify command.

If you’re standing up this kind of migration across a fleet, browse the rest of the Ansible articles for the surrounding tooling, and grab the reusable migration and audit prompts from the prompt library so you’re not rewriting that “flag ambiguity, don’t pick silently” instruction every time. Do the FQCN work before the core upgrade, pin your collections, and let the machine draft while you stay the one who verifies. That’s the only way this scales without trading loud failures for silent ones.

Free download · 368-page PDF

Download the Free 500-Prompt DevOps AI Toolkit

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.