Help us understand the problem. What is going on with this article?

Istio入門:既存のGKE上アプリケーションにIstioを導入するまでの流れ

More than 1 year has passed since last update.

こんにちは。株式会社リクルートライフスタイルでCETというチームに所属している@mihirat です。

社内でIstioを導入した際に、チュートリアルなどではわかりにくいハマりポイントや、既存GKEクラスタの導入にあたって留意すべき点がけっこうあるなと思ったのでまとめます。

対象読者は

  • GKEですでにサービスなどを運用している
  • Istioの概要は知っている、またはチュートリアルは終わった

くらいの方を想定しています。

なお、Istioとはなにかを解説する記事はすでにたくさんあるので、できるだけ作業レベルで導入の仕方を解説していきます。
Istioを知るハンズオンとしては、Googleの中の方のIstio 1.0 を試してみた!などが良いです。

移行したいアプリの構成

もともとの構成はこんな感じでした。

  • GCP上でglobal IPを取得し、FQDNと紐づけて外部流入をさせる
  • トラフィックはIngressで受けており、SSL terminationを行う
  • Ingressの次にNginxのpodがあり、バックエンドのpodへkube-dnsを用いてリバースプロキシさせる
  • バックエンドのpodでは、GCSからリソースを取得するなどの処理をしつつレスポンスを返すサーバーが動作

なお、下記の手順は、クラスタを別で新しく作成して、そこにIstioを導入する想定で進めます。

既存クラスタ上で作業するとダウンタイムが出る危険性が高い気がします。
(namespaceを既存と別に切ればいいような気もしますが、既存namespaceに影響が出ないかを自分は確認していません)

やることの流れ

1. クラスタ作成
2. クラスタへのIstio導入と、namespaceへの有効化
3. namespace内へのリソースのデプロイ

準備

本家のチュートリアルに沿って、Istioを導入します。
利用するのは

$ kubectl apply -f install/kubernetes/istio-demo.yaml

です。
istio-demo-auth.yamlでは、mutualTLSといってpod間の通信が全てTLSになって最高!に見えるのですが、readiness probeとliveness probeが使えなくなります。
サービスが動いているportとは別にprobe専用のportを空けるという話もありますが、それってReadinessの意味がないような?

無事にIstioが導入できたら(サンプルアプリのデプロイまで行ってしっかり確認しましょう)、既存アプリをデプロイするためのnamespaceを作成します。

それが終わったら、下記コマンドでnamespace全体に対してIstioを有効化します。

$ kubectl label namespace my-app istio-injection=enabled

この操作によって、このnamespace内で以後作成されるpodは全て、
Istioで利用されているEnvoyをsidecarとして自動的に注入された状態で立ち上がることになります。
この機能はkubernetes1.9以上じゃないと使えません(kubernetesのpod initializerを利用しているため)

知るべきこと1. IstioではIngressは使わない

最初にして最大の難関。
IstioではIngressは使わず、Istio独自のGatewayとVirtualServiceというリソースを用います。(後述)

(0.8からこうなったらしく、2017年の記事を読むと混乱します。後方互換性のためか公式にもまだIngress関係のリソースが残っていますが、使わないべきです。参考)

その理由としては、IngressではIstioのもつ機能が全部活用できない、とのこと。

In a Kubernetes environment, the Kubernetes Ingress Resource is used to specify services that should be exposed outside the cluster. In an Istio service mesh, a better approach (which also works in both Kubernetes and other environments) is to use a different configuration model, namely Istio Gateway. A Gateway allows Istio features such as monitoring and route rules to be applied to traffic entering the cluster.

本家より

で、そのアオリというわけではないんですが、GCPのglobal IPがIstioでは使えません。しばらくはサポートする予定もないみたいです。

暫定では、ServiceのLoadbalancerIPを用いてくれ、という回答。

