サービスメッシュを使ってみよう

2020年10月15日(木)
宮崎 星冬(みやざき せいとう)

はじめに

前回は、Tekton(OpenShift Pipeline)を用いてCI/CDパイプラインを作成し、ソースコードの更新を契機に自動でパイプラインを動かす方法を紹介しました。

本連載は、今回で最終回となります。今回は、マイクロサービスを管理する手法の1つ「サービスメッシュ」について、これまでに開発してきたサービスをサービスメッシュを用いて実際に管理する方法を解説します。

サービスメッシュとは

第1回で、マイクロサービス導入における課題として以下の3つを挙げました。

  • リソースの増加
  • 運用負荷の増大
  • 多様化・複雑化するランタイムソフトウェア/ミドルウェアの管理

これらのうち、運用負荷の増大についは「Kubernetesがある程度運用負荷を軽減できる」と述べました。しかし、Kubernetesのみで数十~数百のマイクロサービスを運用していくのは困難だというのが実情です。

このような運用負荷の増大に対する1つの解として「サービスメッシュ」があります。サービスメッシュとは、マイクロサービスの各サービス間の通信を専用のソフトウェアに仲介させることで、マイクロサービス特有の課題を解決する仕組みです。

サービスメッシュが提供する代表的な機能として、以下のようなものがあります。

  • トラフィック管理
    各サービスに流れるトラフィックを細かく制御できます。例えば、あるサービスの応答の遅延が他の多数のサービスにまで影響を及ぼすことがあります。
    サービスメッシュによって応答しないサービスへのアクセスを遮断することで、応答遅延の波及を阻止できます。
    また、リクエストの10%を新しいバージョンに割り振るといったことも可能なので、A/Bテストやカナリアリリースの実現にもとても有用です。
  • セキュリティ
    各サービス間で行われる通信の暗号化やアクセスの認証・認可機能を提供します。例えば、Kubernetesの内部では基本的に自由に通信できるため、脆弱性の悪用等であるPodが乗っ取られてしまうと、その影響がクラスター全体に波及してしまう可能性があります。
    サービスメッシュの機能である相互TLS(mTLS)を利用するすることで盗聴や中間者攻撃(MITM)を抑止できます。
    また認証・認可機能により予め通信できるサービスを制限しておくことで、開発環境から本番環境へのアクセスを防止したり、脆弱性の影響を局所化したりできます。
  • 可観測性
    各サービス間で行われる通信のメトリクスを容易に取得できます。マイクロサービスでは1リクエストを処理するのにも複数のサービスが連携して対応するため、問題発生時にチェックすべき箇所がコンテナ基盤や各サービスのログ等多岐に渡り、リクエストがどのように処理されているかの全体像を把握するのが難しくなります。サービスメッシュの分散トレーシング機能を活用すると、マイクロサービスで応答遅延や障害などが発生した際もボトルネックの特定が容易となり、迅速に対応できます。

このように豊富な機能を持つサービスメッシュを活用することで、各サービスの開発者はビジネスロジックの実装に集中できるます。

各サービスの開発言語に依存せずサービスメッシュの機能を提供する手法として、現在では「サイドカーパターン」が主流となっています。

サイドカーパターンでは各々のサービスに通信を仲介するプロキシを横付け、全ての通信をプロキシ(データプレーン)を経由して行います。プロキシはコントロールプレーンから指示を受けて、通信可否や通信先を決定します。

一方で、サービス側からは他のアプリケーションと直接やりとりをしているように見えるため、サービスメッシュの存在を意識する必要がありません。これにより、サービス側のソースコードを変更することなく、サービスメッシュの導入が可能となりました。

サービスメッシュを提供するソフトウェアは数多くありますが、その中でも注目を集めているのがIstioです。IstioはGoogle、IBM、Lyftが主体となり開発しているOSSで、プロキシのEnvoyと組み合わせて利用します。

他のサービスメッシュと比較してもIstioは特にトラフィック管理とセキュリティの面で機能が豊富なため、直近でもコミュニティにおいて活発に開発されています。

Istioの設定はKubernetesと同様にYAMLファイルで管理しますが、その機能の豊富さゆえに、サービスメッシュが拡大していくとともに設定が煩雑となりがちなのが欠点でした。しかし、Istioに特化したGUI管理ツールのKialiを用いるとGUI画面でIstioの設定変更が可能となります。

YAMLでの操作に慣れた運用者でも、Kialiの画面上でYAMLファイルを編集することで、IDEのような自動補完やアラート機能を活用できます。これらのツールに加え、分散トレーシングツールのJaegerを利用すると各サービス内外のリクエストの流れを可視化でき、マイクロサービスのトラブルシューティングも容易にできるようになっています。

Red Hat OpenShift Container Platform(以下、OpenShift)においては、Red Hat OpenShift Service Mesh(以下、OpenShift Service Mesh)としてIstio、Envoy、Kiali、Jaeger等をRed Hat社のサポートと合わせて利用できます。

OpenShift Service Meshの概要図(注:OpenShift本体のコンポーネント等は省略)

今回のゴール

今回は、まずOpenShift Service MeshをOpenShiftに導入します。サービスメッシュの機能は非常に幅広く全てを紹介できないので、今回はセキュリティの面に特化して紹介します。具体的には、前回までに構築したマイクロサービスにmTLSによる相互認証、暗号化とアクセス制御を導入します。

これにより、マイクロサービス内で行われる通信を中間者攻撃等から守り、意図しない通信をブロックすることによるセキュリティ強化をゴールとします。

今回実装していくマイクロサービスの構成を下図に示します。なお、この図は分かりやすさを重視するため、直接関係する要素以外は簡略化しています。

