w_tl00’s blog

インプットをまとめておく

Akatsuki Summer Internship 2020に参加して

お久しぶりです。@wt-l00です。半年ぶりの投稿となってしまいました...

2020年8月に株式会社アカツキさんのサーバサイドチームでインターンをさせていただきました。 本記事では、3週間のインターンシップ期間中に取り組んだこと、感じたことなどを書いています。

本ブログの目次は以下の通りです。

インターンシップで取り組んだテーマとその背景

インターンシップで取り組んだテーマは「ECSのスケーリング周りの改善」というものでした。

このテーマの背景について説明します。私が所属していたチームでは、アプリケーションを動かす環境としてAmazon Elastic Container Service (ECS)を使用しています。データプレーンとしてEC2を使用しており、ECS on EC2の環境でアプリケーションを動かしています。また、本環境では、アプリケーションで使うポート番号の関係上、EC2インスタンス1つに1ECSタスクしか乗らないという制約があります。この環境においてアプリケーションのスケーリング時に、スケーリングの対象となるものとして、ECSサービスのタスクとEC2インスタンスが挙げられます。

所属していたチームではスケーリングに対して、EC2側のインスタンスをスケーリングさせてからECS側のタスクをスケーリングさせるというアプローチを取っていました。 以下が現在のスケールアウトの仕組みです。

f:id:w_tl00:20200826174232p:plain
現在のスケールアウトの仕組み

このアプローチについて説明します。

  1. Auto Scaling GroupによってCPUの使用率などをトリガーとして、EC2インスタンスをスケールアウト・スケールインさせます。
  2. 次にAuto Scaling Groupでインスタンスが増減したイベントをCloudWatch Eventsでフックし、ECSサービス側のタスク数を増減させるためのLambdaが起動されます。
  3. このLambdaがAuto Scaling Groupのインスタンス数に合わせて、ECSサービス側の必要なタスク数を調整する処理を行います。
  4. 最後に、ECSサービスはタスク数が必要なタスク数になるようにタスク数の調整を行います。

これらの仕組みによって、スケーリングを行っていました。

上記のアプローチには課題があります。特にLambda内部で行う処理がかなり複雑であることが挙げられます。スケールアウト時には、増えたEC2インスタンスがECSクラスタに登録されるのを待ってから、ECSサービスの必要タスク数を調整する処理が必要です。また、スケールイン時にはECSクラスタに対して当該EC2インスタンスをDRAINするよう状態を変更させ、タスクが終了していたらEC2インスタンスを終了、していなければタスクが終了するのを待ってEC2インスタンスを終了させ、ECSサービスの必要タスク数を減らす...とかなり複雑な処理が必要です。

少し前置きが長くなってしまいましたが、「このLambdaの使用をやめて、マネージドなサービスを使ってECS on EC2の環境でスケーリングができないか」というのが本テーマの背景です。

本テーマに対するアプローチ

本テーマに対するアプローチとして、「Capacity Provider」と「ECS Service Auto Scaling」を使用しています。本テーマの前任者の方が、こちらの2つのサービスを使用したアプローチが現在のスケーリングの仕組みと比べて十分使えるかであったり、これら2つのサービスの設定内容の検証部分を行ってくれています。

Capacity ProviderとはAuto Scaling Groupをラップし、ECSサービス側の必要なタスク数に応じて、Auto Scaling Groupのインスタンス数を増減させることができるサービスです*1。では、ECSサービスの必要なタスクはどう増減させるのかといった問題が生じますが、これはECS Service Auto Scalingという仕組みによって対処することができます*2。ECS Service Auto Scalingは、ECSサービスにおけるCPU負荷などから、ECSサービスの必要タスク数を増減させることができます。

本アプローチについて説明します。

  1. ECS Service Auto ScalingによってCPUの使用率などをトリガーとして、ECSサービスの必要タスク数を増減させます。
  2. Capacity ProviderがECSサービスの必要タスク数に合わせて、EC2インスタンス数を増減させます。

また、下記のスケーリング手法の比較という図で、現在のスケーリングの仕組みと本アプローチを比較しています。

f:id:w_tl00:20200824212023p:plain
スケーリング手法の比較

抑えていただきたい点としては以下の3つです。

  • 前述の複雑なLambdaの使用が必要なくなる Capacity Providerを使用することで、ECSサービスのECSサービス側の必要なタスク数に応じてAuto Scaling Groupのインスタンス数を増減させることができるため、前述の複雑なLambdaの使用が必要なくなります。

  • スケーリングの判断を下すサービスが異なる

