Kubernetes上のPodにリクエストを送信する方法

6 min read読了の目安(約5500字

記事の概要

この記事では、Kubernetes上のPodに外部からリクエストを送る際にどのように設定すればいいか、また設定をした際にどのようにルーティングされPodに伝わるかについて解説します。
なお、この記事はKubernetes入門者向けの記事です。そのため、主に全体像を伝えることを主眼としており、多少端折っている部分もあります。

Kubernetes全体像

まず初めに、Kubernetesの全体像について簡単に説明します。

kubernetes architecture

*) 理解しやすいためdockerと書いていますが、本来はcontainer runtimeと呼ばれるものであればなんでも大丈夫です。

上図のように、KubernetesはMasterとNodeから構成されます。
基本的にPodはNodeのVM上で動作し、Master VM上にあるコンポーネントはそれらを管理する役割を担います。
EKSやAKS, GKEといったクラウドプロバイダーのマネージドなサービスの場合、このMasterの部分をプロバイダー側で管理してくれます。

Masterのコンポーネント

Masterにはapiserver, controller-manager, scheduler, etcdといったコンポーネントが存在します。
今回のPodへのリクエストルーティングにおいては、これらのコンポーネントには役割がないので省略します。
もし各コンポーネントの役割が知りたい場合は、公式ページを参照してください。

Nodeのコンポーネント

各コンポーネントにはkubelet, docker, kube-proxyといったコンポーネントが存在します。

  • docker
    • 実際にコンテナを実行するコンポーネントです
    • dockerの他にもCRI(Container Runtime Interface)を実装したcontainerdやCRI-Oなどを使うこともできます
  • kubelet
    • dockerに対してどんなコンテナを実行したらいいかを制御するコンポーネントです
    • Kubernetes Masterから要求を受け取りコンテナの起動や停止をdockerに対して指示します
  • kube-proxy
    • kube-proxyはそれぞれのNode VMに存在し、そのNodeにおけるネットワークルールを制御するコンポーネントです。
    • このコンポーネントによりKubernetesクラスター内部からや外部からの通信をPodに伝えることができます
    • ※今回のメインコンポーネントですね

それぞれのコンポーネントについてより詳細が知りたい場合は公式ページを参照してください。
ほんとによくまとまっています。

Podは正常に動いているのに外部からのリクエストがうまく届いていないといった場合にはkube-proxyが動いているか確認すれば原因がわかるかもしれません。(より前段でパケットがドロップされている場合もありますが)

Serviceによるルーティング

Kubernetesにおいて最もシンプルなルーティング方法はServiceリソースを使う方法です。

service-example.yaml
apiVersion: v1
kind: Service
metadata:
  name: my-service
spec:
  type: ClusterIP # このタイプによってServiceの挙動が変わる
  selector:
    app: my-app
  ports:
  - protocol: TCP
    port: 80
    targetPort: 8080

上記の例ですと、my-serviceというServiceオブジェクトの80番ポートに来たリクエストをmy-appの8080番ポートに転送します。
KubernetesではPodという単位でアプリケーションが動いているのでそのPodのIPに直接アクセスできなくはないのですが、以下のような問題があるため、いったんServiceというロードバランサーのようなオブジェクトを経由してアクセスさせることが一般的です。

  • 問題: 異なるホスト上のPodにアクセスできない
    • PodのIPとはコンテナのIPなので各Nodeのdockerによって付与されるものです
    • そのため基本的に別のVM上にデプロイされたPodのIPアドレスはわかりません
    • つまりPodがデプロイされる場所によって通信できたりできなかったりといった非常に不安定な状態になります

Serviceはtypeによって挙動が変わります。
ここでは一つずつ解説したいと思います。

type=ClusterIPの場合

type=ClusterIPの場合はServiceをKubernetesクラスター内部のIPで公開します。
このタイプではServiceはクラスター内部からのみアクセスでき、外部からのアクセスはできません。
このタイプはデフォルトのServiceタイプで、作成時にtypeを省略した場合はこのタイプが使用されます。

