ec2にタグ判定で自動起動/停止を設定する

2024/03/07に公開

1. 目的

常時起動する必要のないEC2インスタンスを料金の削減のために自動で7:00に起動、22:00に停止するようなスケジューラを作成します。

次の要件を満たすことが条件です。

  • cron式を使って定時実行を設定します。
  • 指定のタグからインスタンスIDを抽出して実行します。
    • インスタンスIDの指定の場合は対象を変更する場合再設定が必要になるため、柔軟に対応可能なタグでの実装とします。

参考にさせていただいた記事

Amazon EventBridgeからEC2インスタンスを起動•停止•再起動する簡易な方法5選 | DevelopersIO

[AWS] Pythonのboto3でタグ付きEC2だけを停止させる

検討事項

  • インスタンスのタグを対象として実装するためにLambdaを使用します。
    • EC2の自動起動と停止にはAutomationドキュメント、API、Lambdaを使用するパターンがあります。
    • AutomationドキュメントやAPIを使用する場合はインスタンスタグではなくインスタンスIDを使用する必要があります。
  • 起動/停止日時を指定するためにEventBridge Rulesで実装します。
    • cron式を使用することができます。

2. 実現方法

実装案

今回はLambdaでコードを書いてEventBridge Rulesで定時実行する案を考えます。

必要なリソース

起動/停止用

  • Lambda実行用のIAM Role x1
    • 停止と起動は同じロールを参照します。
  • Lambda実行用のIAM policy x1
    • LambdaがEC2を停止/起動する権限

起動用

  • Lambda関数 x1
  • EventBridge Rules(cron式) x1

停止用

  • Lambda関数 x1
  • EventBridge Rules(cron式) x1

3. 実装

リソースの作成

IAM Roleの作成

Lambdaに必要な権限は下記の通りです。

  • EC2のリストとインスタンスのパラメータ参照権限

    • EC2インスタンスのタグを読み込み、インスタンスIDを取得するために使用します。
    • マネージドポリシーを使用します。
    AmazonEC2ReadOnlyAccess
    {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Action": "ec2:Describe*",
                "Resource": "*"
            },
            {
                "Effect": "Allow",
                "Action": "elasticloadbalancing:Describe*",
                "Resource": "*"
            },
            {
                "Effect": "Allow",
                "Action": [
                    "cloudwatch:ListMetrics",
                    "cloudwatch:GetMetricStatistics",
                    "cloudwatch:Describe*"
                ],
                "Resource": "*"
            },
            {
                "Effect": "Allow",
                "Action": "autoscaling:Describe*",
                "Resource": "*"
            }
        ]
    }
    
  • EC2の開始/停止権限

    EC2を開始/起動させるだけのマネージドポリシーがないため新規作成します。

    lambda-ec2-automation-iam-policy-common
    {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Sid": "VisualEditor0",
                "Effect": "Allow",
                "Action": [
                    "ec2:StartInstances",
                    "ec2:StopInstances"
                ],
                "Resource": "arn:aws:ec2:ap-northeast-1:{yourAccountId}:instance/*"
            }
        ]
    }
    

Lambda関数の作成

Lambda関数ではBoto3というPythonライブラリを使用することで、AWSのAPIコールを実行することができます。

今回書きたい処理は下記の通りです。

  • 起動中のEC2インスタンスから特定のタグが付いたリソースのIDを配列で取得
  • 配列に格納されたIDに起動/停止コマンドを実行

起動と停止はそれぞれ別のLambda関数を作成し、それぞれのスケジューラに紐づけることにします。

  • 関数の作成画面

コードは下記の通りで、boto3からインスタンスの記述と停止のAPIコールを呼び出ししています。

function-lambda-stop-ec2-common
import json
import boto3

def lambda_handler(event, context):
    stop_ec2()
def stop_ec2():
    client = boto3.client('ec2', region_name='ap-northeast-1')
    response = client.describe_instances(Filters=[
        {   # EC2が稼働中
            'Name': 'instance-state-name',
            'Values': ['running'],
        },
        {   # EC2のタグ「AutoStop」の値が「true」
            'Name': 'tag:AutoStop',
            'Values': ['true'],
        },
    ])
    instance_ids = []
    for instance_dic in response['Reservations']:
        instance_ids.append(instance_dic['Instances'][0]['InstanceId'])
    for instance in instance_ids:
        print('instanceid:' + (instance))
    response =  client.stop_instances(InstanceIds=instance_ids)
    print(response)

