{"id":168432,"date":"2026-06-04T21:50:34","date_gmt":"2026-06-04T18:50:34","guid":{"rendered":"https:\/\/computingforgeeks.com\/?p=168432"},"modified":"2026-06-04T21:52:01","modified_gmt":"2026-06-04T18:52:01","slug":"ansible-collections-tutorial","status":"publish","type":"post","link":"https:\/\/computingforgeeks.com\/ansible-collections-tutorial\/","title":{"rendered":"Ansible Collections: Install and Use Community Content"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Your playbook calls <code>community.general.ufw<\/code> and Ansible answers with <code>couldn't resolve module\/action<\/code>. The module exists, it is well maintained, thousands of people use it, but your control node has never heard of it. That gap is what Ansible collections fill. A collection is the unit Ansible ships modules, roles, plugins, and filters in, and <code>ansible-galaxy<\/code> is the tool that pulls one onto your control node so the playbook can find it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This guide wires the whole flow together end to end: what a collection is and how the <code>namespace.collection.module<\/code> name resolves, every way to install one (Ansible Galaxy, a pinned version, a <code>requirements.yml<\/code> file, a Git repo, an offline tarball, a private Automation Hub), where the files land and which copy wins when two paths hold the same collection, how dependencies get pulled in automatically, and how to call the content from a real playbook. We also cover the one thing that trips people up most on a fresh server: matching a collection version to the <code>ansible-core<\/code> you actually have. Every command below was run on a live two-node lab, with the real output.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><em>Confirmed working in June 2026 on a Rocky Linux 10 control node (ansible-core 2.16.16, Python 3.12) managing a Rocky Linux 10 and an Ubuntu 24.04 host.<\/em><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Prerequisites<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">You need a working Ansible control node and at least one managed host with SSH access. If Ansible is not on the box yet, follow <a href=\"https:\/\/computingforgeeks.com\/install-ansible-rocky-linux-ubuntu\/\">install Ansible on Rocky Linux and Ubuntu<\/a> first, then come back. The lab here uses:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li>A control node running ansible-core 2.16.16 (the version in the Rocky Linux 10 AppStream repo)<\/li>\n\n\n<li>Two managed nodes, one Rocky Linux 10 (10.0.1.11) and one Ubuntu 24.04 (10.0.1.12), reachable over SSH<\/li>\n\n\n<li>Outbound HTTPS to <code>galaxy.ansible.com<\/code> from the control node (only the control node downloads collections, never the managed hosts)<\/li>\n\n\n<li>Comfort with <a href=\"https:\/\/computingforgeeks.com\/ansible-playbook-tutorial\/\">Ansible playbooks<\/a> and <a href=\"https:\/\/computingforgeeks.com\/ansible-ad-hoc-commands\/\">ad-hoc commands<\/a><\/li>\n\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">What Ansible collections actually are<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A collection is a packaged bundle of related content under a two-part name: a <strong>namespace<\/strong> and a <strong>collection<\/strong>, like <code>community.general<\/code> or <code>ansible.posix<\/code>. Inside it are modules, roles, plugins (lookup, filter, inventory, connection), and their docs. When you reference content, you use the <strong>fully qualified collection name<\/strong> (FQCN), which is three parts: <code>namespace.collection.content<\/code>. So <code>community.general.ufw<\/code> means the <code>ufw<\/code> module, in the <code>general<\/code> collection, under the <code>community<\/code> namespace.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Here is the part that surprises people. <code>ansible-core<\/code> on its own ships almost no modules. It carries the <code>ansible.builtin<\/code> namespace (things like <code>copy<\/code>, <code>template<\/code>, <code>service<\/code>, <code>file<\/code>) and nothing else. Confirm it on a fresh control node:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>ansible localhost -m ansible.builtin.ping<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The builtin namespace is always there, so the ping answers immediately:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>localhost | SUCCESS =&gt; {\"changed\": false, \"ping\": \"pong\"}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Everything beyond that base, every <code>community.*<\/code>, <code>ansible.posix<\/code>, <code>kubernetes.core<\/code>, <code>amazon.aws<\/code> module, lives in a collection you install separately. The older &#8220;batteries-included&#8221; <code>ansible<\/code> package bundles around a hundred of them at fixed versions, but the modern, deliberate way to manage them is one collection at a time with <code>ansible-galaxy<\/code>. That is what keeps a project reproducible.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Collection<\/th><th>What it lets Ansible reach<\/th><\/tr><\/thead><tbody><tr><td><code>community.general<\/code><\/td><td>A grab-bag of modules: <code>ufw<\/code>, <code>timezone<\/code>, package managers, cloud helpers<\/td><\/tr><tr><td><code>ansible.posix<\/code><\/td><td>POSIX system bits: <code>firewalld<\/code>, <code>sysctl<\/code>, <code>mount<\/code>, <code>authorized_key<\/code><\/td><\/tr><tr><td><code>community.crypto<\/code><\/td><td>TLS keys, CSRs, and certificates<\/td><\/tr><tr><td><code>community.docker<\/code><\/td><td><a href=\"https:\/\/computingforgeeks.com\/how-to-manage-docker-containers-with-ansible\/\">Manage Docker containers<\/a>, images, networks<\/td><\/tr><tr><td><code>kubernetes.core<\/code><\/td><td>Apply manifests and <a href=\"https:\/\/computingforgeeks.com\/ansible-kubernetes-cluster\/\">manage Kubernetes<\/a> objects<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Install a collection from Ansible Galaxy<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/galaxy.ansible.com\/\" target=\"_blank\" rel=\"noreferrer noopener\">Ansible Galaxy<\/a> is the default public source. Installing a single collection is one command:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>ansible-galaxy collection install community.general<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>ansible-galaxy<\/code> resolves the dependency map, downloads the tarball from Galaxy, and unpacks it into your collections path:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>Starting galaxy collection install process\nProcess install dependency map\nStarting collection install process\nDownloading https:\/\/galaxy.ansible.com\/...\/community-general-10.7.9.tar.gz\nInstalling 'community.general:10.7.9' to '...\/collections\/ansible_collections\/community\/general'\ncommunity.general:10.7.9 was installed successfully<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">To see what is installed and exactly where it landed, list it. The <code>list<\/code> subcommand prints the path header followed by each collection and its version:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>ansible-galaxy collection list<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The screenshot below captures the whole loop on the Rocky control node, the install followed by the listing of every collection sitting in the project path:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"2560\" height=\"1206\" src=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-galaxy-collection-install-list-rocky-linux.png\" alt=\"ansible-galaxy collection install and list output on Rocky Linux showing installed collections and versions\" class=\"wp-image-168428\" title=\"\" srcset=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-galaxy-collection-install-list-rocky-linux.png 2560w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-galaxy-collection-install-list-rocky-linux-300x141.png 300w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-galaxy-collection-install-list-rocky-linux-1024x482.png 1024w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-galaxy-collection-install-list-rocky-linux-768x362.png 768w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-galaxy-collection-install-list-rocky-linux-1536x724.png 1536w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-galaxy-collection-install-list-rocky-linux-2048x965.png 2048w\" sizes=\"auto, (max-width: 2560px) 100vw, 2560px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">One quirk worth knowing: if no collections path exists yet, <code>ansible-galaxy collection list<\/code> prints the command&#8217;s usage text instead of an empty table. Once a single collection is installed, the listing behaves normally.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Pin versions and install several at once<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Reproducibility starts with version control. You can install several collections in one call, and pin any of them. Install two together:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>ansible-galaxy collection install ansible.posix community.crypto<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Both install in sequence, each reporting its resolved version:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>ansible.posix:2.2.0 was installed successfully\ncommunity.crypto:2.26.8 was installed successfully<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The version syntax attaches to the collection name after a colon. An exact pin uses <code>==<\/code>, and a range uses comma-separated comparators:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code># exact version\nansible-galaxy collection install 'community.docker:==3.10.0'\n\n# a range: at least 3.10, but stay on the 3.x line\nansible-galaxy collection install 'community.mysql:&gt;=3.10.0,&lt;4.0.0'<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Quote the argument so the shell does not try to interpret the <code>&gt;<\/code> and <code>&lt;<\/code> characters. The range form is the one to reach for in real projects: it lets patch and minor releases through while blocking the next major version that might break your playbooks.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Manage collections with requirements.yml<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Installing by hand is fine for one box. For a project that a team shares, the source of truth is a <code>requirements.yml<\/code> file checked into the repo next to your playbooks. It can declare both collections and roles in one place. Create the file with this content:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>---\ncollections:\n  - name: community.mysql\n    version: \"&gt;=3.10.0,&lt;4.0.0\"\n  - name: ansible.utils\n\nroles:\n  - name: geerlingguy.ntp<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>install<\/code> subcommand (no <code>collection<\/code> keyword) pulls both content types in a single run:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>ansible-galaxy install -r requirements.yml<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Roles download first, then collections, each honoring its version constraint:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>- downloading role 'ntp', owned by geerlingguy\n- geerlingguy.ntp (4.0.0) was installed successfully\nInstalling 'community.mysql:3.16.1' to '...\/community\/mysql'\ncommunity.mysql:3.16.1 was installed successfully\nInstalling 'ansible.utils:6.0.2' to '...\/ansible\/utils'\nansible.utils:6.0.2 was installed successfully<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Note that <code>community.mysql<\/code> resolved to 3.16.1, the newest release still inside the <code>&gt;=3.10.0,&lt;4.0.0<\/code> window. Anyone who clones the repo and runs that one command lands on the same content set you tested with. If you only want the collections and not the roles, use <code>ansible-galaxy collection install -r requirements.yml<\/code> instead. The <a href=\"https:\/\/computingforgeeks.com\/ansible-roles-tutorial\/\">roles guide<\/a> covers the role side in depth.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Where collections live, and which copy wins<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Ansible searches an ordered list of directories for collections. By default that is the project-adjacent <code>.\/collections<\/code>, then <code>~\/.ansible\/collections<\/code>, then the system-wide <code>\/usr\/share\/ansible\/collections<\/code>. The first directory in that order that holds a given collection is the one that loads. You can see the search order with <code>ansible-config<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>ansible-config dump | grep COLLECTIONS_PATH<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">On this lab the project path comes first, which means a project-local collection always overrides a user or system copy:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>COLLECTIONS_PATHS(...\/ansible.cfg) = ['...\/collections-lab\/collections', '\/home\/rocky\/.ansible\/collections']<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">To prove the precedence, put an older release in the user path while the project path holds a newer one, then list the collection. Both copies show up under their own path headers:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code># \/home\/rocky\/.ansible\/collections\/ansible_collections\nCollection        Version\n----------------- -------\ncommunity.general 9.5.7\n\n# \/home\/rocky\/collections-lab\/collections\/ansible_collections\nCollection        Version\n----------------- -------\ncommunity.general 10.7.9<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Because the project path is first in the search order, the newer copy is the one a playbook uses. The user-path copy is shadowed. This is the piece that ties a project together: commit your collections (or your <code>requirements.yml<\/code>) alongside the playbooks and every run uses that exact set, regardless of what is installed globally on the box.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Two ways to target a specific path. The <code>-p<\/code> flag installs into a directory you name:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>ansible-galaxy collection install kubernetes.core -p ~\/extra-collections<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Watch for the catch here: if that directory is not part of your configured collections path, Ansible installs the files but never loads them at runtime. <code>ansible-galaxy<\/code> warns you about exactly that, so read the output rather than assuming success. The <code>ANSIBLE_COLLECTIONS_PATH<\/code> environment variable is the other lever, and it overrides the config file for a single command:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>ANSIBLE_COLLECTIONS_PATH=~\/.ansible\/collections ansible-galaxy collection list community.general<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">That call ignores the project path entirely and reports the older copy from the user directory. Handy for testing how a playbook behaves against a different installed set without editing <code>ansible.cfg<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Install from Git, a tarball, or Automation Hub<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Galaxy is the default, not the only source. Three others come up constantly.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Straight from a Git repository<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">When you need an unreleased fix, a fork, or an internal collection that never goes to Galaxy, install from Git with the <code>git+<\/code> prefix. Append <code>,&lt;branch&gt;<\/code> or <code>,&lt;tag&gt;<\/code> to pin a ref:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>ansible-galaxy collection install git+https:\/\/github.com\/ansible-collections\/community.sops.git<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">It clones the repo, reads the <code>galaxy.yml<\/code> at the root to learn the collection name and version, and installs it like any other:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>Cloning into '...\/community.sops...'\nInstalling 'community.sops:2.3.0' to '...\/collections\/ansible_collections\/community\/sops'\ncommunity.sops:2.3.0 was installed successfully<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Offline, for an air-gapped control node<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">A control node with no internet still needs collections. The pattern is two steps: download on a connected machine, then install on the isolated one. The <code>download<\/code> subcommand fetches the tarballs and writes a matching <code>requirements.yml<\/code> that points at them:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>ansible-galaxy collection download community.crypto -p ~\/airgap<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Look at what it produced. The directory holds the tarball plus a generated requirements file:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>community-crypto-2.26.8.tar.gz\nrequirements.yml<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Copy that whole directory to the air-gapped node (a USB drive, an internal artifact store, whatever your process allows), then install straight from the generated file with no network at all:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>cd ~\/airgap\nansible-galaxy collection install -r requirements.yml<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The install reads the tarball off local disk:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>Installing 'community.crypto:2.26.8' to '...\/community\/crypto'\ncommunity.crypto:2.26.8 was installed successfully<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">From a private Automation Hub<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Teams that run Red Hat Automation Hub or a private Galaxy server point <code>ansible-galaxy<\/code> at it through the <code>[galaxy]<\/code> section of <code>ansible.cfg<\/code>. List your servers in priority order and give each one its URL and token:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>[galaxy]\nserver_list = private_hub, release_galaxy\n\n[galaxy_server.private_hub]\nurl = https:\/\/hub.internal.example.com\/api\/galaxy\/content\/published\/\ntoken = YOUR_HUB_TOKEN\n\n[galaxy_server.release_galaxy]\nurl = https:\/\/galaxy.ansible.com\/<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">With that in place, <code>ansible-galaxy<\/code> tries the private hub first and falls back to public Galaxy. You can also force a single source for one command with the <code>--server<\/code> flag. Keep the token out of the file itself where you can, by reading it from an environment variable or, better, from <a href=\"https:\/\/computingforgeeks.com\/ansible-vault-tutorial\/\">Ansible Vault<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Dependencies resolve on their own<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Collections can depend on other collections, and <code>ansible-galaxy<\/code> pulls the chain in for you. Installing <code>cisco.ios<\/code> into a fresh path drags in the network plumbing it needs:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>ansible-galaxy collection install cisco.ios -p ~\/dep-demo<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Two collections land even though you asked for one. <code>ansible.netcommon<\/code> came along as a dependency:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>Installing 'cisco.ios:11.4.1' to '...\/cisco\/ios'\ncisco.ios:11.4.1 was installed successfully\nInstalling 'ansible.netcommon:8.5.2' to '...\/ansible\/netcommon'\nansible.netcommon:8.5.2 was installed successfully<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">You saw the same thing earlier without realizing it: installing <code>community.docker<\/code> quietly added <code>community.library_inventory_filtering_v1<\/code>. If you are populating an offline bundle and want only the named collection, <code>--no-deps<\/code> turns the automatic resolution off:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>ansible-galaxy collection install cisco.ios -p ~\/nodep-demo --no-deps<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">That installs <code>cisco.ios<\/code> alone, leaving you to supply <code>ansible.netcommon<\/code> yourself. Use it deliberately, because a collection without its dependencies will fail at runtime, not at install time.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Use a collection in a playbook<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Installed content is reached by its FQCN. Always spell out the full <code>namespace.collection.module<\/code> name in tasks. It removes ambiguity, makes the playbook self-documenting about where each module comes from, and avoids the name clashes that bite you when two collections ship a module with the same short name.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Here is where collections earn their keep as an integration layer. The same job, opening port 80, uses a different module per OS family: <code>ansible.posix.firewalld<\/code> on the Rocky host, <code>community.general.ufw<\/code> on the Ubuntu host. A second play generates a self-signed certificate on the control node with <code>community.crypto<\/code>. Save this as <code>site.yml<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>---\n- name: Open a firewall port using the right collection per OS\n  hosts: web\n  become: true\n  tasks:\n    - name: Allow HTTP on RHEL-family hosts (ansible.posix)\n      ansible.posix.firewalld:\n        service: http\n        permanent: true\n        immediate: true\n        state: enabled\n      when: ansible_os_family == \"RedHat\"\n\n    - name: Allow HTTP on Debian-family hosts (community.general)\n      community.general.ufw:\n        rule: allow\n        port: \"80\"\n        proto: tcp\n      when: ansible_os_family == \"Debian\"\n\n- name: Generate a self-signed TLS cert with community.crypto\n  hosts: localhost\n  connection: local\n  gather_facts: false\n  vars:\n    cert_dir: \/home\/rocky\/pki\n  tasks:\n    - name: Ensure the PKI directory exists\n      ansible.builtin.file:\n        path: \"{{ cert_dir }}\"\n        state: directory\n        mode: \"0755\"\n\n    - name: Create a private key\n      community.crypto.openssl_privatekey:\n        path: \"{{ cert_dir }}\/web.key\"\n        size: 2048\n\n    - name: Create a self-signed certificate\n      community.crypto.x509_certificate:\n        path: \"{{ cert_dir }}\/web.crt\"\n        privatekey_path: \"{{ cert_dir }}\/web.key\"\n        provider: selfsigned\n        selfsigned_not_after: \"+365d\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Run it against the inventory:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>ansible-playbook site.yml<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>when<\/code> conditions route each host to the module that fits it. The Rocky host changes through <code>firewalld<\/code> and skips the <code>ufw<\/code> task, the Ubuntu host does the reverse, and the control node mints the key and certificate:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"2560\" height=\"1202\" src=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-collections-cross-platform-playbook-run.png\" alt=\"Ansible playbook run using ansible.posix.firewalld on Rocky and community.general.ufw on Ubuntu with a clean play recap\" class=\"wp-image-168427\" title=\"\" srcset=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-collections-cross-platform-playbook-run.png 2560w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-collections-cross-platform-playbook-run-300x141.png 300w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-collections-cross-platform-playbook-run-1024x481.png 1024w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-collections-cross-platform-playbook-run-768x361.png 768w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-collections-cross-platform-playbook-run-1536x721.png 1536w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-collections-cross-platform-playbook-run-2048x962.png 2048w\" sizes=\"auto, (max-width: 2560px) 100vw, 2560px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">One playbook, two operating systems, three collections doing the work. Run it a second time and the firewall tasks report <code>ok<\/code> instead of <code>changed<\/code>, because the modules are idempotent and the rules already exist. To explore what a collection offers before you write tasks, list its modules with <code>ansible-doc -l community.general<\/code> and read any one of them with <code>ansible-doc community.general.timezone<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Match the collection version to your ansible-core<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">This is the single most common stumbling block on a freshly provisioned server, and it is worth slowing down for. Distributions ship an <code>ansible-core<\/code> that lags the newest releases. Rocky Linux 10 and Ubuntu 24.04 both package ansible-core 2.16, while the current <code>community.general<\/code> targets a newer core. Install the latest and then look at any module:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>ansible-galaxy collection install community.general\nansible-doc community.general.timezone<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Ansible loads the module but warns that the pairing is unsupported:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>[WARNING]: Collection community.general does not support Ansible version 2.16.16<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The module may still run today, but you are off the tested path and a future task can break in ways nobody upstream is checking for. Two clean fixes. The first, and the one most readers want, is to pin the collection to the line that supports your core. The <code>10.x<\/code> line of <code>community.general<\/code> is built for ansible-core 2.16:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>ansible-galaxy collection install 'community.general:&gt;=10.0.0,&lt;11.0.0' --force<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">That resolves to the newest 10.x release and the warning is gone:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"2560\" height=\"842\" src=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-collection-ansible-core-version-compatibility.png\" alt=\"Pinning community.general to the 10.x line to match ansible-core 2.16 and clear the unsupported version warning\" class=\"wp-image-168430\" title=\"\" srcset=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-collection-ansible-core-version-compatibility.png 2560w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-collection-ansible-core-version-compatibility-300x99.png 300w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-collection-ansible-core-version-compatibility-1024x337.png 1024w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-collection-ansible-core-version-compatibility-768x253.png 768w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-collection-ansible-core-version-compatibility-1536x505.png 1536w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-collection-ansible-core-version-compatibility-2048x674.png 2048w\" sizes=\"auto, (max-width: 2560px) 100vw, 2560px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The second fix is to upgrade <code>ansible-core<\/code> itself so you can run the newest collections. Do it in an isolated environment rather than fighting the system package:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>python3 -m pip install --user --upgrade ansible-core<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Pick one approach per project and write it down. A pinned collection set plus a known <code>ansible-core<\/code> is what makes a run on your laptop match a run in CI. The same compatibility check matters when you wire collections into <a href=\"https:\/\/computingforgeeks.com\/ansible-molecule-testing\/\">role tests with Molecule<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Verify and trust what you installed<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Before a collection runs with root on every host you manage, it is fair to ask whether the files on disk are the files the author published. The <code>verify<\/code> subcommand checks the installed content against the checksums on Galaxy:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>ansible-galaxy collection verify community.general<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">A clean install confirms the match in one line:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>Successfully verified that checksums for 'community.general:10.7.9' match the remote collection.<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">To see that the check has teeth, edit one installed file and verify again. The command names the file that no longer matches:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"2560\" height=\"698\" src=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-galaxy-collection-verify-checksums.png\" alt=\"ansible-galaxy collection verify confirming checksums and then detecting a modified file in an installed collection\" class=\"wp-image-168429\" title=\"\" srcset=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-galaxy-collection-verify-checksums.png 2560w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-galaxy-collection-verify-checksums-300x82.png 300w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-galaxy-collection-verify-checksums-1024x279.png 1024w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-galaxy-collection-verify-checksums-768x209.png 768w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-galaxy-collection-verify-checksums-1536x419.png 1536w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-galaxy-collection-verify-checksums-2048x558.png 2048w\" sizes=\"auto, (max-width: 2560px) 100vw, 2560px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">That tamper detection is the round trip you want before trusting third-party automation: install, verify against the source, and only then run. Wire <code>ansible-galaxy collection verify<\/code> into CI alongside the install step and a modified or corrupted collection fails the pipeline instead of reaching production.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Troubleshooting collection resolution<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Most collection problems are really resolution problems: Ansible cannot find the content, or it finds the wrong copy. These are the failures that came up while building this guide and how each one resolves.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">&#8220;couldn&#8217;t resolve module\/action&#8221;<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The collection is not installed in any configured path, or the FQCN is misspelled. Confirm the collection is present with <code>ansible-galaxy collection list<\/code>, then check the namespace and name in your task against the list output. A task that says <code>community.docker.docker_container<\/code> needs <code>community.docker<\/code> installed, not <code>community.general<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">A playbook uses an old version you thought you upgraded<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Two paths hold the same collection and the earlier one in the search order wins. Run <code>ansible-galaxy collection list<\/code> and read every path header, not just the first. If the project <code>.\/collections<\/code> directory holds an old copy, that copy shadows the freshly upgraded one in <code>~\/.ansible\/collections<\/code>. Remove the stale copy or upgrade it in place with <code>--force<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">&#8220;does not support Ansible version&#8221;<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The collection is newer than your <code>ansible-core<\/code>. Pin the collection to the line that matches your core, or upgrade <code>ansible-core<\/code>, as shown above. Check the requirement with <code>ansible --version<\/code> and the collection&#8217;s own <code>runtime.yml<\/code> or its Galaxy page.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">&#8220;installed collection will not be picked up&#8221;<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">You installed with <code>-p<\/code> into a directory outside your configured collections path. Either add that directory to <code>collections_path<\/code> in <code>ansible.cfg<\/code>, set <code>ANSIBLE_COLLECTIONS_PATH<\/code>, or reinstall without <code>-p<\/code> so it lands somewhere Ansible already searches. The fastest habit that avoids all of this is to keep a <code>requirements.yml<\/code> in the repo and let <code>ansible.cfg<\/code> point <code>collections_path<\/code> at a project-local directory, so every clone resolves the same content the same way. Keep the <a href=\"https:\/\/computingforgeeks.com\/ansible-cheat-sheet\/\">Ansible cheat sheet<\/a> within reach and the <a href=\"https:\/\/computingforgeeks.com\/ansible-automation-guide\/\">Ansible automation guide<\/a> open for where collections fit in the bigger workflow.<\/p>\n\n\n","protected":false},"excerpt":{"rendered":"<p>Your playbook calls community.general.ufw and Ansible answers with couldn&#8217;t resolve module\/action. The module exists, it is well maintained, thousands of people use it, but your control node has never heard of it. That gap is what Ansible collections fill. A collection is the unit Ansible ships modules, roles, plugins, and filters in, and ansible-galaxy is &#8230; <a title=\"Ansible Collections: Install and Use Community Content\" class=\"read-more\" href=\"https:\/\/computingforgeeks.com\/ansible-collections-tutorial\/\" aria-label=\"Read more about Ansible Collections: Install and Use Community Content\">Read more<\/a><\/p>\n","protected":false},"author":9,"featured_media":168431,"comment_status":"open","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[606,329],"tags":[314,212,669],"cfg_series":[39825],"class_list":["post-168432","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-ansible","category-automation","tag-ansible","tag-automation","tag-dev","cfg_series-ansible-mastery"],"_links":{"self":[{"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/posts\/168432","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/users\/9"}],"replies":[{"embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/comments?post=168432"}],"version-history":[{"count":3,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/posts\/168432\/revisions"}],"predecessor-version":[{"id":168435,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/posts\/168432\/revisions\/168435"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/media\/168431"}],"wp:attachment":[{"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/media?parent=168432"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/categories?post=168432"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/tags?post=168432"},{"taxonomy":"cfg_series","embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/cfg_series?post=168432"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}