Skip to content

Three-Tier Application

Template · Updated Jul 2026
Validated Jul 2026

Three-tier application

This pattern composes Compute, Network, and Block Storage.

What this template does#

Provisions a classic three-tier architecture with strict network isolation:

  • Web tier: web instances on the web subnet; the first web instance carries a floating IP for public entry. The template puts no load balancer in front of the tier, so additional web instances are reachable only from inside the network.
  • Application tier: business logic instances on a private subnet
  • Database tier: data instances on an isolated subnet with block storage

The template provisions the infrastructure and tier isolation; it installs no application, runtime, or database software. Bring your own configuration (for example with cloud-init or a configuration-management tool) for each tier.

Each tier lives on its own subnet with security groups enforcing that traffic flows only web → app → database. No direct public access to application or database tiers.

Parameters#

ParameterDescriptionDefault
key_nameSSH keypair name (must already exist in your project)required
web_countNumber of web instances2
app_countNumber of app instances2
db_countNumber of database instances1
web_flavorWeb tier instance sizes1a.small
app_flavorApp tier instance sizem2a.large
db_flavorDatabase tier instance sizem2a.xlarge
image_nameOperating system imageUbuntu-24.04
external_networkExternal network for router gateway and floating IPPublicStatic
web_cidrCIDR for the web subnet192.168.60.0/24
app_cidrCIDR for the application subnet192.168.61.0/24
db_cidrCIDR for the database subnet192.168.62.0/24
db_volume_sizeSize in GB for each database data volume50

When to use this pattern#

Use this pattern for classic web, app, and database tiers with per-tier subnets and security groups. Choose Full-Stack Application for a similar layout with cloud-init driven setup. Put an edge reverse proxy or API gateway in front when the web tier needs a dedicated public entry point.

Estimated cost#

Monthly cost estimate

Pricing calculator ↗

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

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

2× Web tier

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

Serves incoming application traffic and terminates client connections.

Runs on shared CPU because the web tier scales horizontally. Add instances with web_count rather than a larger flavor.

$33.00/mo

2× App tier

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

Runs the application logic the web tier calls, on a private subnet with no public IP.

Uses general-purpose dedicated CPU for steady request handling.

$132.00/mo

Database

m2a.xlarge · 4 dedicated vCPU, 16 GiB RAM, 1 Gbps

Runs the database on a private subnet, with a dedicated data volume per instance.

Uses a memory-optimized flavor so the working set stays in RAM.

$132.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.small

2 shared vCPU, 2 GiB RAM, 0.5 Gbps

$16.50

m2a.large

2 dedicated vCPU, 8 GiB RAM, 0.5 Gbps

$66.00

m2a.large

2 dedicated vCPU, 8 GiB RAM, 0.5 Gbps

$66.00

m2a.xlarge

4 dedicated vCPU, 16 GiB RAM, 1 Gbps

$132.00

Compute + RAM rate basis

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

—

Block storage (150 GiB)

150 GiB at $0.08/GiB/mo

$12.00

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.

$106.00/mo

Production on the configured CPU

The headline estimate above; predictable steady-load performance.

$304.00/mo

Saves $198.00/mo while you build on shared CPU.

Shared flavors carry less RAM (m2a.large (8 GiB RAM) -> s1a.small (2 GiB RAM); m2a.xlarge (16 GiB RAM) -> s1a.medium (4 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#

6 files. Download the zip or expand to copy any file.Download three-tier-app.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           = "three-tier-app-private"
  admin_state_up = true
}

resource "openstack_networking_subnet_v2" "web" {
  name       = "three-tier-app-web-sn"
  network_id = openstack_networking_network_v2.private.id
  cidr       = var.web_cidr
  ip_version = 4
}

resource "openstack_networking_subnet_v2" "app" {
  name       = "three-tier-app-app-sn"
  network_id = openstack_networking_network_v2.private.id
  cidr       = var.app_cidr
  ip_version = 4
}

resource "openstack_networking_subnet_v2" "db" {
  name       = "three-tier-app-db-sn"
  network_id = openstack_networking_network_v2.private.id
  cidr       = var.db_cidr
  ip_version = 4
}

resource "openstack_networking_router_v2" "gw" {
  name                = "three-tier-app-router"
  admin_state_up      = true
  external_network_id = data.openstack_networking_network_v2.external.id
}

resource "openstack_networking_router_interface_v2" "web" {
  router_id = openstack_networking_router_v2.gw.id
  subnet_id = openstack_networking_subnet_v2.web.id
}

resource "openstack_networking_router_interface_v2" "app" {
  router_id = openstack_networking_router_v2.gw.id
  subnet_id = openstack_networking_subnet_v2.app.id
}

resource "openstack_networking_router_interface_v2" "db" {
  router_id = openstack_networking_router_v2.gw.id
  subnet_id = openstack_networking_subnet_v2.db.id
}

resource "openstack_networking_secgroup_v2" "web" {
  name        = "three-tier-app-web-sg"
  description = "Web: HTTP, HTTPS, SSH from anywhere"
}

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_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_v2" "app" {
  name        = "three-tier-app-app-sg"
  description = "App: 8080 and SSH from web tier 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.web_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.web_cidr
  security_group_id = openstack_networking_secgroup_v2.app.id
}