Per our chat, it is possible to set the load balancer IP in a LoadBalancer service (search for loadBalancerIP on https://kubernetes.io/docs/concepts/services-networking/service/).
This can be set to a regional static IP but not global static IP - this is limited in Arcus and there's no plan to support global.
The missing part is that you have to use the actual IP address instead of a nice label like you can with an Ingress (kubernetes.io/ingress.global-static-ip-name: my-static-ip).

Issue
なお、この構成の是非については追記をしました。

なので、Ingressで行っていた下記のような書き方はできません。SSL terminationも別のところでやる必要があります。

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: my-app
  annotations:
    kubernetes.io/ingress.global-static-ip-name: "gad-my-app" # 使えない
    kubernetes.io/ingress.allow-http: "false"
    ingress.gcp.kubernetes.io/pre-shared-cert: "myapp-cert" # 使えない

ということで、Ingressで構成を作ってしまっていた場合、IPアドレスの受け先を変更する必要があるため、blue-greenデプロイで既存GKEアプリを移行することになるかと思います。

以下、これを実行していきます。

Regional IPを使って、IstioのServiceのLoadBalancerIPにpatchを当てる

Istioをクラスタに導入したとき、istio-systemというnamespaceにistio-ingressgatewayというk8sのServiceリソースが作成されています。
k8sのServiceでは、LoadBalancerIPを既存IPに張り替えることで、そのServiceでそのIPアドレスのトラフィックを受けることができます。これを使って、静的IPをIstio化したクラスタのnamespaceに対応させます。

具体的には

$ kubectl patch svc istio-ingressgateway --namespace istio-system \
--patch '{"spec": { "loadBalancerIP": "" }'

です。KnativeのReadmeを参考にしました。
これで、IPアドレス宛に来たトラフィックは全てIstioのLBを通って来ます。

SSL termination周りのリソースをsecretとしてデプロイ

Ingressで実行していたterminationは、前述のGatewayで行うことになります。まずsecretをデプロイします。

apiVersion: v1
kind: Secret
metadata:
  name: istio-ingressgateway-certs # 予約語
  namespace: istio-system
type: Opaque
data:
  tls.key: my-key-base64
  tls.crt: my-crt-base64

これで証明書などをIstioのnamespaceにデプロイして、Gatewayから呼び出します。
Gatewayは下記のように記述して、 $ kubectl apply -f gateway.yaml でデプロイします。

apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: my-gateway
  namespace: my-app
spec:
  selector:
    istio: ingressgateway # use istio default controller
  servers:
  - port:
      number: 443
      name: https
      protocol: HTTPS
    tls:
      mode: SIMPLE
      serverCertificate: /etc/istio/ingressgateway-certs/tls.crt # 合わせる
      privateKey: /etc/istio/ingressgateway-certs/tls.key # 合わせる
    hosts:
    - dev.my-app.com

このあたりは本家を参照すると出てきます。

Gatewayはあくまでもトラフィックを受けるためだけのリソースで、受けたリソースをどこに流すかはVirtualServiceが担当します。

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: my-virtualservice
  namespace: my-app
spec:
  hosts:
  - dev.my-app.com
  gateways:
  - my-gateway
  http:
  - route:
    - destination:
        host:
         my-nginx # my-nginxのk8s Serviceに対してトラフィックを流す
        port:
          number: 80

知るべきこと2. Istio内部ではHTTP/1.0は利用できない

「まだ1.0とかありえないしw」と思われる方もいるかもしれませんが、Nginxのproxy_passのデフォルト設定では1.0になっています。1.0のままだと、Nginxからの疎通自体はできるものの、Statusコードが426になって返ってきます。
該当Issue

Istio公式のQuickstartより。

Note: The application must use HTTP/1.1 or HTTP/2.0 protocol for all its HTTP traffic because HTTP/1.0 is not supported.

なので、Nginxの場合は

proxy_http_version 1.1;

などを定義してやる必要があります。

なお、kube-dnsが適切に動作していれば、

proxy_pass  http://<service-name>.<cluster-namespace>.svc.cluster.local

などは引き続きそのまま利用できます。
本家チュートリアル内でデプロイされるサンプルアプリも参考になります。

知るべきこと3. APIなど外部通信は全てHostnameなどで穴あけが必要

個人的に一番詰まったのはここでした。
Istioでは、通信する外部ホストの全てをリストアップして記述してやる必要があります。
例えば、pip installをする場合はpypi.org、google apiを利用する場合はwww.googleapi.comなど。
詳細はこちらの本家記事を読まれることをおすすめします。

今回想定しているアプリでは、Google Cloud Storageと通信するので、利用するAPIのホストとプロトコルは下記の2つ。

https://storage.googleapi.com
http://metadata.google.internal

しかしながら、metadata.google.internalには罠があり、生IPを記述しないとcloud storage apiが使えないという問題が。
該当Issue

一方で、生IPではなくA recordを要求する場合もあるようです。挙動がよくわからないですが…。
自分が直面した事象としては、 GKEのVMのdefault credentialsを取得する際に

503 Failed to retrieve
http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/?recursive=true

のようなエラーが表示されてしまったのですが、これはFQDNを併記することで解決しました。

これらを用いて、下記のように記述してkubectl apply -f external.yaml などでデプロイします。

apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry # Egress相当の役割をもつリソース
metadata:
  name: external-google-api
  namespace: my-app
spec:
  hosts:
  - "*.googleapis.com" # wildcardが使える
  - 169.254.169.254 # metadata.google.internalのIPアドレス
  - "metadata.google.internal" # 併記
  location: MESH_EXTERNAL
  ports:
  - number: 443
    name: https
    protocol: HTTPS
  - number: 80 #metadata.google.internalはhttpなのでこれも必要
    name: http
    protocol: HTTP
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService # ServiceEntryに対応するIngressを記述
metadata:
  name: gcs-tls
  namespace: my-app
spec:
  hosts:
  - "*.googleapis.com"
  tls:
  - match: # 他にもhostがある場合、複数のsni_hostsとdestinationのペアを書く必要があるようだ?
    - port: 443
      sni_hosts:
      - "storage.googleapis.com"
    route:
    - destination:
        host: "storage.googleapis.com"
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService 
metadata:
  name: metadata-http
  namespace: my-app
spec:
  hosts:
  - "metadata.google.internal"
  http: # プロトコルがhttpの場合はこちら
  - route:
    - destination:
        host: "metadata.google.internal"
        port:
          number: 80

これでようやく、最初の構成のアプリケーションがIstioを導入した状態で動作するようになりました。
まだまだIstio初心者ですが、これから知見を貯めていきます。

本記事は元記事の転載です。

recruitlifestyle
飲食・美容・旅行領域の情報サイトや『Airレジ』などの業務支援サービスなど、日常消費領域に関わるサービスの提供するリクルートグループの中核企業
http://www.recruit-lifestyle.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away