Skip to content

How to deploy VMs with Terraform (simple example)

How-to · Updated May 2026

Coming from another cloud?

▸AWS·EC2 Instances

EC2 Instanceshigh

  • Uses EC2 RunInstances API instead of Nova servers.create.
  • Requires predefined instance type selection.
  • Supports per-second On-Demand billing and Spot/Reserved options.
  • Includes hibernation state not standard in OpenStack.
AWS docs ↗
▸Azure·Virtual Machines

Virtual Machineshigh

  • Uses Azure Resource Manager (ARM) REST API at /providers/Microsoft.Compute/virtualMachines instead of OpenStack Nova API at /v2.1/servers.
  • Tightly integrated with Azure services like Azure Active Directory for authentication, unlike OpenStack's Keystone.
  • VM creation requires specifying size from predefined series with hardware-specific features (e.g., AMD/Intel/ARM), not custom flavor configs.
  • Billed per second with complex pricing tiers based on series/reservation options, vs OpenStack's typically hourly or usage-based.
Azure docs ↗
▸DigitalOcean·Droplets

Dropletshigh

  • API surface is DigitalOcean’s proprietary REST/CLI/Terraform tooling rather than OpenStack Nova/Neutron/Glance APIs (Droplets are managed via DigitalOcean UI/CLI/API/Terraform).
  • Billing is usage-based with per-second billing (60-second minimum and monthly cap) rather than the typical per-hour, quota-based charge model users often see in OpenStack-based clouds.
  • Droplets include a bundled outbound transfer allowance with each plan (starting at 500 GiB/month) rather than a separate bandwidth quota/metering model users often encounter in OpenStack deployments.
  • Droplets are described as Linux-based VMs on virtualized hardware with local SSD storage, whereas OpenStack deployments commonly expose distinct block storage (Cinder) and image services (Glance) and may not bundle bandwidth/monitoring/firewalls into the instance offering.
DigitalOcean docs ↗
▸Google Cloud·VM instances

VM instanceshigh

  • Uses REST API 'instances.insert' instead of Nova 'servers.create' with different auth via service accounts vs Keystone.
  • Supports bare metal instances (no hypervisor), not in standard OpenStack Nova.
  • Network interfaces tied to VPC subnets; differs from Neutron ports/floating IPs.
Google Cloud docs ↗
▸Hetzner·Cloud Servers

Cloud Servershigh

  • Servers provisioned individually via Hetzner API (hcloud), not OpenStack Nova flavors; fixed instance types like CX11 (1 vCPU, 2GB RAM, 20GB NVMe).
  • No flavor customization; choose from predefined shared/dedicated vCPU series.
  • Billing hourly with monthly cap per server (e.g., €3.29/mo cap for CX11), charged even when powered off until deleted, unlike typical OpenStack stop-to-pause billing.
  • Custom REST API at api.hetzner.cloud/v1/servers instead of OpenStack Nova /v2.1/servers (different auth, payloads, response formats).
Hetzner docs ↗

How to deploy VMs with Terraform (simple example)

Prerequisites

Windows: CLI examples use bash. Set up a Linux CLI environment on Windows before proceeding.

This configuration deploys a scalable set of virtual machines on Quake AI with associated storage, networking, and security.

Project quota requirements#

The defaults in the downloadable zip are sized for the Developer-tier project quota. Apply consumes:

  • 1 instance at s1a.small (2 GB RAM).
  • 1 floating IP allocation (the instance attaches directly to PublicEphemeral, so no separate FIP is created).
  • 2 block volumes per instance (one OS volume, one data volume).

Confirm the project has the RAM headroom before applying:

bash
openstack limits show --absolute -f value -c name -c value

For production sizing (more VMs or m2a.xlarge flavors at 16 GB each), raise app_vm_count and flavor_app together and request a matching quota raise from the Quake AI operations team. Three m2a.xlarge instances total 48 GB RAM, which exceeds the default-tier 32 GB quota.

Download the code#

Download the files here.

Codebase structure#

The infrastructure is defined across these Terraform files, each responsible for specific components:

  • main.tf - Core provider configuration and initialization
  • variables.tf - Variable definitions for customizing the infrastructure
  • outputs.tf - Output values for verification after apply
  • vms_app.tf - VM instance configurations
  • volumes_app.tf - Storage volume definitions
  • securitygroup_app.tf - Security group rules and configurations
  • keypair.tf - SSH key pair configuration
  • ports_app.tf - Network port configurations
  • servergroup_app.tf - Server group definitions for VM placement
  • data.tf - Data source definitions