Auto Scaling GroupからEC2 Service Auto Scalingにスケーリングの判断を下すサービスが変化しています。CPU負荷などの観点からスケーリングを行う点については現在のスケーリングの仕組みと同様です。

  • スケーリングの対象を増減させる順番が逆

「EC2インスタンス→ECSタスク」から「ECSタスク→EC2インスタンス」へとスケーリングをさせる順番が逆に変化しています。コンテナを動かすための土台を先に増やすか、その上で動くコンテナ自体を先に増やすかという感じでしょうか。

インターンシップにおける課題

前任者の方によって残された課題を解消するのが、私の課題でした。 「Capacity Provider」、「ECS Service Auto Scaling」を導入するにあたっての課題や、これらの導入による変更が色々生じたため、以下の課題がありました。 以下については、一通り実装・動作テストを行うことができました。

  1. 設定の自動化
  2. 時間指定でスケーリング
  3. Capacity Providerの導入に伴うデプロイの仕組みの変更

設定の自動化

「Capacity Provider」、「ECS Service Auto Scaling」を導入するにあたって、やはり設定は自動化しておきたいということでこの課題が設定されました。所属していたチームでは、AWSの構成をCloudFormationで記述しています。私もこれらの設定をCloudFormationで記述しようと取り掛かりました。

Capacity Providerの設定の自動化

CloudFormationを使用した場合、要件を満たす設定ができませんでした。この理由について説明します。Capacity Providerには、マネージド型ターミネーション保護というoptionがあります。Capacity Providerの設定画面において、以下の説明があります。

Amazon ECS は Auto Scaling グループに属していてタスクを含む Amazon EC2 インスタンスがスケールインアクション中に終了するのを防ぎます。マネージド型ターミネーション保護は、Auto Scaling グループでスケールインからのインスタンス保護が有効になっている場合にのみ有効にできます。

必要な機能なのでもちろん有効にしたいです。Auto Scaling Groupのインスタンス保護を設定すれば、Capacity Providerのマネージド型ターミネーション保護オプションを有効にできることがわかりますね。しかし、CloudFormation側でAuto Scaling Groupのインスタンス保護が有効にできないということがわかりました*3

以上から、CloudFormationでは要件に合う設定ができないことが分かったため、AWS CLIを使用したスクリプトを作成することとしました。 スクリプトで以下の内容を行っています。

  • Auto Scaling Groupのインスタンス保護を有効にする
  • Capacity Providerを作成する(マネージド型ターミネーション保護を有効にするなど)
  • Capacity ProviderをECSクラスタに紐付ける設定をする

ECS Service Auto Scalingの設定の自動化

こちらは要件を満たす形で、CloudFormationで設定を自動化することができました。自動化した設定内容としては、ECS Service Auto Scalingで使用するCPU負荷などでスケールするポリシーを作成し、これをECSサービスにアタッチするというものです。この内容をCloudFormationで自動的に設定できるようにしています。

f:id:w_tl00:20200825171246p:plain
設定内容

時間指定でスケーリング

負荷が予め予想されるとき(特にイベント前ですね)にはサービスをスケーリングさせる必要があります。Auto Scaling Groupを使用して時間指定でスケーリングを行う仕組みはあるのですが、スケーリングのアプローチを変えるにあたり、こちらの仕組みにも変更を加える必要があります。

現在の仕組みから説明します。

  1. ある時刻を指定して、Auto Scaling GroupにおけるEC2インスタンスの最小数を引き上げます。
  2. インスタンスが増減したイベントをCloudWatch Eventsでフックし、ECSサービス側のタスク数を増減させるためのLambdaが起動されます。
  3. このLambdaがECSサービスのタスクを増加させる

という流れで、EC2インスタンスとECSサービスのタスクを時間指定でスケーリングさせていました。

前述のLambdaを撤廃するにあたって他の方法が必要になります。 そこで以下の2つの方法について検討を行いました。

  • ECSクラスタにおけるタスクのスケジューリング:要件に合わず不採用
  • Application Auto Scaling:採用

ECSクラスタにおけるタスクのスケジューリング

