Ritolabo
  1. Home
  2. Kubernetes
  3. Ingress: Kubernetes で外からのリクエストをアプリごとに振り分ける

Ingress: Kubernetes で外からのリクエストをアプリごとに振り分ける

  • 公開日
  • カテゴリ:Kubernetes
  • タグ:Kubernetes,学習メモ
Ingress: Kubernetes で外からのリクエストをアプリごとに振り分ける

クラスタの外にいるブラウザから、クラスタの中の Pod にリクエストを届けたい。Kubernetes では、Pod の手前に Service を置き、クラスタの外からの入口は Service の type(NodePort / LoadBalancer)か Ingress で決まる。

この記事では、手元の Mac をクラスタの外に見立てて、次の 3 つを確認する。

  • Service の名前(demo-service)で Pod に届くこと
  • NodePort と kind のポート転送で、Mac の localhost:30080 から Pod に届くこと
  • Ingress と Ingress Controller(Contour)で、Mac の http://localhost/ からホスト名ごとに別の Service に届くこと

contents

  1. 環境
  2. Mac から Pod までの経路
  3. Deployment と Service を作る
  4. Mac から Node のポートに届かせる(NodePort)
  5. Ingress を作る(Ingress Controller なし)
  6. Ingress Controller(Contour)を入れる
  7. Mac から Ingress 経由で届く
  8. ホスト名で振り分ける
  9. 消す
  10. 用語メモ
  11. 参考 URL

環境

kind で作ったクラスタ(Control Plane 1台 + Worker 2台)を使う。(作り方は「Kubernetes の Pod とは?コンテナを動かす最小単位を整理する」を参照)

  • 環境: macOS(Apple Silicon)、Docker Desktop 29.8.0、kind v0.33.0、Kubernetes v1.37.0、kubectl v1.36.1

kind の Node は Docker のコンテナで、そのままでは Mac から Node のポートに届かない。この記事では、kind の設定に extraPortMappings(Mac のポート → Node のポート)を足したクラスタを使う。

# kind-config.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
  - role: control-plane
  - role: worker
    extraPortMappings:
      - containerPort: 80        # Mac の 80 番 → この Node の 80 番(Ingress 用)
        hostPort: 80
        listenAddress: "127.0.0.1"
      - containerPort: 30080     # Mac の 30080 番 → この Node の 30080 番(NodePort 用)
        hostPort: 30080
        listenAddress: "127.0.0.1"
  - role: worker
$ kind create cluster --name k8s-study --config kind-config.yaml

$ docker ps --format '{{.Names}}\t{{.Ports}}' | grep k8s-study

k8s-study-worker	127.0.0.1:80->80/tcp, 127.0.0.1:30080->30080/tcp
k8s-study-control-plane	127.0.0.1:65042->6443/tcp
k8s-study-worker2

k8s-study-worker に Mac の 80 番と 30080 番がつながっている。これは Docker(kind)の設定で、Kubernetes 側の設定ではない。

Mac から Pod までの経路

経路の要素何か実体
Podnginx が動いている。IP は 10.244.x.x(クラスタの中だけで通じる)Node の上のコンテナ
ServicePod の手前に置く固定の名前と IP。demo-service → nginx の Pod 3 つetcd に保存されたデータ。届けているのは各 Node の kube-proxy
NodePortService の type の 1 つ。全部の Node の同じポート(30080)でも受けるService の設定
Ingress「このホスト名の HTTP リクエストは、この Service へ」というルールetcd に保存されたデータ
Ingress ControllerIngress を読んで、そのとおりに HTTP を振り分けるプログラム。Kubernetes に入っていないので自分で入れる今回は Contour(projectcontour namespace の Pod)

外から来た HTTP リクエストは、そのままでは Pod に届かない。Kubernetes に、外から受けて Pod に渡す仕組みが入っていないため。

