Docs
API7 GatewayDeploy and upgradeInstallationDeploy on Kubernetes

Deploy on Kubernetes

Deploy API7 Enterprise on Kubernetes using Helm, including control plane setup, data plane configuration with mTLS, and cloud-specific guidance for AWS EKS, GCP GKE, and Azure AKS.

Applies to:API7 Gateway 3.10.x

Deployments:KubernetesRoles:Platform engineerTime:45 minutesImpact:No restart, no traffic change

Goal

An API7 Enterprise control plane and a data plane running in one Kubernetes cluster, connected over mTLS, with the gateway answering requests and its instances reporting Healthy in API7 Dashboard.

The control plane is the Dashboard, the DP Manager and a PostgreSQL database; the data plane is the API7 Gateway instances that carry API traffic. Creating routes on the gateway afterwards is Configure the Control Plane.

Data Plane

Control Plane

mTLS :7943

mTLS :7943

:9080/:9443

:9080/:9443

PostgreSQL

API7 CP
Dashboard + DP Manager

API7 Gateway 1

API7 Gateway 2

Client

Data Plane

Control Plane

mTLS :7943

mTLS :7943

:9080/:9443

:9080/:9443

PostgreSQL

API7 CP
Dashboard + DP Manager

API7 Gateway 1

API7 Gateway 2

Client

Before you begin

  • A Kubernetes 1.25 or later cluster — EKS, GKE, AKS or self-managed — and kubectl 1.25 or later configured against it. Check it with:

    kubectl version -o json | jq -r '.serverVersion.gitVersion, .clientVersion.gitVersion'

    Expected: two lines, both v1.25.0 or later.

  • Helm 3.10 or later. Check it with:

    helm version --short

    Expected: v3.10.0 or later.

  • A StorageClass the cluster can bind PersistentVolumeClaims against. PostgreSQL and Prometheus enable persistent storage by default, so without one their PVCs stay pending. Check it with:

    kubectl get storageclass -o name | wc -l

    Expected: 1 or more. With none, either configure one or disable persistence — which is not a production option.

  • PostgreSQL 12 or later, and an API7 Enterprise license. An external database is what a production deployment uses; get a trial license if you do not have one yet.

Set the variables this guide uses. The gateway's own origin is the only one needed before the Dashboard exists:

export GATEWAY_URL="http://127.0.0.1:9080"   # kubectl port-forward, or your Service's address

Steps

Seven steps, in two halves: steps 1 to 4 stand the control plane up and activate the license, and steps 5 to 7 attach a data plane to it over mTLS and put the gateway in front of traffic. Both halves install into one namespace, api7.

Step 1: Add the Helm repository and create the namespace

Both charts come from the same repository: api7ee3 is the control plane and gateway is the data plane.

helm repo add api7 https://charts.api7.ai
helm repo update

Then list what the repository serves. Both charts have to be there before anything else works:

helm search repo api7/
NAME            CHART VERSION   APP VERSION     DESCRIPTION
api7/api7ee3    …               …               …
api7/gateway    …               …               …

The two rows are what matters; the columns are whatever the repository is serving today. api7/api7ee3 deploys the Dashboard, the Admin API, the DP Manager, the Developer Portal and, optionally, a bundled PostgreSQL — Installation Packages lists what each chart contains. Then create the namespace both halves install into:

kubectl create namespace api7
namespace/api7 created

What changed: the two charts are resolvable by name and an empty namespace exists.

Step 2: Provision PostgreSQL and store its DSN in a Secret

A production deployment uses an external PostgreSQL — Amazon RDS, Cloud SQL, Azure Database for PostgreSQL or your own instance — for durability and failover. Create the database and the role it will connect as:

CREATE DATABASE api7ee;
CREATE ROLE api7ee LOGIN;
GRANT ALL PRIVILEGES ON DATABASE api7ee TO api7ee;

Set the role's password with psql's own \password api7ee, which prompts for it instead of putting it in the statement you copied. Then put the whole connection string — postgres://api7ee:<password>@<host>:5432/api7ee, with your own password and host — into a Kubernetes Secret in the namespace the control plane will run in, so that no Helm value and no ConfigMap ever holds it:

read -rsp "Database DSN: " DATABASE_DSN && echo

kubectl create secret generic api7-database-dsn \
  --from-literal=DATABASE_DSN="${DATABASE_DSN}" \
  -n api7

unset DATABASE_DSN
secret/api7-database-dsn created

read does not write the DSN to your shell history, and unset drops it from the shell as soon as the Secret exists.