Component overview#

Provider configuration (main.tf)#

The infrastructure uses the OpenStack provider (version 2.0.0). Authentication can be configured either through environment variables or by directly specifying credentials in the provider block.

Variables and customization (variables.tf)#

The infrastructure is configurable through variables including:

  • system_name - Base name for resource naming
  • app_vm_count - Number of VMs to deploy (default: 1)
  • flavor_app - VM instance size (default: s1a.small)
  • image_app - OS image (default: Ubuntu-22.04)
  • volume_app_os - OS volume size in GB (default: 10)
  • volume_app - Additional volume size in GB (default: 10)
  • app_subnet - Subnet configuration (default: 192.168.1)
  • cloud_network - Network name (default: PublicEphemeral)
  • keypair - SSH public key. No default: tofu plan prompts for a value, and apply refuses to run without one.

Virtual machines and storage#

  • Each VM is created with two volumes:
    • An OS volume for the system
    • An additional volume for data storage
  • VMs are configured with network ports and security group rules
  • Server groups ensure proper VM placement and distribution

Networking and security#

  • Security groups define inbound and outbound traffic rules
  • Network ports connect VMs to the specified network
  • The infrastructure uses a pre-existing network (PublicEphemeral)

Access management#

  • SSH access is configured through keypairs
  • Security groups control network access to the VMs

How it works#

  1. When applied, Terraform first initializes the OpenStack provider and validates the configuration.
  2. It then creates the necessary security groups and rules.
  3. Storage volumes are provisioned for each VM.
  4. Network ports are created and configured.
  5. VMs are launched with the specified image and connected to their volumes and network ports.
  6. The server group ensures proper VM distribution across the infrastructure.

Infrastructure diagram#

+------------------------+
|     Public Network     |
+------------------------+
           |
+------------------------+
|    Security Groups     |
+------------------------+
           |
     +----------+
     |   VMs    |
     |  (1-N)   |
     +----------+
         |   |
   +-----+   +-----+
   |               |
+--------+    +---------+
| OS Vol |    |Data Vol |
+--------+    +---------+

Usage notes#

  1. Ensure your OpenStack credentials are configured (see Generate app credentials).

  2. Set the keypair variable with your public SSH key. The variable has no default, so tofu plan will prompt you (or set it via TF_VAR_keypair, a terraform.tfvars file, or -var keypair=...).

  3. Adjust the VM count and specifications in variables.tf as needed; raise app_vm_count and flavor_app together for production sizing.

  4. Use standard OpenTofu commands to manage the infrastructure:

    • tofu init
    • tofu plan
    • tofu apply
    • tofu destroy

Verify#

After tofu apply completes, retrieve the new instance addresses and SSH commands from the OpenTofu outputs declared in outputs.tf:

bash
tofu output
tofu output -json ssh_commands

You can also confirm the instance is running through the OpenStack CLI:

bash
openstack server list
openstack server show <instance-name> -c addresses -c status

To destroy every resource and release the floating IP:

bash
tofu destroy

Detailed file descriptions#

main.tf#

This file serves as the foundation of the infrastructure configuration:

HCL
terraform {
  required_providers {
    openstack = {
      source = "terraform-provider-openstack/openstack"
      version = "2.0.0"
    }
  }
}

provider "openstack" {
  # Configuration via environment variables or direct credentials
}

Key aspects:

  • Provider Block: Specifies OpenStack as the infrastructure provider with version 2.0.0

  • Authentication: Supports two methods:

    1. Environment variables (recommended) using OpenStack RC file
    2. Direct credential configuration in the provider block
  • Version Pinning: Explicitly pins the OpenStack provider version to ensure consistency

  • Provider Source: Uses the official terraform-provider-openstack/openstack source

Best practices implemented:

  • Version constraint to prevent unexpected provider updates
  • Commented credential placeholders for configuration
  • Flexibility in authentication methods

variables.tf#

This file defines all configurable parameters for the infrastructure. Variables are organized into logical groups:

HCL
# System Identification
variable "system_name" {
  type    = string
  default = "simplevms"
}

# VM Configuration (defaults sized for the Developer-tier project quota)
variable "app_vm_count" {
  type    = string
  default = "1"
}
variable "flavor_app" {
  type    = string
  default = "s1a.small"
}
# ... more variables ...

