EIPリリース忘れでの課金を防ぐ方法3選(Lambda/slack通知など)

AWSの勉強中でAWSの無料枠をできるだけ使用してますが、課金されるケースたまにあります。

その中でも初心者あるあるなのがEIPに対する課金です。

今回はEIPリリース忘れで課金を防ぐ方法をテーマにAWSで勉強したことを共有します。

AWSのサービスは色々あるので最適解が難しいですが、参考にしていただればと思います。

**目次**
1.予防策：CloudFormationで構築する
2.EventBridge→Lambdaで定期的に未使用EIPを削除する
3.EventBridge→Lambda→Slackで通知する

# 前提：そもそもどういうシチュエーションで課金されるのか
**「EIPは使っていない時に課金される。2つ目からは常に課金。」**

例えば、EIPを取得して割当してない場合は当然使用中だと判定されませんし、
割当ていても起動していないと使用(機能)してないとみなされて課金されます。


あるあるなのがインスタンスを停止中で課金されてしまうことです。


EC2削除してEIPのリリース忘れも実際には多いでしょう。


# 1.予防策：CloudFormationで構築する
そもそも、リリースを忘れを防ぐためにCloudFormationでEIPも一緒に構築する方法があります。

**改善策というよりも予防策ですね。**

わざわざ、これのためにするって人は少ないと思いますがCloudFormationでインスタンスを作成する際に使えると思ったので共有です。

# 2.EventBridge→Lambdaで定期的に未使用EIPを削除する
**概要**
まず、EIPにAutoDeleteのタグをつけておきます。

その上でEC2を削除するとEIPが使用されていない状態になります。

同じEIPを使用してまた構築したい場合もあると思うので、日次で削除実行をするようにしました。

EIPを数日持っておきたい場合も想定してのタグでの運用です。

削除されたくない場合はタグを外しておけばずっと残ります(**これも落とし穴かも**)

## タグ付与
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2821335/2d7fe817-9ac2-4d15-8a6f-c6a42158b8a8.png)
AutoDelete:trueをEIPに付与します。

## Lambdaコード実装
タイムアウトは15秒と長くしました。

AllocationIdが肝で、これで使用判定を行なっています。

厳密にいうと、NAT Gatewayへの割当時にも使用判定されてしまうのでそこはご留意ください。


```py:lambda_function.py
import boto3

def lambda_handler(event, context):
    ec2 = boto3.client('ec2')
    
    # AutoDelete=true タグがついている EIP を取得
    response = ec2.describe_addresses(
        Filters=[
            {"Name": "tag:AutoDelete", "Values": ["true"]}
        ]
    )

    released = 0

    for address in response['Addresses']:
        alloc_id = address['AllocationId']
        assoc_id = address.get('AssociationId')

        # インスタンスに関連付けられていない場合のみ解放
        if not assoc_id:
            ec2.release_address(AllocationId=alloc_id)
            print(f"Released unused EIP: {alloc_id}")
            released += 1
        else:
            print(f"Skipped in-use EIP: {alloc_id}")

    return {"status": "done", "released": released}

```

## EventBridgeでの実装
特別な実装がないため、今回は割愛します。先ほど作成したlambdaを指定して日次で動かしてください。

## 結果
14:30で設定してましたが、30秒程度で確認からリリースまで完了しました。
使用中なのはSkipされ、未使用なものはReleaseされています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2821335/1739fa98-0a97-45d4-99b9-bcbffbc504ef.png)

# 3.EventBridge→Lambda→Slackで通知する
Lambdaで未使用中のEIP発見→Slackで通知といったものです。

今回もEventBridgeは割愛です。EIPの確認頻度によると思いますが、数時間に1回チェックなどで良いと思います。

どちらかというとslack通知の勉強目的で作成したので運用はあまり考えていません...

## Lambdaコード実装
### 環境変数
webhookのURL取得方法も割愛です。一般的なIncoming Webhookのアプリを追加して取得する方法です。
**環境変数にwebhookのURLを今回は入れる必要があります。**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2821335/00edc343-7c41-42ba-a05b-975d06f1ee1a.png)



```py:lambda_function.py
import boto3
import json
import urllib3
import os

ec2 = boto3.client('ec2')
http = urllib3.PoolManager()
SLACK_WEBHOOK_URL = os.environ['SLACK_WEBHOOK_URL']  # 環境変数に設定しておく

def lambda_handler(event, context):
    addresses = ec2.describe_addresses()['Addresses']
    unused_eips = []

    for address in addresses:
        if 'AssociationId' not in address:  # アタッチされていないEIPのみ
            unused_eips.append(f"- {address['PublicIp']} (ID: {address['AllocationId']})")

    if unused_eips:
        message = {
            "text": ":warning: *未使用のElastic IPが見つかりました！*\n" + "\n".join(unused_eips)
        }
        http.request("POST", SLACK_WEBHOOK_URL, body=json.dumps(message).encode("utf-8"),
                     headers={'Content-Type': 'application/json'})
        print("Slackに通知しました")
        return {
            "status": "EIP found",
            "count": len(unused_eips)
        }

    print("未使用のEIPはありません")
    return {
        "status": "No unused EIP"
    }

```

## 結果
未使用EIPが見つかった場合、slackへと通知が来ます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2821335/d5ae35f9-4320-46b6-9ba4-d374add8a401.png)