Kubernetes Secret data is Base64-encoded by default, not encrypted. In production, restrict RBAC access to Secrets and pod exec, enable encryption at rest for Kubernetes Secrets, and consider an external secret manager through a solution such as External Secrets Operator or Secrets Store CSI Driver. Do not commit a manifest containing the plaintext DSN to source control.

What changed: the database exists and the cluster holds its connection string under one key, DATABASE_DSN, in one Secret.

Step 3: Install the control plane

The three control plane components read the DSN from the environment rather than from their configuration, so each one references the same Secret key and each configuration file names the variable instead of the value.

cp-values.yaml
# Disable the built-in PostgreSQL chart and point the components at your external database.
postgresql:
  builtin: false

dashboard_configuration:
  database:
    dsn: '${DATABASE_DSN}'

dp_manager_configuration:
  database:
    dsn: '${DATABASE_DSN}'

developer_portal_configuration:
  database:
    dsn: '${DATABASE_DSN}'

dashboard:
  extraEnvVars:
    - name: DATABASE_DSN
      valueFrom:
        secretKeyRef:
          name: api7-database-dsn
          key: DATABASE_DSN

dp_manager:
  extraEnvVars:
    - name: DATABASE_DSN
      valueFrom:
        secretKeyRef:
          name: api7-database-dsn
          key: DATABASE_DSN

developer_portal:
  extraEnvVars:
    - name: DATABASE_DSN
      valueFrom:
        secretKeyRef:
          name: api7-database-dsn
          key: DATABASE_DSN

dashboard_service:
  type: ClusterIP
  • postgresql.builtin: false drops the built-in PostgreSQL chart, which is what an external database replaces.
  • dsn: '${DATABASE_DSN}' is the reference each component resolves at startup. The rendered ConfigMaps carry this string, not the DSN.
  • extraEnvVars with valueFrom.secretKeyRef is what puts the value in the process environment. The Secret has to be in the same namespace as the release. With the Developer Portal disabled, leave out both developer_portal_configuration and developer_portal.
  • dashboard_service.type: ClusterIP keeps the Dashboard off the network for the initial setup; step 4 reaches it with kubectl port-forward. Switch to LoadBalancer, or put an ingress in front of it, for permanent access.

Quick Evaluation Setup

For a quick evaluation with the built-in PostgreSQL — not for production — skip step 2 and install with these values instead. postgresql.builtin: true enables the built-in chart, and the Dashboard stays on ClusterIP behind kubectl port-forward:

cp-values-eval.yaml
postgresql:
  builtin: true
  primary:
    persistence:
      enabled: true
      size: 10Gi

dashboard_service:
  type: ClusterIP

Install the release:

helm install api7ee3 api7/api7ee3 -f cp-values.yaml -n api7
STATUS: deployed

Helm returns as soon as the objects are created, so wait for the pods rather than watching them:

kubectl -n api7 wait \
  --for=condition=Ready pod \
  -l app.kubernetes.io/name=api7ee3 \
  --timeout=900s
pod/api7ee3-dashboard-xxxxx-yyyyy condition met
pod/api7ee3-dp-manager-xxxxx-yyyyy condition met
pod/api7ee3-developer-portal-xxxxx-yyyyy condition met

The release name and the chart name are both api7ee3, matching the chart's own Chart.yaml. Any release name works, but the Helm selectors in the commands below assume this one. kubectl get pods -n api7 -w shows the same pods while their images are still being pulled, and with the built-in PostgreSQL a fourth pod, api7-postgresql-0, appears beside them.

The three Services the rest of this guide depends on:

kubectl -n api7 get svc -l app.kubernetes.io/name=api7ee3 \
  -o custom-columns='NAME:.metadata.name,PORTS:.spec.ports[*].port'
NAME                       PORTS
api7ee3-dashboard          7080,7443
api7ee3-developer-portal   4321
api7ee3-dp-manager         7900,7943

For the purpose of each port, see Ports and Endpoints.

What changed: the control plane is running and reachable inside the cluster, and nothing outside the cluster can reach it yet.

Step 4: Activate the license and set the DP Manager address

Forward the Dashboard port, then finish the first-run sequence in the browser.

kubectl -n api7 port-forward svc/api7ee3-dashboard 7443:7443
Forwarding from 127.0.0.1:7443 -> 7443