# Required: SSH public key (no default; apply refuses without a value)
variable "keypair" {
  description = "Public SSH key (ssh-rsa or ssh-ed25519 ...)"
  type        = string
}

Variable categories:

  1. System Identification

    • system_name: Base name for resource identification (default: "simplevms")
  2. VM Configuration

    • app_vm_count: Number of VMs to deploy (default: 1; raise with flavor_app for production)
    • flavor_app: VM size/flavor (default: s1a.small)
    • image_app: OS image selection (default: Ubuntu-22.04)
  3. Storage Configuration

    • volume_app_os: Size of OS volume in GB (default: 10)
    • volume_app: Size of additional data volume in GB (default: 10)
  4. Network Configuration

    • app_subnet: Subnet CIDR base (default: 192.168.1)
    • cloud_network: Network name (default: PublicEphemeral)
  5. Access Configuration

    • keypair: SSH public key for VM access. No default: OpenTofu prompts at plan time and refuses to apply without a value, so a missing key fails fast instead of failing late after creating security groups, ports, and other dependent resources.

Best practices implemented:

  • All variables have explicit types defined
  • Sensible defaults sized for the default-tier project quota
  • Clear grouping and documentation of variables
  • Required variables (like keypair) ship with no default so missing values surface at plan time

vms_app.tf#

This file defines the core VM instances and their configurations using the OpenStack Compute service:

HCL
resource "openstack_compute_instance_v2" "server_app" {
  count           = var.app_vm_count
  name            = "${var.system_name}-app-${format("%02d", count.index + 1)}"
  flavor_name     = var.flavor_app
  key_pair        = openstack_compute_keypair_v2.key.name
  # ... configuration continues ...
}

Key components:

  1. Instance Configuration

    • Dynamic instance count based on app_vm_count
    • Standardized naming with zero-padded indices (for example, app-01, app-02)
    • VM size defined by flavor_app variable
    • SSH key integration for secure access
  2. Network Integration

    • Security group association for network rules

    • Port assignment from pre-configured network ports

      HCL
      network {
        port = openstack_networking_port_v2.app_ports.*.id[count.index]
      }
  3. Storage Configuration

    • Boot volume configuration using specified image

    • Volume size defined by volume_app_os

    • Automatic volume cleanup on instance termination

      HCL
      block_device {
      source_type           = "image"
      destination_type      = "volume"
      delete_on_termination = true
      }
  4. Availability Management

    • Server group integration for anti-affinity
    • Ensures VMs are distributed across different compute nodes
    HCL
    scheduler_hints {
      group = openstack_compute_servergroup_v2.app_server_group_anti_affinity.id
    }

Best practices implemented:

  • Zero-padded instance numbering for consistent sorting
  • Boot from volume configuration for persistence
  • Anti-affinity rules for high availability
  • Integration with security groups and network ports

volumes_app.tf#

This file manages the additional data volumes for the VMs and their attachments:

HCL
resource "openstack_blockstorage_volume_v3" "volume_app" {
  count = length(openstack_compute_instance_v2.server_app)
  name  = "${var.system_name}-volumes-app-${format("%02d", count.index + 1)}"
  size  = var.volume_app
}

resource "openstack_compute_volume_attach_v2" "volume_attach_app" {
  count       = length(openstack_compute_instance_v2.server_app)
  instance_id = openstack_compute_instance_v2.server_app.*.id[count.index]
  volume_id   = openstack_blockstorage_volume_v3.volume_app.*.id[count.index]
}

Key components:

  1. Volume Creation

    • Creates additional volumes for data storage
    • Volume count matches the number of VMs
    • Consistent naming scheme with the rest of the infrastructure
    • Size defined by volume_app variable
  2. Volume Attachment

    • Automatically attaches volumes to corresponding VMs
    • Uses instance and volume IDs for proper mapping
    • Maintains one-to-one relationship between VMs and volumes

Best practices implemented:

  • Dynamic volume count based on VM instances
  • Consistent naming convention with zero-padded indices
  • Automatic volume attachment handling
  • Clear separation between volume creation and attachment

securitygroup_app.tf#

This file defines the network security rules for the VM instances:

HCL
resource "openstack_networking_secgroup_v2" "secgroup_app" {
  name = "${var.system_name}-secgrp_app"
}

resource "openstack_networking_secgroup_rule_v2" "secgroup_rule_app_ssh_from_all" {
  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.secgroup_app.id
}
# ... additional rules ...

