# Use Ansible with OpenTofu on Quake AI

Source: https://docs.quake.ai/docs/automation/how-to/ansible-opentofu-workflow
Markdown: https://docs.quake.ai/docs/automation/how-to/ansible-opentofu-workflow.md

---

# 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](/docs/automation/concepts/ansible). 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](/docs/automation/how-to/getting-started-iac) complete: `tofu` installed, `OS_*` env vars sourced
- [Ansible setup](/docs/automation/how-to/getting-started-ansible) complete: `ansible`, `openstacksdk >= 1.0.0`, and the `openstack.cloud` collection installed
- [Application credentials](/docs/tools/generate-app-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.

<Figure size="md" caption="Sequenced handoff: OpenTofu creates the VM, Ansible reads the floating IP, the playbook configures the host over SSH.">

```mermaid
sequenceDiagram
    accTitle: Sequenced OpenTofu and Ansible handoff
    accDescr: The 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.

    actor Op as Operator
    participant Tofu as tofu apply
    participant API as Quake AI API
    participant Ans as ansible-playbook
    participant VM as Quake AI VM

    Op->>Tofu: run
    Tofu->>API: create VM and floating IP
    API-->>Tofu: resource attributes
    Tofu-->>Op: terraform.tfstate plus output
    Op->>Ans: run with inventory
    Ans->>API: openstack.cloud.openstack inventory query
    API-->>Ans: hosts grouped by metadata
    Ans->>VM: SSH apply tasks
    VM-->>Ans: ok / changed
```

</Figure>

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:

```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
```

## Step 2 (recommended): Configure dynamic inventory

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](/docs/automation/how-to/getting-started-ansible). 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](/docs/automation/how-to/ansible-dynamic-inventory#filter-the-inventory-to-the-current-project) 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](/docs/automation/how-to/ansible-cicd) and the OpenTofu equivalent at [Integrate OpenTofu with CI/CD](/docs/automation/how-to/cicd-integration).

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

- [Ansible on Quake AI](/docs/automation/concepts/ansible)
- [How to get started with Ansible on Quake AI](/docs/automation/how-to/getting-started-ansible)
- [How to use Ansible dynamic inventory on Quake AI](/docs/automation/how-to/ansible-dynamic-inventory)
- [How to get started with Infrastructure as Code](/docs/automation/how-to/getting-started-iac)
- [Integrate OpenTofu with CI/CD](/docs/automation/how-to/cicd-integration)