そこで Ingress Controller を自分で入れる。これはライブラリではなく、クラスタの中で動くプログラム(Pod)。Kubernetes とは別のプロジェクトがいくつも作っていて(nginx ベース、HAProxy ベース、クラウド各社のものなど)、どれかを選んで入れる。この記事では Contour を使う。

Contour を入れると、Pod が 2 種類動く。

  • contour: Ingress を読む
  • envoy: Node の 80 番で受け、Ingress のルール(ホスト名 → Service)に従って、その Service の Pod に転送する

アプリは nginx(Service demo-service)と Apache httpd(Service httpd-service)の 2 つ。振り分け先を 2 つにするためで、httpd も nginx と同じ Web サーバー。

Ingress で実現される構成を図にすると次のとおり。Pod がどの Node にあるかは、この仕組みには関係ないので描いていない。

図を描画しています…
  • 振り分けは 2 段階ある。入口のロードバランサーは「どの Node(マシン)に渡すか」を決める。envoy は「どの Service の Pod に渡すか」を Host ヘッダーで決める。ロードバランサーは HTTP の中身を見ないので、アプリを選ぶ仕事はできない
  • 角が二重の箱(contour、envoy)は動いているプログラム。自分で入れた Pod で、各 Worker Node に 1 組ずつある。どの Node にリクエストが来ても、その Node の envoy が同じ判断をする。Node が 1 台止まっても、残りの Node で受けられる
  • envoy は、Service の宛先一覧にある Pod の IP に送る。その Pod が自分の Node にあるか別の Node にあるかは見ない
  • 自分で書くのは Service と Ingress の YAML だけ
  • この記事の kind では、入口はクラウドのロードバランサーではなく Docker のポート転送で、Mac の 80 番は k8s-study-worker の 80 番だけにつながっている

Deployment と Service を作る

nginx の Deployment(Pod 3 個)と、その手前の Service を作る。クラスタの中から打つための Pod client(busybox、sleep するだけ)も作る。

# demo-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: demo-app
  template:
    metadata:
      labels:
        app: demo-app
    spec:
      containers:
        - name: nginx
          image: nginx:1.16.1
          ports:
            - containerPort: 80
# demo-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: demo-service
spec:
  selector:
    app: demo-app
  ports:
    - port: 80
      targetPort: 80
# client.yaml
apiVersion: v1
kind: Pod
metadata:
  name: client
spec:
  containers:
    - name: client
      image: busybox:1.36
      command: ["sleep", "86400"]
$ kubectl apply -f demo-deployment.yaml -f demo-service.yaml -f client.yaml

$ kubectl get svc demo-service

NAME           TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
demo-service   ClusterIP   10.96.196.69   <none>        80/TCP    4s

$ kubectl exec client -- wget -qO- http://demo-service | grep title

# => <title>Welcome to nginx!</title>

クラスタの中からは、Service の名前 demo-service で nginx に届く。(Service は「Service: Kubernetes で Pod への宛先を固定する」を参照)

Mac から Node のポートに届かせる(NodePort)

Service の type を NodePort にすると、全部の Node の 30080 番でも受けるようになる。

# demo-service-nodeport.yaml
apiVersion: v1
kind: Service
metadata:
  name: demo-service
spec:
  type: NodePort
  selector:
    app: demo-app
  ports:
    - port: 80
      targetPort: 80
      nodePort: 30080            # 書かないと 30000〜32767 から自動で選ばれる
$ kubectl apply -f demo-service-nodeport.yaml

# => service/demo-service configured

$ kubectl get svc demo-service

NAME           TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
demo-service   NodePort   10.96.213.171   <none>        80:30080/TCP   10s

$ curl -s http://localhost:30080 | grep title

# => <title>Welcome to nginx!</title>

PORT(S) の 80:30080/TCP は「Service のポート 80、Node のポート 30080」。Mac の localhost:30080 は kind のポート転送で k8s-study-worker の 30080 番につながり、そこから Service の Pod に届く。ブラウザで http://localhost:30080/ を開いても nginx のページが出る。

