App MeshでIstioのBookinfoを動かす

IstioのBookinfoサンプルをApp Meshで動かしてみたメモ。

App Meshのコントローラーと、ALB Ingress Controllerが導入された環境を利用する。それぞれのセットアップについては以下を参照。

環境

コンポーネント バージョン 備考
eksctl 0.16.0
Kubernetes バージョン 1.14
プラットフォームのバージョン eks.9
Helm v3.1.2
appmesh-controllerチャート 0.4.0
appmesh-controller 0.3.0
appmesh-injectチャート 0.11.0
appmesh-inject 0.4.0
aws-alb-ingress-controller v1.1.4
Istio Bookinfo 1.5.1

サンプルのダウンロード

Istioをダウンロードする。使うのはサンプルのyamlだけ。

ISTIO_VERSION="1.5.1"
curl -L https://istio.io/downloadIstio | sh -

サンプルアプリケーションのデプロイ

Namespacesを作成する。

kubectl create namespace bookinfo

サンプルアプリケーションをデプロイする。

kubectl -n bookinfo apply \
  -f istio-${ISTIO_VERSION}/samples/bookinfo/platform/kube/bookinfo.yaml

確認する。まだサイドカーのインジェクションを有効にしていないので1/1となっている。

$ kubectl -n bookinfo get pod,svc --show-labels
NAME                                  READY   STATUS    RESTARTS   AGE   LABELS
pod/details-v1-c5b5f496d-v2ljf        1/1     Running   0          11s   app=details,pod-template-hash=c5b5f496d,version=v1
pod/productpage-v1-7d6cfb7dfd-n4787   1/1     Running   0          10s   app=productpage,pod-template-hash=7d6cfb7dfd,version=v1
pod/ratings-v1-f745cf57b-hzvlf        1/1     Running   0          11s   app=ratings,pod-template-hash=f745cf57b,version=v1
pod/reviews-v1-85c474d9b8-8njvh       1/1     Running   0          11s   app=reviews,pod-template-hash=85c474d9b8,version=v1
pod/reviews-v2-ccffdd984-ljt22        1/1     Running   0          11s   app=reviews,pod-template-hash=ccffdd984,version=v2
pod/reviews-v3-98dc67b68-vshh7        1/1     Running   0          11s   app=reviews,pod-template-hash=98dc67b68,version=v3

NAME                  TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE   LABELS
service/details       ClusterIP   10.100.89.229    <none>        9080/TCP   11s   app=details,service=details
service/productpage   ClusterIP   10.100.209.14    <none>        9080/TCP   11s   app=productpage,service=productpage
service/ratings       ClusterIP   10.100.235.113   <none>        9080/TCP   11s   app=ratings,service=ratings
service/reviews       ClusterIP   10.100.40.20     <none>        9080/TCP   11s   app=reviews,service=reviews

なお、例えばreviews Serviceのセレクターはapp=reviewsとなっていて、reviews-v1reviews-v2reviews-v3のDeployemntのPodは皆このラベルを持っている。

App MeshにはGatewayの概念がないので、まず、ポートフォワードでproductpageにアクセスしてみる。

kubectl port-forward service/productpage 8888:9080 -n bookinfo

http://localhost:8888/productpageにアクセスすると、おなじみのBookinfoにアクセスできた。reviews Serviceはランダムに振ってるだけなので、リロードするとランダムに変わる。

f:id:sotoiwa:20200331144018p:plain

productpage ServiceをALB Ingress Controllerで外部公開する。

cat <<EOF > productpage-ingress.yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: productpage
  annotations:
    kubernetes.io/ingress.class: alb
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/target-type: ip
  labels:
    app: productpage
