The most surprising thing about Istio Gateways is that they aren’t actually part of Istio itself in the way you might think; they’re just Envoy proxies configured by Istio’s control plane.
Let’s see this in action. Imagine we have a simple web service deployed in our Kubernetes cluster, and we want to expose it to the outside world via an Istio Gateway.
First, we need our application. Here’s a basic Deployment and Service for a fictional my-app:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app-deployment
spec:
replicas: 1
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
containers:
- name: my-app
image: nginxdemos/hello:plain-text
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: my-app-service
spec:
selector:
app: my-app
ports:
- protocol: TCP
port: 80
targetPort: 80
Now, to expose this service externally, we need an Istio Gateway and a VirtualService. The Gateway resource tells Istio (and specifically, the Envoy proxy it manages) which ports to listen on and what TLS certificates to use. The VirtualService then routes incoming traffic based on hostnames and paths to our internal Kubernetes Service.
Here’s a Gateway that listens on port 80 for HTTP traffic and port 443 for HTTPS traffic:
apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
name: my-gateway
spec:
selector:
istio: ingressgateway # This selects the default Istio ingress gateway deployment
servers:
- port:
number: 80
name: http
protocol: HTTP
hosts:
- "*" # Listen for any host
- port:
number: 443
name: https
protocol: HTTPS
tls:
mode: SIMPLE
credentialName: my-tls-secret # Kubernetes secret containing TLS cert and key
hosts:
- "myapp.example.com" # Listen specifically for this host
And here’s the VirtualService that routes traffic for myapp.example.com to our my-app-service:
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: my-app-virtualservice
spec:
hosts:
- "myapp.example.com"
gateways:
- my-gateway # Link this VirtualService to our Gateway
http:
- route:
- destination:
host: my-app-service.default.svc.cluster.local # The internal Kubernetes service
port:
number: 80
To make the HTTPS part work, you’d need a Kubernetes Secret named my-tls-secret in the same namespace as your Gateway, containing your TLS certificate and private key. For example:
kubectl create secret tls my-tls-secret \
--cert=path/to/your/tls.crt \
--key=path/to/your/tls.key
With these resources applied, Istio’s control plane will configure the Envoy proxy running as the istio-ingressgateway (or whatever your Gateway’s selector points to) to listen on the specified ports. Your traffic will hit this Envoy proxy, which will then consult the VirtualService to determine where to send the request within your cluster.
The magic here is that the Gateway resource itself doesn’t do anything; it’s purely a configuration object. Istio’s istiod control plane watches for these Gateway resources and translates their specifications into the complex Envoy configuration that the actual ingress gateway proxy needs to function. This separation allows you to define network entry points independently from how traffic is routed internally.
The default Istio ingress gateway deployment is typically exposed via a Kubernetes Service of type LoadBalancer. This LoadBalancer service provisions an external IP address, which is the entry point for your external traffic into the cluster and then into Istio. You can find this IP with:
kubectl get svc istio-ingressgateway -n istio-system
The most common pitfall is forgetting that the Gateway resource only declares an intent to listen on ports and use certificates; it’s the istio-ingressgateway deployment’s Kubernetes Service (usually of type LoadBalancer) that actually provides the external IP and network reachability.
Once you have external traffic hitting your gateway, the next logical step is to implement more granular routing rules, like traffic splitting for canary deployments, using multiple VirtualService resources.