service access by ClusterIP

ちなみにPodへのルーティングはkube-proxyが担当するので、kube-proxyはリクエストの宛先Podが自Nodeで動いていればそのPod(コンテナ)へリクエストを送信し、そうでなければ対象のNodeに転送します。
このあたりの処理にはiptablesが用いられてたりします。

type=NodePortの場合

type=NodePortの場合は、各Node VMのIPを使ってServiceを公開します。
Node VMのIPを使うので、クラスター内部からだけでなくそのVMにアクセスできる外部のアプリからもアクセスすることができます。
このタイプの場合、そのNodePortのServiceが転送する先のClusterIP Serviceも自動で作成されます。

service access by NodePort

type=LoadBalancerの場合

type=LoadBalancerの場合は、直接Node VMのIPアドレスにアクセスするのではなく、その前段にある外付けのロードバランサーと連携してServiceを公開します。
クラスター外部にあるロードバランサーが転送する先のNodePortとClusterIP Serviceは自動的に作成されます。
下図のように外部からのリクエストは外部のロードバランサー経由でNodeのIPにアクセス、その後kube-proxyによってコンテナまでリクエストが送信されるといった経路をたどります。

service access by LoadBalancer

このタイプを使用するためにはKubernetesクラスターとは別にロードバランサーを準備する必要があります。なお、各クラウドプロバイダーのマネージドサービスを利用時にはこのロードバランサーも準備してくれます。

Ingressを使ったより高度なルーティング

KubernetesではIngressというリソースを使ってより高度なルーティングを実現できます。
イメージとしてはServiceリソースがロードバランサーなのに対し、Ingressリソースはリバースプロキシのようなものだととらえていただくとわかりやすいです。

Ingressでできること

  • SSLの終端
    • IngressリソースにTLSの鍵を設定するとその鍵を使ってTLSを終端してくれます。
  • 名前ベースのVirtual Host
    • 例えば、a.example.comでアクセスされた場合はmy-app-aに、b.example.comでアクセスされた場合はmy-app-bといった具合に、同じIPアドレスでもホスト名によって宛先を変えてリクエストを送れます。
  • パスベースのルーティング
    • 例えば、/aの場合はmy-app-aに、/bの場合はmy-app-bといったように、パスごとに別のアプリにリクエストを振り分けることができます。

Ingressの例

ingress-example.yaml
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: ingress-example
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: / # 後述のNginx Ingress Controllerを使用する場合に/aや/bを/で置き換えてアプリにリクエストするための設定です
spec:
  rules:
  - host: myapp.example.com
    http:
      paths:
      - path: /a
        backend:
          serviceName: my-app-a
          servicePort: 4200
      - path: /b
        backend:
          serviceName: my-app-b
          servicePort: 8080
  - http:
      paths:
      - backend:
          serviceName: default-app
          servicePort: 80

上記の例ですと、myapp.example.com/aでアクセスした場合にはmy-app-aの4200番ポートに、myapp.example.com/bの場合はmy-app-bの8080番ポートに、それ以外の場合はdefault-appの80番ポートにリクエストを送ってくれます。

Ingress Controllerについて

実はIngressリソースは作成しただけでは動作しません。
Ingressリソースを読み取り、その内容によってリクエストを振り分けるIngress Controllerが別途必要になります。
Ingress ControllerはNginx Ingress Controllerなどが有名ですね。

Ingress Controllerは自身にリクエストが来た際、Kubernetesに登録されているIngressリソースを確認し、その定義に従ってリクエストを転送します。
そのため、Ingress Controller自体のServiceリソースはtype=NodePortやtype=LoadBalancerで外部に公開し、各アプリケーションはIngress Controllerからのリクエストのみ通信できればいいのでtype=ClusterIPでServiceリソースを作成します。

Ingress Controller routing

まとめ

この記事ではServiceによるルーティングとIngressによるルーティングについて解説しました。
基本的にはServiceリソースでアプリケーションを公開し、より高度なルーティングが必要になればIngressリソースを活用すればいいと思います。