Open https://localhost:7443 and log in with the default credentials (admin / admin). The Dashboard makes you reset the password on first login and then opens the Activate License screen, where you upload and activate the license. Then, in Gateway Settings, set DP Manager Address to the DP Manager's in-cluster mTLS address — this is the address gateway instances will connect to, so it has to be the Service DNS name and not localhost:

https://api7ee3-dp-manager:7943

What changed: the control plane is licensed and knows the address it will hand to every data plane instance.

Step 5: Create the data plane's mTLS Secret

In API7 Dashboard, select a gateway group such as default, open Gateway Instances, click Add Gateway Instance, choose Kubernetes, fill in the target namespace and generate the deployment script. The script contains a TLS certificate (tls.crt) and private key (tls.key) for the data plane, a CA certificate (ca.crt) that verifies the control plane, a kubectl create secret command for the three, and a complete Helm command for the data plane.

Save the three files out of the generated script, then create the Secret:

kubectl create secret generic api7-ee-3-gateway-tls \
  --from-file=tls.crt=/tmp/tls.crt \
  --from-file=tls.key=/tmp/tls.key \
  --from-file=ca.crt=/tmp/ca.crt \
  -n api7
secret/api7-ee-3-gateway-tls created

The certificates are unique per gateway group. If you have multiple gateway groups, each group needs its own set of certificates and a separate Kubernetes secret.

What changed: the cluster holds the client certificate a data plane needs to authenticate to the DP Manager, and the CA it needs to trust it back.

Step 6: Install the data plane

Use the Helm command the Dashboard generated, or put the generated settings into a values file of your own. The certificates come from the Secret in step 5, and the DP Manager address is the one you set in step 4.

dp-values.yaml
etcd:
  auth:
    tls:
      enabled: true
      existingSecret: api7-ee-3-gateway-tls
      certFilename: tls.crt
      certKeyFilename: tls.key
      verify: true
  host:
    - https://api7ee3-dp-manager:7943

gateway:
  tls:
    existingCASecret: api7-ee-3-gateway-tls
    certCAFilename: ca.crt

apisix:
  extraEnvVars:
    - name: API7_GATEWAY_GROUP_SHORT_ID
      value: default
  replicaCount: 2
  image:
    repository: api7/api7-ee-3-gateway
    tag: "3.10.7"
  • etcd.host points the data plane at the DP Manager's mTLS Service inside the cluster.
  • API7_GATEWAY_GROUP_SHORT_ID is default only for the default gateway group; any other group has its own short ID, which the Dashboard shows beside it.
  • replicaCount and image.tag are yours to choose. Pin an explicit gateway version that the control plane supports — never latest.
helm upgrade --install api7-ee-3-gateway api7/gateway \
  --version "~3.10.0" \
  -f dp-values.yaml \
  -n api7
STATUS: deployed

Both replicas should reach Running:

kubectl -n api7 get pods -l app.kubernetes.io/name=gateway
NAME                            READY   STATUS    RESTARTS   AGE
api7-ee-3-gateway-xxxxx-yyyyy   1/1     Running   0          1m
api7-ee-3-gateway-xxxxx-zzzzz   1/1     Running   0          1m

--version "~3.10.0" keeps the chart inside the API7 Gateway 3.10.x line while allowing compatible chart patches. Every value the chart accepts is in Helm Chart.

What changed: two gateway instances are running, authenticated to the control plane, and serving whatever configuration their gateway group holds.

For production sizing, autoscaling, anti-affinity, and cloud-provider load-balancer details, see Kubernetes Production Tuning.

Step 7: Expose the gateway

The gateway chart creates a NodePort Service by default, which is reachable through any node IP and is enough to verify the deployment from your own machine:

kubectl -n api7 port-forward svc/api7-ee-3-gateway-gateway 9080:80
Forwarding from [::1]:9080 -> 80

kubectl prints one Forwarding from … line per local address it binds. On a cloud provider, switch the Service to LoadBalancer instead, and read the address it provisions:

helm upgrade api7-ee-3-gateway api7/gateway \
  --set "gateway.type=LoadBalancer" \
  -n api7 --reuse-values
STATUS: deployed

The provider then takes a minute or two to attach an address:

kubectl -n api7 get svc api7-ee-3-gateway-gateway \
  -o jsonpath='{.status.loadBalancer.ingress[0].ip}'
203.0.113.10

--reuse-values keeps every value from step 6 and changes only the Service type, so the upgrade replaces the Service without touching the gateway pods. A third option is an ingress resource in front of the gateway Service, if you already run a general-purpose ingress controller such as NGINX Ingress or AWS ALB Controller. That puts a third-party proxy in front of API7 Gateway and is a different thing from API7 Ingress Controller (opens in Ingress Controller docs), which configures API7 Gateway from Kubernetes resources.

