AnsiblePilot — Master Ansible Automation

AnsiblePilot is the leading resource for learning Ansible automation, DevOps, and infrastructure as code. Browse over 1,400 tutorials covering Ansible modules, playbooks, roles, collections, and real-world examples. Whether you are a beginner or an experienced engineer, our step-by-step guides help you automate Linux, Windows, cloud, containers, and network infrastructure.

Popular Topics

About Luca Berton

Luca Berton is an Ansible automation expert, author of 8 Ansible books published by Apress and Leanpub including "Ansible for VMware by Examples" and "Ansible for Kubernetes by Example", and creator of the Ansible Pilot YouTube channel. He shares practical automation knowledge through tutorials, books, and video courses to help IT professionals and DevOps engineers master infrastructure automation.

Ansible ansible.builtin vs ansible.legacy: Collection Namespaces Explained

By Luca Berton · Published 2024-01-01 · Category: installation

Understand ansible.builtin vs ansible.legacy namespaces. Learn when to use FQCN, how module resolution works, and migrate to fully qualified collection names.

Ansible ansible.builtin vs ansible.legacy: Collection Namespaces Explained

What is the "ansible.builtin" Ansible collection? What is the "ansible.legacy" collection? Today we're going to talk about Ansible's most essential modules and plugins and how we can use them in our everyday Playbook.

What is ansible.builtin collection?

The "ansible.builtin" collection refers to modules & plugins shipped with ansible-core. Technically is a synthetic collection, virtually constructed by the core engine.

See also: Ansible Multi-Line Strings: Literal (|) & Folded (>) Block Scalars Guide

What is ansible.legacy collection?

The "ansible.legacy" collection is a superset of "ansible.builtin" with 'custom' plugins in the configured paths and adjacent directories. We use the "ansible.legacy" when we don't specify any Ansible collection in our playbook. Technically is a synthetic collection, virtually constructed by the core engine.
  • https://docs.ansible.com/ansible/latest/reference_appendices/faq.html#what-is-the-difference-between-ansible-legacy-and-ansible-builtin-collections
  • https://docs.ansible.com/ansible/latest/reference_appendices/config.html#default-action-plugin-path

See also: Bard-ing with Ansible: Streamlining Testing for Google's AI Writing Tool

Demo

Live Playbook about "ansible.builtin" vs. "ansible.legacy" collections. Let's jump in a quick Playbook to demonstrate the difference between the "ansible.builtin" vs. "ansible.legacy" collections. I'm going to create a custom "debug" module that prints the extra text "foo" when used with the "msg" parameter. Let's see the different results when we execute with "ansible.builtin", "ansible.legacy" or without specifying any collections. The code is the following.

common code

  • inventory (localhost)
localhost ansible_connection=local
  • ansible.cfg
[defaults]
action_plugins = plugins/action
  • plugins/action/debug.py
# Copyright 2012, Dag Wieers <dag@wieers.com>
# Copyright 2016, Toshio Kuratomi <tkuratomi@ansible.com>
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

from ansible.errors import AnsibleUndefinedVariable
from ansible.module_utils.six import string_types
from ansible.module_utils._text import to_text
from ansible.plugins.action import ActionBase

class ActionModule(ActionBase):
    ''' Print statements during execution '''

    TRANSFERS_FILES = False
    _VALID_ARGS = frozenset(('msg', 'var', 'verbosity'))

    def run(self, tmp=None, task_vars=None):
        if task_vars is None:
            task_vars = dict()

        validation_result, new_module_args = self.validate_argument_spec(
            argument_spec={
                'msg': {'type': 'raw', 'default': 'Hello world!'},
                'var': {'type': 'raw'},
                'verbosity': {'type': 'int', 'default': 0},
            },
            mutually_exclusive=(
                ('msg', 'var'),
            ),
        )

        result = super(ActionModule, self).run(tmp, task_vars)
        del tmp  # tmp no longer has any effect

        # get task verbosity
        verbosity = new_module_args['verbosity']

        if verbosity <= self._display.verbosity:
            if new_module_args['var']:
                try:
                    results = self._templar.template(new_module_args['var'], convert_bare=True, fail_on_undefined=True)
                    if results == new_module_args['var']:
                        # if results is not str/unicode type, raise an exception
                        if not isinstance(results, string_types):
                            raise AnsibleUndefinedVariable
                        # If var name is same as result, try to template it
                        results = self._templar.template("{{" + results + "}}", convert_bare=True, fail_on_undefined=True)
                except AnsibleUndefinedVariable as e:
                    results = u"VARIABLE IS NOT DEFINED!"
                    if self._display.verbosity > 0:
                        results += u": %s" % to_text(e)

                if isinstance(new_module_args['var'], (list, dict)):
                    # If var is a list or dict, use the type as key to display
                    result[to_text(type(new_module_args['var']))] = results
                else:
                    result[new_module_args['var']] = results
            else:
                result['msg'] = (new_module_args['msg'] + "foo")

            # force flag to make debug output module always verbose
            result['_ansible_verbose_always'] = True
        else:
            result['skipped_reason'] = "Verbosity threshold not met."
            result['skipped'] = True

        result['failed'] = False

        return result

