AWSで起動しているインスタンスをPythonでチェックしてSlackに投稿する

Pythonのお勉強とEC2インスタンス停止忘れ防止のため、起動しているEC2インスタンスをチェックしてSlackに投稿するようにしたメモ。

## 参考リンク

以下の先人の記事を参考に作成。

- [AWS EC2 インスタンスが何台起動しているかをモニタリングする](https://qiita.com/aokad/items/eb58d9f503254a7bdf54)

## 動作イメージ

<img width="603" alt="image.png" src="https://qiita-image-store.s3.amazonaws.com/0/86449/ac29a98e-7014-66e2-9d89-932364b5c965.png">

## Slackの準備

### Webhook URLの取得

以下を参考にWebhook URLを取得する。

- [SlackのWebhook URL取得手順](https://qiita.com/vmmhypervisor/items/18c99624a84df8b31008)

## AWS CLI

Pythonの前にAWS CLIで起動しているインスタンスを確認してみる。

AWS CLIをセットアップする。

```shell
pip3 install awscli
aws configure
```

インスタンスを確認する。

```shell:コマンド
aws ec2 describe-instances --filters "Name=instance-state-name,Values=running"
```

## Python + AWS Lambda

AWS Lambda上のPythonからSlackに投稿する。

### IAM Roleの作成

[IAMコンソール](https://console.aws.amazon.com/iam/home#/roles)でRoleを作成し、`AWSLambdaBasicExecutionRole`と`AmazonEC2ReadOnlyAccess`のポリシーをアタッチする。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/86449/f8a11699-0f2e-c5ce-f9b7-8708c20750e7.png)

作成したRoleのARNを確認しておく。

### コード作成

作成するファイルは以下の3つ。→[GitHub](https://github.com/sotoiwa/aws_instance_checker)

```
.
├── lambda.json
├── lambda_function.py
└── requirements.txt
```

`lambda.json`はlambda-uploderが使用するJSON形式のLambda関数の定義ファイル。先ほど作成したRoleのARN、Slack Webhook URL、Slackチャンネル名を指定する。lambda-uploaderの使い方と、jsonの書き方は以下を参照。

- [lambda-uploader](https://github.com/rackerlabs/lambda-uploader)

```json:lambda.json
{
  "name": "aws_instance_checker",
  "description": "My AWS instance checker.",
  "region": "ap-northeast-1",
  "runtime": "python3.7",
  "handler": "lambda_function.lambda_handler",
  "role": "arn:aws:iam::hogehoge",
  "timeout": 300,
  "memory": 128,
  "variables":
    {
      "SLACK_WEBHOOK_URL": "https://hooks.slack.com/services/hogehoge",
      "SLACK_CHANNEL": "#hogehoge"
    }
}
```

`lambda_function.py`が関数本体。以下を参考に作成。

- [Boto3に再入門してみた](https://qiita.com/shiimaxx/items/e436c1e0019f0497b5c4)
- [EC2 - Boto 3 Docs](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ec2.html)
- [Incoming Webhooks - Slack API](https://api.slack.com/incoming-webhooks)

```python:lambda_function.py
import datetime
import os
import json

import boto3
import pytz
import requests

SLACK_WEBHOOK_URL = os.environ['SLACK_WEBHOOK_URL']
SLACK_CHANNEL = os.environ['SLACK_CHANNEL']


def get_instances():
    """boto3を使って全てのリージョンで起動しているEC2インスタンスを取得する

    :return: instances
    """

    # 全リージョンを取得
    client = boto3.client('ec2')
    regions = client.describe_regions()['Regions']
    
    instances = []

    # 各リージョン毎に繰り返し
    for region in regions:
        client = boto3.client('ec2', region_name=region['RegionName'])
    
        response = client.describe_instances(
            Filters=[
                {
                    'Name': 'instance-state-name',
                    'Values': [
                        'running',
                    ]
                },
            ]
        )

        reservations = response['Reservations']

        if not reservations:
            continue

        for reservation in reservations:
            instance = dict()
            instance['id'] = reservation['Instances'][0]['InstanceId']
            tags = reservation['Instances'][0]['Tags']
            instance['name'] = 'null'
            for tag in tags:
                if tag['Key'] == 'Name':
                    instance['name'] = tag['Value']
            instance['type'] = reservation['Instances'][0]['InstanceType']
            instance['region'] = region['RegionName']
            instances.append(instance)

    return instances


def build_message(instances):
    """SlackにPOSTするメッセージボディを作成する

    :param instances:
    :return: message
    """

    if not instances:
        return None

    now = datetime.datetime.now(pytz.timezone('Asia/Tokyo'))

    text = '{}現在稼働しているEC2インスタンス一覧'.format(now.strftime('%Y年%m月%d日%H時%M分'))
    atachement_text = ''
    for instance in instances:
        atachement_text += 'name: {}, type: {}, id: {}, region: {}\n'.format(
            instance['name'], instance['type'], instance['id'], instance['region'])

    atachements = {'text': atachement_text, 'color': 'warning'}

    message = {
        'text': text,
        'channel': SLACK_CHANNEL,
        'attachments': [atachements],
    }
    return message


def post_message(message):
    """SlackにPOSTする

    :param message:
    :return:
    """

    response = requests.post(SLACK_WEBHOOK_URL, data=json.dumps(message))
    response.raise_for_status()


def lambda_handler(event, context):

    instances = get_instances()

    message = build_message(instances)

    if message:
        post_message(message)
```

`requirements.txt`は使用するモジュールの定義。

```:requirements.txt
boto3
pytz
requests
```

### アップロード

lambda-uploaderをインストールする。

- [AWS Lambda Pythonをlambda-uploaderでデプロイ](https://dev.classmethod.jp/cloud/deploy-aws-lambda-python-with-lambda-uploader/)

```
pip3 install lambda-uploader 
```

アップロードする。

```
$ lambda-uploader
Î» Building Package
Î» Uploading Package
Î» Fin
$
```

### スケジュール実行

lambda-uploaderによってLambda関数が作成されるので、[Lambdaコンソール](https://ap-northeast-1.console.aws.amazon.com/lambda/home?region=ap-northeast-1)からトリガーとしてCloudWatch Eventsを追加する。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/86449/ac380923-8d3f-d29d-659b-bfe64f8269b2.png)

スケジュールを設定する。確認のためとりあえず`cron(* * * * ? *)`（毎分実行）を設定する。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/86449/6698f0df-5710-0bf2-bd91-63852f7aa997.png)

Slackに投稿されることを確認する。

<img width="606" alt="image.png" src="https://qiita-image-store.s3.amazonaws.com/0/86449/6d53a85f-7279-c7ee-6d48-f850c5875879.png">

上手く投稿できなかったら、[CloudWatch Logs](https://ap-northeast-1.console.aws.amazon.com/cloudwatch/home?region=ap-northeast-1#logs:)でログを確認する。上手く投稿が確認できたら、[CloudWatch Events](https://ap-northeast-1.console.aws.amazon.com/cloudwatch/home?region=ap-northeast-1#rules:)でスケジュールを`cron(0 * * * ? *)`（毎時）に変更する。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/86449/8ccab2cc-1f6f-1184-d4d5-01bf3e9489c8.png)