Skip to content
IaC Templates

Simple VM with Floating IP

Template · Updated Jun 2026
Validated Jun 2026

Simple VM with Floating IP

This pattern composes Compute and Network.

What this template does#

Provisions a single compute instance with:

  • A private network, subnet (with DNS resolvers), and a router to the external network
  • A neutron port that binds the instance to the private network
  • A security group allowing SSH (port 22) and HTTP (port 80)
  • A volume-backed boot disk, so the instance works with zero-disk flavors
  • A floating IP for SSH and application access

The instance uses an existing SSH keypair named by the key_name variable, so you keep the private key. This is the simplest reliable pattern for a publicly reachable VM on Quake AI.

Parameters#

ParameterDescriptionDefault
flavor_nameInstance sizes1a.small
image_nameOperating system imageUbuntu-24.04
key_nameSSH keypair name (must already exist in your project)No default
instance_nameDisplay name for the instancesimple-vm
external_networkExternal network for the router gateway and floating IPPublicEphemeral
private_cidrCIDR for the private subnet10.10.10.0/24

When to use this pattern#

Reach for this pattern when you need one publicly reachable VM with a private network, security group, and floating IP. Choose Development Environment when you want multiple subnets, a bastion, and shared storage for a team sandbox.

Estimated cost#

Monthly cost estimate

Pricing calculator ↗

Sized as a custom package on shared vCPU.

Starting template$13.10/mo

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

Vm

s1a.small · 2 shared vCPU, 2 GiB RAM, 0.5 Gbps

$16.50/mo

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

s1a.small

2 shared vCPU, 2 GiB RAM, 0.5 Gbps

$16.50

Compute + RAM rate basis

2 vCPU + 2 GiB RAM at $29/dedicated vCPU, $7.25/shared vCPU, $1/GiB RAM (regular). Totals apply the flat −$5/mo package promotion.

—

Block storage (20 GiB)

20 GiB at $0.08/GiB/mo

$1.60

Public IP (included)

1 included with the custom package

$0.00

Package promotional discount

Flat −$5.00/mo on the custom package (same promotion as named plans).

$-5.00

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 more
$0.00

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

$0.00

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.

$0.00

Pricing data last validated: . For current rates, check quake.ai/pricing.

Template source#

6 files. Download the zip or expand to copy any file.Download simple-vm.zip
Show source (6 files)
main.tfHCL
data "openstack_images_image_v2" "os" {
  name        = var.image_name
  most_recent = true
}

data "openstack_networking_network_v2" "external" {
  name = var.external_network
}

resource "openstack_networking_network_v2" "private" {
  name           = "${var.instance_name}-net"
  admin_state_up = true
}

resource "openstack_networking_subnet_v2" "private" {
  name            = "${var.instance_name}-subnet"
  network_id      = openstack_networking_network_v2.private.id
  cidr            = var.private_cidr
  ip_version      = 4
  dns_nameservers = ["1.1.1.1", "8.8.8.8"]
}

resource "openstack_networking_router_v2" "main" {
  name                = "${var.instance_name}-router"
  external_network_id = data.openstack_networking_network_v2.external.id
}

resource "openstack_networking_router_interface_v2" "private" {
  router_id = openstack_networking_router_v2.main.id
  subnet_id = openstack_networking_subnet_v2.private.id
}

resource "openstack_networking_secgroup_v2" "vm" {
  name        = "${var.instance_name}-sg"
  description = "Allow SSH and HTTP"
}

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.vm.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.vm.id
}

resource "openstack_compute_instance_v2" "vm" {
  name        = var.instance_name
  flavor_name = var.flavor_name
  key_pair    = var.key_name

  block_device {
    uuid                  = data.openstack_images_image_v2.os.id
    source_type           = "image"
    destination_type      = "volume"
    volume_size           = 20
    boot_index            = 0
    delete_on_termination = true
  }

  network {
    port = openstack_networking_port_v2.vm.id
  }
}

resource "openstack_networking_port_v2" "vm" {
  name               = "${var.instance_name}-port"
  network_id         = openstack_networking_network_v2.private.id
  security_group_ids = [openstack_networking_secgroup_v2.vm.id]

  fixed_ip {
    subnet_id = openstack_networking_subnet_v2.private.id
  }

  depends_on = [openstack_networking_router_interface_v2.private]
}

