Skip to content

Full-Stack Application

Template · Updated Jul 2026
Validated Jul 2026

Full-stack application

This pattern composes Compute, Network, and Block Storage.

What this template does#

Provisions a complete multi-tier application stack:

  • Web, application, and database servers as separate compute instances
  • Cloud-init bootstraps each tier: Nginx on the web server reverse-proxies to the app server on port 8080, the app server runs a Python service that queries the database, and the db server runs MariaDB
  • Private network with a router for inter-tier communication
  • A block storage volume mounted at /var/lib/mysql on the db tier for database persistence
  • Security groups isolating each tier (web exposed; app and db reachable only from the private network)
  • A floating IP on the web tier for public access

Based on the existing Full-Stack Terraform tutorial, packaged as a reusable, parameterized template.

Set db_password to a strong value when you apply the template. The template ships no default password.

Component overview#

InternetRouterFloating IPPrivate NetworkWeb ServerApp ServerDB ServerData Volume :8080:3306
Click to zoom
Three-tier architecture: web, application, and database servers on a private network with a floating IP and persistent storage.

Parameters#

ParameterDescriptionDefault
key_nameSSH keypair name (must already exist in your project)required
web_flavorWeb server instance sizes1a.small
app_flavorApp server instance sizes1a.medium
db_flavorDatabase instance sizem2a.large
db_volume_sizeDatabase volume in GB50
image_nameOperating system imageUbuntu-24.04
db_nameApplication database nameappdb
db_userApplication database userappuser
db_passwordPassword for the database user (required, no default)none
dns_nameserversResolvers for the private subnet["8.8.8.8", "8.8.4.4"]
external_networkExternal network for router gateway and floating IPPublicStatic
private_cidrAddress range for the private subnet192.168.50.0/24

When to use this pattern#

Provision web, application, and database instances on dedicated subnets with tier-scoped security groups. Choose Three-Tier Application for the same tier model with explicit router interfaces per subnet. Add an edge reverse proxy for a dedicated HTTPS entry point.

Estimated cost#

Monthly cost estimate

Pricing calculator ↗

Sized as a custom package on a mix of shared and dedicated vCPU.

Starting template$119.30/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

Web tier

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

$16.50/mo

App tier

s1a.medium · 4 shared vCPU, 4 GiB RAM, 0.5 Gbps

$33.00/mo

Database

m2a.large · 2 dedicated vCPU, 8 GiB RAM, 0.5 Gbps

$66.00/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

s1a.medium

4 shared vCPU, 4 GiB RAM, 0.5 Gbps

$33.00

m2a.large

2 dedicated vCPU, 8 GiB RAM, 0.5 Gbps

$66.00

Compute + RAM rate basis

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

—

Block storage (110 GiB)

110 GiB at $0.08/GiB/mo

$8.80

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

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.

$69.80/mo

Production on the configured CPU

The headline estimate above; predictable steady-load performance.

$119.30/mo

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.

Template source#

9 files. Download the zip or expand to copy any file.Download full-stack-app.zip
Show source (9 files)
main.tfHCL
data "openstack_images_image_v2" "image" {
  name        = var.image_name
  most_recent = true
}

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

resource "openstack_networking_network_v2" "private" {
  name           = "full-stack-app-private"
  admin_state_up = true
}

resource "openstack_networking_subnet_v2" "private" {
  name            = "full-stack-app-private-subnet"
  network_id      = openstack_networking_network_v2.private.id
  cidr            = var.private_cidr
  ip_version      = 4
  enable_dhcp     = true
  dns_nameservers = var.dns_nameservers
}

resource "openstack_networking_router_v2" "router" {
  name                = "full-stack-app-router"
  admin_state_up      = true
  external_network_id = data.openstack_networking_network_v2.external.id
}

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

resource "openstack_networking_secgroup_v2" "web" {
  name        = "full-stack-app-web"
  description = "Web: SSH, HTTP, HTTPS from any IPv4"
}

resource "openstack_networking_secgroup_rule_v2" "web_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" "web_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" "web_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
}

resource "openstack_networking_secgroup_v2" "app" {
  name        = "full-stack-app-app"
  description = "App: 8080 and SSH from private network only"
}

