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.
Before you begin
-
A Kubernetes 1.25 or later cluster — EKS, GKE, AKS or self-managed — and
kubectl1.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.0or later. -
Helm 3.10 or later. Check it with:
helm version --shortExpected:
v3.10.0or 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 -lExpected:
1or 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 addressSteps
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 updateThen 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 api7namespace/api7 createdWhat 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_DSNsecret/api7-database-dsn createdread 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.
# 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: ClusterIPpostgresql.builtin: falsedrops 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.extraEnvVarswithvalueFrom.secretKeyRefis 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 bothdeveloper_portal_configurationanddeveloper_portal.dashboard_service.type: ClusterIPkeeps the Dashboard off the network for the initial setup; step 4 reaches it withkubectl port-forward. Switch toLoadBalancer, 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:
postgresql:
builtin: true
primary:
persistence:
enabled: true
size: 10Gi
dashboard_service:
type: ClusterIPInstall the release:
helm install api7ee3 api7/api7ee3 -f cp-values.yaml -n api7STATUS: deployedHelm 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=900spod/api7ee3-dashboard-xxxxx-yyyyy condition met
pod/api7ee3-dp-manager-xxxxx-yyyyy condition met
pod/api7ee3-developer-portal-xxxxx-yyyyy condition metThe 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,7943For 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:7443Forwarding from 127.0.0.1:7443 -> 7443Open 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:7943What 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 api7secret/api7-ee-3-gateway-tls createdThe 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.
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.hostpoints the data plane at the DP Manager's mTLS Service inside the cluster.API7_GATEWAY_GROUP_SHORT_IDisdefaultonly for the default gateway group; any other group has its own short ID, which the Dashboard shows beside it.replicaCountandimage.tagare yours to choose. Pin an explicit gateway version that the control plane supports — neverlatest.
helm upgrade --install api7-ee-3-gateway api7/gateway \
--version "~3.10.0" \
-f dp-values.yaml \
-n api7STATUS: deployedBoth replicas should reach Running:
kubectl -n api7 get pods -l app.kubernetes.io/name=gatewayNAME 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:80Forwarding from [::1]:9080 -> 80kubectl 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-valuesSTATUS: deployedThe 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 -1HTTP/1.1 404 Not FoundThese 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
404The 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 api7The 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
| Symptom | Cause | Fix |
|---|---|---|
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. |
Related
- Configure the Control Plane — the settings to tune once the control plane is up, and where routes are created.
- Scale the Data Plane — add gateway instances to an existing group; High Availability for Data Plane covers health checks and failover.
- Helm Chart — every value both charts accept, and how a value becomes gateway
config.yaml. - System Requirements — the capacity baseline the CPU and memory values in step 6 sit on.
- Installation FAQ — the installation problems this table does not cover; Upgrade API7 Enterprise is the next change window on this deployment.