Ingress で Kubernetes トラフィックをルーティングする

KubernetesBeginner
オンラインで実践に進む

はじめに

Service は、クラスター内部で安定したアプリケーションエンドポイントを提供します。Ingress は Service の前段に HTTP ルーティングルールを追加し、ホスト名や URL パスに基づいて、複数のアプリケーションで 1 つの入口を共有できるようにします。

この実験では、準備済みの ingress-nginx コントローラーを確認し、2 つのバックエンドをデプロイして、ホストベースおよびパスベースのルーティングを追加します。その後、Ingress、Service、Pod を経由するリクエストの流れを追跡します。

実験中は、リクエストの経路を常に意識してください。client → ingress-nginx の入口 → Ingress ルール → Service → Ready 状態の Pod という流れです。リクエストが失敗した場合も、Ingress を単一のブラックボックスとして扱うより、この経路を順番に確認するほうが簡単です。

Ingress コントローラーを確認する

このステップでは、2 つ目のコントローラーをインストールするのではなく、CKA 環境にあらかじめ用意されているコントローラーを確認します。

Ingress オブジェクトは設定情報であり、プロキシプロセスそのものではありません。対応するコントローラーが監視していなければ、何の効果もありません。まず、nginx という名前の IngressClass と、利用可能な ingress-nginx コントローラー Deployment の両方が存在することを確認します。

cd /home/labex/project/ingress-routing
kubectl get ingressclasses
kubectl get deployments,pods,services -n ingress-nginx
kubectl wait --for=condition=Available deployment/ingress-nginx-controller -n ingress-nginx --timeout=60s

Ingress リソースには、ルーティングの意図が定義されています。ingress-nginx コントローラーは、ingressClassName が nginx のリソースを監視し、それに基づいて実際のプロキシを構成します。

コントローラーの Service は、このローカルクラスター内で NodePort を通じてプロキシを公開しています。ポート番号は後ほど確認するため、ここでは決め打ちしません。

準備済みの環境では、nginx クラス、利用可能なコントローラー 1 つ、および NodePort Service が次のように表示されます。

NAME    CONTROLLER
nginx   k8s.io/ingress-nginx

deployment.apps/ingress-nginx-controller   1/1   ...
service/ingress-nginx-controller           NodePort   ...   80:.../TCP,443:.../TCP

Completed 状態の Admission Pod が 2 つ表示される場合があります。これらはすでに完了したインストール用 Job であり、コントローラーのレプリカが失敗したわけではありません。

2 つの Service バックエンドをデプロイする

このステップでは、1 つのマルチドキュメントファイルから、レプリカ数 1 の Web ワークロード 2 つと、それぞれに対応する ClusterIP Service を作成します。

これらのバックエンドでは「Kubernetes 入門」で扱う概念を使用するため、ここでの学習の中心は Deployment の構文ではなく、ルーティングの関係です。ファイルには --- で区切られた 5 つの YAML ドキュメントが含まれています。最初に Namespace が 1 つ、その後に各アプリケーション用の Deployment と Service が 1 組ずつ続きます。対応する Pod のラベルと、その横にある各 Service.spec.selector を確認してください。Ingress がリクエストを転送するには、両者が一致している必要があります。

cd /home/labex/project/ingress-routing
cat <<'EOF' > backends.yaml
apiVersion: v1
kind: Namespace
metadata: {name: practice-ingress}
---
apiVersion: apps/v1
kind: Deployment
metadata: {name: storefront, namespace: practice-ingress}
spec:
  replicas: 1
  selector: {matchLabels: {app: storefront}}
  template:
    metadata: {labels: {app: storefront}}
    spec:
      containers: [{name: web, image: nginx:1.27-alpine, imagePullPolicy: IfNotPresent, ports: [{name: http, containerPort: 80}]}]
---
apiVersion: v1
kind: Service
metadata: {name: storefront, namespace: practice-ingress}
spec: {selector: {app: storefront}, ports: [{name: http, port: 80, targetPort: http}]}
---
apiVersion: apps/v1
kind: Deployment
metadata: {name: reports, namespace: practice-ingress}
spec:
  replicas: 1
  selector: {matchLabels: {app: reports}}
  template:
    metadata: {labels: {app: reports}}
    spec:
      containers: [{name: web, image: httpd:2.4-alpine, imagePullPolicy: IfNotPresent, ports: [{name: http, containerPort: 80}]}]
---
apiVersion: v1
kind: Service
metadata: {name: reports, namespace: practice-ingress}
spec: {selector: {app: reports}, ports: [{name: http, port: 80, targetPort: http}]}
EOF

5 つのドキュメントをまとめて適用し、その後で両方の Deployment が完了するまで待機します。個別に待機することで、イメージやセレクターに問題がある場合に、どちらのバックエンドで失敗したかを明確に確認できます。

kubectl apply -f backends.yaml
kubectl rollout status deployment/storefront -n practice-ingress --timeout=60s
kubectl rollout status deployment/reports -n practice-ingress --timeout=60s

Ingress は Service に対してのみルーティングするため、各バックエンドに Ready 状態のエンドポイントが存在していなければなりません。

ルーティングルールを作成する前に、前提条件を明示的に確認します。

kubectl get services,endpointslices -n practice-ingress

各 Service には、Ready 状態のアドレスを 1 つ持つ対応する EndpointSlice が表示されるはずです。

service/reports      ClusterIP   ...   80/TCP
service/storefront   ClusterIP   ...   80/TCP

