スタディサプリ Product Team Blog

株式会社リクルートが開発するスタディサプリのプロダクトチームのブログです

Self-Hosted Cluster から EKS への移行と Platform の Production Readiness

こんにちは。SRE の @chaspy です。

Quipper では AWS 上で Kubernetes Cluster を運用してサービスを提供しています。 これまで kube-aws を用いて Kubernetes Cluster を Self Host してきましたが、このたび Managed Services である Amazon EKS に移行しました。(以下、 Amazon EKS を EKS と表記します)

本記事では、 Kubernetes Cluster の移行で遭遇した問題をどのように解決したかを説明します。また、数多くの Application が稼働している Platform を移行する際にどのような点を考慮するとよいのか、経験を通して学んだことを共有します。

EKS への移行を検討している方はもちろん、Platform Migration に携わる方にとって学びになる内容になりましたら幸いです。

背景

本編に入る前に、背景を説明させてください。

なぜ Self-Hosted Cluster を運用していたのか

Kubernetes Cluster への移行当時、EKS が Tokyo Region で利用可能ではなかったからです。

もともと Quipper の Platform は Heroku からはじまり、Deis*1 というオープンソース版 Heroku のようなものを AWS に Host。その後 Deis v2 という Kubernetes ベースのものを経て、Kubernetes Cluster に移行しました。

EKS が Tokyo Region で利用可能になったのは2018年12月でした。そして、我々が Production 環境に Platform を Kubernetes に切り替えたのは、Global が 2018年9月、日本の StudySapuri が2018年12月でした。

EKS の GA は2018年6月 だったので、Global では利用可能だったものの、StudySapuri との足並みを揃える必要があったのと、移行検証自体はその前から行っていたので、EKS は当時選択肢に入りませんでした。

なぜ EKS へ移行するのか

Control Plane の管理コストと、Cluster Switching コストの減少がおもな狙いです。

Kubernetes Cluster を Self Host するということは、Control Plane および etcd も自分たちで管理する必要があります。幸い、Production 環境で Control Plane 起因で障害が発生したことはありませんが*2問題が発生したときに自分たち自身で対処する必要があります。Managed Service に移行すれば、これらを私たちが考える必要はなくなります。

Deploy は kube-aws で自動化されており、そこまで大変というわけではないにしても、やはりどんなに慣れても1日ぐらいはかかってしまいます。その手間を少しでも軽減できれば良いと考えていました。

とはいえ、上記を鑑みても、実際の温度感は「困ってはいない」ぐらいでした。*3ではなぜ今回移行を決めたかというと、kube-awsKubernetes のサポートバージョンへの追随スピードです。v1.16 を待っている間に v1.15 が EOL になってしまったことが最終的な決定打になりました。*4

Kubernetes は3ヶ月に1度 Minor Version があがり、3 Minor Version しかサポートされません。そのため、Upgrade を素早くできるようになるということは非常に重要です。EKS へ移行することで、Upgrade がやりやすくなるだろうという狙いもありました。

考慮事項

続いて、EKS へ移行する上で考慮すべき事項について話します。

Networking

もっとも大きな違いは Pod Networking でしょう。EKS では Amazon VPC Container Network interface(CNI) plugin for Kubernetes を利用します。これにより、Pod ごとに Worker Node が属する Subnet の IP Address を消費します。

Quipper では VPCIPv4 CIDR Block を /16 で切っており、その中で Worker Node や他 EC2 Instance が利用している Private subnet は /24 で、AZ ごとに存在しています。

仮に既存の Private Subnet がこのようになっていたとします。

cidr first last total AZ
10.10.1.0/24 10.10.1.0 10.10.1.255 256 AZ1
10.10.3.0/24 10.10.3.0 10.10.3.255 256 AZ2
10.10.5.0/24 10.10.5.0 10.10.5.255 256 AZ3

Private Network 全体で 763 個の IP Address を持つことができ、これは現状だと十分な数です。

しかし、1 Pod あたり 1 IP Address を消費するとなった場合、明らかに枯渇することがわかりました。雑に Pod の数を調べてみても、どのクラスタも 1000 は余裕で超えていたからです。

Cluster ごと新規の VPC を構築することも選択肢としてあがりましたが、既存の VPC CIDR Block には十分余裕があったこと、もし不足した場合は VPC CIDR Block が拡張可能であることから、EKS 用に新規の subnet を作成することにしました。

余っている領域から、以下3つを切り出しました。

cidr first last total memo
10.10.0.0/18 10.10.0.0 10.10.63.255 16384 used
10.10.64.0/18 10.10.64.0 10.10.127.255 16384 new
10.10.128.0/18 10.10.128.0 10.10.191.255 16384 new
10.10.192.0/18 10.10.192.0 10.10.255.255 16384 new