今回構築するマイクロサービスの構成図

  1. 利用者からのアクセスをOpenShift Ingressが受け取る…①
  2. OpenShift IngressがIstioのIngress Gatewayにアクセスを転送する。このルーティングはOpenShiftのRouteリソースを基にIngress Operatorが配布した情報を基に行う…②
  3. Ingress Gatewayは、Pilotから配布された情報を元にKubernetesのServiceを利用して各Podにアクセスをルーティングする。各Podと通信する際はmTLSで相互認証と暗号化を行う。…𖯄
  4. マイクロサービス内の各Podが通信する際にはPilotから配布された情報とServiceを利用する。この際もmTLSによる相互認証と暗号化を行う。…④

前提条件

利用ソフトウェアは、以下の通りです。

  • OpenShift v4.5.7
  • Elasticsearch Operator 4.5.0-202008100413.p0
  • OpenShift Service Mesh v1.1.7
    Red Hat社のドキュメントによると、OpenShift Service Mesh v1.1.7は以下のコンポーネントにより構成されています。
    • Istio v1.4.8
    • Jaeger v1.17.4
    • Kiali v1.12.7
    • 3Scale Istio Adapter v1.0.0

なお、今回の内容は前回までに実施した作業の続きとなります。ぜひ、第1回からお読みください。

またsampleサービスと同様に、ソースコードのpushを契機にcallerサービスが動的にビルド・デプロイされるよう、事前にEventTriggerの設定が必要です。YAMLの記述および各種設定については第4回を参考に作成してください。

OpenShift Service Meshのインストール

前回のOpenShift Pipeline同様、OpenShift Service MeshもOperatorHubを利用して簡単にインストールできます。ただし、前回とは異なりOpenShift Service Meshが動作するためには以下のOperatorも必要です。

  • Elasticsearch Operator
  • Red Hat OpenShift Jaeger
  • Kiali Operator

以下の手順では、OpenShift Service Meshの動作に必要なOperatorを全てインストールしていきます。なお、既に利用している等の理由で前提条件となるOperatorがインストール済みであれば、そのOperatorについてはインストールは不要です。

まず、OpenShiftのWebコンソールにcluster-admin権限を持ったユーザーでログインします。Administratorのパースペクティブで「Operators」⇒「OperatorHub」の順に選択します。

なお、今回インストールするOperatorは全てRed Hatが提供するOperatorです。そのため、Red Hatのサポートが受けられないCommunity Operatorを誤って選択しないようにするため、事前に「Provider Type」の箇所で「Red Hat」のみにチェックを付けておくことをお勧めします。

Elasticsearchのインストール

OperatorHubの画面を開いたら、Filter by keywordボックスを使用して「Elasticsearch Operator」を検索します。続いて、Operatorの詳細が表示されるので、左上の「Install」ボタンをクリックして次の画面に進みます。

「Install Operator」画面では様々なオプションを設定しますが、基本的にデフォルトの設定で問題ないため、このまま「Install」ボタンをクリックするとインストールが完了します。

Jaegerのインストール

先ほどと同様に「Red Hat OpenShift Jaeger」をOperatorHubから検索します。Operatorの詳細が表示されるので、左上の「Install」ボタンをクリックして次の画面に進み、こちらもデフォルト設定のまま「Install」ボタンをクリックすればインストールは完了です。

Kialiのインストール

こちらも同様に「Kiali Operator」をOperatorHubから検索します(本稿執筆時では全く同名のCommunity Operatorがありましたので注意してください)。Operatorの詳細が表示されるので、左上の「Install」ボタンをクリックして次の画面に進み、こちらもデフォルト設定のまま「Install」ボタンをクリックすればインストールは完了します。

ひとまず、ここまでのインストール状況を確認してみましょう。Administratorのパースペクティブで「Operators」⇒「Installed Operators」の順に選択します。インストールが正常に行われていれば「Elasticsearch Operator」「Red Hat OpenShift Jaeger」「Kiali Operator」の3つのOperatorが表示され、「Status」欄が「Suceeded」「Up to date」となっているはずです。

各Operatorが正常にインストールされていることが確認できたら、先に進みましょう。

Service Mesh Operatorのインストール

OpenShift Service Meshを利用するにはOperatorのインストールが必要です。インストール方法は今までと同様に「Red Hat OpenShift Service Mesh」をOperatorHubから検索します。Operatorの詳細が表示されるので、左上の「Install」ボタンをクリックして次の画面に進み、こちらもデフォルト設定のまま「Install」ボタンをクリックすればインストールは完了です。

Operatorが正常にインストールされたか確認しましょう。先ほどの「Installed Operators」画面に「Red Hat OpenShift Service Mesh」が追加され、「Status」欄が「Suceeded」「Up to date」となっていれば、OpenShift Service Meshのインストールは正常に完了しています。

コントロールプレーンの準備

前述のサイドカーパターンの部分で説明した通り、OpenShift Service Mesh(Istio)はコントロールプレーンとデータプレーンの2つで構成されています。そこで、まずはコントロールプレーンを準備します。

ここからはCLIを使用して進めていきましょう。まずOpenShiftにcluster-admin権限を持つユーザーでログインし、istio-systemプロジェクトを作成します。

[ec2-user@ip-xx-xx-xx-xx ws_miyazaki]$ oc whoami
system:admin
[ec2-user@ip-xx-xx-xx-xx ws_miyazaki]$ oc new-project istio-system
Now using project "istio-system" on server "https://api.sample.com:6443".

You can add applications to this project with the 'new-app' command. For example, try:

    oc new-app ruby~https://github.com/sclorg/ruby-ex.git

to build a new example application in Ruby. Or use kubectl to deploy a simple Kubernetes application:

    kubectl create deployment hello-node --image=gcr.io/hello-minikube-zero-install/hello-node