ansible.builtin code

  • ansible.builtin.yml
---
- name: debug module Playbook
  hosts: all
  vars:
    fruit: "apple"
  tasks:
    - name: Builtin
      ansible.builtin.debug:
        msg: "{{ fruit }}"

ansible.builtin execution

$ ansible-playbook -i inventory ansible.builtin.yml

PLAY [debug module Playbook] ****************************************************************

TASK [Gathering Facts] ******************************************************************
ok: [localhost]

TASK [Builtin] **************************************************************************
ok: [localhost] => {
    "msg": "apple"
}

PLAY RECAP ******************************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

ansible.legacy code

  • ansible.legacy.yml
---
- name: debug module Playbook
  hosts: all
  vars:
    fruit: "apple"
  tasks:
    - name: Legacy
      ansible.legacy.debug:
        msg: "{{ fruit }}"

ansible.legacy execution

$ ansible-playbook -i inventory ansible.legacy.yml 

PLAY [debug module Playbook] ****************************************************************

TASK [Gathering Facts] ******************************************************************
ok: [localhost]

TASK [Legacy] ***************************************************************************
ok: [localhost] => {
    "msg": "applefoo"
}

PLAY RECAP ******************************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

ansible.vanilla code

  • ansible.vanilla.yml
---
- name: debug module Playbook
  hosts: all
  vars:
    fruit: "apple"
  tasks:
    - name: Vanilla
      debug:
        msg: "{{ fruit }}"

ansible.legacy execution

$ ansible-playbook -i inventory ansible.vanilla.yml 

PLAY [debug module Playbook] ****************************************************************

TASK [Gathering Facts] ******************************************************************
ok: [localhost]

TASK [Vanilla] **************************************************************************
ok: [localhost] => {
    "msg": "applefoo"
}

PLAY RECAP ******************************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Conclusion

Now you know more about the "ansible.builtin" and the "ansible.legacy" collections. This is a great foundation for your Ansible Playbook onward.

See also: Revolutionising Ansible: Testing the Limits of OpenAI’s ChatGPT for Smarter Automation

Quick Comparison

# Short name (legacy, still works)
- copy:
    src: file.txt
    dest: /tmp/

# FQCN - builtin (recommended)
- ansible.builtin.copy:
    src: file.txt
    dest: /tmp/

# FQCN - legacy
- ansible.legacy.copy:
    src: file.txt
    dest: /tmp/

What's the Difference?

NamespaceResolutionWhen Used
ansible.builtinOnly built-in modulesExplicit, recommended
ansible.legacyBuilt-in + custom in configured pathsBackward compatibility
Short name (copy)Legacy resolution orderPre-2.10 style

Why Use FQCN?

# Problem: "copy" could be YOUR custom module or the built-in
- copy:  # Which one?
    src: file.txt
    dest: /tmp/

# Solution: FQCN is explicit
- ansible.builtin.copy:  # Definitely the built-in
    src: file.txt
    dest: /tmp/

- myorg.tools.copy:  # Definitely your custom one
    src: file.txt
    dest: /tmp/

Resolution Order

When you use a short name like copy:

  1. Check ansible.legacy namespace (custom module paths)
  2. Fall back to ansible.builtin
When you use ansible.builtin.copy:
  1. Only checks built-in modules — deterministic

Migrate to FQCN

# Use ansible-lint to find short names
ansible-lint playbook.yml
# Rule: fqcn[action-core] - Use FQCN for builtin module actions

# Or find manually
grep -rn "^\s*- \(copy\|template\|file\|service\|apt\|yum\|debug\|command\|shell\|user\|group\):" roles/

Before (Legacy)

- hosts: all
  tasks:
    - apt: name=nginx state=present
    - template: src=nginx.conf.j2 dest=/etc/nginx/nginx.conf
    - service: name=nginx state=started enabled=true
    - file: path=/var/www state=directory
    - copy: src=index.html dest=/var/www/index.html

After (FQCN)

