AWS Lambda

Lambdaを使ってEC2とRDSを自動停止させよう

はじめに

こんにちはディーネットのタナミです。

皆さんは検証環境など不要なEC2やRDSの停止は行っていますでしょうか?
私は使う時だけ起動させるようにしていますが、それでも毎回忘れずに停止させるのは厳しいところがあります。

正直EC2を止め忘れたところで1日に請求される料金はたかが知れてるんじゃない?と思う方もいらっしゃるかもしれません。
ですがそれは大きな落とし穴です。実際に一か月間EC2を付けっぱなしにした料金と毎日8時間だけ起動させた料金を比べてみましょう。

下図のようになんと16.46$もの差があります。
プライシングカリキュレーターは使用率を下げるとその分のEBSが削除された想定で計算するので、実際の金額差はもう少し縮まると思いますが
それでも毎月映画を1本観れるくらいの料金にはなるかと思います。

では毎月の映画の為に...ではなく月々の料金を抑える為にEC2やRDSが指定の時間に自動停止出来るように
LambdaでEC2とRDSの自動停止をセットアップしましょう。

IAMロール作成

まずはLambdaにアタッチするIAMロールの作成を行いましょう。
今回はEC2とRDSの停止を行うので下記のようにポリシーを設定します。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "EC2Permissions",
            "Effect": "Allow",
            "Action": [
                "ec2:DescribeInstances",
                "ec2:DescribeInstanceAttribute",
                "ec2:DescribeTags",
                "ec2:StopInstances"
            ],
            "Resource": "*"
        },
        {
            "Sid": "RDSPermissions",
            "Effect": "Allow",
            "Action": [
                "rds:DescribeDBInstances",
                "rds:DescribeDBClusters",
                "rds:ListTagsForResource",
                "rds:StopDBInstance",
                "rds:StopDBCluster"
            ],
            "Resource": "*"
        },
        {
            "Sid": "CloudWatchLogs",
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:*:*:*"
        }
    ]
}

Lambda作成

IAMロールの作成が出来たので次にLambdaを作成を行います。
ランタイムはPython3.13を実行ロールは既存のロールから先ほど作成したIAMロールを選択します。

また、設定タブからタイムアウトを30秒へ変更します。
これはタイムアウトによるLambdaの実行エラーを防ぐためのものになります。

Lambdaコード

次にLambda用のコードを設定します。
下記のコードではTag:autostopがアタッチされているEC2とRDSを停止する仕組みになっています。

import boto3
import json
from datetime import datetime