resource "openstack_networking_secgroup_rule_v2" "app_http_alt" {
  direction         = "ingress"
  ethertype         = "IPv4"
  protocol          = "tcp"
  port_range_min    = 8080
  port_range_max    = 8080
  remote_ip_prefix  = var.private_cidr
  security_group_id = openstack_networking_secgroup_v2.app.id
}

resource "openstack_networking_secgroup_rule_v2" "app_ssh" {
  direction         = "ingress"
  ethertype         = "IPv4"
  protocol          = "tcp"
  port_range_min    = 22
  port_range_max    = 22
  remote_ip_prefix  = var.private_cidr
  security_group_id = openstack_networking_secgroup_v2.app.id
}

resource "openstack_networking_secgroup_v2" "db" {
  name        = "full-stack-app-db"
  description = "DB: MySQL and SSH from private network only"
}

resource "openstack_networking_secgroup_rule_v2" "db_mysql" {
  direction         = "ingress"
  ethertype         = "IPv4"
  protocol          = "tcp"
  port_range_min    = 3306
  port_range_max    = 3306
  remote_ip_prefix  = var.private_cidr
  security_group_id = openstack_networking_secgroup_v2.db.id
}

resource "openstack_networking_secgroup_rule_v2" "db_ssh" {
  direction         = "ingress"
  ethertype         = "IPv4"
  protocol          = "tcp"
  port_range_min    = 22
  port_range_max    = 22
  remote_ip_prefix  = var.private_cidr
  security_group_id = openstack_networking_secgroup_v2.db.id
}


resource "openstack_compute_instance_v2" "web" {
  name        = "full-stack-app-web"
  flavor_name = var.web_flavor
  key_pair    = var.key_name

  user_data = templatefile("${path.module}/cloud-init/web.yaml", {
    app_ip = openstack_compute_instance_v2.app.network[0].fixed_ip_v4
  })

  block_device {
    uuid                  = data.openstack_images_image_v2.image.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
  }

  depends_on = [openstack_networking_router_interface_v2.private]
}

resource "openstack_networking_port_v2" "app" {
  name               = "full-stack-app-app-port"
  network_id         = openstack_networking_network_v2.private.id
  security_group_ids = [openstack_networking_secgroup_v2.app.id]

  fixed_ip {
    subnet_id = openstack_networking_subnet_v2.private.id
  }

  depends_on = [openstack_networking_router_interface_v2.private]
}

resource "openstack_compute_instance_v2" "app" {
  name        = "full-stack-app-app"
  flavor_name = var.app_flavor
  key_pair    = var.key_name

  user_data = templatefile("${path.module}/cloud-init/app.yaml", {
    db_ip       = openstack_compute_instance_v2.db.network[0].fixed_ip_v4
    db_name     = var.db_name
    db_user     = var.db_user
    db_password = var.db_password
  })

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

  network {
    port = openstack_networking_port_v2.app.id
  }

  depends_on = [openstack_networking_router_interface_v2.private]
}

resource "openstack_networking_port_v2" "web" {
  name               = "full-stack-app-web-port"
  network_id         = openstack_networking_network_v2.private.id
  security_group_ids = [openstack_networking_secgroup_v2.web.id]

  fixed_ip {
    subnet_id = openstack_networking_subnet_v2.private.id
  }

  depends_on = [openstack_networking_router_interface_v2.private]
}

resource "openstack_networking_port_v2" "db" {
  name               = "full-stack-app-db-port"
  network_id         = openstack_networking_network_v2.private.id
  security_group_ids = [openstack_networking_secgroup_v2.db.id]

  fixed_ip {
    subnet_id = openstack_networking_subnet_v2.private.id
  }

  depends_on = [openstack_networking_router_interface_v2.private]
}

resource "openstack_compute_instance_v2" "db" {
  name        = "full-stack-app-db"
  flavor_name = var.db_flavor
  key_pair    = var.key_name

  user_data = templatefile("${path.module}/cloud-init/db.yaml", {
    db_name     = var.db_name
    db_user     = var.db_user
    db_password = var.db_password
  })

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

  network {
    port = openstack_networking_port_v2.db.id
  }

  depends_on = [openstack_networking_router_interface_v2.private]
}

