Full-Stack Application
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/mysqlon 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#
Parameters#
| Parameter | Description | Default |
|---|---|---|
key_name | SSH keypair name (must already exist in your project) | required |
web_flavor | Web server instance size | s1a.small |
app_flavor | App server instance size | s1a.medium |
db_flavor | Database instance size | m2a.large |
db_volume_size | Database volume in GB | 50 |
image_name | Operating system image | Ubuntu-24.04 |
db_name | Application database name | appdb |
db_user | Application database user | appuser |
db_password | Password for the database user (required, no default) | none |
dns_nameservers | Resolvers for the private subnet | ["8.8.8.8", "8.8.4.4"] |
external_network | External network for router gateway and floating IP | PublicStatic |
private_cidr | Address range for the private subnet | 192.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.
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
App tier
s1a.medium · 4 shared vCPU, 4 GiB RAM, 0.5 Gbps
Database
m2a.large · 2 dedicated vCPU, 8 GiB RAM, 0.5 Gbps
Compute shown per role at custom-package rates ($29/dedicated vCPU, $7.25/shared vCPU, $1/GiB RAM). The headline above is the billed total: the cheaper of a named plan and the custom package, plus add-ons.
Included in baseline
s1a.small
2 shared vCPU, 2 GiB RAM, 0.5 Gbps
s1a.medium
4 shared vCPU, 4 GiB RAM, 0.5 Gbps
m2a.large
2 dedicated vCPU, 8 GiB RAM, 0.5 Gbps
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
Public IP (included)
1 included with the custom package
Package promotional discount
Flat −$5.00/mo on the custom package (same promotion as named plans).
Included at no charge
These line items are zero on Quake AI. Many other providers meter them separately.
Data transfer (inbound and outbound)
Unlimited data transfer on every plan; Quake AI does not meter per-GB egress.
AWS, GCP, and Azure meter outbound transfer per GB. DigitalOcean and Hetzner include an allowance on compute plans, then charge overage.
Learn morePrivate networking
Private networks, subnets, Neutron routers, and security groups are included with the plan.
VPC objects are usually free to create elsewhere, but NAT gateways bill hourly plus per-GB processed. Quake AI uses router SNAT with no separate NAT line item.
Control-plane API requests
OpenStack API calls for provisioning and management are included.
Some managed services on other clouds meter API calls or charge for premium control-plane features.
Dev/test vs production
Start on shared CPU for dev/test, then promote to dedicated for production with a flavor resize. The network, storage, and template stay the same.
Dev/test on shared CPU
Burstable s1a flavors; suited to prototyping and low or bursty load.
Production on the configured CPU
The headline estimate above; predictable steady-load performance.
Saves $49.50/mo while you build on shared CPU.
Shared flavors carry less RAM (m2a.large (8 GiB RAM) -> s1a.small (2 GiB RAM)). A resize reboots the instance; data on attached volumes persists. Size the dedicated flavor for the RAM your production workload needs.
Pricing data last validated: . For current rates, check quake.ai/pricing.
Template source#
Show source (9 files)Hide source
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
}
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
}
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
}
terraform {
required_version = ">= 1.6.0"
required_providers {
openstack = {
source = "terraform-provider-openstack/openstack"
version = "~> 2.0"
}
}
}
provider "openstack" {}
# 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-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-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-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
# 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)
Variations
Deploy walkthroughValidated variants of this template, each adding one capability the base does not include.
- Vector search
full-stack-app-vectorPostgreSQL with pgvector on the database tier for embeddings
Show source and downloadHide source
10 files. Download the zip or copy any file.Download full-stack-app-vector.zipmain.tfHCLdata "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.tfHCLvariable "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.tfHCLoutput "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.tfHCLterraform { 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 appcloud-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 nginxREADME.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
web_flavor="s1a.small"app_flavor="s1a.medium"db_flavor="m2a.large"db_volume_size=50image_name="Ubuntu-24.04"key_namerequiredexternal_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#
- Customize a template's image and flavor
- Add a block volume to a template
- Parameterize a template with a tfvars file
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