resource "openstack_networking_secgroup_v2" "db" {
  name        = "three-tier-app-db-sg"
  description = "DB: MySQL, Postgres, SSH from app tier 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.app_cidr
  security_group_id = openstack_networking_secgroup_v2.db.id
}

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.app_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.app_cidr
  security_group_id = openstack_networking_secgroup_v2.db.id
}

resource "openstack_networking_port_v2" "web" {
  count = var.web_count

  name               = "three-tier-app-web-${count.index + 1}"
  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.web.id
  }

  depends_on = [
    openstack_networking_router_interface_v2.web,
    openstack_networking_router_interface_v2.app,
    openstack_networking_router_interface_v2.db,
  ]
}

resource "openstack_networking_port_v2" "app" {
  count = var.app_count

  name               = "three-tier-app-app-${count.index + 1}"
  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.app.id
  }

  depends_on = [
    openstack_networking_router_interface_v2.web,
    openstack_networking_router_interface_v2.app,
    openstack_networking_router_interface_v2.db,
  ]
}

resource "openstack_networking_port_v2" "db" {
  count = var.db_count

  name               = "three-tier-app-db-${count.index + 1}"
  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.db.id
  }

  depends_on = [
    openstack_networking_router_interface_v2.web,
    openstack_networking_router_interface_v2.app,
    openstack_networking_router_interface_v2.db,
  ]
}

resource "openstack_compute_instance_v2" "web" {
  count = var.web_count

  name        = "three-tier-app-web-${count.index + 1}"
  flavor_name = var.web_flavor
  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.web[count.index].id
  }
}

resource "openstack_compute_instance_v2" "app" {
  count = var.app_count

  name        = "three-tier-app-app-${count.index + 1}"
  flavor_name = var.app_flavor
  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.app[count.index].id
  }
}

resource "openstack_compute_instance_v2" "db" {
  count = var.db_count

  name        = "three-tier-app-db-${count.index + 1}"
  flavor_name = var.db_flavor
  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.db[count.index].id
  }
}

resource "openstack_blockstorage_volume_v3" "db_data" {
  count = var.db_count

  name = "three-tier-app-db-${count.index + 1}-data"
  size = var.db_volume_size
}

resource "openstack_compute_volume_attach_v2" "db_data" {
  count = var.db_count

  instance_id = openstack_compute_instance_v2.db[count.index].id
  volume_id   = openstack_blockstorage_volume_v3.db_data[count.index].id
}

resource "openstack_networking_floatingip_v2" "web_primary" {
  count = var.web_count > 0 ? 1 : 0
  pool  = var.external_network
}

resource "openstack_networking_floatingip_associate_v2" "web_primary" {
  count = var.web_count > 0 ? 1 : 0

  floating_ip = openstack_networking_floatingip_v2.web_primary[0].address
  port_id     = openstack_networking_port_v2.web[0].id

  depends_on = [openstack_compute_instance_v2.web]
}
variables.tfHCL
variable "key_name" {
  description = "Existing SSH keypair name in the project for compute instances"
  type        = string
}

variable "web_count" {
  description = "Number of web tier instances"
  type        = number
  default     = 2
}

variable "app_count" {
  description = "Number of application tier instances"
  type        = number
  default     = 2
}

variable "db_count" {
  description = "Number of database tier instances"
  type        = number
  default     = 1
}

variable "web_flavor" {
  description = "Flavor for web tier instances"
  type        = string
  default     = "s1a.small"
}

variable "app_flavor" {
  description = "Flavor for application tier instances"
  type        = string
  default     = "m2a.large"
}

variable "db_flavor" {
  description = "Flavor for database tier instances"
  type        = string
  default     = "m2a.xlarge"
}

variable "image_name" {
  description = "Boot image name"
  type        = string
  default     = "Ubuntu-24.04"
}

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 "web_cidr" {
  description = "CIDR for the web subnet"
  type        = string
  default     = "192.168.60.0/24"
}