What changed: the gateway is reachable from outside the cluster, at a node port, a load balancer address or through your own ingress.

For provider-specific Service annotations, node-group sizing, and required add-ons for EKS, GKE, and AKS, see Kubernetes Production Tuning.

Validate

Send one request to the gateway from outside the cluster. With no routes configured it answers 404, which is what proves the gateway itself is up and accepting traffic:

curl -si "${GATEWAY_URL}/" | head -1
HTTP/1.1 404 Not Found

These three assertions are the whole deployment in a form a smoke test can run: a ready gateway pod, a Secret holding all three certificate files, and that 404.

kubectl -n api7 wait \
  --for=condition=Ready pod \
  -l app.kubernetes.io/name=gateway \
  --timeout=600s

kubectl -n api7 get secret api7-ee-3-gateway-tls -o json | jq -r '.data | keys[]'

curl -so /dev/null -w '%{http_code}\n' "${GATEWAY_URL}/"
pod/api7-ee-3-gateway-xxxxx-yyyyy condition met
ca.crt
tls.crt
tls.key
404

The Gateway Groups page in API7 Dashboard now lists the data plane instances as Healthy. If they do not appear, the Troubleshoot table below starts with the reason they usually do not.

Roll back / Clean up

Remove the two releases in reverse order, then the Secrets, then the namespace.

helm -n api7 uninstall api7-ee-3-gateway
helm -n api7 uninstall api7ee3
kubectl -n api7 delete secret api7-ee-3-gateway-tls api7-database-dsn
kubectl delete namespace api7

The first command deletes the gateway pods, so every request through the gateway fails from that moment; the second takes the Dashboard, the DP Manager and the Developer Portal down with it. Deleting the namespace also deletes the built-in PostgreSQL PersistentVolumeClaim, and with it the data; an external PostgreSQL database is untouched by all four commands. To roll back only a values change, helm -n api7 rollback api7-ee-3-gateway returns the release to its previous revision.

Troubleshoot

SymptomCauseFix
api7ee3-dp-manager or api7ee3-developer-portal enters CrashLoopBackOff shortly after step 3, and its logs show database connection refused.With the built-in PostgreSQL chart, the control plane components can start before PostgreSQL has finished pulling its image and accepting connections. This state clears on its own.Wait for api7-postgresql-0 to reach 1/1 Running — kubectl logs -n api7 statefulset/api7-postgresql --tail=100 shows how far it is — then re-run the kubectl -n api7 wait command from step 3. If the components keep restarting after PostgreSQL is ready, inspect the pod events and the DSN.
Control plane or data plane pods stay in Pending.The cluster has no room for them, or there is no StorageClass to bind the PostgreSQL and Prometheus PVCs against.kubectl -n api7 describe pod -l app.kubernetes.io/name=api7ee3 and kubectl get events -n api7 --sort-by=.lastTimestamp name which of the two it is; kubectl get storageclass settles the PVC case.
Data plane pods are Running but their logs show connection refused or certificate verify failed, and no instance appears in the Dashboard's gateway group.The mTLS Secret is missing a file or holds a certificate for a different gateway group, or the DP Manager address the pods dial does not resolve from inside the cluster.Check all three keys with kubectl -n api7 get secret api7-ee-3-gateway-tls -o json | jq -r '.data | keys[]', and read the gateway's own account of what happened with kubectl logs -n api7 -l app.kubernetes.io/name=gateway --tail=100. The gateway image ships without curl, wget, or nslookup, so run any DNS or TCP check from a throwaway debug container attached to the pod's network namespace instead: kubectl -n api7 debug -it deploy/api7-ee-3-gateway --image=nicolaka/netshoot -- sh, then nslookup api7ee3-dp-manager and curl -sk --connect-timeout 5 "https://api7ee3-dp-manager:7943/" -o /dev/null -w '%{http_code}' from inside it. A TLS handshake error at that address is the expected answer and proves DNS and network reachability.
The Dashboard does not answer after kubectl port-forward, or on its LoadBalancer address.The Dashboard pod is not ready, its Service has no endpoints, or port 7443 is closed by a network policy or a security group.kubectl get pods -n api7 -l app.kubernetes.io/component=dashboard and kubectl get endpoints -n api7 api7ee3-dashboard cover the first two; the third is your cluster's network rules.