タイムアウトはデフォルトで3秒ですが、APIの実行時間を加味して10秒に変更しました。

ターゲットとなるインスタンスにタグをつけます。

実行結果

function-lambda-stop-ec2-commonをテスト実行すると、インスタンスが停止中のステータスとなりました。

test responce
Test Event Name
test

Response
null

Function Logs
START RequestId: fef43f2c-630f-4038-96f0-df4f7b13ff8c Version: $LATEST
instanceid:i-07xxxxxxxxxxxxxx
{'StoppingInstances': [{'CurrentState': {'Code': 64, 'Name': 'stopping'}, 'InstanceId': 'i-07xxxxxxxxxxxxxx', 'PreviousState': {'Code': 16, 'Name': 'running'}}], 'ResponseMetadata': {'RequestId': 'ffb55593-646b-491b-a0c1-59ea61eaa962', 'HTTPStatusCode': 200, 'HTTPHeaders': {'x-amzn-requestid': 'ffb55593-646b-491b-a0c1-59ea61eaa962', 'cache-control': 'no-cache, no-store', 'strict-transport-security': 'max-age=31536000; includeSubDomains', 'content-type': 'text/xml;charset=UTF-8', 'content-length': '579', 'date': 'Tue, 27 Feb 2024 01:43:50 GMT', 'server': 'AmazonEC2'}, 'RetryAttempts': 0}}
END RequestId: fef43f2c-630f-4038-96f0-df4f7b13ff8c
REPORT RequestId: fef43f2c-630f-4038-96f0-df4f7b13ff8c	Duration: 3506.24 ms	Billed Duration: 3507 ms	Memory Size: 128 MB	Max Memory Used: 89 MB	Init Duration: 363.37 ms

Request ID
fef43f2c-630f-4038-96f0-df4f7b13ff8c

停止用の関数でのテスト結果は問題ないようなので、同様にstartの関数も作成します。

function-lambda-start-ec2-common
import json
import boto3

def lambda_handler(event, context):
    start_ec2()
def start_ec2():
    client = boto3.client('ec2', region_name='ap-northeast-1')
    response = client.describe_instances(Filters=[
        {   # EC2が停止中
            'Name': 'instance-state-name',
            'Values': ['stopped'],
        },
        {   # EC2のタグ「AutoStart」の値が「true」
            'Name': 'tag:AutoStart',
            'Values': ['true'],
        },
    ])
    instance_ids = []
    for instance_dic in response['Reservations']:
        instance_ids.append(instance_dic['Instances'][0]['InstanceId'])
    for instance in instance_ids:
        print('instanceid:' + (instance))
    response =  client.start_instances(InstanceIds=instance_ids)
    print(response)

こちらもテストを実行すると、インスタンス起動が開始しました。

Lambda関数の作成はこれで完了です。

EventBridgeで定期的にLambda関数を呼び出す

ここまで作成した関数は手動で呼び出さなければ動作しないので、EventBridge Rulesで定時実行するように設定します。

startはcronで平日7時から起動するパターンをスケジュールします。

Lambda > 関数 > 設定タブ > トリガー > トリガーの追加を選択

cron式を入力するだけで、IAMロールを作成せずにEventBridge Rulesを作成することができます。

時刻の設定を失敗

cron式をEventBridge Rulesから確認してみると、ローカルタイムゾーンではなくUTCで設定されています。

Amazon EventBridge > ルール > schedule-eventbridge-start-ec2-common > ルールを編集 からルールを修正します。

UTCとのずれは(-9:00)なので設定したい時刻に9時間減算して22時に変更します。

Stopも同様に設定します。

4. まとめ

EC2インスタンスにつけられたタグごとに定時起動/停止を設定したい場合はLambda関数を作成してEventBridgeでcron式で起動する仕組みで実現することができます。

Discussion