API gateway
API gateway
This pattern composes Compute, Network, and Block Storage into a self-hosted API gateway on infrastructure you control.
What this template does#
Provisions a single instance running Apache APISIX, an open-source API gateway, that routes HTTP traffic on a floating IP to private backend services:
- Compute instance that runs APISIX and etcd in Docker, sized for gateway proxy traffic (2 vCPU and 4 GiB RAM by default)
- Private network, subnet, router, port, and security group; a floating IP on the gateway only
- A block volume mounted at
/data, so APISIX config, etcd state, and logs live on a volume you can grow rather than on the boot disk - cloud-init installs Docker, generates an admin API key on first boot, starts etcd bound to localhost, and seeds starter routes from
upstream_services
The backend services stay on the private subnet with no floating IPs of their own. You add rate limits, key auth, and request shaping through the APISIX Admin API, then lock each backend security group to accept traffic only from the gateway security group.
No credential ships with this template. The instance generates the admin API key on first boot and writes it to /root/gateway-admin-credentials (readable only by root).
Honest scope#
This gateway runs in one region on a VM you operate. It is a regional API front door, not a global edge network: Quake AI has no anycast, no global PoPs, and no first-party managed API gateway. For geographic distribution and edge absorption, front the origin with a third-party CDN.
Alternate engines#
This template leads with Apache APISIX (Apache-2.0, NGINX/LuaJIT + etcd, hot config reload, 100+ open plugins). KrakenD fits when you want a Go gateway with no database: stateless, declarative JSON config, the lightest option to operate. Kong Gateway OSS fits when you want the largest plugin ecosystem; it requires Postgres, which adds operational cost beyond the gateway VM. Tyk OSS includes a dashboard and dev portal in the OSS edition. Swap the container stack in cloud-init if you prefer one of them.
Parameters#
| Parameter | Description | Default |
|---|---|---|
key_name | SSH keypair name (must already exist) | No default |
flavor_name | Instance size (APISIX + etcd in 4 GiB) | s1a.medium |
image_name | Operating system image | Ubuntu-24.04 |
app_name | Display name prefix for resources | api-gateway |
apisix_version | APISIX container image tag | 3.11.0-debian |
admin_port | Admin API port (reach via SSH tunnel) | 9180 |
domain | Public hostname for the gateway; empty serves HTTP on the floating IP | "" |
upstream_services | List of {name, host, port, uri} starter routes | service-a at 10.42.0.10, service-b at 10.42.0.11 |
volume_size | Block volume size in GiB, mounted at /data | 20 |
external_network | External network for floating IP allocation | PublicStatic |
private_cidr | CIDR for the private subnet | 10.42.0.0/24 |
Ports and access#
| Port | Purpose |
|---|---|
| 22 | Host SSH for administration |
| 80 | HTTP entry through the gateway data plane |
| 443 | HTTPS entry when TLS is configured on the gateway |
| 9180 | APISIX Admin API (not opened in the security group; use an SSH tunnel) |
| 2379 | etcd (localhost only; not in the security group) |
When to use this pattern#
Run your own API gateway on a VM you operate so client traffic hits one controlled endpoint with rate limits, key auth, and routing policy before it reaches private backend services. This is the developer-facing edge template in the edge and gateway family.
For model routing to LLM backends instead of your own services, see the sibling inference gateway template. For the TLS-only front door without gateway plugins, see the edge reverse proxy template.
Estimated cost#
Monthly cost estimate
Pricing calculator ↗Sized as a custom package on shared 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
Gateway
s1a.medium · 4 shared vCPU, 4 GiB RAM, 0.5 Gbps
Compute shown per role at custom-package rates ($29/dedicated vCPU, $7.25/shared vCPU, $1/GiB RAM). The headline above is the billed total: the cheaper of a named plan and the custom package, plus add-ons.
Included in baseline
s1a.medium
4 shared vCPU, 4 GiB RAM, 0.5 Gbps
Compute + RAM rate basis
4 vCPU + 4 GiB RAM at $29/dedicated vCPU, $7.25/shared vCPU, $1/GiB RAM (regular). Totals apply the flat −$5/mo package promotion.
Block storage (40 GiB)
40 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.
Pricing data last validated: . For current rates, check quake.ai/pricing.
Template source#
This is a validated OpenTofu template.
Show source (7 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 = "${var.app_name}-net"
admin_state_up = true
}
resource "openstack_networking_subnet_v2" "private" {
name = "${var.app_name}-subnet"
network_id = openstack_networking_network_v2.private.id
cidr = var.private_cidr
ip_version = 4
dns_nameservers = ["1.1.1.1", "8.8.8.8"]
}
resource "openstack_networking_router_v2" "main" {
name = "${var.app_name}-router"
external_network_id = data.openstack_networking_network_v2.external.id
}
resource "openstack_networking_router_interface_v2" "private" {
router_id = openstack_networking_router_v2.main.id
subnet_id = openstack_networking_subnet_v2.private.id
}
resource "openstack_networking_secgroup_v2" "gateway" {
name = "${var.app_name}-sg"
description = "SSH and HTTP/HTTPS for the API gateway data plane"
}
resource "openstack_networking_secgroup_rule_v2" "ssh" {
direction = "ingress"
ethertype = "IPv4"
protocol = "tcp"
port_range_min = 22
port_range_max = 22
remote_ip_prefix = "0.0.0.0/0"
security_group_id = openstack_networking_secgroup_v2.gateway.id
}
resource "openstack_networking_secgroup_rule_v2" "http" {
direction = "ingress"
ethertype = "IPv4"
protocol = "tcp"
port_range_min = 80
port_range_max = 80
remote_ip_prefix = "0.0.0.0/0"
security_group_id = openstack_networking_secgroup_v2.gateway.id
}
resource "openstack_networking_secgroup_rule_v2" "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.gateway.id
}
resource "openstack_networking_port_v2" "gateway" {
name = "${var.app_name}-port"
network_id = openstack_networking_network_v2.private.id
security_group_ids = [openstack_networking_secgroup_v2.gateway.id]
fixed_ip {
subnet_id = openstack_networking_subnet_v2.private.id
}
depends_on = [openstack_networking_router_interface_v2.private]
}
resource "openstack_blockstorage_volume_v3" "data" {
name = "${var.app_name}-data"
size = var.volume_size
}
resource "openstack_networking_floatingip_v2" "gateway" {
pool = var.external_network
}
resource "openstack_compute_instance_v2" "gateway" {
name = var.app_name
flavor_name = var.flavor_name
key_pair = var.key_name
user_data = templatefile("${path.module}/cloud-init/gateway.yaml.tftpl", {
apisix_version = var.apisix_version
admin_port = var.admin_port
domain = var.domain
floating_ip = openstack_networking_floatingip_v2.gateway.address
upstream_services = jsonencode(var.upstream_services)
})
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.gateway.id
}
}
resource "openstack_compute_volume_attach_v2" "data" {
instance_id = openstack_compute_instance_v2.gateway.id
volume_id = openstack_blockstorage_volume_v3.data.id
}
resource "openstack_networking_floatingip_associate_v2" "gateway" {
floating_ip = openstack_networking_floatingip_v2.gateway.address
port_id = openstack_networking_port_v2.gateway.id
}
variable "key_name" {
description = "SSH keypair name (must already exist in your project)"
type = string
}
variable "flavor_name" {
description = "Instance size. APISIX plus etcd fit in 4 GiB for a modest API surface; raise the flavor when you front many routes or run high request concurrency."
type = string
default = "s1a.medium"
}
variable "image_name" {
description = "Operating system image. cloud-init targets a Debian-family distribution; Ubuntu 24.04 is the recommended base."
type = string
default = "Ubuntu-24.04"
}
variable "app_name" {
description = "Display name prefix for compute and network resources"
type = string
default = "api-gateway"
}
variable "apisix_version" {
description = "Apache APISIX container image tag. The default tracks the current 3.x release; pin a specific tag for reproducible rebuilds."
type = string
default = "3.11.0-debian"
}
variable "admin_port" {
description = "TCP port for the APISIX Admin API on the instance. The security group opens only 22, 80, and 443; reach the admin API through an SSH tunnel to this port."
type = number
default = 9180
}
variable "domain" {
description = "Public hostname for the gateway front door. When set, gateway_url in the outputs uses https:// on this domain after you point its DNS A record at the floating IP and configure TLS. Leave empty to test over HTTP on the floating IP first."
type = string
default = ""
}
variable "upstream_services" {
description = "Private backend services seeded as starter routes on first boot. Each entry maps a URI prefix to a host:port on the private subnet."
type = list(object({
name = string
host = string
port = number
uri = string
}))
default = [
{
name = "service-a"
host = "10.42.0.10"
port = 8080
uri = "/service-a/*"
},
{
name = "service-b"
host = "10.42.0.11"
port = 8080
uri = "/service-b/*"
},
]
}
variable "volume_size" {
description = "Block volume size in GiB for APISIX config, etcd state, and request logs. The volume mounts at /data so gateway data lives on a resizable volume rather than the boot disk."
type = number
default = 20
}
variable "external_network" {
description = "Shared external network for router gateway and floating IPs; defaults to PublicStatic (persisted FIP / production pattern). Override with PublicEphemeral for ephemeral demos."
type = string
default = "PublicStatic"
}
variable "private_cidr" {
description = "CIDR for the private tenant network the gateway lives in"
type = string
default = "10.42.0.0/24"
}
output "instance_id" {
description = "ID of the compute instance running the API gateway"
value = openstack_compute_instance_v2.gateway.id
}
output "floating_ip" {
description = "Public floating IP address of the API gateway"
value = openstack_networking_floatingip_v2.gateway.address
}
output "private_ip" {
description = "Private IP address of the gateway on the tenant network"
value = openstack_compute_instance_v2.gateway.access_ip_v4
}
output "gateway_url" {
description = "URL for the gateway front door. HTTPS on the domain when set, otherwise HTTP on the floating IP."
value = var.domain != "" ? "https://${var.domain}" : "http://${openstack_networking_floatingip_v2.gateway.address}"
}
output "admin_hint" {
description = "How to retrieve the APISIX admin API key and reach the control plane"
value = "SSH to the instance and read /root/gateway-admin-credentials for the admin API key. Open the admin API with an SSH tunnel: ssh -L ${var.admin_port}:127.0.0.1:${var.admin_port} ubuntu@${openstack_networking_floatingip_v2.gateway.address}. etcd listens on 127.0.0.1:2379 only. Add rate limits, key auth, and routes through the Admin API or deployment walkthrough steps."
}
terraform {
required_version = ">= 1.6.0"
required_providers {
openstack = {
source = "terraform-provider-openstack/openstack"
version = "~> 2.0"
}
}
}
provider "openstack" {}
# Required: SSH keypair must already exist in your project
key_name = "YOUR_KEY_NAME"
# Private backends on the gateway subnet (no floating IPs). After apply, lock each
# backend security group to the gateway security group.
# upstream_services = [
# { name = "service-a", host = "10.42.0.10", port = 8080, uri = "/service-a/*" },
# { name = "service-b", host = "10.42.0.11", port = 8080, uri = "/service-b/*" },
# ]
# Recommended: set a domain and point its DNS A record at the floating IP from
# the outputs after you configure TLS on the gateway.
# domain = "api.example.com"
# apisix_version = "3.11.0-debian"
# admin_port = 9180
# flavor_name = "s1a.medium"
# image_name = "Ubuntu-24.04"
# app_name = "api-gateway"
# volume_size = 20
# external_network = "PublicStatic"
# private_cidr = "10.42.0.0/24"
#cloud-config
package_update: true
packages:
- ca-certificates
- curl
- jq
- openssl
write_files:
- path: /opt/gateway-bootstrap.sh
permissions: "0755"
content: |
#!/usr/bin/env bash
set -euo pipefail
CRED_FILE=/root/gateway-admin-credentials
STACK_DIR=/opt/gateway
CONFIG_DIR=/data/apisix
ETCD_DIR=/data/etcd
ADMIN_PORT=${admin_port}
UPSTREAM_JSON='${upstream_services}'
if [ -f "$CRED_FILE" ]; then
cd "$STACK_DIR"
docker compose up -d
exit 0
fi
mkdir -p "$CONFIG_DIR" "$ETCD_DIR" "$STACK_DIR"
ADMIN_KEY="$(openssl rand -hex 24)"
cat > "$CONFIG_DIR/config.yaml" <<APISIX_CONFIG
apisix:
node_listen:
- 80
enable_ipv6: false
enable_control: true
control:
ip: 127.0.0.1
port: 9090
deployment:
role: traditional
role_traditional:
config_provider: etcd
admin:
admin_key:
- name: admin
key: $ADMIN_KEY
role: admin
allow_admin:
- 127.0.0.0/8
admin_listen:
ip: 127.0.0.1
port: $ADMIN_PORT
etcd:
host:
- "http://127.0.0.1:2379"
prefix: "/apisix"
timeout: 30
APISIX_CONFIG
cat > "$STACK_DIR/docker-compose.yml" <<COMPOSE
services:
etcd:
image: bitnami/etcd:3.5.11
restart: unless-stopped
network_mode: host
environment:
- ETCD_ENABLE_V2=true
- ALLOW_NONE_AUTHENTICATION=yes
- ETCD_LISTEN_CLIENT_URLS=http://127.0.0.1:2379
- ETCD_ADVERTISE_CLIENT_URLS=http://127.0.0.1:2379
volumes:
- /data/etcd:/bitnami/etcd
apisix:
image: apache/apisix:${apisix_version}
restart: unless-stopped
network_mode: host
volumes:
- /data/apisix/config.yaml:/usr/local/apisix/conf/config.yaml:ro
depends_on:
- etcd
COMPOSE
cd "$STACK_DIR"
docker compose up -d
for i in $(seq 1 60); do
if curl -sf -o /dev/null -H "X-API-KEY: $ADMIN_KEY" "http://127.0.0.1:$ADMIN_PORT/apisix/admin/routes"; then
break
fi
sleep 2
done
echo "$UPSTREAM_JSON" | jq -c '.[]' | while read -r svc; do
NAME=$(echo "$svc" | jq -r '.name')
HOST=$(echo "$svc" | jq -r '.host')
PORT=$(echo "$svc" | jq -r '.port')
URI=$(echo "$svc" | jq -r '.uri')
curl -sf -X PUT "http://127.0.0.1:$ADMIN_PORT/apisix/admin/routes/$NAME" \
-H "X-API-KEY: $ADMIN_KEY" \
-H "Content-Type: application/json" \
-d "{\"uri\":\"$URI\",\"upstream\":{\"type\":\"roundrobin\",\"nodes\":{\"$HOST:$PORT\":1}}}"
done
umask 077
cat > "$CRED_FILE" <<CRED
APISIX admin API key (generated on first boot)
admin_key: $ADMIN_KEY
admin_url: http://127.0.0.1:$ADMIN_PORT (reach via SSH tunnel only)
gateway_url: ${domain != "" ? "https://${domain}" : "http://${floating_ip}"}
Use this key as the X-API-KEY header on Admin API calls. Mint consumer keys
and attach rate-limit or key-auth plugins through the Admin API.
CRED
chmod 600 "$CRED_FILE"
runcmd:
- |
set -e
DEV=/dev/sdb
for i in $(seq 1 30); do [ -b "$DEV" ] && break; sleep 5; done
if ! blkid "$DEV" >/dev/null 2>&1; then mkfs.ext4 -F -L gatewaydata "$DEV"; fi
mkdir -p /data
mount "$DEV" /data
grep -q "$DEV" /etc/fstab || echo "$DEV /data ext4 defaults,nofail 0 2" >> /etc/fstab
curl -fsSL https://get.docker.com | sh
systemctl enable --now docker
/opt/gateway-bootstrap.sh
# API gateway
Single compute instance running [Apache APISIX](https://apisix.apache.org), an open-source API gateway, on infrastructure you control. The gateway holds the public floating IP, routes HTTP traffic to private backend services, and exposes rate limiting, key auth, and request shaping through APISIX plugins.
**Network class:** production — `external_network` defaults to `PublicStatic` for persisted floating IPs and multi-tier stacks; override with `PublicEphemeral` for ephemeral demos.
The instance provisions a private network, a floating IP, and a block volume mounted at `/data` so APISIX config, etcd state, and logs live on a resizable volume rather than the boot disk. cloud-init installs Docker, starts etcd and APISIX, generates an admin API key on first boot, and seeds starter routes to your `upstream_services`.
## Where this fits
This template productizes the developer-facing API edge from the edge and perimeter program: a self-hosted API gateway on a VM you operate. It shares the same single-VM edge shape as the [edge reverse proxy](/resources/iac-templates/edge-reverse-proxy) and [edge WAF](/resources/iac-templates/edge-waf) templates but runs a dedicated gateway engine with an Admin API instead of plain TLS termination or L7 filtering.
For model routing to LLM backends, see the sibling [inference gateway](/resources/iac-templates/inference-gateway) template (same edge shape, different upstream).
This gateway runs in one region on a VM you operate. It is a regional API front door, not a global edge network: Quake AI has no anycast and no global PoPs.
## Prerequisites
- OpenTofu >= 1.6.0 or Terraform >= 1.6.0
- Quake AI account with OpenStack credentials
- An existing SSH keypair in your project (the value of `key_name` must match that keypair)
- Private backend instances on the gateway subnet (or plan to deploy them after apply)
## Resource baseline
APISIX plus etcd fit in 4 GiB for a modest API surface. The default `s1a.medium` flavor (2 shared vCPU, 4 GiB RAM) handles steady gateway traffic; raise the flavor when you front many routes or run high request concurrency. The boot disk is 20 GiB; gateway state lives on the separate data volume (`volume_size`, default 20 GiB).
## Usage
1. Clone or copy this template directory
2. Copy `terraform.tfvars.example` to `terraform.tfvars` and set `key_name` and `upstream_services` (and `domain` if you have one)
3. Source your OpenStack credentials: `source openrc.sh`
4. Initialize: `tofu init`
5. Preview: `tofu plan`
6. Apply: `tofu apply`
cloud-init takes several minutes on first boot to install Docker, mount the data volume, start etcd and APISIX, and seed starter routes. After boot, retrieve the admin API key and configure plugins (see Admin API below).
## Admin API and upstream routes
No admin credential ships in this repository. After apply:
1. SSH to the instance at `floating_ip`
2. Read `/root/gateway-admin-credentials` for the generated admin API key
3. Reach the Admin API through an SSH tunnel: `ssh -L 9180:127.0.0.1:9180 ubuntu@FLOATING_IP`, then call `http://127.0.0.1:9180/apisix/admin/...` with the `X-API-KEY` header
4. etcd listens on `127.0.0.1:2379` only; it is not exposed in the security group
5. Add rate limits, key auth, and additional routes through the Admin API
Lock each backend security group so it accepts traffic on the service port only from the gateway security group (`api-gateway-sg` by default).
## Alternate engines
This template leads with Apache APISIX (Apache-2.0, NGINX/LuaJIT + etcd, hot config reload, 100+ open plugins). [KrakenD](https://www.krakend.io) fits when you want a Go gateway with no database: it is stateless and driven by a declarative JSON config file, so it is the lightest option to operate. [Kong Gateway OSS](https://konghq.com/install) fits when you want the largest plugin ecosystem; it requires Postgres, which adds operational cost on top of the gateway VM. [Tyk OSS](https://tyk.io/docs/tyk-oss-api-gateway/) ships a dashboard and dev portal in the OSS edition. Swap the container stack in cloud-init if you prefer one of them.
## Outputs
| Output | Description |
| --- | --- |
| `floating_ip` | Public floating IP of the API gateway |
| `private_ip` | Private IP of the gateway on the tenant network |
| `gateway_url` | HTTPS on the domain when set, otherwise HTTP on the floating IP |
| `admin_hint` | Commands to retrieve the admin API key and open the Admin API |
| `instance_id` | Compute instance ID |
## Validation
This template passes `tofu validate` in CI. That check confirms the OpenTofu configuration is well-formed against the provider schema; it does not run `tofu apply` against a live account.
Resources, parameters, and variables
key_namerequiredflavor_name="s1a.medium"image_name="Ubuntu-24.04"app_name="api-gateway"apisix_version="3.11.0-debian"admin_port=9180domain=""upstream_services=[ { name = "service-a" host = "10.42.0.10" port = 8080 uri = "/service-a/*"volume_size=20external_network="PublicStatic"private_cidr="10.42.0.0/24"
Customize this pattern#
- Customize a template's image and flavor
- Add a block volume to a template
- Parameterize a template with a tfvars file
See also#
Usage Guidelines
The sample code, software libraries, command line tools, proofs of concept, templates, and other related technology on this page (including any of the foregoing that is provided by Quake AI personnel) is provided to you as Quake AI Content under the Quake AI Customer Agreement, or the relevant written agreement between you and Quake AI (whichever applies). Do not use this Quake AI Content in your production accounts, or on production or other critical data. You are responsible for testing, securing, and optimizing the Quake AI Content (such as sample code) as appropriate for production grade use based on your specific quality control practices and standards. Deploying Quake AI Content may incur Quake AI charges for creating or using Quake AI chargeable resources, such as running Compute instances or storing data in Object Storage. Your use is also subject to the Acceptable Use Policy.
For the full policy, see Usage Guidelines.
Last validated: 07.07.2026
See Also
Terraform and OpenTofu on Quake AI
Prerequisite
Networks
Prerequisite
Authoring IaC templates for Quake AI
Shares: Volumes, Security Groups
Deploy an API gateway with the api-gateway template
Shares: Volumes, Security Groups
Deploy a regional edge cache with the edge-cache template
Shares: Volumes, Security Groups