AWSのEC2インスタンスを祝日を除いた平日に自動起動するLambdaを作る記録
- EC2インスタンスを決まった時間に自動起動したいです。
- S3にバケットを作ります。
- 取得実リストを取得するLambda関数を作成します。
- EC2インスタンスを自動起動するLambda関数を作成します。
- 失敗したこと
EC2インスタンスを決まった時間に自動起動したいです。
起動しっぱなしでいいインスタンスですが、たまにうっかり誰かが停止しちゃったりします。
なので、始業時間になって停止していたら自動で起動してほしいです。
AutoStartタグに設定した時間になったら起動したいです。
起動したい時間は、時と共に変わるかもしれませんし、インスタンスによっても変わります。
なので、インスタンスに「AutoStart」というタグとその値に起動したい時間を設定します。
祝日は自動起動しないでほしいです。
平日(月~金曜日)にLambdaの実行を設定しても、祝日は意識してもらえません。
祝日は、使わないので節約のためにも起動しなくていいんです。
先人の知恵をパクッて使います。
S3にバケットを作ります。
祝日判定をするために使用するGoogleカレンダーの祝日リストを入れるためのバケットです。
祝日判定を行うにあたって、祝日のリストが必要になります。
今回はGoogleカレンダーで取得できる祝日リストを利用したいと思います。
下記URLから自由にダウンロードでき、現在から前後1年間を含む3年間分の祝日データが取得可能となっております。
https://www.google.com/calendar/ical/ja.japanese%23holiday%40group.v.calendar.google.com/public/basic.ics
Lambdaで祝日判定 | AWSやシステム・アプリ開発の最新情報|クロスパワーブログ
取得実リストを取得するLambda関数を作成します。
- AWSのコンソールにある[Lambda] > [関数の作成]ボタンで画面を開きます。
- 必要な項目を入力後に[関数の作成]ボタンで関数を作成します。
- オプション : [一から作成]
- 関数名 : get_google_holiday_list(任意の関数名でOK)
- ランタイム : Python3.8
- 実行ロール : 既存のロールを使用する
- 既存のロール : [lambda_basic_execution]を選択します。
- [関数の作成]ボタンで関数を作成します。
Lambda関数を実行するトリガーを作成します。
- [Designer]にある[トリガーを追加]ボタンでトリガーの設定画面を開きます。
- プルダウンから[CloudWatch Events]を選択します。
- 各入力欄を記載します。
- ルール : [新規ルールの作成]
- ルール名(必須) : get_google_holiday_list(任意の関数名でOK)
- ルールタイプ : [スケジュール式]
- スケジュール式 : cron(0 8 1 * ? *)(毎月1日の午前 8:00)
- [追加]ボタンでトリガーを追加する
関数を実装します。
import boto3 import urllib.request import re import os s3 = boto3.resource('s3') # Googleカレンダーの祝日リストを入れるためのバケット bucket = s3.Bucket('google-holiday-list') def write_holiday_list(list, file): for num in range(len(list)): pattern = r"DTSTART;VALUE=DATE:" # DTSTART;VALUE=DATE:yyyyMMddの行の正規表現 repattern = re.compile(pattern) target_line = list[num-1].decode('utf-8') match = repattern.search(target_line) if match is None: pass else: print('出力対象行:' + target_line) # 出力対象行(\r\n含む)の後ろから10文字目から8文字を出力する file.write(target_line[-10:-2] + '\n') return 0 def lambda_handler(event, context): # 祝日リストの取得元URL url = 'https://www.google.com/calendar/ical/ja.japanese%23holiday%40group.v.calendar.google.com/public/basic.ics' try: response = urllib.request.urlopen(url) list = response.readlines() except Exception as e: print('祝日リストの取得に失敗しました。') return 1 # 書き込みでファイルを開く f = open('/tmp/holiday.txt','w') write_holiday_list(list, f) f.close() data = open('/tmp/holiday.txt', 'rb') result = bucket.put_object(Key='holiday.txt', Body=data) data.close() os.remove('/tmp/holiday.txt')
使った関数の情報
- open : [Python入門]ファイル操作の基本 (1/3):Python入門 - @IT
- re : re --- 正規表現操作 — Python 3.8.2rc1 ドキュメント
- os : 【これでバッチリ!】Pythonのosモジュール使い方まとめ | 侍エンジニア塾ブログ(Samurai Blog) - プログラミング入門者向けサイト
- put_object() : S3 — Boto 3 Docs 1.12.0 documentation
- urllib.request : urllib.request --- URL を開くための拡張可能なライブラリ — Python 3.8.2rc1 ドキュメント
- スライスを使ってリストの指定した範囲の要素が含まれる新しいリストを取得する | Python入門
EC2インスタンスを自動起動するLambda関数を作成します。
- AWSのコンソールにある[Lambda] > [関数の作成]ボタンで画面を開きます。
- 必要な項目を入力後に[関数の作成]ボタンで関数を作成します。
- オプション : [一から作成]
- 関数名 : start_instances_by_tag_value(任意の関数名でOK)
- ランタイム : Python3.8
- 実行ロール : 既存のロールを使用する
- 既存のロール : [lambda_basic_execution]を選択します。
- [関数の作成]ボタンで関数を作成します。
Lambda関数を実行するトリガーを作成します。
- [Designer]にある[トリガーを追加]ボタンでトリガーの設定画面を開きます。
- プルダウンから[CloudWatch Events]を選択します。
- 各入力欄を記載します。
- ルール : [新規ルールの作成]
- ルール名(必須) : start_instances_by_tag_value(任意の関数名でOK)
- ルールタイプ : [スケジュール式]
- スケジュール式 : cron(0/10 8-11 ? * MON-FRI *) (平日AM8:00-11:00で10分毎)
- [追加]ボタンでトリガーを追加する
関数を実装します。
# -*- coding: utf-8 -*- from __future__ import print_function import sys import json from datetime import datetime, timedelta, timezone, date import boto3 import os DATETIME_FORMAT = '%Y-%m-%dT%H:%M:%SZ' JST = timezone(timedelta(hours=+9)) REGION_NAME = 'ap-northeast-1' TAG_NAME = 'AutoStart' TMP_HOLIDAY_FILE = '/tmp/holiday.txt' # CALENDAR_URL = "https://calendar.google.com/calendar/ical/ja.japanese%23holiday%40group.v.calendar.google.com/public/basic.ics" # NTP_URL = "http://ntp-b1.nict.go.jp/cgi-bin/json" print("Loading function") s3 = boto3.resource('s3') ec2 = boto3.client('ec2', REGION_NAME) def get_holiday_list(): """ S3バケットから祝日リストを取得する """ # Googleカレンダーの祝日リストを入れてあるバケット bucket = s3.Bucket('google-holiday-list') holiday_obj = bucket.Object('holiday.txt') with open(TMP_HOLIDAY_FILE, 'wb') as data: holiday_obj.download_fileobj(data) f = open(TMP_HOLIDAY_FILE) holiday_list = f.readlines() f.close() os.remove(TMP_HOLIDAY_FILE) return holiday_list def is_holiday(): """ 土日祝日判定処理 """ is_holiday = False day = date.today() today = day.strftime('%Y%m%d') week = day.weekday() holiday_list = get_holiday_list() check = today + '\n' in holiday_list if check == True: print('今日は祝日です。') is_holiday = True elif week == 5 or week == 6: print('今日は週末です。') is_holiday = True elif check == False: print('今日は平日です。') else: print('土日祝日判定に失敗しました') return is_holiday def get_tag(instance): """ TAG_NAMEのタグを配列で取得する. """ tag_list = instance['Tags'] tag = next(iter(filter(lambda tag: tag['Key'] == TAG_NAME and (tag['Value'] is not None and tag['Value'] != ''), tag_list)), None) return tag def get_tag_value_time(tag): """ タグに設定された時間を取得する. """ val = tag["Value"] if val == '': print('タグに時間が指定されていません。' + tag) return '' time = val.split(':') now = datetime.now(JST) tag_time = datetime(now.year, now.month, now.day, int(time[0]), int(time[1]), 0, 0, JST) return tag_time def is_start_instance(event, tag_time): utc_event_time = datetime.strptime(event["time"], DATETIME_FORMAT) print('実行時間(UTC)は、' + utc_event_time.strftime(DATETIME_FORMAT)) jst_event_time = utc_event_time.astimezone(JST) print('実行時間(JST)は、' + jst_event_time.strftime(DATETIME_FORMAT)) # AutoStopタグに指定された時刻の前後5分以内であればインスタンス起動する start_time_from = tag_time + timedelta(minutes=-5) start_time_to = tag_time + timedelta(minutes=5) print('実行時間(' + jst_event_time.strftime(DATETIME_FORMAT) + ')が、' + \ start_time_from.strftime(DATETIME_FORMAT) + 'から' + start_time_to.strftime(DATETIME_FORMAT) + 'だったら起動します。') return (start_time_from <= jst_event_time) and (jst_event_time <= start_time_to) def start_instance(event, instance): """ インスタンス起動処理 """ auto_start_tag = get_tag(instance) tag_time = get_tag_value_time(auto_start_tag) if tag_time == '': return print(instance['InstanceId'] + 'のタグに指定された時間(JST)は、' + tag_time.strftime(DATETIME_FORMAT)) if is_start_instance(event, tag_time): ec2.start_instances(InstanceIds=[instance['InstanceId']]) print(instance['InstanceId'] + 'を起動しました。') def lambda_handler(event, context): print("Received event: " + json.dumps(event, indent=2)) if is_holiday(): # 土日祝日なら処理終了 print('土日祝日は起動しません。') return # (停止している)かつ([AutoStart]タグのついている)インスタンス情報を取得する instances = ec2.describe_instances( Filters=[ {'Name': 'instance-state-name', 'Values': ['stopped']}, {'Name': 'tag-key', 'Values': [TAG_NAME]} ] )['Reservations'][0]['Instances'] if len(instances) > 0: # インスタンスを順番に処理していく for instance in instances: start_instance(event, instance)
使った関数の情報
- describe_instances : インスタンスの情報を辞書形式で取得する。
- download_fileobj() : Downloading Files — Boto 3 Docs 1.12.1 documentation
失敗したこと
'ec2.ServiceResource' object has no attribute 'describe_instances'
- 原因 : 使うオブジェクトを間違ってしまった。
- describe_instances()は、boto3.client('ec2')で取得したオブジェクトに含まれる関数なのに誤ってboto3.resource('ec2')で取得したオブジェクトで実行してしまった。
- 人のコードをコピペして使っているとこういうことが起こります。
ec2 = boto3.resource('ec2') #<<<< boto3.client('ec2')が正解 instances = ec2.describe_instances()
- 対応 : boto3.client('ec2')で取得したオブジェクトを使う
can't compare offset-naive and offset-aware datetimes
- 原因 : 異なるタイムゾーンのdatetimeオブジェクトを比較しているから
- 経緯 :
def get_tag_value_time(tag): ...省略... # 1. タイムゾーンを指定していなかった tag_time = datetime(now.year, now.month, now.day, int(time[0]), int(time[1])) print('タグに指定された時間(JST)は、' + tag_time.strftime(DATETIME_FORMAT)) return tag_time def is_start_instance(event, tag_time): ...省略... # 2. 実行時間はタイムゾーンをJSTにした jst_event_time = utc_event_time.astimezone(timezone(timedelta(hours=+9))) print('実行時間(JST)は、' + jst_event_time.strftime(DATETIME_FORMAT)) # AutoStopタグに指定された時刻の前後5分以内であればインスタンス起動する # 3. タイムゾーンはNoneになっていた start_time_from = tag_time + timedelta(minutes=-5) start_time_to = tag_time + timedelta(minutes=5) print('処理時間(' + jst_event_time.strftime(DATETIME_FORMAT) + ')が、' + \ start_time_from.strftime(DATETIME_FORMAT) + 'から' + start_time_to.strftime(DATETIME_FORMAT) + 'だったら起動します。') # 4. タイムゾーンがJSTとNoneのdatetimeオブジェクトを比較してエラーになった return (start_time_from <= jst_event_time) and (jst_event_time <= start_time_to) def start_instance(event, instance): ...省略... tag_time = get_tag_value_time(auto_start_tag) if is_start_instance(event, tag_time): ...省略...
- 調べた方法 : 各datetimeオブジェクトについてutcoffset()でタイムゾーンを確認した
print(tag_time.utcoffset()) # None print(jst_event_time.utcoffset()) # 9:00:00 print(start_time_from.utcoffset()) # None print(start_time_to.utcoffset()) # None
- 対応 : タグから取得した時間でdatetimeオブジェクトを作るときにタイムゾーンを指定する
JST = timezone(timedelta(hours=+9)) ...省略... def get_tag_value_time(tag): ...省略... tag_time = datetime(now.year, now.month, now.day, int(time[0]), int(time[1]), 0, 0, JST) print('タグに指定された時間(JST)は、' + tag_time.strftime(DATETIME_FORMAT)) return tag_time
Unable to import module 'lambda_function': No module named 'urllib2'
- 原因 : Python 3から urllib2 モジュールがなくなったから
import urllib2
...省略...
response = urllib2.urlopen(url)
注釈 urllib2 モジュールは、Python 3 で urllib.request, urllib.error に分割されました。 2to3 ツールが自動的にソースコードのimportを修正します。
20.6. urllib2 --- URL を開くための拡張可能なライブラリ — Python 2.7.17 ドキュメント
- 対応 : urllib.requestを使う
import urllib.request
...省略...
response = urllib.request.urlopen(url)
cannot use a string pattern on a bytes-like object
- 原因 : byte型とstr型を比較するから
- type関数で型を調べるとbyte型とstr型でした。
pattern = r"DTSTART;VALUE=DATE:" # DTSTART;VALUE=DATE:yyyyMMddの行の正規表現 repattern = re.compile(pattern) print(type(pattern)) # >>>>>>>>>>>>>>>>>>> <class 'str'> print(type(list[num-1])) #>>>>>>>>>>>>>>>>> <class 'bytes'> print(list[num-1]) #>>>>>>>>>>>>>>>>>>>>> b'END:VCALENDAR\r\n' match = repattern.search(list[num-1])
- 対応 : byte型をstr型に変換してから比較する
pattern = r"DTSTART;VALUE=DATE:" # DTSTART;VALUE=DATE:yyyyMMddの行の正規表現 repattern = re.compile(pattern) match = repattern.search(list[num-1].decode('utf-8')) #<<< 変換してから比較する