resource "openstack_networking_floatingip_v2" "vm" {
  pool = var.external_network
}

resource "openstack_networking_floatingip_associate_v2" "vm" {
  floating_ip = openstack_networking_floatingip_v2.vm.address
  port_id     = openstack_networking_port_v2.vm.id
}
variables.tfHCL
variable "flavor_name" {
  description = "Instance size"
  type        = string
  default     = "s1a.small"
}

variable "image_name" {
  description = "Operating system image"
  type        = string
  default     = "Ubuntu-24.04"
}

variable "key_name" {
  description = "SSH keypair name (must already exist in your project)"
  type        = string
}

variable "instance_name" {
  description = "Display name for the instance"
  type        = string
  default     = "simple-vm"
}

variable "external_network" {
  description = "Shared external network for router gateway and floating IPs; defaults to PublicEphemeral (quickstart-aligned). Set PublicStatic in tfvars for a persisted floating IP."
  type        = string
  default     = "PublicEphemeral"
}

variable "private_cidr" {
  description = "CIDR for the private tenant network the VM lives in"
  type        = string
  default     = "10.10.10.0/24"
}
outputs.tfHCL
output "instance_id" {
  description = "ID of the compute instance"
  value       = openstack_compute_instance_v2.vm.id
}

output "floating_ip" {
  description = "Public floating IP address"
  value       = openstack_networking_floatingip_v2.vm.address
}

output "private_ip" {
  description = "Private IP address of the instance"
  value       = openstack_compute_instance_v2.vm.access_ip_v4
}
versions.tfHCL
terraform {
  required_version = ">= 1.6.0"

  required_providers {
    openstack = {
      source  = "terraform-provider-openstack/openstack"
      version = "~> 2.0"
    }
  }
}

provider "openstack" {}
terraform.tfvars.exampleHCL
# Required: SSH keypair must already exist in your project
key_name = "YOUR_KEY_NAME"

# flavor_name = "s1a.small"
# image_name = "Ubuntu-24.04"
# instance_name = "simple-vm"
# private_cidr = "10.10.10.0/24"
README.mdMarkdown
# Simple VM

Single compute instance with floating IP and security group.


**Network class:** throwaway / quickstart-aligned — `external_network` defaults to `PublicEphemeral`; use `PublicStatic` in tfvars when you need a persisted floating IP.

## Prerequisites

- OpenTofu >= 1.6.0 or Terraform >= 1.6.0
- Quake AI account with OpenStack credentials
- An existing SSH keypair in your project (the value of `key_name` must match that keypair)

## Usage

1. Clone or copy this template directory
2. Copy `terraform.tfvars.example` to `terraform.tfvars` and fill in your values
3. Source your OpenStack credentials: `source openrc.sh`
4. Initialize: `tofu init`
5. Preview: `tofu plan`
6. Apply: `tofu apply`

## Variables

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `key_name` | string | yes | n/a | SSH keypair name (must already exist in your project) |
| `flavor_name` | string | no | `s1a.small` | Instance size |
| `image_name` | string | no | `Ubuntu-24.04` | Operating system image |
| `instance_name` | string | no | `simple-vm` | Display name for the instance |
| `external_network` | string | no | `PublicEphemeral` | Defaults to ephemeral quickstart path; set `PublicStatic` for persisted FIP |
| `private_cidr` | string | no | `10.10.10.0/24` | CIDR for the private tenant network the VM lives in |

## Documentation

Full documentation: [Simple VM template](/docs/automation/templates/simple-vm)

