IstioでHTTPのリクエストを振り分ける

前回の記事で,Envoyの話をした. Envoy,すごく便利なのだがこの設定を全コンテナごとに書いてやるのはなかなかにめんどくさい. せっかくSidecarなのでもっと楽に差し込めたら良い.

あと,せっかくEnvoyを使っているので,static_resourcesだけじゃなくdynamic_resourcesをつかったカナリアリリースとかやってみたい.

というわけでIstioを使ってみる. この分野だとIstioは非常に筋がいいのだが,いまいち枯れてないので,本番導入するのはちょっと悩む……

Istio関連だと,公式で出しているBookinfoというアプリケーションを 動かしてみた という記事はたくさん見かけるんだけど,自分のアプリケーションでIstio組み込んだ記事が非常に少ない.

istio.io

ので,今回は自分で作ったサンプルアプリケーションをIstioの上で動かしてみる.もちろんKubernetesクラスタは構築済みという前提で話をすすめる.

Istioのインストール

とりあえずIstioのインストールをする必要がある. Istioの設定が書かれたyamlをいちいちapplyしてもいいのだが,結構量が多くてめんどくさいのでHelmを使うことをおすすめする.

Istioctl

istioctlは別にインストールしてもしなくても良い気がしている.のだが,とりあず getLatestIstio は大事なのでダウンロードしておく.

$ curl -L https://git.io/getLatestIstio | sh -
$ cd istio-1.*
$ sudo cp bin/istioctl /usr/local/bin/
$ istioctl --help

Helm

$ cd istio-1.*
$ kubectl create -f install/kubernetes/helm/helm-service-account.yaml
$ helm init --service-account tiller

これでhemlが使えるようになる.

Istio

hemlを使ってインストールする.

$ kubectl create namespace istio-system
$ cd istio-1.*
$ helm install \
--wait \
--name istio \
--namespace istio-system \
install/kubernetes/helm/istio
...
$ helm status istio
LAST DEPLOYED: Fri Feb  1 22:40:42 2019
NAMESPACE: istio-system
STATUS: DEPLOYED
...

helm status istio のSTATUSが DEPLOYED になればOK.うまく行かない場合は何かしら問題が発生している.

このオプションだと,結構いろんなPodを立ち上げる(PrometheusとかGrafanaとか)ので,Podを立ち上げる余力がクラスタにあるかはちゃんと確認しておいたほうが良い. 俺はいつもEKSを使うのだけれど,EKSの場合,PodにはENIが割り当てられる.そしてインスタンスサイズによって割当可能なENIに上限が設定されており,インスタンスをケチるとENIが足らずにPodが立ち上げられなくなったりした.インスタンスサイズを上げてやり直したらうまく行った.

istio-system にこれらのPodが立ち上がっていれば問題ない.

$ kubectl get pods -n istio-system
NAME                                     READY   STATUS    RESTARTS   AGE
istio-citadel-7dd558dcf-ldb44            1/1     Running   0          17h
istio-egressgateway-88887488d-bp7qv      1/1     Running   0          17h
istio-galley-787758f7b8-txr4p            1/1     Running   0          17h
istio-ingressgateway-58c77897cc-qn554    1/1     Running   0          17h
istio-pilot-868cdfb5f7-znvjr             2/2     Running   0          17h
istio-policy-56c4579578-25ztf            2/2     Running   0          17h
istio-sidecar-injector-d7f98d9cb-gnckv   1/1     Running   0          17h
istio-telemetry-7fb48dc68b-4826f         2/2     Running   0          17h
prometheus-76db5fddd5-48zqq              1/1     Running   0          17h

auto injectionを設定する

IstioはIstio-proxyとしてEnvoyのコンテナを,各PodのSidecarとして配置する必要がある.このテンプレとは istioctl kube-inject で吐き出すことができるのだが,それも大量のyamlである.それをいちいち自分で管理するのは嫌なので,auto injection機能を使うことが多い. これを使うと,テンプレを自分で吐き出して,applyする必要がなく,すべてのPodにIstio-proxyが勝手にInjectされて立ち上がるようになる(これをやってくれるのが istio-sidecar-injector というPodで,↑のPod一覧でも起動しているのが見える).

なお,これを行うには,Webhook Admission Controllersという機能をKubernetesクラスタがサポートしている必要がある.EKSとかだとすでにサポートされているのでそのまま使える.

aws.amazon.com

auto injectionの設定はnamespaceごとに設定される.というわけでnamespaceをこのようにして作る.

