以下の記事で紹介した小ネタの、具体的な利用例です。
1. 対策の内容
リロード攻撃(F5アタック)への対策としては、前段のApacheにmod_dosdetectorやmod_evasiveを入れる、WAFを導入する等があります。
このように、前段ですべて対応できれば良いのですが、
- リロード攻撃を行っているのが正規ユーザである
- リクエストによって(また、ユーザ毎に処理に必要なデータ量の多寡によって)レスポンスタイムにばらつきがある
というような場合、なかなか前段だけでの対処は難しいのではないかと思います。
そこで、Webアプリケーションサーバ(Tomcatなど)の処理が詰まってしまったときに、サービスの完全停止を避けるためにWebアプリケーションサーバの再起動を行うことがあると思いますが、
- 「処理が詰まった」といっても、詰まり始めのうちは「特定の遅い処理」だけが「詰まる」のであり、それ以外のリクエストに対する処理は正常にレスポンスを返すことができている
- 正常なレスポンスを返すことができるリクエストまで、再起動で中断するのは(なるべく)避けたい
ということで、Apacheのgraceful restartのような処理をしよう…というのが今回の内容です。
※きちんとgracefulな処理をするためには、Webアプリケーションサーバは複数台必要です。
2. ポリシー・IAM Roleの準備
まずは、先ほどの記事にある通り、以下の作業を行います。
- ポリシーを設定する
- 設定したポリシーをEC2用IAM Roleにアタッチする
- そのIAM RoleをEC2(Webアプリケーションサーバ)にアタッチする
なお、今回のケースでは、「ec2:DescribeInstances」に対する権限は不要ですので、この部分はカットしても良いでしょう。
3. EC2上の設定
以下の記事を参考に、EC2(Webアプリケーションサーバ)にlogmonを導入します。
- CloudWatchとlogmonでEC2 Linuxのログを監視する(mooapp/moomindaniさん)
logmon.confには、以下の内容を設定します。
- 1行目 : 監視対象のログファイル(Apacheのエラーログなら「:/var/log/httpd/error_log」など)
- 2行目 : 監視対象のキーワード(正規表現/Tomcatのレスポンスが返らない場合を拾うのなら「[error] (70007)The timeout specified has expired」にマッチする内容)
- 3行目 : 先の記事に示されているとおり
続いて、最初の記事で紹介したスクリプト「aws_utils.sh」を配置し(私の例では「/usr/local/sbin/」内)、「get_instance_id()」の部分だけ以下の内容に置き換えます。
#######################################
# 自身のインスタンス ID を取得する
# Returns:
# INSTANCE ID
#######################################
get_my_instance_id() {
instance_id=`/usr/bin/curl http://169.254.169.254/latest/meta-data/instance-id`
if [ -z "$instance_id" ]; then
echo 'host not found'
exit 2
fi
echo ${instance_id}
}
※draining(登録解除の遅延)時間の長さに合わせて「SLEEP」の秒数も調整します。
それから、crontabから一定間隔で呼び出すスクリプト(私の例では「/usr/local/sbin/check_count.sh」)を配置します。
#! /bin/sh
# スクリプトをインポートする
. /usr/local/sbin/aws_utils.sh
# トリガ判定
if [ `cat /tmp/logmon_count` -ge 20 ]; then
# 閾値越え -> logmonサービス停止
/sbin/service logmon stop
# 二重トリガ起動防止(countを0に)
echo 0 > /tmp/logmon_count
# ALBでターゲットグループから外す
ALB_TARGET_GROUP_ARN=('arn:aws:elasticloadbalancing:ap-northeast-1:XXXXXXXXXXXX:targetgroup/YYYY/zzzzzzzzzzzzzzzz' 'arn:aws:elasticloadbalancing:ap-northeast-1:XXXXXXXXXXXX:targetgroup/YYYY/zzzzzzzzzzzzzzzz')
INSTANCE_ID=$(get_my_instance_id)
for arn in ${ALB_TARGET_GROUP_ARN[@]}
do
alb_deregister ${arn} ${INSTANCE_ID}
done
# ALBでdraining完了待ち
for arn in ${ALB_TARGET_GROUP_ARN[@]}
do
alb_waiter ${arn} ${INSTANCE_ID} 'unused' > /dev/null
done
# Webサービス停止
/sbin/service tomcat8 stop
/bin/sleep 10
# サービス起動
/sbin/service tomcat8 start
/bin/sleep 10
/sbin/service logmon start
# ALBでターゲットグループに戻す
for arn in ${ALB_TARGET_GROUP_ARN[@]}
do
alb_register ${arn} ${INSTANCE_ID}
done
# ターゲットグループに戻ったことを確認する
for arn in ${ALB_TARGET_GROUP_ARN[@]}
do
alb_waiter ${arn} ${INSTANCE_ID} 'healthy' > /dev/null
done
fi
# countを0に
echo 0 > /tmp/logmon_count
if文の「20」は閾値です。適切な値に調整してください。
この例ではWebアプリケーションサーバとしてTomcat8を使っていますが、適切なものに置き換えてください。Tomcatの場合は、draining前後でPrintClassHistogramの出力などもしておくと良いです。
また、この例ではEC2(Webアプリケーションサーバ)を複数のターゲットグループ(配列「ALB_TARGET_GROUP_ARN」)に登録しています。
それぞれのターゲットグループでdraining時間が違う場合は、時間が短いものを先に記述すると良いです(後述の通りログを記録する場合は特に)。
1つの場合は配列にせず、for文で回す必要もありません。
なお、この例ではログを /dev/null に捨てていますが、実際に使うときにはきちんとログファイルに記録しておいたほうが良いです(時刻などとあわせて)。
最後に、このスクリプトを、実行ユーザ(rootなど)のcrontabに登録します(私の例では1分間隔で実行⇒閾値は1分当たりのカウントに対して設定)。このとき、1行目に「SHELL=/bin/bash」を挿入しておきます。
SHELL=/bin/bash
*/1 * * * * /bin/sh /usr/local/sbin/check_count.sh
設定できたら、カウントファイル(私の例では「/tmp/logmon_count」)に閾値以上の値を書き出して、正しくdraining→ターゲットから削除→Webアプリケーションサーバ再起動→ターゲットに登録が行われるか、確認します。
echo 20 > /tmp/logmon_count
4. 注意点
drainingすると再起動には時間が掛かるので、Webアプリケーションサーバは最低でも4台程度は必要です。