def lambda_handler(event, context):
    ec2 = boto3.client('ec2')
    rds = boto3.client('rds')

    stopped_resources = {
        'ec2_instances': [],
        'rds_instances': []
    }

    try:
        # EC2インスタンスの処理
        print("=== EC2インスタンスの処理開始 ===")
        ec2_response = ec2.describe_instances(
            Filters=[
                {'Name': 'tag-key', 'Values': ['autostop']},
                {'Name': 'instance-state-name', 'Values': ['running']}
            ]
        )

        ec2_instance_ids = []

        for reservation in ec2_response['Reservations']:
            for instance in reservation['Instances']:
                ec2_instance_ids.append(instance['InstanceId'])
                print(f"対象EC2インスタンス: {instance['InstanceId']}")

        if ec2_instance_ids:
            ec2_stop_response = ec2.stop_instances(InstanceIds=ec2_instance_ids)
            stopped_resources['ec2_instances'] = ec2_instance_ids
            print(f"停止したEC2インスタンス: {ec2_instance_ids}")
        else:
            print("停止対象のEC2インスタンスが見つかりませんでした")

        # RDSインスタンスの処理
        print("=== RDSインスタンスの処理開始 ===")
        rds_response = rds.describe_db_instances()

        rds_instance_ids = []

        for db_instance in rds_response['DBInstances']:
            db_instance_identifier = db_instance['DBInstanceIdentifier']
            db_instance_status = db_instance['DBInstanceStatus']

            if db_instance_status == 'available':
                try:
                    db_arn = db_instance['DBInstanceArn']
                    tags_response = rds.list_tags_for_resource(ResourceName=db_arn)

                    has_autostop_tag = any(tag['Key'] == 'autostop' for tag in tags_response['TagList'])

                    if has_autostop_tag:
                        rds_instance_ids.append(db_instance_identifier)
                        print(f"対象RDSインスタンス: {db_instance_identifier}")

                except Exception as tag_error:
                    print(f"RDSインスタンス {db_instance_identifier} のタグ取得でエラー: {str(tag_error)}")
                    continue

        # RDSインスタンスの停止
        if rds_instance_ids:
            for rds_id in rds_instance_ids:
                try:
                    print(f"RDSインスタンス {rds_id} を停止中...")

                    rds.stop_db_instance(
                        DBInstanceIdentifier=rds_id
                    )
                    stopped_resources['rds_instances'].append(rds_id)
                    print(f"停止したRDSインスタンス: {rds_id}")

                except Exception as rds_stop_error:
                    print(f"RDSインスタンス {rds_id} の停止でエラー: {str(rds_stop_error)}")
                    # エラーの詳細をログに出力
                    import traceback
                    print(traceback.format_exc())
                    continue
        else:
            print("停止対象のRDSインスタンスが見つかりませんでした")

        # 結果まとめ
        total_stopped = len(stopped_resources['ec2_instances']) + len(stopped_resources['rds_instances'])

        if total_stopped > 0:
            return {
                'statusCode': 200,
                'body': json.dumps({
                    'message': f'合計{total_stopped}個のリソースを停止しました',
                    'stopped_ec2_instances': stopped_resources['ec2_instances'],
                    'stopped_rds_instances': stopped_resources['rds_instances']
                }, ensure_ascii=False)
            }
        else:
            return {
                'statusCode': 200,
                'body': json.dumps({
                    'message': '停止対象のリソースはありませんでした'
                }, ensure_ascii=False)
            }

    except Exception as e:
        print(f"エラーが発生しました: {str(e)}")
        import traceback
        print(traceback.format_exc())
        return {
            'statusCode': 500,
            'body': json.dumps({'error': str(e)}, ensure_ascii=False)
        }

実行テスト

次にテストタブからLambdaが正常に動作するかテストします。
下図のような結果が帰ってくれば成功です。

EventBridge設定

無事Lambdaの動作を確認出来たので、最後に指定の時間にLambdaが自動的に動作するように設定します。
トリガーを追加から下図のようにEventBridgeを設定します。

また、cronはcron(分 時 日 月 曜日 年)のように表すことが出来ます。
EventBridgeはUTCで時間を設定するので、日本時間の19時に動かしたい場合は下記のように設定します。

cron(0 10 * * ? *)

おまけ

Lambdaの料金について疑問に思った方もいらっしゃるかもしれません。
ですが今回のLambdaは無料利用枠の範囲内に収まっているためLambdaの実行料金はかかりません。

今回のLambdaの1ヶ月の実行回数と実行時間は下記のようになります。

実行回数:毎日1回 × 30日 = 30回/月
実行時間:6秒 × 30日 = 180秒/月
GB秒   : 0.125GB × 180秒 = 22.5GB秒/月

下図の無料利用枠に収まる計算になります。

まとめ

今回は月々の料金を抑える為にLambdaでEC2とRDSの自動停止を設定しました。

ただし、今回の設定では自動停止のみとなっているので、起動は手動で行う必要があります。
より便利にしたい場合は、起動用のLambdaも設定すると良いかもしれません。

最後に注意点として、RDSの場合は停止期間が7日を超えると自動的に起動してしまうので
長期間使わない場合はスナップショットを取得してRDSを削除することも検討してください。

返信を残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA