マイクロサービスアーキテクチャ向けにサービスメッシュを提供する「Istio」の概要と環境構築、トラフィックルーティング設定
サービスメッシュとは
昨今ではサーバーインフラを提供するクラウドサービスやKubernetesなどのコンテナクラスタ管理ソフトウェアの利用が広まっている。そして、こういったクラウドサービスやコンテナクラスタでサービスを運用するためのアーキテクチャの1つとして、前回記事で紹介した「マイクロサービス」がある。
マイクロサービスアーキテクチャはサービスの構成要素を複数の小規模のプログラムに分割し、それらをそれぞれ独立して開発・運用できるようにするという手法だ。このアーキテクチャを採用して開発されたサービスを運用する場合、同時に多数のサービスを独立に動作させることが必要となる。たとえばインフラにクラウドやコンテナクラスタを利用する場合、基本的に1つの仮想マシンやコンテナで1つのマイクロサービス仮想マシンやコンテナを動かすのが一般的だ。そうすると運用・管理する仮想マシンやコンテナの数は大きく増えることになり、運用・管理作業を効率良く行うことが求められる。そのためのツールとして注目されているのが、「Service Mesh」(サービスメッシュ)だ。
サービスメッシュの背景
サービスメッシュが生まれた背景の1つとして、多くのクラウドやコンテナクラスタ環境においては、実行するサービスに割り当てられるIPアドレスを事前に知ることができないという制約の存在がある。そのため、何らかの方法を使ってサービスの稼働後にほかのサービスにアクセスするためのIPアドレスを知る必要がある(これは「サービスディスカバリ」などと呼ばれる)。また、負荷分散を行うためのロードバランシングや、外部からのリクエストのルーティングといった処理も必要となる。さらに、サービスに障害が発生した場合に、その影響を最小限に抑えるようなルーティング処理も求められる。
しかし、このような処理をすべてアプリケーション側に実装しようとすると多大なコストがかかる。特にマイクロサービスアーキテクチャではマイクロサービス毎に使用するプログラミング言語やデータベースが異なるケースもあり、そういった環境では言語毎に個別にネットワーク関連機能を作り込まなければならない。
そこで、こういったネットワーク関連の処理を別のソフトウェアレイヤーで行おう、という考え方が生まれた。このソフトウェアレイヤーこそがサービスメッシュである。
ちなみに「メッシュ」は「網の目」という意味だ。マイクロサービスアーキテクチャにおいては、各マイクロサービスが(コンテナやプロセス)がそれぞれ別のマイクロサービスと通信してサービスを実現する(図1)。その様子が網の目のようになることからこのように名付けられている。
クラウドサービスやコンテナクラスタを構成するインフラストラクチャによっては、こういった管理機能が提供されている場合もある。たとえばKubernetesでは、DNSベースでの名前解決やルーティング処理を行う「Service」という機能が提供されている。
Istioとは?
今回紹介するIstioは、このサービスメッシュを実現するためのソフトウェアだ。Kubernetesなどのコンテナベースのクラスタ環境で利用できる。
前述のとおりKubernetesではサービスメッシュ機能が提供されているものの、提供される機能はクラスタ運用に必要な基本的なもののみに留まっている。また、クラスタ外からのトラフィックに関する制御は基本的にはKubernetesでは提供されず、クラスタインフラ側の実装に依存していた。Istioではより高度なルーティング機能を提供し、さらにクラスタ外からのトラフィックの管理やトラフィック監視など、より高度な機能が提供されているのが特徴となる。
Istioのアーキテクチャ
Istioはコンテナ上で動作する「Envoy」というプロクシと組み合わせて各種機能を実現している。この構成こそがIstioの最大の特徴となる(図2)。
Istio環境で実際にマイクロサービス間の通信を仲介する作業を行うのはこのEnvoyで、IstioはこのEnvoyの動作を設定・管理したり、Envoy経由でネットワークトラフィックの情報を取得する働きをする。
Envoyはさまざまな機能を持っているが、メインとなる機能はネットワークトラフィックの仲介だ。IstioおよびEnvoyではコンテナ内で動作するサービスの外向けトラフィックをEnvoyが受け取れるようにクラスタのネットワーク設定を変更し、受け取ったネットワークトラフィックをその内容に応じて別のコンテナ内で動作しているEnvoyに転送する。転送されたトラフィックを受け取ったEnvoyはそのトラフィックをコンテナ内で稼動しているサービスに転送する。つまり、サービス間の通信を各コンテナ内で動作するEnvoyが仲介するような形になる。
なお、IstioのようなEnvoyを管理するソフトウェアは「Control Plane(コントロールプレーン)」と呼ばれている。また、Envoyは必ずしもIstioを組み合わせて利用する必要はなく、別のコントロールプレーンと組み合わせて利用することも可能だ。
また、Kubernetesのトラフィックルーティング機能とEnvoyとの違いとして、Kubernetesでは単純にIPアドレスとポート単位(いわゆるトランスポート層レベル)でトラフィックの管理を行っているのに対し、Envoyは通信内容に応じたトラフィック管理(いわゆるアプリケーション層レベルでのルーティング)を行えるという点がある。たとえばHTTPのトラフィックであれば、リクエストメソッドやパスに応じてトラフィックの送信先を変える、といったような処理が可能になる。
ちなみに、このように本来プログラム本体が持つべき機能(Envoyの例ではネットワークトラフィック管理機能)を別のプログラムとして分離して実行させる手法は「サイドカーパターン」などと呼ばれている。
Istioが提供する機能
Istioが提供する機能は、次の3つとなる。
- トラフィックルーティングなどのトラフィック制御
- 認証やアクセス制限、暗号化などのセキュリティ
- 通信内容やトラフィック状況などの監視
まずトラフィック制御だが、前述した通りサービスディスカバリやトラフィックのルーティングといった機能が提供される。通信内容に応じたロードバランシングや、意図的に小規模なネットワークエラーを発生させてテストを行うための「Fault Injection」、トラフィックが集中した場合などに意図的に一部のトラフィックを遮断する「Cricuit breakers」といった機能もある。
セキュリティ支援機能としては、平文でやり取りされるトラフィックを暗号化された経路でラップする機能や、サービス/ユーザーの認証機能、サービスへのアクセスコントロールといった機能が提供される。
監視機能では、個々のリクエストの処理にかかった時間の記録やネットワークトラフィックの監視などが可能だ。Istioは監視データの記録に「Prometheus」を利用しており、取得したデータは「Grafana」を使って可視化できる。これによって容易にマイクロサービス間の通信状況を確認できる。複数のマイクロサービスにまたがる通信内容を追跡できる機能(分散トレーシング)も用意されている。
Istioを構成するコンポーネント
Istio自体もマイクロサービスアーキテクチャに従って設計されており、Istioが提供するこれらの機能はそれぞれ複数のコンポーネントによって提供される。これらコンポーネントはそれぞれ異なるコンテナとして実行され、適切に構築されたKubernetes環境上で実行するかぎり、自動的に冗長化された構成で稼動される。
なお、Istioを構成する個別コンポーネントの名称と提供する機能は表1の通りだ。
コンポーネント名 | 役割 | Kubernetes上でのDeployment名 |
---|---|---|
Envoy | プロクシとしてネットワークトラフィックを制御する | なし(各マイクロサービスのコンテナ内で実行される) |
Pilot | 指定したトラフィック制御ルールに応じたEnvoyの設定管理を行う | istio-pilot |
Citadel | 暗号鍵や証明書を管理する | istio-citadel |
Mixer | 認証や監査、各種データ収集などを行う | istio-policy、istio-telemetry |
Galley | Istioの設定や各種処理の実行などを行う | istio-galley |
Prometheus | 各種監視データの管理を行う | prometheus |
sidecar-injector | コンテナ内で「サイドカー」としてEnvoyプロセスを実行させる | istio-sidecar-injector |
Egress Gateway | クラスタ外へのトラフィックを管理する | istio-egressgateway |
Ingress Gateway | クラスタ内へのトラフィックを管理する | istio-ingressgateway |
Istio環境の構築
さて、続いては実際にIstioを利用できるクラスタ環境を構築していく流れを紹介していこう。
前提条件
Istioの利用には、まずコンテナエンジンとしてDockerが必要となる。また、対応するコンテナクラスタはKubernetes(バージョン1.9以降)もしくはNomad+サービスディスカバリツールConsul環境となっている。ただし、現時点ではNomadベースのクラスタでの利用は未テストというステータスのようだ。そのため今回は独自に構築したKubernetesベースのクラスタ上でIstioを利用する流れを説明する。
なお、IstioはKubernetesのServiceやPod、Deploymentといった機能と連携して動作するようになっている。そのため、利用にはKubernetesの知識が前提となる。本記事もKubernetesに関する知識がないと理解が難しい点があると思われるのでご注意いただきたい。
Kubernetes上での環境構築については公式ドキュメントにもまとめられている。今回は独自に構築したKubernetesクラスタ上にIstioをインストールするが、商用のKubernetesクラウドサービス上で利用する場合は独自の手順が必要となるので、その場合は該当する公式ドキュメントを参照して欲しい。
また、Istioの各種コンポーネントはKubernetes上のPod(Kubernetesがコンテナを管理する単位)として動作するが、これらPodのデプロイや設定を行うツールとして、Kubernetes向けのパッケージ管理ツールである「Helm」を利用することが推奨されている。そのため、今回はこのHelmを使ったIstio環境構築の手順を紹介する。作業の流れとしては以下のようになる。
- Kubernetes環境の確認
- Helmクライアントのダウンロードとインストール
- Istioのダウンロード
- Helmの利用に必要なPodの作成
- Helmを使ったIstioのインストール
ちなみにこれらの作業はkubectlコマンドを利用して行う。HelmやIstioを利用するのに必要なコンポーネントはKubernetesクラスタ上にデプロイされるため、クラスタ上のどのノードで作業を行っても構わないし、kubectlコマンドでの操作が可能であればクラスタに属していないマシン上での作業も可能だ。
Kubernetes環境の確認
Helmを使ったインストールでは、インストール作業を行うユーザーがkubectlコマンドでKubernetesクラスタを管理できる必要がある。事前にKubernetesなどの各種設定を行っておこう。
また、クラスタノードで利用できる空きメモリ容量が少ない場合、Istioのデプロイに失敗する場合がある。筆者が試したところ、少なくとも各ノードのメモリ容量が4GB以上あればとりあえずIstioのデプロイには成功するようだ。もちろんノード数などによってこの数値は変わってくるとは思うが、余裕を持った構成にしておくことをおすすめする。
なお、以下ではKubernetes 1.13ベースのクラスタでの動作確認を行っている。
Helmのインストール
Helmは、GitHubのHelmプロジェクトでソースコードが公開されている。また、WindowsやLinux、macOS向けのバイナリはリリースページから入手できる。Linux版バイナリはamd64/arm/arm64/i386/ppc64le/s390x向けのものが用意されているので、使用する環境に応じたものをダウンロードしよう。今回は一般的なAMD64環境で利用するので、「Linux amd64」向けバイナリ(記事執筆時点での最新版はv2.11.0)をダウンロードした。
ダウンロードしたアーカイブには「helm」および「tiller」という2つのバイナリとドキュメントが含まれている。helmがパッケージのインストール操作や管理などを行うフロントエンドで、tillerが実際の処理を実行するサーバープログラムとなる。ただし、今回はtillerは別途Kubernetesクラスタ上で実行させるため使用しない(後述)。そのため、helmコマンドのみをパスの通ったディレクトリにコピーしておこう。今回は「/usr/local/bin」ディレクトリにコピーした。
$ wget https://storage.googleapis.com/kubernetes-helm/helm-v2.11.0-linux-amd64.tar.gz $ tar xvzf helm-v2.11.0-linux-amd64.tar.gz linux-amd64/ linux-amd64/ # cp linux-amd64/helm /usr/local/bin
ちなみにこの時点ではtillerが動作していないため、helmコマンドを実行しても各種操作は行えない。たとえば「helm version」コマンドを実行すると、次のようにエラーとなる。
$ helm version Client: &version.Version{SemVer:"v2.11.0", GitCommit:"2e55dbe1fdb5fdb96b75ff144a339489417b146b", GitTreeState:"clean"} Error: could not find tiller
また、Helmは各クラスタノード上で処理を実行する際に「socat」コマンドを利用する。このコマンドが利用できない場合、helmコマンドでの操作時に「socat not found.」といったメッセージが表示される(helmリポジトリに投稿されたIssue)。筆者が構築したクラスタ環境(OSはDebian Linux 9.4)ではこのコマンドがデフォルトではインストールされていなかったため、手動で各クラスタノード上にこのコマンドをインストールした。
# apt-get install socat
Istioのダウンロード
続いてIstioをダウンロードして各種設定を行っていく。IstioについてもGitHub上でソースコードとLinuxおよびmacOS、Windows向けのバイナリが用意されているので、使用する環境に応じたものをリリースページからダウンロードしよう。リリースページではプレリリース版(スナップショット版)も同時に公開されているが、不具合などが残っている可能性があるのでプレリリース版は避けた方が良いだろう。今回は記事公開時の最新版であるバージョン1.0.4(Linux版)を選択した。
続いてダウンロードしたアーカイブを適当なディレクトリに展開し、そのトップディレクトリで各種作業を行う。
$ wget https://github.com/istio/istio/releases/download/1.0.4/istio-1.0.4-linux.tar.gz $ tar xvzf istio-1.0.4-linux.tar.gz $ cd istio-1.0.4
アーカイブ内には各種設定ファイルやドキュメントに加えて、Istioの管理コマンドである「istioctl」がbinディレクトリ内に格納されている。このistioctlコマンドもhelmコマンドと同様にパスの通ったディレクトリにコピーしておこう。
# cp bin/istioctl /usr/local/bin/
「Tiller」のインストール
Istioの配布アーカイブ中には、Helmで実際に各種処理を実行するサービスであるTillerをKubernetesクラスタ内で実行するための設定ファイルが用意されているので、これを利用してまずTillerを立ち上げる。
まず、Helmで利用するサービスアカウントを作成する。
$ kubectl apply -f install/kubernetes/helm/helm-service-account.yaml serviceaccount/tiller created clusterrolebinding.rbac.authorization.k8s.io/tiller created
続いて「helm init」コマンドを「--service-account tiller」コマンド付きで実行すると、TillerがKubernetesクラスタにデプロイされるとともに、Tillerにアクセスするための設定がホームディレクトリの.helmディレクトリ以下に作成される。
$ helm init --service-account tiller Creating /home/hylom/.helm Creating /home/hylom/.helm/repository Creating /home/hylom/.helm/repository/cache Creating /home/hylom/.helm/repository/local Creating /home/hylom/.helm/plugins Creating /home/hylom/.helm/starters Creating /home/hylom/.helm/cache/archive Creating /home/hylom/.helm/repository/repositories.yaml Adding stable repo with URL: https://kubernetes-charts.storage.googleapis.com Adding local repo with URL: http://127.0.0.1:8879/charts $HELM_HOME has been configured at /home/hylom/.helm. Tiller (the Helm server-side component) has been installed into your Kubernetes Cluster. Please note: by default, Tiller is deployed with an insecure 'allow unauthenticated users' policy. To prevent this, run `helm init` with the --tiller-tls-verify flag. For more information on securing your installation see: https://docs.helm.sh/using_helm/#securing-your-helm-installation Happy Helming!
デプロイが完了すると、各種helmのサブコマンドが実行できるようになる。たとえば「helm version」コマンドを実行すると、次のようにクライアント(helmコマンド)とサーバー(Tiller)のバージョンが確認できる。
$ helm version Client: &version.Version{SemVer:"v2.11.0", GitCommit:"2e55dbe1fdb5fdb96b75ff144a339489417b146b", GitTreeState:"clean"} Server: &version.Version{SemVer:"v2.11.0", GitCommit:"2e55dbe1fdb5fdb96b75ff144a339489417b146b", GitTreeState:"clean"}
なお、TillerはKubernetes上で「kube-system」というネームスペース上にデプロイされる。
$ kubectl -n kube-system get deploy NAME READY UP-TO-DATE AVAILABLE AGE coredns 2/2 2 2 3d23h tiller-deploy 1/1 1 1 3m49s
デプロイしたTillerを削除するには、「helm reset」コマンドを使用する。ここで「--remove-helm-home」オプションを指定すると、同時にホームディレクトリ下に作成された設定ファイルも削除される。
$ helm reset --remove-helm-home
Istioのインストール
以上でHelmが利用可能になったので、続いてhelmコマンドを使ってIstioの各コンポーネントをKubernetesクラスタ上にデプロイする。
$ helm install install/kubernetes/helm/istio --name istio --namespace istio-system --set gateways.istio-ingressgateway.type=NodePort --set gateways.istio-egressgateway.type=NodePort
ここでは、Istioの配布アーカイブの「install/kubernetes/helm/istio」ディレクトリ内にある設定ファイルに従ってデプロイするよう指定している。「--name istio」オプションは「istio」という名称を付けてデプロイするよう指示するもので、「--namespace」オプションは使用するネームスペースを指定するオプションだ。今回は「istio-system」とした。
「--set gateways.istio-ingressgateway.type=NodePort --set gateways.istio-egressgateway.type=NodePort」オプションは、Kubernetesの「NodePort」機能を使って外部からの流入トラフィックを受け入れる場合に必要な設定だ。Kubernetesではロードバランサ経由で外部からのトラフィックを受け入れるための仕組みが用意されているが、今回のように独自に構築したKubernetesクラスタではこの機能がそのままでは利用できない(別途ロードバランサと連携するためのコードを実装すれば利用可能ではあるが、ハードルが高い)。そのため、このオプションを指定してKubernetesの「NodePort」と呼ばれる機能を使ってポート番号ベースでのトラフィック受け入れを行うよう設定する(詳しくは後述)。
以上のコマンドを実行すると、Istioの各種コンポーネントがKubernetesクラスタ上にデプロイされるとともに、各種リソース設定が行われる。次のようなメッセージとともにデプロイ結果が表示されれば完了だ。
NAME: istio LAST DEPLOYED: Tue Dec 18 20:15:49 2018 NAMESPACE: istio-system STATUS: DEPLOYED
また、デプロイ済みの環境については「helm list」コマンドでも確認できる。ここで「STATUS」が「DEPLOYED」となっていれば、正常にデプロイが完了した状態となっている。
$ helm list NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE istio 1 Wed Dec 12 21:15:47 2018 DEPLOYED istio-1.0.4 1.0.4 istio-system
なお、デプロイ時に何かトラブルが発生した場合、次の操作を行うことで各種設定を削除できる。
$ helm delete --purge istio $ kubectl -n istio-system delete job --all $ kubectl delete -f install/kubernetes/helm/istio/templates/crds.yaml -n istio-system
自動インジェクションの設定
前述のとおり、IstioではKubernetesのPod内でEnvoyを稼動させ、それをプロクシとして使用することでさまざまな機能を実現している。そのため、Podの作成時にPod内でEnvoyを稼動させるための「sidecar injection」と呼ばれる設定が必要となる(Istioの公式ドキュメント)。これには、2通りの方法がある。まず1つは、istioctlコマンドを利用する方法だ。
istioctlコマンドでは、Podなどの設定が記述されたマニフェストファイルを「-f」オプションの引数として与えて実行することで、Envoyを実行するための設定を追加したマニフェストファイルを出力できる「kube-inject」サブコマンドが用意されている。これを利用して次のように実行することで、Envoyを組み込んだDeploymentやPodなどを作成できる。
$ istioctl kube-inject -f <Podなどの設定が記述されたマニフェストファイル> | kubectl apply -f -
ただ、DeploymentやPodなどの作成時に毎回このような作業を実行するのは面倒だ。そのため、Istioでは自動的にsidecar injectionを実行する仕組みが用意されている。この機能を有効にするには、自動sidecar injectionを利用したいネームスペースに対し「istio-injection=enabled」というラベルを付与すれば良い。たとえば「default」ネームスペースで自動sidecar injectionを有効にするには次のように実行する。
$ kubectl label ns default istio-injection=enabled
また、このラベルが付けられているかどうかは次のようにして確認できる。
$ kubectl get ns -L istio-injection NAME STATUS AGE ISTIO-INJECTION default Active 4d22h enabled istio-system Active 22h kube-public Active 4d22h kube-system Active 4d22h
Istioで動かすPod、Serviceの前提
IstioでPodやServiceの連携を行う場合、PodやServiceなどに対し若干ではあるが制約が課せられる(Istioのドキュメント)。具体的には次のとおりだ。
- Serviceでのポート定義には名前が設定されている必要がある
- 許可されているポート名のスタイルは「<プロトコル>」もしくは「<プロトコル>-<サフィックス>」
- プロトコルとして認識されるのは「http」「http2」「grpc」「mongo」「redis」
- それ以外はTCPトラフィックとして扱われる
- 複数のServiceに所属するPodの場合、異なるプロトコルを同じポート番号で利用することはできない
- Deploymentsでは「app」および「version」ラベルでアプリケーション名やバージョンを明示する
大きな制約ではないが、注意しておこう。
Istioでのトラフィック管理
さて、Istioでは前述のとおりトラフィック制御とセキュリティ、監視という3つの機能がある。いずれもさまざまな設定ができる多機能なものなので、まず今回はトラフィック制御について紹介していこう。
Istioで管理されるトラフィックのゲートウェイとなる「istio-ingressgateway」
まずは基本的なルーティングとして、外部からクラスタに向けたトラフィックを適切なServiceにルーティングする設定を紹介する。
前述のとおり、今回のような独自に実装したKubernetesクラスタではKubernetesのNodePort機能を利用して外部からのトラフィックを受け付けることになる。NodePortは、各クラスタノードの指定したポート宛のトラフィックを指定したServiceに自動的に転送する機能だ(図3)。
今回の例では、helmコマンドでのIstioのインストール時に「--set gateways.istio-ingressgateway.type=NodePort --set gateways.istio-egressgateway.type=NodePort」というオプションを指定した。このオプションを指定していた場合、Istioで外部トラフィックの受け入れ口となる「istio-ingressgateway」Serviceに対し、NodePort機能によって複数のポートが割り当てられる。これは次のようにして確認できる。
$ kubectl -n istio-system get service istio-ingressgateway NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE istio-ingressgateway NodePort 10.103.211.249 <none> 80:31380/TCP,443:31390/TCP,31400:31400/TCP,15011:32201/TCP,8060:31027/TCP,853:31338/TCP,15030:30176/TCP,15031:31502/TCP 22h
たとえばこの場合、80番ポート(HTTP)には31380ポートが割り当てられていることがわかる。これによってクラスタノードの31380番ポート向けの通信はistio-ingressgatewayに転送され、そこから指定したルールに従ってルーティングが行われるようになる。
ちなみに、istio-ingressgatewayに割り当てられているポートについての情報は次のように実行することで確認できる。ここで「ports」以下の部分が割り当てられているポートの情報になる。
$ kubectl -n istio-system get service istio-ingressgateway -o yaml apiVersion: v1 kind: Service metadata: creationTimestamp: "2018-12-18T11:15:50Z" labels: app: istio-ingressgateway chart: gateways-1.0.4 heritage: Tiller istio: ingressgateway release: istio name: istio-ingressgateway namespace: istio-system resourceVersion: "450585" selfLink: /api/v1/namespaces/istio-system/services/istio-ingressgateway uid: 46f763bd-02b6-11e9-8126-9ca3ba2dc475 spec: clusterIP: 10.103.211.249 externalTrafficPolicy: Cluster ports: - name: http2 nodePort: 31380 port: 80 protocol: TCP targetPort: 80 - name: https nodePort: 31390 port: 443 protocol: TCP targetPort: 443 - name: tcp nodePort: 31400 port: 31400 protocol: TCP targetPort: 31400 - name: tcp-pilot-grpc-tls nodePort: 32201 port: 15011 protocol: TCP targetPort: 15011 - name: tcp-citadel-grpc-tls nodePort: 31027 port: 8060 protocol: TCP targetPort: 8060 - name: tcp-dns-tls nodePort: 31338 port: 853 protocol: TCP targetPort: 853 - name: http2-prometheus nodePort: 30176 port: 15030 protocol: TCP targetPort: 15030 - name: http2-grafana nodePort: 31502 port: 15031 protocol: TCP targetPort: 15031 selector: app: istio-ingressgateway istio: ingressgateway sessionAffinity: None type: NodePort status: loadBalancer: {}
「Gateway」および「VirtualService」の設定
istio-ingressgatewayが受け付けたトラフィックをどのようにルーティングするかは、「Gateway」および「VirtualService」というリソースで管理される(Istioのリファレンスマニュアル:Gateway、Istioのリファレンスマニュアル:VirtualService)。
まずGatewayだが、こちらはその名の通りistio-ingressgatewayがトラフィックを受信する条件を設定するもので、ポート番号やプロトコル、ホスト名(待ち受けするホスト名)などが指定できる。前述のとおりIstioではアプリケーション層でのルーティングに対応しており、たとえばHTTPであれば「Hosts:」リクエストヘッダに応じて異なるルーティングを行える。
また、「VirtualService」はKubernetesのServiceに似たリソースで、受信したトラフィックのルーティング先を決定する条件を指定する。プロトコル毎に指定できるルーティング条件は異なるが、たとえばHTTPではURLやリクエストメソッド、リクエストヘッダの内容などに応じてルーティング先を変えることも可能だ。
たとえば、次のようなマニフェストファイル(http-echo-foo.yaml)を使って作成した「http-echo」Serviceに対しトラフィックをルーティングする設定を実際に行ってみよう。
apiVersion: v1 kind: Service metadata: name: http-echo-foo labels: app: http-echo-foo spec: ports: - name: http port: 80 protocol: TCP selector: app: http-echo-foo --- apiVersion: v1 kind: Pod metadata: name: http-echo-foo labels: app: http-echo-foo spec: containers: - name: http-echo-foo image: hashicorp/http-echo ports: - containerPort: 80 name: http args: - "-text=foo" - "-listen=:80"
http-echoサーバーは、HTTPでアクセスするとコンテナ作成時に指定した文字列(この例の場合は「foo」)を返すというシンプルなHTTPサーバーだ。このマニフェストファイルを使って次のようにリソースを作成すると、「http-echo-foo」という名前のPodと、このPodに接続するための「http-echo-foo」というServiceが作成される。
$ kubectl apply -f http-echo-foo.yaml
このHTTPサーバーに対し外部からのトラフィックをルーティングするには、次の2つのリソースを作成すれば良い。
- 外部からHTTPトラフィックを受け取るGateway
- 受け取ったトラフィックを「httpd-test」Serviceに転送するVirtualService
まずGatewayの設定だが、マニフェストファイルは次のようになる(http-gw.yaml)。
apiVersion: networking.istio.io/v1alpha3 kind: Gateway metadata: name: http-gateway spec: selector: istio: ingressgateway servers: - port: number: 80 name: http protocol: HTTP hosts: - "*"
内容としては、80番ポート(HTTP)向けのトラフィックを受け取る「http-gateway」という名前のGatewayを作成するというものだ。なお、NodePortを利用している環境では実際にトラフィックを受け取るポートはNodePort設定で指定されたポート(今回の例では31380番ポート)になる点に注意したい。また、「hosts:」では任意のホスト名に対応する「*」を指定している。ここで特定のホスト名を指定することで、「Hosts:」リクエストヘッダが指定したホスト名にマッチするトラフィックだけを受け入れることも可能だ。
続いてVirtualServiceの設定だが、こちらは次のようになる(http-echo-vsvc.yaml)。
apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: http-echo spec: hosts: - "*" gateways: - http-gateway http: - match: - uri: prefix: / route: - destination: port: number: 80 host: http-echo-foo.default.svc.cluster.local
これは「http-gateway」というGatewayが受け取ったトラフィックのうち、そのパスが「/」で始まるものを「http-echo-foo.default.svc.cluster.local」というホストの80番ポートに転送するという設定になっている。ちなみに「http-echo-foo.default.svc.cluster.local」は「default」ネームスペースの「httpd-echo-foo」Serviceに割り当てられたホスト名だ。VirtualServiceではこのようにFQDNで転送先のホスト名を指定することが推奨されている。
さて、これらのマニフェストファイルを指定して「kubectl apply」コマンドを実行すると、次のようにリソースが作成される。
$ kubectl apply -f http-gw.yaml gateway.networking.istio.io/http-gateway created $ kubectl apply -f http-echo-vsvc.yaml virtualservice.networking.istio.io/http-echo created $ kubectl get gateway NAME AGE http-gateway 1m $ kubectl get virtualservice NAME AGE http-echo 1m
この状態でクラスタノードの31380ポートに対しcurlコマンドでHTTPリクエストを送信すると、適切にルーティングが行われてhttp-echoサーバーから応答が帰ってくることがわかる。
$ curl http://127.0.0.1:31380 foo
また、「-v」オプションを指定して通信内容を確認すると、いくつかのヘッダからEnvoyによって通信が仲介されていることが確認できる。
$ curl -v http://127.0.0.1:31380 * Rebuilt URL to: http://127.0.0.1:31380/ * Trying 127.0.0.1... * TCP_NODELAY set * Connected to 127.0.0.1 (127.0.0.1) port 31380 (#0) > GET / HTTP/1.1 > Host: 127.0.0.1:31380 > User-Agent: curl/7.52.1 > Accept: */* > < HTTP/1.1 200 OK < x-app-name: http-echo < x-app-version: 0.2.3 < date: Wed, 19 Dec 2018 11:51:15 GMT < content-length: 4 < content-type: text/plain; charset=utf-8 < x-envoy-upstream-service-time: 1 < server: envoy < foo * Curl_http_done: called premature == 0 * Connection #0 to host 127.0.0.1 left intact
ちなみに、この作業はクラスタノード上で行っているのでホスト名として127.0.0.1(localhost)を指定しているが、クラスタノード外で作業を行っている場合は適当なクラスタノードのIPアドレスもしくはホスト名を指定すれば良い。
トラフィック内容に応じて転送先を振り分ける
続いてhttp-echoサーバーを実行するPodおよびServiceを追加し、トラフィックをそれぞれのServiceに振り返る設定を行ってみよう。新たに、次のようなマニフェストファイル(http-echo-bar.yaml)を作成し、「http-echo-bar」Serviceとそれに紐付けたPodを作成する。
apiVersion: v1 kind: Service metadata: name: http-echo-bar labels: app: http-echo-bar spec: ports: - name: http port: 80 protocol: TCP selector: app: http-echo-bar --- apiVersion: v1 kind: Pod metadata: name: http-echo-bar labels: app: http-echo-bar spec: containers: - name: http-echo-bar image: hashicorp/http-echo ports: - containerPort: 80 name: http args: - "-text=bar" - "-listen=:80"
これは先ほど用意した「http-echo-foo.yaml」とほぼ同じ内容だが、名前やラベルが異なるのと、HTTPサーバーが返す文字列を指定する「-text」引数の値を変更している点が異なる。続いてこのマニフェストファイルを元にリソースを作成する。
$ kubectl apply -f http-echo-bar.yaml
これで、「bar」という文字列を返す「http-echo-bar」および「foo」という文字列を返す「http-echo-foo」という2つのServiceと、これらに紐付けられたPodが作成された。
$ kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE http-echo-bar ClusterIP 10.100.136.25 <none> 80/TCP 5m41s http-echo-foo ClusterIP 10.110.23.17 <none> 80/TCP 20m kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 5d
続いて、先ほど作成した「http-echo-vsvc.yaml」を修正し、トラフィックのルーティングルールを変更する。たとえば次の設定は「/bar」というパスへのリクエストについては「http-echo-bar」Serviceに、それ以外のパスへのリクエストについては「http-echo-foo」Serviceにルーティングするものだ。ここでは「match」ルールを2つ記述し、それぞれ「prefix」と「host」の値を変更している。
apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: http-echo spec: hosts: - "*" gateways: - http-gateway http: - match: - uri: prefix: /bar route: - destination: port: number: 80 host: http-echo-bar.default.svc.cluster.local - match: - uri: prefix: / route: - destination: port: number: 80 host: http-echo-foo.default.svc.cluster.local
この設定を適用し、それぞれのパスを指定してcurlコマンドを実行してみると、応答が変わる(つまり接続先が変わっている)ことがわかる。
$ kubectl apply -f http-echo-vsvc.yaml $ curl http://127.0.0.1:31380 foo $ curl http://127.0.0.1:31380/bar bar $ curl http://127.0.0.1:31380/hoge foo
トラフィックの割り振りについては、さまざまな条件での指定が可能だ。たとえば次のように重みを指定する「weight」パラメータを指定することで、指定した重みに応じてトラフィックをそれぞれのサービスに振り分けることができる。
apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: http-echo spec: hosts: - "*" gateways: - http-gateway http: - match: - uri: prefix: / route: - destination: port: number: 80 host: http-echo-foo.default.svc.cluster.local weight: 10 - destination: port: number: 80 host: http-echo-bar.default.svc.cluster.local weight: 90
ここでは「destination」として「http-echo-foo.default.svc.cluster.local」と「http-echo-bar.default.svc.cluster.local」の2つを指定し、それぞれの重みを「10」と「90」に指定している。この設定を「kubectl apply」コマンドで反映させてcurlコマンドでアクセスしてみると、応答の割合が変わることが確認できる。
$ curl http://127.0.0.1:31380 bar $ curl http://127.0.0.1:31380 bar $ curl http://127.0.0.1:31380 bar $ curl http://127.0.0.1:31380 foo $ curl http://127.0.0.1:31380 bar
サービス間のルーティング設定
続いて、サービス間のルーティング設定についても見ていこう。KubernetesではServiceリソースを作成してPodと紐付けることで、サービスディスカバリやルーティングを実現していた。Istioを利用する場合もこの流れは基本的には変わらないが、IstioではVirtual Serviceを利用することで、さらに細かいルーティングルールを実現できる。
たとえば、フロントエンドに対するリクエストに応じてほかのServiceにリクエストを行い、その結果を返すようなサービスを考えてみよう(図4)。
今回はテストのため、フロントエンドにプロクシとして動作するnginxを設置し、nginxへのリクエストをhttp-echo Serviceに転送してその結果を返す、という環境を構築する。
まず、次のような内容のマニフェストファイル(http-echo-svc.yaml)を使って「http-echo」というServiceを作成する。
apiVersion: v1 kind: Service metadata: name: http-echo labels: app: http-echo spec: ports: - name: http port: 80 protocol: TCP selector: app: http-echo
$ kubectl apply -f http-echo-svc.yaml
次にこのhttp-echo Serviceに紐付けた「http-echo-foo」というPodを作成する。使用するマニフェストファイルは次のようになる(http-echo-foo2.yaml)。
apiVersion: v1 kind: Pod metadata: name: http-echo-foo labels: app: http-echo version: v1 spec: containers: - name: http-echo-foo image: hashicorp/http-echo ports: - containerPort: 80 name: http args: - "-text=foo" - "-listen=:80"
ここで、ラベルとして「version: v1」を指定している点に注目して欲しい。詳しくは後述するが、IstioではこのようにPodに対して指定したラベルを使ってトラフィック制御を行う仕組みになっている。
続いて同様に「http-echo-bar」というPodを作成する。こちらのマニフェストファイルは以下のようになる(http-echo-bar2.yaml)。内容としては「http-echo-foo2.yaml」とほぼ同じだが、ラベルとして指定した「version:」の値と、コンテナに与える引数が異なる。
apiVersion: v1 kind: Pod metadata: name: http-echo-bar labels: app: http-echo version: v2 spec: containers: - name: http-echo-bar image: hashicorp/http-echo ports: - containerPort: 80 name: http args: - "-text=bar" - "-listen=:80"
これらマニフェストファイルを使ってリソースを作成すると、作成されたPodがhttp-echo Serviceに紐付けられる。このサービスに割り当てられたIPアドレス(下記の例の場合「10.100.43.251」)に対しリクエストを送信すると、ランダムに「foo」もしくは「bar」という文字列が返されるようになる。
$ kubectl apply -f http-echo-foo2.yaml $ kubectl apply -f http-echo-bar2.yaml $ kubectl get svc http-echo NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE http-echo ClusterIP 10.100.43.251 <none> 80/TCP 4m2s $ curl 10.100.43.251 foo $ curl 10.100.43.251 bar $ curl 10.100.43.251 foo $ curl 10.100.43.251 bar
Kubernetesでは、Serviceに紐付けられたPodが複数存在する場合、デフォルト設定ではラウンドロビン方式でそれぞれのPodに均等にトラフィックをルーティングするようになる。そのため、リクエスト毎に「foo」を返すPodと、「bar」を返すPodにトラフィックが転送されることになる。
続いて、フロントエンドとなるnginxを稼動させるPodを作成する。今回は次のようなマニフェストファイル(nginx.yaml)を使用した。nginxの設定内容としてはシンプルなもので、「/」以下へのリクエストを「http://http-echo.default.svc.cluster.local/」というホストに転送するというものだ。同時に、このPodへのルーティングを行うための「http-nginx」というServiceも作成している。
apiVersion: v1 kind: ConfigMap metadata: name: nginx-proxy-config data: proxy.conf: | server { listen 80; location / { proxy_pass http://http-echo.default.svc.cluster.local/; proxy_http_version 1.1; } } --- apiVersion: v1 kind: Pod metadata: name: nginx labels: app: nginx spec: containers: - name: nginx image: nginx:1.15 volumeMounts: - name: config-volume mountPath: /etc/nginx/conf.d volumes: - name: config-volume configMap: name: nginx-proxy-config --- apiVersion: v1 kind: Service metadata: name: http-nginx labels: app: http-nginx spec: ports: - name: http port: 80 protocol: TCP selector: app: nginx
このマニフェストファイルを使ってリソースを作成後、作成したhttp-nginx Service(次の例の場合、IPアドレスは「10.108.2.24」)に対しHTTPリクエストを複数回送信すると、http-echoサービスにリクエストを送信した場合と同様、「bar」および「foo」が同じ割合で帰ってくることがわかる。
$ kubectl apply -f nginx.yaml NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE http-nginx ClusterIP 10.108.2.24 <none> 80/TCP 8s $ curl 10.108.2.24 bar $ curl 10.108.2.24 foo $ curl 10.108.2.24 bar $ curl 10.108.2.24 foo
この状態ではhttp-echoサービスに対応するVirtualServiceが作成されていないため、nginxからhttp-echoサービスへのルーティングはKubernetesのService機能によるルーティングがそのまま使われている。
続いて、http-echoサービスに対応するVirtualServiceを作成し、Istioでトラフィックを制御してみよう。マニフェストファイルは次のようになる(http-echo-route.yaml)。
apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: http-echo-version spec: host: http-echo.default.svc.cluster.local subsets: - name: v1 labels: version: v1 - name: v2 labels: version: v2 --- apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: http-echo-route spec: hosts: - http-echo.default.svc.cluster.local http: - match: - uri: prefix: / route: - destination: host: http-echo.default.svc.cluster.local subset: v1
このマニフェストファイル前半では、VirtualServiceによるルーティングの際の送信先を定義する「DestinationRule」を作成している。ここでは「version: v1」というラベルが設定されたPodを「v1」、「version: v2」というラベルが設定されたPodを「v2」として定義している。
また、後半部分は「http-echo.default.svc.cluster.local」宛のトラフィックを「http-echo.default.svc.cluster.local」に紐付けられたPodのうち「v1」に該当するPodのみに転送する、という指定だ。
このマニフェストファイルを適用した後にnginxに対しリクエストを送信すると、この設定が反映され、次のように「foo」だけが返されるようになる。
$ kubectl apply -f http-echo-route.yaml $ curl 10.108.2.24 foo $ curl 10.108.2.24 foo $ curl 10.108.2.24 foo $ curl 10.108.2.24 foo
また、VirtualService以下の設定を次のようにすれば、「/foo」というリクエストのみを「v1」に該当するPodに転送し、それ以外は「v2」に該当するPodに転送する、という設定が行える。
apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: http-echo-route spec: hosts: - http-echo.default.svc.cluster.local http: - match: - uri: prefix: /foo route: - destination: host: http-echo.default.svc.cluster.local subset: v1 - match: - uri: prefix: / route: - destination: host: http-echo.default.svc.cluster.local subset: v2
変更したマニフェストファイルを適用すると、次のように設定したとおりの動作になることがわかる。
$ kubectl apply -f http-echo-route.yaml $ curl http://10.108.2.24 bar $ curl http://10.108.2.24/foo foo $ curl http://10.108.2.24/hoge bar
Kubernetesよりも複雑なルーティング設定が可能に
このように、Istioを利用することでKubernetesのService機能によるルーティングよりも複雑なルーティング設定が可能になる。これを利用することで、たとえばアップデートの際にトラフィックの一部のみを新しいバージョンのプログラムに転送する「カナリアリリース」のような手法を簡単に実現できるようになる。
また、Istioの機能は前述したとおりルーティング設定の管理だけでなく、セキュリティや監視・管理のための機能もある。これらについては次回以降の記事で紹介する予定だ。