インフラ担当の柴田です。
今回は、Amazon EC2 Auto Scaling のウォームプールを利用した時に発生した問題と、それをライフサイクルフックで解決したお話です。
Amazon EC2 Auto Scaling のウォームプール
ウォームプールは、事前にインスタンスをプールしておいて(作成しておいて)、スケールアウトが発生した時にプールからサービスインすることで、 インスタンスの初期化にかかる時間を削減できるサービスです。
ウォームプールでは、インスタンスを起動した状態でプールする方法と、インスタンスを停止した状態でプールする方法があります。 起動した状態でプールするとインスタンスの利用料金が発生するので、基本的にはインスタンスを停止した状態でプールして利用する方が多いと思います。 (課金されるなら、サービス提供した方が得なのでAuto Scalingグループのインスタンスを増やした方が良いと考えられる)
ウォームプールで発生した問題
今回問題が発生したお客様は、Windows Serverを利用してサービスを提供していました。
Windows ServerのインスタンスからAMIを作成する場合はsysprepを利用して、OS固有の状態を削除したり初期設定をしたりします。そのため、Windows Serverのインスタンスは初回起動に時間がかかります。今回のお客様の場合は、インスタンスの作成を開始してからサービスを提供できるようになるまで十数分かかっていました。
起動に時間がかかるのは初回インスタンスを作成する時だけですので、ウォームプールを利用することでサービス提供までの時間が短くなることを期待してウォームプールを導入したのですが問題が発生しました。
インスタンスがいつまで待っても正常に起動しないのです。
Windowsの起動に失敗している
インスタンスは起動しているものの、ステータスチェックに失敗している状態となっており、ALBのヘルスチェックにも失敗している状態でした。 状況を確認するために、問題のインスタンスのスクリーンショットを取得したところ図1が取得できました。
コンピューターが予期せず再起動されたか、予期しないエラーが発生しました。Windows のインストールを続行できません。Windowsをインストールするには、[OK]をクリックしてコンピューターを再起動してから、インストールを再実行して下さい。
どうやら、Windowsが正常に起動できていないようです。
原因についてはAWSのサポートにも確認をしますが、何とかしないといけません。 エラーメッセージから考えると、OSの構成中に再起動してしまったのが原因のようです。 再起動が原因だとすると、ウォームプールがインスタンスを停止している所が怪しいので、停止の処理を何とかしようと考えました。
(なお、念のための確認として起動状態でプールする方式を試したところ問題無くサービスインできましたので、やはりウォームプールの停止処理が怪しいとなりました。)
正常に起動したことを確認してから停止する
ウォームプールがインスタンスを停止するタイミングが早すぎるのが原因のようだと考えられたので、正常に起動したのを確認してから停止する方法を考えます。
AWSのドキュメントにも載っていますが、ここでライフサイクルフックを利用します。
ライフサイクルフックは、新しいインスタンスがウォームプールに、またはウォームプールから移行するタイミングを制御します。これらのフックは、ライフサイクルフックの最後にウォームプールに追加される前に、インスタンスがアプリケーション用に完全に設定されていることを確認するのに役立ちます。
https://docs.aws.amazon.com/ja_jp/autoscaling/ec2/userguide/ec2-auto-scaling-warm-pools.html
ライフサイクルフックで利用するLambda
ライフサイクルフックを利用すると、インスタンスを作成するタイミングで処理を挟むことができます。そこで、インスタンスの作成時にLambdaでインスタンスが正常に起動したのを確認してからインスタンスを停止するようにします。
対象のインスタンスは、ALBからのヘルスチェックを受け入れるようにHTTPのエンドポイントがありますので、ヘルスチェック用のエンドポイントにリクエストを投げてHTTPのステータスコード200
が返ってきたら正常に起動したと判断することにします。
以下が作成したLambdaのコードで、Python 3.9で動作を確認しています。参考にされる場合は、ログなどの出力部分削っていますので、pass
の部分等に適切にログの出力などを追加して下さい。
import boto3 import urllib.request import time def lambda_handler(event, context): status = None wait=10 # 10秒毎に確認する。 while status != 200: time.sleep(wait) ec2 = boto3.resource('ec2') instance = ec2.Instance(event['detail']['EC2InstanceId']) ip = instance.private_ip_address url = f'http://{ip}' req = urllib.request.Request(url) try: with urllib.request.urlopen(req) as res: status= res.status except urllib.error.HTTPError as err: pass except urllib.error.URLError as err: pass # 200が返ってきたらライフサイクルフックを終了する as_client = boto3.client('autoscaling') try: res = as_client.complete_lifecycle_action( LifecycleHookName=event['detail']['LifecycleHookName'], AutoScalingGroupName=event['detail']['AutoScalingGroupName'], LifecycleActionToken=event['detail']['LifecycleActionToken'], LifecycleActionResult='CONTINUE' ) except Exception as e: pass return { "message" : "finished" }
また、延々と待機してもしかたがないので、16分ぐらいたてば一応停止しても大丈夫だろうということで、ウォームプール側のライフサイクルフックの設定で、「ハートビートタイムアウト」を「960秒」と「デフォルト」を「CONTINUE」にしました。
まとめ
少し長くなってきましたので、ライフサイクルフックの設定については割愛をさせて頂いて、強引にまとめにはいります。
今回は、Amazon EC2 Auto ScalingのウォームプールとWindows Serverを組み合わせたところ、OSの起動に失敗したため、ライフサイクルフックで起動完了まで待機するようにしました。 ライフサイクルフックの王道的な使い方ではありますが、意外と今まで使ってこなかったので新鮮な気持ちで対応していました。
後日談ですが、お客様からAWSのサポートからもOSの起動途中にウォームプールによって停止させられているのが原因と考えられると回答を頂いたと共有いただきました。