# Deploy an API gateway with the api-gateway template

Source: https://docs.quake.ai/resources/deployments/deploy-api-gateway-template
Markdown: https://docs.quake.ai/resources/deployments/deploy-api-gateway-template.md

---

# Deploy an API gateway with the api-gateway template

Stand up an [Apache APISIX](https://apisix.apache.org) API gateway on one Quake AI instance using the [validated OpenTofu template](/docs/platform/validation#how-infrastructure-templates-are-checked) `api-gateway`. You apply the template, launch two private backends on the same subnet, verify starter routes through the gateway floating IP, add rate limiting and key authentication, point a domain at the gateway, and lock each backend security group to the gateway.

The gateway holds the only public floating IP. The backend services stay on the private subnet with no addresses on the internet. You operate the gateway and backends yourself; this is a regional API front door you run, not a managed global edge gateway.

<Figure size="md" caption="What you'll build: APISIX on a gateway instance with a floating IP routes two private backend services with rate limiting and key auth at the edge">

```d2
direction: right

client: API client {shape: person}
fip: Floating IP\n80 / 443
gw: Gateway VM\nAPISIX {
  plugins: rate limit\nkey auth
}
svcA: Service A\n10.42.0.10
svcB: Service B\n10.42.0.11

client -> fip: HTTPS + apikey
fip -> gw.plugins
gw.plugins -> svcA: /service-a/*
gw.plugins -> svcB: /service-b/*
```

</Figure>

<PricingCompanion
  components={[
    { kind: "template", slug: "api-gateway", required: true },
    { kind: "primitive", required: true, label: "Private backend instances", vm: { flavor: "s1a.small", count: 2 } },
  ]}
/>

## Prerequisites

You need:

- OpenTofu 1.6.0 or later (or Terraform 1.6.0 or later) installed locally.
- Your OpenStack credentials sourced into the shell (`source openrc.sh`). See [the OpenStack CLI guide](/docs/tools/openstack-cli).
- An SSH keypair that already exists in your project. Record its name for the `key_name` variable.
- A copy of the `api-gateway` template directory from [the template reference page](/resources/iac-templates/api-gateway).
- A domain you can point at the gateway floating IP when you reach the TLS step.

## Step 1: Apply the gateway template

Copy the template's example variables file and set `key_name`. Keep the default `upstream_services` unless you plan different private addresses:

```bash
cp terraform.tfvars.example terraform.tfvars
```

```hcl
key_name = "YOUR_KEY_NAME"
```

Initialize, preview, and apply:

```bash
tofu init
tofu plan
tofu apply
```

OpenTofu provisions a private network, router, security group, data volume, gateway instance, and floating IP. cloud-init installs Docker, starts etcd on localhost, starts APISIX, generates an admin API key, and seeds starter routes to `10.42.0.10` and `10.42.0.11`.

Record the outputs:

```bash
tofu output floating_ip
tofu output private_ip
tofu output gateway_url
tofu output admin_hint
```

Wait three to five minutes for cloud-init to finish before you test the gateway.

## Step 2: Launch two private backends on the gateway network

The template creates network `api-gateway-net` and subnet `api-gateway-subnet`. Launch two small backend instances at `10.42.0.10` and `10.42.0.11` with no floating IPs.

1. Create a security group for the backends:

```bash
openstack security group create api-backends-sg \
  --description "Private API backends; ingress from gateway only after step 8"
```

Allow SSH from your workstation while you configure the backends:

```bash
openstack security group rule create \
  --protocol tcp --dst-port 22 --remote-ip YOUR_IP/32 api-backends-sg
```

2. Resolve the subnet ID:

```bash
SUBNET_ID=$(openstack subnet list -f value -c ID -c Name | awk '/api-gateway-subnet/ {print $1}')
```

3. Write cloud-init that serves distinct responses on port 8080 for each backend:

```bash
cat > backend-a-cloud-init.yaml <<'EOF'
#cloud-config
package_update: true
packages:
  - nginx
runcmd:
  - |
    echo 'service-a ok' > /var/www/html/index.html
    printf '%s\n' 'server {' '  listen 8080;' '  root /var/www/html;' '}' > /etc/nginx/sites-available/default
    systemctl restart nginx
EOF

cat > backend-b-cloud-init.yaml <<'EOF'
#cloud-config
package_update: true
packages:
  - nginx
runcmd:
  - |
    echo 'service-b ok' > /var/www/html/index.html
    printf '%s\n' 'server {' '  listen 8080;' '  root /var/www/html;' '}' > /etc/nginx/sites-available/default
    systemctl restart nginx
EOF
```

4. Create ports and launch the instances:

```bash
openstack port create \
  --network api-gateway-net \
  --fixed-ip subnet=$SUBNET_ID,ip-address=10.42.0.10 \
  --security-group api-backends-sg \
  service-a-port

openstack port create \
  --network api-gateway-net \
  --fixed-ip subnet=$SUBNET_ID,ip-address=10.42.0.11 \
  --security-group api-backends-sg \
  service-b-port

openstack server create \
  --flavor s1a.small \
  --image Ubuntu-24.04 \
  --key-name YOUR_KEY_NAME \
  --port service-a-port \
  --user-data backend-a-cloud-init.yaml \
  service-a

openstack server create \
  --flavor s1a.small \
  --image Ubuntu-24.04 \
  --key-name YOUR_KEY_NAME \
  --port service-b-port \
  --user-data backend-b-cloud-init.yaml \
  service-b
```

Wait until both instances report `ACTIVE`.

## Step 3: Verify starter routes through the gateway

From your workstation, request each seeded route over HTTP on the gateway floating IP:

```bash
curl -s http://YOUR_FLOATING_IP/service-a/
curl -s http://YOUR_FLOATING_IP/service-b/
```

The responses should show `service-a ok` and `service-b ok`. Traffic flows client to gateway floating IP to APISIX to the private backend.

If either route returns `502`, cloud-init may still be running on the gateway or a backend. Retry after a minute. SSH to the gateway and run `docker compose -f /opt/gateway/docker-compose.yml ps` to confirm APISIX and etcd are up.

## Step 4: Open the Admin API and read the admin key

Port 9180 is not open in the gateway security group. Open an SSH tunnel from your workstation:

```bash
ssh -L 9180:127.0.0.1:9180 ubuntu@YOUR_FLOATING_IP
```

In another terminal on the same workstation, read the admin key from the gateway host:

```bash
ssh ubuntu@YOUR_FLOATING_IP 'sudo grep admin_key /root/gateway-admin-credentials'
```

Export it for the Admin API calls in the next steps:

```bash
export ADMIN_KEY="PASTE_ADMIN_KEY_HERE"
```

Confirm the Admin API answers through the tunnel:

```bash
curl -s -H "X-API-KEY: $ADMIN_KEY" http://127.0.0.1:9180/apisix/admin/routes | head -c 200
```

## Step 5: Add key authentication

Create a consumer with an API key APISIX checks on every request:

```bash
curl -s -X PUT http://127.0.0.1:9180/apisix/admin/consumers/demo \
  -H "X-API-KEY: $ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "username": "demo",
    "plugins": {
      "key-auth": {
        "key": "demo-api-key"
      }
    }
  }'
```

Enable the `key-auth` plugin on both routes:

```bash
for ROUTE in service-a service-b; do
  curl -s -X PATCH "http://127.0.0.1:9180/apisix/admin/routes/$ROUTE" \
    -H "X-API-KEY: $ADMIN_KEY" \
    -H "Content-Type: application/json" \
    -d '{"plugins":{"key-auth":{}}}'
done
```

Verify an unauthenticated request is rejected:

```bash
curl -s -o /dev/null -w "%{http_code}\n" http://YOUR_FLOATING_IP/service-a/
```

The status code should be `401`. A request with the API key should pass:

```bash
curl -s -H "apikey: demo-api-key" http://YOUR_FLOATING_IP/service-a/
```

The response body should show `service-a ok`.

## Step 6: Add a rate limit

Attach APISIX `limit-count` to both routes. This example allows two requests per 60 seconds per client IP, then returns `429`:

```bash
for ROUTE in service-a service-b; do
  curl -s -X PATCH "http://127.0.0.1:9180/apisix/admin/routes/$ROUTE" \
    -H "X-API-KEY: $ADMIN_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "plugins": {
        "key-auth": {},
        "limit-count": {
          "count": 2,
          "time_window": 60,
          "rejected_code": 429,
          "key": "remote_addr"
        }
      }
    }'
done
```

Send three authenticated requests in quick succession:

```bash
for i in 1 2 3; do
  curl -s -o /dev/null -w "request $i: %{http_code}\n" \
    -H "apikey: demo-api-key" http://YOUR_FLOATING_IP/service-a/
done
```

The first two requests should return `200`; the third should return `429`.

## Step 7: Point a domain at the gateway

1. Create a DNS **A record** for your domain (for example `api.example.com`) pointing at `YOUR_FLOATING_IP`. Follow [How to point a domain at a Quake AI resource](/docs/network/how-to/point-domain-to-quake-ai). Wait until it resolves:

```bash
dig +short api.example.com
```

2. Terminate TLS at the gateway. The template serves HTTP on port 80 by default. For a production domain, add an SSL listener and certificate through the Admin API or place a TLS-terminating reverse proxy in front of APISIX. For this walkthrough, continue testing over HTTP on the floating IP until you configure HTTPS for your domain.

For certificate background, see [How to issue and auto-renew a TLS certificate with Let's Encrypt](/docs/network/how-to/lets-encrypt-certificate).

## Step 8: Lock the backend security groups to the gateway

Restrict each backend so it accepts application traffic only from the gateway security group `api-gateway-sg`:

```bash
openstack security group rule create \
  --protocol tcp \
  --dst-port 8080 \
  --remote-group api-gateway-sg \
  api-backends-sg
```

List the backend rules and confirm port 8080 allows only the gateway group:

```bash
openstack security group rule list api-backends-sg -f table
```

The backends have no floating IPs, so they are not reachable directly from the internet. Only the gateway can forward traffic to them on the private subnet.

Verify authenticated traffic still passes:

```bash
curl -s -H "apikey: demo-api-key" http://YOUR_FLOATING_IP/service-b/
```

The response should show `service-b ok`.

## What you built

- **Applied the `api-gateway` template** to provision a private network, gateway security group, data volume, APISIX host with etcd, and floating IP
- **Launched two private backends** at `10.42.0.10` and `10.42.0.11` with no public addresses
- **Verified starter routes** through the gateway floating IP
- **Added key authentication** so unauthenticated requests return `401`
- **Added rate limiting** so excess requests return `429`
- **Locked the backend security group** so application traffic accepts only the gateway as its source

## Scope of this deployment

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. For model routing to LLM backends instead of your own services, see the sibling [inference gateway deployment](/resources/deployments/deploy-inference-gateway-template).

The walkthrough adds two small backend instances; those VMs are not part of the OpenTofu template. Size the gateway with the template defaults; scale backends independently for your services.

## Next steps

- [API gateway template](/resources/iac-templates/api-gateway): parameters, ports, and resource map
- [Inference gateway deployment](/resources/deployments/deploy-inference-gateway-template): the LLM-routing sibling with the same edge shape
- [Edge WAF template](/resources/iac-templates/edge-waf): add L7 attack filtering in front of the same private-backend shape
- [Security hardening checklist](/docs/security/hardening-checklist): audit security groups and floating IP usage

## Clean up

When you no longer need the deployment, destroy the OpenTofu stack and delete the backend resources you created in step 2:

```bash
tofu destroy
openstack server delete service-a service-b
openstack port delete service-a-port service-b-port
openstack security group delete api-backends-sg
```

Remove any DNS A record you pointed at the gateway floating IP.
