オンプレミスの k8s クラスタを GKE に移行した時のメモ

2023/03/29に公開

はじめに

社内に kubeadm で作ったオンプレミスの k8s クラスタがあったのですが、建物の全館停電があった際に停止に失敗して飛んでしまい、それ以降 microk8s で作った簡易的なシングルノードクラスタで凌いできました。

この度、ようやく社長の許可を得て k8s クラスタをクラウドに移行する事になったので、その時に調べた事や引っかかったところをメモとして残しておきます。

オンプレミスクラスタではマイクロサービスで構成された社内システムが動いており、

  • microk8s
  • Istio (認証/ルーティング)
  • PostgreSQL (クラスタ外)

という構成で動いています。
これを

  • GKE + ASM
  • Cloud SQL

の構成にしていきます。

移植先のクラスタを作成する

移植元に Istio が使われていたため、GKE + ASM の構成でクラスタを作っていきます。色々と試行錯誤した結果を別記事にまとめましたので、詳しくは以下ご参照ください。

https://zenn.dev/soumi/articles/26c91f246ac630

デプロイの準備

引っかかったところもありましたが、基本的に公式ドキュメントがとてもよくできているので、公式ドキュメント通りに進めたら出来る事が多いです。

データベースを Cloud SQL に移植する

現行サイトのデータを Cloud SQL に流し込むところ+αまでを別記事にまとめたので、ご参照ください。

https://zenn.dev/soumi/articles/e28aeb38950d43

Artifact Registry にイメージを登録する

今回は社内システムという事でイメージがオープンに出来ないため、Artifact Registry を利用してプロジェクト内で閉じたプライベートなレジストリにイメージを登録します。

https://cloud.google.com/artifact-registry/docs/docker/pushing-and-pulling?hl=ja

ローカルの kubectl から GKE を参照できるようにする

https://cloud.google.com/kubernetes-engine/docs/how-to/cluster-access-for-kubectl?hl=ja

サービスアカウントを使って gcloud 認証した後、kubectl の認証情報を取得します。

この設定は CI を使う場合でも同様です。

GKE から Cloud SQL に接続できるようにする

GKE から Cloud SQL に接続するには、Cloud SQL Proxy を使用する必要があります。

https://cloud.google.com/sql/docs/mysql/connect-kubernetes-engine?hl=ja

ドキュメントを参考にし、今回はサイドカーパターン、認証は Workload Identity を使用しました。

https://cloud.google.com/sql/docs/mysql/connect-kubernetes-engine?hl=ja#workload-identity

Cloud SQL を参照するサービスはシステム内に複数あったのですが、 Cloud SQL Proxy 用のサービスアカウントは1つにし、全ての Cloud SQL Proxy サイドカーでそのアカウントを参照するようにしました。

cloud-sql-proxy という名前でサービスアカウントを作っています。

kubectl apply -n <名前空間> -f - <<EOF
apiVersion: v1
kind: ServiceAccount
metadata:
  name: cloud-sql-proxy
  namespace: <名前空間>
EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  name: <Cloud SQLプロキシ サイドカーの名前>
  namespace: <名前空間>
spec:
  template:
    spec:
      serviceAccountName: cloud-sql-proxy
      containers:
        - name: cloud-sql-proxy
          image: gcr.io/cloudsql-docker/gce-proxy
          command:
            - "/cloud_sql_proxy"
            - "-log_debug_stdout"
            - "-instances=<接続名>"
          securityContext:
            runAsNonRoot: true

Workload Identity の設定方法は上記リンクにある通りですが、GKE のサービスアカウントが特定の名前空間内にある場合は、kubectl annotate serviceaccount コマンド内でも同様に指定する必要があります。

kubectl annotate serviceaccount \
-n <名前空間> \
cloud-sql-proxy \
iam.gke.io/gcp-service-account=<GCPのサービスアカウント名>@<プロジェクト名>.iam.gserviceaccount.com

istio-ingressgateway を gatewayAPIに移行する

クラスタ作成の所で GatewayAPI を使用しているのですが、旧システムは istio-ingressgateway を使用しているため、書き替え作業が必要になりました。

ざっくりとした理解ですが、

  • (新)Gateway -> (旧)Gateway
  • VirtualService -> HTTPRoute

というイメージです。
旧システムでは (旧)Gateway を複数作っていましたが、1つの (新)Gateway から HTTPRoute を生やした方が今回は取り回しが良さそうだったので、そのように書き換えました。

Gateway (1つだけ作成)

kubectl apply -f - <<EOF
apiVersion: gateway.networking.k8s.io/v1beta1
kind: Gateway
metadata:
  namespace: <名前空間>
  name: <Gatewayの名称>
  annotations:
    networking.gke.io/certmap: <認証マップ名>
spec:
  gatewayClassName: gke-l7-gxlb
  listeners:
    - name: https
      port: 443
      protocol: HTTPS
  addresses:
    - type: NamedAddress
      value: <予約済みのIP>
EOF

gatewayClassName に指定している gke-l7-gxlb は、最初に紹介した記事の設定を進めると作成されます。

HTTPRoute (アプリごとに作成)

apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
  name: <HTTPRouteの名称>
  namespace: <名前空間>
spec:
  hostnames:
  - <ドメイン>
  parentRefs:
  - name: <Gatewayの名称>
  rules:
  - backendRefs:
    - name: <アプリの名称>
      port: <アプリの動作ポート>
    matches:
    - path:
        type: PathPrefix
        value: <待ち受けているパス>

Cloud Load Balancing のヘルスチェックに対応する