spec:
  rules:
  - http:
      paths:
      - path: /*
        backend:
          serviceName: productpage
          servicePort: 9080
EOF
kubectl apply -f productpage-ingress.yaml -n bookinfo

Ingressを確認する。

$ kubectl get ingress -n bookinfo
NAME          HOSTS   ADDRESS                                                                       PORTS   AGE
productpage   *       8276906b-bookinfo-productp-c7b2-1223038083.ap-northeast-1.elb.amazonaws.com   80      7s

ALBが作成されるので、このALB経由でもアクセスできることを確認する。

f:id:sotoiwa:20200331144044p:plain

サイドカーインジェクションの有効化

bookinfo Namespaceにラベルをつけてサイドカーのオートインジェクションを有効にする。

kubectl label \
  namespace bookinfo appmesh.k8s.aws/sidecarInjectorWebhook=enabled

Podを削除してサイドカーをインジェクションさせる。

kubectl get pod -n bookinfo -o json | jq -r '.items[].metadata.name' | xargs kubectl delete pod -n bookinfo

サイドカーがインジェクションされたことを確認する。

$ kubectl get pod -n bookinfo
NAME                              READY   STATUS    RESTARTS   AGE
details-v1-c5b5f496d-5qd5t        2/2     Running   0          53s
productpage-v1-7d6cfb7dfd-5tzr9   2/2     Running   0          53s
ratings-v1-f745cf57b-25sbh        2/2     Running   0          53s
reviews-v1-85c474d9b8-j99bx       2/2     Running   0          53s
reviews-v2-ccffdd984-mmtc9        2/2     Running   0          53s
reviews-v3-98dc67b68-xr85m        2/2     Running   0          53s

再びALB経由で/productpageにアクセスすると、502 Bad Gatewayとなり繋がらない。ALBのターゲットがunhealthyとなっている。

ポートフォワードして確認してみる。

kubectl port-forward service/productpage 8888:9080 -n bookinfo

ページは表示されるが、バックエンドにはアクセスできていない。

f:id:sotoiwa:20200331144110p:plain

Istioの場合は、サイドカーをインジェクションしただただけだとそのまま動くが、App Meshの場合はちゃんとCRDを定義してあげないといけないと思われる。

BookinfoのApp Mesh対応

BookinfoがApp Meshで動くようにする。

Meshの作成

メッシュを作成する。

cat <<EOF > bookinfo-mesh.yaml
apiVersion: appmesh.k8s.aws/v1beta1
kind: Mesh
metadata:
  name: bookinfo
EOF
kubectl apply -f bookinfo-mesh.yaml -n bookinfo

K8s ServiceとVirtualNode/VirtualServiceの関係

IstioとApp Meshではアプリケーションの各バージョンの扱い方が異なる。IstioではDestnationRuleでサブセットを定義する。

f:id:sotoiwa:20200331155711p:plain

App Meshではバージョン毎にVirtualNodeを作成する。VirtualNodeはK8s Deploymentに対応するが、エンドポイントが必要なので、各バージョン毎のK8s Serviceが必要となる。すなわちVirtualNodeはK8s Serviceに対応するともいえる。

このため、2種類のやり方がある。

その1

  • 各バージョンのDeploymentに対応するバージョン付きのK8s Serviceを作成し、VirtualNodeに対応させる
  • バージョン番号を持たないK8s Serviceを作成する
    • このサービスは名前解決の為に必要なだけなので、セレクターはなくてもかまわない
    • このK8s Serviceに対応するVirtualServiceを作る
  • このやり方はEKSでは可能だが、ECSではできないかも知れない

f:id:sotoiwa:20200331155736p:plain

その2

  • ベースバージョンのDeploymentについては、対応するサービスとしてバージョンを持たないK8s Serviceを作成し、これをVirtualNodeに対応させる
    • こちらはセレクターが必要
    • このK8s Serviceに対応するVirtualServiceを作る
  • その他のバージョンについては、各バージョンのDeploymentに対応するバージョンつきのK8s Serviceを作成し、VirtualNodeに対応させる
  • このやり方の場合、作成するK8s Serviceが一つ少なくて済むが、全てのバージョンが対称ではないので、わかりにくい
  • App Meshの公式サンプルはこうなっているものがある気がする

f:id:sotoiwa:20200331155755p:plain

Serviceの作成

今回は複数のバージョンを持つreviews Serviceについて、「その1」のやり方でやることにする。それ以外については、バージョンが1つしかないので、「その2」のやり方とする。

reviews Serviceについては各バージョン付きのDeploymentに対応するServiceが追加で必要となるので作成する。

cat <<EOF > services.yaml
apiVersion: v1
kind: Service
metadata:
  name: reviews-v1
spec:
  type: ClusterIP
  ports:
  - name: http
    port: 9080
    protocol: TCP
    targetPort: 9080
  selector:
    app: reviews
    version: v1
---
apiVersion: v1
kind: Service
metadata:
  name: reviews-v2
spec:
  type: ClusterIP
  ports:
  - name: http
    port: 9080
    protocol: TCP
    targetPort: 9080
  selector:
    app: reviews
    version: v2
---
apiVersion: v1
kind: Service
metadata:
  name: reviews-v3
spec:
  type: ClusterIP
  ports:
  - name: http
    port: 9080
    protocol: TCP
    targetPort: 9080
  selector:
    app: reviews
    version: v3
EOF
kubectl apply -f services.yaml -n bookinfo

作ったServiceを確認する。

$ kubectl get svc -n bookinfo
NAME          TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE
details       ClusterIP   10.100.89.229    <none>        9080/TCP   19m
productpage   ClusterIP   10.100.209.14    <none>        9080/TCP   19m
ratings       ClusterIP   10.100.235.113   <none>        9080/TCP   19m
reviews       ClusterIP   10.100.40.20     <none>        9080/TCP   19m
reviews-v1    ClusterIP   10.100.210.216   <none>        9080/TCP   35s
reviews-v2    ClusterIP   10.100.154.102   <none>        9080/TCP   34s
reviews-v3    ClusterIP   10.100.161.99    <none>        9080/TCP   34s

VirtualNodeの作成

VirtualNodeを作成する。VirtualNodeはバージョン付きのK8s Serviceに対応する形で必要。

cat <<EOF > virtualnodes.yaml
apiVersion: appmesh.k8s.aws/v1beta1
kind: VirtualNode
metadata:
  name: productpage
spec:
  meshName: bookinfo
  listeners:
    - portMapping:
        port: 9080
        protocol: http
  serviceDiscovery:
    dns:
      hostName: productpage.bookinfo.svc.cluster.local
  backends:
    - virtualService:
        virtualServiceName: reviews
    - virtualService:
        virtualServiceName: details
---
apiVersion: appmesh.k8s.aws/v1beta1
kind: VirtualNode
metadata:
  name: details
spec:
  meshName: bookinfo
  listeners:
    - portMapping:
        port: 9080
        protocol: http
  serviceDiscovery:
    dns:
      hostName: details.bookinfo.svc.cluster.local
---
apiVersion: appmesh.k8s.aws/v1beta1
kind: VirtualNode
metadata:
  name: ratings
spec:
  meshName: bookinfo
  listeners:
    - portMapping:
        port: 9080
        protocol: http
  serviceDiscovery:
    dns:
      hostName: ratings.bookinfo.svc.cluster.local
---
apiVersion: appmesh.k8s.aws/v1beta1
kind: VirtualNode
metadata:
  name: reviews-v1
spec:
  meshName: bookinfo
  listeners:
    - portMapping:
        port: 9080
        protocol: http
  serviceDiscovery:
    dns:
      hostName: reviews-v1.bookinfo.svc.cluster.local
---
apiVersion: appmesh.k8s.aws/v1beta1
kind: VirtualNode
metadata:
  name: reviews-v2
spec:
  meshName: bookinfo
  listeners:
    - portMapping:
        port: 9080
        protocol: http
  serviceDiscovery:
    dns:
      hostName: reviews-v2.bookinfo.svc.cluster.local
  backends:
    - virtualService:
        virtualServiceName: ratings
---
apiVersion: appmesh.k8s.aws/v1beta1
kind: VirtualNode
metadata:
  name: reviews-v3
spec:
  meshName: bookinfo
  listeners:
    - portMapping:
        port: 9080
        protocol: http
  serviceDiscovery:
    dns:
      hostName: reviews-v3.bookinfo.svc.cluster.local
  backends:
    - virtualService:
        virtualServiceName: ratings
EOF
kubectl apply -f virtualnodes.yaml -n bookinfo

VirtualNodeを確認する。

$ kubectl get virtualnode -n bookinfo
NAME          AGE
details       48s
productpage   49s
ratings       48s
reviews-v1    48s
reviews-v2    48s
reviews-v3    48s

VirtualServiceの作成

VirtualServiceはアプリケーションの通信の宛先なので、元々のK8s Serviceの単位で作成する。

cat <<EOF > virtualservices.yaml
apiVersion: appmesh.k8s.aws/v1beta1
kind: VirtualService
metadata:
  name: details
spec:
  meshName: bookinfo
  virtualRouter:
    name: details-router
  routes:
    - name: details-route
      http:
        match:
          prefix: /
        action:
          weightedTargets:
            - virtualNodeName: details
              weight: 100
---
apiVersion: appmesh.k8s.aws/v1beta1
kind: VirtualService
metadata:
  name: ratings
spec:
  meshName: bookinfo
  virtualRouter:
    name: ratings-router
  routes:
    - name: ratings-route
      http:
        match:
          prefix: /
        action:
          weightedTargets:
            - virtualNodeName: ratings
              weight: 100
---
apiVersion: appmesh.k8s.aws/v1beta1
kind: VirtualService
metadata:
  name: reviews
spec:
  meshName: bookinfo
  virtualRouter:
    name: reviews-router
  routes:
    - name: reviews-route
      http:
        match:
          prefix: /
        action:
          weightedTargets:
            - virtualNodeName: reviews-v1
              weight: 50
            - virtualNodeName: reviews-v2
              weight: 50
EOF
kubectl apply -f virtualservices.yaml -n bookinfo

確認する。

$ kubectl get virtualservice -n bookinfo
NAME      AGE
details   10s
ratings   10s
reviews   10s

操作確認すると、まだ通信出来ない。

Envoyコンテナの環境変数

サイドカーのEnvoyコンテナの環境変数APPMESH_VIRTUAL_NODE_NAMEという環境変数があるが、この値がおかしいようだ。自動でDeployment名から定義されている。メッシュ名も意図とは異なる別のメッシュが指定されている。

$ k get po -n bookinfo -o yaml | grep -A 1 APPMESH_VIRTUAL_NODE_NAME
      - name: APPMESH_VIRTUAL_NODE_NAME
        value: mesh/dj-app/virtualNode/details-v1-bookinfo
--
      - name: APPMESH_VIRTUAL_NODE_NAME
        value: mesh/dj-app/virtualNode/productpage-v1-bookinfo
--
      - name: APPMESH_VIRTUAL_NODE_NAME
        value: mesh/dj-app/virtualNode/ratings-v1-bookinfo
--
      - name: APPMESH_VIRTUAL_NODE_NAME
        value: mesh/dj-app/virtualNode/reviews-v1-bookinfo
--
      - name: APPMESH_VIRTUAL_NODE_NAME
        value: mesh/dj-app/virtualNode/reviews-v2-bookinfo
--
      - name: APPMESH_VIRTUAL_NODE_NAME
        value: mesh/dj-app/virtualNode/reviews-v3-bookinfo

APPMESH_VIRTUAL_NODE_NAMEという環境変数のもととなるメッシュ名と仮想ノード名はPodのアノテーションで指定できる。

istio-${ISTIO_VERSION}/samples/bookinfo/platform/kube/bookinfo.yamlをコピーし、各DeploymentのPodTemplateを修正し、アノテーションを追加する。

$ diff bookinfo.yaml bookinfo-appmesh.yaml
70a71,73
>       annotations:
>         appmesh.k8s.aws/mesh: bookinfo
>         appmesh.k8s.aws/virtualNode: details-bookinfo
121a125,127
>       annotations:
>         appmesh.k8s.aws/mesh: bookinfo
>         appmesh.k8s.aws/virtualNode: ratings-bookinfo
172a179,181
>       annotations:
>         appmesh.k8s.aws/mesh: bookinfo
>         appmesh.k8s.aws/virtualNode: reviews-v1-bookinfo
212a222,224
>       annotations:
>         appmesh.k8s.aws/mesh: bookinfo
>         appmesh.k8s.aws/virtualNode: reviews-v2-bookinfo
252a265,267
>       annotations:
>         appmesh.k8s.aws/mesh: bookinfo
>         appmesh.k8s.aws/virtualNode: reviews-v3-bookinfo
316a332,334
>       annotations:
>         appmesh.k8s.aws/mesh: bookinfo
>         appmesh.k8s.aws/virtualNode: productpage-bookinfo

Bookinfoを更新する。

kubectl -n bookinfo apply \
  -f istio-${ISTIO_VERSION}/samples/bookinfo/platform/kube/bookinfo-appmesh.yaml

Podが更新されるので確認する。

$ k get po -n bookinfo -o yaml | grep -A 1 APPMESH_VIRTUAL_NODE_NAME
      - name: APPMESH_VIRTUAL_NODE_NAME
        value: mesh/bookinfo/virtualNode/details-bookinfo
--
      - name: APPMESH_VIRTUAL_NODE_NAME
        value: mesh/bookinfo/virtualNode/productpage-bookinfo
--
      - name: APPMESH_VIRTUAL_NODE_NAME
        value: mesh/bookinfo/virtualNode/ratings-bookinfo
--
      - name: APPMESH_VIRTUAL_NODE_NAME
        value: mesh/bookinfo/virtualNode/reviews-v1-bookinfo
--
      - name: APPMESH_VIRTUAL_NODE_NAME
        value: mesh/bookinfo/virtualNode/reviews-v2-bookinfo
--
      - name: APPMESH_VIRTUAL_NODE_NAME
        value: mesh/bookinfo/virtualNode/reviews-v3-bookinfo

意図通りの設定がされた。

動作確認

ポートフォワードおよびALB経由でアクセスできることを確認する。

kubectl port-forward service/productpage 8888:9080 -n bookinfo

f:id:sotoiwa:20200331144553p:plain

v1とv2を50対50の割合を定義しているので、その割合でルーティングされる。

はまりどころ

最初まったく繋がらず何が悪いのかの判別するのにとても時間がかかった。これもサービスメッシュの難しさ。大きく2箇所ではまった。

APPMESH_VIRTUAL_NODE_NAME環境変数

APPMESH_VIRTUAL_NODE_NAME環境変数が正しく設定されていなかった。メッシュ名が違ったのはdj-appという別のメッシュが既にあったからで、メッシュがbookinfoしかなければ大丈夫。また、仮想ノード名が適切でなかったのは、reviewsについては「その1」のやり方、productpagedetailsratings「その2」のやり方を採用したからで、全て「その1」のやり方であれば、Deployment名から設定される値で問題なかった。なので、全て「その1」のやり方でやったほうが結局のところ簡単で、こんなにはまらなかった。

VirtualServiceの名前

VirtualServiceの名前をreviews.bookinfo.svc.cluster.localではなくreviewsとする必要があった。VirtualNodeのbackendにVirtualServiceの名前を設定する部分があるが、ここもreviewsとした。App Meshの公式サンプルだとドメイン名まで含めているものが多かったように思うので、これはアプリがどのようにバックエンドを呼んでいるかに依存すると思われる。