clusterのserver実装小話 Custom Termination Policyは神

これは クラスター Advent Calendar 2021、9日目の記事です。

が，とくにネタがないので，ここ最近で一番神機能だと思ったAWS AutoScaling GroupのCustom Termination Policyについて書きます．

https://docs.aws.amazon.com/autoscaling/ec2/userguide/lambda-custom-termination-policy.html

なんでこれを推してるかを説明するのにいくつか前提があるのでまずはそれを説明します．

clusterではユーザーの皆様が自由にワールド／イベントを作成することができ，好きなタイミングでアプリを起動して参加(入室)することができます．
また，最近[^homelobby]ではホームやロビー機能により，自分でイベントを作る必要もなくなりました．お手軽ですね．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/18974/d20a9748-7ee4-b70b-7949-d73e8fe2e591.png)
**Enter Home** でホームへ行くとアバターも作れる[^avatarmaker]ので，手ぶらでも大丈夫です．お手軽ですね．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/18974/fa47303a-b372-12b2-eef8-c51b93e88909.png)

## アプリを起動して入室するときに起きること
clusterのアプリを起動して入室するとき，サーバー側では大雑把に下記のような処理がおこなわれています．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/18974/44bd0a8d-b812-4b45-b93d-20e22a3cce56.png)

(1)例えばイベントであれば，開催期間内であるか，有料イベントであればチケットを持っているか，などのチェックがあります．
(2)clusterのスマホアプリを起動していただいてホームタブを見ると分かりやすいのですが，パブリックなワールドに誰が入室しているのかが分かるようになっています．こういった情報をリアルタイムで出すためにセッション情報を作ります．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/18974/91dd06f8-c0d5-b099-726a-b3cb349eaa55.png)
このあたりの概要は [CUS-64：バーチャル SNS の裏側](https://resources.awscloud.com/aws-summit-online-japan-2020-on-demand-industry-3-12034/cus-64-aws-summit-online-2020-cluster-inc) とかでも話した気がするのでお時間があれば見てみてください．
(3)入室してる人が誰もいないならサーバーは不要なため割り当てられていないので，入室リクエストの処理のなかではじめてサーバーの選択／割り当てをおこない，利用するサーバーが確定されます．

以上の処理が成功すると，クライアントは自身の接続先サーバーの情報を受けとることができ，そこへ接続して晴れて入室完了となります．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/18974/92d9fe6e-1985-71f3-c771-b0898d10c3be.png)
ホームへ入室した図


## 割り当て可能サーバー群を適切に維持するための苦労
「サーバーを割り当てる」と書きましたが，もう少し詳しく言うと，入室処理が発生したときの割り当て用として管理されているサーバー群があり，その中から割り当て可能なものを選ぶ，というものです．割り当てられたサーバーは一定の条件のもと，他への割り当てには利用できなくなるのですが，この減った分のサーバーは必要に応じて補充されます．

割り当て可能なサーバーが足りなくなってしまうと入室に失敗する可能性が高まります．clusterはサービスの性質上，やはり入室**後**の体験がメインなので，そもそも入室できないという状態は極力避けたいところです．かといってサーバーを常に過剰に用意しておくのはコスト面や運用面で許容できない部分もあります．

clusterでは，割り当て可能なサーバー群をAutoScaling Group(ASG)内で起動するEC2数で調整していて，概ね問題はないのですが，1つ大きな課題がありました．

**スケールインがめちゃくちゃ面倒くさい**

たとえば，普段10個のEC2でサーバー群を構成しているとします．
ある日，大規模イベントが開催されることとなり，参加者が多く見込まれるのでEC2を100個にしました．

イベント終了後，EC2は普段の10個に戻したいですが，単純にスケールインさせることができません．
clusterのサービス的に止めてよいEC2(=割り当てがおこなわれていない未使用のEC2)と，ASGが止めようとするEC2が同じとは限らないため，入室処理の成功率低下や最悪の場合利用中のサーバーを止めてしまうこともありえるからです．

また，スケールイン中にアクセスが増えてやっぱりサーバーが必要，となる場合もあるため，あまり大量のEC2を一気に止めるのはリスクが高く，数個ずつ様子をみながら減らしていく，などの暖かみのある手動オペレーションをおこなっていました．

## 神機能 Custom Termination Policy
https://docs.aws.amazon.com/autoscaling/ec2/userguide/lambda-custom-termination-policy.html

一言でいうと，ASGがterminateするEC2を任意で決められる機能です．(Lambdaは自分で実装する必要があります)

ASGがEC2を止めようとするタイミング(スケールインに限らない)で，設定されたLambda関数を呼びだしてくれるので，そこで「clusterのサービス的に止めてよいEC2」を返すことができればそれを止めてくれます．
もし止めてよいEC2が無いなら何も返さないことも可能で，その場合はどのEC2もterminateされません．
このLambda関数呼び出しは，ASGが自身の設定を充足するまで続きます．

従来，安全にスケールインをおこなうためには

1. 止めたいEC2を割り当てには使うなとマークする
1. ASGから当該EC2をデタッチ
1. 当該EC2をterminate

のような手順を踏んでいく必要がありましたが，この機能によって

1. ASGの設定を変更する
1. スケールインが発生しそうになるとLambdaが呼ばれる
1. Lambdaの処理で，止めたいEC2を割り当てに使うなとマークし，そのinstance-idをASGに返す
1. ASGが止めてくれる

となって，実際のオペレーションは(1)のASGの設定をいじるだけになったのでした．

## 他のケースでも大活躍
いろいろな事情で，ASG内のEC2をすべて入れ替えたいケースが稀によくあります．
たとえばAMIを更新したとか，太陽が眩しいな〜とか．

そういうときのために? Instance Refreshという機能が以前からあったのですが，これも活用できるようになりました．

https://docs.aws.amazon.com/autoscaling/ec2/userguide/asg-instance-refresh.html

安全にEC2を入れ替えるためにはスケールイン時とだいたい同じ手順を踏む必要があり，サーバーが足りなくならないように様子を見ながら入れ替えたりしていたのですが，

1. ASGでInstance Refreshを開始する
1. EC2の入れ替えが発生しそうになるとLambdaが呼ばれる
1. Lambdaの処理で，入れ替えたいEC2を割り当てに使うなとマークし，そのinstance-idをASGに返す
1. ASGが止めてくれる
1. 新しいEC2が起動してくる
 
この通り，オペレーションは(1)だけになったのでした．

## おまけ
Custom Termination Policyを使うにあたり，一部不明な挙動をした点をAWSサポートに問い合わせたりしたのですが，めっちゃ迅速に対応していただいて無事導入することができました．ありがとうございます．

## まとめ
ついでにマネジメントコンソールのAutoScaling GroupのInstance ManagementからEC2を直接terminateできるようにならないですかね?

[^homelobby]: https://note.com/cluster_official/n/nddda05cec75a そんなに最近でもなかった
[^avatarmaker]: https://note.com/cluster_official/n/n2eb5600e04ff べんり