Exposing Kubernetes Services with ngrok's Ingress Controller: A Practical Trade-off
ngrok's Kubernetes Ingress Controller creates public tunnels to internal services, replacing manual LoadBalancer and DNS setup for development and webhooks.
18 Mar 2026, 17:20 UTC

The Problem: A Public URL for a Temporary Service
You have a service running inside a Kubernetes cluster and an external system needs to reach it—a GitHub webhook, a Stripe callback, or a partner API that only accepts public endpoints. The standard Kubernetes path is to create a Service of type LoadBalancer, wait for your cloud provider to assign an external IP, configure DNS, and front it with a TLS certificate. For a service that only needs to be reachable for a few hours, that ceremony is disproportionate.
ngrok's Kubernetes Ingress Controller offers a lighter path. Instead of cloud networking plumbing, it watches your cluster for Ingress resources and creates public tunnels that route traffic directly to your pods. The useful takeaway: you can get a stable public URL in minutes, but you accept dependency on ngrok's cloud plane and the operational limits of your ngrok account tier.
How the Controller Works
An Ingress Controller is a pod that watches Kubernetes Ingress resources—declarative rules that map HTTP hosts and paths to internal Services. The ngrok controller extends this pattern by translating those rules into ngrok tunnels.
Under the hood, the controller installs Custom Resource Definitions (CRDs) to track ngrok-specific state such as reserved domains and tunnel endpoints. When you create an Ingress resource, the controller opens a secure outbound connection from your cluster to the ngrok edge. That edge terminates TLS, so you do not need to manage certificates inside the cluster for the public listener. Traffic then flows through the tunnel to a standard ClusterIP Service and on to your pods.
This replaces the need for a cloud-provider LoadBalancer or NodePort exposure for external access. It is conceptually similar to running an ngrok agent on your laptop, except the agent lifecycle is managed by Kubernetes and driven by cluster resources rather than a local command.
A Worked Example
The following pattern illustrates the typical flow. Because exact chart repositories, CRD versions, and annotation syntax change over time, treat the placeholders as templates and verify them against current ngrok documentation before running in a real cluster.
1. Install the controller
Run Helm from a machine with kubectl access to your cluster. You need an ngrok authentication token from your account dashboard.
# Run from your cluster administration context
helm install ngrok-ingress <ngrok-helm-chart-repo>/kubernetes-ingress-controller \
--namespace ngrok-ingress \
--create-namespace \
--set credentials.authToken=<your-ngrok-authtoken>
Expected check: the controller pod should reach a Running state in the ngrok-ingress namespace. If it crashes, the most common cause is an invalid or missing auth token.
2. Create an Ingress resource
Assume you have a Service named demo-service listening on port 8080. Create an Ingress that maps a public ngrok domain to that Service.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: demo-webhook
annotations:
kubernetes.io/ingress.class: ngrok
spec:
rules:
- host: <your-ngrok-domain>
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: demo-service
port:
number: 8080
Depending on the controller version you deploy, the class may be specified through spec.ingressClassName or a controller-specific CRD rather than the legacy annotation. Confirm which mechanism your installed chart expects.
Expected check: kubectl get ingress demo-webhook -n <namespace> should populate an ADDRESS column with the public hostname. The ngrok dashboard should also show the tunnel as online.
3. Verify end to end
Send a test request to the public URL and confirm it reaches the pod. A practical check is to inspect the ngrok dashboard traffic logs alongside your application logs. If requests appear in the dashboard but not in the pod, the issue is usually Service port mismatch or pod readiness.
Trade-offs and Limitations
- Account tier limits: Traffic routed through ngrok is subject to the bandwidth and connection limits of your specific account tier. A free or low-tier plan may throttle or drop connections under sustained load.
- Security exposure: A public URL means anyone who discovers it can reach the service. The controller does not replace application-level authentication. Add API keys, OAuth, or IP restrictions at the application layer, and avoid exposing admin endpoints.
- Cloud plane dependency: Connectivity relies on ngrok's availability. If the ngrok edge or your tunnel session degrades, external traffic stops even though your cluster is healthy.
- TLS boundary: TLS is terminated at the ngrok edge. Unless you configure end-to-end encryption, traffic between the edge and your pod may travel in plaintext inside the tunnel. That is often acceptable for development, but it requires deliberate review for anything handling sensitive data.
When to Use It
This approach fits development, staging, and webhook prototyping where speed matters more than long-term infrastructure ownership. It is less suitable as a default production ingress for high-traffic or compliance-sensitive workloads unless you have evaluated tier limits, security controls, and the availability implications of depending on an external cloud plane.
To try it safely, start in a non-production namespace with a sample service. Reserve a domain in the ngrok dashboard, deploy the controller, create the Ingress, and verify the tunnel in both kubectl and the ngrok dashboard. If the workflow solves your immediate problem, keep it for ephemeral exposure. If you need permanent public ingress with custom TLS management and cloud-provider integration, a traditional LoadBalancer or ingress controller remains the better long-term fit. To remove the setup, delete the Ingress resource and uninstall the Helm release.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.