📘

AWS Elastic Beanstalk(woker環境)のEC2をwoker処理後に自動で削除する

2023/11/24に公開

こんにちは。
株式会社DELTAでインフラエンジニアをしております浜崎です。

今回はAWS Elastic Beanstalk woker環境内のEC2インスタンスを、処理の終了と同時に自動削除できないかと考え、検証を行いました。

背景

Elastic Beanstalk woker環境を、任意のタイミングで一時的に複数の処理を実行する用途で利用しています。

処理を実行していない時間はリソースを使用していないので、日常的にAWSコンソールなどを目視でちょこちょこ確認し、処理が完了していれば手動でEC2を削除する運用をします。

ですが、業務時間外に処理が完了する場合や、確認忘れが発生すると、不要なEC2が稼働し続け、不要なコストが発生します。

そこで、処理の終了と同時に自動でEC2を削除できないかと考えました。

最初に結論

SQSがデフォルトでCloudWachに送信するApproximateAgeOfOldestMessageメトリクスをトリガーにLambda関数を実行し、Elastic Beanstalkで設定されるEC2インスタンス数を0に上書きすることで、自動削除を実現しました。

構成図は以下になります。

SQSのキュー内メッセージ数を示すApproximateNumberOfMessagesの監視はカスタムメトリクスの作成が必要のため、今回はSQSがデフォルトでCloudWachに送信するApproximateAgeOfOldestMessageを監視し、この値によってEC2を削除する構成を作成しました。

ApproximateAgeOfOldestMessageメトリクスは、キューで削除されていない最も古いメッセージの経過時間を示すものです。
このメトリクスが0になったことでキュー内のメッセージが0になったと判断し、Elastic Beanstalkで設定するインスタンス数を0にします。

今回のサービスでは必要な処理を一度にSQSにリクエストするため、「最も古いメッセージの経過時間が0になる=最も古いメッセージがキューから消えた」タイミングで処理が完了したと判断できます。

監視はCloudWatchAlarm、Elastic Beanstalkの設定変更はLambda、トリガーにEventBridgeを使用します。

検証環境構築

検証環境を構築します。

流れとしては、
Elastic Beanstalk環境 -> CloudWatchAlarm -> Lambda -> EventBridge -> wrk実行環境 の順番で作成しました。

Elastic Beanstalk環境は、AWS公式からサンプルコードをダウンロードし、検証用に一部関数を追加、AWSコンソールからElastic Beanstalk環境を作成します。

その後、自動作成されたSQSのApproximateAgeOfOldestMessageメトリクスを選択して、CloudWatchAlarmの作成。

次に、Elastic Beanstalkの設定を上書きするLambda関数を作成し、CloudWatchAlarmがアラーム状態に遷移した際にLambda関数を起動するようEventBridgeで設定しました。

最後に、構築したSQSに実際にメッセージを送信する環境を作成しました。
今回はwrkを使用するため、新規にEC2インスタンスを立ち上げ、wrkをインストールします。

検証ではpythonを使用します。

〇 AWS Elastic Beanstalk(Woker環境)構築

下記AWS公式サイトからpythons.zipをダウンロードします。
https://docs.aws.amazon.com/ja_jp/elasticbeanstalk/latest/dg/tutorials.html

ダウンロードした「application.py」ファイルにsleep関数を追加します。こちらは処理毎に一時停止期間を設けることで、SQSキューに一定時間メッセージを留めることがねらいです。

import timeを追加

application.py
import logging.handlers
#下記を追加
import time

POST処理後にtime.sleep(3)を追加

application.py
if method == 'POST':
        try:
            if path == '/':
                request_body_size = int(environ['CONTENT_LENGTH'])
                request_body = environ['wsgi.input'].read(request_body_size)
                logger.info("Received message: %s" % request_body)
		#下記を追加
                time.sleep(3)

AWSコンソールよりElastic Beanstalk環境を構築します。

AWSコンソールでElastic Beanstalkへ移動後、「環境を作成」を押下。
下記を入力後、「レビューまでスキップ」、レビュー画面を確認後に「送信」を押下します。

