Routing External Traffic with Istio + Kubernetes Gateway API (Gateway → HTTPRoute → Service)

Contents

Overview

For a long time, Ingress was the primary way to receive external traffic in Kubernetes, but Gateway API is now becoming the de facto standard. Istio can act as a Gateway API controller, so Gateway/HTTPRoute alone can define the "domain → routing → service" flow.

This post provides a reproducible checklist for verifying that applications communicate correctly through Gateway API in an Istio-based environment. (The same structure can be used for validation in both Sidecar and Ambient modes.)


Goals (Success Criteria)


Overall Structure (Concept)

Diagram 1Host: app.example.com 요청이 LoadBalancer, Istio Gateway(Envoy), HTTPRoute, Service를 차례로 거쳐 Pod에 닿는다.Host: app.example.comClientLoadBalancerIstio Gateway EnvoyHTTPRouteServicePod
Host: app.example.com 요청이 LoadBalancer, Istio Gateway(Envoy), HTTPRoute, Service를 차례로 거쳐 Pod에 닿는다.

Gateway API describes the "routing intent (specification)," while Istio handles the actual implementation (controller). It is therefore normal to see istio-envoy in the response header when successful.


Line guide

  • 설정 참조·허용 조건
  • 논리적 요청·응답 흐름
Istio Gateway API: 설정 관계와 요청 흐름설정층: Gateway가 gatewayClassName istio로 GatewayClass를 참조한다. Namespace demo 라벨이 allowedRoutes selector와 맞으면 route attach가 허용된다. HTTPRoute는 parentRefs로 Gateway의 http 리스너에 연결되고 hostnames app.example.com, PathPrefix /, backendRefs demo:80을 정한다. Istio가 이 설정을 Gateway Envoy로 구현한다. 요청층: Client가 Host app.example.com으로 LoadBalancer에 보내고, Istio Gateway Envoy를 거쳐 Service demo:80, app demo 라벨의 Pod nginx로 간다.설정층: 점선은 참조와 허용 조건요청층: 실선은 논리적 요청 흐름Namespace demoshared-gateway-access=trueGatewayClassistioGatewaygateway / istio-systemHTTPRoutedemo / demogatewayClassName: istioselector 일치 → attach 허용parentRefs: gatewaysectionName: httphostnames: app.example.comPathPrefix: /backendRefs: demo:80Istio가 이 설정을Envoy로 구현ClientHost:app.example.comLoadBalancerIstio GatewayEnvoyServicedemo:80Podnginx설정된 backendapp: demo 선택 Istio Gateway API: 설정 관계와 요청 흐름설정층: Gateway가 gatewayClassName istio로 GatewayClass를 참조한다. Namespace demo 라벨이 allowedRoutes selector와 맞으면 route attach가 허용된다. HTTPRoute는 parentRefs로 Gateway의 http 리스너에 연결되고 hostnames app.example.com, PathPrefix /, backendRefs demo:80을 정한다. Istio가 이 설정을 Gateway Envoy로 구현한다. 요청층: Client가 Host app.example.com으로 LoadBalancer에 보내고, Istio Gateway Envoy를 거쳐 Service demo:80, app demo 라벨의 Pod nginx로 간다.설정층: 점선은 참조·허용 조건GatewayClassistioGatewaygateway / istio-systemgatewayClassName: istioNamespace demoshared-gateway-access=trueHTTPRoutedemo / demoselector 일치→ attach 허용parentRefs: gatewaysectionName: http요청층: 실선은 논리적 요청 흐름ClientLoadBalancerIstio Gateway Envoy설정을 Istio가 구현Servicedemo:80PodnginxHost: app.example.com설정된 backendapp: demo 선택backendRefs
Gateway가 받은 요청을 HTTPRoute 규칙이 Service와 Pod로 보내는 논리 흐름이다. Namespace에서 Gateway로 향한 점선은 allowedRoutes selector의 라벨 일치가 attach 허용 조건이라는 관계다.
Read the diagram as text
  1. Gateway(gateway, istio-system)가 gatewayClassName: istio로 GatewayClass istio를 참조한다.
  2. Namespace demo의 라벨 shared-gateway-access=true가 Gateway allowedRoutes selector와 맞아야 route attach가 허용된다.
  3. HTTPRoute demo가 parentRefs(gateway, istio-system, sectionName: http)로 Gateway에 연결된다.
  4. HTTPRoute가 hostnames app.example.com, PathPrefix /, backendRefs demo:80을 정한다.
  5. Gateway API는 라우팅 의도이고, Istio가 이 설정을 Istio Gateway Envoy로 구현한다.
  6. 요청: Client가 Host: app.example.com으로 LoadBalancer에 보낸다.
  7. LoadBalancer에서 Istio Gateway Envoy로 간다.
  8. Envoy가 설정된 backend인 Service demo:80으로 보낸다.
  9. Service가 app: demo 라벨로 Pod(nginx)를 선택한다.

1) Prerequisites

