EC2インスタンスの状態変化をLambda(boto3)とSNSで自動通知する

# はじめに
皆さま初めましてjimbot3です。botと入っておりますがchatbotではなくboto3のほうです。

Qiitaを読む習慣を持っている皆さまであれば、1度は"サーバレス"という単語を聞いたことがあると思います。
ざっくり言うと「サーバを持たずコードを実行する仕組み」のこと。
今回以下の理由でAWSのサーバレスアーキテクチャ"Lambda"とLambdaのトリガ用に"CloudWatch"を使ってサーバレスを体験してみました。

- これから徐々に、もしくは急激にサーバレスは流行るだろうと考えている(jimbot3リサーチ社調べ)
- 自分のコーディングスキルを向上させたい
- 「この前サーバレスやってみたんだけど～」とか言ってみたい
- 研修でLambdaを使う課題が出されたから、と言う最たる理由があったりなかったり

# つくってみたLambdaとその解説
#### 1. EC2インスタンスの起動・停止をLambda経由でSNS(E-mail)通知するLambda
##### 実行するLambdaの中身

```python
import boto3

def lambda_handler(event, context):
    ec2 = boto3.client('ec2')
    sns = boto3.client('sns')
    mystatus= ec2.describe_instances(
        Filters=[{'Name':'instance-id', 'Values':['i-04XXXXXXXXXXXXXXX']}] # 取得したいEC2のインスタンスIDを入力
    )["Reservations"][0]["Instances"][0]['State']['Name'] 
    if mystatus == 'stopped':
        print ('stop!!')
        topic = 'arn:aws:sns:ap-northeast-1:08XXXXXXXXXX:Myaddress' # 送付したいARNを入力
        subject = 'StatusChangeMail'
        message = 'stop!!'
        region = '{ap-northeast-1}'
        response = sns.publish(
            TopicArn=topic,
            Message=message,
            Subject=subject,
            MessageStructure='raw'
    )
    elif mystatus == 'running': # 後程説明しますが、今回はCloudWatchでrunningとstoppedの値のみ拾う仕様なのでelifでrunningを指定しております
        print ('running!!')
        topic = 'arn:aws:sns:ap-northeast-1:08XXXXXXXXXX:Myaddress' # 送付したいARNを入力
        subject = 'StatusChangeMail'
        message = 'running!!'
        region = '{ap-northeast-1}'
        response = sns.publish(
            TopicArn=topic,
            Message=message,
            Subject=subject,
            MessageStructure='raw'
    )
```
>1. mystatusに取得したいEC2インスタンスのStatusを格納  
2. 停止(==stop)状態であればstopメッセージ/起動(==start)状態であればstartメッセージのメール送付  
  
  
##### ↑のLambdaをトリガーさせるCloudWatchの中身
※Lambda画面からCloudWatch Eventsの設定もできますが今回はCloudWatch画面での設定
<img width="762" alt="clwa.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/322881/bfb6875e-3d82-3d69-b171-3dc6f7e32566.png">

>取得したいEC2のインスタンスIDを指定し、起動もしくは停止したのをトリガーにLambdaを実行

##### 届いたメールはこんな感じ
<img width="435" alt="gmail1.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/322881/93dc8569-597b-c623-ffc9-abec2072dd6b.png">

つまりLambdaとCloudWatchを使うことで以下のような通知が行える
1. EC2のステータス変更をきっかけにCloudWatchがLambdaを実行
2. LambdaでEC2の状態を拾いメール

#### 2. 24hのEC2インスタンスの起動・停止の集計をSNS(E-mail)通知するLambda
###### 実行するLambdaの中身
```python
import boto3
import datetime
import calendar

def lambda_handler(event, context):
    group_name = '/aws/lambda/XXXXXXX' # 先ほどの1.でつくった起動停止Lambdaのロググループを入力
    client = boto3.client('logs')
    timeto = datetime.datetime.now() # print(timeto) → 2018-12-08 10:24:20.499645
    u_to = calendar.timegm(timeto.utctimetuple()) * 1000 # print(u_to) →　1544264660000
    timefrom = timeto - datetime.timedelta(days=1) # print(timefrom) → 2018-12-07 10:24:20.499645
    u_from = calendar.timegm(timefrom.utctimetuple()) * 1000 # print(u_from) → 1544178260000
    #u_toとu_fromを*1000したのは後のfilter_log_eventsの期間指定がミリ秒のため

    response = client.filter_log_events(
        logGroupName=group_name,
        startTime = u_from,
        endTime = u_to
    )
    #print(response) →　{'events': [], 'searchedLogStreams': [], 'ResponseMetadata': {'RequestId': '6cdXXXXX~

    file_name = '/tmp/' + 'temp.log'
    with open(file_name, 'a') as f:
        for stream in response['events']:
            message = '[{}] {}'.format(datetime.datetime.fromtimestamp(int(str(stream['timestamp'])[:10])) 
            + datetime.timedelta(hours=9), stream['message']) #[:10]はミリ秒を秒に直すため14桁のうち左から10桁
            f.write(message)

    topic = 'arn:aws:sns:ap-northeast-1:08XXXXXXXXXX:Myaddress' # 送付したいARNを入力
    subject = '過去24時間分 起動停止記録'
    file = open(file_name, 'r')
    message = file.read()
    sns = boto3.client('sns')
    region = '{ap-northeast-1}'
    if message:
        response = sns.publish(
            TopicArn=topic,
            Message=message,
            Subject=subject,
            MessageStructure='raw'
        )
    else:
        response = sns.publish(
            TopicArn=topic,
            Message="直近24時間に起動/停止はございませんでした。",
            Subject=subject,
            MessageStructure='raw'
        )
```

>
1. 先ほどの1. で作った起動停止Lambdaの過去24時間(1日)のログを"client.filter_log_events"でフィルターし拾う。
2. そのログをtempファイルとしてに1行ずつフォーマットしながら格納。
3. 格納が終わったファイルを開き、そのtempファイルの中身をメッセージとしてメールの送付。ログが無い場合は起動/停止が無かった旨のメールを送付。

##### ↑のLambdaをトリガーさせるCloudWatchの設定
<img width="649" alt="clwa22.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/322881/ce67ffd7-0111-3322-b2eb-d477785cb48b.png">

>Lambdaをcronでスケジュール実行。(0 8 * * ? *)だと日次で日本時間の17:00に実行されます。

##### 届いたメールはこんな感じ
<img width="418" alt="mail2.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/322881/4311be63-fde6-360d-c575-1c65abe1c490.png">

つまりLambdaとCloudWatchを使うことで以下のような通知が行える
1. 日次で日本時間の17:00にCloudWatchがLambdaを実行
2. Lambdaで過去24hのEC2インスタンスの起動停止履歴を拾いメール

# 感想
- 取り敢えず1回手を動かすとサーバレスの仕組みは"何となく"理解できる。  
- 1回触ってみると「次はこれを作ってみよう」と思えるので、成長のサイクルが生まれるサービスだと思う。
(例えばEC2やVPCを1回作っても、もう1回作ってみようとは私は思えなかった。)    
- LambdaはAWSのサービスであるが必要なのはプログラミングスキルで、AWSコンソールのボタンポチポチスキルではない。