続いて、ServiceMeshControlPlaneリソースを作成します。Red Hat社からサンプルのYAMLが提供されているので(サンプルはこちらの「istio-installation.yamlの例」から入手可能)、今回はそれを利用してデプロイします。

なお、この後の手順で利用するIstio OpenShift Routing(IOR)と呼ばれるIstioとOpenShiftの連携機能を有効化するため、1箇所サンプルとは異なる設定を行います。

apiVersion: maistra.io/v1
kind: ServiceMeshControlPlane
metadata:
  name: basic-install
spec:
  istio:
    global:
      proxy:
        resources:
          requests:
            cpu: 100m
            memory: 128Mi
          limits:
            cpu: 500m
            memory: 128Mi
    gateways:
      istio-egressgateway:
        autoscaleEnabled: false
      istio-ingressgateway:
        autoscaleEnabled: false
        ior_enabled: true # ここをfalse --> trueに変更
    mixer:
      policy:
        autoscaleEnabled: false
      telemetry:
        autoscaleEnabled: false
        resources:
          requests:
            cpu: 100m
            memory: 1G
          limits:
            cpu: 500m
            memory: 4G
    pilot:
      autoscaleEnabled: false
      traceSampling: 100
    kiali:
      enabled: true
    grafana:
      enabled: true
    tracing:
      enabled: true
      jaeger:
        template: all-in-one

サンプルに沿ってistio-installation.yamlを作成したら、実際にOpenShiftにデプロイしてみましょう。

[ec2-user@ip-xx-xx-xx-xx ws_miyazaki]$ oc create -n istio-system -f istio-installation.yaml
servicemeshcontrolplane.maistra.io/basic-install created
[ec2-user@ip-xx-xx-xx-xx ws_miyazaki]$ oc get smcp -n istio-system
NAME            READY   STATUS           TEMPLATE   VERSION   AGE
basic-install           PausingInstall   default    v1.1      13s
[ec2-user@ip-xx-xx-xx-xx ws_miyazaki]$ oc get smcp -n istio-system
NAME            READY   STATUS              TEMPLATE   VERSION   AGE
basic-install   9/9     InstallSuccessful   default    v1.1      2m32s
[ec2-user@ip-xx-xx-xx-xx ws_miyazaki]$ oc get po -n istio-system
NAME                                      READY   STATUS    RESTARTS   AGE
grafana-577fd9ffc7-c8dph                  2/2     Running   0          4m25s
istio-citadel-6f9b74b754-t994m            1/1     Running   0          6m5s
istio-egressgateway-64ffbdb8c8-r27hm      1/1     Running   0          4m47s
istio-galley-7c6fb78655-pk4ds             1/1     Running   0          5m29s
istio-ingressgateway-6c77fdbbd4-j7vnn     1/1     Running   0          4m47s
istio-pilot-f74779745-hcx8j               2/2     Running   0          5m
istio-policy-884697ff7-f9cxd              2/2     Running   0          5m16s
istio-sidecar-injector-66fd9459d9-kh8v4   1/1     Running   0          4m40s
istio-telemetry-5d8b8bf754-b6rpl          2/2     Running   0          5m16s
jaeger-74f88db84f-vng5n                   2/2     Running   0          5m30s
kiali-597dc4968c-k6d4l                    1/1     Running   0          3m38s
prometheus-5fd75799b5-vrfss               2/2     Running   0          5m53s

初回インストールのため多少時間がかかりますが、2分程度でコントロールプレーンのデプロイが完了します。

データプレーンの準備

続いて、データプレーンの準備を進めていきましょう。

既存アプリケーションの確認

今回の様に既にアプリケーションをデプロイしているOpenShiftプロジェクト(Namespace)では、OpenShift Service Meshを有効化する前に、動作している既存のアプリケーションに影響が及ばないか必ず確認しましょう。

なお、今回はmicrosvc-sampleというプロジェクトにマイクロサービスをデプロイしている前提で進めていきます。別のプロジェクト名を使用している場合は、以下の説明を適宜読み替えてください。

・Routeの設定

OpenShift Service Meshを有効にすると、Istioを経由していないRouteはデフォルトでブロックされてしまうので、事前に現在有効にしているRouteを確認しましょう。oc get routeで現在の設定状況を見てみます。

[ec2-user@ip-xx-xx-xx-xx ws_miyazaki]$ oc get route -n microsvc-sample
NAME        HOST/PORT                                    PATH   SERVICES    PORT            TERMINATION   WILDCARD
caller      caller-microsvc-sample.apps.example.com             caller      8080                          None
el-caller   el-caller-microsvc-sample.apps.example.com          el-caller   http-listener                 None
el-sample   el-sample-microsvc-sample.apps.example.com          el-sample   http-listener                 None
sample      sample-microsvc-sample.apps.example.com             sample      8080                          None

ここに表示されているRouteを継続して機能させるには、以下のいずれかの操作を行う必要があります。

  1. サービスメッシュの管理対象とする
  2. サービスメッシュをインストールしない別のOpenShiftプロジェクト(Namespace)に対応するServiceやPodとともに移行する
  3. サービスメッシュの例外として追加し、今まで通り利用する

ここで表示されるRouteのうち、sampleとcallerは今回のサービスメッシュで管理する予定なので1番目の方法で良いのですが、el-sampleとel-callerはGitレポジトリからのWebhookを受け付けるEventListenerのためのRouteで、サービスメッシュとは直接の関係はありません。

もちろんこれらのRouteもサービスメッシュで管理できますが、今回は3番目のサービスメッシュの例外に追加する方針で進めていきます。