variable "app_cidr" {
  description = "CIDR for the application subnet"
  type        = string
  default     = "192.168.61.0/24"
}

variable "db_cidr" {
  description = "CIDR for the database subnet"
  type        = string
  default     = "192.168.62.0/24"
}

variable "db_volume_size" {
  description = "Size in GB for each database data volume"
  type        = number
  default     = 50
}
outputs.tfHCL
output "web_floating_ip" {
  value = try(openstack_networking_floatingip_v2.web_primary[0].address, null)
}

output "web_ips" {
  value = [for i in openstack_compute_instance_v2.web : i.network[0].fixed_ip_v4]
}

output "app_ips" {
  value = [for i in openstack_compute_instance_v2.app : i.network[0].fixed_ip_v4]
}

output "db_ips" {
  value = [for i in openstack_compute_instance_v2.db : i.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_count = 2
# app_count = 2
# db_count = 1
# web_flavor = "s1a.small"
# app_flavor = "m2a.large"
# db_flavor = "m2a.xlarge"
# image_name = "Ubuntu-24.04"
# external_network = "PublicStatic"
# web_cidr = "192.168.60.0/24"
# app_cidr = "192.168.61.0/24"
# db_cidr = "192.168.62.0/24"
# db_volume_size = 50
README.mdMarkdown
# Three Tier App

Three-tier architecture with separate web, application, and database subnets, multiple instances per tier where configured, and isolated network segments 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 several compute instances, routers, and database volumes

## 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_count` | number | no | `2` | Number of web tier instances |
| `app_count` | number | no | `2` | Number of application tier instances |
| `db_count` | number | no | `1` | Number of database tier instances |
| `web_flavor` | string | no | `s1a.small` | Flavor for web tier |
| `app_flavor` | string | no | `m2a.large` | Flavor for application tier |
| `db_flavor` | string | no | `m2a.xlarge` | Flavor for database tier |
| `image_name` | string | no | `Ubuntu-24.04` | Boot image name |
| `external_network` | string | no | `PublicStatic` | Persisted FIP / production default; override with `PublicEphemeral` for demos |
| `web_cidr` | string | no | `192.168.60.0/24` | Web subnet CIDR |
| `app_cidr` | string | no | `192.168.61.0/24` | Application subnet CIDR |
| `db_cidr` | string | no | `192.168.62.0/24` | Database subnet CIDR |
| `db_volume_size` | number | no | `50` | Database data volume size in GB per instance |

## Documentation

Full documentation: [Three tier app template](/docs/automation/templates/three-tier-app)

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

  • Monitoringthree-tier-app-monitored+$95/mo over the base

    Prometheus and Grafana monitoring for the three-tier stack

    Show source and download
    8 files. Download the zip or copy any file.Download three-tier-app-monitored.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           = "three-tier-app-private"
      admin_state_up = true
    }
    
    resource "openstack_networking_subnet_v2" "web" {
      name       = "three-tier-app-web-sn"
      network_id = openstack_networking_network_v2.private.id
      cidr       = var.web_cidr
      ip_version = 4
      dns_nameservers = ["1.1.1.1", "8.8.8.8"]
    }
    
    resource "openstack_networking_subnet_v2" "app" {
      name       = "three-tier-app-app-sn"
      network_id = openstack_networking_network_v2.private.id
      cidr       = var.app_cidr
      ip_version = 4
      dns_nameservers = ["1.1.1.1", "8.8.8.8"]
    }
    
    resource "openstack_networking_subnet_v2" "db" {
      name       = "three-tier-app-db-sn"
      network_id = openstack_networking_network_v2.private.id
      cidr       = var.db_cidr
      ip_version = 4
      dns_nameservers = ["1.1.1.1", "8.8.8.8"]
    }
    
    resource "openstack_networking_router_v2" "gw" {
      name                = "three-tier-app-router"
      admin_state_up      = true
      external_network_id = data.openstack_networking_network_v2.external.id
    }
    
    resource "openstack_networking_router_interface_v2" "web" {
      router_id = openstack_networking_router_v2.gw.id
      subnet_id = openstack_networking_subnet_v2.web.id
    }
    
    resource "openstack_networking_router_interface_v2" "app" {
      router_id = openstack_networking_router_v2.gw.id
      subnet_id = openstack_networking_subnet_v2.app.id
    }
    
    resource "openstack_networking_router_interface_v2" "db" {
      router_id = openstack_networking_router_v2.gw.id
      subnet_id = openstack_networking_subnet_v2.db.id
    }
    
    resource "openstack_networking_secgroup_v2" "web" {
      name        = "three-tier-app-web-sg"
      description = "Web: HTTP, HTTPS, SSH from anywhere"
    }
    
    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_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_v2" "app" {
      name        = "three-tier-app-app-sg"
      description = "App: 8080 and SSH from web tier 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.web_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.web_cidr
      security_group_id = openstack_networking_secgroup_v2.app.id
    }
    
    resource "openstack_networking_secgroup_v2" "db" {
      name        = "three-tier-app-db-sg"
      description = "DB: MySQL, Postgres, SSH from app tier 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.app_cidr
      security_group_id = openstack_networking_secgroup_v2.db.id
    }
    
    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.app_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.app_cidr
      security_group_id = openstack_networking_secgroup_v2.db.id
    }
    
    resource "openstack_networking_port_v2" "web" {
      count = var.web_count
    
      name               = "three-tier-app-web-${count.index + 1}"
      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.web.id
      }
    
      depends_on = [
        openstack_networking_router_interface_v2.web,
        openstack_networking_router_interface_v2.app,
        openstack_networking_router_interface_v2.db,
      ]
    }
    
    resource "openstack_networking_port_v2" "app" {
      count = var.app_count
    
      name               = "three-tier-app-app-${count.index + 1}"
      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.app.id
      }
    
      depends_on = [
        openstack_networking_router_interface_v2.web,
        openstack_networking_router_interface_v2.app,
        openstack_networking_router_interface_v2.db,
      ]
    }
    
    resource "openstack_networking_port_v2" "db" {
      count = var.db_count
    
      name               = "three-tier-app-db-${count.index + 1}"
      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.db.id
      }
    
      depends_on = [
        openstack_networking_router_interface_v2.web,
        openstack_networking_router_interface_v2.app,
        openstack_networking_router_interface_v2.db,
      ]
    }
    
    resource "openstack_compute_instance_v2" "web" {
      count = var.web_count
    
      name        = "three-tier-app-web-${count.index + 1}"
      flavor_name = var.web_flavor
      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.web[count.index].id
      }
    }
    
    resource "openstack_compute_instance_v2" "app" {
      count = var.app_count
    
      name        = "three-tier-app-app-${count.index + 1}"
      flavor_name = var.app_flavor
      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.app[count.index].id
      }
    }
    
    resource "openstack_compute_instance_v2" "db" {
      count = var.db_count
    
      name        = "three-tier-app-db-${count.index + 1}"
      flavor_name = var.db_flavor
      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.db[count.index].id
      }
    }
    
    resource "openstack_blockstorage_volume_v3" "db_data" {
      count = var.db_count
    
      name = "three-tier-app-db-${count.index + 1}-data"
      size = var.db_volume_size
    }
    
    resource "openstack_compute_volume_attach_v2" "db_data" {
      count = var.db_count
    
      instance_id = openstack_compute_instance_v2.db[count.index].id
      volume_id   = openstack_blockstorage_volume_v3.db_data[count.index].id
    }
    
    resource "openstack_networking_floatingip_v2" "web_primary" {
      count = var.web_count > 0 ? 1 : 0
      pool  = var.external_network
    }
    
    resource "openstack_networking_floatingip_associate_v2" "web_primary" {
      count = var.web_count > 0 ? 1 : 0
    
      floating_ip = openstack_networking_floatingip_v2.web_primary[0].address
      port_id     = openstack_networking_port_v2.web[0].id
    
      depends_on = [openstack_compute_instance_v2.web]
    }
    
    locals {
      monitored_scrape_targets_yaml = join("\n", concat(
        [
          "        - job_name: \"three-tier-web\"",
          "          static_configs:",
          "            - targets:"
        ],
        [for ip in [for w in openstack_compute_instance_v2.web : w.network[0].fixed_ip_v4] :
          "                - ${yamlencode("${ip}:9100")}"]
      ))
    }
    
    resource "openstack_networking_secgroup_v2" "prometheus" {
      name        = "three-tier-app-monitored-prometheus"
      description = "Prometheus 9090 from private subnets; 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.web_cidr
      security_group_id = openstack_networking_secgroup_v2.prometheus.id
    }
    
    resource "openstack_networking_secgroup_rule_v2" "prometheus_http_app" {
      direction         = "ingress"
      ethertype         = "IPv4"
      protocol          = "tcp"
      port_range_min    = 9090
      port_range_max    = 9090
      remote_ip_prefix  = var.app_cidr
      security_group_id = openstack_networking_secgroup_v2.prometheus.id
    }
    
    resource "openstack_networking_secgroup_rule_v2" "prometheus_http_db" {
      direction         = "ingress"
      ethertype         = "IPv4"
      protocol          = "tcp"
      port_range_min    = 9090
      port_range_max    = 9090
      remote_ip_prefix  = var.db_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        = "three-tier-app-monitored-grafana"
      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_secgroup_rule_v2" "web_node_exporter" {
      direction         = "ingress"
      ethertype         = "IPv4"
      protocol          = "tcp"
      port_range_min    = 9100
      port_range_max    = 9100
      remote_ip_prefix  = var.web_cidr
      security_group_id = openstack_networking_secgroup_v2.web.id
    }
    
    resource "openstack_blockstorage_volume_v3" "prometheus_data" {
      name = "three-tier-app-monitored-prometheus-data"
      size = var.prometheus_volume_size
    }
    
    resource "openstack_blockstorage_volume_v3" "grafana_data" {
      name = "three-tier-app-monitored-grafana-data"
      size = var.grafana_volume_size
    }
    
    resource "openstack_networking_port_v2" "prometheus" {
      name               = "three-tier-app-monitored-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.web.id
      }
    
      depends_on = [openstack_networking_router_interface_v2.web]
    }
    
    resource "openstack_compute_instance_v2" "prometheus" {
      name        = "three-tier-app-monitored-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.monitored_scrape_targets_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.web,
        openstack_compute_instance_v2.web,
      ]
    }
    
    resource "openstack_networking_port_v2" "grafana" {
      name               = "three-tier-app-monitored-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.web.id
      }
    
      depends_on = [openstack_networking_router_interface_v2.web]
    }
    
    resource "openstack_compute_instance_v2" "grafana" {
      name        = "three-tier-app-monitored-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 "key_name" {
      description = "Existing SSH keypair name in the project for compute instances"
      type        = string
    }
    
    variable "web_count" {
      description = "Number of web tier instances"
      type        = number
      default     = 2
    }
    
    variable "app_count" {
      description = "Number of application tier instances"
      type        = number
      default     = 2
    }
    
    variable "db_count" {
      description = "Number of database tier instances"
      type        = number
      default     = 1
    }
    
    variable "web_flavor" {
      description = "Flavor for web tier instances"
      type        = string
      default     = "s1a.small"
    }
    
    variable "app_flavor" {
      description = "Flavor for application tier instances"
      type        = string
      default     = "m2a.large"
    }
    
    variable "db_flavor" {
      description = "Flavor for database tier instances"
      type        = string
      default     = "m2a.xlarge"
    }
    
    variable "image_name" {
      description = "Boot image name"
      type        = string
      default     = "Ubuntu-24.04"
    }
    
    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 "web_cidr" {
      description = "CIDR for the web subnet"
      type        = string
      default     = "192.168.60.0/24"
    }
    
    variable "app_cidr" {
      description = "CIDR for the application subnet"
      type        = string
      default     = "192.168.61.0/24"
    }
    
    variable "db_cidr" {
      description = "CIDR for the database subnet"
      type        = string
      default     = "192.168.62.0/24"
    }
    
    variable "db_volume_size" {
      description = "Size in GB for each database data volume"
      type        = number
      default     = 50
    }
    
    variable "prometheus_flavor" {
      description = "Flavor for the Prometheus instance"
      type        = string
      default     = "m2a.large"
    }
    
    variable "grafana_flavor" {
      description = "Flavor for the Grafana instance"
      type        = string
      default     = "s1a.small"
    }
    
    variable "prometheus_volume_size" {
      description = "Cinder volume size in GiB for Prometheus TSDB data"
      type        = number
      default     = 50
    }
    
    variable "grafana_volume_size" {
      description = "Cinder volume size in GiB for Grafana data"
      type        = number
      default     = 20
    }
    
    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 "web_floating_ip" {
      value = try(openstack_networking_floatingip_v2.web_primary[0].address, null)
    }
    
    output "web_ips" {
      value = [for i in openstack_compute_instance_v2.web : i.network[0].fixed_ip_v4]
    }
    
    output "app_ips" {
      value = [for i in openstack_compute_instance_v2.app : i.network[0].fixed_ip_v4]
    }
    
    output "db_ips" {
      value = [for i in openstack_compute_instance_v2.db : i.network[0].fixed_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
    key_name = "YOUR_KEY_NAME"
    
    # web_count = 2
    # app_count = 2
    # db_count = 1
    # web_flavor = "s1a.small"
    # app_flavor = "m2a.large"
    # db_flavor = "m2a.xlarge"
    # image_name = "Ubuntu-24.04"
    # external_network = "PublicStatic"
    # web_cidr = "192.168.60.0/24"
    # app_cidr = "192.168.61.0/24"
    # db_cidr = "192.168.62.0/24"
    # db_volume_size = 50
    
    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
    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 $(seq 1 30); do
          getent hosts archive.ubuntu.com >/dev/null 2>&1 && break
          sleep 5
        done
        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
    
    README.mdMarkdown
    # Three Tier App
    
    Three-tier architecture with separate web, application, and database subnets, multiple instances per tier where configured, and isolated network segments 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 several compute instances, routers, and database volumes
    
    ## 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_count` | number | no | `2` | Number of web tier instances |
    | `app_count` | number | no | `2` | Number of application tier instances |
    | `db_count` | number | no | `1` | Number of database tier instances |
    | `web_flavor` | string | no | `s1a.small` | Flavor for web tier |
    | `app_flavor` | string | no | `m2a.large` | Flavor for application tier |
    | `db_flavor` | string | no | `m2a.xlarge` | Flavor for database tier |
    | `image_name` | string | no | `Ubuntu-24.04` | Boot image name |
    | `external_network` | string | no | `PublicStatic` | Persisted FIP / production default; override with `PublicEphemeral` for demos |
    | `web_cidr` | string | no | `192.168.60.0/24` | Web subnet CIDR |
    | `app_cidr` | string | no | `192.168.61.0/24` | Application subnet CIDR |
    | `db_cidr` | string | no | `192.168.62.0/24` | Database subnet CIDR |
    | `db_volume_size` | number | no | `50` | Database data volume size in GB per instance |
    
    ## Documentation
    
    Full documentation: [Three tier app template](/docs/automation/templates/three-tier-app)
    
  • Cachethree-tier-app-cache+$17/mo over the base

    Valkey cache tier for the application layer

    Show source and download
    7 files. Download the zip or copy any file.Download three-tier-app-cache.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           = "three-tier-app-private"
      admin_state_up = true
    }
    
    resource "openstack_networking_subnet_v2" "web" {
      name       = "three-tier-app-web-sn"
      network_id = openstack_networking_network_v2.private.id
      cidr       = var.web_cidr
      ip_version = 4
    }
    
    resource "openstack_networking_subnet_v2" "app" {
      name       = "three-tier-app-app-sn"
      network_id = openstack_networking_network_v2.private.id
      cidr       = var.app_cidr
      ip_version = 4
    }
    
    resource "openstack_networking_subnet_v2" "db" {
      name       = "three-tier-app-db-sn"
      network_id = openstack_networking_network_v2.private.id
      cidr       = var.db_cidr
      ip_version = 4
    }
    
    resource "openstack_networking_router_v2" "gw" {
      name                = "three-tier-app-router"
      admin_state_up      = true
      external_network_id = data.openstack_networking_network_v2.external.id
    }
    
    resource "openstack_networking_router_interface_v2" "web" {
      router_id = openstack_networking_router_v2.gw.id
      subnet_id = openstack_networking_subnet_v2.web.id
    }
    
    resource "openstack_networking_router_interface_v2" "app" {
      router_id = openstack_networking_router_v2.gw.id
      subnet_id = openstack_networking_subnet_v2.app.id
    }
    
    resource "openstack_networking_router_interface_v2" "db" {
      router_id = openstack_networking_router_v2.gw.id
      subnet_id = openstack_networking_subnet_v2.db.id
    }
    
    resource "openstack_networking_secgroup_v2" "web" {
      name        = "three-tier-app-web-sg"
      description = "Web: HTTP, HTTPS, SSH from anywhere"
    }
    
    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_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_v2" "app" {
      name        = "three-tier-app-app-sg"
      description = "App: 8080 and SSH from web tier 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.web_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.web_cidr
      security_group_id = openstack_networking_secgroup_v2.app.id
    }
    
    resource "openstack_networking_secgroup_v2" "db" {
      name        = "three-tier-app-db-sg"
      description = "DB: MySQL, Postgres, SSH from app tier 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.app_cidr
      security_group_id = openstack_networking_secgroup_v2.db.id
    }
    
    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.app_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.app_cidr
      security_group_id = openstack_networking_secgroup_v2.db.id
    }
    
    resource "openstack_networking_port_v2" "web" {
      count = var.web_count
    
      name               = "three-tier-app-web-${count.index + 1}"
      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.web.id
      }
    
      depends_on = [
        openstack_networking_router_interface_v2.web,
        openstack_networking_router_interface_v2.app,
        openstack_networking_router_interface_v2.db,
      ]
    }
    
    resource "openstack_networking_port_v2" "app" {
      count = var.app_count
    
      name               = "three-tier-app-app-${count.index + 1}"
      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.app.id
      }
    
      depends_on = [
        openstack_networking_router_interface_v2.web,
        openstack_networking_router_interface_v2.app,
        openstack_networking_router_interface_v2.db,
      ]
    }
    
    resource "openstack_networking_port_v2" "db" {
      count = var.db_count
    
      name               = "three-tier-app-db-${count.index + 1}"
      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.db.id
      }
    
      depends_on = [
        openstack_networking_router_interface_v2.web,
        openstack_networking_router_interface_v2.app,
        openstack_networking_router_interface_v2.db,
      ]
    }
    
    resource "openstack_compute_instance_v2" "web" {
      count = var.web_count
    
      name        = "three-tier-app-web-${count.index + 1}"
      flavor_name = var.web_flavor
      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.web[count.index].id
      }
    }
    
    resource "openstack_compute_instance_v2" "app" {
      count = var.app_count
    
      name        = "three-tier-app-app-${count.index + 1}"
      flavor_name = var.app_flavor
      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.app[count.index].id
      }
    }
    
    resource "openstack_compute_instance_v2" "db" {
      count = var.db_count
    
      name        = "three-tier-app-db-${count.index + 1}"
      flavor_name = var.db_flavor
      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.db[count.index].id
      }
    }
    
    resource "openstack_blockstorage_volume_v3" "db_data" {
      count = var.db_count
    
      name = "three-tier-app-db-${count.index + 1}-data"
      size = var.db_volume_size
    }
    
    resource "openstack_compute_volume_attach_v2" "db_data" {
      count = var.db_count
    
      instance_id = openstack_compute_instance_v2.db[count.index].id
      volume_id   = openstack_blockstorage_volume_v3.db_data[count.index].id
    }
    
    resource "openstack_networking_floatingip_v2" "web_primary" {
      count = var.web_count > 0 ? 1 : 0
      pool  = var.external_network
    }
    
    resource "openstack_networking_floatingip_associate_v2" "web_primary" {
      count = var.web_count > 0 ? 1 : 0
    
      floating_ip = openstack_networking_floatingip_v2.web_primary[0].address
      port_id     = openstack_networking_port_v2.web[0].id
    
      depends_on = [openstack_compute_instance_v2.web]
    }
    
    resource "openstack_networking_secgroup_v2" "redis" {
      name        = "three-tier-app-cache-redis-sg"
      description = "Valkey cache from app tier only"
    }
    
    resource "openstack_networking_secgroup_rule_v2" "redis_valkey" {
      direction         = "ingress"
      ethertype         = "IPv4"
      protocol          = "tcp"
      port_range_min    = 6379
      port_range_max    = 6379
      remote_ip_prefix  = var.app_cidr
      security_group_id = openstack_networking_secgroup_v2.redis.id
    }
    
    resource "openstack_networking_port_v2" "redis" {
      name               = "three-tier-app-cache-redis-port"
      network_id         = openstack_networking_network_v2.private.id
      security_group_ids = [openstack_networking_secgroup_v2.redis.id]
    
      fixed_ip {
        subnet_id = openstack_networking_subnet_v2.app.id
      }
    
      depends_on = [
        openstack_networking_router_interface_v2.web,
        openstack_networking_router_interface_v2.app,
        openstack_networking_router_interface_v2.db,
      ]
    }
    
    resource "openstack_compute_instance_v2" "redis" {
      name        = "three-tier-app-cache-redis"
      flavor_name = var.cache_flavor
      key_pair    = var.key_name
    
      user_data = file("${path.module}/cloud-init/redis.yaml")
    
      block_device {
        uuid                  = data.openstack_images_image_v2.os.id
        source_type           = "image"
        destination_type      = "volume"
        volume_size           = 10
        boot_index            = 0
        delete_on_termination = true
      }
    
      network {
        port = openstack_networking_port_v2.redis.id
      }
    
      depends_on = [
        openstack_networking_router_interface_v2.web,
        openstack_networking_router_interface_v2.app,
        openstack_networking_router_interface_v2.db,
      ]
    }
    
    variables.tfHCL
    variable "key_name" {
      description = "Existing SSH keypair name in the project for compute instances"
      type        = string
    }
    
    variable "web_count" {
      description = "Number of web tier instances"
      type        = number
      default     = 2
    }
    
    variable "app_count" {
      description = "Number of application tier instances"
      type        = number
      default     = 2
    }
    
    variable "db_count" {
      description = "Number of database tier instances"
      type        = number
      default     = 1
    }
    
    variable "web_flavor" {
      description = "Flavor for web tier instances"
      type        = string
      default     = "s1a.small"
    }
    
    variable "app_flavor" {
      description = "Flavor for application tier instances"
      type        = string
      default     = "m2a.large"
    }
    
    variable "db_flavor" {
      description = "Flavor for database tier instances"
      type        = string
      default     = "m2a.xlarge"
    }
    
    variable "image_name" {
      description = "Boot image name"
      type        = string
      default     = "Ubuntu-24.04"
    }
    
    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 "web_cidr" {
      description = "CIDR for the web subnet"
      type        = string
      default     = "192.168.60.0/24"
    }
    
    variable "app_cidr" {
      description = "CIDR for the application subnet"
      type        = string
      default     = "192.168.61.0/24"
    }
    
    variable "db_cidr" {
      description = "CIDR for the database subnet"
      type        = string
      default     = "192.168.62.0/24"
    }
    
    variable "db_volume_size" {
      description = "Size in GB for each database data volume"
      type        = number
      default     = 50
    }
    
    variable "cache_flavor" {
      description = "Flavor for the Valkey cache instance"
      type        = string
      default     = "s1a.small"
    }
    
    outputs.tfHCL
    output "web_floating_ip" {
      value = try(openstack_networking_floatingip_v2.web_primary[0].address, null)
    }
    
    output "web_ips" {
      value = [for i in openstack_compute_instance_v2.web : i.network[0].fixed_ip_v4]
    }
    
    output "app_ips" {
      value = [for i in openstack_compute_instance_v2.app : i.network[0].fixed_ip_v4]
    }
    
    output "db_ips" {
      value = [for i in openstack_compute_instance_v2.db : i.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_count = 2
    # app_count = 2
    # db_count = 1
    # web_flavor = "s1a.small"
    # app_flavor = "m2a.large"
    # db_flavor = "m2a.xlarge"
    # image_name = "Ubuntu-24.04"
    # external_network = "PublicStatic"
    # web_cidr = "192.168.60.0/24"
    # app_cidr = "192.168.61.0/24"
    # db_cidr = "192.168.62.0/24"
    # db_volume_size = 50
    
    cloud-init/redis.yamlYAML
    #cloud-config
    package_update: true
    packages:
      - valkey-server
    runcmd:
      - |
        set -e
        # Valkey listens on the private interface only; security group restricts clients to the subnet.
        sed -i 's/^bind .*/bind 0.0.0.0/' /etc/valkey/valkey.conf
        sed -i 's/^protected-mode .*/protected-mode yes/' /etc/valkey/valkey.conf
        systemctl enable valkey-server
        systemctl restart valkey-server
    
    README.mdMarkdown
    # Three Tier App
    
    Three-tier architecture with separate web, application, and database subnets, multiple instances per tier where configured, and isolated network segments 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 several compute instances, routers, and database volumes
    
    ## 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_count` | number | no | `2` | Number of web tier instances |
    | `app_count` | number | no | `2` | Number of application tier instances |
    | `db_count` | number | no | `1` | Number of database tier instances |
    | `web_flavor` | string | no | `s1a.small` | Flavor for web tier |
    | `app_flavor` | string | no | `m2a.large` | Flavor for application tier |
    | `db_flavor` | string | no | `m2a.xlarge` | Flavor for database tier |
    | `image_name` | string | no | `Ubuntu-24.04` | Boot image name |
    | `external_network` | string | no | `PublicStatic` | Persisted FIP / production default; override with `PublicEphemeral` for demos |
    | `web_cidr` | string | no | `192.168.60.0/24` | Web subnet CIDR |
    | `app_cidr` | string | no | `192.168.61.0/24` | Application subnet CIDR |
    | `db_cidr` | string | no | `192.168.62.0/24` | Database subnet CIDR |
    | `db_volume_size` | number | no | `50` | Database data volume size in GB per instance |
    
    ## Documentation
    
    Full documentation: [Three tier app template](/docs/automation/templates/three-tier-app)
    
Resources, parameters, and variables
Provisions
Parameterized by
Variables
  • key_namerequired
  • web_count=2
  • app_count=2
  • db_count=1
  • web_flavor="s1a.small"
  • app_flavor="m2a.large"
  • db_flavor="m2a.xlarge"
  • image_name="Ubuntu-24.04"
  • external_network="PublicStatic"
  • web_cidr="192.168.60.0/24"
  • app_cidr="192.168.61.0/24"
  • db_cidr="192.168.62.0/24"
  • db_volume_size=50

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?