インターン
今年の 8/21 ~ 9/1 の2週間の間、freeeのSRE DXチームにお邪魔させていただきました。
テーマは、Argo Rollouts によるカナリアリリースをスキップするための、Argo Workflow の実装です。
カナリアリリース
freeeのプロダクトは殆どコンテナ化されており、EKS上にデプロイされています。kubernetesのマニフェストは全てGitHub のモノレポで管理されており、GitOpsを実現するCDツールである ArgoCDによりマネージドされています。これに関しては、Akitoさんの [freee-SRE船-DX(Developer eXperience)でインターンしてみた]で触れられています。
また、アプリケーションのリリースでは、Argo Rolloutsによるカナリアリリースが採用されています。これは、アプリケーションのリリースを段階的に行う方法で、1段階目では新バージョンのリリースを全体の数割に制限し、時間をおいて問題が発生していない場合、全体にリリースを適応させる方法です。1段階目の時点でバグが発覚した場合は、即座にロールバックされ被害を最小限に抑えることができます。
さて、このカナリアリリースですが、カナリアリリースをスキップしたいという時が存在します。例えば緊急の修正(Hotfix)等であり、このような場合は即座に全体にリリースする(修正パッチを適応する)ことが求められます。
この時の対処法は二つあり、一つはArgoCDのUI上で一つずつぽちぽち操作する方法です。ただし2,3個ならまだしも、数十単位であるRolloutをUI上で操作するというのは、現実的ではありません。二つ目は、kibela(社内ドキュメント)上にある記事を参考に、踏み台サーバーを経由してシェルスクリプトを実行することです。こちらの方法では、確かに一つずつ実行する手間は省けますが、やはり手作業が発生するためオペレーションミスが怖いといった問題や、そもそも一部の権限が不足していて実行ができないといった問題がありました。この関係の問題で月に2,3回程度DXチームに問い合わせがあり、開発者体験(DX)的にも良くないよね、と議題に上がっていました。
そこで、一連の操作を一つのArgo Workflowにまとめて、Workflow のDashboard上のボタン一つで実行できるようにしたいというのが、今回のタスクの目的です。
実装
検証環境(Sandbox環境)での実装
Sandbox環境に検証用クラスタを立てて、Argo Workflowを作成します。
まず最初に、Terraform のEKS moduleを用いてクラスタを作成します。Argo Rollout、Argo Workflowは公式のhelmチャートが存在するため、それを利用します。次に、検証用にカナリアリリースのサンプルを用意します。これは、幸いなことに [Argo Rollouts Demo Application | Github] が提供してくれています。ただし、サンプルはKustomizeで書かれているため、(HelmはKustomizeに対応しているとはいえ)Helmチャートに書き直した方がスタイルが統一されて望ましいでしょう。
Workflowもkubernetes Manifestで記述します。コンテナイメージを指定して、実行するコマンドを記述するという点では、Github Actionsが近いですが、順次実行するためには- -
と書く必要があったりと、細かな違いがあります。この辺りは、makocchiさんの [使いこなせ!Argo Workflows / How to use Argo Workflows] が参考になります。
Workflow本体は、alpineイメージにkubectl をインストールして、
kubectl argo rollouts promote ROLLOUT_NAME --full
を実行することによりカナリアリリースのスキップができます。
Workflowの権限管理
kubernetesの権限管理で理解しておくべき概念は3つあります。それは Role,ServiceAccount,RoleBindingです。それぞれ説明していきます。
Role
Roleは、「何に(Resource)、どうする(verb) ことを許可する」といった形式で記述します。Namespace単位のものと、クラスター単位のものがあります。AWSの IAM Policyにあたるものです。
ServiceAccount(SA)
ユーザー以外の、kubernetsのリソースが使用するためのアカウントです。AWSのIAM Userや IAM Roleに当たります。
RoleBinding
RoleBindingは、User/Group/SAにRole(権限)を紐づけるAPIです。これもNamespace単位のものと、クラスター単位のものがあります。AWSだと、attach-role-policy が近い概念でしょうか。
これを踏まえると、今回の課題では
kubectl argo rollouts promote ROLLOUT_NAME --full
を実行するための最小限のRoleを探し、Workflow用に作成したSAにRoleBindingでバインドしてあげることになります。最小限のRoleを探すのは、多少の試行錯誤が必要ですが、エラーの度に、kubectlが不足している権限を教えてくれるため、そこまで難しくありません。
最終的に以下の図のようになります。
これで、同一クラスタ内で Argo RolloutsによるカナリアリリースをスキップするWorkflowを実装できました。ここで前半の1週間が終わりました。
マルチクラスタ間権限管理
次に、マルチクラスタ間の権限管理を考えます。
これが必要な理由は、freeeのクラスタ管理の方法が関係しています。freeeはそれぞれのプロダクト別のクラスタと、全てのプロダクトのクラスタを管理するクラスタが存在します。
権限管理上、管理クラスタからWorkflowを実行できるようにするのが望ましく、そのためには管理クラスタを出て、AWSのVPCを経由して、各プロダクトのクラスタに再度入る必要があります。そのために、クラスタ内とクラスタ外の権限を紐づける必要があります。そこで、検証用クラスタも二つに分けて、引き続き実装を進めます。
aws-auth と IRSA
EKSにおいてクラスタ内の権限とクラスタ外の権限を紐づける方法として、aws-authとIRSAが存在します。aws-authはクラスタ外の権限、つまりAWSの権限(IAM UserやIAM Role)をクラスタ内の権限(GroupやUser,SA)に紐づける機能です。IRSAは逆に、クラスタ内の権限をAWSの権限と紐づける機能です。
ここで、クラスタ内とクラスタ外の権限を紐づける方法が、なぜ内から外と、外から内で分かれているのかという疑問を抱きます。その質問に対し、幾つかの回答が考えられますが、簡潔に言うと用法が全く異なるからです。
aws-authはkubernetesにアクセスする「すべてのユーザー」が利用する機能です。つまり、kubernetesにアクセスするAWSのリソースの認証情報だけでなく、メンテナンスを行うSREチームの認証情報もここに書かれており、普遍的に使われます。そして、認証はAWSが行うため複雑な設定は不要です。
一方IRSAは、PodからS3にアクセスするといった、kubernetes内のリソースがAWSのリソースにアクセスする一部の事例に限定されます。またkubernetesは、デフォルトで独自の認証基盤を持たないため、aws-authと比べて複雑な認証フローを取ります。
IRSAは割と最近登場した機能で、それまではワーカーノードのノードグループに割り振られるIAM Roleを使用したり、kiamをはじめとするOSSを利用していました。この辺りの流れは、[ Kubernetes サービスアカウントに対するきめ細やかな IAM ロール割り当ての紹介 | AWS Japan] を参考にしてください。
実装としては、以下の図のようになります
手順としては以下の通りです
- IRSAで紐づけるためのIAM Role(argo-wf)とSA(argo-wf-role)を作成します
- Terraform のEKS module のパラメーター
enable_irsa
をTrueに設定します。これにより、EKS用のOIDCプロバイダーが自動で作成されます - OIDCプロバイダーを用いた信頼Policyを作成し、作成したIAM Roleに紐付けます
eks:DescribeCluster
を許可するリソースPolicyを作成し、作成したIAM Roleに紐付けます
(これは、ワーカーノードグループがeks:DescribeCluster
のポリシーをデフォルトで保持するため、必須ではありませんが、明記した方が丁寧かなと感じています)- aws-authのconfig mapにIAM RoleとSA(rollout-control)の対応関係を追記します
これで、管理クラスタ上のWorkflowから、アプリケーションクラスタ上のRolloutにアクセスするための権限管理が終了しました。
Workflow内でのkubeconfigの設定
kubectlが操作対象のクラスタをどこに記述しているかというと、それはkubeconfigに記述しています。kubectlが複数の操作対象クラスタを持っている場合は、
kubectl config use-context [context]
で切り替えることが出来ます。
マルチクラスタにする前は、kubectlがデフォルトで自身のクラスタを操作対象のクラスタとしていたため問題ありませんでしたが、マルチクラスタの場合は話が別になります。アプリケーションクラスタをkubectlに操作対象クラスタとして指定してあげる必要があります。
ローカルで実行する場合は実は簡単で、これはawscliの
aws eks update-kubeconfig --region [region-code] --name [my-cluster]
コマンド一つで設定できます。
ただし、Workflow上で実行する場合は、コンテナにawscliをダウンロードしてあげる必要があります。しかしalpineイメージには、awscliの動作要件であるglibcが含まれておらず、また単純にダウンロードしただけではmusl libcと干渉してしまいます。(参照:[Alpine ベースのコンテナイメージで AWS CLI v2 を使う | 理系学生日記 ] )
そのため、Workflowで用いるベースイメージを、Alpineから公式に対応されているAmazon Linuxに変更します。これでマルチクラスタ間における、カナリアリリースをスキップするWorkflowの実装ができました。
Integration環境への導入
これまではSandbox環境で検証してきたわけですが、Integration環境に実際に導入してみて動作確認を行います。
freeeでは、Production /Staging /Integration 環境の TerraformとHelmfileは、それぞれGithub上のモノレポで管理されています。ゆくゆくはProduction環境に導入することを目指すわけですが、その前にPSIRT(プロダクトセキュリティ対応チーム)による確認が必要だったりと、インターンの2週間では時間的に厳しいため、ひとまずIntegration環境に導入することとします。
今まで学んだことを活かして、既存の環境にWorkflowを導入します。
ArgoCDによるGitOpsで管理されているため、全てのクラスタを管理するリポジトリにPRを立てます。
導入対象のクラスタには、Argo WorkflowやRolloutはすでに導入されているため、WorkflowのHelmチャート内に、作成したWorkflowを追加します。新たなServiceAccountを作成し、IRSAで紐づけられたIAM RoleをAnotationに記述します。
PRを作成した後は、メンターさんにレビューをしてもらいます。freee内にはargo-cd-diff-kun というGoで書かれた社内ツールがあり、PRを立てると、自動でPRのhelmfileと今立っているkubernetesリソースの差分をチェックしてくれます。これにより、ヒューマンエラーや不正なPRを防ぐことが出来ます。Approveをもらい、Mergeされると無事動きました!
プロダクトのリリース時には、Github ActionsによりECRのイメージタグの書き換えのPRが自動で作成されるのを確認できます。このような、実際の運用の流れも学ぶことが出来たのは非常に勉強になりました。
さいごに
今回のインターンでkubernetesに初めて触れました。kubernetesは以前から興味があったのですが、個人開発やサークルの開発では大量のコンテナを管理する機会が少なかったので、freeeでの事例を現場で見ながら学べたのは、非常に貴重でした。
また、メンターさんとDXチームの皆さんにお世話になりました。メンターさんがArgo FamilyのContributerで非常に刺激を受けたほか、最終日に [kubernetesのおすすめの本] を教えてもらったため、早速Amazonで購入し読み始めています!
freee 最高!!!!!
[完]