サービスメッシュの例外に追加するためには、maistra.io/expose-route=trueというラベルをPodに設定する必要があります。今回ラベルを設定するPodはEventListenerリソースによって管理されており、EventListenerリソースに付与されたラベルは自動的にDeploymentとPodに付与されるため、EventListenerにこのラベルを付与します。

[ec2-user@ip-xx-xx-xx-xx ws_miyazaki]$ oc get el --show-labels -n microsvc-sample
NAME     AGE   LABELS
caller   9h    <none>
sample   26d   <none>
[ec2-user@ip-xx-xx-xx-xx ws_miyazaki]$ oc label el caller sample maistra.io/expose-route=true -n microsvc-sample
eventlistener.triggers.tekton.dev/caller labeled
eventlistener.triggers.tekton.dev/sample labeled
[ec2-user@ip-xx-xx-xx-xx ws_miyazaki]$ oc get el --show-labels -n microsvc-sample
NAME     AGE   LABELS
caller   9h    maistra.io/expose-route=true
sample   26d   maistra.io/expose-route=true
# EventListenerにラベルが付与された
[ec2-user@ip-xx-xx-xx-xx ws_miyazaki]$ oc get po --show-labels -l eventlistener -n microsvc-sample
NAME                         READY   STATUS        RESTARTS   AGE     LABELS
el-caller-59cfb4994-tgs57    1/1     Terminating   0          2m51s   app.kubernetes.io/managed-by=EventListener,app.kubernetes.io/part-of=Triggers,eventlistener=caller,pod-template-hash=59cfb4994
el-caller-6bc8475b46-fthpg   1/1     Running       0          22s     app.kubernetes.io/managed-by=EventListener,app.kubernetes.io/part-of=Triggers,eventlistener=caller,maistra.io/expose-route=true,pod-template-hash=6bc8475b46
el-sample-684bb5db4-wd52g    1/1     Terminating   0          2m51s   app.kubernetes.io/managed-by=EventListener,app.kubernetes.io/part-of=Triggers,eventlistener=sample,pod-template-hash=684bb5db4
el-sample-788b75787d-vqvld   1/1     Running       0          22s     app.kubernetes.io/managed-by=EventListener,app.kubernetes.io/part-of=Triggers,eventlistener=sample,maistra.io/expose-route=true,pod-template-hash=788b75787d
[ec2-user@ip-xx-xx-xx-xx ws_miyazaki]$ oc get po --show-labels -l eventlistener -n microsvc-sample
NAME                         READY   STATUS    RESTARTS   AGE   LABELS
el-caller-6bc8475b46-fthpg   1/1     Running   0          56s   app.kubernetes.io/managed-by=EventListener,app.kubernetes.io/part-of=Triggers,eventlistener=caller,maistra.io/expose-route=true,pod-template-hash=6bc8475b46
el-sample-788b75787d-vqvld   1/1     Running   0          56s   app.kubernetes.io/managed-by=EventListener,app.kubernetes.io/part-of=Triggers,eventlistener=sample,maistra.io/expose-route=true,pod-template-hash=788b75787d
# Podが再作成されてPodにもラベルが付与された

・callerの接続先の確認

callerサービスがsampleサービスへアクセスする際のURLを確認しましょう。IstioはHTTPリクエストの内部まで確認してトラフィックを制御しているため、OpenShift内部で行われる通信にOpenShift内部のDNS名(cluster.local)を利用すると、外部からのアクセスと内部でのアクセスを分けて管理できるようになります。callerレポジトリのsrc/main/resources/application.propertiessample_api.base_path=で始まる行があるので、これを内部DNS名に書き換えます。

sample_api.base_path=http://sample.microsvc-sample.svc.cluster.local:8080

書き換えたら、Gitレポジトリにプッシュして変更をアプリケーションに反映しましょう。

コントロールプレーンと
OpenShiftプロジェクト(Namespace)の紐づけ

OpenShift Service Meshは、コントロールプレーンをインストールしても自動では有効にならず、サービスメッシュの適用有無をプロジェクト単位で手動で設定する必要があります。この理由はいくつかありますが、例えばサービスメッシュの適用を想定していないプロジェクトで突然サービスメッシュが有効になってしまうと、疎通できなくなることなどです。

他にも、OpenShiftを構成するAPIサーバー等のコンポーネントにIstioが干渉してしまうと、OpenShiftの動作に悪影響を与えます。そのため、本項で説明する手順でコントロールプレーンとOpenShiftプロジェクト(Namespace)を明示的に紐づけ、サービスメッシュを有効化する必要があります。

なお、OpenShift Service Meshでは1つのOpenShiftクラスターに複数のコントロールプレーンをデプロイすることも可能で、各プロジェクトをどのコントロールプレーンに紐づけるかを明示的に指定できるようになっています。

コントロールプレーンとOpenShiftプロジェクトの紐づけは、クラスター管理者が実施するServiceMeshMemberRollを利用する方法と各OpenShiftプロジェクトの管理者に権限を委譲するServiceMeshMemberを利用する方法の2つありますが、今回はServiceMeshMemberRollを利用して紐づけをします。

こちらもRed Hat社のドキュメントにあるサンプル(smmr-default.yaml)を利用します。

apiVersion: maistra.io/v1
kind: ServiceMeshMemberRoll
metadata:
  name: default
  # nameは default が必須です。
  namespace: istio-system
spec:
  members:
    - microsvc-sample
    # 実際にマイクロサービスをデプロイしたプロジェクト名を指定します。

ファイルを作成したら、実際にデプロイしていきましょう。

[ec2-user@ip-xx-xx-xx-xx ws_miyazaki]$ oc create -f smmr-default.yaml 
servicemeshmemberroll.maistra.io/default created
[ec2-user@ip-xx-xx-xx-xx ws_miyazaki]$ oc get smmr -n istio-system
NAME      READY   STATUS       AGE
default   1/1     Configured   7s