NodePort は「ポート番号 1 つ = Service 1 つ」。アプリが増えるとポート番号も増える。1 つの入口で複数のアプリに振り分けるのが、次の Ingress。

Ingress を作る(Ingress Controller なし)

まず、Ingress Controller を入れずに Ingress だけ作る。ルールは「全部の HTTP リクエストを demo-service へ」。

# demo-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: demo-ingress
spec:
  rules:
    - http:                      # host を書かないので、どのホスト名でも当てはまる
        paths:
          - path: /
            pathType: Prefix     # / で始まるパス = 全部
            backend:
              service:
                name: demo-service   # 送り先の Service
                port:
                  number: 80
$ kubectl apply -f demo-ingress.yaml

# => ingress.networking.k8s.io/demo-ingress created

$ kubectl get ingress

NAME           CLASS    HOSTS   ADDRESS   PORTS   AGE
demo-ingress   <none>   *                 80      6s

$ kubectl describe ingress demo-ingress

Name:             demo-ingress
Labels:           <none>
Namespace:        default
Address:
Ingress Class:    <none>
Default backend:  <default>
Rules:
  Host        Path  Backends
  ----        ----  --------
  *
              /   demo-service:80 (10.244.1.2:80,10.244.3.3:80,10.244.1.3:80)
Annotations:  <none>
Events:       <none>

$ curl -m 3 http://localhost/

# => curl: (52) Empty reply from server

Rules: の Backends に、Service の名前とその先の Pod の IP まで出る。データとしてはつながっているが、Mac から http://localhost/ を打っても返事がない。Node の 80 番を聞いているプログラムがいないため。

Ingress は、Deployment や Service と違って、読んで動くプログラムが Kubernetes に最初から入っていない。公式ドキュメントも "Only creating an Ingress resource has no effect." としている。

Ingress Controller(Contour)を入れる

Ingress Controller は、Ingress を読んで、そのとおりに HTTP を振り分けるプログラム。中身はリバースプロキシ(外から来たリクエストを受け取って、後ろにいるサーバーのどれかに渡すプログラム)で、Ingress のルールをその設定に変換して動かす。何種類もある中から、この記事では Contour を使う。

Contour の Pod何をするか
contour(Deployment、2 つ)Ingress と Service を kube-apiserver から読んで、Envoy の設定に変換して配る
envoy(DaemonSet、Worker ごとに 1 つ)リバースプロキシ本体(Envoy)。Node の 80 番で HTTP を受け、設定どおりの Pod に転送する
$ kubectl apply -f https://projectcontour.io/quickstart/contour.yaml

namespace/projectcontour created
serviceaccount/contour created
serviceaccount/envoy created
configmap/contour created
customresourcedefinition.apiextensions.k8s.io/contourconfigurations.projectcontour.io created
customresourcedefinition.apiextensions.k8s.io/contourdeployments.projectcontour.io created
customresourcedefinition.apiextensions.k8s.io/extensionservices.projectcontour.io created
customresourcedefinition.apiextensions.k8s.io/httpproxies.projectcontour.io created
customresourcedefinition.apiextensions.k8s.io/tlscertificatedelegations.projectcontour.io created
serviceaccount/contour-certgen created
rolebinding.rbac.authorization.k8s.io/contour created
role.rbac.authorization.k8s.io/contour-certgen created
job.batch/contour-certgen-v1-33-7 created
clusterrolebinding.rbac.authorization.k8s.io/contour created
rolebinding.rbac.authorization.k8s.io/contour-rolebinding created
clusterrole.rbac.authorization.k8s.io/contour created
role.rbac.authorization.k8s.io/contour created
service/contour created
service/envoy created
deployment.apps/contour created
daemonset.apps/envoy created

$ kubectl get pods -n projectcontour -o wide

