関連づけられてないEBSをSlack通知 & 削除するLambda

# はじめに
以前、[「関連づけられてないEIPをSlack通知 & 削除するLambda」](https://qiita.com/suzuki_kento/items/6adb5e7e70bb17148699)を作成しました。

今回は上記のLambdaを応用し、何も関連づけられていない（= 「使用可能」状態）のEBSボリュームを削除するLambdaを作成してみました。


なぜ、作成するに至ったかというと、自分の所属するチームの検証環境には**59台の「使用可能」状態のEBSが存在しており、毎月3000円ほど無駄な費用がかかっていた**ためです。

また、**「使用可能」状態のEBSは「終了時に削除」が有効化されていないAMIからインスタンスを作成してしまうと、作成者の意図しないところで湧き出す**のです。
なので、定期的に駆除したいと感じていました。

# 仕様
- 検証環境に存在する何も紐づけられていないEBSをSlack通知・削除(全リージョン)
- Amazon EventBridgeと組み合わせて毎日定時で実行
- 容赦ない削除、慈悲はない
- 削除されたくない場合、以下で回避可能
    - EC2に紐づけておく
    - スナップショットを取得しておく
- 以下のようなSlack通知
![delete-ebs.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2109911/cd45cb93-76ae-290c-fe8c-2cad8125ad6a.png)

「何も関連づけられていないEBSボリュームはお金かかるし必要ないよね！？」
「消されるのが嫌だったらスナップショットを取得しておいてね！」

という気持ちで作っております。

# 作成方法
Lambdaの中身であるpythonスクリプトや権限以外は[EIP削除用Lambdaと同じです](https://qiita.com/suzuki_kento/items/6adb5e7e70bb17148699)
### 作成までの流れ
今回はpythonのファイルと外部モジュールをzipパッケージ化してLambdaへアップロードします。
手順は以下となります。

1. Cloud9でzipパッケージを作成
2. Lambdaを作成
3. Amazon EventBridgeのルール作成

# 1. Cloud9でzipパッケージを作成
1-1. Cloud9のコンソールで[Create environment]をクリック
1-2. 名前を適当につけて[Next Step]
1-3. 以降のステップは何も変更しないで[Next Step] > [Create environment]
1-4. Cloud9が起動したら、ターミナルで以下のコマンドを実行
```
# ディレクトリを作成し、作成したディレクトリへ移動
$ mkdir lambda_workspace && cd lambda_workspace

# Lambdaに呼び出されるPythonのファイルを作成
$ touch lambda_function.py
```
1-5. 「lambda_function.py」を開き、以下のスクリプトをコピペして保存
```
# coding:utf-8

import datetime
from http import client
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():
    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_volumes(
            Filters=[
                {
                    'Name': 'status',
                    'Values': [
                        'available',
                    ]
                },
            ]
        )
        volumes = response['Volumes']
        if not volumes:
            continue
        for volume in volumes:
            instance = dict()
            instance['id'] = volume['VolumeId']
            tags = volume.get('Tags')
            instance['name'] = 'null'
            instance['owner'] = 'null'
            if tags is not None:
                for tag in tags:
                    if tag['Key'] == 'Name':
                        instance['name'] = tag['Value']
                    elif tag['Key'] == 'Owner':
                        instance['owner'] = tag['Value']
                    elif tag['Key'] == 'owner':
                        instance['owner'] = tag['Value']
            instance['type'] = volume['VolumeType']
            instance['size'] = volume['Size']
            instance['region'] = region['RegionName']
            instances.append(instance)
    return instances
def delete_volume(instances):
    if not instances:
        return None
    for instance in instances:
        client = boto3.client('ec2',region_name=instance['region'])
        client.delete_volume(VolumeId=instance['id'])
def build_message(instances):
    if not instances:
        return None
    now = datetime.datetime.now(pytz.timezone('Asia/Tokyo'))
    text = '{} アタッチされていないEBS一覧'.format(now.strftime('%Y年%m月%d日%H時%M分'))
    atachements_text = ''
    for instance in instances:
            atachements_text += '削除→ name: {}, type: {}, size: {}GB, id: {}, region: {}\n'.format(
                instance['name'], instance['type'], instance['size'], instance['id'], instance['region'])
    atachements = {'text': atachements_text, 'color': 'red'}
    message = {
        'text': text,
        'channel': SLACK_CHANNEL,
        'attachments': [atachements],
    }
    return message
def post_message(message):
    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)
            delete_volume(instances)
```
1-6. Cloud9のターミナルで以下のコマンドを実行
```
# 必要なライブラリ(pytzとrequests)を「lambda_workspace」のディレクトリへインストール
$ pip install pytz -t .
$ pip install requests -t .

# 「lambda_workspace」のディレクトリの中身をzip圧縮
$zip -r lambda_workspace.zip *
```
1-7. 作成されたzipファイルをダウンロード
![test_-_AWS_Cloud9-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2109911/fea7181c-077f-f790-b925-3c6fc360d463.png)

## 2. Lambdaを作成
2-1. Lambdaのコンソールで[関数の作成]をクリック
2-2. 任意の関数名を入力し、ランタイムに「Python3.9」を選択し、[関数の作成]をクリック
2-3. 作成されたLambdaの[コード]タブから[アップロード元]　> [.zipファイル]をクリック
![releaseEIP-test_-_Lambda.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2109911/b2bf558a-140a-40b9-360d-9d688a6ac39f.png)
2-4. 1の手順で作成したzipファイルをアップロード
2-5. [設定]タブ > [アクセス権限] > ロールをクリック
![releaseEIP-test_-_Lambda-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2109911/f50b31ab-f22e-1ee5-3894-ec85b1fef681.png)
2-6. Lambdaのデフォルトロールにアタッチされているポリシーをクリック
2-7. [ポリシーの編集]をクリックし、ポリシーに以下の権限を追加
- ec2:DescribeRegions
- ec2:DescribeVolumes
- ec2:DeleteVolume

2-8. 作成したLambdaの[設定]タブから[環境変数] > [編集]をクリックし、以下を入力
（※値は適宜修正してください）

| キー | 値 | 
| --- | --- |
| SLACK_CHANNEL | 通知したいチャンネル名| 
| SLACK_WEBHOOK_URL | SlackのWebhook URL (例：https://hooks.slack.com/services/~)|

2-9. 作成したLambdaの[設定]タブから[一般設定]でタイムアウトを３分に変更
## 3. Amazon EventBridgeの設定
※スケジュールで(毎日)Lambda実行したい場合はこの手順が必要
3-1. Amazon EventBridgeのコンソールで[ルール]をクリック
3-2. [名前]を適当に入れて[ルールタイプ]スケジュールを選択し[次へ]
3-3. Cron式でスケジュールを入力
以下の例は毎日18:00に実行される
![Event.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2109911/1007f7b0-ff8b-08d5-99d7-1efb41889cde.png)
3-4. ターゲットで前の手順で作成したLambdaを選択し[次へ]
3-5. あとは任意で設定し、ルールを作成

これで毎日18:00に何も関連付けられていないEBSを駆逐です！

# おわりに
不要なEBSにお困りの皆様、是非こちらのLambdaをお使いください！！