これによって、合計 49152 個の Pod 数を許容することができます。これは現状の Pod 数の数十倍以上であることから、少なくともしばらくは枯渇しないと判断しました。

kubeconfig の配布方法

Kubernetes の認証は、aws-iam-authenticator を用いていました。各 IAM ユーザは属するグループごとに、KubernetesCluster Role に対応した Policy を Assume することで認証を行っています。

クラスタが増減するたびに kubeconfig を生成し、S3 に配置します。そして、kubectl を Install する script を実行すると kubeconfig も update される仕組みを構築していました。このタイミングで Slack 上に以下のように通知されていました。

kubeconfig が更新されたときに Slack 上に通知される

しかし、今回はこの方法を踏襲せず、aws clieks update-kubeconfig を使うことにしました。理由はよりシンプルになるからです。

さて、Quipper ではクラスタごとに3種類の Role が存在しています。コンテキストと合わせて表示すると以下のようになります。

Context ClusterRole role
cluster-name-admin quipper-admin KubernetesAdminProduction
cluster-name-app-admin quipper:app-admin*5 KubernetesAppAdminProduction
cluster-name-viewer quipper-viewer KubernetesViewerProduction

というわけで、このようなことを実現する kubeconfig を生成するために、以下のようなコマンドを叩いてもらえばいいと思ったのですが、

aws eks update-kubeconfig --name CLUSTER_NAME --role-arn ROLE_ARN --alias ALIAS

update-kubeconfig は、kubeconfig 内の "cluster" を1つしか持たない点が問題でした。つまり同一クラスタに複数回実行したとしても、1番最後のものだけが残ってしまいます。

考えた結果、各 IAM Group に対して、使用する Context を静的に決めてしまうことにしました。

これまでは一律にクラスタに対して上記 3 Role の kubeconfig context が追加されていましたが、例えば Developer は admin を使えませんし、Production Cluster の場合は Viewer のみです。もちろん、利用用途に合わせて、更新操作をしないときは危険防止のため Viewer を使う、というのが適切ではありますが、そのような使い方をしているひとは多数派ではなさそうでした。そのため、よりシンプルにできるこの方法を選択しました。

とはいえ、毎回 Cluster の名前は変わってしまいますし、Assume する RoleArn や Alias 名など、クラスタが増減するたびにいちいち考えてられないので、update-kubeconfig のラッパーツールを Go で書きました。仕様としては

  • 使用する IAM User の所属 Group から Assume する Role を取得する
  • 現在 Ready のクラスタに対して eks update-kubeconfig を実行する

というものになっています。クラスタが Ready かどうかは EKS の Tag に "quipper/ready" という項目を用意して判断しています。また、クラスタ固有のシリアル番号なしの、最新のクラスタを示す Context のために、"quipper/latest" という Tag も同様に用意しています。

余談ですが、Quipper SRE Team では 100行を超える Programming を行う場合は Go を Standard としています。これは Kubernetes や Terraform など、SRE が密接に関わる OSS は Go 製のものが多いため、コードを読んだり、upstream にパッチを投げたり、Plugin を書いたりする際に Go が書けることは有益だからです。また、現在の SRE Team には Go が得意なメンバーが多いこともその理由の1つです。今回、自分自身も(簡単なツールではありますが)仕事で Go を書ける機会を持てて良かったと思います。

Managed Node Groups の制限事項

今回、あまり深く考えずに Managed Node Groups を採用しました。メリットとしては Node Group の AMI のメンテナンスをしなくていいことがあげられます。しかし、現状かなり制約が多く、導入に非常に苦労しました。

任意の Security Group を付与できない

前提として、Quipper では VPC 内の EC2 Instance や RDS に "Default" という特別な Security Group (以下 SG)を持たせており、Default SG は Default SG からの通信を許可します。

これまでも Worker Node には Default SG を持たせることで他のインスタンスや RDS と通信可能でしたが、Managred Node Group ではそれができません。

これについては、EKS Cluster 作成時に生成される Cluster SG を、Default SG への通信に許可させるようにしました。

[EKS] [request]: EKS Managed Nodes should allow for custom security groups · Issue #609 · aws/containers-roadmap · GitHub

Taint をサポートしていない

これまでは Taint を用いて Node Group ごとの Schedule を制御していました。特に指定がないものは Default の Node Group に Schedule されますが、Taint がサポートされないことから、すべての Deployment に明示的に Node Affinity を指定する必要がありました。