ECSクラスタにおけるタスクのスケジューリングの方は、時間指定でスケーリングを行うことができませんでした。この理由について説明します。 ECSクラスタにおけるタスクのスケジューリングでは、時間(cron式、at式、rate式)を指定してタスクをスケジューリングすることができます。この仕組みとしては、実行したいタスクの定義と数を設定し、指定した時間にCloudWatch Eventsが発火してタスクの実行を行うというものです。

こちらを実験して、挙動を確かめました。挙動としては、ECSクラスタにスケジューリングで指定したタスクを実行できるだけのリソース(今回であればEC2インスタンス)があればタスクを実行するが、なければ実行しないというものでした。Capacity Providerの設定も行っていましたが、指定した時間にCloudWatch Eventsが発火してもECSサービスにおける必要なタスク数が増えないため、Capacity Providerが反応してくれずEC2インスタンスが増えませんでした。時間指定でスケーリングをするにあたって文字通り「スケーリング」する必要がありますから、タスクを実行しようとするだけでは不十分です。

おそらく、ECSクラスタにおけるタスクのスケジューリングの用途としては、バッチジョブを想定しているのだと思います...。

Application Auto Scaling

Application Auto Scalingを使用すると、時間指定でスケーリングを行うことが可能となりました。 Application Auto Scalingでは時間(cron式、at式、rate式)を指定して、ECSサービスのタスクの最小数、最大数を設定することができます。また、Capacity Providerを使うと、タスクの最小数の増加に伴ってAuto Scaling Group側のEC2インスタンスを増加させることができます。

f:id:w_tl00:20200825171712p:plain
Application Auto Scalingを使用したスケーリング

Application Auto Scalingを使用すると、時間指定をしてECSサービスのタスクの最小数を引き上げることができました。しかし、Capacity Providerによって算出される必要なEC2インスタンスが1台ずつしか増加しないという問題がありました(最終的には必要な数に到達するが、時間を要する)。

こちらの原因は「Capacity ProviderがラップしているAuto Scaling GroupのAZ設定がシングルAZではない」というものでした*4。Capacity ProviderがラップしているAuto Scaling GroupのAZ設定がシングルAZではないと、Capacity Providerで算出されるEC2インスタンス数がminimumStepScalingSize(defaultは1)で増加します。おそらく、AZごとにminimumStepScalingSizeずつEC2インスタンス配置することで冗長性を確保してくれているのだと思います。

所属していたチームでは、Auto Scaling GroupをマルチAZ構成にしていたため、これをシングルAZ構成のAuto Scaling Groupを複数用意することとしました。以下の図のようなイメージです。

f:id:w_tl00:20200825181912p:plain
AZ構成の変更

シングルAZ構成のAuto Scaling Groupを複数用意し、それぞれのAuto Scaling Groupに対してCapacity Providerを設定してやると、必要なECSタスク数に対して必要なEC2インスタンス数が一気に算出されるようになりました。AZ間のEC2インスタンスの配置の割合は、Capacity Provider Strategyという機能を使えば自由に決めることができます。Capacity Provider Strategyを使って、それぞれのAuto Scaling Groupに1:1にインスタンスを振り分けている図を以下に示します。

f:id:w_tl00:20200826175303p:plain
Capacity Provider Strategyを使用してAuto Scaling Groupに1:1にインスタンスを割り振る図

シングルAZ構成のAuto Scaling Groupを複数用意するということをすると、他にも嬉しいことがあります。メンターの方によると「ApplicationをECS化する前はAZごとにAuto Scaling Groupや、Application Load Balancerを分けて、ユーザからの接続を自分のデータがあるUserDBのAZ側に誘導することをしていた。AZ間の通信のレイテンシを抑えて高速化するということをしていたことがある」「ECS化の際にデプロイがややこしいのでAZ誘導機能は廃止したが、Capacity Providerにややこしい処理を押し付けられるのであれば、この機能を復活させることができるかも」とのことです。

Capacity Providerの導入に伴うデプロイの仕組みの変更

Capacity Providerの導入によって、デプロイの仕方にも変更を加えました。

まず現在のデプロイの仕組みから説明します。本環境では、ローリングアップデートを行っています。ローリングアップデートとは、ECSのタスクを完全には停止せずに新しいタスクへ徐々に入れ替えを行う手法です。現在のデプロイの仕組みは以下です。

  1. 新しいタスク定義を準備して、これをつかうようにECSサービスを更新する。
  2. ECS最小ヘルス率からスケールさせるEC2インスタンス数を決定し、Auto Scaling Group側で必要なEC2インスタンス数を増やす。
  3. ECSサービスで実行されているタスクがすべて新しいタスク定義のものになるまで待つ。
  4. 増やしたEC2インスタンス数をもとの数にもどしてデプロイ完了。

