Skip to content

Use Ansible with OpenTofu on Quake AI

Tutorial · Updated Jun 2026
Before this

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: tofu installed, OS_* env vars sourced
  • Ansible setup complete: ansible, openstacksdk >= 1.0.0, and the openstack.cloud collection installed
  • Application credentials configured in ~/.config/openstack/clouds.yaml as 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.

Sequenced OpenTofu and Ansible handoffThe operator runs tofu apply, which calls the OpenStack API to create a VM and floating IP. Ansible then reads inventory from either dynamic discovery or tofu output, connects to the VM over SSH, and runs the playbook.Quake AI VMansible-playbookQuake AI APItofu applyQuake AI VMansible-playbookQuake AI APItofu applyOperatorruncreate VM and floating IPresource attributesterraform.tfstate plus outputrun with inventoryopenstack.cloud.openstackinventory queryhosts grouped by metadataSSH apply tasksok / changedOperatorClick to zoom
Sequenced handoff: OpenTofu creates the VM, Ansible reads the floating IP, the playbook configures the host over SSH.

Two common ways bridge from OpenTofu to Ansible:

ApproachCouplingBest for
Dynamic inventory pluginLooseMost teams. Ansible queries Quake AI directly. No file passes between tools.
tofu output -json to a generated inventory fileTightTeams 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:

bash
mkdir tofu-ansible && cd tofu-ansible
mkdir infra playbooks inventory

Add infra/main.tf:

HCL
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:

HCL
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:

bash
cd infra
tofu init
tofu apply -var "key_name=YOUR_KEYPAIR_NAME"
cd ..

Confirm the floating IP:

bash
tofu -chdir=infra output web_floating_ip

Add inventory/openstack.yml:

YAML
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: role

The 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:

bash
ansible-inventory -i inventory/openstack.yml --list

You 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:

bash
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.ini

The resulting inventory/hosts.ini looks like:

ini
[web]
web-01 ansible_host=203.0.113.42 ansible_user=ubuntu

This 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:

YAML
---
- 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: true

The 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:

bash
ansible-playbook -i inventory/openstack.yml playbooks/site.yml

The play recap should show ok=5 changed=5 unreachable=0 failed=0.

Verify the host responds:

bash
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.

YAML
# .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.yml

For 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#

bash
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

Quick answers

Was this page helpful?