今回はGKEクラスタで運用中のサービスを別のGoogle Cloudプロジェクトに移す方法を紹介します。
当初簡単に考えていたのですが、思いがけず罠があったので紹介します。
はじめに
要件
現在GKE上で稼働しているサービスを別のGoogle Cloudプロジェクにダウンタイム無しで移行したい。
構成
サービスはGoogle Kubernetes Engine(GKE)のPod上で稼働しており、APIのアクセスポイントがIngress経由で外部に公開されています。
DNSはGoogle ClloudのCloud DNSで管理、TLS/SSL証明書はGoogle マネージド SSL 証明書を使用しています。
この状態でサービスを別のGoogle Cloudプロジェクトへ移行することとなりました。
移行前の準備
移行前の計画
移行前に考えていたのは次の流れでした。
移行用の新しいドメインを作る。
移行先のGoogle CloudプロジェクトのGKEクラスタに既存と同じ構成を用意する。
どちらのPodでもリクエストを受け付けられる状態にして、リクエストもとのドメインを移行先のドメインに切りかえる。
これでシンプルに移行できる予定でした。
計画の頓挫
しかし、詳細を検討していくと問題があることに気づきました。
サービスがステートフルだった
移行予定のサービスではユーザーからのリクエストによって内部状態が変更されるステートフルなサービスでした。
移行期間中に新旧クラスタへのアクセスが混在したときにも、状態を一貫して保持する必要があります。
一部リクエストでドメインを切り替えられない
運用上の問題で一部サービスを呼び出している部分についてドメインを切り替えられないことが判明しました。
つまり旧ドメインへのアクセスが残り続けます。
新しい計画
上記の問題に対応すべく新しい計画を考えました。
ステートフルなサービスへの対応
複数のクラスタ間で内部状態を同期出来るように内部状態を提供する新たなAPIを追加しました。
内部状態に依存した処理を実行した時に以前の状態が存在しない場合は他方のクラスタにリクエストして内部状態を取得するようにしました。
これにより、新旧クラスタへのリクエストが混在しても内部状態が一貫するようになりました。
このAPIは外部に提供したくないためCloud Armorを使用してアクセス元を制限しています。
リクエスト元のドメインを切り替えられない問題の対応
旧ドメインへのアクセスについて、新ドメインへリダイレクトする方法と、旧ドメインのDNSを書き換えて新クラスタへアクセスさせる方法を検討しました。
前者であればリダイレクトするだけなので対応は簡単ですが、リダイレクトする仕組みが残り続けると余計な管理コストが発生します。また少なからずレスポンスタイムも悪化することになります。
一方でDNSの書き換えは移行作業自体が大変でも、作業完了後はより理想的な状況となります。
そこで後者、DNSの書き換えを採用します。
実行中のサービスでDNSを切り替えるときの問題
DNSの切り替えだけでは不十分
DNSの切り替えはCloud DNSのAレコードを新クラスタに切り替えるだけではありません。
新クラスタに旧ドメインのTLS/SSL証明書をもたせる必要があります。
旧ドメインのTLS/SSL証明書がないままDNSを切り替えるとブラウザからなりすましと判断され通信が行えません。
TLS/SSL証明書はGoogle マネージド SSL 証明書を使うことで更新作業などを心配することなく管理することができます。
terraformで旧ドメインのGoogle マネージド SSL 証明書の発行依頼をかけるには次のように行います。
resource "google_compute_managed_ssl_certificate" "legacy-domain-cert" {
name = "legacy-domain-cert"
provider = google-beta
managed{
domains = ["old.example.com"]
}
新Google Cloudプロジェクトで証明書が発行されない
このリクエストはいつまでたってもProgressing
のままとなり、TLS/SSL証明書の作成は行われません。
新プロジェクトが旧ドメインの持ち主であることを証明できていないためです
この証明なくTLS/SSL証明書が発行されると誰でも好き勝手に好きなドメインを処理できるので証明書の意味がなくなってしまいます。
稼働中のサービスでGoogle マネージド SSL 証明書を発行するとダウンタイムが発生する
新Google CloudプロジェクトでGoogle マネージド SSL 証明書を発行してもらうにはDNSの向き先を新Goolge CloudプロジェクトにあるIngressなどのロードバランサーに向ける必要があります。
しかし、この状態でDNSを切り替えると旧ドメインへのアクセスが新クラスタに向いてしまいます。
新クラスタにはまだ旧ドメインのTLS/SSL証明書がないためブラウザはなりすましと判断しSSLエラーを出してしまうでしょう。
つまり、DNSを切り替えるには事前に旧ドメインのTLS/SSL証明書を新クラスタに発行してもらう必要があるが、旧ドメインのTLS/SSL証明書を新クラスタに発行してもらうには事前にDNSを切り替える必要がある。というデッドロック状態になってしまいました。
サービス停止が許されるのであれば、DNSを切り替えてGoogle マネージド SSL 証明書の発行依頼をかけるという選択肢もありえます。
この場合DNSを切り替えてからGoogle マネージド SSL 証明書が発行されIngressに設定するまでの間サービスを受け付けられなくなります。
さらなる注意点としてこのときメンテナンス画面などを表示することもできず該当ドメインへのアクセスはすべてSSLエラーになります。
Google マネージド SSL 証明書はエクスポートできない
そこであらかじめ旧GKEクラスタで使用しているTLS/SSL証明書を新Google Cloudプロジェクトに登録しようとしました。
しかし、Google マネージド SSL 証明書にはエクスポートする機能がなく旧クラスタのTLS/SSL証明書を取得できませんでした。
ダウンタイムは許容したくなかったため、別の方法を検討します。
Let's encryptを使った移行
別の証明書サービスを使う
Google マネージド SSL 証明書はエクスポートができませんが、GKEはGoogle マネージド SSL 証明書以外の独自に用意した証明書を使用可能です。
そこでエクスポート可能な証明書を用意して移行することにしました。
今回は無料で使えるLet's encryptを使用します。
Let's encryptもドメインの所有確認を行う必要があります。
ドメインの確認はDNS-01を採用しました。
この方法もDNSを使用するため、TLS/SSL証明書は旧ドメインのDNSが向いている旧クラスタで発行します。
証明書の発行にはKubernets上でLet's encryptの証明書発行を自動で行える cert-managerを使用しました。
cert-managerの使用方法はesakaさんのGKEのIngressでLet's Encryptの証明書を利用できるようにするという記事がとても参考になりました。
証明書の移行
cert-managerで取得した証明書はcertificate.yml
のsecretName
に記載した名前でsecret
に格納されるため、kubectl
コマンドで取り出します。
crt部分に証明書が格納され、key部分に秘密鍵がbase64でエンコードされて格納されているので、それぞれbase64にデコードして取得します。
kubectl get secret <SECRET_NAME> -n cert-manager -o jsonpath='{.data.tls\.crt}' | base64 --decode > cert.pem
kubectl get secret <SECRET_NAME> -n cert-manager -o jsonpath='{.data.tls\.key}' | base64 --decode > cert.key
新Google CloudプロジェクトのCertificate Manager で証明書を追加を選び、セルフマネージド証明書を作成する を選んで先ほど取得した証明書をアップロードします。
新クラスタのIngressで ingress.kubernetes.io/ssl-cert:
に移行用のTLS/SSL証明書を 追加 します。
このとき現在使用しているTLS/SSL証明書を上書きしないように注意してください。現在使用しているTLS/SSL証明書は新ドメイン向けの証明書のため、これを削除すると新ドメインへのアクセスがSSLエラーを起こすようになってしまいます。
カンマ区切りで複数の証明書を指定します。
ingress.kubernetes.io/ssl-cert: new-domain-cert,migrate-cert
これで旧ドメインへのアクセスを新クラスタで受け付けられるようになります。
DNSの移行
DNSの向き先を新クラスタのIngressに変更します。
resource "google_dns_record_set" "api-record" {
中略
type = "A"
rrdatas = ["<新クラスタIngressのIPアドレス>"]
}
DNSが切り替わると次第に旧ドメインへのリクエストが新クラスタへ流れてくるようになります。
DNSはキャッシュによりアドレスが浸透するまで時間がかかるため、旧クラスタへのアクセスが完全になくなるまでしばらく待ちます。
余裕があれば72時間以上待つのが望ましいです。
Google管理証明書の作成
Let's Encryptは証明書の有効期間が短く短期間での更新が必要です。
今回はcert-managerを使いましたが、cert-managerを実行しているのは旧クラスタのため次回の更新は失敗します。
そこで、証明書をGoogle管理証明書に切り替えます。
Google管理証明書を使用することで証明書の更新などをマネージドに任せることが可能です。
すでにDNSの向き先が新クラスタに変わっているので、新Google CloudプロジェクトでGoogle管理証明書を発行することができます。
resource "google_compute_managed_ssl_certificate" "legacy-domain-cert" {
name = "legacy-domain-cert"
provider = google-beta
managed{
domains = ["old.example.com"]
}
Certificate Managerを開き、証明書が FAILED_NOT_VISIBLE
となっているのを確認します。
これは、まだ新しいGoogle管理証明書をIngressに登録していないためです。
移行用の証明書を登録したときと同じように、新しく作成した旧ドメイン用のGoogle管理証明書をingressに追記します。
ingress.kubernetes.io/ssl-cert: new-domain-cert,migrate-cert,legacy-domain-cert
このときも移行用の証明書を登録したときと同じように、新ドメインや移行用の証明書を削除しないように注意してください。
Certificate Managerに戻り証明書が有効になるのを待ちます。
移行用証明書の削除
Google管理証明書が有効になったら、IngressとCertificate Managerから移行用の証明書を削除します。
ingress.kubernetes.io/ssl-cert: new-domain-cert,legacy-domain-cert
急ぐ必要はないですが、有効期限が切れる前に削除する必要があります。
certbotを使用して移行用の証明書を失効させます。
certbot revoke --cert-path cert.pem
旧クラスタのお掃除
これで移行作業は完了しました。
旧クラスタを削除することができます。
移行用に作成した状態を同期するためのAPIやCloud Armorの設定も削除しておきます。
おわりに
今回Google Cloudプロジェクトを移行するという比較的シンプルな要件に対して思ったより罠が待ち構えている感じでした。
参考にさせていただいた方やご助力いただいた方に感謝したいと思います。
Google Cloudプロジェクトをダウンタイム無しで移動させたいと思ったときは思い出していただけると嬉しいです。