RailsアプリとかをAWSのレガシーシステムからGCPのイケイケシステムに移行した話
Contents
- はじめに
- レガシーシステムとイケイケシステム
- ざっくり全体像
- レガシーシステム
- イケイケシステム
- なぜ移行したのか
- アーキテクチャ刷新の目的
- インフラ移行の目的
- プロジェクトについて
- 状況
- プロジェクトチーム
- プロジェクト全体の流れ
- 技術調査
- 根回しとか稟議的なアレ
- Terraform整備
- GCPのProject構成
- Kubernetesクラスタの設定
- Railsアプリの移行準備
- コンフィグ整理
- バグ潰し
- リファクタリング
- SMTPの廃止
- fluentdの廃止
- オブジェクトストレージ整理
- Docker化
- Kubernetes化
- リソース構成
- デプロイ方法
- マニフェストYAML生成
- Secret管理
- Railsアセット配信方法の変遷
- Connection refused問題
- CI整備
- 動画変換機能のGCP対応
- メンテナンスサーバ構築
- 移行手順書作成
- 移行リハーサル
- 負荷テスト
- QAテスト
- 移行作業
- 移行後
- 良かった点と反省点
- 今後
- 終わりに
はじめに
Rails アプリケーションを中心とするシステムを AWS から GCP に移行しました。本記事ではその過程をできるだけ赤裸々に公開します。
本プロジェクトではインフラ移行と同時にアーキテクチャも刷新しました。AWS がレガシーで GCP がイケイケという意味ではなく、移行対象システムのアーキテクチャがレガシーからイケイケになったという意味です。
技術的な内容については詳細は省いて概要の説明にとどめています。AWS、GCP、Docker、Kubernetes あたりの知識があるとスッと読めると思います。
書きたいこと書いたので長い記事になってますがぜひお付き合いください。
レガシーシステムとイケイケシステム
まず、移行前のレガシーシステムと移行後のイケイケシステムについて軽く説明します。
タイトルをキャッチーにするためこうしましたが、特別レガシーでもイケイケでもないのでご了承ください。ちょっと前と今の普通のアーキテクチャという感じです。
ざっくり全体像
移行前のシステムのざっくりとした全体像はこんな感じです。