apiVersion: v1
kind: Namespace
metadata:
  name: istio-http-example
  labels:
    name: istio-http-example
    istio-injection: enabled

Webサーバを立てる

Webサーバはgolangのechoで作った適当なサーバを使う.

github.com

また,今回は マイクロサービス間の通信をIstioでコントロールしたい ので,外部からアクセス可能な状態までは作らずに,クラスタ内でサーバとクライアントを作成し,サービス間通信ができる状態を目指す.

まずはバックエンドのWebサーバを作る.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend-0
  namespace: istio-http-example
  labels:
    app: backend-0
spec:
  replicas: 1
  selector:
    matchLabels:
      app: backend
      version: "0"
  template:
    metadata:
      labels:
        app: backend
        version: "0"
    spec:
      containers:
        - name: echo
          image: h3poteto/playground-echo-go:master
          imagePullPolicy: Always
          ports:
            - containerPort: 8080
              name: http
          env:
            - name: PORT
              value: "8080"
            - name: VERSION
              value: "0"

後々,versionによるカナリアリリースを実現したいので,versionというlabelを付与しているが,とりあえずはこれは使わない.

次に,namespace内からアクセス可能にさせるためにServiceを作る.

apiVersion: v1
kind: Service
metadata:
  name: backend
  namespace: istio-http-example
  labels:
    app: backend
spec:
  ports:
  - port: 9090
    targetPort: 8080
    protocol: TCP
    name: http
  selector:
    app: backend

これで,backend という名前でアクセスできるようになる.

これらを kubectl apply -f deployment.yaml すると,yamlに定義したコンテナは1つだが,Istio-proxyが勝手に挟み込まれてコンテナが2つ起動するようになるはずである.

$ kubectl get pods -n istio-http-example
NAME                         READY   STATUS    RESTARTS   AGE
backend-0-7bc54786df-4fgvz   2/2     Running   0          15d

もしIstio-proxyのauto injectionが働かない場合は,namespaceの設定か,クラスタ全体でhookが動くようになっているかを確認する必要がある.

Istioによるロードバランスを特に変更せずにアクセスする

とりあえず,Istioは通すがIstioには特にアクセス制御をさせずに普通にバックエンドのサービスに疎通させてみる.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: client-0
  namespace: istio-http-example
  labels:
    app: client-0
spec:
  replicas: 1
  selector:
    matchLabels:
      app: client
      version: "0"
  template:
    metadata:
      labels:
        app: client
        version: "0"
    spec:
      containers:
        - name: client
          image: h3poteto/playground-echo-client:master
          imagePullPolicy: Always
          env:
            - name: SERVER_HOST
              value: "backend"
            - name: SERVER_PORT
              value: "9090"

こいつは SERVICE_HOST:SERVICE_PORTcURLを投げるだけのコンテナだ.とりあえずbackendのWebサーバにリクエストを投げてみる.

と,見事に疎通している.

$ kubectl logs -f client-0-796bb44b64-kmwpg -n istio-http-example -c client
Hello, World!, version: 0
Hello, World!, version: 0
Hello, World!, version: 0
Hello, World!, version: 0
Hello, World!, version: 0
Hello, World!, version: 0
Hello, World!, version: 0
Hello, World!, version: 0
Hello, World!, version: 0

Istioによりアクセスを振り分ける

さて,ここまで来たらいよいよIstioの機能を使っていこうと思う. まず,backendのWebサーバとして,version: 0とversion: 1を用意する.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend-0
  namespace: istio-http-example
  labels:
    app: backend-0
spec:
  replicas: 1
  selector:
    matchLabels:
      app: backend
      version: "0"
  template:
    metadata:
      labels:
        app: backend
        version: "0"
    spec:
      containers:
        - name: echo
          image: h3poteto/playground-echo-go:master
          imagePullPolicy: Always
          ports:
            - containerPort: 8080
              name: http
          env:
            - name: PORT
              value: "8080"
            - name: VERSION
              value: "0"
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend-1
  namespace: istio-http-example
  labels:
    app: backend-1
spec:
  replicas: 1
  selector:
    matchLabels:
      app: backend
      version: "1"
  template:
    metadata:
      labels:
        app: backend
        version: "1"
    spec:
      containers:
        - name: echo
          image: h3poteto/playground-echo-go:master
          imagePullPolicy: Always
          ports:
            - containerPort: 8080
              name: http
          env:
            - name: PORT
              value: "8080"
            - name: VERSION
              value: "1"