こちらは作成後すぐに「Configured」となりました。これで、コントロールプレーンとOpenShiftプロジェクト(Namespace)の紐づけは完了です。

サイドカーの挿入

続いて、前回までに開発したアプリケーションにサイドカーを挿入していきます。コミュニティ版のIstioでは、サービスメッシュを導入したいNamespaceにラベルを付けることでサイドカーが自動挿入されますが、OpenShift Service MeshではPodにAnnotationを付与することで、OpenShiftのビルド機能等がサービスメッシュの影響を受けないようにしています。

今回のアプリケーションはfabric8により自動生成されたマニフェストファイルを利用していますが、fabric8では設定をカスタマイズできるので、これを利用してPodへAnnotationを付与します。

・サービス(sample)側の設定

sampleのGitレポジトリsrc/main/以下にfabric8ディレクトリを作成し、下記のsample-deployment.yamlを配置してpushします。sampleの部分はDeploymentの名前と合わせてください。

spec:
  template:
    metadata:
      annotations:
        sidecar.istio.io/inject: "true"

このYAMLファイル単体ではDeploymentとして不完全ですが、不足している部分はfabric8が自動的に補足してくれるため、この内容で問題ありません。

それでは、実際にpushしていきます。前回でCI/CDは構築できているので、pushをすれば自動的にコンテナイメージのビルドとデプロイが実行されます。その過程をoc getコマンドで見てみましょう。

[ec2-user@ip-xx-xx-xx-xx ws_miyazaki]$ oc get po -n microsvc-sample
NAME                         READY   STATUS    RESTARTS   AGE
caller-1-gbk2z               1/1     Running   0          14h
el-sample-788b75787d-6nmls   1/1     Running   0          14h
sample-26-m9dt2              1/1     Running   0          4m39s

[ec2-user@ip-xx-xx-xx-xx ws_miyazaki]$ oc get po -o jsonpath='{.metadata.annotations.sidecar\.istio\.io/inject}' sample-26-m9dt2
 # まだAnnotationを設定していないので何も表示されない

[ec2-user@ip-xx-xx-xx-xx ws_miyazaki]$ oc get po -n microsvc-sample -o jsonpath='{.spec.containers[*].name}' sample-26-m9dt2 
spring-boot # <-- Spring Bootのコンテナのみ

[ec2-user@ip-xx-xx-xx-xx ws_miyazaki]$ oc get po -n microsvc-sample
NAME                         READY   STATUS    RESTARTS   AGE
caller-1-gbk2z               1/1     Running   0          14h
el-sample-788b75787d-6nmls   1/1     Running   0          14h
sample-26-m9dt2              1/1     Running   0          4m55s

# ここでgit pushしてしばらく待機

[ec2-user@ip-xx-xx-xx-xx ws_miyazaki]$ oc get po -n microsvc-sample
NAME                                                           READY   STATUS      RESTARTS   AGE
caller-1-gbk2z                                                 1/1     Running     0          14h
el-sample-788b75787d-6nmls                                     1/1     Running     0          14h
maven-build-sample-sample-zrfd4-sample-build-dbczs-pod-2588z   0/2     Completed   0          3m45s
sample-28-9hcrj                                                2/2     Running     0          29s
sample-28-deploy                                               0/1     Completed   0          33s
sample-s2i-16-build                                            0/1     Completed   0          102s

[ec2-user@ip-xx-xx-xx-xx ws_miyazaki]$ oc get po -n microsvc-sample -o jsonpath='{.metadata.annotations.sidecar\.istio\.io/inject}' sample-28-9hcrj
true # 設定したAnnotationが反映された

[ec2-user@ip-xx-xx-xx-xx ws_miyazaki]$ oc get po -n microsvc-sample -o jsonpath='{.spec.containers[*].name}' sample-28-9hcrj 
spring-boot istio-proxy # <-- istio-proxyコンテナが確かに入っている

無事にサイドカーがsampleのPodに挿入されたら完了です。

・呼び出し元サービス(caller)側の設定

先ほどと同様にcallerのGitレポジトリsrc/main/以下にfabric8ディレクトリを作成し、caller-deployment.yamlを配置してpushします。内容は前述のsample-deployment.yamlと同じです。

これで、sample, callerの両サービスにサイドカーが挿入されたはずです。実際に確認してみましょう。

[ec2-user@ip-xx-xx-xx-xx ws_miyazaki]$ oc get po -n microsvc-sample -l app=sample 
NAME              READY   STATUS    RESTARTS   AGE
sample-33-rjzlt   2/2     Running   0          5m4s
[ec2-user@ip-xx-xx-xx-xx ws_miyazaki]$ oc get po -n microsvc-sample sample-33-rjzlt -o jsonpath='{.spec.containers[*].name}'
spring-boot istio-proxy
[ec2-user@ip-xx-xx-xx-xx ws_miyazaki]$ oc get po -n microsvc-sample -l app=caller
NAME             READY   STATUS    RESTARTS   AGE
caller-5-r59h2   2/2     Running   0          13m
[ec2-user@ip-xx-xx-xx-xx ws_miyazaki]$ oc get po -n microsvc-sample caller-5-r59h2 -o jsonpath='{.spec.containers[*].name}'
spring-boot istio-proxy

このように、sample、callerのPodがRunningとなり、istio-proxyコンテナが挿入されていれば完了です。

Gateway、VirtualServiceの設定

続いて、外部からサービスメッシュ内部へのアクセスを設定します。