環境枠:ワーカー環境
アプリケーション名:任意のアプリケーション名
プラットフォーム:Python
環境名:任意の環境名 ※自動入力

プラットフォームブランチ:Python 3.11 running on 64bit Amazon Linux 2023 ※自動入力
プラットフォームのバージョン:4.0.6 (Recommended)

アプリケーションコード:コードをアップロード
バージョンラベル:1.0
プリセット:高可用性

EC2 キーペア:任意のキーペア

〇 CloudWatch Alarm作成

[CloudWatch]->[すべてのアラーム]画面から「アラームの作成」を押下。
「メトリクスの選択」からElastic Beanstalk環境構築で自動生成されたSQSのApproximateAgeOfOldestMessageメトリクスを選択します。

条件は下記の設定をします。

アクションの設定でデフォルトで通知設定が入力されているので、「削除」を押下。
任意のアラーム名を入力し、プレビュー画面で「アラームの作成」を押下します。

〇 Lambdaの作成

[Lambda]画面より「関数の作成」を押下。
「一から作成」を選択後、下記を入力選択し、「関数の作成」を押下します。

関数名:任意の関数名
ランタイム:Python 3.11
アーキテクチャ:x86_64

lambda_function.py
import json
import boto3

def lambda_handler(event, context):
    
    environment_name = '[Elastic Beanstalk環境名]'
    region = '[Elastic Beanstalkリージョン名(例:ap-northeast-1)]'
    
    # Elastic Beanstalkクライアントの作成
    eb_client = boto3.client('elasticbeanstalk', region_name=region)
    
    # Elastic Beanstalkの環境を更新してEC2の数を0に設定
    response = eb_client.update_environment(
        EnvironmentName=environment_name,
        OptionSettings=[
            {
                'Namespace': 'aws:autoscaling:asg',
                'OptionName': 'MinSize',
                'Value': '0'
            },
            {
                'Namespace': 'aws:autoscaling:asg',
                'OptionName': 'MaxSize',
                'Value': '0'
            }
        ]
    )

作成後、Lambda実行ロールにElastic Beanstalkの設定変更に必要なポリシーを追加します。
今回は一時的な検証作業のため特にセキュリティは考慮せず、「AdministratorAccess-AWSElasticBeanstalk」ポリシーを追加しました。

〇 EventBridgeの作成

[Amazon EventBridge]->[ルール]より、「ルールを作成」を押下します。

名前:任意の名前
ルールタイプ:イベントパターンを持つルール

イベントソース:AWS イベントまたは EventBridge パートナーイベント
イベントパターン

{
  "source": ["aws.cloudwatch"],
  "detail-type": ["CloudWatch Alarm State Change"],
  "resources": ["arn:aws:cloudwatch:ap-northeast-1:************:alarm:test-sqs-ApproximateAgeOfOldestMessage-alarm"],
  "detail": {
    "state": {
      "value": ["ALARM"]
    }
  }
}

ターゲットタイプ:AWS のサービス
ターゲットを選択:前項で作成したLambda関数を選択

[レビューと作成]画面で「ルールの作成」を押下。

〇 メッセージ送信環境構築

検証時のSQSへのメッセージの送信にはwrkを利用します。

※参考
https://dev.classmethod.jp/articles/send-message-batch-with-wrk/

今回、wrkの実行はEC2から実施するため、wrk実行環境となるEC2を構築します。
新規EC2起動後、以下のコマンドを実行します。(※OS imageはAmazonLinux2023を想定しています。)

sudo dnf install git
git clone https://github.com/wg/wrk.git
cd wrk
make
sudo cp wrk /usr/local/bin/
wrk --version
バージョンが出力されればインストール成功です

Luaスクリプトにテストリクエストを定義します。

vi wrk.lua
wrk.lua
wrk.method = "POST"
wrk.body = "Action=SendMessage&MessageBody=wrk-test"
wrk.headers["Content-Type"] = "application/x-www-form-urlencoded"

〇 SQSアクセスポリシー設定

最後にSQS側のアクセスポリシーです。
SendMessageの許可と、匿名リクエストを許可するため暗号化を無効にします。