Validated variants of this template, each adding one capability the base does not include.

  • Backupssimple-vm-backups

    Scheduled tarball backups to Object Storage

    Show source and download
    7 files. Download the zip or copy any file.Download simple-vm-backups.zip
    main.tfHCL
    data "openstack_images_image_v2" "os" {
      name        = var.image_name
      most_recent = true
    }
    
    data "openstack_networking_network_v2" "external" {
      name = var.external_network
    }
    
    resource "aws_s3_bucket" "backups" {
      bucket = var.backup_bucket_name
    }
    
    resource "aws_s3_bucket_acl" "backups" {
      bucket = aws_s3_bucket.backups.id
      acl    = "private"
    }
    
    resource "openstack_networking_network_v2" "private" {
      name           = "${var.instance_name}-net"
      admin_state_up = true
    }
    
    resource "openstack_networking_subnet_v2" "private" {
      name            = "${var.instance_name}-subnet"
      network_id      = openstack_networking_network_v2.private.id
      cidr            = var.private_cidr
      ip_version      = 4
      dns_nameservers = ["1.1.1.1", "8.8.8.8"]
    }
    
    resource "openstack_networking_router_v2" "main" {
      name                = "${var.instance_name}-router"
      external_network_id = data.openstack_networking_network_v2.external.id
    }
    
    resource "openstack_networking_router_interface_v2" "private" {
      router_id = openstack_networking_router_v2.main.id
      subnet_id = openstack_networking_subnet_v2.private.id
    }
    
    resource "openstack_networking_secgroup_v2" "vm" {
      name        = "${var.instance_name}-sg"
      description = "Allow SSH and HTTP"
    }
    
    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.vm.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.vm.id
    }
    
    resource "openstack_compute_instance_v2" "vm" {
      name        = var.instance_name
      flavor_name = var.flavor_name
      key_pair    = var.key_name
    
      user_data = templatefile("${path.module}/cloud-init/vm.yaml", {
        s3_endpoint       = var.s3_endpoint
        s3_region         = var.s3_region
        s3_access_key     = var.s3_access_key
        s3_secret_key     = var.s3_secret_key
        backup_bucket     = aws_s3_bucket.backups.id
        backup_schedule   = var.backup_schedule
      })
    
      block_device {
        uuid                  = data.openstack_images_image_v2.os.id
        source_type           = "image"
        destination_type      = "volume"
        volume_size           = 20
        boot_index            = 0
        delete_on_termination = true
      }
    
      network {
        port = openstack_networking_port_v2.vm.id
      }
    }
    
    resource "openstack_networking_port_v2" "vm" {
      name               = "${var.instance_name}-port"
      network_id         = openstack_networking_network_v2.private.id
      security_group_ids = [openstack_networking_secgroup_v2.vm.id]
    
      fixed_ip {
        subnet_id = openstack_networking_subnet_v2.private.id
      }
    
      depends_on = [openstack_networking_router_interface_v2.private]
    }
    
    resource "openstack_networking_floatingip_v2" "vm" {
      pool = var.external_network
    }
    
    resource "openstack_networking_floatingip_associate_v2" "vm" {
      floating_ip = openstack_networking_floatingip_v2.vm.address
      port_id     = openstack_networking_port_v2.vm.id
    }
    
    variables.tfHCL
    variable "flavor_name" {
      description = "Instance size"
      type        = string
      default     = "s1a.small"
    }
    
    variable "image_name" {
      description = "Operating system image"
      type        = string
      default     = "Ubuntu-24.04"
    }
    
    variable "key_name" {
      description = "SSH keypair name (must already exist in your project)"
      type        = string
    }
    
    variable "instance_name" {
      description = "Display name for the instance"
      type        = string
      default     = "simple-vm"
    }
    
    variable "external_network" {
      description = "Shared external network for router gateway and floating IPs; defaults to PublicStatic (persisted FIP / production pattern). Override with PublicEphemeral for ephemeral demos."
      type        = string
      default     = "PublicStatic"
    }
    
    variable "private_cidr" {
      description = "CIDR for the private tenant network the VM lives in"
      type        = string
      default     = "10.10.10.0/24"
    }
    
    variable "s3_access_key" {
      description = "EC2-compatible access key for Object Storage"
      type        = string
      sensitive   = true
    }
    
    variable "s3_secret_key" {
      description = "EC2-compatible secret key for Object Storage"
      type        = string
      sensitive   = true
    }
    
    variable "backup_bucket_name" {
      description = "S3 bucket name for scheduled VM backups"
      type        = string
    }
    
    variable "s3_endpoint" {
      description = "Quake AI S3-compatible endpoint URL"
      type        = string
      default     = "https://object.us-east-1.rumble.cloud"
    }
    
    variable "s3_region" {
      description = "S3 region identifier passed to the AWS provider"
      type        = string
      default     = "us-east-1"
    }
    
    variable "backup_schedule" {
      description = "Cron schedule for tarball backups to Object Storage"
      type        = string
      default     = "0 3 * * *"
    }
    
    outputs.tfHCL
    output "instance_id" {
      description = "ID of the compute instance"
      value       = openstack_compute_instance_v2.vm.id
    }
    
    output "floating_ip" {
      description = "Public floating IP address"
      value       = openstack_networking_floatingip_v2.vm.address
    }
    
    output "private_ip" {
      description = "Private IP address of the instance"
      value       = openstack_compute_instance_v2.vm.access_ip_v4
    }
    
    output "backup_bucket" {
      description = "Object Storage bucket name for scheduled backups"
      value       = aws_s3_bucket.backups.id
    }
    
    versions.tfHCL
    terraform {
      required_version = ">= 1.6.0"
    
      required_providers {
        openstack = {
          source  = "terraform-provider-openstack/openstack"
          version = "~> 2.0"
        }
        aws = {
          source  = "hashicorp/aws"
          version = "~> 5.0"
        }
      }
    }
    
    provider "openstack" {}
    
    provider "aws" {
      region                      = var.s3_region
      access_key                  = var.s3_access_key
      secret_key                  = var.s3_secret_key
      skip_credentials_validation = true
      skip_metadata_api_check     = true
      skip_requesting_account_id  = true
    
      endpoints {
        s3 = var.s3_endpoint
      }
    }
    
    terraform.tfvars.exampleHCL
    # Required: SSH keypair must already exist in your project
    key_name = "YOUR_KEY_NAME"
    
    # flavor_name = "s1a.small"
    # image_name = "Ubuntu-24.04"
    # instance_name = "simple-vm"
    # external_network = "PublicStatic"
    # private_cidr = "10.10.10.0/24"
    
    cloud-init/vm.yamlYAML
    #cloud-config
    package_update: true
    packages:
      - python3-pip
    write_files:
      - path: /usr/local/bin/vm-backup.sh
        permissions: "0755"
        content: |
          #!/bin/bash
          set -e
          export AWS_ACCESS_KEY_ID="${s3_access_key}"
          export AWS_SECRET_ACCESS_KEY="${s3_secret_key}"
          export AWS_DEFAULT_REGION="${s3_region}"
          ts=$(date +%Y%m%d-%H%M%S)
          tar -czf /tmp/vm-backup-$ts.tar.gz /etc /var/www /home 2>/dev/null || tar -czf /tmp/vm-backup-$ts.tar.gz /etc /home
          aws s3 cp /tmp/vm-backup-$ts.tar.gz s3://${backup_bucket}/ --endpoint-url ${s3_endpoint}
          rm -f /tmp/vm-backup-$ts.tar.gz
      - path: /etc/cron.d/vm-backup
        owner: root:root
        permissions: "0644"
        content: |
          ${backup_schedule} root /usr/local/bin/vm-backup.sh
    runcmd:
      - pip3 install --break-system-packages awscli
    
    README.mdMarkdown
    # Simple VM
    
    Single compute instance with floating IP and security group.
    
    
    **Network class:** production — `external_network` defaults to `PublicStatic` for persisted floating IPs and multi-tier stacks; override with `PublicEphemeral` for ephemeral demos.
    
    ## Prerequisites
    
    - OpenTofu >= 1.6.0 or Terraform >= 1.6.0
    - Quake AI account with OpenStack credentials
    - An existing SSH keypair in your project (the value of `key_name` must match that keypair)
    
    ## Usage
    
    1. Clone or copy this template directory
    2. Copy `terraform.tfvars.example` to `terraform.tfvars` and fill in your values
    3. Source your OpenStack credentials: `source openrc.sh`
    4. Initialize: `tofu init`
    5. Preview: `tofu plan`
    6. Apply: `tofu apply`
    
    ## Variables
    
    | Name | Type | Required | Default | Description |
    | --- | --- | --- | --- | --- |
    | `key_name` | string | yes | n/a | SSH keypair name (must already exist in your project) |
    | `flavor_name` | string | no | `s1a.small` | Instance size |
    | `image_name` | string | no | `Ubuntu-24.04` | Operating system image |
    | `instance_name` | string | no | `simple-vm` | Display name for the instance |
    | `external_network` | string | no | `PublicStatic` | Persisted FIP / production default; override with `PublicEphemeral` for demos |
    | `private_cidr` | string | no | `10.10.10.0/24` | CIDR for the private tenant network the VM lives in |
    
    ## Documentation
    
    Full documentation: [Simple VM template](/docs/automation/templates/simple-vm)
    
  • Monitoringsimple-vm-hardened+$40/mo over the base

    Fail2ban hardening plus Prometheus and Grafana visibility

    Show source and download
    9 files. Download the zip or copy any file.Download simple-vm-hardened.zip
    main.tfHCL
    data "openstack_images_image_v2" "os" {
      name        = var.image_name
      most_recent = true
    }
    
    data "openstack_networking_network_v2" "external" {
      name = var.external_network
    }
    
    resource "openstack_networking_network_v2" "private" {
      name           = "${var.instance_name}-net"
      admin_state_up = true
    }
    
    resource "openstack_networking_subnet_v2" "private" {
      name            = "${var.instance_name}-subnet"
      network_id      = openstack_networking_network_v2.private.id
      cidr            = var.private_cidr
      ip_version      = 4
      dns_nameservers = ["1.1.1.1", "8.8.8.8"]
    }
    
    resource "openstack_networking_router_v2" "main" {
      name                = "${var.instance_name}-router"
      external_network_id = data.openstack_networking_network_v2.external.id
    }
    
    resource "openstack_networking_router_interface_v2" "private" {
      router_id = openstack_networking_router_v2.main.id
      subnet_id = openstack_networking_subnet_v2.private.id
    }
    
    resource "openstack_networking_secgroup_v2" "vm" {
      name        = "${var.instance_name}-sg"
      description = "Allow SSH and HTTP"
    }
    
    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.vm.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.vm.id
    }
    
    resource "openstack_networking_secgroup_rule_v2" "node_exporter" {
      direction         = "ingress"
      ethertype         = "IPv4"
      protocol          = "tcp"
      port_range_min    = 9100
      port_range_max    = 9100
      remote_ip_prefix  = var.private_cidr
      security_group_id = openstack_networking_secgroup_v2.vm.id
    }
    
    resource "openstack_compute_instance_v2" "vm" {
      name        = var.instance_name
      flavor_name = var.flavor_name
      key_pair    = var.key_name
    
      user_data = file("${path.module}/cloud-init/vm.yaml")
    
      block_device {
        uuid                  = data.openstack_images_image_v2.os.id
        source_type           = "image"
        destination_type      = "volume"
        volume_size           = 20
        boot_index            = 0
        delete_on_termination = true
      }
    
      network {
        port = openstack_networking_port_v2.vm.id
      }
    }
    
    resource "openstack_networking_port_v2" "vm" {
      name               = "${var.instance_name}-port"
      network_id         = openstack_networking_network_v2.private.id
      security_group_ids = [openstack_networking_secgroup_v2.vm.id]
    
      fixed_ip {
        subnet_id = openstack_networking_subnet_v2.private.id
      }
    
      depends_on = [openstack_networking_router_interface_v2.private]
    }
    
    resource "openstack_networking_floatingip_v2" "vm" {
      pool = var.external_network
    }
    
    resource "openstack_networking_floatingip_associate_v2" "vm" {
      floating_ip = openstack_networking_floatingip_v2.vm.address
      port_id     = openstack_networking_port_v2.vm.id
    }
    
    locals {
      hardened_scrape_yaml = "        - job_name: \"simple-vm\"\n          static_configs:\n            - targets:\n                - ${yamlencode("${openstack_compute_instance_v2.vm.network[0].fixed_ip_v4}:9100")}"
    }
    
    resource "openstack_networking_secgroup_v2" "prometheus" {
      name        = "${var.instance_name}-prometheus-sg"
      description = "Prometheus 9090 from private subnet; SSH from anywhere"
    }
    
    resource "openstack_networking_secgroup_rule_v2" "prometheus_http" {
      direction         = "ingress"
      ethertype         = "IPv4"
      protocol          = "tcp"
      port_range_min    = 9090
      port_range_max    = 9090
      remote_ip_prefix  = var.private_cidr
      security_group_id = openstack_networking_secgroup_v2.prometheus.id
    }
    
    resource "openstack_networking_secgroup_rule_v2" "prometheus_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.prometheus.id
    }
    
    resource "openstack_networking_secgroup_v2" "grafana" {
      name        = "${var.instance_name}-grafana-sg"
      description = "Grafana 3000 from anywhere; SSH from anywhere"
    }
    
    resource "openstack_networking_secgroup_rule_v2" "grafana_http" {
      direction         = "ingress"
      ethertype         = "IPv4"
      protocol          = "tcp"
      port_range_min    = 3000
      port_range_max    = 3000
      remote_ip_prefix  = "0.0.0.0/0"
      security_group_id = openstack_networking_secgroup_v2.grafana.id
    }
    
    resource "openstack_networking_secgroup_rule_v2" "grafana_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.grafana.id
    }
    
    resource "openstack_networking_port_v2" "prometheus" {
      name               = "${var.instance_name}-prometheus-port"
      network_id         = openstack_networking_network_v2.private.id
      security_group_ids = [openstack_networking_secgroup_v2.prometheus.id]
    
      fixed_ip {
        subnet_id = openstack_networking_subnet_v2.private.id
      }
    
      depends_on = [openstack_networking_router_interface_v2.private]
    }
    
    resource "openstack_compute_instance_v2" "prometheus" {
      name        = "${var.instance_name}-prometheus"
      flavor_name = var.prometheus_flavor
      key_pair    = var.key_name
    
      user_data = templatefile("${path.module}/cloud-init/prometheus.yaml", {
        retention_days      = var.retention_days
        scrape_targets_yaml = local.hardened_scrape_yaml
      })
    
      block_device {
        uuid                  = data.openstack_images_image_v2.os.id
        source_type           = "image"
        destination_type      = "volume"
        volume_size           = 20
        boot_index            = 0
        delete_on_termination = true
      }
    
      network {
        port = openstack_networking_port_v2.prometheus.id
      }
    
      depends_on = [
        openstack_networking_router_interface_v2.private,
        openstack_compute_instance_v2.vm,
      ]
    }
    
    resource "openstack_networking_port_v2" "grafana" {
      name               = "${var.instance_name}-grafana-port"
      network_id         = openstack_networking_network_v2.private.id
      security_group_ids = [openstack_networking_secgroup_v2.grafana.id]
    
      fixed_ip {
        subnet_id = openstack_networking_subnet_v2.private.id
      }
    
      depends_on = [openstack_networking_router_interface_v2.private]
    }
    
    resource "openstack_compute_instance_v2" "grafana" {
      name        = "${var.instance_name}-grafana"
      flavor_name = var.grafana_flavor
      key_pair    = var.key_name
    
      user_data = templatefile("${path.module}/cloud-init/grafana.yaml", {
        prometheus_ip        = openstack_compute_instance_v2.prometheus.network[0].fixed_ip_v4
        grafana_admin_password = var.grafana_admin_password
      })
    
      block_device {
        uuid                  = data.openstack_images_image_v2.os.id
        source_type           = "image"
        destination_type      = "volume"
        volume_size           = 20
        boot_index            = 0
        delete_on_termination = true
      }
    
      network {
        port = openstack_networking_port_v2.grafana.id
      }
    
      depends_on = [openstack_compute_instance_v2.prometheus]
    }
    
    resource "openstack_networking_floatingip_v2" "grafana" {
      pool = var.external_network
    }
    
    resource "openstack_networking_floatingip_associate_v2" "grafana" {
      floating_ip = openstack_networking_floatingip_v2.grafana.address
      port_id     = openstack_networking_port_v2.grafana.id
    }
    
    variables.tfHCL
    variable "flavor_name" {
      description = "Instance size"
      type        = string
      default     = "s1a.small"
    }
    
    variable "image_name" {
      description = "Operating system image"
      type        = string
      default     = "Ubuntu-24.04"
    }
    
    variable "key_name" {
      description = "SSH keypair name (must already exist in your project)"
      type        = string
    }
    
    variable "instance_name" {
      description = "Display name for the instance"
      type        = string
      default     = "simple-vm"
    }
    
    variable "external_network" {
      description = "Shared external network for router gateway and floating IPs; defaults to PublicStatic (persisted FIP / production pattern). Override with PublicEphemeral for ephemeral demos."
      type        = string
      default     = "PublicStatic"
    }
    
    variable "private_cidr" {
      description = "CIDR for the private tenant network the VM lives in"
      type        = string
      default     = "10.10.10.0/24"
    }
    
    variable "prometheus_flavor" {
      description = "Flavor for the Prometheus monitoring instance"
      type        = string
      default     = "s1a.small"
    }
    
    variable "grafana_flavor" {
      description = "Flavor for the Grafana dashboard instance"
      type        = string
      default     = "s1a.small"
    }
    
    variable "retention_days" {
      description = "Prometheus TSDB retention in days"
      type        = number
      default     = 15
    }
    
    variable "grafana_admin_password" {
      description = "Initial Grafana admin password"
      type        = string
      sensitive   = true
    }
    
    outputs.tfHCL
    output "instance_id" {
      description = "ID of the compute instance"
      value       = openstack_compute_instance_v2.vm.id
    }
    
    output "floating_ip" {
      description = "Public floating IP address"
      value       = openstack_networking_floatingip_v2.vm.address
    }
    
    output "private_ip" {
      description = "Private IP address of the instance"
      value       = openstack_compute_instance_v2.vm.access_ip_v4
    }
    
    output "grafana_floating_ip" {
      description = "Public floating IP for Grafana"
      value       = openstack_networking_floatingip_v2.grafana.address
    }
    
    output "prometheus_private_ip" {
      description = "Private IPv4 of the Prometheus instance"
      value       = openstack_compute_instance_v2.prometheus.network[0].fixed_ip_v4
    }
    
    versions.tfHCL
    terraform {
      required_version = ">= 1.6.0"
    
      required_providers {
        openstack = {
          source  = "terraform-provider-openstack/openstack"
          version = "~> 2.0"
        }
      }
    }
    
    provider "openstack" {}
    
    terraform.tfvars.exampleHCL
    # Required: SSH keypair must already exist in your project
    key_name = "YOUR_KEY_NAME"
    
    # flavor_name = "s1a.small"
    # image_name = "Ubuntu-24.04"
    # instance_name = "simple-vm"
    # external_network = "PublicStatic"
    # private_cidr = "10.10.10.0/24"
    
    cloud-init/grafana.yamlYAML
    #cloud-config
    package_update: true
    packages:
      - apt-transport-https
      - ca-certificates
      - curl
      - gnupg
    
    write_files:
      - path: /etc/apt/sources.list.d/grafana.list
        content: deb [signed-by=/usr/share/keyrings/grafana.gpg] https://apt.grafana.com stable main
        owner: root:root
        permissions: "0644"
      - path: /etc/grafana/provisioning/datasources/prometheus.yaml
        content: |
          apiVersion: 1
          datasources:
            - name: Prometheus
              type: prometheus
              access: proxy
              url: http://${prometheus_ip}:9090
              isDefault: true
              editable: false
        owner: root:root
        permissions: "0644"
      - path: /etc/systemd/system/grafana-server.service.d/override.conf
        content: |
          [Service]
          Environment=GF_SECURITY_ADMIN_PASSWORD=${grafana_admin_password}
          Environment=GF_SERVER_HTTP_ADDR=0.0.0.0
        owner: root:root
        permissions: "0600"
    
    runcmd:
      - |
        set -e
        export DEBIAN_FRONTEND=noninteractive
        DEV=/dev/sdb
        for i in $(seq 1 12); do [ -b "$DEV" ] && break; sleep 5; done
        if [ -b "$DEV" ]; then
          if ! blkid "$DEV" >/dev/null 2>&1; then mkfs.ext4 -F -L grafdata "$DEV"; fi
          mkdir -p /var/lib/grafana
          mount "$DEV" /var/lib/grafana
          grep -q "$DEV" /etc/fstab || echo "$DEV /var/lib/grafana ext4 defaults,nofail 0 2" >> /etc/fstab
        fi
        PROM="${prometheus_ip}"
        for i in $(seq 1 60); do
          if curl -sf "http://$${PROM}:9090/-/ready" >/dev/null 2>&1; then break; fi
          sleep 5
        done
        install -d /usr/share/keyrings
        curl -fsSL https://apt.grafana.com/gpg.key | gpg --batch --yes --dearmor -o /usr/share/keyrings/grafana.gpg
        for i in 1 2 3; do apt-get update && break; sleep 10; done
        apt-get install -y grafana
        chown -R grafana:grafana /var/lib/grafana
        systemctl daemon-reload
        systemctl enable grafana-server
        systemctl restart grafana-server
    
    cloud-init/prometheus.yamlYAML
    #cloud-config
    package_update: true
    write_files:
      - path: /tmp/prometheus.default
        content: |
          ARGS="--web.enable-lifecycle --web.listen-address=0.0.0.0:9090 --storage.tsdb.retention.time=${retention_days}d"
        owner: root:root
        permissions: "0644"
      - path: /tmp/prometheus.yml
        content: |
          global:
            scrape_interval: 15s
          scrape_configs:
            - job_name: "prometheus"
              static_configs:
                - targets: ["localhost:9090"]
    ${scrape_targets_yaml}
        owner: root:root
        permissions: "0644"
    runcmd:
      - |
        set -e
        export DEBIAN_FRONTEND=noninteractive
        DEV=/dev/sdb
        for i in $(seq 1 12); do [ -b "$DEV" ] && break; sleep 5; done
        if [ -b "$DEV" ]; then
          if ! blkid "$DEV" >/dev/null 2>&1; then mkfs.ext4 -F -L promdata "$DEV"; fi
          mkdir -p /var/lib/prometheus
          mount "$DEV" /var/lib/prometheus
          grep -q "$DEV" /etc/fstab || echo "$DEV /var/lib/prometheus ext4 defaults,nofail 0 2" >> /etc/fstab
        fi
        for i in 1 2 3; do apt-get update && break; sleep 10; done
        apt-get install -y prometheus
        cp /tmp/prometheus.yml /etc/prometheus/prometheus.yml
        cp /tmp/prometheus.default /etc/default/prometheus
        chown -R prometheus:prometheus /var/lib/prometheus
        systemctl enable prometheus
        systemctl restart prometheus
    
    cloud-init/vm.yamlYAML
    #cloud-config
    package_update: true
    packages:
      - fail2ban
      - prometheus-node-exporter
    runcmd:
      - systemctl enable --now fail2ban prometheus-node-exporter
    
    README.mdMarkdown
    # Simple VM
    
    Single compute instance with floating IP and security group.
    
    
    **Network class:** production — `external_network` defaults to `PublicStatic` for persisted floating IPs and multi-tier stacks; override with `PublicEphemeral` for ephemeral demos.
    
    ## Prerequisites
    
    - OpenTofu >= 1.6.0 or Terraform >= 1.6.0
    - Quake AI account with OpenStack credentials
    - An existing SSH keypair in your project (the value of `key_name` must match that keypair)
    
    ## Usage
    
    1. Clone or copy this template directory
    2. Copy `terraform.tfvars.example` to `terraform.tfvars` and fill in your values
    3. Source your OpenStack credentials: `source openrc.sh`
    4. Initialize: `tofu init`
    5. Preview: `tofu plan`
    6. Apply: `tofu apply`
    
    ## Variables
    
    | Name | Type | Required | Default | Description |
    | --- | --- | --- | --- | --- |
    | `key_name` | string | yes | n/a | SSH keypair name (must already exist in your project) |
    | `flavor_name` | string | no | `s1a.small` | Instance size |
    | `image_name` | string | no | `Ubuntu-24.04` | Operating system image |
    | `instance_name` | string | no | `simple-vm` | Display name for the instance |
    | `external_network` | string | no | `PublicStatic` | Persisted FIP / production default; override with `PublicEphemeral` for demos |
    | `private_cidr` | string | no | `10.10.10.0/24` | CIDR for the private tenant network the VM lives in |
    
    ## Documentation
    
    Full documentation: [Simple VM template](/docs/automation/templates/simple-vm)
    
Resources, parameters, and variables
Provisions
Parameterized by
Variables
  • flavor_name="s1a.small"
  • image_name="Ubuntu-24.04"
  • key_namerequired
  • instance_name="simple-vm"
  • external_network="PublicEphemeral"
  • private_cidr="10.10.10.0/24"

Customize this pattern#

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

Was this page helpful?