サービスメッシュ内部への通信は全てIstioのIngress Gatewayを経由し、アクセス先のURL等に基づいてIstioが振り分けます。その振り分けに必要となる情報をIstioに読み込ませるためにGatewayVirtualServiceを設定します。今回は、以下のような設定を行うYAMLファイルを作成します。

今回実施するGatewayとVirtualServiceの設定

そして、下記が今回使用するYAMLファイル(vs-gw.yaml)です。通常、OpenShiftで外部アクセスを受け入れる際はRoute機能を用いてURLとServiceを直接紐づけますが、サービスメッシュ内部へのアクセスの際はIstioのIngress Gatewayを経由するように設定する必要があります。

しかし、OpenShift Service MeshではGatewayリソースのhostsに記載されたホスト名を自動的にRouteに反映するIstio OpenShift Routing(IOR)という機能があるため、この点は特に意識する必要がなくなり便利です。

# Ingress gatewayの80番ポートで'hosts'に記載されたホスト名に対するHTTPアクセスを待ち受ける。
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: sample-gateway
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts: # ここに記載したホスト名はIORによってRouteが自動設定される
    - sample.apps.example.com
    - caller.apps.example.com
---
# 条件に一致するHTTPリクエストをsample Serviceに転送する
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: sample
spec:
  hosts: # リクエストされたホスト名
  - sample.apps.example.com
  gateways: # 使用するGateway
  - sample-gateway
  http:
  - match:
    - uri:
        prefix: "/"  # "/"で始まるURI⇒全てのURIに対して有効
    route:
    - destination: # リクエストの転送先
        host: sample.microsvc-sample.svc.cluster.local
        port:
          number: 8080
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: caller
spec:
  hosts:
  - caller.apps.example.com
  gateways:
  - sample-gateway
  http:
  - match:
    - uri:
        prefix: "/"
    route:
    - destination:
        host: caller.microsvc-sample.svc.cluster.local
        port:
          number: 8080

YAMLファイルを作成したら、OpenShiftに適用します。

[ec2-user@ip-xx-xx-xx-xx ws_miyazaki]$ oc apply -f vs-gw.yaml -n microsvc-sample
gateway.networking.istio.io/sample-gateway created
virtualservice.networking.istio.io/sample created
virtualservice.networking.istio.io/caller created

[ec2-user@ip-xx-xx-xx-xx ws_miyazaki]$ oc get route -n istio-system
NAME                                  HOST/PORT                                            PATH   SERVICES               PORT    TERMINATION          WILDCARD
grafana                               grafana-istio-system.apps.example.com                       grafana                <all>   reencrypt            None
# Gatewayのhostsが反映されている ------------------------------------------------------
microsvc-sample-sample-gateway-5w5hq   sample.apps.example.com                                    istio-ingressgateway   http2                        None 
microsvc-sample-sample-gateway-72tm5   caller.apps.example.com                                    istio-ingressgateway   http2                        None
# ------------------------------------------------------------------------------------
istio-ingressgateway                  istio-ingressgateway-istio-system.apps.example.com          istio-ingressgateway   8080                         None
jaeger                                jaeger-istio-system.apps.example.com                        jaeger-query           <all>   reencrypt            None
kiali                                 kiali-istio-system.apps.example.com                         kiali                  <all>   reencrypt/Redirect   None
prometheus                            prometheus-istio-system.apps.example.com                    prometheus             <all>   reencrypt            None

この時点で、sample、callerどちらのサービスもIstioを経由してアクセスが可能となっているはずです。実際にデータをHTTP POSTで送信して、各サービスからの応答があるか確認してみましょう。

