Minimal Kong Gateway with Keycloak, Jenkins, Prometheus, and Grafana for local Kubernetes testing.
kong-keycloak-stack/
├── k8s/
│ ├── base/
│ │ ├── kustomization.yaml # Lists all resources
│ │ ├── namespace.yaml # kong-system namespace
│ │ ├── postgres.yaml # PostgreSQL database
│ │ ├── keycloak.yaml # Keycloak identity provider
│ │ ├── kong.yaml # Kong gateway
│ │ ├── httpbin.yaml # httpbin test backend
│ │ ├── jenkins-rbac.yaml # Jenkins RBAC
│ │ ├── jenkins.yaml # Jenkins CI/CD server
│ │ ├── prometheus.yaml # Prometheus metrics collection
│ │ ├── loki.yaml # Loki log aggregation
│ │ ├── promtail.yaml # Promtail log collector
│ │ ├── grafana.yaml # Grafana dashboards & visualization
│ │ └── pulumi-state-pvc.yaml
│ └── overlays/
│ └── local/
│ └── kustomization.yaml # Local dev overlay
├── argo/
│ └── values/ # Helm values for the Argo stack
│ ├── argo-cd.yaml
│ ├── argo-workflows.yaml
│ └── argo-rollouts.yaml
├── scripts/ # See scripts/README.md
│ ├── deploy.sh # Deploy the whole stack — main entrypoint
│ ├── port-forward.sh # Start port-forwards — main entrypoint
│ ├── teardown.sh # Remove the stack — main entrypoint
│ ├── lib/ # Helpers invoked by deploy.sh (don't run directly)
│ │ ├── install-argo.sh
│ │ ├── create-keycloak-realm.sh
│ │ └── create-jenkins-jobs.py
│ └── utils/ # Standalone utilities (run occasionally)
│ ├── configure-auth.sh
│ ├── manage-keys.sh
│ ├── bootstrap-local.sh
│ ├── build-keycloak-theme.sh
│ └── set-login-theme.sh
└── README.md
Prerequisites: a local Kubernetes cluster (Docker Desktop), kubectl, and
helm. deploy.sh brings up the full stack — Kong, Keycloak, Jenkins,
monitoring (Prometheus/Loki/Grafana), and the Argo stack (Argo CD, Argo
Workflows, Argo Rollouts) — so a new teammate can clone and run one command.
# Deploy the stack
./scripts/deploy.sh
# Start port-forwards
./scripts/port-forward.sh
# Configure Kong with httpbin service, API key auth, and Prometheus plugin
./scripts/utils/configure-auth.sh
# Test the API
curl -H 'apikey: my-api-key-123' http://localhost:8000/api/httpbin/get| Service | URL | Credentials |
|---|---|---|
| Keycloak | http://localhost:8080 | admin/admin |
| Kong Manager | http://localhost:8002 | - |
| Kong Proxy | http://localhost:8000 | - |
| Kong Admin API | http://localhost:8001 | - |
| Jenkins | http://localhost:8081 | admin/admin |
| PostgreSQL | localhost:5433 | (see below) |
| httpbin | http://localhost:8082 | - |
| Prometheus | http://localhost:9090 | - |
| Grafana | http://localhost:3001 | admin/admin |
| Argo CD | http://localhost:8083 | admin / (see below) |
| Argo Workflows | http://localhost:2746 | - |
The Argo stack is installed via Helm (argo-helm charts) by
scripts/lib/install-argo.sh, which deploy.sh runs automatically. It is
idempotent (helm upgrade --install), so re-running deploy.sh is safe.
| Component | Namespace | Chart |
|---|---|---|
| Argo CD | argocd |
argo/argo-cd |
| Argo Workflows | argo |
argo/argo-workflows |
| Argo Rollouts | argo-rollouts |
argo/argo-rollouts |
- Pinned versions live at the top of
scripts/lib/install-argo.sh; bump them deliberately to track production. Per-app config is inargo/values/. - Argo CD admin password:
kubectl -n argocd get secret argocd-initial-admin-secret \ -o jsonpath='{.data.password}' | base64 -d
- Argo CD runs insecure (HTTP) for convenient local port-forwarding — fine for a workstation, not for shared environments.
- Argo Rollouts dashboard:
kubectl argo rollouts dashboard.
Existing clusters: if a cluster already has Argo installed via raw manifests (
kubectl apply -f install.yaml), Helm will refuse to adopt those resources. Remove them first —kubectl delete ns argocd argo argo-rollouts— then rundeploy.sh. Fresh workstations need no such step.
Prometheus scrapes Kong metrics every 15s. After running scripts/utils/configure-auth.sh, the global Prometheus plugin is enabled on Kong.
- UI: http://localhost:9090
- Kong metrics endpoint: http://localhost:8001/metrics
- Data retention: 7 days
- Storage: 2Gi PVC
Useful PromQL queries:
# Request rate by status code
sum(rate(kong_http_requests_total[5m])) by (code)
# P99 latency
histogram_quantile(0.99, sum(rate(kong_request_latency_ms_bucket[5m])) by (le))
# Error rate percentage
sum(rate(kong_http_requests_total{code=~"5.."}[5m])) / sum(rate(kong_http_requests_total[5m])) * 100
Grafana comes pre-configured with a Prometheus datasource and a Kong Gateway Overview dashboard.
- UI: http://localhost:3001
- Credentials: admin/admin
- Pre-loaded dashboard: Kong Gateway Overview (9 panels)
- Storage: 1Gi PVC
The Kong Gateway Overview dashboard includes:
- Request rate by status code
- Latency distribution (p50/p95/p99)
- Error rate (5xx %)
- Upstream health
- Requests by service
- Rate limiting (429s)
- Bandwidth
- Active connections
- Request logs (Loki)
Container logs are collected cluster-wide by a Promtail DaemonSet and pushed to Loki, which is wired into Grafana as a datasource. Browse them in Grafana → Explore → Loki, or via the Request logs panels on the dashboards.
Kong also runs a structured-logging plugin that emits one JSON object per
request — including request/response headers, service, route,
consumer, client_ip and latencies. Bodies are not captured by default
(see below).
Logging is enabled two ways, so every API is always covered:
- Globally by the
kong-bootstrap-pluginsJob (catches all traffic, including unmatched routes and the Admin API). - Per API by the Pulumi/Jenkins onboarding pipeline
(
jenkins/Jenkinsfile.pulumi), which auto-injects anx-kong-plugin-file-loginto every spec — exactly like it does forx-kong-plugin-prometheus— so logging is baked into each API's provisioned (Pulumi-managed) config and does not depend on the global Job existing in that environment. SetLOG_HTTP_ENDPOINTon the pipeline to injecthttp-logto a collector instead, and a spec that already declares any*-logplugin is left untouched.
Useful LogQL queries:
# All Kong logs
{namespace="kong-system", container="kong"}
# Structured request logs, parsed
{namespace="kong-system", container="kong"} | json | source="kong"
# Errors for a specific onboarded API (matches the per-service dashboard panel)
{namespace="kong-system", container="kong"} | json | service_name="my-api" | response_status >= 500
# Filter by a request header
{namespace="kong-system", container="kong"} | json | request_headers_host="api.example.com"
The global log plugin is provisioned by the kong-bootstrap-plugins Job and is
configurable via the LOG_PLUGIN env var on that Job:
LOG_PLUGIN |
Behaviour | Required env |
|---|---|---|
file-log |
JSON to /dev/stdout → tailed by Promtail into Loki (default) |
— |
http-log |
POST JSON to an external collector (prod-style) | LOG_HTTP_ENDPOINT |
tcp-log |
Stream JSON to a TCP sink (e.g. Logstash) | LOG_TCP_HOST, LOG_TCP_PORT |
off |
No log plugin | — |
Switching transports leaves the previous global plugin in place; remove it via
the Admin API (DELETE /plugins/{id}) before re-running the Job.
Bodies are opt-in per API because of size/PII/performance concerns. Add an
x-kong-plugin-post-function to the API's OpenAPI spec — the provisioner
attaches it per route, and the captured values appear in the structured log
object (request.body / response.body). Validated recipe:
x-kong-plugin-post-function:
config:
access:
# Buffer the upstream response so its body is readable later, and
# capture the request body (available in the access phase).
- "kong.service.request.enable_buffering()"
- "kong.log.set_serialize_value('request.body', kong.request.get_raw_body())"
header_filter:
# Read the buffered upstream response body.
- "kong.log.set_serialize_value('response.body', kong.service.response.get_raw_body())"Note:
enable_buffering()disables response streaming for that route. Only capture bodies where you need them, and beware of logging sensitive data.
This local stack runs Kong OSS (kong/kong:3.9.1); production runs Kong Gateway
Enterprise (3.14.0.3). The logging setup above is portable — it uses only
bundled plugins (file-log/http-log/tcp-log, post-function) and stable
PDK calls present in both editions. Differences to account for in prod:
- Delivery: plugins are managed via decK (see
jenkins/Jenkinsfile.deck), not an Admin API bootstrap Job — declare the global log plugin and per-APIpost-functionin decK config / the OpenAPI specs. - Transport: prefer
http-log/tcp-logto a real collector overfile-log→ stdout (setLOG_PLUGINaccordingly). - Admin API: Enterprise enforces RBAC and workspaces — Admin API calls need
a
Kong-Admin-Tokenand the target workspace; provision the plugin in the correct workspace.
Two databases are available:
| Database | User | Password | Purpose |
|---|---|---|---|
| kong | kong | kongpass | Kong Gateway |
| appdb | appuser | apppass | Your applications |
Add to your .env.local:
DATABASE_URL="postgresql://appuser:apppass@localhost:5433/appdb"Or with individual variables:
POSTGRES_HOST=localhost
POSTGRES_PORT=5433
POSTGRES_DB=appdb
POSTGRES_USER=appuser
POSTGRES_PASSWORD=apppasspostgresql://appuser:apppass@localhost:5433/appdb
Jenkins is configured with security enabled via Configuration as Code (JCasC).
- URL: http://localhost:8081
- Username: admin
- Password: admin
- Log in to Jenkins at http://localhost:8081
- Click your username (top right) → Security
- Scroll to Security section
- Click Add new Token → give it a name → Generate
- Copy the token (it won't be shown again)
# With curl
curl -u admin:YOUR_TOKEN http://localhost:8081/api/json
# Trigger a build
curl -X POST -u admin:YOUR_TOKEN http://localhost:8081/job/JOB_NAME/build# Create a consumer
./scripts/utils/manage-keys.sh create-consumer myuser
# Create an API key (auto-generated)
./scripts/utils/manage-keys.sh create-key myuser
# Create an API key (custom)
./scripts/utils/manage-keys.sh create-key myuser my-custom-key
# List keys
./scripts/utils/manage-keys.sh list-keys myuserFor Kong OSS + Keycloak integration, use the JWT plugin:
- Create a realm and client in Keycloak
- Get the realm's public key from the OIDC config endpoint
- Configure Kong JWT plugin with Keycloak's issuer
See ./scripts/utils/configure-auth.sh for detailed instructions.
# Remove services but keep PostgreSQL, Jenkins, Prometheus, and Grafana data
./scripts/teardown.sh
# Remove everything including all data
./scripts/teardown.sh --delete-dataPostgreSQL, Jenkins, Prometheus, and Grafana data are persisted in PersistentVolumeClaims and survive normal teardowns.