Ansible 13 Upgrade Guide: Breaking Changes, Removals, and Migration Steps
By Luca Berton · Published 2024-01-01 · Category: installation
Complete Ansible 13 upgrade guide covering all breaking changes in ansible-core 2.20. Python 3.12+ required, INJECT_FACTS_AS_VARS deprecated, failed_when.
Ansible 13 is based on ansible-core 2.20. The biggest changes: Python 3.12+ required on controllers, Python 3.9+ on targets, INJECT_FACTS_AS_VARS deprecated, and the failed_when exception key renamed. Here's everything you need to update before upgrading.
Prerequisites
Python Version Requirements
| Component | Ansible 12 (core 2.19) | Ansible 13 (core 2.20) | |-----------|----------------------|----------------------| | Controller | Python 3.11+ | Python 3.12+ | | Target hosts | Python 3.8+ | Python 3.9+ |
# Check your Python version
python3 --version
# If below 3.12, upgrade first
# Ubuntu/Debian:
sudo apt install python3.12
# RHEL/CentOS:
sudo dnf install python3.12
This is the most common upgrade blocker — ensure all controller nodes run Python 3.12+.
Upgrade Path
Always upgrade through Ansible 12 first. The ansible-core 2.19 templating changes (introduced in Ansible 12) are prerequisites for Ansible 13.
# Step 1: Upgrade to Ansible 12 first
pip install 'ansible>=12,<13'
# Step 2: Fix any templating issues (see Ansible 12 Upgrade Guide)
# Step 3: Upgrade to Ansible 13
pip install 'ansible>=13,<14'
See also: Ansible 12 Upgrade Guide: Breaking Changes, Data Tagging & What to Test First
Breaking Changes in ansible-core 2.20
1. INJECT_FACTS_AS_VARS Deprecated
The biggest behavioral change. Currently, Ansible injects facts as top-level variables (ansible_distribution) AND stores them in the ansible_facts dictionary (ansible_facts['distribution']). The top-level injection is deprecated and will be removed in ansible-core 2.24.
# ❌ DEPRECATED — will stop working in 2.24
- ansible.builtin.debug:
msg: "OS: {{ ansible_distribution }}"
- ansible.builtin.debug:
msg: "IP: {{ ansible_default_ipv4.address }}"
# ✅ RECOMMENDED — works in all versions
- ansible.builtin.debug:
msg: "OS: {{ ansible_facts['distribution'] }}"
- ansible.builtin.debug:
msg: "IP: {{ ansible_facts['default_ipv4']['address'] }}"
Important: Inside ansible_facts, the ansible_ prefix is removed:
| Old (deprecated) | New (recommended) |
|-------------------|-------------------|
| ansible_distribution | ansible_facts['distribution'] |
| ansible_os_family | ansible_facts['os_family'] |
| ansible_hostname | ansible_facts['hostname'] |
| ansible_default_ipv4 | ansible_facts['default_ipv4'] |
| ansible_memtotal_mb | ansible_facts['memtotal_mb'] |
| ansible_processor_vcpus | ansible_facts['processor_vcpus'] |
To silence warnings now (while migrating):
# ansible.cfg
[defaults]
inject_facts_as_vars = True
Migration strategy: Search your playbooks for ansible_ variables and update them. This is the highest-effort change in Ansible 13.
# Find all affected files
grep -rn 'ansible_distribution\|ansible_os_family\|ansible_hostname\|ansible_default_ipv4\|ansible_memtotal' roles/ playbooks/
2. failed_when Exception Key Renamed
When you use failed_when: false to suppress errors, the exception key in the result has been renamed:
# ❌ BREAKS in Ansible 13
- ansible.builtin.command: /bin/false
register: result
failed_when: false
- ansible.builtin.debug:
msg: "Exception: {{ result.exception }}"
when: result.exception is defined
# ✅ WORKS in Ansible 13
- ansible.builtin.command: /bin/false
register: result
failed_when: false
- ansible.builtin.debug:
msg: "Exception: {{ result.failed_when_suppressed_exception }}"
when: result.failed_when_suppressed_exception is defined
3. PowerShell Quote Stripping Removed
Windows module utilities no longer automatically remove quotes from paths:
# If you relied on automatic quote removal:
- ansible.windows.win_copy:
src: '"C:\source\file.txt"' # Quotes were previously stripped
dest: C:\dest\
# Fix: Remove the extra quotes yourself
- ansible.windows.win_copy:
src: 'C:\source\file.txt'
dest: C:\dest\
4. smart Transport Removed
The DEFAULT_TRANSPORT = smart option (which auto-selected ssh or paramiko) has been removed:
# ❌ BREAKS in Ansible 13
[defaults]
transport = smart
# ✅ FIX — specify explicitly
[defaults]
transport = ssh
# or
transport = paramiko
5. DataLoader.get_basedir Returns Absolute Path
If you write custom plugins that use DataLoader.get_basedir(), it now returns an absolute path instead of relative. Update any path manipulation code accordingly.
6. Argument Spec: None Treated as Empty String
None values are now treated as empty strings for the str type in argument spec validation. This improves consistency with pre-2.19 templating conversions but may affect custom modules that rely on None vs "" distinction.
Removed Features
These previously deprecated features are now gone:
| Removed | Replacement |
|---------|-------------|
| vault/unvault filter vaultid param | Use vault_id instead |
| Galaxy v2 server API | Galaxy servers must support v3 |
| dnf/dnf5 install_repoquery option | Remove from playbooks |
| encrypt module passlib_or_crypt API | Use updated API |
| paramiko PARAMIKO_HOST_KEY_AUTO_ADD | Use SSH config |
| paramiko PARAMIKO_LOOK_FOR_KEYS | Use SSH config |
| yum_repository keepcache option | Remove from playbooks |
| Vars plugins get_host_vars/get_group_vars fallback | Inherit from BaseVarsPlugin |
See also: AAP 2.6 Migration from AWX: Complete Upgrade and Data Migration Guide
Collection-Level Breaking Changes
community.mysql
# Python 2 no longer supported on targets
# mysqlclient connector deprecated — use PyMySQL
# mysql_db: pipefail now defaults to true
- community.mysql.mysql_db:
name: mydb
state: dump
target: /tmp/dump.sql
pipefail: false # Set explicitly if not using bash
community.vmware
# Requires ansible-core 2.19+
# Requires vmware.vmware >= 2.0.0
# Dependencies changed: pyvmomi → vcf-sdk
pip install vcf-sdk # New dependency
# Removed modules (use vmware.vmware collection):
# vmware_cluster → vmware.vmware.cluster
# vmware_cluster_dpm → vmware.vmware.cluster_dpm
# vmware_cluster_drs → vmware.vmware.cluster_drs
community.general
# Removed modules:
# - bearychat (service discontinued)
# - facter (replaced by community.general.facter_facts)
# - yaml callback (use default callback with result_format=yaml)
# Deprecated — will be removed in community.general 13.0.0:
# - catapult, dimensiondata_*, typetalk, hiera lookup
# - spotinst_aws_elastigroup, pushbullet
community.docker
# Docker SDK for Python 1.x (docker-py) no longer supported
pip install 'docker>=2.0.0' # Required
# Python 3.6 and below no longer supported
# ansible-core 2.15/2.16 no longer supported
awx.awx
The awx.awx collection will be removed from Ansible 14 due to ongoing refactoring. Plan your migration:
# After removal, install manually:
ansible-galaxy collection install awx.awx
include_vars Changes
Two breaking changes for include_vars:
# ❌ BREAKS — extensions must be a list
- ansible.builtin.include_vars:
dir: vars/
extensions: "yml" # String no longer accepted
# ✅ FIX
- ansible.builtin.include_vars:
dir: vars/
extensions: ["yml"]
# ❌ DEPRECATED — ignore_files as string
- ansible.builtin.include_vars:
dir: vars/
ignore_files: ".gitkeep"
# ✅ FIX
- ansible.builtin.include_vars:
dir: vars/
ignore_files: [".gitkeep"]
See also: How to Upgrade from AAP 2.4 to AAP 2.6 — Step-by-Step Guide
replace Module: Unicode Mode
The replace module now reads files as unicode instead of bytes:
# If you relied on byte-level regex matching,
# this may produce different results
- ansible.builtin.replace:
path: /etc/config
regexp: '\xc3\xa9' # Byte pattern — may not work
replace: 'é'
# Fix: Use unicode patterns directly
- ansible.builtin.replace:
path: /etc/config
regexp: 'é'
replace: 'e'
Pre-Upgrade Checklist
1. [ ] Python 3.12+ on all controller nodes
2. [ ] Python 3.9+ on all target hosts
3. [ ] Upgraded to Ansible 12 first (fix templating issues)
4. [ ] Searched playbooks for `ansible_` fact variables → migrate to `ansible_facts['...']`
5. [ ] Searched for `result.exception` → rename to `result.failed_when_suppressed_exception`
6. [ ] Removed `transport = smart` from ansible.cfg
7. [ ] Updated include_vars: extensions/ignore_files to lists
8. [ ] Checked vault/unvault filters for deprecated `vaultid` parameter
9. [ ] Updated community.vmware: install vcf-sdk, update module names
10. [ ] Updated community.docker: ensure docker SDK >= 2.0.0
11. [ ] Tested in staging with Ansible 13
FAQ
Can I skip Ansible 12 and go straight to 13?
Not recommended. Ansible 12 (core 2.19) introduced major templating changes that surface issues in playbooks. Upgrading through 12 first lets you fix those issues before adding the Ansible 13 changes on top.
How long until INJECT_FACTS_AS_VARS is fully removed?
The deprecation targets ansible-core 2.24 (approximately Ansible 17). You have several major versions to migrate, but starting now avoids a painful bulk migration later.
Will my roles from Ansible Galaxy still work?
Most will, but roles that use deprecated fact syntax (ansible_distribution vs ansible_facts['distribution']) will show deprecation warnings. Roles that use removed features (vault vaultid, smart transport) will break.
What about Execution Environments?
Update your EE container images to include Python 3.12+ and the latest collection versions. If your EE uses community.vmware, switch from pyvmomi to vcf-sdk.
Conclusion
Ansible 13's biggest changes are Python 3.12+ requirement and INJECT_FACTS_AS_VARS deprecation. Start migrating fact variable syntax now — it's the highest-effort change. Fix failed_when exception keys, remove smart transport, and update collection dependencies. Always upgrade through Ansible 12 first.
Related Articles
• Ansible 12 Upgrade Guide • ansible-core 2.19 Templating Changes • Ansible facts: Gather, Use, Create • Ansible Variable Precedence GuideCategory: installation