Amazon Web Services ブログ

Kubernetes アプリケーションの公開 Part 1: Service と Ingress リソース

この記事は Exposing Kubernetes Applications, Part 1: Service and Ingress Resources (記事公開日: 2022 年 11 月 22 日) を翻訳したものです。

はじめに

連載「Kubernetes アプリケーションの公開」では、Kubernetes クラスターで実行されているアプリケーションを、外部からのアクセスのために公開する方法に焦点を当てます。

連載の Part 1 では、Kubernetes クラスターでインバウンドトラフィックの制御を定義する 2 つの方法である Service と Ingress リソースタイプについて探ります。Service コントローラーと Ingress コントローラーによるこれらのリソースタイプの処理について説明し、その後、いくつかのコントローラーの実装バリエーションの利点と欠点について概要を説明します。

Part 2 では、Service と Ingress 両方のコントローラーの AWS のオープンソース実装である AWS Load Balancer Controller の概要を説明します。このコントローラーのセットアップ、設定、想定されるユースケース、制限事項について説明します。

Part 3 では、Ingress コントローラーのまた別のオープンソース実装である NGINX Ingress Controller について、AWS Load Balancer Controller とのいくつかの違いも含めて、同様のウォークスルーを行います。

モチベーション

Kubernetes は、コンテナ化されたアプリケーションをデプロイ、スケーリング、および管理することができるコンテナオーケストレーションエンジンです。

クラスター管理者とアプリケーション開発者は、クラスターとその中のアプリケーションのあるべき状態を記述する論理的なリソースを定義します。そして、Kubernetes のさまざまなメカニズムが、その状態を実現し維持するために動作します。

一部のアプリケーション (バッチ処理や非同期処理を行うアプリケーションなど) では、ネットワークアクセスを有効にする必要がない場合があります。RESTful なバックエンドサービスや Web アプリケーションなど、その他の場合はネットワークアクセスを有効にすることが必須です。

さまざまなユースケースと、これらのアプリケーションを公開する適切な方法を理解することは、スケーラブルで運用に適したクラスターインフラストラクチャをセットアップするために重要です。

Kubernetes Service

Kubernetes アプリケーションは、1 つまたは複数のコンテナを実行する 1 つまたは複数の Pod で構成されます。すべての Pod は独自の IP アドレスを取得します。Pod の IP アドレスがクラスターの外部からアクセス可能かどうかは、クラスターの Container Network Interface (CNI) プラグインの実装によって異なります。一部の CNI プラグインの実装では、Kubernetes ノード全体にオーバーレイネットワークを作成し、Pod の IP アドレスをクラスターの内部に保持します。Amazon Virtual Private Cloud (VPC) CNI プラグインは VPC の IP アドレスを使用するため、すべての Pod は VPC の CIDR 範囲から有効な IP アドレスを取得します。これにより、クラスターの内部と外部の両方で同じ IP アドレスにアクセスできます。

Pod の IP アドレスを使用して直接アプリケーションにアクセスすることは可能ですが、通常はアンチパターンです。Pod は非永続的なオブジェクトであり、スケーリングイベントやノードの入れ替え、あるいは設定の変更によって、クラスターのノード間を「移動」しながら作成および破棄される可能性があります。加えて、直接アクセス方式では、アプリケーションの負荷分散やルーティングの要件に対応できません。

これらの問題に対処し、アプリケーションを確実に公開するために、Service リソースタイプが導入されました。Service は、ラベルセレクターのセットで定義された Pod のセットを動的に構築する抽象化です。

この連載記事では、Kubernetes の用語に従います。リソースタイプ (例えば Service) は論理的な定義で、Kubernetes Application Programming Interface (API) コールを介して作成されたときにリソース (例えば Service) になります。リソースはオブジェクトとも呼ばれます。

ラベルセレクターを使用して、Deployment 内の Pod のセットにマッチする Service 定義の例を以下に示します。

apiVersion: v1
kind: Service
metadata:
  name: some-service
  namespace: some-namespace
spec:
  type: ClusterIP
  selector:
    app.kubernetes.io/name: some-app
  ports:
    - name: svc-port
      port: 80
      targetPort: app-port
      protocol: TCP
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: some-deployment
  namespace: some-namespace
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: some-app
  template:
    metadata:
      labels:
        app.kubernetes.io/name: some-app
    spec:
      containers:
        - name: nginx
          image: public.ecr.aws/nginx/nginx
          ports:
            - name: app-port
              containerPort: 80
...

Service の公開

