Provision and configure with OpenTofu plus Ansible
Provision and configure with OpenTofu plus Ansible
This pattern composes Compute and Network.
A combined template that runs OpenTofu to provision a Quake AI VM with a floating IP, then runs Ansible against the new host using the openstack.cloud dynamic inventory plugin. A Makefile sequences the two steps so a single make deploy produces a configured, reachable host.
This is the canonical Day 0 plus Day 1 workflow on Quake AI. For the conceptual rationale and a deeper walkthrough of the inventory handoff, read How to use Ansible with OpenTofu on Quake AI.
Monthly cost estimate
Pricing calculator ↗Sized as a custom package on dedicated vCPU.
Monthly total for the required template above. Use the configurator below to add optional pieces and see the total update.
What each resource is for
m2a.large
m2a.large · 2 dedicated vCPU, 8 GiB RAM, 0.5 Gbps
Compute shown per role at custom-package rates ($29/dedicated vCPU, $7.25/shared vCPU, $1/GiB RAM). The headline above is the billed total: the cheaper of a named plan and the custom package, plus add-ons.
Included in baseline
m2a.large
2 dedicated vCPU, 8 GiB RAM, 0.5 Gbps
Compute + RAM rate basis
2 vCPU + 8 GiB RAM at $29/dedicated vCPU, $7.25/shared vCPU, $1/GiB RAM (regular). Totals apply the flat −$5/mo package promotion.
Public IP (included)
1 included with the custom package
Package promotional discount
Flat −$5.00/mo on the custom package (same promotion as named plans).
Included at no charge
These line items are zero on Quake AI. Many other providers meter them separately.
Data transfer (inbound and outbound)
Unlimited data transfer on every plan; Quake AI does not meter per-GB egress.
AWS, GCP, and Azure meter outbound transfer per GB. DigitalOcean and Hetzner include an allowance on compute plans, then charge overage.
Learn morePrivate networking
Private networks, subnets, Neutron routers, and security groups are included with the plan.
VPC objects are usually free to create elsewhere, but NAT gateways bill hourly plus per-GB processed. Quake AI uses router SNAT with no separate NAT line item.
Control-plane API requests
OpenStack API calls for provisioning and management are included.
Some managed services on other clouds meter API calls or charge for premium control-plane features.
Dev/test vs production
Start on shared CPU for dev/test, then promote to dedicated for production with a flavor resize. The network, storage, and template stay the same.
Dev/test on shared CPU
Burstable s1a flavors; suited to prototyping and low or bursty load.
Production on dedicated CPU
The headline estimate above; predictable steady-load performance.
Saves $49.50/mo while you build on shared CPU.
Shared flavors carry less RAM (m2a.large (8 GiB RAM) -> s1a.small (2 GiB RAM)). A resize reboots the instance; data on attached volumes persists. Size the dedicated flavor for the RAM your production workload needs.
Pricing data last validated: . For current rates, check quake.ai/pricing.
How to use this template#
Prerequisites#
- OpenTofu 1.6+ installed: see Get started with OpenTofu on Quake AI.
- Ansible 6+ installed: see Get started with Ansible on Quake AI.
- The
openstack.cloud,ansible.posix, andcommunity.generalcollections installed (see Required collections below). - Application credentials configured in
~/.config/openstack/clouds.yamlas a named cloudrumble. - An SSH keypair already uploaded to your Quake AI project. Set its name in
infra/terraform.tfvars.
Required collections#
The configure step uses ansible.posix.authorized_key for the operator key and community.general.ufw for the host firewall, in addition to openstack.cloud for the dynamic inventory plugin. Declare all three in a requirements.yml at the project root:
collections:
- name: openstack.cloud
- name: ansible.posix
version: ">=1.5.4"
- name: community.general
version: ">=8.0.0"Install them once before the first run:
ansible-galaxy collection install -r requirements.ymlThe standalone Configure a VM with Ansible template documents the same collection set; one requirements.yml covers both workflows.
File layout#
provision-and-configure/
Makefile
infra/
main.tf
outputs.tf
terraform.tfvars
inventory/
openstack.yml
playbooks/
site.yml
group_vars/
all.ymlRun command#
cd provision-and-configure
make deploymake deploy runs tofu apply and then ansible-playbook against the dynamic inventory. The Ansible run waits for SSH to come up before starting tasks.
To tear everything down: make destroy.
What this template produces#
- One Quake AI compute instance (
Ubuntu-24.04, flavorm2a.large) with a floating IP and a security group that allows SSH, HTTP, and HTTPS - The same configured host the Configure a VM with Ansible template produces: non-root user, hardened SSH,
ufw, andnginxserving a placeholder page - Instance metadata
role: webandenvironment: devso future Ansible runs can target the host through the dynamic inventory plugin
How to extend#
- Scale to a fleet by adding a
count = Nto theopenstack_compute_instance_v2resource and tagging each instance through metadata. - Replace the placeholder configuration with your application's
roles/. Reference Configure a VM with Ansible for the baseline tasks the playbook below imports inline. - Scale to several application VMs and put an edge reverse proxy in front, then point Ansible's
keyed_groupsat the application hosts. - Move secrets such as the operator email or registry credentials into Ansible Vault and reference them with
vars_files.
OpenTofu configuration#
infra/main.tf provisions the instance, security group, floating IP, and the keypair binding. Security groups attach to the Neutron port with security_group_ids; see Authoring IaC templates.
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
}
variable "external_network" {
description = "Public network used as the floating-IP pool. Discover with `openstack network list --external`."
type = string
default = "PublicStatic"
}
resource "openstack_networking_secgroup_v2" "web" {
name = "ansible-demo-web"
description = "Allow SSH, HTTP, and HTTPS"
}
resource "openstack_networking_secgroup_rule_v2" "ssh" {
direction = "ingress"
ethertype = "IPv4"
protocol = "tcp"
port_range_min = 22
port_range_max = 22
remote_ip_prefix = "0.0.0.0/0"
security_group_id = openstack_networking_secgroup_v2.web.id
}
resource "openstack_networking_secgroup_rule_v2" "http" {
direction = "ingress"
ethertype = "IPv4"
protocol = "tcp"
port_range_min = 80
port_range_max = 80
remote_ip_prefix = "0.0.0.0/0"
security_group_id = openstack_networking_secgroup_v2.web.id
}
resource "openstack_networking_secgroup_rule_v2" "https" {
direction = "ingress"
ethertype = "IPv4"
protocol = "tcp"
port_range_min = 443
port_range_max = 443
remote_ip_prefix = "0.0.0.0/0"
security_group_id = openstack_networking_secgroup_v2.web.id
}
data "openstack_images_image_v2" "ubuntu" {
name = "Ubuntu-24.04"
most_recent = true
}
data "openstack_networking_network_v2" "external" {
name = var.external_network
}
resource "openstack_networking_port_v2" "web" {
name = "web-01-port"
network_id = data.openstack_networking_network_v2.external.id
security_group_ids = [openstack_networking_secgroup_v2.web.id]
}
resource "openstack_compute_instance_v2" "web" {
name = "web-01"
flavor_name = "m2a.large"
key_pair = var.key_name
metadata = {
role = "web"
environment = "dev"
}
block_device {
uuid = data.openstack_images_image_v2.ubuntu.id
source_type = "image"
destination_type = "volume"
volume_size = 20
boot_index = 0
delete_on_termination = true
}
network {
port = openstack_networking_port_v2.web.id
}
}
resource "openstack_networking_floatingip_v2" "web" {
pool = var.external_network
}
resource "openstack_networking_floatingip_associate_v2" "web" {
floating_ip = openstack_networking_floatingip_v2.web.address
port_id = openstack_networking_port_v2.web.id
}infra/outputs.tf exposes the floating IP so other tooling can read it without parsing state directly.
output "web_floating_ip" {
description = "Public IP of the configured web host"
value = openstack_networking_floatingip_v2.web.address
}
output "web_instance_id" {
description = "OpenStack instance ID of the web host"
value = openstack_compute_instance_v2.web.id
}infra/terraform.tfvars sets the keypair name. Replace my-keypair with the keypair you have uploaded to Quake AI.
key_name = "my-keypair"Ansible dynamic inventory#
inventory/openstack.yml configures the openstack.cloud.openstack plugin. The plugin queries Quake AI and groups instances by their role and environment metadata, which the OpenTofu configuration sets at create time.
plugin: openstack.cloud.openstack
clouds:
- rumble
expand_hostvars: false
inventory_hostname: name
keyed_groups:
- key: metadata.role
prefix: role
- key: metadata.environment
prefix: env
compose:
ansible_host: interface_ip
ansible_user: ubuntuinterface_ip is the address openstacksdk resolves for connecting to the host (the floating IP if attached, otherwise the first fixed IP). The plugin also emits meta-role_web and meta-environment_dev siblings to the role_web and env_dev groups; either set works as a playbook target.
Verify the inventory after tofu apply completes:
ansible-inventory -i inventory/openstack.yml --list
# expect: role_web group containing web-01 with ansible_host set to the floating IPGroup variables#
group_vars/all.yml is the same shape as the standalone configure-vm template; it centralizes the values the playbook references.
operator_user: deploy
operator_ssh_key: "{{ lookup('file', '~/.ssh/id_ed25519.pub') }}"
baseline_packages:
- curl
- git
- vim
- fail2ban
- ufw
- nginx
firewall_allowed_ports:
- 22
- 80
- 443Site playbook#
playbooks/site.yml runs against the role_web group emitted by the dynamic inventory. The task list is the same as the standalone Configure a VM template; collapse the two playbooks into a shared role if you maintain both.
---
- name: Wait for SSH on freshly provisioned hosts
hosts: role_web
gather_facts: false
tasks:
- name: Wait for SSH to accept connections
ansible.builtin.wait_for:
host: "{{ ansible_host }}"
port: 22
delay: 5
timeout: 300
delegate_to: localhost
- name: Configure Quake AI web host
hosts: role_web
become: true
gather_facts: true
tasks:
- name: Update apt cache
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
- name: Upgrade installed packages
ansible.builtin.apt:
upgrade: dist
autoremove: true
- name: Install baseline packages
ansible.builtin.apt:
name: "{{ baseline_packages }}"
state: present
- name: Create non-root sudo user
ansible.builtin.user:
name: "{{ operator_user }}"
groups: sudo
shell: /bin/bash
create_home: true
append: true
- name: Authorize operator SSH key
ansible.posix.authorized_key:
user: "{{ operator_user }}"
key: "{{ operator_ssh_key }}"
state: present
- name: Ensure /run/sshd exists for sshd validate
ansible.builtin.file:
path: /run/sshd
state: directory
mode: "0755"
- name: Harden sshd_config
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: "{{ item.regexp }}"
line: "{{ item.line }}"
state: present
validate: sshd -t -f %s
loop:
- { regexp: "^#?PermitRootLogin", line: "PermitRootLogin no" }
- { regexp: "^#?PasswordAuthentication", line: "PasswordAuthentication no" }
- { regexp: "^#?MaxAuthTries", line: "MaxAuthTries 3" }
notify: Restart sshd
- name: Set ufw default-deny inbound
community.general.ufw:
direction: incoming
policy: deny
- name: Allow firewall ports
community.general.ufw:
rule: allow
port: "{{ item }}"
proto: tcp
loop: "{{ firewall_allowed_ports }}"
- name: Enable ufw
community.general.ufw:
state: enabled
- name: Render placeholder index page
ansible.builtin.copy:
dest: /var/www/html/index.html
mode: "0644"
content: |
<!doctype html>
<html>
<head><title>Provisioned and configured</title></head>
<body>
<h1>Hello from Quake AI</h1>
<p>OpenTofu provisioned this host and Ansible configured it.</p>
</body>
</html>
- name: Ensure nginx is running
ansible.builtin.service:
name: nginx
state: started
enabled: true
- name: Verify nginx responds
ansible.builtin.uri:
url: http://127.0.0.1/
status_code: 200
return_content: false
handlers:
- name: Restart sshd
ansible.builtin.service:
name: ssh
state: restartedMakefile#
The Makefile is the single entry point. make deploy runs OpenTofu and then Ansible; make configure reruns only the playbook against the existing inventory; make destroy tears everything down.
.PHONY: init plan apply deploy configure destroy
INFRA := infra
INVENTORY := inventory/openstack.yml
PLAYBOOK := playbooks/site.yml
init:
tofu -chdir=$(INFRA) init
plan: init
tofu -chdir=$(INFRA) plan
apply: init
tofu -chdir=$(INFRA) apply -auto-approve
configure:
ansible-playbook -i $(INVENTORY) $(PLAYBOOK)
deploy: apply configure
destroy:
tofu -chdir=$(INFRA) destroy -auto-approveRun it complete#
cd provision-and-configure
make deployExpected output:
tofu applyreports a single instance, security group, three security group rules, one floating IP, and one floating-IP association created.ansible-inventory --list(run by the playbook step) groups the instance underrole_webwithansible_hostset to the floating IP.ansible-playbookreportsokfor every task on the first run andok=N changed=0on subsequent runs.
After make deploy, the host responds at http://<floating-ip>/. To converge changes after editing the playbook or group_vars/all.yml, run make configure.
When to use this pattern#
Run OpenTofu provisioning and an Ansible playbook from one Makefile-driven workflow. Choose Configure a VM with Ansible when the VM already exists, or Simple VM with Floating IP for provision-only.
Customize this pattern#
- Customize a template's image and flavor
- Add a block volume to a template
- Parameterize a template with a tfvars file
Resources, parameters, and variables
See also#
- How to use Ansible with OpenTofu on Quake AI: conceptual walkthrough of the dynamic-inventory handoff
- Configure a VM with Ansible: the standalone Day 1 template this combined workflow imports
- Ansible dynamic inventory on Quake AI: keyed-group patterns and inventory caching
- Ansible openstack.cloud module reference: module catalog
- Simple VM with Floating IP: the OpenTofu-only equivalent of the provision step
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: 07.07.2026
Quick answers
See Also
Use Ansible with OpenTofu on Quake AI
Shares: Ansible, Configuration Management
Automation concepts
Shares: Ansible, Configuration Management
Automation how-to guides
Shares: Ansible, Configuration Management
Terraform and OpenTofu on Quake AI
Prerequisite
How to provision a Quake AI instance with Ansible
Shares: Ansible, Configuration Management