Run Benchmarks on AWS EKS
This guide walks you through reproducing the published AWS EKS benchmark results on your own AWS account. The setup uses three isolated EKS node groups — one each for API7 Gateway, the NGINX upstream, and the wrk2 load generator — so that none of the three components contends with the others for CPU or network resources.
For general benchmark methodology and optimization guidance, see Performance Benchmark first. The standard production install procedure for API7 Gateway on Kubernetes is covered in Deploy on Kubernetes; this page only documents the benchmark-specific deviations.
Prerequisites
- An AWS account with permission to create EKS clusters and EC2 instances.
kubectlandhelminstalled locally, at versions compatible with your target EKS cluster.- An EKS cluster service role and an EKS node IAM role. See Amazon EKS cluster IAM role and Amazon EKS node IAM role.
- A valid API7 Enterprise license. Request a trial if you do not already have one.
Step 1: Create the EKS cluster
In the AWS Console, create a new EKS cluster:
- Add cluster in the EKS console.
- Attach the EKS cluster service role you created in the prerequisites.
- Configure VPC and subnets so that the three node groups will be able to reach each other on the cluster network.
- Configure observability (CloudWatch logging) as you prefer.
- Enable the cluster add-ons you normally run (for example, VPC CNI and CoreDNS).
- Create the cluster and wait for it to reach the
ACTIVEstate.
Step 2: Create three isolated node groups
The benchmark requires three separate EC2 node groups running c5.4xlarge instances so that API7 Gateway, the upstream, and wrk2 each have 16 vCPUs and 32 GB RAM without contending with the others.
For each node group:
- Open Compute > Add node group in the EKS console.
- Give the node group a name (for example,
api7ee,upstream, orwrk2). - Attach the EKS node IAM role.
- Select Amazon Linux 2 (AL2_x86_64) as the AMI type.
- Select c5.4xlarge as the instance type.
- Set the desired size to at least 1 node.
- Enable SSH access if you want to run diagnostic commands on the node.
- Save and wait for the node group to reach
ACTIVE.
Repeat until you have three node groups: api7ee, upstream, and wrk2.
Do not use burstable instance families (such as t3 or t4g) for benchmarks. Their credit-based CPU model produces non-repeatable results. See Avoid burstable cloud instances.
Step 3: Connect kubectl and label the nodes
Configure kubectl to talk to the new cluster:
aws eks update-kubeconfig --region <region-code> --name <your-cluster-name>
Verify the connection:
kubectl get svc
Expected output (the kubernetes service is created automatically):
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.100.0.1 <none> 443/TCP 39m
Label one node in each node group so that Kubernetes can schedule pods onto them deterministically:
kubectl get nodes
kubectl label nodes <api7ee-node-name> nodeName=api7ee
kubectl label nodes <upstream-node-name> nodeName=upstream
kubectl label nodes <wrk2-node-name> nodeName=wrk2
Step 4: Install API7 Enterprise on the api7ee node
Create the namespace and install the API7 Enterprise control plane Helm chart, pinning every CP component (Dashboard, PostgreSQL, Prometheus) to the api7ee node.
kubectl create namespace api7
helm repo add api7 https://charts.api7.ai
helm repo update
helm install api7ee3 api7/api7ee3 \
--set nodeSelector."nodeName"=api7ee \
--set postgresql.primary.nodeSelector."nodeName"=api7ee \
--set prometheus.server.nodeSelector."nodeName"=api7ee \
-n api7
The bundled PostgreSQL and Prometheus enable persistent storage by default. If your cluster does not have a default StorageClass and you want to keep the benchmark environment stateless, add --set postgresql.primary.persistence.enabled=false and --set prometheus.server.persistence.enabled=false to the command above.
Wait for the control plane pods to be ready:
kubectl -n api7 get pods -l app.kubernetes.io/name=api7ee3 -w
Port-forward the Dashboard and upload your license:
kubectl -n api7 port-forward svc/api7ee3-dashboard 7443:7443
Open https://localhost:7443, log in with the default credentials (admin/admin), and upload the license from Organization > Settings > License.
In the Dashboard, go to Gateway Settings and set the Control Plane address to https://api7ee3-dp-manager:7943 so that the data plane you install next can connect to it.
Disable the global Prometheus plugin
The benchmark measures raw request-processing performance, so the global prometheus plugin should be disabled for the test run — otherwise every request incurs the Prometheus metric-recording cost. Disable the global prometheus plugin from the Dashboard before you start the benchmark.
Step 5: Install API7 Gateway on the api7ee node
In the Dashboard, click Add Gateway Instance and select Kubernetes to generate the install script. The dashboard generates three certificates (tls.crt, tls.key, ca.crt) and the Helm command you need to run.
Save the certificates and create the Kubernetes secret:
kubectl create secret generic -n api7 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
Install the gateway chart with the benchmark-specific overrides:
helm upgrade --install -n api7 --create-namespace api7-ee-3-gateway api7/gateway \
--set "etcd.auth.tls.enabled=true" \
--set "etcd.auth.tls.existingSecret=api7-ee-3-gateway-tls" \
--set "etcd.auth.tls.certFilename=tls.crt" \
--set "etcd.auth.tls.certKeyFilename=tls.key" \
--set "etcd.auth.tls.verify=true" \
--set "gateway.tls.existingCASecret=api7-ee-3-gateway-tls" \
--set "gateway.tls.certCAFilename=ca.crt" \
--set "apisix.extraEnvVars[0].name=API7_GATEWAY_GROUP_SHORT_ID" \
--set "apisix.extraEnvVars[0].value=default" \
--set "etcd.host[0]=https://api7ee3-dp-manager:7943" \
--set "apisix.replicaCount=1" \
--set "apisix.image.repository=api7/api7-ee-3-gateway" \
--set "apisix.image.tag=${GATEWAY_VERSION}" \
--set "nginx.workerProcesses=1" \
--set "apisix.nodeSelector.nodeName=api7ee" \
--set "apisix.securityContext.runAsNonRoot=false" \
--set "apisix.securityContext.runAsUser=0"
Benchmark-specific overrides compared with a normal production install:
| Override | Purpose |
|---|---|
apisix.replicaCount=1 | Measure single-instance throughput. Scale this up only after you have the single-instance number. |
nginx.workerProcesses=1 | Start with a single worker process for the baseline. Raise it later to measure multi-core scaling. |
apisix.nodeSelector.nodeName=api7ee | Pin the gateway pod to the isolated benchmark node. |
apisix.securityContext.runAsNonRoot=false, apisix.securityContext.runAsUser=0 | Run as root so that diagnostic tools (for example, perf, strace, tcpdump) can be installed inside the pod for profiling. Do not use these settings in production. |
apisix.image.tag=${GATEWAY_VERSION} | Pin the data plane image to the version that matches your control plane release. Look up the exact tag on Docker Hub or in the Supported Versions page. |
Step 6: Deploy the NGINX upstream on the upstream node
The upstream is a minimal NGINX server that returns a 200-byte static response. Access logging is disabled so that upstream disk I/O does not become the bottleneck.
Create nginx-upstream.yaml:
apiVersion: v1
kind: ConfigMap
metadata:
namespace: api7
name: nginx-config
data:
nginx.conf: |
master_process on;
worker_processes 1;
events {
worker_connections 4096;
}
http {
access_log off;
server_tokens off;
keepalive_requests 10000000;
server {
listen 1980;
server_name _;
location / {
return 200 "hello world\n";
}
}
}
---
apiVersion: apps/v1
kind: Deployment
metadata:
namespace: api7
name: nginx-deployment
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
nodeSelector:
nodeName: upstream
containers:
- name: nginx
image: nginx:1.25.4
volumeMounts:
- name: nginx-config
mountPath: /etc/nginx/nginx.conf
subPath: nginx.conf
volumes:
- name: nginx-config
configMap:
name: nginx-config
---
apiVersion: v1
kind: Service
metadata:
namespace: api7
name: nginx-upstream
spec:
selector:
app: nginx
ports:
- port: 1980
targetPort: 1980
Apply it:
kubectl apply -f nginx-upstream.yaml
Step 7: Deploy wrk2 on the wrk2 node
wrk2 is the load generator. Pin it to the wrk2 node so that it does not share CPU with either the gateway or the upstream.
Create wrk2.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
namespace: api7
name: wrk2-deployment
spec:
replicas: 1
selector:
matchLabels:
app: wrk2
template:
metadata:
labels:
app: wrk2
spec:
nodeSelector:
nodeName: wrk2
containers:
- name: wrk2
image: bootjp/wrk2
command: ["sleep", "infinity"]
Apply it:
kubectl apply -f wrk2.yaml
exec into the pod when you are ready to run the load generator:
kubectl -n api7 exec -it deploy/wrk2-deployment -- sh
Step 8: Apply the test scenario configurations
The public api7-gateway-performance-benchmark repository contains ready-to-use ADC configurations for every scenario in the benchmark table. Clone the repository and apply each scenario in sequence using the ADC CLI:
| Scenario | ADC config |
|---|---|
| One route without plugins | 1-one-route-without-plugin.yaml |
One route with limit-count | 2-one-route-with-limit-count.yaml |
One route with key-auth + limit-count | 3-one-route-with-key-auth-and-limit-count.yaml |
One route + one consumer with key-auth | 4-one-route-with-key-auth.yaml |
| 100 routes without plugins | 5-100-route-without-plugin.yaml |
100 routes with limit-count | 6-100-route-with-limit-count.yaml |
100 routes + 100 consumers with key-auth + limit-count | 7-100-route-and-consumer-with-key-auth-limit-count.yaml |
100 routes + 100 consumers with key-auth | 8-100-route-and-consumer-with-key-auth.yaml |
Apply a scenario with adc sync:
adc sync \
--server https://<dashboard-host>:7443 \
--token <your-api7-api-key> \
--gateway-group default \
-f 1-one-route-without-plugin.yaml
Step 9: Run the load test
From inside the wrk2 pod, run the load generator against the API7 Gateway service. Target the internal service DNS name so that traffic stays on the cluster network:
wrk -t4 -c100 -d120s -R300000 \
--latency \
http://api7-ee-3-gateway.api7.svc.cluster.local:9080/get
Flags reference:
| Flag | Meaning |
|---|---|
-t4 | 4 threads on the load generator |
-c100 | 100 concurrent connections |
-d120s | 2-minute run (matches the published methodology) |
-R300000 | Target request rate of 300,000 requests/second |
--latency | Emit latency percentiles |
Run each test case 5 times and average the results. Record QPS, P95, and P99 for each scenario, then compare against the published numbers in Performance Benchmark.
If your results deviate significantly (more than ~10%) from the published numbers, review the Optimization recommendations and verify that:
- Access logs are disabled on both API7 Gateway and the NGINX upstream.
ulimit -nis at least1024000on all three nodes.- The global
prometheusplugin is disabled. - No other pods are scheduled on the three benchmark nodes.
- Error logs for API7 Gateway are clean (no upstream DNS errors, no connection failures).
Clean up
Delete the test namespace when you are done:
kubectl delete namespace api7
Then delete the three node groups and the EKS cluster from the AWS Console to stop EC2 charges.
Next steps
- Performance Benchmark — methodology, published results, and optimization guidance.
- Scale the Data Plane — apply what you learned from benchmarking to a production scaling plan.
- Deploy on Kubernetes — the standard production install procedure.