resource "openstack_blockstorage_volume_v3" "db_data" {
  name = "full-stack-app-db-data"
  size = var.db_volume_size
}

resource "openstack_compute_volume_attach_v2" "db_data" {
  instance_id = openstack_compute_instance_v2.db.id
  volume_id   = openstack_blockstorage_volume_v3.db_data.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
}
variables.tfHCL
variable "web_flavor" {
  type    = string
  default = "s1a.small"
}

variable "app_flavor" {
  type    = string
  default = "s1a.medium"
}

variable "db_flavor" {
  type    = string
  default = "m2a.large"
}

variable "db_volume_size" {
  type    = number
  default = 50
}

variable "image_name" {
  type    = string
  default = "Ubuntu-24.04"
}

variable "key_name" {
  description = "Existing SSH keypair name in the project for compute instances"
  type        = string
}

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" {
  type    = string
  default = "192.168.50.0/24"
}

variable "dns_nameservers" {
  description = "Resolvers for the private subnet so instances can reach package mirrors during cloud-init"
  type        = list(string)
  default     = ["8.8.8.8", "8.8.4.4"]
}

variable "db_name" {
  description = "Application database name created on the db tier"
  type        = string
  default     = "appdb"
}

variable "db_user" {
  description = "Application database user the app tier connects as"
  type        = string
  default     = "appuser"
}

variable "db_password" {
  description = "Password for the application database user. Set this to a strong value; no default is shipped."
  type        = string
  sensitive   = true
}
outputs.tfHCL
output "web_floating_ip" {
  description = "Floating IPv4 address of the web instance"
  value       = openstack_networking_floatingip_v2.web.address
}

output "app_private_ip" {
  description = "Fixed IPv4 address of the app instance on the private network"
  value       = openstack_compute_instance_v2.app.network[0].fixed_ip_v4
}