[SQS]画面より対象キューを選択し、[アクセスポリシー]タブから「編集」を押下。暗号化「無効」の選択、下記のポリシーを記載します。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowSendMessage",
      "Effect": "Allow",
      "Principal": "*",
      "Action": "SQS:SendMessage",
      "Resource": "*"
    }
  ]
}

検証

構築作業が終わりましたので、実際に意図した挙動をするか検証します。

前項で構築したSQSに複数のメッセージを送信し、ApproximateAgeOfOldestMessageが0になった後、稼働中のEC2が削除されるかを確認します。

wrk実行環境のEC2に接続し、Luaスクリプトを作成したディレクトリに移動。
wrkを実行します。

sh-5.2$ wrk -c 3 -d 3 -t 3 -s wrk.lua https://sqs.ap-northeast-1.amazonaws.com/************/awseb-e-fgk4ktwkfm-stack-AWSEBWorkerQueue-nrTg5xAxzOWW

こんなレスポンスがあれば成功。

Running 3s test @ https://sqs.ap-northeast-1.amazonaws.com/************/awseb-e-fgk4ktwkfm-stack-AWSEBWorkerQueue-nrTg5xAxzOWW
  3 threads and 3 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     5.78ms    3.65ms  48.28ms   95.01%
    Req/Sec   184.34     23.66   222.00     70.00%
  1653 requests in 3.00s, 0.88MB read
Requests/sec:    550.29
Transfer/sec:    300.37KB
sh-5.2$

キューのメッセージ数を取得し、メッセージの滞留を確認します。

sh-5.2$ aws sqs get-queue-attributes --queue-url https://sqs.ap-northeast-1.amazonaws.com/************/awseb-e-fgk4ktwkfm-stack-AWSEBWorkerQueue-nrTg5xAxzOWW --attribute-names ApproximateNumberOfMessages
{
    "Attributes": {
        "ApproximateNumberOfMessages": "1426"
    }
}
sh-5.2$

AWSコンソールからも滞留しているメッセージ数と処理中のメッセージ数が確認できます。



数分後・・・

キューのメッセージの滞留とともに、ApproximateAgeOfOldestMessageメトリクスも上昇を始めました!

これからしばらくは、ApproximateAgeOfOldestMessageメトリクスが上昇を続けます。
一時的に送信したリクエストがキューに滞留しているので、その時間をメトリクスが指し示しているということですね。



しばらくして、キュー内のメッセージがなくなりました!

sh-5.2$ aws sqs get-queue-attributes --queue-url https://sqs.ap-northeast-1.amazonaws.com/************/awseb-e-fgk4ktwkfm-stack-AWSEBWorkerQueue-nrTg5xAxzOWW --attribute-names ApproximateNumberOfMessages
{
    "Attributes": {
        "ApproximateNumberOfMessages": "0"
    }
}
sh-5.2$

ApproximateAgeOfOldestMessageメトリクスも0になり、CloudWatchAlarmがアラーム状態に遷移しています。



Elastic Beanstalkの設定画面を確認すると・・・
Lambdaが実行され、Elastic Beanstalkのインスタンス数設定が0に上書きされています。

EC2インスタンスの状態がシャットダウンに変化しました。

これで意図した通り、処理の終了後、自動でEC2インスタンスが削除されることが確認できました!

まとめ

今回は、不要なEC2インスタンスの稼働時間を最小限にするため、自動削除を検討しました。

EC2はインスタンスタイプ、稼働台数によってはコストが膨大になるため、不要に稼働している時間がないか見直すと、大きなコスト削減につながることもあります。
運用中の環境のEC2インスタンス(その他、稼働時間により料金が発生するリソース)について、こちらの観点で是非コストの見直しを実施いただければと思います。

We're hiring!

DELTAではチームの一員になっていただける仲間を募集中です!
下記フォームよりお気軽にご連絡ください!

https://docs.google.com/forms/d/e/1FAIpQLSfQuWNU1il5lq2rVdICM0tSK_jTsjqwc52LYEwUxBq7_ImtrQ/viewform

DELTAテックブログ

Discussion