Ansible can turn inventory data and facts into files on managed hosts: render /etc/hosts from hostvars and groups, package directories with community.general.archive, extract bundles with ansible.builtin.unarchive, and publish HTML or text reports with ansible.builtin.template. This guide connects three admin workflows—generate files from inventory, archive or extract payloads, and collect facts for reports—on Rocky Linux 10.2 with playbooks you can rerun safely.
It assumes you can run plays from your first playbook and understand basic Jinja2 templates. Full DNS design, custom facts deep dives, and the complete copy/fetch tutorial are out of scope.
Tested on: Rocky Linux 10.2 (Red Quartz); kernel 6.12.0-211.16.1.el10_2.0.1.x86_64; ansible-core 2.16.16;
community.general9.5.2.
~/ansible-project, inventory group lab, and playbooks in playbooks/. Use your own host names and paths if yours differ.
| Task | Module |
|---|---|
| Gather facts | ansible.builtin.setup / gather_facts: true |
Render /etc/hosts |
ansible.builtin.template |
| Create archive | community.general.archive |
| Extract archive | ansible.builtin.unarchive |
| Create report directory | ansible.builtin.file |
| Fetch generated reports | ansible.builtin.fetch |
| Verify generated file | ansible.builtin.command or ansible.builtin.slurp |
| Data source | Use when |
|---|---|
ansible_host |
Network address for /etc/hosts (set in inventory for every host) |
inventory_hostname |
Hostname column—name clients resolve |
hostvars[host].ansible_default_ipv4.address |
Optional fallback only if ansible_host is missing—not recommended for /etc/hosts |
groups['all'] |
You want every host in inventory |
groups['lab'] |
You want only hosts in a selected group |
What This Article Covers
- Generating
/etc/hostsfrom inventory network addresses (ansible_host) and hostnames only - Grouping hostnames that share the same
ansible_hoston one line - Deploying templates with
backup: true community.general.archivefortar.gzand other formatsansible.builtin.unarchivewithremote_src: truefor on-host archivesansible.builtin.setup/gather_factsfor report data- Rendering system reports with Jinja2 and fetching them to the control node
| Cover here | Avoid here |
|---|---|
/etc/hosts generation |
Full DNS design |
hostvars, groups, inventory_hostname |
Full magic variables article |
| Facts for reports | Full facts/custom facts article |
template for generated files |
Full Jinja2/filter tutorial |
archive / unarchive basics |
Backup strategy or disaster recovery |
fetch generated reports |
Full file/copy/fetch article |
| Verification | Full troubleshooting article |
Why Generate Files and Reports with Ansible?
| Manual edits and shell scripts | Ansible modules |
|---|---|
/etc/hosts drifts per server |
One template driven by inventory |
Ad-hoc tar commands |
community.general.archive creates archives with predictable paths and module parameters |
| Reports copied host by host | template + fetch in a playbook |
| No backup before overwrite | backup: true on template |
Generating files from inventory keeps lab and exam environments consistent—especially when EX294 objectives expect resolvable hostnames. Keeping logic in playbooks and thin templates matches idempotency practice. Use FQCN in playbooks—see modules and ansible-doc.
Prerequisites and Required Collections
- Managed hosts running Rocky Linux 10 with Python for Ansible modules
ansible-coreon the control node- Inventory with host names and connectivity (
ansible_hostwhen names differ from DNS) community.generalon the control node forarchivebecome: truewhen writing/etc/hosts, archives under/var, or system paths
Pin collections in requirements.yml:
---
collections:
- name: community.general
version: "9.5.2"ansible-galaxy collection install -r requirements.ymlSample output:
Nothing to do. All requested collections are already installed.ansible lab -m ansible.builtin.pingSample output:
rocky1 | SUCCESS => {
"changed": false,
"ping": "pong"
}Without community.general, archive tasks fail with couldn't resolve module/action 'community.general.archive'.
Key Modules Used in This Article
| Module | Role in this guide |
|---|---|
ansible.builtin.setup |
Populates ansible_* facts and hostvars for templates |
ansible.builtin.template |
Renders Jinja2 files such as /etc/hosts and reports |
community.general.archive |
Creates compressed archives on the managed host |
ansible.builtin.unarchive |
Extracts archives on the managed host |
ansible.builtin.file |
Creates report or application directories |
ansible.builtin.fetch |
Pulls generated reports to the control node |
Official references appear in References at the end of this page.
Generate /etc/hosts from Inventory
/etc/hosts is for network name resolution only: map each unique IP address to one or more hostnames. It is not the place for OS facts, memory, or report data—those belong in separate report templates later in this guide.
The IP is the unique network identity on each line; hostnames are aliases clients resolve to that address.
Set ansible_host in inventory for every managed host—that value is the network address Ansible uses to connect, and it is what belongs in /etc/hosts. Do not pull OS or hardware facts into this file; discovered addresses such as ansible_default_ipv4 can differ from the address you intend in a lab or exam inventory.
Example inventory:
[lab]
rocky1 ansible_host=10.0.2.15 ansible_connection=local
rocky2 ansible_host=10.0.2.15 ansible_connection=local/etc/hosts from Ansible is useful in lab and exam practice, but production hosts may regenerate or override that file—cloud-init, NetworkManager, DHCP hooks, or config-management agents can rewrite entries after your play. Always set backup: true on the template task so Ansible keeps a timestamped copy before replace. In production, coordinate with your platform team (DNS, DHCP reservations, or the sanctioned hosts-management tool) instead of treating a playbook as the only source of truth.
Deploy from inventory alone—no fact-gathering play required for /etc/hosts:
- name: Deploy /etc/hosts from inventory
hosts: rocky1
become: true
gather_facts: false
tasks:
- name: Render /etc/hosts from inventory network addresses
ansible.builtin.template:
src: ../templates/hosts.j2
dest: /etc/hosts
owner: root
group: root
mode: '0644'
backup: trueansible-playbook playbooks/deploy-hosts.ymlSample output:
TASK [Render /etc/hosts from inventory network addresses] ***********************
changed: [rocky1]A second run reports ok with changed: false when the template content is unchanged.
Use hostvars, groups and inventory_hostname
Inside a Jinja2 template, loop over a group, read each host's ansible_host, and group hostnames that share the same network address on one line:
{% set ns = namespace(hosts_by_ip={}) %}
{% for host in groups['lab'] | sort %}
{% set ip = hostvars[host].ansible_host | default('') %}
{% if ip %}
{% if ip not in ns.hosts_by_ip %}
{% set _ = ns.hosts_by_ip.update({ip: []}) %}
{% endif %}
{% set _ = ns.hosts_by_ip[ip].append(host) %}
{% endif %}
{% endfor %}
{% for ip in ns.hosts_by_ip | sort %}
{{ ip }} {{ ns.hosts_by_ip[ip] | join(' ') }}
{% endfor %}ansible_hostis the only address column—inventory defines the network identity per host.inventory_hostnameis the name column—the hostname clients look up.hostvars[host]reaches variables for hosts that are not the current task target.
See Ansible variables for naming rules; avoid variable names that collide with magic variables such as hostvars or groups.
Use Facts to Get IP Addresses and Host Details
For /etc/hosts, use inventory only: ansible_host plus inventory_hostname. That keeps the file aligned with how you designed connectivity—one network address per host, defined before the play runs.
Do not mix in ansible_default_ipv4, ansible_hostname, or other discovered facts for /etc/hosts. Those values come from gather_facts at runtime and may not match your inventory plan (multiple NICs, bridges, or lab hosts sharing one machine). Save facts for system reports later in this guide.
If a host truly has no ansible_host in inventory, fix the inventory rather than guessing from facts. A fact-based fallback is possible but discouraged:
{% set ip = hostvars[host].ansible_host %}That path requires a prior gather_facts play and is not the pattern used here.
For report templates, use richer ansible_* facts—see Ansible facts and custom facts.
Create a Jinja2 Template for /etc/hosts
Full templates/hosts.j2 from the lab:
# {{ ansible_managed }}
127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4
::1 localhost localhost.localdomain localhost6 localhost6.localdomain6
{% set ns = namespace(hosts_by_ip={}) %}
{% for host in groups['lab'] | sort %}
{% set ip = hostvars[host].ansible_host | default('') %}
{% if ip %}
{% if ip not in ns.hosts_by_ip %}
{% set _ = ns.hosts_by_ip.update({ip: []}) %}
{% endif %}
{% set _ = ns.hosts_by_ip[ip].append(host) %}
{% endif %}
{% endfor %}
{% for ip in ns.hosts_by_ip | sort %}
{{ ip }} {{ ns.hosts_by_ip[ip] | join(' ') }}
{% endfor %}The ansible_managed comment marks the file as Ansible-controlled—useful when you audit changes later. Keep heavy logic in tasks (set_fact, vars) when templates grow beyond simple loops; see template module patterns.
Deploy /etc/hosts Safely with Backup
Always set backup: true on production /etc/hosts tasks:
- name: Render /etc/hosts from inventory and facts
ansible.builtin.template:
src: ../templates/hosts.j2
dest: /etc/hosts
backup: trueAnsible saves the previous file with a timestamp suffix before replacing it. Pair with owner, group, and mode: '0644' so permissions stay correct.
Verify Generated /etc/hosts
Inspect the rendered file on the target:
tail -6 /etc/hostsSample output:
# Ansible managed
127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4
::1 localhost localhost.localdomain localhost6 localhost6.localdomain6
10.0.2.15 rocky1 rocky2One line per unique IP; multiple inventory hostnames on the same address become aliases on that line.
Confirm name resolution for an inventory host:
getent hosts rocky2Sample output:
10.0.2.15 rocky2Inside a playbook, ansible.builtin.command with changed_when: false or ansible.builtin.slurp can capture file content for assertions—see debug playbook techniques.
Create Archives with community.general.archive
community.general.archive creates or replaces an archive on the managed host. The archive is not copied to the control node.
- name: Archive log directory as tar.gz
community.general.archive:
path: /var/log/ansible-demo
dest: /tmp/ansible-demo-logs.tar.gz
format: gz
remove: falseansible-playbook playbooks/create-archive.ymlSample output:
TASK [Archive log directory as tar.gz] *****************************************
changed: [rocky1]ls -lh /tmp/ansible-demo-logs.tar.gzSample output:
-rw-r--r--. 1 root root 231 Jul 9 14:59 /tmp/ansible-demo-logs.tar.gzpath accepts a directory, glob, or list. format supports gz, bz2, zip, tar, and others per the archive module documentation.
community.general.archive creates or replaces the archive on the managed host. If you need strict change reporting for backup workflows, test reruns with your source tree because archive metadata can still make tasks report changed.
Extract Archives with ansible.builtin.unarchive
When the archive already exists on the managed host, set remote_src: true:
- name: Ensure extraction directory exists
ansible.builtin.file:
path: /opt/demoapp
state: directory
mode: '0755'
- name: Extract archive on managed host
ansible.builtin.unarchive:
src: /tmp/ansible-demo-logs.tar.gz
dest: /opt/demoapp
remote_src: true
creates: /opt/demoapp/ansible-demo/app.logansible-playbook playbooks/extract-archive.ymlSample output:
TASK [Ensure extraction directory exists] **************************************
ok: [rocky1]
TASK [Extract archive on managed host] *****************************************
changed: [rocky1]cat /opt/demoapp/ansible-demo/app.logSample output:
ansible archive demo log entryUse creates: so extraction is skipped when the destination already exists—idempotent reruns stay ok.
archive vs unarchive vs fetch
| Module | Direction | Typical use |
|---|---|---|
community.general.archive |
Compress on managed host | Package logs or config trees before backup |
ansible.builtin.unarchive |
Expand on managed host | Deploy application bundles |
ansible.builtin.fetch |
Managed host → control node | Pull generated reports or diagnostics |
archive and unarchive operate on the remote system. fetch is the reverse of copy—use it after reports are rendered on each host. For everyday file copy workflows, see file, copy and fetch modules.
Collect System Facts with setup
gather_facts: true on a play (or ansible.builtin.setup as a task) runs the fact collector and fills ansible_* variables used in reports:
- name: Generate system reports from facts
hosts: lab
gather_facts: true
tasks:
- name: Render system report from facts
ansible.builtin.template:
src: ../templates/system_report.j2
dest: "/var/reports/ansible/{{ inventory_hostname }}-report.txt"Facts are per host—inventory_hostname, ansible_distribution, ansible_memtotal_mb, and similar keys are available inside the template without extra hostvars lookups on the same host.
Generate System Reports with Templates
Example templates/system_report.j2:
# Ansible system report for {{ inventory_hostname }}
Hostname: {{ ansible_hostname }}
FQDN: {{ ansible_fqdn }}
OS: {{ ansible_distribution }} {{ ansible_distribution_version }}
Kernel: {{ ansible_kernel }}
Architecture: {{ ansible_architecture }}
Primary IPv4: {{ ansible_default_ipv4.address | default('n/a') }}
Memory MB: {{ ansible_memtotal_mb }}
Processor count: {{ ansible_processor_vcpus }}ansible-playbook playbooks/generate-reports.ymlSample output:
TASK [Render system report from facts] *****************************************
changed: [rocky1]
changed: [rocky2]Save Reports on Managed Hosts
Create the destination directory before templating:
- name: Ensure report directory exists
ansible.builtin.file:
path: /var/reports/ansible
state: directory
mode: '0755'
- name: Render system report from facts
ansible.builtin.template:
src: ../templates/system_report.j2
dest: "/var/reports/ansible/{{ inventory_hostname }}-report.txt"
mode: '0644'Using {{ inventory_hostname }} in the filename avoids collisions when every host writes its own report locally.
head -8 /var/reports/ansible/rocky1-report.txtSample output:
# Ansible system report for rocky1
Hostname: rocky1
FQDN: rocky1
OS: Rocky 10.2
Kernel: 6.12.0-211.16.1.el10_2.0.1.x86_64
Architecture: x86_64Fetch Reports to the Control Node
ansible.builtin.fetch stores files in a hostname-based tree under dest:
- name: Fetch system report from managed host
ansible.builtin.fetch:
src: "/var/reports/ansible/{{ inventory_hostname }}-report.txt"
dest: "{{ playbook_dir }}/../fetched-reports/"
flat: false
fail_on_missing: trueansible-playbook playbooks/fetch-reports.ymlSample output:
TASK [Fetch system report from managed host] ***********************************
changed: [rocky1]
changed: [rocky2]On the control node, files land under per-host paths:
fetched-reports/rocky1/var/reports/ansible/rocky1-report.txt
fetched-reports/rocky2/var/reports/ansible/rocky2-report.txtAvoid flat: true when fetching the same basename from many hosts—later fetches overwrite earlier ones.
Common Examples
Generate /etc/hosts for all inventory hosts
{% for host in groups['all'] | sort %}
{% if hostvars[host].ansible_host %}{{ hostvars[host].ansible_host }} {{ hostvars[host].inventory_hostname }}{% endif %}
{% endfor %}Set ansible_host on every inventory host; no gather_facts required.
Map ansible_host to inventory_hostname
{{ hostvars[host].ansible_host }} {{ hostvars[host].inventory_hostname }}Create a tar.gz archive of a log directory
- name: Archive application logs
community.general.archive:
path: /var/log/myapp
dest: /tmp/myapp-logs.tar.gz
format: gzExtract an application archive to a target directory
- name: Ensure extraction directory exists
ansible.builtin.file:
path: /opt/myapp
state: directory
mode: '0755'
- name: Deploy application bundle
ansible.builtin.unarchive:
src: /tmp/myapp-1.0.tar.gz
dest: /opt/myapp
remote_src: trueGenerate a system report from facts
- name: Write fact-based report
ansible.builtin.template:
src: ../templates/system_report.j2
dest: "/var/reports/ansible/{{ inventory_hostname }}-report.txt"Fetch reports from all managed hosts
- name: Collect reports on control node
hosts: lab
tasks:
- name: Fetch report file
ansible.builtin.fetch:
src: "/var/reports/ansible/{{ inventory_hostname }}-report.txt"
dest: "{{ playbook_dir }}/../fetched-reports/"Common Mistakes
| Mistake | Fix |
|---|---|
Missing ansible_host in inventory |
Set the network address per host in inventory |
Using facts in /etc/hosts |
Use ansible_host only; facts belong in reports |
Overwriting /etc/hosts without backup |
Use backup: true on template |
| Generating hosts entries for the wrong group | Loop over the intended group, such as groups['lab'], not always groups['all'] |
| Creating archives without required collection | Install community.general |
| Extracting into a missing directory | Create dest with ansible.builtin.file before unarchive |
Using unarchive without remote_src for on-host archives |
Set remote_src: true |
Fetching reports with flat: true from many hosts |
Use per-host directories (flat: false) |
| Putting too much logic in templates | Prepare variables in tasks when possible |
Best Practices
| Practice | Why |
|---|---|
Set ansible_host for every inventory host |
/etc/hosts uses inventory network addresses only |
Keep /etc/hosts separate from fact-based reports |
Facts are for reports; inventory is for name-to-IP mapping |
Pin community.general in requirements.yml |
archive is not in ansible-core |
Use backup: true on /etc/hosts |
Roll back if a template error breaks resolution |
Include ansible_managed in generated files |
Shows automation ownership |
Use creates: on unarchive |
Idempotent extraction |
Name reports with inventory_hostname |
Unique files per host |
Fetch with flat: false |
Preserves hostname layout on control node |
| Keep templates thin | Easier to test and review |
Verify with getent hosts or slurp |
Confirms rendered content works |
Summary
On Rocky Linux 10, generate /etc/hosts from inventory network addresses only: ansible_host on the left, inventory_hostname as aliases, one line per unique IP. Deploy with ansible.builtin.template and backup: true—no fact gathering required for the hosts file. Use gather_facts and ansible_* variables for system reports, not for /etc/hosts. Package directories with community.general.archive, extract with ansible.builtin.unarchive, and fetch reports with ansible.builtin.fetch.
References
- template module
- setup module
- community.general.archive module
- unarchive module
- fetch module