# sampleの確認
[ec2-user@ip-xx-xx-xx-xx ws_miyazaki]$ curl http://sample.apps.example.com/api/sample/apply -v -X POST \
> -H "Content-Type:application/json" -d "{\"user_name\":\"Taro\", \"store_id\":1111, \"store_name\":\"Shop_A\"}"
* About to connect() to sample.apps.example.com port 80 (#0)
*   Trying xx.xx.xx.xx...
* Connected to sample.apps.example.com (xx.xx.xx.xx) port 80 (#0)
> POST /api/sample/apply HTTP/1.1
> User-Agent: curl/7.29.0
> Host: sample.apps.example.com
> Accept: */*
> Content-Type:application/json
> Content-Length: 60
> 
* upload completely sent off: 60 out of 60 bytes
< HTTP/1.1 201 Created
< content-type: application/json
< date: Thu, 10 Sep 2020 22:15:45 GMT
< x-envoy-upstream-service-time: 5
< server: istio-envoy # Istioを経由していることが分かる
< transfer-encoding: chunked
< set-cookie: a5b17077322e132d2c3d0951c9a48323=17b1cbd28f5c6314dbae7bed61cbe453; path=/; HttpOnly
< 
* Connection #0 to host sample.apps.example.com left intact
{"user_id":39}
# callerの確認
[ec2-user@ip-xx-xx-xx-xx ws_miyazaki]$ curl http://caller.apps.example.com/api/caller/apply -v -X POST \
> -H "Content-Type:application/json" -d "{\"user_name\":\"Taro\", \"store_id\":1111, \"store_name\":\"Shop_A\"}"
* About to connect() to caller.apps.example.com port 80 (#0)
*   Trying xx.xx.xx.xx...
* Connected to caller.apps.example.com (xx.xx.xx.xx) port 80 (#0)
> POST /api/caller/apply HTTP/1.1
> User-Agent: curl/7.29.0
> Host: caller.apps.example.com
> Accept: */*
> Content-Type:application/json
> Content-Length: 60
> 
* upload completely sent off: 60 out of 60 bytes
< HTTP/1.1 201 Created
< content-type: application/json
< date: Thu, 10 Sep 2020 22:16:23 GMT
< x-envoy-upstream-service-time: 28
< server: istio-envoy # Istioを経由していることが分かる
< transfer-encoding: chunked
< set-cookie: d68de88d3610a37cc14e49ce6c4e41d6=17b1cbd28f5c6314dbae7bed61cbe453; path=/; HttpOnly
< 
* Connection #0 to host caller.apps.example.com left intact
{"user_id":39}

このように、Istioが有効な状態で各サービスと通信できることが確認できました。これでサービスメッシュの構築は完了です。

mTLSを有効にしてみよう

続いて、Istioの機能の1つであるmTLSを有効にして、セキュリティを強化していきます。まずはmTLSが無効な状態で、サービスと同じNamespaceにcurlを叩くだけの簡単なPodを立ち上げてみましょう。

[ec2-user@ip-xx-xx-xx-xx ws_miyazaki]$ oc run -n microsvc-sample --image=rhel7:latest -it --rm --restart=Never test \
> -- curl caller.microsvc-sample.svc.cluster.local:8080/api/caller/apply -X POST -H "Content-Type:application/json" \
> -d "{\"user_name\":\"Taro\", \"store_id\":1111, \"store_name\":\"Shop_A\"}"
If you don't see a command prompt, try pressing enter.
{"user_id":39}pod "test" deleted

このPodにistio-proxyは入っていませんが、通常通り通信できることが分かります。mTLSの有効化はIstioのインストール時に使用したServiceMeshControlPlaneリソースを利用すると簡単にできます。インストール時に作成したistio-installation.yamlを以下のように書き換えてください。

apiVersion: maistra.io/v1
kind: ServiceMeshControlPlane
metadata:
  name: basic-install
spec:
  istio:
    global:
      mtls:              # この行と
        enabled: true    # この行を追加
      proxy:
# 以下省略

あとはこのYAMLファイルを適用するだけなのですが、OpenShift Service Mesh内部におけるmTLSの仕組みをもう少し詳しく説明します。

mTLSの有効化には、内部でServiceMeshPolicyDestinationRuleリソースを利用します。これらのリソースの役割を説明したものが下図です。

ServiceMeshPolicyとDestinationRuleの役割

ServiceMeshPolicyはリクエストの受信側の設定でmTLSを利用していない通信をブロックします。なお、このServiceMeshPolicyはOpenShift Service Mesh独自の設定であり、OSS版のIstio 1.4では類似の設定であるPolicyリソースを利用します。一方のDestinationRuleはリクエストの送信側で指定したホスト名にmTLSを利用するために設定します。

それでは、更新したistio-installation.yamlをOpenShiftに適用していきます。

[ec2-user@ip-xx-xx-xx-xx ws_miyazaki]$ oc get servicemeshpolicy,destinationrule -n istio-system
NAME                                                  AGE
servicemeshpolicy.authentication.maistra.io/default   129m

NAME                                                                HOST                                             AGE
destinationrule.networking.istio.io/disable-mtls-jaeger-collector   jaeger-collector                                 128m
destinationrule.networking.istio.io/disable-mtls-zipkin             zipkin                                           128m
destinationrule.networking.istio.io/istio-policy                    istio-policy.istio-system.svc.cluster.local      128m
destinationrule.networking.istio.io/istio-telemetry                 istio-telemetry.istio-system.svc.cluster.local   128m
[ec2-user@ip-xx-xx-xx-xx ws_miyazaki]$ oc apply -f istio-installation.yaml -n istio-system
servicemeshcontrolplane.maistra.io/basic-install configured
[ec2-user@ip-xx-xx-xx-xx ws_miyazaki]$ oc get servicemeshpolicy,destinationrule -n istio-system
NAME                                                  AGE
servicemeshpolicy.authentication.maistra.io/default   129m  # ServiceMeshPolicyが更新された

# DestinationRuleが2つ新規作成された
NAME                                                                HOST                                             AGE
destinationrule.networking.istio.io/api-server                      kubernetes.default.svc.cluster.local             12s
destinationrule.networking.istio.io/default                         *.local                                          12s
destinationrule.networking.istio.io/disable-mtls-jaeger-collector   jaeger-collector                                 129m
destinationrule.networking.istio.io/disable-mtls-zipkin             zipkin                                           129m
destinationrule.networking.istio.io/istio-policy                    istio-policy.istio-system.svc.cluster.local      128m
destinationrule.networking.istio.io/istio-telemetry                 istio-telemetry.istio-system.svc.cluster.local 

これでmTLSは有効になりましたが、本当に有効になったかを確認してみましょう。先ほどと同じPodを立ち上げてみます。有効になっていればIngress gatewayを経由したアクセスは通常通りできる一方、istio-proxyがいないtest Podからはアクセスができなくなるはずです。

# istio-ingressgateway経由でのアクセス
[ec2-user@ip-xx-xx-xx-xx ws_miyazaki]$ curl http://caller.apps.sample.com/api/caller/apply -X POST \
> -H "Content-Type:application/json" -d "{\"user_name\":\"Taro\", \"store_id\":1111, \"store_name\":\"Shop_A\"}"
{"user_id":39} # 通常通りアクセス可能

# test Podからのアクセス
[ec2-user@ip-xx-xx-xx-xx ws_miyazaki]$ oc run -n microsvc-sample --image=rhel7:latest -it --rm --restart=Never test \
> -- curl caller.microsvc-sample.svc.cluster.local:8080/api/caller/apply -X POST -H "Content-Type:application/json" \
> -d "{\"user_name\":\"Taro\", \"store_id\":1111, \"store_name\":\"Shop_A\"}"
If you don't see a command prompt, try pressing enter.
curl: (56) Recv failure: Connection reset by peer # アクセスに失敗した
pod "test" deleted
pod microsvc-sample/test terminated (Error)

このように、各サービス間の通信は正常に出来ている一方で、mTLSが利用できないPodからはアクセスできなくなり、mTLSが有効になったことが確認できました。

アクセス制御を実装してみよう

それでは、最後にIstioを活用して簡単なアクセス制御を実装していきます。Istioでのサービス間の認可にはAuthorizationPolicyを利用します。

AuthorizationPolicyでは、IPアドレスやmTLSから提供されるアクセス元の情報を基に、許可するHTTPリクエストの操作(GET、POST)やパス等を指定できます。ここでは下図のようにsampleサービスを外部に公開したくないサービスに見立てて、sampleへのアクセスを同一Namespace内からのみに制限するというアクセス制御を入れていきましょう。

【注】:sampleサービスを公開する設定をGatewayとVirtualServiceから削除すれば結果的には同様の効果が得られますが、このような設定を入れておくことで、誤ってサービスを公開してもアクセスを防ぐことができます。

AuthorizationPolicyを利用したアクセス制御

まず、以下のようなauthz-policy.yamlを作成します。

apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
 name: to-sample
 namespace: microsvc-sample
spec:
 selector:         # このポリシーを適用するPodを決める
   matchLabels:
     app: sample
 rules:
 - from:
   - source:
       namespaces: ["microsvc-sample"]
   # microsvc-sampleのNamespaceからのアクセスを許可する
   to: []
   # []の場合、他の条件に合致する全てのアクセスを許可する

なお、AuthorizationPolicyはホワイトリスト形式で適用されるため、記載がないものは全てブロックされます。

それでは、外部からcaller, sampleにアクセスできる状態であることを確認したうえでAuthorizationPolicyを導入し、sampleサービスへの直接アクセスがブロックされることを確認してみます。

[ec2-user@ip-xx-xx-xx-xx ws_miyazaki]$ curl http://caller.apps.example.com/api/caller/apply \
> -X POST -H "Content-Type:application/json" \
> -d "{\"user_name\":\"Taro\", \"store_id\":1111, \"store_name\":\"Shop_A\"}"
{"user_id":39}

[ec2-user@ip-xx-xx-xx-xx ws_miyazaki]$ curl http://sample.apps.example.com/api/sample/apply \
> -X POST -H "Content-Type:application/json" \
> -d "{\"user_name\":\"Taro\", \"store_id\":1111, \"store_name\":\"Shop_A\"}"
{"user_id":39}
# caller, sample両方にアクセス可能

[ec2-user@ip-xx-xx-xx-xx ws_miyazaki]$ oc apply -f authz-policy.yaml 
authorizationpolicy.security.istio.io/to-sample created

[ec2-user@ip-xx-xx-xx-xx ws_miyazaki]$ oc get authorizationpolicy -n microsvc-sample
NAME        AGE
to-sample   29s

[ec2-user@ip-xx-xx-xx-xx ws_miyazaki]$ curl http://caller.apps.example.com/api/caller/apply \
> -X POST -H "Content-Type:application/json" \
> -d "{\"user_name\":\"Taro\", \"store_id\":1111, \"store_name\":\"Shop_A\"}"
{"user_id":39}
# caller経由では通常通りsampleにアクセス可能

[ec2-user@ip-xx-xx-xx-xx ws_miyazaki]$ curl http://sample.apps.example.com/api/sample/apply \
> -X POST -H "Content-Type:application/json" \
> -d "{\"user_name\":\"Taro\", \"store_id\":1111, \"store_name\":\"Shop_A\"}"
RBAC: access denied
# sampleへの直接のアクセスは拒否された

このように、AuthorizationPolicyでアクセスを制御できることが確認できました。本稿ではこれ以上の詳しい解説はしませんが、この他にもサービスごとに許可する操作を指定して、より細かいアクセス制御を行うことも可能です。さらに詳しく知りたい方はIstioのドキュメント(英語)も合わせて参照ください。

おわりに

今回はOpenShift Service Meshを用いて、前回までに開発したサービスへサービスメッシュを導入する方法を紹介しました。加えて、サービスメッシュでmTLSやアクセス制御の設定を行うことで、マイクロサービス内で行われる通信を保護し、意図していない通信をブロックする方法も紹介しました。

ここまでお読みいただいた皆さまには、マイクロサービスにサービスメッシュを導入し、セキュリティを強化する方法が理解できたのではないでしょうか。今回は紹介しきれませんでしたが、サービスメッシュには他にも多彩な機能がありますので、ぜひ一度試してみてください。

さて、ここまで全5回にわたり「Red Hat OpenShift Container Platform with Runtimesによるマイクロサービス開発」と題して、マイクロサービスとは何かという話から、マイクロサービス開発手法、そしてマイクロサービスと密接な関係にあるCI/CDとサービスメッシュについて紹介してきましたが、いかがでしたか。

本連載が皆さまにとって、マイクロサービス導入への第一歩となれば、筆者としてこれ以上喜ばしいことはありません。

著者
宮崎 星冬(みやざき せいとう)
株式会社日立製作所 プラットフォームサービス部
株式会社日立製作所 プラットフォームサービス部所属。日立のDevOpsサービスの設計や、Kubernetes, OpenShiftといったコンテナ管理基盤や関連するOSS等のソフトウェア製品の評価に従事。

連載バックナンバー

Think ITメルマガ会員登録受付中

Think ITでは、技術情報が詰まったメールマガジン「Think IT Weekly」の配信サービスを提供しています。メルマガ会員登録を済ませれば、メルマガだけでなく、さまざまな限定特典を入手できるようになります。

Think ITメルマガ会員のサービス内容を見る

他にもこの記事が読まれています