
Ansible and Incus meet on two uses worth keeping apart: configuring the inside of instances that already exist (the community.general.incus connection plugin, which goes through incus exec with no SSH), and creating the instances themselves (provisioning). This guide covers both, with one important point of honesty: there is no native Ansible module dedicated to Incus, so provisioning goes through lxd_container. Everything is tested on Incus 7.0.
What you will learn
- The difference between the connection plugin and provisioning.
- Manage an existing instance with
community.general.incus, without SSH. - Build a dynamic inventory from the running instances.
- Create an Incus instance with
community.general.lxd_container. - The real state of Ansible support for Incus.
Prerequisites
- Ansible installed with the
community.generalcollection (shipped with the fullansiblepackage, otherwiseansible-galaxy collection install community.general). - The
incusclient on the machine running Ansible: both approaches rely on it (incus exec, local socket). - Incus basics: see First steps with Incus.
Two uses not to confuse
Before writing anything, pick the right tool for the need. The table sums up the split, which structures the rest of this guide.
| Need | Tool | How |
|---|---|---|
| Configure the inside of an existing instance | community.general.incus connection plugin | Ansible runs incus exec, no SSH |
| List the instances that exist | community.general.incus inventory plugin | Ansible calls the incus CLI |
| Create or delete an instance | community.general.lxd_container module | Through the API, with an explicit Incus socket |
| Build an image | Packer plus Ansible | See the dedicated guide |
Managing an existing instance (connection plugin)
The community.general.incus plugin treats an instance as a remote machine, but without SSH: every task runs through incus exec. It is ideal for applying configuration to a container that is already running.
The inventory declares the connection and the instance name on the Incus side.
all: hosts: tf-web: ansible_connection: community.general.incus ansible_incus_remote: local ansible_incus_project: default ansible_incus_host: tf-web ansible_user: rootThe playbook first installs Python if needed (through raw, which does not depend on Python), then applies ordinary tasks.
- name: Configure an existing instance through incus hosts: tf-web gather_facts: false tasks: - name: Make sure python3 is present ansible.builtin.raw: command -v python3 || (apt-get update -qq && apt-get install -y -qq python3) changed_when: false
- name: Drop a marker file ansible.builtin.copy: content: "managed by ansible through the incus connection plugin\n" dest: /root/ansible-marker.txt mode: "0644"Execution goes through incus exec, not the network: no SSH key, no port opened inside the instance.
PLAY [Configure an existing instance through incus] ****TASK [Make sure python3 is present] ********************TASK [Drop a marker file] ******************************tf-web : ok=3 changed=2 unreachable=0 failed=0Discovering instances with the inventory plugin
Writing the inventory by hand stops scaling as soon as instances come and go. The community.general.incus inventory plugin, added in community.general 12.0.0, builds the list by calling the incus CLI:
plugin: community.general.incusfilters: - type=virtual-machine - status=runningansible-inventory -i incus.yml --graphThe filters option takes the same expressions as incus list, so the inventory narrows to exactly the instances you mean. The plugin can cover several remotes through remotes, restrict itself to one project with the remote:project syntax, and by default names hosts INSTANCE.PROJECT.REMOTE, which keeps two instances of the same name in different projects apart. Set host_fqdn: false to get the short name back.
Combined with the connection plugin, this gives a setup where nothing is written down by hand: the inventory reflects what is actually running, and each task reaches the instance without SSH.
Provisioning an instance (lxd_container)
To create an instance, you use the community.general.lxd_container module. That module targets LXD by default: to aim at Incus, you point its socket explicitly with url: unix:/var/lib/incus/unix.socket.
- name: Provision an Incus instance hosts: localhost connection: local gather_facts: false tasks: - name: Create and start ans-web on Incus community.general.lxd_container: name: ans-web url: unix:/var/lib/incus/unix.socket state: started source: type: image mode: pull server: https://images.linuxcontainers.org protocol: simplestreams alias: debian/13 profiles: ["default"] wait_for_ipv4_addresses: true timeout: 150The module is idempotent: a second run recreates nothing. The instance appears on the Incus side, here placed on node2 of the cluster.
localhost : ok=2 changed=1 unreachable=0 failed=0
+---------+---------+-----------+----------+| NAME | STATE | TYPE | LOCATION |+---------+---------+-----------+----------+| ans-web | RUNNING | CONTAINER | node2 |+---------+---------+-----------+----------+Key points
- Two distinct uses: configuring the inside (connection plugin) against creating (provisioning).
community.general.incusruns tasks throughincus exec, with no SSH.- The inventory plugin of the same name builds the host list from
incus list, with filters and multiple remotes. - A minimal
images:image needs Python installed withrawbefore the modules run. - You provision with
lxd_containerplusurl: unix:/var/lib/incus/unix.socket. - No native Incus module: use
lxd_container, or the CLI and API directly.
FAQ: common questions about Ansible and Incus
The community.general.incus connection plugin
The plugin treats the instance as a remote machine but goes through incus exec, not SSH.
all:
hosts:
tf-web:
ansible_connection: community.general.incus
ansible_incus_remote: local
ansible_incus_project: default
ansible_incus_host: tf-web
ansible_user: root
No SSH key, no port opened inside the instance. Tasks run straight in the container.
No native module, but two native plugins
There is no incus_container or incus_instance module in community.general. As of 13.4.0, the modules are lxd_container, lxd_profile, lxd_project, lxd_storage_pool_info and lxd_storage_volume_info. Incus support goes through lxd_container (the unified /1.0/instances API endpoint) with an overridden url:
community.general.lxd_container:
name: ans-web
url: unix:/var/lib/incus/unix.socket
state: started
Two plugins, on the other hand, are named incus and are native: the connection plugin and the inventory plugin. For networks or profiles, drive the CLI (ansible.builtin.command) or the API (ansible.builtin.uri).
The community.general.incus inventory plugin
Rather than maintaining a static inventory by hand, the inventory plugin (added in community.general 12.0.0) discovers instances by calling the incus CLI.
# incus.yml
plugin: community.general.incus
filters:
- type=virtual-machine
- status=running
ansible-inventory -i incus.yml --graph
The filters option takes the same expressions as incus list. The plugin can cover several remotes through remotes, restrict itself to one project with the remote:project syntax, and builds host names as INSTANCE.PROJECT.REMOTE unless host_fqdn is disabled.
Provisioning with lxd_container
community.general.lxd_container:
name: ans-web
url: unix:/var/lib/incus/unix.socket
state: started
source:
type: image
mode: pull
server: https://images.linuxcontainers.org
protocol: simplestreams
alias: debian/13
profiles: ["default"]
wait_for_ipv4_addresses: true
The module is idempotent: a second run recreates nothing. The instance shows up on the Incus side, placed automatically on a cluster node.
Install Python with raw first
The images: images are minimal and do not always carry Python, which Ansible modules need. The workaround is a first task using raw, run by /bin/sh without Python:
- name: Make sure python3 is present
ansible.builtin.raw: command -v python3 || (apt-get update -qq && apt-get install -y -qq python3)
changed_when: false
Without it, the copy or command modules fail on a missing interpreter.
Next steps
- Terraform provider: create the instances that these playbooks then configure.
- Automating Incus image builds: bake the configuration into the image with Packer and Ansible.
- Securing Incus: the guard rails to set on instances your automation creates in bulk.