Google Kubernetes Engine (GKE)のProduction環境でIstioを動かす

by

@wapa5pow

ogp

Kubernetesで動いていた既存のサービスを、GKE上のIstioで動くようにしました。プロダクション環境のためHTTPSや監視もいれてあります。

Istioはインストール時のオプションで、PrometheusとかGrafanaも一緒にインストールされて動きます。構築が楽になりそうなので気になっていました。今回新しいGKE環境を作る機会があったので、監視もととのっているIstioを導入しました。ほとんどの部分はIstioの公式ドキュメントを見れば構築できます。ただ各種クラウドプラットフォームに対応するためにドキュメントが散らばっていたり、どの手順を適用したらいいのかわかりずらかったりするので、自分はこのパターンでやりましたというのを残しておきます。HTTPSを使うためLet's Encryptで証明書を取得していたのですが、dns-01 challengeが使えずにhttp-01 challengeを使って、certbotで証明書を更新する形にしたのでそこはドキュメントにないのでそのような状況にある場合は参考になるかもしれません。

要件

  • IstioがGKE上で動作
  • サーバ監視(今回はPrometheusとGrafanaでやります)
  • 既存WebサービスをGKE上のIstioで動作
  • HTTPSで通信

各種作業はmacOS上からやっています。

IstioをGKE上で動かす

まずはIstioがGKE上で動作するようにします。手順は各種飛ばしているところもあるので、以下で補いながら実施するといいと思います。

クラスタの準備

GCPのWebコンソール上のKubernetes Engineからクラスタを作ります。
以下以外はデフォルトで作成します。ノードは3台だとリソースがなくてpodが作れなくなりがちなので4としておきます。

  • Number of nodes: 4
  • Machien type: n1-standard-1

なお、開発環境などで費用を節約したい場合は、Enable preemptible nodesを選択するといいと思います。これは最大24時間稼働し続けることができるノードがわりあてられるもので、いつノードが削除されて再生成されるかわからないかわりに1/3とか安い費用で利用できます。
Kubernetesはポッドが削除・再生される前提なので、そのテストの意味でも強制的に削除されるノードがあるのはいいかもしれません。

ローカル環境でkubectlが使えるようにする

gcloud auth loginなどでログインしておきます。

以下を実行し、kubectlが使えるように設定します。プロジェクト名やリージョンは各自かえてください。

gcloud config set project wapa5pow
gcloud config set container/cluster wapa5pow
gcloud config set compute/zone asia-northeast1-a
gcloud container clusters get-credentials wapa5pow
gcloud auth configure-docker --quiet

kubectl cluster-infoがエラーなく実行できたら大丈夫です。
認証系のエラーがでたら以下が参考になりそうです。

Helmのインストール

今回、Istioインストール時にHelmを使うのでここを参考にインストールしてください。
helm versionが正しく実行できれば大丈夫です。

Istioのインストール

ここを参考にして、リリース版のistioを取得します。
記事を書いている時点では、1.0.1でした。パスを通すと、istioctlが使えるようになります。

IstioをHelm経由でインストールする際、Helmのインストール時に入れたTillerのポッド経由でIstioを入れるか、HelmのコマンドラインでKubernetesのマニフェストを出力し、それを使うかの2種類あります。今回は、マニフェストをgitで管理したかったので、マニフェストに書き出す形にします。

リリース版のistio-1.0.1のフォルダ直下に行き、以下のコマンドを打ちます。

helm template install/kubernetes/helm/istio --name istio --namespace istio-system
--set grafana.enabled=true \
> /var/tmp/istio.yaml

/var/tmp/istio.yamlにKubernetesのマニフェストできるので確認します。
なお、インストールオプション--set <key>=<value>で設定できるオプションが確認できます。
若干ここに書いていないもの(cert-managerのemailなど)もあるので、HelmのChartを見ながら確認するといいです。

istio.yamlをKubernetesに適用する前に、現在使用しているアカウントにRBACベースのアクセス権をあたえておきます。(参考)

kubectl create clusterrolebinding cluster-admin-binding \
  --clusterrole=cluster-admin \
  --user="$(gcloud config get-value core/account)"

次に、Istioの管理で使われるネームスペースを作成します。

kubectl create namespace istio-system

次に、最初に作成したistio.yamlを適用します。

