This guide walks you through deploying a fully working API management platform locally using:
WSO2 API Manager (APIM) — Control Plane
WSO2 API Platform for Kubernetes (APK) — Kubernetes-native API Gateway
Minikube — local Kubernetes cluster
In this tutorial, you'll deploy:
1. apk-wso2-apk-adapter Purpose: The "Bridge" / XDS Server. Details: This component acts as the translator between the Management Plane (API Manager / Control Plane) and the Data Plane (Gateway). It fetches API definitions, subscriptions, and policies from the Control Plane and converts them into Kubernetes Custom Resources (CRs). It also serves as the xDS server, pushing dynamic configurations to the Envoy-based gateway. 2. apk-wso2-apk-gateway-runtime (Envoy + Enforcer) Purpose: The Traffic Handler (Data Plane). Details: This is the core ingress gateway that processes actual API traffic. Envoy: A high-performance proxy that intercepts incoming requests, handles routing, and performs SSL termination. Enforcer: A filter/sidecar that applies "API Management" logic to the traffic. It performs authentication (OAuth2, JWT), authorization, and enforces policies (e.g., rate limiting) before allowing Envoy to forward the request to your backend. Note: In newer APK versions, Enforcer logic is increasingly implemented as native Envoy filters (Golang-based) for performance. 3. apk-wso2-apk-common-controller Purpose: The K8s Resource Reconciler. Details: This is a Kubernetes controller that watches for changes in specific APK Custom Resources (CRDs) across the cluster (or specific namespaces). It creates and manages the lifecycle of dependent child resources required for an API to function, ensuring the cluster state matches the desired state defined in your API CRs. It simplifies multi-namespace API management. 4. apk-wso2-apk-config-ds (Config Domain Service) Purpose: The Configuration Generator. Details: "DS" stands for Domain Service. This component is responsible for generating the actual Kubernetes resources (like HTTPRoute, Service, Gateway, etc.) based on abstract API definitions. When you deploy an API, the Adapter often relies on this service to help generate the complex K8s YAML/JSON structures required to configure the gateway. 5. apk-wso2-apk-idp Purpose: Internal Identity Provider / Key Manager. Details: This acts as a lightweight internal Identity Provider (IDP). It is responsible for minting and validating authentication tokens (like JWTs) for the system's internal use or for testing APIs. In a full production environment, this might be replaced or integrated with an external Identity Provider (like WSO2 Identity Server, Okta, or Keycloak), but apk-idp allows the system to function standalone. 6. redis Purpose: Shared State Store (Rate Limiting & Caching). Details: Redis is used as a fast, in-memory data store for: Rate Limiting: Storing counters to track how many requests a user has made (e.g., "10 requests per minute"). This allows multiple gateway replicas to enforce limits accurately. Token Caching: Storing validated security tokens to reduce the overhead of validating them against the IDP for every single request. 7. cert-manager Purpose: Certificate Lifecycle Management. Details: This is a standard Cloud Native Computing Foundation (CNCF) tool used to automate the management of TLS certificates. In WSO2 APK, it:
- Issues certificates for the API Gateways (enabling HTTPS).
- Manages internal mTLS certificates for secure communication between APK components.
- Automatically renews certificates before they expire.
This post is based on a real, complete setup — not a theoretical guide.
Architecture Summary
Control Flow: Developer → Control Plane (APIM) → Adapter → Common Controller / Config DS → K8s API Server.
Traffic Flow: User → Cert-Manager (TLS) → Gateway Runtime (Envoy) → Redis (Check Limits) → Backend.
🧰 Prerequisites
Minikube (v1.30+)
kubectl
Docker
4+ CPUs, 8–16GB RAM
Linux/macOS/WSL recommended
🟦 Step 1 — Start Minikube
minikube start --cpus=4 --memory=8192
Set namespace:
kubectl create namespace apk
kubectl config set-context --current --namespace=apk
🟩 Step 2 — Enable Ingress
APK requires ingress for hostname-based routing.
minikube addons enable ingress
minikube addons enable metallb
Verify:
$ kubectl get pods -n ingress-nginx
NAME READY STATUS RESTARTS AGE
ingress-nginx-controller-67c5cb88f-vff6x 1/1 Running 0 18h
🟪 Step 3 — Install APIM Control Plane + APK Agent
Again directly from the Quick Start With Control Plane: apk.docs.wso2.com
# Add APK Helm repo
helm repo add wso2apk https://github.com/wso2/apk/releases/download/1.3.0-1
helm repo update
# Install Kubernetes Gateway data plane
helm install apk wso2apk/apk-helm \
--version 1.3.0-1 \
-f https://raw.githubusercontent.com/wso2/apk/refs/tags/1.3.0-1/helm-charts/samples/apk/1.3.0-1-cp-enabled-values.yaml \
-n apk
NAME: apk
LAST DEPLOYED: Thu Nov 27 09:49:57 2025
NAMESPACE: apk
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
Welcome to the WSO2 API Platform for Kubernetes!
Congratulations. You've successfully deployed WSO2 APK using Helm, you'll need to monitor and manage the deployment to ensure everything is running smoothly.
- Monitor Pods:
Check the status of the pods to ensure they are up and running:
---
kubectl get pods
---
- Monitor Services:
Verify that the services are running and find their external IPs to access the APIs:
---
kubectl get services
---
For more detailed information, troubleshooting, and advanced configurations, we encourage you to explore the official WSO2 documentation.
- APK Documentation: [https://apk.docs.wso2.com/en/latest/get-started/quick-start-guide/]
This is just the beginning of your APK journey. Feel free to customize and tailor your deployment to match your organization's specific needs.
For any questions or assistance, don't hesitate to reach out to our discord channel.
Happy API management with WSO2 APK!
$ kubectl get pod
NAME READY STATUS RESTARTS AGE
apim-wso2am-acp-deployment-1-7f868d47b8-hbl7l 1/1 Running 0 4h13m
🟧 Step 4 — Install WSO2 APK Gateway
Add repo:
# Helm repo for the APIM–APK agent
helm repo add wso2apkagent https://github.com/wso2/product-apim-tooling/releases/download/1.3.0
helm repo update
Install APK:
# Install the agent
helm install apim-apk-agent wso2apkagent/apim-apk-agent \
--version 1.3.0 \
-f https://raw.githubusercontent.com/wso2/apk/main/helm-charts/samples/apim-apk-agent/1.3.0-values.yaml \
-n apk
Verify:
kubectl get pods -n apk
Expected components:
apk-wso2-apk-adapter
apk-wso2-apk-gateway-runtime (Envoy + enforcer)
apk-wso2-apk-common-controller
apk-wso2-apk-config-ds
apk-wso2-apk-idp
redis
cert-manager
APK gateway is now running.
🟫 Step 5 — Configure Local Hostnames
Find Minikube IP:
minikube ip
http://192.168.58.2:30349
$ kubectl get httproutes -n apk
Add these to /etc/hosts:
192.168.58.2 am.wso2.com
192.168.58.2 api.am.wso2.com
192.168.58.2 idp.am.wso2.com
These hostnames are used by APIM and APK.
🟩 Step 6 — Access APIM
✔ Disable HSTS for that domain (Chrome advanced)
⚠ Not recommended
⚠ Works only if Chrome does NOT preload HSTS for that domain
(am.wso2.com is in preload → so this won’t work)
But for future reference:
chrome://net-internals/#hsts
Delete domain under “Delete domain security policies”.
Publisher:
https://am.wso2.com/publisher
Devportal:
https://am.wso2.com/devportal
Login:
admin / admin
🟥 Step 7 — Create & Deploy an API (EchoAPI)
1. Create API in Publisher
Publisher → Create API → REST API
Name: EchoAPI
Context: /echo
Version: 1.0.0
Backend URL: https://httpbin.org/anything
Click Create.
2. Deploy to APK
Go to Deployments → Deploy New Revision
Select Kubernetes Gateway environment.
3. Publish API
Save this generated/default key
Go to Lifecycle → Publish.
🟦 Step 8 — Verify Deployment in APK
kubectl get httproutes -n apk
You should see:
That means APIM → APK sync is working.
Make sure DNS is fine
Include gateway host entry in /etc/hosts.
192.168.58.2 default.gw.wso2.com
🟨 Step 9 — How to call the API from host (externally)
minikube service apk-wso2-apk-gateway-service -n apk --url
Example:
http://192.168.58.2:32513
🔹 Inside the pod → port 9095 is HTTPS
🔹 NodePort 32513 also expects HTTPS
Check logs for the gateway
kubectl logs deploy/apk-wso2-apk-gateway-runtime-deployment -n apk -c router | tail -n 40
kubectl logs deploy/apk-wso2-apk-gateway-runtime-deployment -n apk -c enforcer | tail -n 40
Call the gateway with HTTPS + hostname
Use the hostname in the URL so SNI matches:
# Internal key from Devportal
TOKEN="<your_internal_key_here>"
curl -v -k "https://default.gw.wso2.com:32513/echo/1.0.0" \
-H "Internal-Key: $TOKEN"
Notes:
https:// because the listener is HTTPS.
default.gw.wso2.com in URL so TLS SNI matches the cert/vHost.
-k to ignore self-signed cert.
Use the Internal-Key header because your JWT has "token_type":"InternalKey".
If instead you generate a Production OAuth token in Devportal, then:
OAUTH_TOKEN="<your_oauth_access_token>"
curl -v -k "https://default.gw.wso2.com:32513/echo/1.0.0" \
-H "Authorization: Bearer $OAUTH_TOKEN"
You should see a 200 OK and the JSON echoed from https://httpbin.org/anything.
🟩 Step 10 Why this will work (quick recap)
Service: apk-wso2-apk-gateway-service
→ 9095:32513/TCP (HTTPS_9095_listener in Envoy)NodePort: 32513 speaks HTTPS, not HTTP.
httproutes show your API is bound to default.gw.wso2.com.
Router/enforcer logs show they eventually connect to the control-plane and load config successfully.
So the only missing piece was:
use HTTPS + correct hostname + correct auth header.
$ curl -v -k "https://default.gw.wso2.com:32513/echo/1.0.0" \
-H "Internal-Key: $TOKEN"
* Trying 192.168.58.2:32513...
* Connected to default.gw.wso2.com (192.168.58.2) port 32513 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* TLSv1.0 (OUT), TLS header, Certificate Status (22):
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS header, Certificate Status (22):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS header, Finished (20):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.2 (OUT), TLS header, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* ALPN, server accepted to use h2
* Server certificate:
* subject: [NONE]
* start date: Nov 27 09:34:55 2025 GMT
* expire date: Feb 25 09:34:55 2026 GMT
* issuer: CN=apk
* SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway.
* Using HTTP2, server supports multiplexing
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* Using Stream ID: 1 (easy handle 0x5d7a71ba39f0)
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
> GET /echo/1.0.0 HTTP/2
> Host: default.gw.wso2.com:32513
> user-agent: curl/7.81.0
> accept: */*
> internal-key: eyJraWQiOiJnYXRld2F5X2NlcnRpZmljYXRlX2FsaWFzIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJhZG1pbkBjYXJib24uc3VwZXIiLCJhdWQiOiI0OTczZWU4ZC01ZWZlLTQ5NWItYjhiOC01ZDY3ZjU2Yzk4NjIiLCJpc3MiOiJodHRwOi8vYW0ud3NvMi5jb206NDQzL3Rva2VuIiwia2V5dHlwZSI6IlBST0RVQ1RJT04iLCJzdWJzY3JpYmVkQVBJcyI6W3sibmFtZSI6IkVjaG9BUEkiLCJjb250ZXh0IjoiL2VjaG8vMS4wLjAiLCJ2ZXJzaW9uIjoiMS4wLjAiLCJwdWJsaXNoZXIiOiJhZG1pbiIsInN1YnNjcmlwdGlvblRpZXIiOm51bGwsInN1YnNjcmliZXJUZW5hbnREb21haW4iOm51bGx9XSwiZXhwIjoxNzY0Mjk4MTIwLCJ0b2tlbl90eXBlIjoiSW50ZXJuYWxLZXkiLCJpYXQiOjE3NjQyMzgxMjAsImp0aSI6IjkxOTlmNjk2LTVmMzctNGMyNi1hZDIxLTlkZjE3MmI3MTRmMiJ9.mJ6_qwcu6oX5YMpuB7aUeoAl9P-q3yPr_0YukbWq_hTBd2mxvn8jNfUDbCig19DyX8UQ4O5cUfPV3-Id2_Q23K0rdCqCoNKZGX-AsYU54eI-hPjPWvrz3UIlrsiMmMu_fpWdbOE8WXbKRhRE44JRVY5W8NWLAlunZf5Z9bWni0qZbdBhqszJ55OWDRgtmIoxE1TVW_9wt-eYsY-g5yk8uyTdV0oICFbkcMKPN_MPkMsibciP88nRrXaniXGZDE8xVtMSbaKt9NYaRgttveoA4Wru8UKTAku3e6rOJNavRTsMh1lUIn7h39A53UtkmoEn-LOraHqjavA9ObkBatiYpQ
>
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* old SSL session ID is stale, removing
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* Connection state changed (MAX_CONCURRENT_STREAMS == 2147483647)!
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
< HTTP/2 200
< date: Thu, 27 Nov 2025 10:45:02 GMT
< content-type: application/json
< content-length: 341
< server: envoy
< access-control-allow-origin: *
< access-control-allow-credentials: true
< vary: Accept-Encoding
<
{
"args": {},
"data": "",
"files": {},
"form": {},
"headers": {
"Accept": "*/*",
"Host": "httpbin.org",
"User-Agent": "curl/7.81.0",
"X-Amzn-Trace-Id": "Root=1-69282bad-019b76fc70de68d24dc4234b"
},
"json": null,
"method": "GET",
"origin": "92.244.7.43",
"url": "https://httpbin.org/anything"
}
* Connection #0 to host default.gw.wso2.com left intact
🎉 Conclusion
You now have a fully working:
WSO2 API Manager control plane
WSO2 APK gateway (Envoy)
API deployment + publishing
HTTPS API invocation via NodePort
End-to-end Envoy routing
This is the recommended WSO2 architecture for Kubernetes.