ほぼ全てのサービスに Node Affinity をつける YAML たんぽぽ職人の仕事はなかなかにしんどかったです。

[EKS] [request]: Managed Node Groups support for node taints · Issue #864 · aws/containers-roadmap · GitHub

Managed Node Groups の Tag が ASG および Instance に伝播しない

Quipper では監視に Datadog を使っています。AWS のリソースのタグは基本的にそのまま Datadog でも活用できます。しかし、このタグが伝播しないことで、これまでできていた Monitor がそのまま使えなかったり、Dashboard がそのまま利用できないという問題がありました。

これに関しては @d-kuro が Datadog Agent 側でこれまで使っていたタグを付与することで回避しました。ありがとうございます。

[EKS] [request]: Nodegroup should support tagging ASGs · Issue #608 · aws/containers-roadmap · GitHub [EKS]: EKS Cluster Tagging Propagation · Issue #374 · aws/containers-roadmap · GitHub

Tag に kubernetes.io がついたものを付与できない

Quipper では Cloud Logging (旧 Stackdriver Logging) に Container の Log を送るために、fluentd-gcp を使っています。

ダウンロードした YAML を apply すると、beta.kubernetes.io/fluentd-ds-ready=true が Node Selector となっています。しかし、kubernetes.io は予約されており、Managed Node Groups に付与しようとすると validation で弾かれてしまいます。kubectl label コマンドで付与することはできるものの、Node は常に増減するため、Node Group 側で付与されて欲しいです。

基本的にこれらは DaemonSet で動くため、Apply する前にこの Node Selector を取り除く対応を行いました。

同様に、kubectl get node で表示される Role も node-role.kubernetes.io タグからくるので、Role が表示されません。不便。。。

[EKS] [request]: tag Kubernetes managed nodes with node-role.kubernetes.io/<node-group-name>=true, reopen #733 issue · Issue #854 · aws/containers-roadmap · GitHub

Scale to 0 できない

minimum size が 1 のため、不要なときに完全に減らしてしまったり、Managed Node Groups 自体の入れ替えも、単なる縮退だけでは行えず、Drain をするか、Managed Node Groups そのものを削除してしまわないとできません。

[EKS] [request]: Managed Nodes scale to 0 · Issue #724 · aws/containers-roadmap · GitHub

Launch Template をサポートしてない

以前までのクラスタでは、Kernel Parameter に変更を加えていました。幸い本番移行後はそれらに関する問題は発生していませんが、今後発生しないとも限らないので、自由度があったほうが望ましいと思います。

[EKS] Managed Node Groups Launch Template Support · Issue #585 · aws/containers-roadmap · GitHub

[EKS] [request]: Managed Node Groups Custom Userdata support · Issue #596 · aws/containers-roadmap · GitHub

Instance Class 変更時 Rolling Update されない

これもできればされてほしいものです。

[EKS] [request]: Rolling update to change instance type for Managed Nodes · Issue #746 · aws/containers-roadmap · GitHub


これらの制限事項を事前に想定できていなかったため、移行検証中にかなりてこずりました。終わってしまった今からすると、特に Managed Node Group をやめたいモチベーションはないのですが、SpotInstance を活用したい、という要求が高まった場合はやめる可能性もあります。

[EKS] [request]: Spot instances for managed node groups · Issue #583 · aws/containers-roadmap · GitHub

今でもどちらを選ぶべきだったかは悩みますが、少なくとももう少しだけ時間をかけて2つのオプションを比較して相談したり、containers-roadmap を覗いておくべきだったなと反省しています。

移行方法

今回、大きく以下の流れで検証を行いました。

  • Self-Hosted Cluster から EKS Cluster の移行(Staging)
  • EKS Cluster の移行検証(Staging)
  • Self-Hosted Cluster から EKS Cluster の移行(Production)

このプロセスを経ることで、Developer に kubeconfig の update を試してもらうとともに、移行時の問題点を洗い出しました。

また、各クラスタの切り替えに関しては、移行検証を行うことで手順が確立しました。

  1. Create a new cluster with Terraform
  2. Deploy Cluster-Level Kubernetes Resources
  3. Backup and Restore application pods with velero
  4. Update deploy definition of monorepo
  5. Switch DNS record for service-router(branch-router)*6
  6. Remove the cluster definition at a kubernetes-clusters repository
  7. Destroy an old cluster with Terraform

3つ目の Backup and Restore application pods with velero ですが、velero を用いて Application Resource のバックアップとリストアを行っています。この方法は以前から @d-kuro が導入していた方法です。今回、引継ぎもかねて、velero の Install をコード化するとともに、手順を Script 化することでより容易に行うことができました。

