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を数日持っておきたい場合も想定してのタグでの運用です。
削除されたくない場合はタグを外しておけばずっと残ります(これも落とし穴かも)
タグ付与
Lambdaコード実装
タイムアウトは15秒と長くしました。
AllocationIdが肝で、これで使用判定を行なっています。
厳密にいうと、NAT Gatewayへの割当時にも使用判定されてしまうのでそこはご留意ください。
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されています。
3.EventBridge→Lambda→Slackで通知する
Lambdaで未使用中のEIP発見→Slackで通知といったものです。
今回もEventBridgeは割愛です。EIPの確認頻度によると思いますが、数時間に1回チェックなどで良いと思います。
どちらかというとslack通知の勉強目的で作成したので運用はあまり考えていません...
Lambdaコード実装
環境変数
webhookのURL取得方法も割愛です。一般的なIncoming Webhookのアプリを追加して取得する方法です。
環境変数にwebhookのURLを今回は入れる必要があります。
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"
}