output "db_private_ip" {
  description = "Fixed IPv4 address of the db instance on the private network"
  value       = openstack_compute_instance_v2.db.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
key_name = "YOUR_KEY_NAME"

# web_flavor = "s1a.small"
# app_flavor = "s1a.medium"
# db_flavor = "m2a.large"
# db_volume_size = 50
# image_name = "Ubuntu-24.04"
# external_network = "PublicStatic"
# private_cidr = "192.168.50.0/24"
cloud-init/app.yamlYAML
#cloud-config
package_update: true
packages:
  - python3-pymysql
write_files:
  - path: /opt/app/app.py
    permissions: "0755"
    content: |
      import http.server
      import socketserver
      import pymysql

      DB = dict(
          host="${db_ip}",
          user="${db_user}",
          password="${db_password}",
          database="${db_name}",
          connect_timeout=3,
      )


      class Handler(http.server.BaseHTTPRequestHandler):
          def do_GET(self):
              try:
                  conn = pymysql.connect(**DB)
                  cur = conn.cursor()
                  cur.execute("SELECT note FROM status WHERE id = 1")
                  note = cur.fetchone()[0]
                  conn.close()
                  body = "app tier OK; db says: {}".format(note)
                  self.send_response(200)
              except Exception as exc:
                  body = "app tier up; db not ready yet: {}".format(exc)
                  self.send_response(503)
              self.send_header("Content-Type", "text/plain")
              self.end_headers()
              self.wfile.write(body.encode())

          def log_message(self, *args):
              pass


      socketserver.TCPServer.allow_reuse_address = True
      with socketserver.TCPServer(("0.0.0.0", 8080), Handler) as httpd:
          httpd.serve_forever()
  - path: /etc/systemd/system/app.service
    permissions: "0644"
    content: |
      [Unit]
      Description=full-stack-app application tier
      After=network-online.target
      Wants=network-online.target

      [Service]
      ExecStart=/usr/bin/python3 /opt/app/app.py
      Restart=always
      RestartSec=3

      [Install]
      WantedBy=multi-user.target
runcmd:
  - systemctl daemon-reload
  - systemctl enable --now app
cloud-init/db.yamlYAML
#cloud-config
package_update: true
runcmd:
  - |
    set -e
    export DEBIAN_FRONTEND=noninteractive
    DEV=/dev/sdb
    # The data volume attaches as /dev/sdb on this platform (not /dev/vdb).
    for i in $(seq 1 30); do [ -b "$DEV" ] && break; sleep 5; done
    if ! blkid "$DEV" >/dev/null 2>&1; then mkfs.ext4 -F "$DEV"; fi
    mkdir -p /var/lib/mysql
    mount "$DEV" /var/lib/mysql
    grep -q "$DEV" /etc/fstab || echo "$DEV /var/lib/mysql ext4 defaults,nofail 0 2" >> /etc/fstab
    apt-get update
    apt-get install -y mariadb-server
    printf '[mysqld]\nbind-address = 0.0.0.0\n' > /etc/mysql/mariadb.conf.d/99-bind.cnf
    systemctl enable mariadb
    systemctl restart mariadb
    mysql -e "CREATE DATABASE IF NOT EXISTS ${db_name};"
    mysql -e "CREATE USER IF NOT EXISTS '${db_user}'@'%' IDENTIFIED BY '${db_password}';"
    mysql -e "GRANT ALL PRIVILEGES ON ${db_name}.* TO '${db_user}'@'%'; FLUSH PRIVILEGES;"
    mysql ${db_name} -e "CREATE TABLE IF NOT EXISTS status (id INT PRIMARY KEY, note VARCHAR(255)); INSERT IGNORE INTO status VALUES (1, 'full-stack-app database tier online');"
cloud-init/web.yamlYAML
#cloud-config
package_update: true
packages:
  - nginx
write_files:
  - path: /etc/nginx/sites-available/default
    permissions: "0644"
    content: |
      server {
          listen 80 default_server;
          server_name _;

          location / {
              proxy_pass http://${app_ip}:8080;
              proxy_set_header Host $host;
              proxy_set_header X-Real-IP $remote_addr;
          }
      }
runcmd:
  - systemctl enable --now nginx
  - systemctl restart nginx
README.mdMarkdown
# Full Stack App

Web, application, and database tiers on a private network behind a router, with security groups isolating traffic between layers.


**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
- Quota for multiple instances and at least one block volume for the database tier

## 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 | Existing SSH keypair name in the project for compute instances |
| `web_flavor` | string | no | `s1a.small` | Flavor for web tier instances |
| `app_flavor` | string | no | `s1a.medium` | Flavor for application tier instances |
| `db_flavor` | string | no | `m2a.large` | Flavor for database tier instances |
| `db_volume_size` | number | no | `50` | Database data volume size in GB |
| `image_name` | string | no | `Ubuntu-24.04` | Boot image name |
| `external_network` | string | no | `PublicStatic` | Persisted FIP / production default; override with `PublicEphemeral` for demos |
| `private_cidr` | string | no | `192.168.50.0/24` | Private network CIDR for the stack |

## Documentation

Full documentation: [Full stack app template](/docs/automation/templates/full-stack-app)

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

  • Vector searchfull-stack-app-vector

    PostgreSQL with pgvector on the database tier for embeddings

    Show source and download
    10 files. Download the zip or copy any file.Download full-stack-app-vector.zip
    main.tfHCL
    data "openstack_images_image_v2" "image" {
      name        = var.image_name
      most_recent = true
    }
    
    data "openstack_networking_network_v2" "external" {
      name = var.external_network
    }
    
    resource "openstack_networking_network_v2" "private" {
      name           = "full-stack-app-private"
      admin_state_up = true
    }
    
    resource "openstack_networking_subnet_v2" "private" {
      name            = "full-stack-app-private-subnet"
      network_id      = openstack_networking_network_v2.private.id
      cidr            = var.private_cidr
      ip_version      = 4
      enable_dhcp     = true
      dns_nameservers = var.dns_nameservers
    }
    
    resource "openstack_networking_router_v2" "router" {
      name                = "full-stack-app-router"
      admin_state_up      = true
      external_network_id = data.openstack_networking_network_v2.external.id
    }
    
    resource "openstack_networking_router_interface_v2" "private" {
      router_id = openstack_networking_router_v2.router.id
      subnet_id = openstack_networking_subnet_v2.private.id
    }
    
    resource "openstack_networking_secgroup_v2" "web" {
      name        = "full-stack-app-web"
      description = "Web: SSH, HTTP, HTTPS from any IPv4"
    }
    
    resource "openstack_networking_secgroup_rule_v2" "web_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" "web_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" "web_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
    }
    
    resource "openstack_networking_secgroup_v2" "app" {
      name        = "full-stack-app-app"
      description = "App: 8080 and SSH from private network only"
    }
    
    resource "openstack_networking_secgroup_rule_v2" "app_http_alt" {
      direction         = "ingress"
      ethertype         = "IPv4"
      protocol          = "tcp"
      port_range_min    = 8080
      port_range_max    = 8080
      remote_ip_prefix  = var.private_cidr
      security_group_id = openstack_networking_secgroup_v2.app.id
    }
    
    resource "openstack_networking_secgroup_rule_v2" "app_ssh" {
      direction         = "ingress"
      ethertype         = "IPv4"
      protocol          = "tcp"
      port_range_min    = 22
      port_range_max    = 22
      remote_ip_prefix  = var.private_cidr
      security_group_id = openstack_networking_secgroup_v2.app.id
    }
    
    resource "openstack_networking_secgroup_v2" "db" {
      name        = "full-stack-app-db"
      description = "DB: MySQL and SSH from private network only"
    }
    
    resource "openstack_networking_secgroup_rule_v2" "db_postgres" {
      direction         = "ingress"
      ethertype         = "IPv4"
      protocol          = "tcp"
      port_range_min    = 5432
      port_range_max    = 5432
      remote_ip_prefix  = var.private_cidr
      security_group_id = openstack_networking_secgroup_v2.db.id
    }
    
    resource "openstack_networking_secgroup_rule_v2" "db_ssh" {
      direction         = "ingress"
      ethertype         = "IPv4"
      protocol          = "tcp"
      port_range_min    = 22
      port_range_max    = 22
      remote_ip_prefix  = var.private_cidr
      security_group_id = openstack_networking_secgroup_v2.db.id
    }
    
    
    resource "openstack_compute_instance_v2" "web" {
      name        = "full-stack-app-web"
      flavor_name = var.web_flavor
      key_pair    = var.key_name
    
      user_data = templatefile("${path.module}/cloud-init/web.yaml", {
        app_ip = openstack_compute_instance_v2.app.network[0].fixed_ip_v4
      })
    
      block_device {
        uuid                  = data.openstack_images_image_v2.image.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
      }
    
      depends_on = [openstack_networking_router_interface_v2.private]
    }
    
    resource "openstack_networking_port_v2" "app" {
      name               = "full-stack-app-app-port"
      network_id         = openstack_networking_network_v2.private.id
      security_group_ids = [openstack_networking_secgroup_v2.app.id]
    
      fixed_ip {
        subnet_id = openstack_networking_subnet_v2.private.id
      }
    
      depends_on = [openstack_networking_router_interface_v2.private]
    }
    
    resource "openstack_compute_instance_v2" "app" {
      name        = "full-stack-app-app"
      flavor_name = var.app_flavor
      key_pair    = var.key_name
    
      user_data = templatefile("${path.module}/cloud-init/app.yaml", {
        db_ip       = openstack_compute_instance_v2.db.network[0].fixed_ip_v4
        db_name     = var.db_name
        db_user     = var.db_user
        db_password = var.db_password
      })
    
      block_device {
        uuid                  = data.openstack_images_image_v2.image.id
        source_type           = "image"
        destination_type      = "volume"
        volume_size           = 20
        boot_index            = 0
        delete_on_termination = true
      }
    
      network {
        port = openstack_networking_port_v2.app.id
      }
    
      depends_on = [openstack_networking_router_interface_v2.private]
    }
    
    resource "openstack_networking_port_v2" "web" {
      name               = "full-stack-app-web-port"
      network_id         = openstack_networking_network_v2.private.id
      security_group_ids = [openstack_networking_secgroup_v2.web.id]
    
      fixed_ip {
        subnet_id = openstack_networking_subnet_v2.private.id
      }
    
      depends_on = [openstack_networking_router_interface_v2.private]
    }
    
    resource "openstack_networking_port_v2" "db" {
      name               = "full-stack-app-db-port"
      network_id         = openstack_networking_network_v2.private.id
      security_group_ids = [openstack_networking_secgroup_v2.db.id]
    
      fixed_ip {
        subnet_id = openstack_networking_subnet_v2.private.id
      }
    
      depends_on = [openstack_networking_router_interface_v2.private]
    }
    
    resource "openstack_compute_instance_v2" "db" {
      name        = "full-stack-app-db"
      flavor_name = var.db_flavor
      key_pair    = var.key_name
    
      user_data = templatefile("${path.module}/cloud-init/postgres.yaml", {
        pg_version      = var.pg_version
        backup_schedule = var.backup_schedule
        private_cidr    = var.private_cidr
        db_name         = var.db_name
        db_user         = var.db_user
        db_password     = var.db_password
      })
    
      block_device {
        uuid                  = data.openstack_images_image_v2.image.id
        source_type           = "image"
        destination_type      = "volume"
        volume_size           = 20
        boot_index            = 0
        delete_on_termination = true
      }
    
      network {
        port = openstack_networking_port_v2.db.id
      }
    
      depends_on = [openstack_networking_router_interface_v2.private]
    }
    
    resource "openstack_blockstorage_volume_v3" "db_data" {
      name = "full-stack-app-db-data"
      size = var.db_volume_size
    }
    
    resource "openstack_compute_volume_attach_v2" "db_data" {
      instance_id = openstack_compute_instance_v2.db.id
      volume_id   = openstack_blockstorage_volume_v3.db_data.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
    }
    
    variables.tfHCL
    variable "web_flavor" {
      type    = string
      default = "s1a.small"
    }
    
    variable "app_flavor" {
      type    = string
      default = "s1a.medium"
    }
    
    variable "db_flavor" {
      type    = string
      default = "m2a.large"
    }
    
    variable "db_volume_size" {
      type    = number
      default = 50
    }
    
    variable "image_name" {
      type    = string
      default = "Ubuntu-24.04"
    }
    
    variable "key_name" {
      description = "Existing SSH keypair name in the project for compute instances"
      type        = string
    }
    
    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" {
      type    = string
      default = "192.168.50.0/24"
    }
    
    variable "dns_nameservers" {
      description = "Resolvers for the private subnet so instances can reach package mirrors during cloud-init"
      type        = list(string)
      default     = ["8.8.8.8", "8.8.4.4"]
    }
    
    variable "db_name" {
      description = "Application database name created on the db tier"
      type        = string
      default     = "appdb"
    }
    
    variable "db_user" {
      description = "Application database user the app tier connects as"
      type        = string
      default     = "appuser"
    }
    
    variable "db_password" {
      description = "Password for the application database user. Set this to a strong value; no default is shipped."
      type        = string
      sensitive   = true
    }
    
    variable "pg_version" {
      description = "PostgreSQL major version installed on the db tier with pgvector"
      type        = string
      default     = "16"
    }
    
    variable "backup_schedule" {
      description = "Cron schedule for PostgreSQL backups on the db tier"
      type        = string
      default     = "0 2 * * *"
    }
    
    outputs.tfHCL
    output "web_floating_ip" {
      description = "Floating IPv4 address of the web instance"
      value       = openstack_networking_floatingip_v2.web.address
    }
    
    output "app_private_ip" {
      description = "Fixed IPv4 address of the app instance on the private network"
      value       = openstack_compute_instance_v2.app.network[0].fixed_ip_v4
    }
    
    output "db_private_ip" {
      description = "Fixed IPv4 address of the db instance on the private network"
      value       = openstack_compute_instance_v2.db.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
    key_name = "YOUR_KEY_NAME"
    
    # web_flavor = "s1a.small"
    # app_flavor = "s1a.medium"
    # db_flavor = "m2a.large"
    # db_volume_size = 50
    # image_name = "Ubuntu-24.04"
    # external_network = "PublicStatic"
    # private_cidr = "192.168.50.0/24"
    
    cloud-init/app.yamlYAML
    #cloud-config
    package_update: true
    packages:
      - python3-psycopg2
    write_files:
      - path: /opt/app/app.py
        permissions: "0755"
        content: |
          import http.server
          import socketserver
          import psycopg2
    
          DB = dict(
              host="${db_ip}",
              user="${db_user}",
              password="${db_password}",
              dbname="${db_name}",
              connect_timeout=3,
          )
    
    
          class Handler(http.server.BaseHTTPRequestHandler):
              def do_GET(self):
                  try:
                      conn = psycopg2.connect(**DB)
                      cur = conn.cursor()
                      cur.execute("SELECT extname FROM pg_extension WHERE extname = 'vector'")
                      ext = cur.fetchone()
                      conn.close()
                      body = "app tier OK; pgvector extension: {}".format(ext[0] if ext else "pending")
                      self.send_response(200)
                  except Exception as exc:
                      body = "app tier up; db not ready yet: {}".format(exc)
                      self.send_response(503)
                  self.send_header("Content-Type", "text/plain")
                  self.end_headers()
                  self.wfile.write(body.encode())
    
              def log_message(self, *args):
                  pass
    
    
          socketserver.TCPServer.allow_reuse_address = True
          with socketserver.TCPServer(("0.0.0.0", 8080), Handler) as httpd:
              httpd.serve_forever()
      - path: /etc/systemd/system/app.service
        permissions: "0644"
        content: |
          [Unit]
          Description=full-stack-app-vector application tier
          After=network-online.target
          Wants=network-online.target
    
          [Service]
          ExecStart=/usr/bin/python3 /opt/app/app.py
          Restart=always
          RestartSec=3
    
          [Install]
          WantedBy=multi-user.target
    runcmd:
      - systemctl daemon-reload
      - systemctl enable --now app
    
    cloud-init/db.yamlYAML
    #cloud-config
    package_update: true
    runcmd:
      - |
        set -e
        export DEBIAN_FRONTEND=noninteractive
        DEV=/dev/sdb
        # The data volume attaches as /dev/sdb on this platform (not /dev/vdb).
        for i in $(seq 1 30); do [ -b "$DEV" ] && break; sleep 5; done
        if ! blkid "$DEV" >/dev/null 2>&1; then mkfs.ext4 -F "$DEV"; fi
        mkdir -p /var/lib/mysql
        mount "$DEV" /var/lib/mysql
        grep -q "$DEV" /etc/fstab || echo "$DEV /var/lib/mysql ext4 defaults,nofail 0 2" >> /etc/fstab
        apt-get update
        apt-get install -y mariadb-server
        printf '[mysqld]\nbind-address = 0.0.0.0\n' > /etc/mysql/mariadb.conf.d/99-bind.cnf
        systemctl enable mariadb
        systemctl restart mariadb
        mysql -e "CREATE DATABASE IF NOT EXISTS ${db_name};"
        mysql -e "CREATE USER IF NOT EXISTS '${db_user}'@'%' IDENTIFIED BY '${db_password}';"
        mysql -e "GRANT ALL PRIVILEGES ON ${db_name}.* TO '${db_user}'@'%'; FLUSH PRIVILEGES;"
        mysql ${db_name} -e "CREATE TABLE IF NOT EXISTS status (id INT PRIMARY KEY, note VARCHAR(255)); INSERT IGNORE INTO status VALUES (1, 'full-stack-app database tier online');"
    
    cloud-init/postgres.yamlYAML
    #cloud-config
    package_update: true
    write_files:
      - path: /tmp/zz-template.conf
        content: |
          listen_addresses = '*'
          archive_mode = on
          archive_command = 'test ! -f /var/lib/postgresql/wal_archive/%f && cp %p /var/lib/postgresql/wal_archive/%f'
        owner: root:root
        permissions: "0644"
      - path: /usr/local/bin/pg-backup.sh
        permissions: "0755"
        content: |
          #!/bin/bash
          set -e
          install -d -o postgres -g postgres /var/lib/postgresql/backups
          ts=$(date +%Y%m%d-%H%M%S)
          sudo -u postgres pg_dumpall | gzip > /var/lib/postgresql/backups/pgdumpall-$ts.sql.gz
      - path: /etc/cron.d/pg-backup
        owner: root:root
        permissions: "0644"
        content: |
          ${backup_schedule} root /usr/local/bin/pg-backup.sh
    runcmd:
      - |
        set -e
        export DEBIAN_FRONTEND=noninteractive
        DEV=/dev/sdb
        for i in $(seq 1 30); do [ -b "$DEV" ] && break; sleep 5; done
        if ! blkid "$DEV" >/dev/null 2>&1; then mkfs.ext4 -F -L pgdata "$DEV"; fi
        mkdir -p /var/lib/postgresql
        mount "$DEV" /var/lib/postgresql
        grep -q "$DEV" /etc/fstab || echo "$DEV /var/lib/postgresql ext4 defaults,nofail 0 2" >> /etc/fstab
        for i in 1 2 3; do apt-get update && break; sleep 10; done
        apt-get install -y postgresql-${pg_version} postgresql-${pg_version}-pgvector
        install -d -o postgres -g postgres /var/lib/postgresql/wal_archive /var/lib/postgresql/backups
        cp /tmp/zz-template.conf /etc/postgresql/${pg_version}/main/conf.d/zz-template.conf
        echo "host all all ${private_cidr} scram-sha-256" >> /etc/postgresql/${pg_version}/main/pg_hba.conf
        systemctl enable postgresql
        systemctl restart postgresql
        sudo -u postgres psql -v ON_ERROR_STOP=0 -c "CREATE DATABASE ${db_name};"
        sudo -u postgres psql -v ON_ERROR_STOP=0 -c "CREATE USER ${db_user} WITH PASSWORD '${db_password}';"
        sudo -u postgres psql -v ON_ERROR_STOP=0 -c "GRANT ALL PRIVILEGES ON DATABASE ${db_name} TO ${db_user};"
        sudo -u postgres psql -d ${db_name} -v ON_ERROR_STOP=1 -c "CREATE EXTENSION IF NOT EXISTS vector;"
    
    cloud-init/web.yamlYAML
    #cloud-config
    package_update: true
    packages:
      - nginx
    write_files:
      - path: /etc/nginx/sites-available/default
        permissions: "0644"
        content: |
          server {
              listen 80 default_server;
              server_name _;
    
              location / {
                  proxy_pass http://${app_ip}:8080;
                  proxy_set_header Host $host;
                  proxy_set_header X-Real-IP $remote_addr;
              }
          }
    runcmd:
      - systemctl enable --now nginx
      - systemctl restart nginx
    
    README.mdMarkdown
    # Full Stack App
    
    Web, application, and database tiers on a private network behind a router, with security groups isolating traffic between layers.
    
    
    **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
    - Quota for multiple instances and at least one block volume for the database tier
    
    ## 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 | Existing SSH keypair name in the project for compute instances |
    | `web_flavor` | string | no | `s1a.small` | Flavor for web tier instances |
    | `app_flavor` | string | no | `s1a.medium` | Flavor for application tier instances |
    | `db_flavor` | string | no | `m2a.large` | Flavor for database tier instances |
    | `db_volume_size` | number | no | `50` | Database data volume size in GB |
    | `image_name` | string | no | `Ubuntu-24.04` | Boot image name |
    | `external_network` | string | no | `PublicStatic` | Persisted FIP / production default; override with `PublicEphemeral` for demos |
    | `private_cidr` | string | no | `192.168.50.0/24` | Private network CIDR for the stack |
    
    ## Documentation
    
    Full documentation: [Full stack app template](/docs/automation/templates/full-stack-app)
    
Resources, parameters, and variables
Provisions
Parameterized by
Variables
  • web_flavor="s1a.small"
  • app_flavor="s1a.medium"
  • db_flavor="m2a.large"
  • db_volume_size=50
  • image_name="Ubuntu-24.04"
  • key_namerequired
  • external_network="PublicStatic"
  • private_cidr="192.168.50.0/24"
  • dns_nameservers=["8.8.8.8", "8.8.4.4"]
  • db_name="appdb"
  • db_user="appuser"
  • db_passwordrequired

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

Was this page helpful?