実際に切り替える際は、アクセスの少ない時間帯に、DNS による切り替えを行いました。この方法は以前と同様です。もしエラーレートが急上昇したときはすぐに Revert をすることで旧クラスタに戻すことができます。

このように、十分な移行期間を持って事前に問題を潰しきるとともに、本番での切り替え時もすぐに戻せる準備をして挑み、無事切り替えが完了しました。

Platform の Production Readiness

さて、今回無事に Platform の移行を終わらせることができました。これまでも、過去の経験からなんとなく、Platform の移行を安全に行う方法を考えて実行してきましたが、いい機会なので、Platform の Production Readiness についてそれを言語化してみようと思います。

もともと、新規サービス(Application)に関しては、Production Release の前に Design Doc や Production Readiness Checklist というプロセスがあり、「本番対応」の方法は固まってきています。今回もそちらを参照にしながら考えてみました。

quipper.hatenablog.com

Service Level

サービスが動く Platform にはそれ自体にも Service Level の定義が必要です。

今回、移行検証途中で Deploy するための CI の Workflow がかなりの確率で失敗する事象が発生し、急遽 Platform SLO を設定しました。以下が SLI です。

  • 99% の Job Success Rate: 差分検知の Job *7
  • 99% の Job Success Rate: Deploy の Job

いずれも、CI から Kubernetes Cluster の API を実行するものであり、かつ、どの Workflow でも必ず通る Job であることから、これらを SLI として選択しました。

最近出たばかりの Datadog の SLO Error Budget Alert を活用し、これらが下回ったときには原因調査*8と改善を行うようにしています。

待望の Datadog の Error Budget Alert(Beta)早速使い倒しています。

このような指標があることで、Platform 上で開発する Developer も「何か最近よく失敗するな 🤔」で終わることなく、Fact Based に SRE と協力して指標の改善に向かうことができます。

また、大前提として、Platform 上で動くサービスの SLO が設定され、きちんと運用されていることが大切です。*9Platform の影響でサービスの SLO が違反した場合にすぐ気付けることは、Platform の Migration を行う際に大きな安心感を与えてくれました。

Monitoring / Logging

これらは Platform においても重要です。SLI/SLO として設定する項目はもちろん、何を計測すべきで、何を Dashboard に表示すべきなのか。必要なログは永続化ストレージに送られていつでも見れるようになっているのか。Kubernetes 上で動く Application のように画一な表現は難しいかもしれませんが、どんな Platform でもこの観点は必要なはずです。

Migration

実際に Platform を移管する場合は、Platform というだけあって、その影響範囲は広く、リスクも高いことが多いでしょう。そして、どれほど準備を重ねても、想定外のことが起きるものです。その場合、どういったことを検討すればいいでしょうか。

Staging で十分な時間運用する

今回、EKS に移行してなんだかんだ1ヶ月間ぐらいは Staging 環境で運用しました。Quipper では週に1度 Weekly Release のための Regression Test が行われています。新しい Platform 上でこれらを何度も通していることは非常に安心できます。また、運用面でも、ある程度の期間 Developer に使ってもらうことで、問題の見落としを避けることができます。

問題があったときすぐに切り戻すことができる

これが1番大事かもしれません。

そのためには大前提ですが、インフラの変更がコードで管理されていて、Revert PR をすぐに出して Apply できる必要があります。

新旧の同一性を保証する

何らかの手段で、新旧 Platform の同一性を保証できるとより自信を持ってリリースができると思います。

この答えの大半は設定のコード化で解決できるとは思いますが、予期しない点で差分が生じることがあるかもしれません。

可能であれば現状の設定を、実際の Platform から dump して diff が取れるのが理想的だと思います。

今回は Cluster Level の Resource に関しては CI で同一のものをApply した上で、kubectl で取得したリソースが同じ数あるかどうかを簡単に確認しました。

また、Application に関しては前述したように velero で backup/restore を行うことで同一性を保証しました。

今後の課題

最後に、Platform に関する今後の課題について述べます。

System Component の GitOps 化

Cluster Level で適用する System Component(i.e. Datadog, RBAC, Ingress, Fluentd, ClusterAutoscaler etc.)は、1つのリポジトリ内で、shellscript と kustomize で差分を展開したのち apply しています。

しかし、この Script も十分に大きくなってきており、メンテナンスが難しい状態になってきています。また、System Component の Version Up に関しても、気づいたときに気づいたひとがやる、という状況になっています。

現在、@d-kuro が Application に関して ArgoCD による GitOps 化を進めており、これを機会に System Component に関しても GitOps 化を進める予定です。

先月入社した @int128 が早速 Issue を書いてくれて、頼もしい限りです。

