1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

EC2インスタンスの自動起動停止を、より柔軟にしてみた(Lambda)

Last updated at Posted at 2023-01-07

はじめに

こんにちは、はやぴー(@HayaP)です。
皆さん、サーバーの節約対策していますか?

クラウドを使うのであれば、なるべくサーバーを
効率的に使いたいものです。

そこで、今回は
EC2インスタンスの自動起動を、より柔軟に実装した
経験を記したいと思います。

対象読者

  • サーバー代を節約したい。
  • サーバー毎に起動、停止時間を柔軟にしたい。

この記事が、クライドネイティブな運用の参考になれればと願っています。

概要

実現方法

AWS公式記事や、技術ブログを見ていると
複数のやり方がありますが、主に下記だと思います。

今回は、Lambdaを用いて
より柔軟なサーバーの自動起動停止を実現したいと思います。

要件

タイトルにもありますが、今回はより柔軟に自動起動停止を実現したいと思います。
具体的には、

  • サーバー毎に、1h単位で自動起動時間を設定したい
  • 平日と休日でも、起動パターンを変えたい
  • サーバー数が、今後増える想定のためスケーラブルに
    というのが要件です。

例:
サーバーA
起動:08:00
停止:18:00

サーバーB
起動:10:00
停止:24:00

サーバーC
起動:(平日のみ)10:00
停止:(平日のみ)18:00

問題

開発、運用面を考えると、AWS ソリューションライブラリーが
ベストだと思います。

しかし、要件にあるような柔軟さを実現しようとすると
複数の起動パターンを作る必要があり、最適とは言えません。

解決策

そのため、EC2タグと、Lambdaを用いた方法で
よりシンプルに実現する方法を考えました。

かなり雑ですが、構成は下記のとおりです。

  1. 毎時、自動起動対象インスタンスを検索するLambdaを起動
  2. 対象インスタンスを、SNS,SQSに通知
  3. 自動起動/停止を実行するLambdaを、非同期に呼び出す
    auto_shutdown.drawio.png

では、具体的な方法を記していきます。

詳細

リソース毎に説明をします。順序ではないので、注意してください。

EventBridge

auto_shutdown.drawio (1).png

スケジュールを作成します。
毎時起動したいため、
cron 0 * * * ? *
を指定します。

ターゲットは、一つ目のLambda(ターゲット検索)を指定します。

MicrosoftTeams-image (16).png

MicrosoftTeams-image (17).png

SNS

auto_shutdown.drawio (3).png

トピックを作成します。タイプはスタンダードにしています。
要件に応じて変更をしてください。
MicrosoftTeams-image (18).png

次に、サブスクリプションを作成します。
エンドポイントは、後続で作成するSQSキューを指定してください。
MicrosoftTeams-image (19).png

SQS

auto_shutdown.drawio (4).png

キューを作成します。
タイプは標準にしています。
要件に応じて変更をしてください。
MicrosoftTeams-image (20).png

次に、Lambdaトリガーを設定します。
後続で作成するLambdaを指定してください。
MicrosoftTeams-image (21).png

Lambda

auto_shutdown.drawio (2).png

最後に、
2つのLambdaを作成します。

1. ターゲット検索
2. 自動起動/停止実行

設定は、基本的にデフォルトでOKです。要件に応じて変更してください。
下記は、サンプルコードです。

1. ターゲット検索

getTarget.py
import boto3
import datetime
from datetime import datetime
from zoneinfo import ZoneInfo

REGION = "ap-northeast-1"
TIME_ZONE = "Asia/Tokyo"
TOPIC_ARN = "xxx"

AUTO_START_TAG_KEY = "AutoStart"
AUTO_STOP_TAG_KEY = "AutoStop"

ec2_boto3 = boto3.client('ec2', region_name=REGION)
sns_boto3 = boto3.client('sns')

def get_instance_info(instance_state, tag_key, tag_value):
    return ec2_boto3.describe_instances(Filters=[
        {
            'Name': 'instance-state-name',
            'Values': [instance_state],
        },
        {
            'Name': f'tag:{tag_key}',
            'Values': [tag_value],
        },
    ])


def create_instance_id_list(response):
    instance_id_list = []
    for instance_info in response['Reservations']:
        instance_id_list.append(instance_info["Instances"][0]["InstanceId"])
    return instance_id_list


def publish_sns(message, subject):
    sns_boto3.publish(
        TopicArn=TOPIC_ARN,
        Message=message,
        Subject=subject,
    )

def lambda_handler(event, context):
    try:
        now = datetime.now(ZoneInfo(TIME_ZONE))
        hour = now.strftime("%H")
        # 5->土曜日, 6->日曜日
        if now.weekday() != (5 or 6):
            start_target_list = get_instance_info(
                "stopped", AUTO_START_TAG_KEY, "wd" + hour)
            stop_target_list = get_instance_info(
                "running", AUTO_STOP_TAG_KEY, "wd" + hour)
            if start_target_list["Reservations"]:
                for instance_id in start_target_list["Reservations"][0]["Instances"]:
                    publish_sns(instance_id["InstanceId"], "start")
            if stop_target_list["Reservations"]:
                for instance_id in stop_target_list["Reservations"][0]["Instances"]:
                    publish_sns(instance_id["InstanceId"], "stop")
        start_target_list = get_instance_info(
            "stopped", AUTO_START_TAG_KEY, hour)
        stop_target_list = get_instance_info(
            "running", AUTO_STOP_TAG_KEY, hour)
        if start_target_list["Reservations"]:
            for instance_id in start_target_list["Reservations"][0]["Instances"]:
                publish_sns(instance_id["InstanceId"], "start")
        if stop_target_list["Reservations"]:
            for instance_id in stop_target_list["Reservations"][0]["Instances"]:
                publish_sns(instance_id["InstanceId"], "stop")
        return {
            'statusCode': 200
        }
    except Exception as e:
        print("errorMessage:", e)
        raise Exception
execute.py
import json
import boto3

ec2_boto3 = boto3.client('ec2')
ssm_boto3 = boto3.client('ssm')
lambda_boto3 = boto3.client('lambda')

def parse_event(event):
    instance_id = json.loads(event["Records"][0]["body"])["Message"]
    execution_type = json.loads(event["Records"][0]["body"])["Subject"]
    return instance_id, execution_type

def lambda_handler(event, context):
    try:
        # execution_type = "start" or "stop"
        instance_id, execution_type = parse_event(event)
        print("instance_id:",instance_id,"execution_type:",execution_type)
        if execution_type == "start":
            ec2_boto3.start_instances(InstanceIds=[instance_id])
        elif execution_type == "stop":
            ec2_boto3.stop_instances(InstanceIds=[instance_id])
        else:
            raise
        return {
            'statusCode': 200
        }
    except Exception as e:
        print("errorMessage:",e)
        raise Exception

まとめ

いかがだったでしょうか?

今回は、Lambdaを使用しましたが、
初めに紹介した方法に加え、StepFunctionsでも実装できると思います。

要件と、ケイパビリティによって技術選定をしましょう。
是非、活用してみてください!

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?