Key components:

  1. Security Group Definition

    • Creates a named security group for the application
    • Uses consistent naming convention with system name prefix
  2. Ingress Rules

    • SSH Access (Port 22)
      • Allows remote SSH connections from any IP
    • HTTP Access (Port 80)
      • Enables web traffic on standard HTTP port
    • HTTPS Access (Port 443)
      • Supports secure web traffic
    • ICMP (Ping)
      • Allows basic network connectivity testing
  3. Rule Configuration

    • All rules are ingress (incoming traffic)
    • IPv4 protocol support
    • Specific port ranges for each service
    • Global access (0.0.0.0/0) for all services

Best practices implemented:

  • Clear separation of rules by service
  • Standard ports for common services
  • Basic network connectivity testing enabled
  • Consistent rule structure and naming
  • Explicit direction and protocol definitions

keypair.tf#

This file manages the SSH key pair used for secure access to the VMs:

HCL
resource "openstack_compute_keypair_v2" "key" {
  name       = "${var.system_name}-keypair"
  public_key = var.keypair
}

Key components:

  1. Keypair Resource

    • Creates a named keypair in OpenStack
    • Uses consistent naming with system name prefix
    • Imports the public key specified in variables
  2. Integration Points

    • Referenced by VM instances for SSH access
    • Uses the public key defined in variables.tf
    • Enables secure remote access to instances

Best practices implemented:

  • Consistent resource naming
  • Separation of key material from configuration
  • Integration with VM provisioning
  • Uses OpenStack's key management system

ports_app.tf#

This file manages the network ports for the VM instances:

HCL
resource "openstack_networking_port_v2" "app_ports" {
  count              = var.app_vm_count
  name               = "${var.system_name}-app_ports-${format("%02d", count.index + 1)}"
  network_id         = data.openstack_networking_network_v2.cloud_network.id
  security_group_ids = [openstack_networking_secgroup_v2.secgroup_app.id]
}

Key components:

  1. Port Creation

    • Creates network ports for each VM instance
    • Dynamic port count based on app_vm_count
    • Consistent naming scheme with zero-padded indices
  2. Network Integration

    • Associates ports with the specified cloud network
    • References network ID from data source
    • Links security groups to ports
  3. Security Integration

    • Applies security group rules at the port level
    • Direct integration with the application security group

Best practices implemented:

  • Dynamic port creation matching VM count
  • Consistent resource naming convention
  • Security group integration at network level
  • Clean separation of networking concerns

servergroup_app.tf#

This file defines the server group policy for VM placement:

HCL
resource "openstack_compute_servergroup_v2" "app_server_group_anti_affinity" {
  name     = "${var.system_name}-app_server_group_anti_affinity"
  policies = ["soft-anti-affinity"]
}

Key components:

  1. Server Group Definition

    • Creates a named server group for VM placement
    • Uses consistent naming with system name prefix
    • Implements soft anti-affinity policy
  2. Anti-Affinity Policy

    • Uses "soft-anti-affinity" for flexible VM distribution
    • Encourages VMs to run on different compute nodes
    • Allows fallback if strict distribution isn't possible
  3. Integration Points

    • Referenced by VM instances in their scheduler hints
    • Helps OpenStack make intelligent placement decisions
    • Supports high availability goals

Best practices implemented:

  • High availability through VM distribution
  • Flexible placement with soft anti-affinity
  • Consistent resource naming
  • Integration with VM scheduling

Note: Soft anti-affinity is preferred over strict anti-affinity as it allows the infrastructure to still function even if perfect distribution isn't possible.

data.tf#

This file defines the data sources used to reference existing OpenStack resources:

HCL
data "openstack_networking_network_v2" "cloud_network" {
  name = var.cloud_network
}

data "openstack_images_image_v2" "image_app" {
  name = var.image_app
}

Key components:

  1. Network Data Source

    • References existing network by name
    • Uses network name from cloud_network variable
    • Provides network ID for port creation
  2. Image Data Source

    • References VM image by name
    • Uses image name from image_app variable
    • Provides image ID for VM creation
  3. Integration Points

    • Network data used in port configuration
    • Image data used in VM boot volume configuration
    • Enables reuse of existing OpenStack resources

Best practices implemented:

  • Separation of data sources from resource creation
  • Reuse of existing infrastructure components
  • Dynamic resource referencing
  • Clean integration with variables

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

Was this page helpful?