ENECHANGE Developer Blog

ENECHANGE開発者ブログ

EC2 Auto Scaling のライフサイクルフックによる graceful shutdown の実現

ENECHANGE の CTO 室にてインフラエンジニアを務めている岩本です。AWS の各種サービスには地味ながらも便利な機能が多くあります。EC2 Auto Scaling のライフサイクルフックもそのひとつです。今回はこの機能のおかげで解決できた課題をご紹介します。

# 状況

ENECHANGE では自社で開発したツールを Google Chrome や Firefox での E2E テストに使っています。Google スプレッドシートでテストケースを定義できる仕組みのため、自動テストを手軽に始められる便利なツールです。

このツールはふたつの Elastic Beanstalk 環境で構成されています。ひとつは UI を提供するウェブサーバー環境、もうひとつはテストを実行するワーカー環境です。

ウェブサーバー環境とワーカー環境、いずれの環境も、EC2 インスタンスがオートスケールします。これは Auto Scaling グループのトリガーで実現しているものです。特にワーカー環境では大量のテストを並列実行するためにオートスケールが欠かせません。

ただ、このツールのワーカー環境には、テストを実行中のインスタンスがスケールインするとテストが途中で終わってしまう問題がありました。途中で終わったテストは頭からやり直さなければならず、手間も EC2 のコストも余分にかかる状況です。

# タスク

CTO の田中との 1on1 でこの問題の存在を知り、解決に向けて私が動くこととなりました。

解決策として思いついたのは、テスト実行中インスタンスのスケールインをテスト完了まで遅らせる方針でした。テストが途中で終わる問題を防ぐため、テスト完了までスケールインのタイミングを遅らせようというわけです。

この方針を言い換えると「EC2 インスタンスを graceful shutdown させる」となります。graceful shutdown は、実行中の処理が終わるまで停止を遅らせる、httpd でおなじみの手法です。これを EC2 インスタンスレベルで実現できれば、問題が解決します。

こうして、EC2 インスタンスレベルでの graceful shutdown の実現が私のタスクとなりました。

# 行動

## 実現手段の検討

実現に向けて、まずは EC2 Auto Scaling のライフサイクルフックを試すことにしました。ユースケースに「インスタンス終了前のログ退避」があることを知っていたので、応用できるだろうと踏んだのです。

スケールイン イベントが発生すると、ライフサイクルフックによってインスタンスが終了される前に一時停止され、Amazon EventBridge を使用して通知が送信されます。インスタンスが待機状態の間に、AWS Lambda関数を呼び出したり、インスタンスに接続して、インスタンスが完全に終了される前にログやその他のデータをダウンロードできます。


引用元:Amazon EC2 Auto Scaling のライフサイクルフック - Amazon EC2 Auto Scaling (日本語)

## 検証環境の用意

検証のため、さっそく下記の環境を用意しました。

### ライフサイクルフックの通知先を EventBridge にした理由

ここで、ライフサイクルフックの通知先を SNS や SQS ではなく EventBridge としたのは、それがベストプラクティスとされているためです。

ベストプラクティスとして、 EventBridge を使用することをお勧めします。Amazon SNS および Amazon SQS に送信される通知には、Amazon EC2 Auto Scaling が EventBridge に送信する通知と同じ情報が含まれています。EventBridge 以前は、SNS または SQS に通知を送信し、別のサービスを SNS または SQS に統合してプログラムによるアクションを実行するのがスタンダードな方法でした。今日、EventBridge では、対象となるサービスのオプションが増え、サーバーレス アーキテクチャを使用してイベントを処理しやすくなりました。


引用元:ライフサイクルフックの通知ターゲットの設定 - Amazon EC2 Auto Scaling (日本語)

### Nginx を graceful shutdown することにした理由

また、シェルスクリプトにおいて Nginx を graceful shutdown することにしたのは、Nginx へのリクエストでテストが実行されているためです。テストが終わるまで、つまり Nginx がリクエストを処理し終えるまで、スケールインを遅らせる必要があります。

Elastic Beanstalk のワーカー環境では、このプロセスを簡素化するために Amazon SQS キューを管理し、キューから読み取る各インスタンスでデーモンプロセスを自動的に実行します。デーモンは、キューから項目を取得すると、キューメッセージの内容を本文に含めて、HTTP POST リクエストをローカルにポート 80 の http://localhost/ に送信します。アプリケーションに必要なのは、POST に応じて実行時間が長いタスクを実行することだけです。


引用元:Elastic Beanstalk ワーカー環境 - AWS Elastic Beanstalk

### シェルスクリプトの内容

シェルスクリプトの内容は下記の通りです。

#!/bin/bash
set -eu

lifecycle_hook_name=$1
auto_scaling_group_name=$2
lifecycle_action_token=$3

# Nginx に QUIT (graceful shutdown) シグナルを送る
nginx -s quit

# Nginx プロセスが残っている場合
while [ $(pgrep -c nginx) -gt 0 ]
do
  # インスタンスの終了を遅らせる
  aws autoscaling record-lifecycle-action-heartbeat \
    --lifecycle-hook-name "${lifecycle_hook_name}" \
    --auto-scaling-group-name "${auto_scaling_group_name}" \
    --lifecycle-action-token "${lifecycle_action_token}" \
    --region ap-northeast-1
  sleep 5
done

# インスタンスを終了する
aws autoscaling complete-lifecycle-action \
  --lifecycle-hook-name "${lifecycle_hook_name}" \
  --auto-scaling-group-name "${auto_scaling_group_name}" \
  --lifecycle-action-token "${lifecycle_action_token}" \
  --lifecycle-action-result ABANDON \
  --region ap-northeast-1

## 挙動の確認

このような環境で EC2 Auto Scaling の TerminateInstanceInAutoScalingGroup API を呼び出し、テスト実行中インスタンスのスケールインをリクエストしたところ、意図通り、テスト完了後にインスタンスが終了しました。

続けて本番環境にも適用し、そちらでも意図通りの挙動となることを確認しました。

# 結果

以上の通り、ライフサイクルフックのおかげで、EC2 インスタンスレベルでの graceful shutdown が簡単に実現できました。

ただ、これで終わりではなく、ライフサイクルフックを活用できそうな環境が他にないか確認したいと考えています。ENECHANGE では 100 を超える Elastic Beanstalk 環境を運用しているため、活用できる環境が他にもあるかもしれないからです。

なお、今回のタスクを通じて、AWS に関する知識を広げる重要性にあらためて気づかされました。もしライフサイクルフックについて知らなければ、解決に時間を要したはずです。「AWS 認定 11 冠」を当面の目標として勉強を続けます。