- 基本はモノリシックな Rails アプリ
- クライアントとして Android アプリ、iOS アプリがあり、それらは Rails の API を叩いている
- Web は Rails で HTML を出力している
- 管理画面も同じ Rails アプリで実装している
- モノリシックな Rails アプリ以外にも周辺にいくつかアプリケーションが存在する
- 多くの外部サービスに依存している
レガシーシステム
モノリシックな Rails アプリケーションを中心として AWS 上に構築されたシステムです。6 年間開発されていてそれなりに負債もたまっています。
EC2-Classic の VM インスタンスに OpsWorks の Chef でプロビジョニングを行い、OpsWorks でデプロイしていました。データベースやストレージは RDS(MySQL)、ElastiCache(Redis、Memcached)、DynamoDB、S3、CloudSearch などを使用していました。
Rails アプリ以外にも、EC2-VPC にデプロイされた Go のアプリケーション、Lambda や AWS Batch で動作するアプリケーションなどが存在しました。
イケイケシステム
同じくモノリシックな Rails アプリケーションを中心として GCP 上に構築されたシステムです。
各アプリケーションはコンテナ化され、GKE の Kubernetes 上で動作しています。データベースやストレージも一部を除き GCP のサービスを使用しています。
DynamoDB や CloudSearch など引き続き使用している AWS のサービスもあります。
なぜ移行したのか
本プロジェクトではアーキテクチャ刷新とインフラ移行を同時に行いました。本記事のアーキテクチャという言葉はシステムのインフラ構成ぐらいの意味で使っています。
アーキテクチャ刷新の目的としては 4 つありました。
- 運用コスト削減
- セキュリティ向上
- 開発効率向上
- 今後のビジネス展開の準備
インフラ移行の目的としては 3 つありました。
- 新アーキテクチャの構築
- セキュリティ向上
- インフラコスト削減
それぞれについて説明します。
アーキテクチャ刷新の目的
運用コスト削減
旧システムは急ごしらえで構築され、現在ではインフラ担当者もおらずほぼ放置で長年運用されていたためかなりガタがきていました。運用のコストも馬鹿にならなかったのでそれを改善する目的がありました。
旧システムのガタとしてはこんな感じでした。
- アラート頻発
- デプロイに 30 分から 1 時間かかる
- デプロイするたびに障害発生
- Chef のコードは不要なコードだらけのコピペ祭りだし一部はエラーで実行不可能
- Ruby のバージョンアップは VM に SSH でログインして頑張る
- インフラ構築した人はすでにおらず Infra as Code もされていないので構築意図がまったくわからず何か起こるたびに困る
- などなど
高頻度で様々な障害対応が発生するけど誰にも聞けずに辛みが深いし、インフラを改善しようにもほぼコード化されてないし部分的にコード化されてる Chef もリファクタリングが必要とかいうレベルではない上にそもそもエラーで実行できない箇所があるという状況でした。
こういった状況を抜本的に改善するために、アーキテクチャを刷新するという選択をしました。
セキュリティ向上
今までのインフラはセキュリティ意識が低く構築されていました。
- OpsWorks と Chef コードの制約からめっちゃ古い OS を使い続けている
- Kernel もミドルウェアも古い
- 新しい脆弱性に対するセキュリティパッチがない
- SSH の鍵は Dropbox で広く共有されている
- MySQL の root パスワードが誰かの名前
- IAM の権限がめっちゃ強い
- などなど
という具合です。脆弱性など致命的な部分は都度対応しているものの、それしかできていない状態でした。
セキュリティに関しても抜本的に作り直したほうが早く改善できるという判断でした。
開発効率向上
次のような施策によって開発効率の向上を目指しました。
- インフラまわりの単純化
- 徹底的な Infra as a Code
- コンテナ化
- Kubernetes 化
新システムではインフラまわりを単純化することで理解しやすくして開発効率向上を目指しました。旧システムは歴史的経緯なのかそもそもの設計が悪いのかわかりませんが無駄な複雑さが多くありました。そういった複雑な依存や機能をひとつひとつ紐解きシンプルに構築しなおすことで理解しやすくしました。
新システムではほぼすべてを Terraform で構築しました。Terraform でカバーできない範囲もコード化し CI/CD するようにしました。旧システムでは誰が何を意図して作ったかもよくわからないインスタンスや設定が多々あるし、Chef のレシピがあったとしても実はエラーで実行されなかったり、実際の設定は手動で変更されてたりするので期待される正しい状態がわからないという状況でした。そういったことがないように構成管理は Terraform に一任し、コードは Git でバージョン管理するようにしました。
コンテナ化によってアプリケーションの実行環境に対してアプリ開発者が責任を持てるようにしました。新しいライブラリが必要になったり Ruby のバージョンアップしたくなったりしても Dockerfile を修正するだけで済みます。
Kubernetes を採用することでインフラの単純化、インフラのコード化、実行環境に対する権限の移譲をシンプルに実現しました。Kubernetes 自体がシンプルかどうかは様々な観点で議論があると思いますが、アプリ開発者がアプリケーションを継続的に運用するという点では一から AWS で同じものを構築するより簡単に実現できます1。Kubernetes はとてもよくインフラを抽象化していて、理解すれば様々なことを標準機能2で実現できます。標準機能でできるということが大切で、Kubernetes 採用にあたっては標準機能でできないことはしないということに気をつけました。
今後のビジネス展開の準備
今後のビジネス展開として新しいサービスを開発していくための準備という目的がありました。「サービスを新規開発していくからマイクロサービスができるようによろしくやっといてくれ」みたいなことを言われていました。サービスの新規開発と Microservices とはまったく別の話ですが、新しいアプリケーションを構築する際にも統一的なインフラ基盤があったほうが開発・運用の効率がいいことは間違いないのでそれに備えるという目的がありました。
インフラ移行の目的
なぜ AWS でアーキテクチャを刷新せずに GCP に移行したか、という話です。これは簡単で、Kubernetes を使うためです。
技術選定をしたときにマネージド Kubernetes を使おうと思ったら GKE 一択だったので GCP 以外に選択肢は考えていませんでした。また、BigQuery を使うためにデータ分析基盤が GCP に構築されており、今後のデータ活用を考えると GCP に統一した方が転送量等のコストも抑えられるし開発運用がやりやすいというのも理由です。
他にも値段あたりの VM 性能が良かったり、セキュリティへの安心感3があったりという理由もありました。
プロジェクトについて
プロジェクトについて、社内やチームの状況、全体の流れなどを説明します。
状況
なかなか特殊な状況だったので、まずそれを説明します。状況が異なればプロジェクトの進め方等も異なってくると思います。
自分について
会社には数ヶ月の業務委託を経て入社しました。業務委託期間を含めて移行プロジェクトを始めるまでは CTO の傭兵のような立ち位置で次のようなことを行っていました。
- データ・機械学習系
- ログ分析基盤構築
- 類似画像検索エンジン開発
- 画像置換システム開発
- 記事カテゴリ分類 API 開発
- 機械学習チーム立ち上げ
- インフラ系
- 障害対応
- パフォーマンスチューニング
- セキュリティ対応
- 調査とか掃除とか
- Rails のパフォーマンスチューニング
- 勉強会の主催
- などなど
機械学習寄りでいろいろやりつつ、他にできる人がいないのでインフラまわりも最低限は面倒をみていました。Rails に関しては、アプリケーションの機能開発にはまったく関わらず、使用 Gem のせいでめちゃくちゃ遅くなっていた部分に関して泣く泣く Rails にパッチをあてたり、CI を高速化したりと裏方的なところをやっていました。
そんな中で、インフラやべーからなんとかしないと、という話がずっとありました。あるタイミングで CTO とバックエンドのリードエンジニアと、いつかはやらないといけないしインフラ移行やろう、という話をして自分がやることになりました。
自分は Rails アプリの機能開発は一切していなかったので、ドメインは全然詳しくないし、コードベースもほぼ触ってないし、インフラは一番詳しいかもしれないけどまだまだ闇は深い、という状況でプロジェクトが開始しました。何かあれば CTO とリードエンジニアと相談しつつ進めようという感じでした。
開発チームについて
本プロジェクト開始と同時期に会社がごたついて全社的な退職のビッグウェーブが来てしまい、開発チームも CTO 含め Rails エンジニアが全員退職しました。以前は業務委託等でもっと多かったみたいですが、本プロジェクト開始とほぼ同時にサービスの Rails 開発者がゼロになりました。
CTO は事業責任者も兼任していて、サービスの Product Management、Project Management、技術チームのリードなどなどかなり広範囲のことをやっていたし、その後の会社の対応もよくなくて社内はまあ荒れました。詳しくは大人の事情で割愛します。
その後、クローズが決まった他サービスを開発していた Rails エンジニアが開発チームにジョインしましたが、もちろんドメインには詳しくないしコードベースには触っていないというところからでした。
そんな感じで、誰も何も知らないし何も決められないという状況でプロジェクトを進めることになりました。
プロジェクトチーム
本プロジェクトのチームについて説明します。
といっても自分一人でした。前述のような状況だったので、プロジェクトマネジメントや実作業を 1 人でやっていました。本プロジェクト以外にも通常の運用業務や Rails 含むバックエンドの技術的なケア、その他の割り込み開発、機械学習チームのリードをやりつつ、という感じでした。
後半はいろいろあって機械学習チームが自然消滅した4のでメンバーの 1 人には週 2 で移行プロジェクトを手伝ってもらいました。移行当日の深夜作業も手伝ってもらったり、彼なしでは途中で心が折れてプロジェクトを完遂できなかったと思います。圧倒的感謝です 🙏 🙏 🙏
また、他サービスからジョインした Rails エンジニアにもコードレビューしてもらったり、確認のタスクをやってもらったりしました。🙏
最後の QA テストでは PM や iOS、Android のエンジニアにも手伝ってもらい、不具合を修正できました 🙏
また、後半のメンテナンス等の調整は PM にやっていただきました 🙏
謝辞みたいになってしまいましたがそんな感じでした。基本的には 1 人で、他にケツ持つ人もおらず、相談相手もいないという状況でした。
プロジェクト全体の流れ
プロジェクトの流れはこんな感じでした。単純にひとつずつこなしていったというわけでもないので、多少の前後はあります。また、以降で説明するものに絞って列挙しています。
- 技術調査
- 根回しとか稟議的なアレ
- Terraform 整備
- Kubernetes クラスタの設定
- Rails アプリの準備
- Docker 化
- Kubernetes 化
- CI 整備
- 動画変換機能の GCP 対応
- メンテナンスサーバ構築
- 移行手順書作成
- 移行リハーサル
- 負荷試験
- QA テスト
- 移行作業
技術調査
最初に新しいシステムをどういう技術スタックで構成するかを決定するために調査・検討しました。
例として次のような判断がありました。補足として選定理由やコメントも付け加えています。
- Docker でいこう
- コンテナで動かしてまずいワークロードはなかった
- Kubernetes/GKE でいこう
- マネージドで考えると ECS もあったが Kubernetes on AWS の噂もありわざわざプロプライエタリな ECS を学習したくなかった
- Kubernetes の経験があったし好きだった
- Cloud SQL でいこう
- メンテナンスは許容できる
- RDS を使っているが、Cloud SQL でも性能は問題なさそう
- Redis は HA 構成で Kubernetes クラスタにデプロイしよう
- 選定当時 Memorystore がなかった
- セキュリティめんどくさくなるしパフォーマンスの観点から ElastiCache は使いたくなかった
- HA 構成 Redis の構築・運用経験があった
- → 選定後に東京リージョンに Memorystore が追加されたので最終的にはそっちを使った
- Memcached は Kubernetes クラスタにデプロイしよう
- キャッシュだし
- DynamoDB は使い続けよう
- DynamoDB と密結合してる部分があった
- レイテンシは問題なかった
- Kubernetes のクラスタは 1 つでいこう
- 本番環境、ステージング環境を同じクラスタに同居させる
- 1 人で複数クラスタの面倒をみつつ移行作業するのは負担がでかいと判断した
- → 移行後、本番環境専用のクラスタとそれ以外の開発クラスタに分割した
- Spinnaker はやめておこう
- 検証はしたが必要なかった
- 運用つらそう、ルール作りつらそう、コードで管理できない
- デプロイするために Kubernetes に加えて Spinnaker の知識が必要となってしまう。開発者の学習コストを抑えたかった
- CronJob でいこう
- Job を高可用、スケーラブルにできる
- それまでは whenever gem を使って 1 つの VM で定期バッチをすべて実行していたが問題が多かった
- 証明書は cert-manager で取得しよう
- ACM で取得していた証明書の代替が可能
- 多少バグがあったりしたが問題なかった
根回しとか稟議的なアレ
根回しというか、技術的な部分以外でプロジェクトを始めるまでにやったことと理由やコメントです。
- GCP の営業チームとミーティング
- プロジェクト初期は定期的にやっていた
- 社内への Google さんと一緒にやってますよというアピールの意味合いも強かった
- 今後リリースされるサービスのクローズドな情報を教えてもらえてよかった
- GCP を使う上での注意点など教えてもらえてよかった
- 社内でインフラの技術的な話ができる人はいなかったので、GCP のエンジニアと同じレベル感で話せてコメントがもらえるのがよかった
- Google オフィスに行くのは楽しかった ☺️
- GCP のエンジニアに弊社にきてもらって技術ハンズオンしてもらった
- 弊社と GCP の偉い人同士のミーティング
- GCP の偉い人にきてもらって、弊社の偉い人に話をしてもらった
- 新しい事業責任者にプロジェクトを説明
- 社内の話
- プロジェクトのゴーサインを責任者にもらうため
- 何をするのか、なぜ必要なのかを説明
- ダウンタイムが発生するということも説明
- その他
- CTO/事業責任者がいなくなってたので、ある程度偉い人にちょいちょい移行しますよ、よろしく。という話をしたりしてた
最初の Google チームが丁寧に対応してくれていなかったらプロジェクトが開始できていなかったかもしれないので感謝しています 🙏
Terraform整備
新しいインフラの構築には Terraform を使いました。GCP だけでなく新システムに必要な AWS や CDN のリソースも Terraform 化しました。
移行時の Terraform 運用は単純で、Pull Request を作ると terraform plan の結果がコメントされ、master ブランチにマージされると terraform apply されるというものでした。通知やコメントには mercari/tfnotify を使っています。
コード構成も単純で、アプリケーションごとに module としてディレクトリを分割していました。ここでいうアプリケーションは Rails アプリケーション、Go の広告配信アプリケーション、機械学習による記事カテゴリ分類 API、といった粒度です。Terraform の Workspace は使わず本番環境やステージング環境のコードが重複して存在していました。
最初期はアプリケーションごとに terraform apply するように実装しましたが、まだ必要無いと判断してスピードを出せるようにこのような構成にしました。Workspace を使わなかったのも同じ理由です。
移行後は一段落したので安全に運用できるように Terraform のコードと運用を構築し直しました。アプリケーションごとに plan/apply できるようにして影響範囲を抑え plan 結果を見やすくして高速化しました。また、Workspace も導入して本番環境とそれ以外の環境を分離しました。
GCPのProject構成
GCP では上述のアプリケーションごとにプロジェクトを作るようにしています。そうすることで、IAM での権限管理がしやすくなります。
Kubernetesクラスタの設定
Kubernetes クラスタは Terraform でデプロイしましたが、その他のクラスタに対する設定は専用のリポジトリでマニフェスト YAML を kubectl apply でデプロイするようにしています。Terraform で一元して管理したかったのですが当時は Kubernetes プロバイダがまだ充実していませんでした。
次のようなものを YAML で管理しています。
- ClusterRole
- ClusterRoleBinding
- StorageClass
- PodSecurityPolicy
- DaemonSet とそれに関わる Namespace や Role、Secret など
- Helm 関係
このリポジトリも Terraform と同様に、Pull Request で dry run して master ブランチにマージするとデプロイされるようにしました。
Railsアプリの移行準備
当初はアプリケーションコードにはあまり変更を加えずにインフラ移行・アーキテクチャ刷新をするという方針でしたが、結果的にはそれなりに手を加えることになりました。
以下の点について大きく修正しました。
- コンフィグ整理
- バグ潰し
- リファクタリング
- SMTP の廃止
- fluentd の廃止
- オブジェクトストレージ整理
それぞれについて説明します。
コンフィグ整理
まずはじめにコンフィグの整理をしました。これには次の 2 つの目的がありました。
- アプリケーションを知る
- どのような環境依存動作があるのか
- どのような外部依存があるのか
- コンフィグ周辺の機能やドメイン、コードの把握
- 移行中に必要となる様々な環境で動作するようにする
- AWS の production/staging 環境
- GCP の production/staging 環境
- AWS 用で今までどおり開発している開発者のローカル環境
- GCP の移行準備をしている開発者のローカル環境
コンフィグと言っているのは主に環境ごとに異なる次のような定数のことです。また、Rails.env をみて動作を変えるような分岐もここでのコンフィグに含みます。
- 各種 API キーやパスワードなどの認証情報
- データベースや外部サービスの接続先
- オブジェクトストレージのバケットやパス
- データベースなどの prefix や名前空間
- ホスト名やポート番号
- HTTP or HTTPS
- などなど
それまで各種コンフィグは様々な場所に散らばっていました。
config/application.rbconfig/database.ymlconfig/environments/*.rbconfig/initializers/*.rbconfig/secrets.ymlapp/やlib/の中の定数やクラス変数
つまりあらゆる場所にありました。これらを次のように整理しました。
- コンフィグは環境変数で設定する
- The Twelve-Factor App
- Kubernetes 環境で簡単に設定可能
- 本番環境、ステージング環境のコンフィグは Kubernetes の ConfigMap または Secret で管理する
- 環境変数は
config/my_app.rbで一元管理するconfig/my_app.rbを見ればすべてのコンフィグを確認できる- コンフィグを抽象化するため
- アプリケーション側では
ENV['NAME']のように直接環境変数を見ずにMyApp.config.keyのようにアクセスする- アプリケーション側が直接環境変数の面倒をみなくてよくする
- Boolean、Hash、Array などを扱える
- API キーやパスワードなどの秘匿情報は暗号化してコミットする
- 今までは平文でコミットされていた
- 本番環境、ステージング環境の秘匿情報は Kubernetes の Secret の YAML を暗号化してコミットしている
- Credentials を導入し全環境共通の秘匿情報は
config/credentials.yml.encで管理する- ここでの
RAILS_MASTER_KEYは Secret の暗号化の暗号キーとしても用いている
- ここでの
MyApp.config を実現するために、要件を満たして最もシンプルだった dry-configurable を導入しました。また、Credentials を使うために Rails を 5.2 にアップデートしました。
環境ごとに異なる動作をするようなコードは移行で必要になる様々な環境を考慮すると if 文が非常に複雑になってしまうため、ENABLE_MYFUNC のような環境変数を用意して分岐するようにしました。
# 修正前
do_myfunc if Rails.env.production?
# 修正後
do_myfunc if MyApp.config.enabled_myfunc?
今まで環境変数によるコンフィグ管理はしていなかったので、ローカル開発環境用のコンフィグは direnv で管理して、移行が終わるまでの AWS の本番環境、ステージング環境のコンフィグは dotenv を使って .env.production/.env.staging で管理するようにしました。
このコンフィグ整理でアプリケーションについて多くのことを知れたのと、設定が楽になり移行がスムーズにできたので最初に取り組んで正解でした。
バグ潰し
バグ潰しをしました。それまでは常に Sentry に数百の Issue が溜まっている状態だったので、GCP 環境でエラーが出ても埋もれて気づかないといったことを避けるためです。
コンフィグ整理と同じく、バグを修正することでアプリケーションを知るという目的もありました。
ただし、こちらはあまりにも数が多く、一筋縄ではいかないようなものもあり、さらに作業中も新しい Issue がどんどん増えるのである程度減らしたところで終了しました。
リファクタリング
前述のコンフィグ整理、バグ潰しはボーイスカウトになりきって作業しました。もう誰も知らない触らない部分も多かったので良い機会だとガツガツとリファクタリングしました。気づいたそのときにリファクタリングしないとコードはどんどん魔物化していくので重要なことです。
SMTPの廃止
それまでは Rails から SMTP でメールを送信していましたが、GCE では基本的に SMTP が使えないので Sendgrid の API でメールを送信するようにしました。これについては AWS で動作しているときに切り替えました。
fluentdの廃止
旧システムでは fluentd で様々なログを収集していましたが、新システムでは欲しいログは特になにもしなくても Stackdriver Logging に集約されるので、設定の管理コストや運用コストをなくすために fluentd を廃止することにしました。
Rails アプリにも fluent-logger で送信しているログがあったので、これを Stackdriver Logging に直接送信するように修正しました。こんな感じです。
# TODO(GCP): Remove fluentd
if MyApp.config.enabled_fluentd?
Fluent::Logger.post_with_time(table, data, timestamp)
end
if MyApp.config.enabled_stackdriver?
StackdriverLogger.write(
MyApp.config.stackdriver_log_name,
data.merge(timestamp: timestamp.utc.iso8601),
)
end
このとき、google-cloud-logging gem で Stackdriver Logging にログを送信しようとすると Segmentation fault で落ちるという問題が発生しました。結論としては Puma の Cluster Mode で preload_app! すると grpc がセグフォする、というバグでした5。メモリ効率は悪くなりますが Cluster Mode をやめることで対応しました。
オブジェクトストレージ整理
Rails のアセットファイル、ユーザーにアップロードされたファイル、それ以外のロゴ画像などの静的ファイルは S3 にアップロードして配信していました。今回のプロジェクトではせっかくダウンタイムがあるし、オブジェクトストレージも同時に移行しようということで S3 から GCS に移行しました。そのとき、オブジェクトストレージまわりでこれは美しくなさすぎて見過ごせないという以下の点を見つけたので修正しました。
- 全環境で 1 つの S3 バケットを使っている
- 環境の prefix がバラバラ
- 例えば production 環境のファイルの prefix には
s3.myapp.com/web/images/p/やs3.myapp.com/assets/production/といったものがある
- 例えば production 環境のファイルの prefix には
- バケット内の prefix を見てもどういう種類のファイルかわからない
- CarrierWave でアップロードされたものなのか?誰かが直接アップロードしたものなのか?
- 人手で直接 S3 にアップロードされたもの、
app/assetsにあるもの、public/にあるものが明確な基準がなく混在している
これを新システムでは次の方針で整理しました。
- 環境ごとにバケットはわける
- s.myapp.com
- s.staging.myapp.com
- s.development.myapp.com
- prefix でどういう種類のファイルかわかるようにする
- CarrierWave でアップロードされたもの:
s.myapp.com/upload/ - そうでないもの:
s.myapp.com/static/
- CarrierWave でアップロードされたもの:
s.myapp.com/static/にアップロードするファイルはすべて Git でpublic/static/にコミットする- assets、packs は GCS にはアップロードしない
- アプリケーションサーバから配信して CDN でキャッシュする
これを実現するためには S3 から GCS へのデータ転送とそれぞれのファイルのパス変更が必要になります。移行時にこの 2 つを一気にやろうとすると非常に時間がかかるので、移行まで日次で以下の処理を行うバッチを実行するようにしました。
- GCP の Storage Transfer Service を使って S3 から GCS の中間バケットに全ファイルを転送する
- prefix マッピングテーブルに従って中間バケットから GCS の各環境用バケットにファイルをコピーする
- このとき各ファイルで更新時間を比較し、更新がなければコピーしない
このバッチスクリプトははじめは Ruby で実装していましたが、数日経っても終了しないので Go で実装しなおしたところ数時間でおわるようになりました。
日次で実行しても新しい prefix へのマッピングは約 50M 個のファイルをすべてチェックする必要があるので 6 時間強必要でした。S3 から GCS への 1 日分のファイルの転送は 30 分程度なので、合計 7 時間程度処理にかかっていました。
実はオブジェクトストレージの移行はやるかどうかかなり悩みました。というか最初はやらないつもりでした。アプリケーションサーバは GCP でもそのまま S3 を使うことはできたし、汚いまま GCS に移行することもできたからです。移行することで工数はガツッと増えるし、移行で気にすることが増えるため、「移行」プロジェクトとして考えたときには大きいデメリットがありました。しかし、まだ続いていくサービスとしてはやったほうがいいことは明らかでした。
結局、ダウンタイムなしでこれを実現するにはアプリケーション側で頑張らないといけないけど頑張る人はいなくなったし、今やらないと今後永久にできないだろうということで、これ以上エンジニアの SAN 値を削らずサービスを存続させるためにもこのプロジェクトでやることに決めました。男気のある良い決断だったと思います。
Docker化
Rails アプリについてはそれまでも Docker 化しようという試みはあり Dockerfile は存在したのですが、CentOS 6 に rvm で Ruby をインストールして Nginx やら Node やらを詰め込んで monit を起動するという VM 用の Chef をそのまま移植したみたいな代物でした。さすがにそれを使うわけにはいかないので一から作り直しました。
どのアプリケーションの Dockerfile も特殊なことはせず、こんな感じになっています。
- Rails アプリ
- Base イメージは ruby:x.x.x-slim-stretch
- Multi-stage ビルドのビルドステージで次のことをしている
bundle installyarn installrake assets:precompile
- Go アプリ
- Base イメージはなし(scratch)
- Multi-stage ビルドのビルドステージでビルド
また、この 2 種以外にも Docker イメージはあり、すべての Dockerfile で統一したユーザを作ってそのユーザを使うようにしています。6
Kubernetes化
Docker コンテナとして起動できるようにした後、Kubernetes で動作するようにしました。移行時は 1 つのクラスタに 18 個の Namespace があり、7 個のアプリケーションの本番環境とステージング環境が稼働していました。アプリケーションによって Kubernetes での構成要素やデプロイ方法が多少変わりますが、ここではメインとなる Rails アプリのみ説明します。
リソース構成
Rails アプリを構成する Kubernetes のリソース一覧です。
- Namespace
- 本番環境 Rails アプリ
- ステージング環境 Rails アプリ
- Deployment
- Puma
- Sidekiq
- Memcached
- Console
- 移行時にログインして作業するため
- Service
- Puma
- Ingress を使うため NodePort
- Memcached
- 外には公開しないので ClusterIP
- Puma
- Ingress
- Puma
- Job
- デプロイ時の
db:migrateや初期構築時のdb:createなどの Rake タスクたち
- デプロイ時の
- CronJob
- Whenever で管理していた定期実行の Rake タスクたち
- ConfigMap
- Rails 環境変数
- 環境変数で Rails アプリを設定できるようにしたので ConfigMap にすべてまとめて
envFromで設定している
- 環境変数で Rails アプリを設定できるようにしたので ConfigMap にすべてまとめて
- その他いろいろ
- Rails 環境変数
- Secret
- Rails 環境変数
- ConfigMap と同じ
- その他いろいろ
- Cloud SQL Proxy や cert-manager 用の Service Account の Credentials JSON など
- Rails 環境変数
- HorizontalPodAutoscaler
- Puma
- Sidekiq
- RoleBinding
- 開発者
- ステージング環境に admin 権限を与える
- 運用者
- 本番環境に admin 権限を与える
- PodSecurityPolicy 用
- default ServiceAccount に PodSecurityPolicy の use 権限を与える
- 開発者
- LimitRange
- default
- 意図しない kill 等を防ぐため
- default
- Certificate (cert-manager)
- Puma Ingress 用
- Issuer (cert-manager)
デプロイ方法
デプロイは CircleCI でデプロイ用の Bash スクリプトを実行しています。ブランチモデルは git-flow の簡易版で、feature ブランチを develop ブランチにマージしたらステージング環境にデプロイ、develop ブランチを master ブランチにマージしたら本番環境にデプロイするという運用になっています。
スクリプトはこんな感じです。Namespace が存在しない場合は初期構築の手順が追加されますが、ここでは省略しています。
- gcloud 等をインストール
- gcloud、kubectl の認証
- ブランチ名から適切な Kubernetes の Namespace を設定
- develop ->
export NAMESPACE=myapp-staging - master ->
export NAMESPACE=myapp-production
- develop ->
- Namespace 用の変数を設定
- 後述する
--build-argや Replica 数など ConfigMap で設定できないもの source k8s/namespaces/${NAMESPACE}/config.sh
- 後述する
- Docker イメージの build、push
- このとき Webpack(
assets:precompile)用の環境変数を--build-argで設定する
- このとき Webpack(
- Rake タスクで ERB テンプレートから Deployment や Job の YAML を生成する
- Namespace の各種リソースを
kubectl applyするkubectl apply -f k8s/namespaces/${NAMESPACE}/*.yaml
- Namespace の Secret を復号してデプロイする
db:migrateの Job を apply し、終了するまで待つ- CronJob を
kubectl apply --pruneする - Sidekiq、Puma の Deployment を
kubectl applyする
マニフェストYAML生成
Kubernetes のマニフェスト YAML は 3 種類の方法で管理しています。
- 普通の YAML
- 暗号化された YAML
- ERB テンプレート
Docker イメージの指定が必要ないマニフェストに関しては普通に YAML でコミットして、Secret は YAML を暗号化してコミットしています。
CI で Docker イメージをビルドするとき、Git のコミットハッシュをタグに使っています。そして、デプロイでは Deployment や Job の ERB テンプレートにコミットハッシュを埋め込んで YAML を生成する Rake タスクを実行して、生成された YAML を kubectl apply しています。ERB や Rake を採用したのは Rails との親和性が高く Rails 開発者が触りやすいからです。
また、Rake タスク実行用の Job や CronJob に関してはタスク名も埋め込めるようにしています。CronJob に関してはスケジュールとタスク名を定義する config/schedule.yaml というファイルから自動生成しています。config/schedule.yaml は Rails 開発者が Kubernetes を意識せず気軽に変更できるため、CronJob のみ kubectl apply --prune で削除にも対応しています。その他のリソースは手動で削除しています。
Secret管理
前述のとおり Secret は暗号化してコミットしています。そのために Sekret という簡単なコマンドラインツールを開発しました。sekret (new|edit|show|encrypte|decrypt) foobar.yaml.enc のように簡単に暗号化ファイルを扱えて、Secret のバリデーションもやってくれます。
Rails の credentials で使われている ActiveSupport::EncryptedFile や rails encrypted:edit を使うことも考えましたが、以下の理由で採用を見送りました。
- ただ YAML を修正したいだけなのに Rails なので起動が遅い
- Rails が動くまで環境構築しないと YAML を修正できない
- 暗号化してコミットしてしまうので編集時にスキーマのバリデーションをしたいができない
そんなわけで、お手軽に暗号化 YAML を扱えてスキーマチェックしてくれて CI で使いやすいワンバイナリな Sekret を作りました。
ぜひ使ってみてください(宣伝)。
Railsアセット配信方法の変遷
assets:precompile で生成されるアセットを配信する方法は紆余曲折ありました。
一番最初はアセットを asset_sync で GCS にアップロードしてアプリケーションとは別ドメインで配信していました。これは旧アーキテクチャがそうなっていて、プロジェクト初期段階でとりあえず GKE で動かすためにこうしていました。細かい手順としては、CI で Puma Deployment の apply 前に assets:precompile assets:sync を Job として実行し、Puma Deployment の Init Container で empty ボリュームに manifest.json をダウンロードする、という感じです。
asset_sync による方法は最初から変更するつもりでした。アプリケーションサーバ(Puma)の前段にも CDN がいるので GCS にアップロードせず直接 Puma から配信して複雑性を減らしたかったからです。
Puma からアセットを配信するときに Puma コンテナがアセットを持っておく必要があります。その場合、いつ assets:precompile して、どこに持つかということが問題になります。以下のような選択肢があります。
- いつ
assets:precompileするか- Puma コンテナ起動時 (
bundle exec pumaの直前) - Puma Pod 起動時 (Init Container)
- デプロイ時
- Docker イメージビルド時
- Puma コンテナ起動時 (
- どこに持つか
オブジェクトストレージ- 外部永続ディスク
- その他ボリューム (
emptyDirやhostPathなど) - 起動後のコンテナ
- Docker イメージ内
2 つ目の方法は、CI で Puma Deployment の apply 前にアセット用の Regional Persistent Disk を ReadWriteOnce でマウントした Job で assets:precompile を実行し、Deployment は ReadOnlyMany でその Disk をマウントするという方法でした。この方法は
- Docker イメージビルドがはやく、イメージが軽くなる
assets:precompileが 1 回で済む- Pod の起動がはやい
- Webpack に渡す環境変数を ConfigMap や Secret で管理できる
というメリットがあり、なにより GKE っぽいイケイケな感じで本来やりたい方法でした。
ところがこの方法もあとから問題が発覚しました。ボリューム関係のエラーで Pod が起動しないということが続き、調べると Regional Persistent Disk はそもそも ReadOnlyMany をサポートしておらず、レプリケーションも 2 つのゾーン間のみということがわかりました。完全に調査不足でした 😭
最終的に、Docker イメージビルド時に assets:precompile するという方法に落ち着きました。Pod の起動時間を犠牲にせず、最もシンプルな方法ということで採用しました。
最終的な方法ではイメージビルドが遅くなったり、イメージサイズが大きくなったり、Webpack 用の環境変数が ConfigMap で管理できないという課題があります。GKE においてそれなりのサイズのボリュームをマルチゾーンに Pod で共有する方法がない7以上、アセットは各 Pod でそれぞれ持つしかありません。そうするとイメージに含める以外だと各 Pod でダウンロードするかコンパイルするかになるので Pod の起動時間が大幅に長くなってしまいます。なので、イメージサイズについては諦めています。環境変数については Kubernetes 上で kaniko などを使ってイメージをビルドすることで ConfigMap や Secret で管理できそうなのでそのうち試してみたいと考えています。
ここに関してはまだ改善できると感じているのでいい方法を模索していきたいです。
Connection refused問題
デプロイしたときや、CronJob で稀に MySQL の Connection refused エラーが発生するという問題がありました。Rails 側から MySQL へリクエストするときに Sidecar の Cloud SQL Proxy が起動していないことが原因でした。これは Pod の起動時と停止時どちらにも発生し得るので、Rails 起動前に Sleep を入れることと停止時に Cloud SQL Proxy の preStop で sleep することで対応しました。
CI整備
Kubernetes で動くようにした後は CI を整備しました。スムーズに移行できるように旧システムへのデプロイと並行して、develop ブランチでステージング環境用の Namespace に、master ブランチで本番環境用の Namespace にデプロイするようにしました。移行プロジェクト中盤までは通常の開発に影響しないように Kubernetes へのデプロイが失敗しても CI はパスするようにしていました。
動画変換機能のGCP対応
システムの中で AWS の機能に依存していて GCP では代替が難しいものがいくつかあり、その中でも動画変換機能が AWS と GCP をうまく連携させて Kubernetes の恩恵を感じられたので紹介します。
動画変換機能はユーザが S3 にアップロードした動画を AWS の Elastic Transcoder で変換して S3 に保存するというものでした。Elastic Transcoder が変換後の動画を配信用 S3 バケットに保存してくれるので AWS で完結していれば非常に単純な仕組みです。図にするとこんな感じです。

- ユーザが S3 に動画をアップロード
- S3 オブジェクト作成イベントをトリガーに Lambda 関数を実行し、Elastic Transcoder の動画変換ジョブを作成
- Elastic Transcoder はアップロードされた動画を変換して配信用 S3 バケットに保存し、終了を SNS で通知
- SNS から動画変換終了を HTTPS で Rails アプリに通知
- Rails アプリは通知を受け取ったらデータベースの動画ステータスを更新
新システムでは変換後の動画を S3 ではなく GCS に保存する必要があります。S3 から配信することもできましたが、それまでの配信用の URL を GCP に向けるためドメインを変える必要があることや、動画だけ AWS から配信されるという複雑な状況を避けるために GCS から配信することにしました。
次の図が新システムでのアーキテクチャです。HTTPS で Rails アプリに直接通知せず、SQS 経由で動画転送アプリを通して Rails アプリに通知するようにしました。

- SNS から動画変換終了を SQS に通知
- SQS をポーリングしている動画転送アプリが通知を受信
- 動画転送アプリは GCP の Storage Transfer Service の転送ジョブを作成し動画を GCS に転送
- 転送が終了したら HTTPS で Rails アプリに SNS 互換の通知を送信
- Rails アプリは通知を受け取ったらデータベースの動画ステータスを更新
動画転送アプリから Rails アプリへの通知は SNS 互換としました。これには 2 つ理由があります。1 つ目は Rails アプリへの変更が不要だからです。2 つ目は Rails アプリを気にせず動画変換機能単体で移行ができるからです。

動画変換機能の移行作業中はこのように従来通り SNS から Rails アプリに通知を送りつつ、動画転送アプリも稼働させていました。Webhook のエンドポイントは冪等だったので、動画転送アプリがうまく動作していることが確認できたら動画転送アプリからも Rails アプリに通知を送るようにしました。そして最後に SNS からの通知を切りました。こうすることで、バツッと切替えるみたいにドキドキすることなく移行が完了しました。また、この機能は AWS で旧システムを運用している段階で移行して、本移行作業では無視できるようにしました。
動画転送アプリは独立したアプリとして Go で実装しました。Rails アプリに依存がなく、Rails だけでやろうとするとエンドポイントを増やしたりインフラの構成要素を増やしたり Sidekiq のスレッドを長く専有したりする必要があったからです。Kubernetes を採用したことで気軽にこのような選択肢を取ることができました。
メンテナンスサーバ構築
移行作業中にメンテナンスページを表示するためのメンテナンスサーバを構築しました。「もう少し待ってね」という HTML を 503 で返すだけのページです。何かをトリガーにしてメンテナンスモードになるようなものではなく、あくまでも本プロジェクトの移行作業のために構築しました。
また、それまでメンテナンスモードがなかったのでモバイルアプリには新しく 503 が返ってきたらメンテナンス表示がでるようにしました。
移行手順書作成
移行前に手順書を作成しました。手順書は Markdown で書き GitHub のリポジトリにコミットしました。作業当日に実行した手順をチェックできるように、手順一つ一つにチェックボックスをつけるようにしました。こんな感じです。
02 停止手順書
===========
# Webサーバインスタンス停止
* [ ] myapp-webのオートスケールスケジュールを設定する
aws --region us-east-1 opsworks describe-instances \
--layer-id ${myapp_web_layer_id} \
| jq -r ...
* [ ] myapp-webインスタンスを停止する
aws --region us-east-1 opsworks describe-instance \
--layer-id ${myapp_web_layer_id} \
| jq -r ...
GitHub の Web だとコミットされているファイルのチェックボックスにはチェックできないので、作業当日は手順書を Issue にコピーしてチェックしていきました。Issue にしておけばコメントで作業ログも残せるし、1 つの手順書が完了するとクローズできてテンションあがるのでいい方法でした。
ファイルとしては次の 6 つを用意しました。
- 準備手順書: 移行作業当日までの準備の TODO
- 停止手順書: AWS の旧システムを停止する手順書
- 転送手順書: MySQL やオブジェクトストレージのデータを転送する手順書
- 起動手順書: GCP の新システムを起動する手順書
- 起動後手順書: 新システムが起動したあと、周辺システムなどを新システムへ切替える手順書
- ロールバック手順書: 作業途中で問題が発生したときに AWS の旧システムを復旧させる手順書
だいたいの手順はチェックボックス 1 つにつき 1 つのコマンドとなっていましたが、関連した一連のコマンドを実行して長時間待って終わったら Slack に通知、のような作業は 1 つの Bash スクリプトにまとめたりもしました。
また、手順書のコマンドを実行するための作業サーバを構築しました。ローカルの意図しない影響が入らないようにするためと、ネットワーク切断や電源が落ちる等のトラブルを避けるためです。作業サーバでは screen を使って作業しました。作業サーバ構築もスクリプトで自動化して、何かあったときすぐ再構築できるようにしました。
移行リハーサル
旧システムを停止してから新システムを起動するまで迅速に作業できるように、転送手順書、起動手順書のリハーサルを行いました。これには負荷テストや QA テストのために本番環境のリアルデータを新システムにコピーするという目的もありました。
リハーサルでは各作業の時間を計測しながら実行し、手順書にミスがあれば修正してコミットしました。
負荷テスト
新システムが負荷に耐えられるか確認するため負荷テストを行いました。
今回のプロジェクトではシステムにかかる負荷が予めわかっていたので目標設定は簡単でした。
- シナリオは高負荷時に実際のスマホアプリから発生するリクエスト群
- 最高負荷時の倍のリクエスト (rps) をさばける
- レイテンシは旧システムより良い値を維持できる
負荷テストツールには Locust を使いました。選定理由は以下のとおりです。
- 上記のように目標が明らかで詳細な分析が不要だった
- Locust 単体で Web でグラフ表示できる
- シナリオを XML じゃない言語で書けて Git で管理できる
- 分散負荷テストが可能
インフラとしては GCP に Master 1 台と Slave 5 台の VM を用意しました。
最初はクライアント数が増えるとリクエストをさばけずにコンテナが再起動してしまっていたので、コンテナ内外の仮で設定した値をチューニングしました。
- Puma の Worker 数や Thread 数やメモリ制限
- コンテナの CPU、メモリ
- Deployment の Replica 数
- Horizontal Pod Autoscaler
HPA はスパイク時にはまったく追いつかないことがわかったので、高負荷時のために予め Pod 数を増やしておくようにしました。
通常なら負荷テストはもっと早い段階でやっておきたいところですが、今回はシステムの改修後に実データでやりたかったのでここで実施しました。そんなに負荷がかかるサービスでもないのでまあ大丈夫だろうと予想していて、技術調査の段階でもパフォーマンスのボトルネックになりそうな Cloud SQL の負荷テストは実施していました。
QAテスト
プロジェクトの最終チェックとして QA テストを行いました。自分は普段サービスを開発していないので、実作業は主に Rails エンジニアやアプリエンジニアなど他のエンジニアにやってもらいました。
対応漏れや、GCP 移行対応をしたあとに GCP を考慮せず追加された部分が見つかり修正しました。
しっかりチェックしてもらい非常に助かりました。
移行作業
土曜の夜から日曜の朝にかけて移行作業を行いました。業務委託で移行プロジェクトを手伝ってくれていたエンジニアと 2 人で作業しました。
その週はデプロイ禁止にして、最終的な確認・調整や AWS から GCP に切替える Pull Request を準備しました。また、作業開始から 1 時間程度前に集合して手順書、作業の流れ、分担を確認しました。
23 時から作業開始で最初にトラフィックをすべてメンテナンスサーバに向けるという作業があったのですが、ここでいきなりトラブルが発生しました 😱 切り替え作業をやったにもかかわらずメンテナンスサーバが表示されずに CDN のエラーメッセージが表示されていました。そのトラブルも 2 つの原因が重なっていて、作業開始早々いきなりの緊急事態発生でした。本プロジェクトで最もアツかった時間でした 😤 🔥 原因はメンテナンスサーバが HTTPS に対応できてなかったことと、Nginx の設定の不備だったので手分けして対応しました。ELB を作成し ACM で証明書を取得して HTTPS に対応し、Nginx の設定は直接サーバで設定を書き換えました。すぐにメンテナンスページを表示するはずが表示まで 50 分ほどかかってしまいました。
初っ端で躓いたもののそれ以降は特にトラブルもなく旧システムを停止し、仮眠などしつつデータ転送が終わるのを待ちました。
4 時半頃に MySQL のデータ転送が終了したので新システムの起動を開始しました。7 時半頃にはオブジェクトストレージのデータ転送も終了し、公開作業を開始しました。
8 時頃公開作業が完了し、待機してくれていた他のエンジニアが動作確認を開始しました。その動作確認で QA テストで漏れていた問題が見つかったり、HTTP から HTTPS に統一したことによる問題が見つかりましたがすぐに修正して 9 時前に公式にメンテナンス終了のアナウンスをしました。
その後諸々の確認や残タスクをこなし、15 時頃に移行作業を完全に終えました。
移行後
移行後は大きな問題はなく運用できています。問題と言えば Sidekiq のキューが詰まりスレッド数とコンテナ数を調整したぐらいです。
インフラ費用は安くなり、デプロイのたびに心の準備をしなくてよくなり、30 分から 1 時間かかっていたデプロイは 8〜15 分で終わるようになり、Ruby のバージョンアップができるようになり、同じ構成のステージング環境ができて、今の所いいことばかりです。
移行前後の 2 ヶ月を Pingdom で比較したところダウンタイムは 30 分から 14 分に減り、レスンポンスタイムは約 500ms はやくなりました。8

良かった点と反省点
良かった点は、なんと言っても移行後ほぼ問題なく運用できていることです。ただインフラを移行するだけでなくアーキテクチャやアプリケーションコードをガッツリ変更したにも関わらず、トラブルがなく運用できているのは素晴らしい成果だと思います。また、かなりスムーズに移行作業ができたのも非常に良かった点です。準備が長くて嫌にもなりましたが、しっかり準備をしたおかげですね。
プロジェクトの進行もうまくいったと思います。1 人でスケジュール管理してタスク管理していろんなリポジトリを行ったりきたりして目が回りましたが、それほど手戻りもなくスケジュールどおりに進めることができました。
反省点ですが、そもそも 1 人でやるようなプロジェクトではありませんでした。移行プロジェクトにフルコミットしてくれるエンジニアを探したり、PM をお願いしたりしたほうがよかったかもしれません。時間もかかったし、やりたかったけどできてないことも多々あります。一斉退職があった時点でプロジェクトを諦めたり会社を辞めたりという選択肢もありましたが、自分にとって今までにない挑戦だしいい経験になると思いやりました。
あと、メンテナンスサーバについては反省しかないですね 😂 他と比べて簡単な部分だったので気が抜けていました。
今後
まだまだやりたいことはいろいろあります。例をざっとあげるとこんな感じです。
- Service Mesh
- すでにいくつか Microservices 的なものがあり、Envoy あったらな〜的なことがあるので何かしらやりたい
- 分散トレーシング
- すでにいくつか Microservices 的なものがあり、このエラーどこがどうなった的なことがあるのでほしい
- One-shot なジョブを実行する仕組み
- Rake タスクとか。今はコンテナに入って実行する感じ
- Horizontal Pod Autoscaler の Custom metrics 利用
- Sidekiq をキューに溜まってるジョブ数でスケールするとか
- NetworkPolicy
- Namespace 間で通信を制限したい
- ヘルスチェック用エンドポイント
- 今は Rack が起動してるかどうかなので、ちゃんと readiness probe できるエンドポイントがほしい
- 監視の充実
- 今は最低限の監視しかないので充実させたい
- などなど
終わりに
今までも新規開発でエンジニア 1 人というプロジェクトの経験はありましたが、6 年続いている既存のシステムに関するプロジェクトでエンジニアどころか PM はおろか社内に相談相手すらおらず本当に 1 人で長期間走るのは(精神的に)大変でした。前半はごたごたのおかげでプロジェクト外のストレスも多かったですが、後半は週 2 日といえども頼りになるエンジニアが手伝ってくれたのと会社に行かずリモートワークとフレックスで様々なストレスをシャットアウトできたのがよかったんだと思います 😃 💭
プロジェクト自体は大成功と言っても良い結果となったので良かったです。
いろいろ知見や経験も得られたので、今後ひとつずつ何かしらの形で公開していきたいです。
Footnotes
-
簡単さの比較に関してはもちろん組み立て方次第なんですが、なんとなく雰囲気を感じ取っていただければ幸いです。参考: Kubernetes は辛いのか? - @amsy810’s Blog ↩
-
GKE のようなマネージドサービスの機能も含む ↩
-
GCP はかなりセキュリティに力を入れてるし、例えばコンテナまわりの脆弱性が発表されたときに GKE の Container-Optimized OS の場合は対応不要ということも多かった。 ↩
-
語り尽くせない出来事がいろいろあったりしたのですが、本筋と関係ないので泣く泣く割愛します。 ↩
-
正確にはないことはないが、ないと言っていいぐらい面倒な方法しかないはず。簡単な方法があったら教えてください。 ↩
-
あくまでも Pingdom のレスンポンスタイム ↩