Service リソースの公開方法は、その spec.type の設定により制御されます。今回の議論に関連するタイプは以下の通りです。

  • ClusterIP (上記の例) は、クラスター内部でのみアクセス可能な仮想 IP アドレスを割り当てます。これがデフォルトです。
  • NodePort は、各クラスターノード上の静的ポートを介して上記の ClusterIP を公開します。
  • LoadBalancer は、自動的に ClusterIP を作成し、NodePort を設定し、クラスターのインフラストラクチャ環境 (クラウドプロバイダーなど) に Service のバックエンドの Pod を公開するための負荷分散コンポーネントを作成することを指示します。

Service のターゲットとなる Pod が作成され準備が整うと、その IP アドレスは ClusterIP にマッピングされ、Pod 間の負荷分散が提供されます。各クラスターノード上の kube-proxy デーモンは、(デフォルトでは) iptables ルール で Linux カーネルがネットワークパケットをルーティングする際に使用するマッピングを定義しますが、ClusterIP 自体は実際にデータパス上にあるわけではありません。

Kubernetes は、組み込みの内部サービスディスカバリーと通信メカニズムも提供します。各 Service の ClusterIP には、<service-name>.<namespace-name>.svc.cluster.local 形式のクラスター内部でのみアクセス可能な DNS 名が提供され、クラスター内の Pod からアクセスできます。

外部からのアクセスを許可するには、LoadBalancer タイプが通常は好ましいソリューションです。これは、他のオプションに加えて負荷分散機能を、そして場合によっては追加の機能を備えているからです。AWS では、これらの機能には、ロードバランサーの種類によって、AWS WAF サービスによる Distributed Denial of Service (DDoS) 保護、AWS Certificate Manager による証明書管理など、さまざまな機能含まれます。

Kubernetes において、コントローラーはコントロールループパターンの実装です。その責務はさまざまな Kubernetes リソースによって定義された望ましい状態と、システムの実際の状態を調整 (reconcile) することです。Service コントローラーは、新しい Service リソースが作成されるのを監視し、spec.typeLoadBalancer の場合、コントローラーはクラウドプロバイダーの API を使用してロードバランサーをプロビジョニングします。そして、ロードバランサーのリスナーとターゲットグループを構成し、Service のバックエンドにある Pod をターゲットとして登録します。

プロビジョニングされたロードバランサーが Pod にルーティングする方法は、ロードバランサーの種類と Service コントローラーに固有のものです。例えば、AWS の Network Load BalancerApplication Load Balancer では、ターゲットグループを構成し、インスタンスターゲットタイプを使用してクラスターに関連付けられたノードの <NodeIP>:<NodePort> にルーティングするか、IP ターゲットタイプを使用して Pod の IP アドレスに直接ルーティングすることができます。

図式化すると、IP ターゲットタイプの場合、以下のようになります。

Service コントローラーによって作成されたロードバランサーを介した、クライアントから Pod へのトラフィックフロー

in-tree Service コントローラー

現在、Kubernetes の各バージョンと並行して、クラスターとクラウドプロバイダー API 間の統合に責任を持つ、クラウドプロバイダー固有のコードの対応するバージョンのリリースがあります。このコードは、最近まで Kubernetes のリポジトリ内に存在していたため、in-tree クラウドプロバイダーと呼ばれ、対応するクラウドプロバイダーの Kubernetes クラスターにデフォルトでインストールされています。Amazon EKS クラスターは、AWS クラウドプロバイダーを介して、ここでは in-tree Service コントローラーと呼ばれる Service コントローラーを含む AWS Cloud Controller Manager がプリインストールされています。

コントローラーがデプロイされていない Amazon EKS クラスターでは、Service オブジェクトを処理するのは in-tree コントローラーです。次の Service 定義は Classic Load Balancer を作成してノードに接続します (Classic Load Balancer はインスタンスターゲットタイプ相当の方式しかサポートしていません) 。

apiVersion: v1
kind: Service
metadata:
  name: some-service
  namespace: apps
  labels:
    app.kubernetes.io/name: some-service
spec:
  type: LoadBalancer
  selector:
    app.kubernetes.io/name: some-service
  ports:
    - name: svc-port
      port: 80
      targetPort: app-port
      protocol: TCP

アノテーションによって、in-tree コントローラーの構成を制御することができます。例えば、以下のようにすると、Classic Load Balancer の代わりに Network Load Balancer がプロビジョニングされます (この場合も、インスタンスターゲットタイプのみがサポートされます) 。