Multi-Cluster Support

基本的に Quipper ではクラスタの切り替えは Blue/Green Deployment 方式を採用しており、DNS で切り替えています。これにより問題があったときにすぐ切り戻せるようになっています。

なぜこのような方式を取っているかというと、前段に Internet Facfing の Reverse Proxy が受けたのち、ALB Ingress を通じてクラスタへ通信しているからです。

現状、以下の2点を課題に感じています。

  1. 本番切り替え時、Canary Release を行えない
  2. クラスタが利用可能な状態で、新クラスタの動作確認ができない

正確には、いずれも"できない"わけではないのですが、

  1. 現状だと Route53 Weighted Routing という選択肢もあります(未検証)。ですが、Proxy Layer で Percentage Base で Traffic Splitting ができたほうがよりコントローラブルだと思います。
  2. 例えば learn-exp.quipper.com というサブドメインの場合は新クラスタにアクセスさせたい、とした場合、Reverse Proxy の config を各 Virtual Host ごとに変更する必要があり、少し面倒です。

これらの課題を解決するためには、ALB Ingress を外し、Kubernetes 管理でない ALB から両クラスタへの Proxy をコントロールできるようにしたいと考えています。

早速 Issue を書きました。次回 v1.15 から v1.17 への Upgrade を行う際にはこの課題を解決したいと考えています。

謝辞

Kubernetes に関して非常に豊富な知識を持ち、これまでもクラスタの切り替えを何度も行った経験から、多数のアドバイスをくれた @d-kuro に心から感謝します。本番環境での切り替えにも立ち会ってくれてありがとうございました。当初このタスクは @d-kuro が行う想定ではありましたが、クラスタ切り替えの経験がない僕が行うことで、(時間はかかってしまったものの)チームとしては Knowledge Transfer が行えたとともに、彼は GitOps による CI/CD 分離*10に注力することができて結果的にはよかったと思います。

毎週の Meeting でアドバイスをくれた @yuya-takeyama, @d-kuro に心から感謝します。最近の SRE Team では人数が増えたことにより、プロジェクト制を取っており、リード1名とそれをサポートする1、2名の少人数で問題に立ち向かう体制を取っています。今回はこの体制が非常にうまくいった例だと思っています。

認証の方針や、EKS における懸念事項など相談に載っていただいた技術顧問の @mumoshu に心から感謝します。

kubeconfig の更新のためのラッパーツールのレビューをしてくれた Go Lover の @pankona@suzuki-shunsuke に心から感謝します。

日頃からいろんな面でサポートしてくれた SRE Team のみんなに心から感謝します。

Thanks @global-web-developers to give me a feedback for the migration.

個人活動ではありますが、sre.fm#2 にゲストとして参加してくれた Wantedly@koudaiiiサイボウズ@_a0i に心から感謝します。このとき Platform の Production Readiness を考えたことで今回の経験に活かすことができました。

おわりに

Managed Service である EKS に移行したことで、Control Plane の管理が不要になり、さらに Kubernetes Cluster の迅速な Upgrade が可能になりました。このことは Platform を今後より進化させていくための重要な第一歩になったと考えています。

今後も SRE Team はプロダクトチームの Productivity を爆速にしつつ、Reliability も担保できる最高の Platform を提供するために進化を続けます。

Quipper では世界の果てまで学びを届けたい仲間を募集しています。

*1:2017年に Microsoft に買収されました

*2:Staging では2回ぐらいあった

*3:そのため、EKS が Tokyo Region で利用可能になった後も、EKS への移行のモチベーションは高くなかった

*4:本記事執筆現在、サポートバージョンはv1.16.10

*5:app-admin は System Component 以外を触れるように一部制限した ClusterRole です

*6:Cluster に入ったトラフィックを受け、Service Routing を行う Nginx のことを service-router と呼んでいます。詳細はKubernetes導入で実現したい世界とその先にあるMicroservices を参照ください。

*7:実態の Deployment に対して変更差分があるかどうかを検知して、変更のあるものだけど Deploy するための仕組み。詳細はCI の修正をリリース前に本番と同じ条件下で検証出来る仕組みを構築した話をご覧ください。

*8:Script が失敗したときは CircleCI の Build URL などのメタデータを含んだ情報を Sentry に送るようにしており、失敗したジョブにすぐ飛べるようにしてます

*9:サービスの SLO に関しては以前書いたSRE NEXT 2020 で「SLO Review」というタイトルで登壇しました #srenext を参照ください。なお、このときより現在は運用方法も進化しています。これについては別途記事を書こうと思います。

*10:近いうちにアウトプットがあるはずです!たぶん。