これをapplyするだけだと,まだIstioは何もしてくれていないので,version: 0とversion: 1がランダムにアクセスを受け付ける(これはKubernetesのServiceがロードバランスしてくれている).

version: 0だけにアクセスさせる

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: backend
  namespace: istio-http-example
spec:
  hosts:
    - "backend"
  http:
  - route:
    - destination:
        host: backend
        subset: v0
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: backend
  namespace: istio-http-example
spec:
  host: backend
  trafficPolicy:
    loadBalancer:
      simple: RANDOM
  subsets:
  - name: v0
    labels:
      version: "0"
  - name: v1
    labels:
      version: "1"

subsetsDestinationRuleの項目として定義する.それを VirtualService で利用することができる.

VirtualServiceはKubernetesで言うところの,Serviceに入る前段に挟み込むことができる. これにより,

  • hostがhogehogeだったら
  • pathが/hogeだったら

というようなルーティングルールを作ることができる.

とりあえず今回はすべてのリクエストを,subset: v0 (すなわち labels.version: "0" を持つbackend) にのみ振り分ける.

これをapplyすると,version: 0にのみアクセスがいくようになる.

$ kubectl logs -f client-0-796bb44b64-kmwpg -n istio-http-example -c client
Hello, World!, version: 0
Hello, World!, version: 0
Hello, World!, version: 0
Hello, World!, version: 0
Hello, World!, version: 0
Hello, World!, version: 0

version: 1にだけアクセスさせる

今度は逆にversion: 1にだけアクセスがいくようにする.

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: backend
  namespace: istio-http-example
spec:
  hosts:
    - "backend"
  http:
  - route:
    - destination:
        host: backend
        subset: v1
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: backend
  namespace: istio-http-example
spec:
  host: backend
  trafficPolicy:
    loadBalancer:
      simple: RANDOM
  subsets:
  - name: v0
    labels:
      version: "0"
  - name: v1
    labels:
      version: "1"
$ kubectl logs -f client-0-796bb44b64-kmwpg -n istio-http-example -c client
Hello, World!, version: 1
Hello, World!, version: 1
Hello, World!, version: 1
Hello, World!, version: 1
Hello, World!, version: 1
Hello, World!, version: 1

となる.

version: 0に90%,version: 1に10%

最後に,割合を偏らせた形でリクエストを振り分けることもできる.

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: backend
  namespace: istio-http-example
spec:
  hosts:
    - "backend"
  http:
  - route:
    - destination:
        host: backend
        subset: v0
      weight: 90
    - destination:
        host: backend
        subset: v1
      weight: 10
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: backend
  namespace: istio-http-example
spec:
  host: backend
  trafficPolicy:
    loadBalancer:
      simple: RANDOM
  subsets:
  - name: v0
    labels:
      version: "0"
  - name: v1
    labels:
      version: "1"
$ kubectl logs -f client-0-796bb44b64-kmwpg -n istio-http-example -c client
Hello, World!, version: 0
Hello, World!, version: 0
Hello, World!, version: 0
Hello, World!, version: 0
Hello, World!, version: 0
Hello, World!, version: 0
Hello, World!, version: 0
Hello, World!, version: 1
Hello, World!, version: 0
Hello, World!, version: 0
Hello, World!, version: 1
Hello, World!, version: 0

こうなる.

Tips

Istioctlの使いどころ

istioの定義ファイルもkubernetesと同じくyamlを書くことになる. これらは kubectl apply でapplyできるが,デプロイされたリソースの確認をするにはkubectlでもできるが,istioctlでもできる.

$ kubectl get virtualservice -n istio-http-example
NAME      AGE
backend   8m
$ istioctl get virtualservices -n istio-http-example
VIRTUAL-SERVICE NAME   GATEWAYS   HOSTS     #HTTP     #TCP      NAMESPACE            AGE
backend                           backend       1        0      istio-http-example   8m

他にもIstioはいろんなことができる

今回紹介したくらいまでができれば,あとはIstioのドキュメントに書いてある機能をひたすら試してみるだけなので楽に横展開できると思う.

istio.io

この辺を見ると,ルーティングにどんなものが使えるか色々書いてあるので,だいぶ参考になる.

あと認証もできるよ.

istio.io

gRPCもできる

基本的にIstioはgRPCのリクエストもさばける. が,これについては次の記事で具体例を作ってみようと思うので,次回にご期待ください.