apiVersion: v1
kind: Service
metadata:
  name: some-service
  namespace: apps
  labels:
    app.kubernetes.io/name: some-service
  annotations:
    service.beta.kubernetes.io/aws-load-balancer-type: nlb
spec:
...

in-tree コントローラーは、コンテナ化されたアプリケーションに適した IP ターゲットタイプをサポートしていないことからもわかるように、比較的機能が制限されています。連載の Part 2 では、より機能が豊富な代替手段である AWS Load Balancer Controller について説明する予定です。

多数の Service を扱う

重要なアプリケーションには、外部に公開する必要のある多数の API エンドポイントが含まれる場合があります。このようなアプリケーションでは、これらの API エンドポイントを公開する Service ごとに 1 つずつ、何十ものロードバランサーが必要になります。これは、運用の複雑さとインフラストラクチャのコストをさらに増加させることになります。

考えられる解決策は、単一のロードバランサーを作成し、Service とその Pod をロードバランサーのターゲットグループに接続することです。これは比較的複雑な実装です。特に、さまざまな Service が独立した開発サイクルやデプロイサイクルを持つさまざまなチームに属している可能性を考慮すると、優先順位付けとマージのプロセスを確立し、これらのチームがロードバランサーの構成に何らかの形でアクセスできるようにする必要がありますが、これは多くの場合望ましくありません。

幸いなことに、Kubernetes はそのプロセスを抽象化した Ingress リソースタイプを提供しています。

Ingress とは?

Ingress は組み込みの Kubernetes リソースタイプで、Service と組み合わせて機能し、Service のバックエンドにある Pod へのアクセスを提供します。受信した HTTP/HTTPS トラフィックをバックエンドの Service にルーティングするためのルールと、ルールにマッチしない場合のデフォルトのバックエンドを定義します (TCP/UDP はサポートしていません) 。各ルールでは、期待するホスト、パス、およびマッチした場合にトラフィックを受け取るバックエンドを定義することができます。

Ingress の定義の例は、以下のようになります。

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: some-ingress
    annotations:     
      alb.ingress.kubernetes.io/load-balancer-name: ingress
      alb.ingress.kubernetes.io/target-type: ip
      alb.ingress.kubernetes.io/scheme: internet-facing
spec:
  ingressClassName: alb
  rules:
    - host: my.example.com
      http:
        paths:
          - path: /some-path
            pathType: Prefix
            backend:
              service:
                name: service-a
                port:
                  number: 80
          - path: /some-other-path
            pathType: Exact
            backend:
              service:
                name: service-b
                port:
                  number: 81
    - host: '*.example.com'
      http:
        paths:
          - path: /some-path
            pathType: Prefix
            backend:
              service:
                name: service-c
                port:
                  number: 82 
    - http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: service-d
            port:
              number: 80

上記のコードは、すべてのトラフィックを次のように定義しています。

  • my.example.com へのリクエストは
    • パスが some-path で始まる場合、service-a のポート 80 にルーティングされます
    • パスが some-other-path に等しい場合、 service-b のポート 81 にルーティングされます
  • example.com の他の直接のサブドメインへのリクエストは
    • パスが some-path で始まる場合、service-c のポート 82 にルーティングされます
  • ホスト名が定義されていないリクエストは、service-d のポート 80 にルーティングされます

これらのシナリオのいずれにも一致しない場合、リクエストは事前に定義されたデフォルトのバックエンド (Default Backend) にルーティングされます。

概要と完全な仕様については、Kubernetes の公式ドキュメントを参照してください。

加えて、いくつかのアノテーションがあります。まず、kubernetes.io/ingress.class は、Ingress リソースとその実装の間の接続を定義しています (訳注: 上記のマニフェストではアノテーションではなく ingressClassName フィールドで定義しています) 。alb.ingress.kubernetes.io/target-type は、プロビジョニングするロードバランサーのターゲットタイプを定義します。その他のアノテーションを使用して、実装固有の追加の設定パラメータを提供することもできます。

Ingress の実装

もし上記の Ingress リソースを Kubernetes クラスター内に作成したとしても、Kubernetes の キーバリュー型のデータストアである etcd 内にオブジェクト表現が作成される以外は、何も起きないでしょう。

Ingress オブジェクトの場合、必要な接続を作成し、負荷分散コンポーネントをプロビジョニングし、クラスターの状態を調整するのは、Ingress コントローラーの責務です。Kubernetes とともに配布される Controller Manager には、デフォルトの Ingress コントローラーは含まれていないため、別途インストールする必要があります。

この記事では、Ingress コントローラーの実装として考えられる 2 つのアプローチである「外部ロードバランサー」と「クラスター内リバースプロキシ」について、それぞれの違いやメリットデメリットを説明します。