kubectl apply -f /var/tmp/istio.yaml

しばらくしたら、Istioが正しく起動できているか確認します。ここにあるように、istio-systemのネームスペースを指定して確認してください。ドキュメントの結果はやや古いので結果がやや違うこともありますが、ポッドがエラーなく起動していれば大丈夫です。

kubectl get svc -n istio-system
kubectl get pods -n istio-system

これでIstioがインストールできました。Istioはサービスのistio-ingressgatewayがLoad Balancerとなってインターネットからアクセスを受け付けており、kubectl get svc -n istio-systemで見てみたときに、EXTERNAL-IPがあります。ドメインを設定する場合は、ここであらかじめDNSに設定しておいてください。また、IPがかわらないように、GCPのコンソール上でStatic IPにしておくといいかなと思います。

サーバ監視

せっかくGrafanaを入れたので、こちらの手順でGrafanaにアクセスできるかどうか確認してみてください。
Prometheusもこちらの手順で確認できます。

Istioを使っていない時は、Helmも使わないでPrometheusとGrafanaを入れて設定が非常に大変でしたが、Istioだとオプションを設定するだけでするっとはいってしまいます。その時は、ノードプールをわけたりストレージ分けたりしていましたが、今回のは、ひとまずデフォルトとしておきます。

注意としてPrometheusは、デフォルトの設定では6時間しかデータを保存しないのと、保存先もとくに分けらていません。
長期的なデータもみたい場合は、Prometheus用にpoolをわけるとか個別に設定したほうがいいと思います。

既存WebサービスをGKE上のIstioで動かす

Istio上で、既存のサービスを動かせるようにします。既存のサービスがKubernetesですでに動いている前提で書きます。まだKubernetesで動いていない場合は、docker-composeを使って、ローカルで動かせれるところまでいってからKubernetesで動かすと楽です。

IstioのEnvoy sidecarのInject

KubernetesのマニフェストをIstio上で動作させるには、既存のマニフェストにEnvoyを通して通信するように変更する必要があります。その変更方法には、以下の2通りのやり方があります。

  • istioctl kube-inject: このコマンドを使うと、既存のマニフェストに対してEnvoyが適用されたマニフェストが出力されるのでそれをkubectl apply -f ...で適用させます。
  • Istio-sidecar-injector: Istioを使っていない時と同じマニフェストをkubectl apply -f ...で適用すると、hookがかかり、自動でEnvoyの設定を適用してくれます。ネームスペース単位で制御でき、istio-injection=enabledを適用したネームスペースだけEnvoyが適用されます。

前者のはいちいちマニフェストファイルを変換しなければならず面倒なのでIstio-sidecar-injectorを使ったやりかたで設定します。
defaultのネームスペースにIstio-sidecar-injectorを適用しないで新しいネームスペースを作ってからそちらに既存Webサービスの設定を適用します。
以下のファイルを作成し、kubectl apply -f namespace.yamlで適用します。

title=namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: wapa5pow-namespace
  labels:
    istio-injection: enabled

metadataに先程作成したネームスペースを記載してから、既存Webサービスのマニフェストをいままでと同じようにkubectl apply -f ...で適用させます。

IngressとEgressの設定

外部からIstio内部への既存ebサービスへのアクセスできるようにするための設定をします。Istioでは、GatewayVirtualServiceでIstioのIngress設定をします。参考

注意として、既存のWebサービスでKubernetesのIngressを使用していたとしてもIstioでは使わず、すべてingressgateway経由でアクセスされます。もし既存のIngressがあった場合は、IstioのIngressを設定し終わったあとに削除しても大丈夫です。

今回、Gatewayは以下のように設定しました。後ほど、HTTPSの設定もしますがその設定も、あらかじめ入れておきます。

apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: wapa5pow-gateway
  namespace: wapa5pow
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - "www.wapa5pow.com"
#  - port:
#      number: 443
#      protocol: HTTPS
#      name: https-default
#    tls:
#      mode: SIMPLE
#      serverCertificate: "/etc/istio/ingressgateway-certs/tls.crt"
#      privateKey: "/etc/istio/ingressgateway-certs/tls.key"
#    hosts:
#    - "www.wapa5pow.com"

VirtualServiceは以下のように設定しました。webというサービスのポートらへんは各自自分のアプリに読み替えて設定してください。

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: wapa5pow-service
  namespace: wapa5pow