2のECS最小ヘルス率はデプロイ時にRUNNING状態に保つServiceのTaskの下限数をTaskの必要数から割り出すためのパーセント値であり、ローリングアップデートの際にこれを満たす必要があります。例えば、ECS最小ヘルス率 = 100%で5タスクが必要であるという状況を考えます。この状況でデプロイを行なうとECS最小ヘルス率の制約から5タスクはRUNNING状態であることが求められます。また、この状況でローリングアップデートを行おうとすると新しいタスク定義のタスクを新たに立ち上げてから、古いタスク定義のタスクを終了させていく必要があります。 このため、新しいタスク定義のタスクを動かすためのEC2インスタンスが必要になります。EC2インスタンスを用意するために、2でAuto Scaling Group側で必要なEC2インスタンス数を増やすということを行っていました。増やしたインスタンス数はもとに戻さなければならないので、ECSサービスで実行されているタスクがすべて新しいタスク定義のものになるまで待ってから、この作業を行なっていました。

Capacity Providerによって、デプロイのために一時的にAuto Scaling GroupのEC2インスタンス数を操作する必要がなくなるという点が主な変更点となります。

新しいデプロイの仕組みについて見てみます。

  1. 新しいタスク定義を準備して、これをつかうようにECSサービスを更新する。
  2. Capacity Providerによってデプロイに必要になる(ECS最小、最大ヘルス率)タスク数分のEC2インスタンスが起動される。
  3. 古いタスク定義のタスクが動くEC2インスタンスをドレインし、新しいタスク定義のタスクにすべて変更されればデプロイ完了。

Capacity Providerのおかげで、2で一時的にデプロイのために必要なEC2インスタンスが起動されます。また、デプロイのために一時的に起動したインスタンス数が増えるのですが、Capacity Providerはこれを自動的にもとに戻すように、Auto Scaling Groupを操作します。このため、自前で増やしたEC2インスタンス数をもとの数に戻すことはしなくて済みます。

インターンシップを通じて感じたこと

やりたいことができる

基本的にやりたいことは何でもやっていいという雰囲気を感じました。所属していたチームではいい意味でタスクの幅が広く、やりたいことを各々が進めている印象を受けました。チームの人でカバーしている領域が大きいので、どんなことをやっても高いレベルの手厚いサポートが受けられると思います。かと言って、ゲスト的に扱うというわけではなく、対等な関係として接してくれるのが良かったです。

フルリモートでインターンってどう?

会社の現場の雰囲気を十分に感じることは難しいかもしれません。しかし、業務を行なう上でそれほど困ったところは特になく、開発に集中できました。働きやすい環境が整備されていて、体験としてとてもよかったです(ぜひ会社にもいってみたいです)。

基本的に会話はslackで、モブプログラミングやミーティングをするときなどにはzoomをつなぐという感じでした。また、チームの方やインターン同期の方、人事の方とは主にslackで交流したり、zoomでランチ会をしたりと少し心配だった交流も色々とできました。

自身を見つめ直す

リモートワークではチャットベースで会話をすることが多いため、言語化が何より大切であると感じました。言語化ができないと満足な議論すらできませんし、相手に何を手伝ってほしいのかを伝えることができません。 私はこの能力がまだまだ不十分で、訓練する必要があると感じました。 また、原因の切り分けをどうするかが作業のスピードを決めると痛感しました。原因をいくつかに分割して、検討をつけてデバッグしていくことが作業スピードを向上させる上で必要です。メンターの方はこのレベルが本当に高くて、こういう能力が自分にはまだまだ足りないと感じました。

おわりに

3週間という短い期間でしたが、インフラ周りのタスクに深く取り組むことができたように思います。やりたいことをやらせていただきましたし、技術的な部分も人との関わりの部分も充実していました。 自分に足りないものが浮き彫りになったので、これを鍛えていきたいと思っています。 最後になりましたが、メンターのお二人、受け入れてくださったチーム、人事の方々、インターンの同期の皆様、本当にありがとうございました。またぜひ成長した姿でお会いしたいです。