Three-Tier Application
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#
| Parameter | Description | Default |
|---|---|---|
key_name | SSH keypair name (must already exist in your project) | required |
web_count | Number of web instances | 2 |
app_count | Number of app instances | 2 |
db_count | Number of database instances | 1 |
web_flavor | Web tier instance size | s1a.small |
app_flavor | App tier instance size | m2a.large |
db_flavor | Database tier instance size | m2a.xlarge |
image_name | Operating system image | Ubuntu-24.04 |
external_network | External network for router gateway and floating IP | PublicStatic |
web_cidr | CIDR for the web subnet | 192.168.60.0/24 |
app_cidr | CIDR for the application subnet | 192.168.61.0/24 |
db_cidr | CIDR for the database subnet | 192.168.62.0/24 |
db_volume_size | Size in GB for each database data volume | 50 |
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.
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.
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.
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.
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.small
2 shared vCPU, 2 GiB RAM, 0.5 Gbps
m2a.large
2 dedicated vCPU, 8 GiB RAM, 0.5 Gbps
m2a.large
2 dedicated vCPU, 8 GiB RAM, 0.5 Gbps
m2a.xlarge
4 dedicated vCPU, 16 GiB RAM, 1 Gbps
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
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 $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#
Show source (6 files)Hide source
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]
}
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
}
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]
}
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_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
# 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)
Variations
Deploy walkthroughValidated variants of this template, each adding one capability the base does not include.
- Monitoring
three-tier-app-monitored+$95/mo over the basePrometheus and Grafana monitoring for the three-tier stack
Show source and downloadHide source
8 files. Download the zip or copy any file.Download three-tier-app-monitored.zipmain.tfHCLdata "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.tfHCLvariable "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.tfHCLoutput "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.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_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 = 50cloud-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-servercloud-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 prometheusREADME.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) - Cache
three-tier-app-cache+$17/mo over the baseValkey cache tier for the application layer
Show source and downloadHide source
7 files. Download the zip or copy any file.Download three-tier-app-cache.zipmain.tfHCLdata "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.tfHCLvariable "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.tfHCLoutput "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.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_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 = 50cloud-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-serverREADME.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
key_namerequiredweb_count=2app_count=2db_count=1web_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#
- Customize a template's image and flavor
- Add a block volume to a template
- Parameterize a template with a tfvars file
See also#
- Deploy the three-tier application template with OpenTofu: end-to-end tutorial that applies this template, verifies tier isolation, and tears the stack down
- Full-Stack Application template
- Edge reverse proxy template
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
See Also
Networks
Prerequisite
Deploy the full-stack application template with OpenTofu
Shares: Subnets, Routers
Deploy the three-tier application template with OpenTofu
Shares: Subnets, Routers
Terraform and OpenTofu on Quake AI
Prerequisite
Build a private network with two virtual machines
Shares: Subnets, Routers