spec:
  hosts:
  - "www.wapa5pow.com"
  gateways:
  - wapa5pow-gateway
  http:
  - match:
    - uri:
        prefix: /.well-known/acme-challenge/
    route:
    - destination:
        port:
          number: 8089
        host: certbot
  - match:
    - uri:
        prefix: /
    route:
    - destination:
        port:
          number: 3000
        host: web

上記のマニフェストをkubectl apply -f マニフェスト名で適用します。
指定したドメインでアクセスできるか確認してみます。うまくできない場合は、kubectl get pod --all-namespacesでistio-ingressgatewayのポッドの名前を確認して、以下のような形でログを確認すつつアクセスしてみます。

kubectl logs istio-ingressgateway-6fd6575b8b-wpkzp -n istio-system -f

ほとんどのサービスがそうだと思いますが、データベースなどのクラスタ外部のサービスにアクセスする必要がある場合は、Egressの設定であるServiceEntryを設定する必要があります。Istio内部から外部にアクセスするサービスを定義します。今回は簡易的に、HTTP/HTTPSとPostgreSQLのポートをすべて許可するようにしておきます。

apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: wapa5pow
  namespace: wapa5pow
spec:
  hosts:
  - "*"
  ports:
  - number: 80
    name: http
    protocol: HTTP
  - number: 443
    name: https
    protocol: HTTPS
  - number: 5432
    name: postgres
    protocol: TCP

ポッドの中から外部ドメイン経由でアクセスできれば正しく設定されています。

HTTPSで通信する

今回は、Let's EncryptでHTTPS通信を実現します。
Istioの設定で一番つらかったのはこれです。HTTPSの設定は、はまると時間かかって大変です。

通常、Kubernetesだと、Ingressのアノテーションに、cert-manager用の設定をすればさくっと、いけます。しかしIstioは、Ingressではなく、istio-ingressgatewayを通して外部からの通信を行うので、Ingress経由では証明書を取得できません。

辛いといってもdns-01 challengeでリンクにあげられているDNSプロバイダを使っていた場合簡単にできます。Istioにはオプションを設定するとcert-managerを入れてくれて、dns-01の設定ができるのでその場合はすぐできます。

使っているDNSがDNSプロバイダにのっていない場合は、http-01 challengeで行います。cert-managerだとできなかったので、certbotを使って設定していきます。
以下のような形でhttp-01 challengeに成功するようにします。2の部分は/.well-known/acme-challenge/だけcertbotにリクエストがいくようにします。VirtualServiceですでに設定済みです。

http-01 challenge == 1 ==> istio-ingressgateway == 2 ==> certbot

certbotを適当に設定し、ポッドとしてcertbotを動かして証明書を取得します。以下みたいなシェルスクリプトを走らせるDockerfileを作ってビルドすればいいかなと思います。

#!/usr/bin/env bash

set -e

echo START: `date "+%Y-%m-%d %H:%M:%S"`

certbot certonly --standalone --preferred-challenges http -d ${CERT_DOMAIN} -n --config-dir /var/tmp --agree-tos --email=${CERT_EMAIL}

cp /var/tmp/live/${CERT_DOMAIN}/fullchain.pem tls.crt
cp /var/tmp/live/${CERT_DOMAIN}/privkey.pem tls.key

kubectl delete secret istio-ingressgateway-certs --ignore-not-found -n istio-system
kubectl create -n istio-system secret tls istio-ingressgateway-certs --key ./tls.key --cert ./tls.crt

Securing Gateways with HTTPSに書いてあるのですが、注意点として証明書と秘密鍵は、istio-systemネームスペースのistio-ingressgateway-certsのシークレットに保存する必要があります。
そうするとistio-ingressgatewayが自動的に証明書と秘密鍵を読んでくれて、/etc/istio/ingressgateway-certs/tls.crt/etc/istio/ingressgateway-certs/tls.keyに展開してくれます。

RBACが有効になっているクラスタの場合、シークレットを作成するのに権限がいるので、ServiceAccount/ClusterRole/ClusterRoleBindingを設定してcertbotが書き込みできるようにしてあげてください。

証明書が正しくとれたら、以前設定したGatewayのコメント部分をはずし、kubectl apply -f ...で適用してください。
これでHTTPSで通信できるはずです。