NAME                            READY   STATUS              RESTARTS   AGE   IP           NODE                NOMINATED NODE   READINESS GATES
contour-859c5dcfdc-d52lr        0/1     ContainerCreating   0          7s    <none>       k8s-study-worker2   <none>           <none>
contour-859c5dcfdc-xjqcg        0/1     ContainerCreating   0          7s    <none>       k8s-study-worker    <none>           <none>
contour-certgen-v1-33-7-cpg4v   0/1     Completed           0          7s    10.244.1.4   k8s-study-worker2   <none>           <none>
envoy-6jl64                     0/2     Init:0/1            0          7s    <none>       k8s-study-worker2   <none>           <none>
envoy-rbsm2                     0/2     Init:0/1            0          7s    <none>       k8s-study-worker    <none>           <none>

1 分ほどで contour が 1/1、envoy が 2/2 の Running になる(contour-certgen は証明書を作る Job で Completed のまま)。

Mac から Ingress 経由で届く

$ curl -s http://localhost/ | grep title

# => <title>Welcome to nginx!</title>

$ curl -si http://localhost/ | head -4

HTTP/1.1 200 OK
server: envoy
date: Sun, 11 Oct 2026 00:27:48 GMT
content-type: text/html

さっき返事がなかった http://localhost/ で nginx が返るようになった。返事の server ヘッダーが envoy になっていて、間に Envoy がいることがわかる。

Envoy のログには、Mac からのリクエストと、転送先の Pod の IP が残る。

$ kubectl logs -n projectcontour -l app=envoy -c envoy --tail=1

# => [2026-10-11T00:27:48.031Z] "GET / HTTP/1.1" 200 - 0 612 3 2 "172.21.0.1" "curl/8.7.1" "685ca5ab-ab0b-4c11-8405-1cf27d26ff0e" "localhost" "10.244.1.3:80"

最後の "10.244.1.3:80" が転送先で、nginx の Pod の IP。Envoy は Service の宛先一覧(EndpointSlice)から Pod の IP を受け取り、Pod に直接送っている。

ホスト名で振り分ける

Ingress の使いどころは、1 つの入口に来たリクエストを HTTP の Host ヘッダー(ホスト名)で別々の Service に振り分けること。2 つ目の Web サーバーとして Apache httpd を足す。

# demo-httpd.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo-httpd
spec:
  replicas: 1
  selector:
    matchLabels:
      app: demo-httpd
  template:
    metadata:
      labels:
        app: demo-httpd
    spec:
      containers:
        - name: httpd
          image: httpd:2.4
          ports:
            - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: httpd-service
spec:
  selector:
    app: demo-httpd
  ports:
    - port: 80
      targetPort: 80

Ingress を、ホスト名つきのルール 2 つに書き換える。

# demo-ingress-hosts.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: demo-ingress
spec:
  rules:
    - host: nginx.example.com    # Host ヘッダーがこの名前なら demo-service へ
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: demo-service
                port:
                  number: 80
    - host: httpd.example.com    # この名前なら httpd-service へ
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: httpd-service
                port:
                  number: 80
$ kubectl apply -f demo-httpd.yaml -f demo-ingress-hosts.yaml

deployment.apps/demo-httpd created
service/httpd-service created
ingress.networking.k8s.io/demo-ingress configured

$ kubectl get ingress

NAME           CLASS    HOSTS                                 ADDRESS   PORTS   AGE
demo-ingress   <none>   nginx.example.com,httpd.example.com             80      12m

nginx.example.com も httpd.example.com も実在しない名前なので、curl の -H で Host ヘッダーだけを付けて、宛先は localhost のまま打つ。

$ curl -s -H 'Host: nginx.example.com' http://localhost/ | grep title

# => <title>Welcome to nginx!</title>

$ curl -s -H 'Host: httpd.example.com' http://localhost/ | grep title