- hosts: all
  tasks:
    - ansible.builtin.apt: name=nginx state=present
    - ansible.builtin.template: src=nginx.conf.j2 dest=/etc/nginx/nginx.conf
    - ansible.builtin.service: name=nginx state=started enabled=true
    - ansible.builtin.file: path=/var/www state=directory
    - ansible.builtin.copy: src=index.html dest=/var/www/index.html

Common Built-in Modules

# All these are ansible.builtin.*
ansible.builtin.apt
ansible.builtin.yum
ansible.builtin.dnf
ansible.builtin.copy
ansible.builtin.template
ansible.builtin.file
ansible.builtin.service
ansible.builtin.systemd
ansible.builtin.user
ansible.builtin.group
ansible.builtin.command
ansible.builtin.shell
ansible.builtin.debug
ansible.builtin.set_fact
ansible.builtin.assert
ansible.builtin.include_role
ansible.builtin.include_tasks
ansible.builtin.import_tasks
ansible.builtin.uri
ansible.builtin.get_url
ansible.builtin.unarchive
ansible.builtin.lineinfile
ansible.builtin.blockinfile
ansible.builtin.stat
ansible.builtin.setup
ansible.builtin.ping
ansible.builtin.raw
ansible.builtin.reboot
ansible.builtin.wait_for

Collection Modules (Not Built-in)

# These need collection installation
community.general.timezone
community.general.ufw
community.docker.docker_container
amazon.aws.ec2_instance
ansible.posix.sysctl
ansible.windows.win_copy
community.mysql.mysql_db

ansible-lint FQCN Rules

# .ansible-lint
rules:
  fqcn:
    # Require FQCN for builtin modules
    - fqcn[action-core]
    # Require FQCN for all modules
    - fqcn[action]

FAQ

Do I have to use FQCN?

No — short names still work. But FQCN is recommended for clarity and to avoid conflicts with custom modules sharing the same name.

Is ansible.legacy deprecated?

No — ansible.legacy exists for backward compatibility. It's not deprecated but using ansible.builtin explicitly is better practice.

What about action plugins?

Same rules apply — ansible.builtin.include_role, ansible.builtin.import_tasks, etc. FQCN works for all plugin types.

Quick Comparison

NamespaceWhat It ContainsExample
ansible.builtinCore modules shipped with ansible-coreansible.builtin.copy
ansible.legacyFallback namespace for unqualified namesansible.legacy.copy

FQCN (Fully Qualified Collection Name)

# FQCN — recommended
- ansible.builtin.copy:
    src: file.txt
    dest: /tmp/file.txt

# Short name — still works but triggers lint warning
- copy:
    src: file.txt
    dest: /tmp/file.txt

How Resolution Works

When you write copy: (without namespace):

  1. Ansible checks ansible.builtin.copy
  2. If not found, checks ansible.legacy.copy
  3. ansible.legacy checks library/ dirs and collections in order

ansible-lint fqcn Rule

fqcn[action-core]: Use FQCN for builtin module actions (copy).
# Fix: add ansible.builtin. prefix
- ansible.builtin.apt:
    name: nginx
- ansible.builtin.service:
    name: nginx
    state: started
- ansible.builtin.template:
    src: config.j2
    dest: /etc/config

Collection Modules

# Community collections use their own namespace
- community.general.ufw:
    rule: allow
    port: 80

- community.docker.docker_container:
    name: web
    image: nginx

- amazon.aws.ec2_instance:
    instance_type: t3.micro

When ansible.legacy Matters

# Custom module in library/ directory
# library/my_custom_module.py

# Referenced as:
- my_custom_module:          # resolves via ansible.legacy
    param: value

# Or explicitly:
- ansible.legacy.my_custom_module:
    param: value

Migration to FQCN

# Find all non-FQCN module usage
ansible-lint --profile moderate .

# Common replacements:
# copy → ansible.builtin.copy
# file → ansible.builtin.file
# apt → ansible.builtin.apt
# yum → ansible.builtin.yum
# service → ansible.builtin.service
# command → ansible.builtin.command
# shell → ansible.builtin.shell
# debug → ansible.builtin.debug
# template → ansible.builtin.template
# user → ansible.builtin.user
# group → ansible.builtin.group

FAQ

Do I MUST use FQCN?

Not technically — short names still work. But FQCN is best practice: it's explicit, avoids name collisions between collections, and satisfies ansible-lint.

What if two collections have the same module name?

FQCN prevents ambiguity. Without it, resolution depends on collection loading order — fragile and unpredictable.

Will short names be removed?

No — ansible.legacy fallback is maintained for backward compatibility. But new code should always use FQCN.

Category: installation

Watch the video: Ansible ansible.builtin vs ansible.legacy: Collection Namespaces Explained — Video Tutorial

Browse all Ansible tutorials · AnsiblePilot Home