外部ロードバランサー

このアプローチは、前に説明した Service ベースのアプローチに似ています。

外部ロードバランサーを使用した Ingress コントローラーの実装

Ingress コントローラーの AWS 実装である AWS Load Balancer Controller は、Ingress ルール、パラメーター、そしてアノテーションを Application Load Balancer の設定に変換し、リスナーやターゲットグループを作成して、そのターゲットをバックエンド Service に接続します。

このセットアップでは、可用性の高くスケーラブルなロードバランサーのマネージドサービスを使用することで、ルーティングメカニズムの監視、管理、およびスケーリングの複雑さをクラウドプロバイダーにオフロードします。DDoS 攻撃保護や認証などの追加機能も、ロードバランサーサービスで処理できます。

クラスター内リバースプロキシ

このアプローチの場合、クラスター内部のレイヤー 7 リバースプロキシ (NGINX など) に実装が委ねられます。リバースプロキシはクラスター外部からのトラフィックを受け取り、Ingress 設定に基づいてバックエンドサービスにルーティングします。

内部リバースプロキシを使用した Ingress コントローラーの実装

リバースプロキシのインストール、設定、そしてメンテナンスはクラスター運用者が行うことになります。これは「外部ロードバランサー」のアプローチがフルマネージドな Elastic Load Balancing サービスを利用するのとは対照的です。その結果、クラスターで実行されているアプリケーションのニーズに合わせてより高度なカスタマイズが可能になりますが、この柔軟性には代償が伴います。

リバースプロキシの実装は、データ経路上に新たな要素を置くことになり、レイテンシーに影響を与えます。さらに重要なことに、運用上の負担が大幅に増加します。前のアプローチの実装で使用されていたフルマネージドな Elastic Load Balancing サービスとは異なり、プロキシソフトウェアとそれが動作するインスタンスの監視、保守、スケーリング、およびパッチ適用はクラスター運用者の責任です。

場合によっては、2 つのコントローラーの実装を並行して使用し、クラスターの別々のセグメントを処理したり、組み合わせて完全なソリューションを形成したりできることにも注意してください。この例については、連載の Part 3 で紹介します。

Kubernetes Gateway API

実装の詳細には触れませんが、Kubernetes Gateway API (現在はベータ版) も、Service と Ingress リソースタイプに加えて、Kubernetes クラスターでアプリケーションを公開する方法を提供する仕様の 1 つです。

Gateway API では、抽象化をさらに次のように分解します。

  • GatewayClass は、IngressClass と同様に、API オブジェクトを処理するコントローラーを示します
  • Gateway は、Ingress と同様に、コントローラーと Gateway の定義に基づいて、エントリーポイントを定義し、負荷分散コンポーネントの作成をトリガーします
  • HTTPRoute (TLSRoute、TCPRoute、および UDPRoute も予定) は、Gateway をそのバックエンドにある Service に接続するルーティングルールを定義し、ホスト、ヘッダー、およびパスに基づいてトラフィックをマッチングし、重みによる分割を可能にします
  • ReferencePolicy は、どの Gateway を介してどのルートを公開できるか、およびどの Service を公開できるか (Namespace 横断を含む) を制御することを可能にします

以下は大まかな概要にすぎませんが、この記事の中で見たものとよく似ているはずです。

クライアントから Pod へのトラフィックルーティングを行う Gateway API 実装

上記の図には、コントローラーによる負荷分散コンポーネントの実際のプロビジョニングがありませんが、どちらかのルート (外部ロードバランサーまたはクラスター内リバースプロキシ) を取るか、サービスメッシュのような別のパラダイムにフックすることができます。

訳注: Kubernetes Gateway API については以下の AWS ブログ記事もご参照ください。

まとめ

連載の Part 1 では、Kubernetes クラスターで実行されているアプリケーションを公開するいくつかの方法について説明しました。外部ロードバランサーを利用する Service ベースと、外部ロードバランサーまたはクラスター内部のレイヤー 7 リバースプロキシを利用する Ingress ベースです。アプリケーションの公開方法をさらに制御できるようにすることを目的とした、新しい Kubernetes Gateway API についても簡単に紹介しました。

今後の連載では、AWS Load Balancer ControllerNGINX Ingress Controller を用いた Ingress ベースの実装に焦点を当て、それらのセットアップと設定について説明し、構成例についてウォークスルーを行います。

翻訳はプロフェッショナルサービスの杉田が担当しました。原文はこちらです。