endpointslice.../reports-...      IPv4   80   10.244.104...
endpointslice.../storefront-...   IPv4   80   10.244.104...

生成されるサフィックスやアドレスは環境によって異なりますが、各 Service 名に対応する空でない EndpointSlice が 1 つ存在している必要があります。Ingress で、バックエンドを持たない Service を修復することはできません。この確認では、まず下位レイヤーが正常であることを意図的に検証しています。

ホストベースのルーティングを追加する

このステップでは、store.practice.test へのリクエストを storefront Service にルーティングします。

Ingress ルールでは、host、path、バックエンド Service のポートという 3 つの条件でルーティング先を決定します。ingressClassName: nginx によって使用するコントローラーを選択します。rewrite アノテーションはコントローラー固有の設定であり、バックエンドへ転送するリクエストパスを / に変更します。これにより、2 つのデモサーバーのどちらでも応答できます。

cd /home/labex/project/ingress-routing
cat <<'EOF' > ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: practice-routing
  namespace: practice-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  ingressClassName: nginx
  rules:
    - host: store.practice.test
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: storefront
                port: {name: http}
EOF

ルールを適用し、describe を使って、解決済みのバックエンドエンドポイントを含むコントローラーの解釈を確認します。

kubectl apply -f ingress.yaml
kubectl describe ingress practice-routing -n practice-ingress

pathType: Prefix は / およびそのサブパスに一致します。rewrite アノテーションによって、選択されたリクエストはバックエンドの / に送られ、デモ Web サーバーのページが返されます。ホスト名は HTTP ルーティングのキーであり、このローカルテストのためにパブリック DNS を用意する必要はありません。

describe の結果では、ルールが Pod アドレスまで解決されていることを確認できます。

Rules:
  Host                 Path  Backends
  store.practice.test
                       /   storefront:http (10.244.104...:80)

このローカル環境では、作成直後にトップレベルの Address が空であっても失敗ではありません。重要なのは、storefront:http バックエンドとその Pod アドレスが解決されていることです。

パスルーティングを追加してリクエストをテストする

このステップでは、/reports のルーティングを追加し、ノードの入口を通じて 2 つのルールをテストします。

JSON Patch のパスは /- で終わっています。配列において - は、既存の / ルートを置き換えるのではなく、新しい項目を末尾に追加することを意味します。追加するバックエンドは、名前付き http ポートを使って reports Service を参照します。

次の追加パッチは 1 回だけ実行してください。kubectl apply とは異なり、/- への追加操作はべき等ではありません。繰り返すと、既存の項目を認識するのではなく、同じ /reports 項目がもう 1 つ追加されます。

続く 2 つの代入では、エンドポイントを決め打ちせずに検出します。minikube ip -p labex-v135 は指定した Minikube プロファイルのノード IP を返し、コマンド置換 $(...) は各コマンドの出力をシェル変数に格納します。JSONPath クエリは http という名前の Service ポートを選択し、その NodePort を取り出します。

kubectl patch ingress practice-routing -n practice-ingress --type='json' -p='[{"op":"add","path":"/spec/rules/0/http/paths/-","value":{"path":"/reports","pathType":"Prefix","backend":{"service":{"name":"reports","port":{"name":"http"}}}}}]'
NODE_IP=$(minikube ip -p labex-v135)
HTTP_NODE_PORT=$(kubectl get service ingress-nginx-controller -n ingress-nginx -o jsonpath='{.spec.ports[?(@.name=="http")].nodePort}')
echo "Ingress endpoint: ${NODE_IP}:${HTTP_NODE_PORT}"

この 2 つの変数により、環境固有のアドレスを手順へ直接埋め込まずに済みます。次に、ルールで要求されている Host ヘッダーを付けてリクエストを送信します。各 curl コマンドでは、-s が進捗メーターを非表示にし、--retry 5 が一時的な失敗を再試行します。--retry-connrefused は最初に接続拒否された場合も再試行し、--retry-delay 2 は試行間に 2 秒待機します。-H は HTTP Host ヘッダーを指定し、パイプによってレスポンスを grep に渡すことで、識別用のページテキストだけを表示します。

curl -s --retry 5 --retry-connrefused --retry-delay 2 -H 'Host: store.practice.test' "http://${NODE_IP}:${HTTP_NODE_PORT}/" | grep 'Welcome to nginx'
curl -s --retry 5 --retry-connrefused --retry-delay 2 -H 'Host: store.practice.test' "http://${NODE_IP}:${HTTP_NODE_PORT}/reports" | grep 'It works'

2 つのレスポンスにより、1 つの入口から異なる Service へルーティングできていることが確認できます。

1 つ目のレスポンスは NGINX、2 つ目は Apache httpd から返されます。ページの内容が異なるのは、プロキシが単に HTTP 200 を返したのではなく、パスルールによって別のバックエンドが選択されたことを示す意図的な証拠です。

一致する行として、次の内容が表示されます。

<title>Welcome to nginx!</title>
<h1>Welcome to nginx!</h1>
<title>It works! Apache httpd</title>
<p>It works!</p>

この結果から、カスタム Host ヘッダーが重要である理由も分かります。2 つのリクエストは同じノード IP と NodePort に到達しますが、どのルールとバックエンドを選択するかを決めるのはホスト名とパスです。

まとめ

準備済みの ingress-nginx コントローラーを再利用し、Ingress ルールを Ready 状態の Service に接続して、1 つの入口を通じたホストベースおよびパスベースのルーティングをテストしました。