Skip to content
IaC Templates

API gateway

Template · Updated Jul 2026
Validated Jul 2026

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#

ParameterDescriptionDefault
key_nameSSH keypair name (must already exist)No default
flavor_nameInstance size (APISIX + etcd in 4 GiB)s1a.medium
image_nameOperating system imageUbuntu-24.04
app_nameDisplay name prefix for resourcesapi-gateway
apisix_versionAPISIX container image tag3.11.0-debian
admin_portAdmin API port (reach via SSH tunnel)9180
domainPublic hostname for the gateway; empty serves HTTP on the floating IP""
upstream_servicesList of {name, host, port, uri} starter routesservice-a at 10.42.0.10, service-b at 10.42.0.11
volume_sizeBlock volume size in GiB, mounted at /data20
external_networkExternal network for floating IP allocationPublicStatic
private_cidrCIDR for the private subnet10.42.0.0/24

Ports and access#

PortPurpose
22Host SSH for administration
80HTTP entry through the gateway data plane
443HTTPS entry when TLS is configured on the gateway
9180APISIX Admin API (not opened in the security group; use an SSH tunnel)
2379etcd (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.

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

Gateway

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

$33.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.medium

4 shared vCPU, 4 GiB RAM, 0.5 Gbps

$33.00

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

$3.20

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

Pricing data last validated: . For current rates, check quake.ai/pricing.

Template source#

This is a validated OpenTofu template.

7 files. Download the zip or expand to copy any file.Download api-gateway.zip
Show source (7 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           = "${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
}
variables.tfHCL
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"
}
outputs.tfHCL
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."
}
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: 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-init/gateway.yaml.tftpl
#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
README.mdMarkdown
# 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
Provisions
Parameterized by
Variables
  • key_namerequired
  • flavor_name="s1a.medium"
  • image_name="Ubuntu-24.04"
  • app_name="api-gateway"
  • apisix_version="3.11.0-debian"
  • admin_port=9180
  • domain=""
  • upstream_services=[ { name = "service-a" host = "10.42.0.10" port = 8080 uri = "/service-a/*"
  • volume_size=20
  • external_network="PublicStatic"
  • private_cidr="10.42.0.0/24"

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?