マイクロサービスアーキテクチャ向けにサービスメッシュを提供する「Istio」の概要と環境構築、トラフィックルーティング設定

サービスメッシュとは

昨今ではサーバーインフラを提供するクラウドサービスやKubernetesなどのコンテナクラスタ管理ソフトウェアの利用が広まっている。そして、こういったクラウドサービスやコンテナクラスタでサービスを運用するためのアーキテクチャの1つとして、前回記事で紹介した「マイクロサービス」がある。

マイクロサービスアーキテクチャはサービスの構成要素を複数の小規模のプログラムに分割し、それらをそれぞれ独立して開発・運用できるようにするという手法だ。このアーキテクチャを採用して開発されたサービスを運用する場合、同時に多数のサービスを独立に動作させることが必要となる。たとえばインフラにクラウドやコンテナクラスタを利用する場合、基本的に1つの仮想マシンやコンテナで1つのマイクロサービス仮想マシンやコンテナを動かすのが一般的だ。そうすると運用・管理する仮想マシンやコンテナの数は大きく増えることになり、運用・管理作業を効率良く行うことが求められる。そのためのツールとして注目されているのが、「Service Mesh」(サービスメッシュ)だ。

サービスメッシュの背景

サービスメッシュが生まれた背景の1つとして、多くのクラウドやコンテナクラスタ環境においては、実行するサービスに割り当てられるIPアドレスを事前に知ることができないという制約の存在がある。そのため、何らかの方法を使ってサービスの稼働後にほかのサービスにアクセスするためのIPアドレスを知る必要がある(これは「サービスディスカバリ」などと呼ばれる)。また、負荷分散を行うためのロードバランシングや、外部からのリクエストのルーティングといった処理も必要となる。さらに、サービスに障害が発生した場合に、その影響を最小限に抑えるようなルーティング処理も求められる。

しかし、このような処理をすべてアプリケーション側に実装しようとすると多大なコストがかかる。特にマイクロサービスアーキテクチャではマイクロサービス毎に使用するプログラミング言語やデータベースが異なるケースもあり、そういった環境では言語毎に個別にネットワーク関連機能を作り込まなければならない。

そこで、こういったネットワーク関連の処理を別のソフトウェアレイヤーで行おう、という考え方が生まれた。このソフトウェアレイヤーこそがサービスメッシュである。

ちなみに「メッシュ」は「網の目」という意味だ。マイクロサービスアーキテクチャにおいては、各マイクロサービスが(コンテナやプロセス)がそれぞれ別のマイクロサービスと通信してサービスを実現する(図1)。その様子が網の目のようになることからこのように名付けられている。

図1 サービスメッシュのイメージ
図1 サービスメッシュのイメージ

クラウドサービスやコンテナクラスタを構成するインフラストラクチャによっては、こういった管理機能が提供されている場合もある。たとえばKubernetesでは、DNSベースでの名前解決やルーティング処理を行う「Service」という機能が提供されている。

Istioとは?

今回紹介するIstioは、このサービスメッシュを実現するためのソフトウェアだ。Kubernetesなどのコンテナベースのクラスタ環境で利用できる。

前述のとおりKubernetesではサービスメッシュ機能が提供されているものの、提供される機能はクラスタ運用に必要な基本的なもののみに留まっている。また、クラスタ外からのトラフィックに関する制御は基本的にはKubernetesでは提供されず、クラスタインフラ側の実装に依存していた。Istioではより高度なルーティング機能を提供し、さらにクラスタ外からのトラフィックの管理やトラフィック監視など、より高度な機能が提供されているのが特徴となる。

Istioのアーキテクチャ

Istioはコンテナ上で動作する「Envoy」というプロクシと組み合わせて各種機能を実現している。この構成こそがIstioの最大の特徴となる(図2)。

図2 Istioのアーキテクチャ
図2 Istioのアーキテクチャ

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の通りだ。

表1 Istioを構成するコンポーネント
コンポーネント名 役割 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)。

図3 NodePortを利用したトラフィックのルーティング
図3 NodePortを利用したトラフィックのルーティング

今回の例では、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のリファレンスマニュアル:GatewayIstioのリファレンスマニュアル: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)。

図4 今回構築するルーティング設定
図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の機能は前述したとおりルーティング設定の管理だけでなく、セキュリティや監視・管理のための機能もある。これらについては次回以降の記事で紹介する予定だ。