Use Ansible with OpenTofu on Quake AI
Use Ansible with OpenTofu on Quake AI
This guide combines OpenTofu provisioning with Ansible configuration into a single workflow. You will use OpenTofu to create a VM with a floating IP, hand the IP to Ansible, and run a playbook that configures the host. The same pattern scales from one VM to a fleet.
For the conceptual rationale, read Ansible on Quake AI. The two-step provision-then-configure pattern is the recommended Quake AI workflow for any host that needs more than the base image.
Prerequisites#
- OpenTofu setup complete:
tofuinstalled,OS_*env vars sourced - Ansible setup complete:
ansible,openstacksdk >= 1.0.0, and theopenstack.cloudcollection installed - Application credentials configured in
~/.config/openstack/clouds.yamlas a named cloud (rumble) - An SSH keypair already uploaded to your Quake AI project
How the handoff works#
The provision step creates infrastructure and tags each instance with metadata. The configure step discovers those instances by metadata and applies a playbook to each one.
Two common ways bridge from OpenTofu to Ansible:
| Approach | Coupling | Best for |
|---|---|---|
| Dynamic inventory plugin | Loose | Most teams. Ansible queries Quake AI directly. No file passes between tools. |
tofu output -json to a generated inventory file | Tight | Teams that want a version-controlled, deterministic inventory committed to git. |
This guide shows the dynamic inventory plugin path first because it is the simpler default. The generated-inventory pattern follows as an alternative.
Step 1: Define the OpenTofu configuration#
Create the project structure:
mkdir tofu-ansible && cd tofu-ansible
mkdir infra playbooks inventoryAdd infra/main.tf:
terraform {
required_version = ">= 1.6.0"
required_providers {
openstack = {
source = "terraform-provider-openstack/openstack"
version = "~> 2.0"
}
}
}
provider "openstack" {}
variable "key_name" {
description = "Name of an existing keypair in the Quake AI project"
type = string
}
resource "openstack_compute_instance_v2" "web" {
name = "web-01"
image_name = "Ubuntu-24.04"
flavor_name = "m2a.large"
key_pair = var.key_name
network {
name = "PublicStatic"
}
metadata = {
environment = "production"
role = "web"
}
}
resource "openstack_networking_floatingip_v2" "web" {
pool = "PublicStatic"
}
resource "openstack_compute_floatingip_associate_v2" "web" {
floating_ip = openstack_networking_floatingip_v2.web.address
instance_id = openstack_compute_instance_v2.web.id
}Add infra/outputs.tf:
output "web_floating_ip" {
value = openstack_networking_floatingip_v2.web.address
}
output "web_instance_id" {
value = openstack_compute_instance_v2.web.id
}The metadata block is the bridge. The dynamic inventory plugin reads environment and role and groups the instance into env_production and role_web.
The pool argument is the external network name. In us-east-1 the external network is PublicStatic; discover the name in your region with openstack network list --external.
Initialize and apply:
cd infra
tofu init
tofu apply -var "key_name=YOUR_KEYPAIR_NAME"
cd ..Confirm the floating IP:
tofu -chdir=infra output web_floating_ipStep 2 (recommended): Configure dynamic inventory#
Add inventory/openstack.yml:
plugin: openstack.cloud.openstack
clouds:
- rumble
expand_hostvars: false
inventory_hostname: name
compose:
ansible_user: "'ubuntu'"
keyed_groups:
- key: openstack.metadata.environment
prefix: env
- key: openstack.metadata.role
prefix: roleThe clouds list references the named cloud you configured in ~/.config/openstack/clouds.yaml during the Ansible getting-started guide. The compose: block sets ansible_user to ubuntu (quoted as a Jinja string literal) so playbooks SSH in with the Quake AI Ubuntu image's default user. The plugin sets ansible_host to the floating IP when one is attached.
Verify the inventory resolves the new instance:
ansible-inventory -i inventory/openstack.yml --listYou should see web-01 listed under env_production and role_web (from keyed_groups) and the auto-generated meta-environment_production and meta-role_web siblings (from the plugin's metadata-driven group expansion). Either set works as a playbook target. See the dynamic inventory how-to for filtering to the current project when the application credential has access to a shared project.
Step 2 (alternative): Generate inventory from tofu output#
If you prefer a static inventory file that lives in git, generate it from OpenTofu output:
tofu -chdir=infra output -json > /tmp/tofu-output.json
jq -r '"[web]\nweb-01 ansible_host=" + .web_floating_ip.value + " ansible_user=ubuntu"' \
/tmp/tofu-output.json > inventory/hosts.iniThe resulting inventory/hosts.ini looks like:
[web]
web-01 ansible_host=203.0.113.42 ansible_user=ubuntuThis pattern is brittle when the inventory grows: you have to regenerate the file every time OpenTofu changes anything. Reach for the dynamic plugin first.
Step 3: Write the playbook#
Add playbooks/site.yml:
---
- name: Configure web hosts
hosts: role_web
become: true
tasks:
- name: Wait for SSH to come up
ansible.builtin.wait_for_connection:
timeout: 300
- name: Update apt cache
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
- name: Install nginx
ansible.builtin.apt:
name: nginx
state: present
- name: Render landing page
ansible.builtin.copy:
dest: /var/www/html/index.html
content: "Provisioned by OpenTofu, configured by Ansible.\n"
owner: www-data
group: www-data
mode: "0644"
- name: Ensure nginx is running
ansible.builtin.service:
name: nginx
state: started
enabled: trueThe wait_for_connection task is important: a freshly provisioned VM may take 30 to 60 seconds before the SSH daemon is ready. Without it, the first run of the playbook can fail intermittently on the first task.
The hosts: role_web selector matches the role metadata you set in main.tf. Add or remove instances by editing the OpenTofu configuration; the playbook target stays the same.
Step 4: Run the full workflow#
With OpenTofu already applied, run the playbook:
ansible-playbook -i inventory/openstack.yml playbooks/site.ymlThe play recap should show ok=5 changed=5 unreachable=0 failed=0.
Verify the host responds:
curl http://$(tofu -chdir=infra output -raw web_floating_ip)For repeat runs, re-apply OpenTofu (idempotent at the infra layer) and re-run the playbook (idempotent at the host layer).
Step 5: sequence the steps in CI#
A typical CI pipeline runs the two steps as sequential jobs. The provisioning job emits inventory information; the configuration job consumes it.
# .github/workflows/deploy.yml
name: Deploy
on:
workflow_dispatch:
jobs:
provision:
runs-on: ubuntu-latest
env:
OS_AUTH_URL: ${{ secrets.OS_AUTH_URL }}
OS_APPLICATION_CREDENTIAL_ID: ${{ secrets.OS_APPLICATION_CREDENTIAL_ID }}
OS_APPLICATION_CREDENTIAL_SECRET: ${{ secrets.OS_APPLICATION_CREDENTIAL_SECRET }}
steps:
- uses: actions/checkout@v4
- uses: opentofu/setup-opentofu@v1
- run: tofu -chdir=infra init
- run: tofu -chdir=infra apply -auto-approve -var "key_name=ci"
configure:
needs: provision
runs-on: ubuntu-latest
env:
OS_AUTH_URL: ${{ secrets.OS_AUTH_URL }}
OS_APPLICATION_CREDENTIAL_ID: ${{ secrets.OS_APPLICATION_CREDENTIAL_ID }}
OS_APPLICATION_CREDENTIAL_SECRET: ${{ secrets.OS_APPLICATION_CREDENTIAL_SECRET }}
steps:
- uses: actions/checkout@v4
- run: pip install "openstacksdk>=1.0.0" ansible
- run: ansible-galaxy collection install openstack.cloud
- run: ansible-playbook -i inventory/openstack.yml playbooks/site.ymlFor the full CI walk-through, including secret management and the parallel pattern in GitLab CI, see How to run Ansible in CI/CD and the OpenTofu equivalent at Integrate OpenTofu with CI/CD.
Tear down#
tofu -chdir=infra destroy -var "key_name=YOUR_KEYPAIR_NAME"Ansible has nothing to clean up: the SSH session is short-lived, no daemon was installed, and the VM no longer exists.
See also#
Usage Guidelines
The sample code, software libraries, command line tools, proofs of concept, templates, and other related technology on this page (including any of the foregoing that is provided by Quake AI personnel) is provided to you as Quake AI Content under the Quake AI Customer Agreement, or the relevant written agreement between you and Quake AI (whichever applies). Do not use this Quake AI Content in your production accounts, or on production or other critical data. You are responsible for testing, securing, and optimizing the Quake AI Content (such as sample code) as appropriate for production grade use based on your specific quality control practices and standards. Deploying Quake AI Content may incur Quake AI charges for creating or using Quake AI chargeable resources, such as running Compute instances or storing data in Object Storage. Your use is also subject to the Acceptable Use Policy.
For the full policy, see Usage Guidelines.
Last validated: 18.06.2026