# => <title>It works! Apache httpd</title>

$ curl -si http://localhost/ | head -1

# => HTTP/1.1 404 Not Found
Host ヘッダー届く先
nginx.example.comdemo-service → nginx の Pod
httpd.example.comhttpd-service → httpd の Pod
指定なし(localhost)どのルールにも当てはまらず、Envoy が 404 を返す

同じ localhost:80 に来たリクエストが、ホスト名で 2 つの Service に分かれた。本番では DNS で両方の名前を同じ入口の IP に向けておき、1 つの入口で複数のアプリを公開する。

消す

$ kubectl delete -f demo-ingress-hosts.yaml -f demo-httpd.yaml -f demo-service-nodeport.yaml -f demo-deployment.yaml -f client.yaml

ingress.networking.k8s.io "demo-ingress" deleted from default namespace
deployment.apps "demo-httpd" deleted from default namespace
service "httpd-service" deleted from default namespace
service "demo-service" deleted from default namespace
deployment.apps "demo-app" deleted from default namespace
pod "client" deleted from default namespace

Contour も消すときは、入れたときと同じ YAML で kubectl delete -f https://projectcontour.io/quickstart/contour.yaml。

用語メモ

用語読み方意味
ClusterIPクラスター アイピーService に付く固定の IP(10.96.x.x)。クラスタの中からだけ届く。type を書かないときの Service の種類
NodePortノード ポートService の種類。ClusterIP に加えて、全部の Node の同じポート(30000〜32767)でも受ける
LoadBalancerロード バランサーService の種類。NodePort に加えて、クラウドのロードバランサーを作ってもらい、その IP で受ける。kind には作るプログラムがいないので EXTERNAL-IP が <pending> のまま
extraPortMappingsエクストラ ポート マッピングスkind の設定。Mac のポートを Node(Docker コンテナ)のポートにつなぐ。Kubernetes の設定ではない
IngressイングレスHTTP 専用の振り分けルール。「ホスト名 / パス → Service とポート」。読むプログラムがいないと何も起きない
Ingress Controllerイングレス コントローラーIngress を読んで、リバースプロキシを設定して振り分けるプログラム。Kubernetes に入っていないので自分で入れる
リバースプロキシ─外からのリクエストを受け取って、後ろにいるサーバーのどれかに代わりに渡すプログラム。nginx、Envoy、HAProxy など
ContourコンツアーIngress Controller の 1 つ。contour(Ingress を読む Deployment)と envoy(リバースプロキシの DaemonSet)の 2 種類の Pod で動く
Envoyエンボイリバースプロキシ。Contour の中で HTTP を受けて転送している本体
pathTypeパス タイプIngress のパスの条件の種類。Prefix(/ で区切った前方一致)、Exact(完全一致)。必須

まとめ

  • クラスタの中からは、Service の名前(demo-service)で Pod に届く
  • クラスタの外から Node に届く経路は Kubernetes の外にある。kind では extraPortMappings で Mac のポートを Node のポートにつなぐ。クラウドではロードバランサーがその役
  • Service の type: NodePort は、全部の Node の同じポートで受ける。ポート番号 1 つにつき Service 1 つ
  • Ingress は「ホスト名 / パス → Service」の HTTP 専用のルール。作っただけでは何も起きず、Ingress Controller を入れると動く
  • Ingress Controller は Kubernetes に入っていない。Contour なら kubectl apply 1 つで入り、Ingress を読む Pod と HTTP を受けて転送する Pod(Envoy)が動く
  • 1 つの入口(localhost:80)に来たリクエストを、Host ヘッダーで別々の Service に振り分けられる

参考 URL


[Prev] Rolling Update / Rollback: Kubernetes でアプリを止めずに Pod を入れ替える

Author

rito

rito

  • Backend Engineer
  • Tokyo, Japan
  • PHP 5 技術者認定上級試験 認定者
  • 統計検定 3 級