AWSのEC2を自動停止するLambdaを作る記録

実行権限を作成します。

  1. IAMのポリシーを作成する」を参考にポリシーを作成します。
    • [JSON]タブで設定するポリシーは以下になります。
  2. IAMのロールを作成する」を参考にロールを作成します。
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogStream",
                "ec2:DescribeInstances",
                "ec2:StopInstances",
                "logs:CreateLogGroup",
                "logs:PutLogEvents"
            ],
            "Resource": "*"
        }
    ]
}

自動停止の対象と停止時間を設定できるようにするためにEC2インスタンスにAutoStopタグを追加します。

自動停止の対象と停止時間を設定できるようにするためにRDSにAutoStopタグを追加します。」を参考にEC2インスタンスにAutoStopタグを追加します。

Lambda関数を作成します。

  1. AWSのコンソールにある[Lambda] > [関数の作成]ボタンで画面を開きます。
  2. 必要な項目を入力後に[関数の作成]ボタンで関数を作成します。
    • オプション : [一から作成]
    • 関数名 : 任意の関数名
    • ランタイム : Python3.8
    • 実行ロール : 既存のロールを使用する
      • 作成したロールを選択します。
  3. Lambda関数を実行するトリガーを作成します。」を参考にトリガーを作成します。

関数を実装します。

  1. [関数コード] > [lambda_function.py]に以下のコードを張り付けて[保存]ボタンで保存します。
    • 定数の[REGION_NAME]には停止対象のEC2インスタンスがあるリージョンを指定してください。
  2. 関数を動かしてみます。」を参考にエラーが無くなるまで関数を動かして修正を繰り返します。
# -*- coding: utf-8 -*-

from __future__ import print_function

import sys
import json

import boto3
from datetime import datetime, timedelta, timezone, date
import re

# 停止対象のインスタンスがあるリージョンを設定する
REGION_NAME = 'ap-northeast-1'
DATETIME_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
print("Loading function")

def get_instance_list(ec2):
    """
    起動中かつ[AutoStop]タグが設定されたインスタンスを取得する処理.

    Parameters
    ----------
    ec2
    """
    instances = ec2.describe_instances(
        Filters=[
            {"Name": "instance-state-name", "Values": ["running"]},
            {'Name': 'tag-key', 'Values': ['AutoStop']}
        ]
    )['Reservations']
    new_instances = []
    for instance in instances:
        new_instances.append(instance['Instances'][0])
    return new_instances

def get_auto_stop_tag(instance):
    """
    AutoStopタグに設定された時間文字列を取得する.
    """
    result = ''
    val = ''
    tag_list = instance['Tags']
    tag = next(iter(filter(lambda tag: tag['Key'] == 'AutoStop' and (tag['Value'] is not None and len(tag['Value']) != 0), tag_list)), None)

    if tag:
        val = tag["Value"]

    if not val:
        print('タグに時間が指定されていません。')
    elif not re.fullmatch('\d{1,2}:\d{1,2}', val):
        print('タグに設定されている文字列が時間(hh:mm)ではありません。' + val)
    else:
        print('タグに設定された時間は、' + val + 'です。')
        result = val

    return result

def get_utc_time(jst_time_string):
    """
    HH:MM形式のJST時間文字列をUTC時間にして取得する.
    """
    # 時間文字列を{時間, 分}の配列にする
    jst_time = jst_time_string.split(':');
    today = datetime.now()

    # 設定時刻が8:59以前である場合、UTC変換時に前日にならないよう日付を1日進めておく
    if int(jst_time[0]) < 9:
        today = today + timedelta(day=1)

    jst_time = datetime(today.year, today.month, today.day, int(jst_time[0]), int(jst_time[1]))
    utc_time = jst_time + timedelta(hours=-9)
    return utc_time

def is_stop(event_time, auto_stop_time):
    """
    インスタンスを停止するかを判定する
    """
    # 設定時間の5分前
    from_time = auto_stop_time + timedelta(minutes=-5)
    # 設定時間の5分後
    to_time = auto_stop_time + timedelta(minutes=5)

    print("イベント時間(" + event_time.strftime(DATETIME_FORMAT) + ")が、" + \
          from_time.strftime(DATETIME_FORMAT) + "から" + to_time.strftime(DATETIME_FORMAT) + "の間であればインスタンスを停止します。")
    # AutoStopタグに指定された時刻の前後5分以内であればインスタンス停止
    is_stop = from_time <= event_time and event_time <= to_time
    return is_stop

def lambda_handler(event, context):
    print("Received event: " + json.dumps(event, indent=2))
    ec2 = boto3.client('ec2', REGION_NAME)

    instances = get_instance_list(ec2)

    if len(instances) == 0:
        print('処理対象のインスタンスはありません。')
        return 0

    # インスタンス終了処理
    for instance in instances:
        print(instance['InstanceId'] + 'は起動中です。')
        # [AutoStop]タグに設定された時間文字列を取得する
        tag_time_string = get_auto_stop_tag(instance)

        if tag_time_string:
            # [AutoStop]タグに設定された時間文字列をUTC時間にする
            auto_stop_time = get_utc_time(tag_time_string)
            print('タグに設定された時間(UTC)は、' + auto_stop_time.strftime(DATETIME_FORMAT))

            # 引数のイベント時間を取得
            event_time = datetime.strptime(event["time"], DATETIME_FORMAT)

            if is_stop(event_time, auto_stop_time):
                ec2.stop_instances(InstanceIds=[instance['InstanceId']])
                print(instance['InstanceId'] + "を停止しました。")

    return 0
使用した関数のドキュメント

失敗したこと

An error occurred (UnauthorizedOperation) when calling the DescribeInstances operation: You are not authorized to perform this operation.

  • 原因 : describe_instances関数を実行する権限がなかった。
  • 対応 : ポリシーの[Action]に「ec2:DescribeInstances」を追加しました。
Response:
{
  "errorMessage": "An error occurred (UnauthorizedOperation) when calling the DescribeInstances operation: You are not authorized to perform this operation.",
  "errorType": "ClientError",
  "stackTrace": [
    "  File \"/var/task/lambda_function.py\", line 34, in lambda_handler\n    instance_list = get_instance_list()\n",
    "  File \"/var/task/lambda_function.py\", line 19, in get_instance_list\n    instances = ec2.describe_instances(\n",