GatewayAPI を使用すると、ロードバランサとして Cloud Load Balancing を利用する事になります。
Cloud Load Balancingを利用するためにはヘルスチェックを通す必要があるのですが、今回作ったアプリの中にはルートパス(/)で待ち受けていないものもあり、そういったものはヘルスチェックが通らずに導通が出来ませんでした。

ヘルスチェックのパスを変更する事も出来るようなのですが、今回作ったアプリは Istio を使ってJWT のトークン認証を行っており、ヘルスチェックはその影響も受けるようです。

そのため、アプリが実際に待ち受けているパスでヘルスチェックを行っても結局 Istio に阻まれて導通できず、最終的にはアプリの方を変更してルートに無条件で 200 を返すエンドポイントを設け、Istio の設定でもルート(/)は認証無しで通過できるようにしました。

若干強引な気もしましたが、HTTPRoute のパスルールでルートを指定しなければ外部からここに到達する事は出来ないので、今回は良しとしました。

mTLS は使わない

移植元では強制的に mTLS を使うように設定されていたので、以下のように有効にしたのですが…

kubectl apply -f - <<EOF
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: default
  namespace: <名前空間>
spec:
  mtls:
    mode: STRICT
EOF

Cloud Load Balancing では mTLS が非対応でした。

https://cloud.google.com/load-balancing/docs/ssl-certificates?hl=ja

これを有効にしているとヘルスチェックが通らず、ここで1日溶かしました…

mTLSは何に使っていた?

旧環境で mTLS を使用していたのは istioAuthorizationPolicy でサービスアカウントベースの認証ポリシーを使用していたからでした。
仕様上どうしようもないので、今回は諦めて別の方法で認証するように変更しました。

デプロイする

今回は GitLab CI + Skaffold でデプロイするため、 .gitlab-ci.yml を書いてデプロイしました。ここの構成は色々な前提があったり複雑だったりするので、機会があったらまた別の記事で書こうと思います。

2023.04.19 追記: 関連記事を書きました

Skaffold + Kustomize についての記事を書きました。

https://zenn.dev/soumi/articles/8816bd6f24dfe8

Skaffold を使用した例ではないですが、GitLab Runnerを使ったデプロイについての記事を書きました。

https://zenn.dev/soumi/articles/ed23a05316cc2c

http を強制的に https にする

このままだと https でしか接続できないので http で接続された際は強制的に https にリダイレクトされるようにします。
基本的には以下の内容の通りに設定するだけで実現できます。

https://cloud.google.com/load-balancing/docs/https/setting-up-http-https-redirect?hl=ja#partial-http-lb

料金の調整をする

ここまでで基本的な動作はするようになったのですが、2~3日動かしてみると想像以上に料金がかかっている事が発覚。調べてみるとリソースの事は全く考えずにデフォルト値を使ってデプロイしているので、今回必要なリソースに対してかなり過剰なリソースが割り当てられている事が分かりました。

リソースの調整をする

今まではオンプレミスだった事もあってあまり気にしてこなかったのですが、しっかりとリソースの割り当てを調整する必要がありそうです。
Kubernetes 標準機能で割り当てリソースの調整はできますが、調べてみると GKE の制約で好きな値に設定できるわけではない事が分かりました。

https://cloud.google.com/kubernetes-engine/docs/concepts/autopilot-resource-requests?hl=ja#autopilot-resource-management

https://cloud.google.com/service-mesh/docs/managed/provision-managed-anthos-service-mesh?hl=ja#custom-injection

また、今回 istio-proxy は自動的にインジェクションされる設定になっているので、 Deployment を個別に設定する事が出来ません。しかし、以下の方法を用いる事で外部(?)から調整が出来るようです。

https://cloud.google.com/service-mesh/docs/troubleshooting/troubleshoot-sidecar-proxies?hl=ja

今回はインジェクト対象の Deployment にアノテーションを追加する形で調整しました。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: <アプリ名称>
spec:
  template:
    metadata:
      annotations:
        sidecar.istio.io/proxyCPU: 125m
        sidecar.istio.io/proxyMemory: 128Mi
    spec:
      containers:
        - name: <アプリ名称>
	  image: <アプリのイメージ>
          resources:
            requests:
              cpu: 500m
              ephemeral-storage: 100Mi
              memory: 512Mi

Cloud SQL に関しても同様に調整しています。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: <Cloud SQLプロキシ サイドカーの名前>
  namespace: <名前空間>
spec:
  template:
    spec:
      serviceAccountName: cloud-sql-proxy
      containers:
        - name: cloud-sql-proxy
          image: gcr.io/cloudsql-docker/gce-proxy
          command:
            - "/cloud_sql_proxy"
            - "-log_debug_stdout"
            - "-instances=<接続名>"
          securityContext:
            runAsNonRoot: true
          resources:
            requests:
              cpu: 125m
              ephemeral-storage: 200Mi
              memory: 128Mi

Spot Pod が使えないか検討する

GKE には Spot Pod というオプションがあり、これを使うと最大9割ほど安くなるようです。

https://cloud.google.com/kubernetes-engine/docs/how-to/autopilot-spot-pods?hl=ja#use-nodeselector

今回移行したのは社内システムですが、一瞬でも止まると会社が立ち行かなくなるようなものではないので、導入してみる事にしました。

おわりに

今回 GKE への移行時に調べた事、引っかかった事をまとめて記事にしたのですが、記事にしていない部分も多く、全体として分かりにくくなっているかもしれません…

部分的にでも、同じような所で引っかかった人の助けになれば幸いです。

Discussion