The examples below include common issues in AWS environments (NLB). In other environments, adjust only the LoadBalancer/firewall sections as needed.


2) Install Gateway API CRDs

Gateway API requires CRDs first. (Pinning the version is recommended in production.)


3) Install Istio (Sidecar / Ambient)

Sidecar Mode (Typical Setup)

bash
helm repo add istio https://istio-release.storage.googleapis.com/charts
helm repo update

helm upgrade --install istio-base istio/base -n istio-system --create-namespace
helm upgrade --install istiod istio/istiod -n istio-system

Ambient Mode (Additional Components Required)

Ambient centers on ztunnel/waypoint rather than sidecars, so it requires more components.

bash
helm repo add istio https://istio-release.storage.googleapis.com/charts
helm repo update

helm upgrade --install istio-base istio/base -n istio-system --create-namespace
helm upgrade --install istiod istio/istiod -n istio-system --set profile=ambient
helm upgrade --install istio-cni istio/cni -n istio-system --set profile=ambient
helm upgrade --install ztunnel istio/ztunnel -n istio-system

4) Create a (Shared) Gateway

In production, it is usually easier to manage one shared Gateway for the team, with each service adding only an HTTPRoute.

Here is an example.

yaml
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: gateway
  namespace: istio-system
  annotations:
    # (AWS 예시) internet-facing
    service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
spec:
  gatewayClassName: istio
  listeners:
    - name: http
      hostname: "*.example.com"
      port: 80
      protocol: HTTP
      allowedRoutes:
        namespaces:
          from: Selector
          selector:
            matchLabels:
              shared-gateway-access: "true"

After creation, first check that Istio is actually controlling Gateway API, as shown below. This makes debugging faster.

bash
kubectl get gatewayclass
kubectl -n istio-system get gateway gateway -o wide

5) Prepare the Application Namespace and Workload

Common Setup (Label Allowing Route Attachment)

Because the Gateway restricts allowedRoutes with a selector, label the namespace accordingly.

bash
kubectl create ns demo
kubectl label ns demo shared-gateway-access=true --overwrite

Additional Setup for Ambient (Ambient Opt-In)

Ambient requires namespace opt-in.

bash
kubectl label ns demo istio.io/dataplane-mode=ambient --overwrite

6) Deploy the Sample Application (nginx)

The goal is to verify "Gateway → HTTPRoute → Service," so keep the backend as simple as possible.

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo
  namespace: demo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: demo
  template:
    metadata:
      labels:
        app: demo
    spec:
      containers:
        - name: nginx
          image: nginx:1.27
          ports:
            - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: demo
  namespace: demo
spec:
  selector:
    app: demo
  ports:
    - name: http
      port: 80
      targetPort: 80
bash
kubectl apply -f demo.yaml
kubectl -n demo rollout status deploy/demo

7) Connect the Service with HTTPRoute

An HTTPRoute defines which service receives requests for a given Host/Path.

yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: demo
  namespace: demo
spec:
  parentRefs:
    - name: gateway
      namespace: istio-system
      sectionName: http
  hostnames:
    - "app.example.com"
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: demo
          port: 80

8) DNS and Access Tests

8-1) Check HTTPRoute Status

bash
kubectl -n demo get httproute demo -o wide
kubectl -n demo describe httproute demo

If Accepted=True and ResolvedRefs=True (and ResolvedWaypoints=True in Ambient mode) are confirmed here, the routing configuration can be considered successfully attached.

8-2) Check the Gateway Address (LoadBalancer DNS)

Usually, you can check it with one of the following two methods.

bash
kubectl -n istio-system get gateway gateway -o jsonpath='{.status.addresses[0].value}{"\n"}'

Or from the LoadBalancer Service created by Istio:

bash
kubectl -n istio-system get svc gateway-istio -o jsonpath='{.status.loadBalancer.ingress[0].hostname}{"\n"}'

Immediately after an NLB is created, incomplete DNS propagation may briefly cause Could not resolve host. Waiting a few minutes and retrying often resolves it.

8-3) Verify Host-Based Routing with curl

bash
export LB_DNS="<위에서 확인한 값>"
curl -i -H "Host: app.example.com" "http://${LB_DNS}/"

If working correctly:

8-4) (Optional) Access Through a Real Domain

The curl -H "Host: ..." approach above is useful for checking whether Host routing works without DNS.

To use a real domain such as app.example.com in production, configure DNS as follows.

Example (CNAME):

txt
app.example.com.  CNAME  <LB_DNS>

9) Troubleshooting Checklist

A. HTTPRoute Accepted=False

The most common cause is a mismatch in the allowedRoutes conditions.

B. curl Times Out

This is common with AWS (NLB) setups.

C. Could not resolve host


Closing Notes

Next steps:


References

Diagram

100%

Drag to pan; pinch or use the wheel to zoom. Keyboard: arrow keys pan, + − zoom, 0 fits, Esc closes. The same content is in “Read the diagram as text” below the figure.