無駄なクラウドコストを排除するためLambda+EventBridgeで不要リソース自動検知の仕組みを構築してみた
自己紹介
こんにちは。株式会社DELTAでインフラエンジニアをしている浜崎です。
この記事は AWS Lambda と Serverless Advent Calendar 2023 の22日目の記事となります。
はじめに
AWS上に保存されているリソースの棚卸は定期的に実施されていますでしょうか?
棚卸を実施していないために不要なリソースの存在に気づけず、意図しない料金が発生している状況もあるかと思います。
だから定期的にリソースの棚卸はした方がいい。
しかし、普段の運用の中でそこに時間を取れない。
そんな状況でも最低限の不要リソースを検知できるよう、サーバレスで検知の仕組みを構築しました。
不要リソースといっても、削除忘れのリソースや過去のバックアップ等いろいろあるかと思います。今回はその中でも削除忘れによって比較的高額な料金が発生するEBSを検出する仕組みを紹介します。
つくったもの
最初に構築したものの紹介です。
構成図
Lambdaで保存されるEBSの一覧を取得し、不要なものがあればその一覧をSNSTopicで通知します。
不要なEBSってどんなEBS?
不要なEBSを検知すると書いていますが、不要なEBSとは具体的にどんなEBSでしょうか。
今回はそれを以下と定義しました。
-
ステータスが"available"であるリソース
ステータスが"available"のEBSはどこのリソースにも紐づいていないボリュームなので、不要である可能性が高いです。
また、意図的に残しているのであれば、スナップショットを取得して削除するのがコスト的に望ましいため、いずれにせよ削除対象になるかと思います。
通知先はメールやSlack等ありますが、ここではメールとします。
最後に、このLambda関数をEventBridgeで毎日AM5時に実行することで、日毎に不要リソースに気づけるようにしています。
エラー検知
こちらは不要リソース検知と直接関係ないですが、Lambdaのエラー検知の仕組みも同時に構築しました。
Lambdaのログが出力されるCloudWatchLogsで"ERROR"文字列をフィルターするメトリクスフィルターを設定し、エラーを検知したらメールで通知がされる仕組みです。
※CloudWatchLogsサブスクリプションフィルターを使用すると、CloudWatchアラームを使用せずにエラー通知できるようです。一通りエラー検知の仕組みを作った後にこの方法に気づいたので、後日この構成に変更しようと思います。
Lambdaコード
不要リソースを検知するLambdaコードを記載します。
ランタイムはpython3.12です。
SNSTopicのARNは環境変数で設定するようにしています。
import boto3
import os
sns = boto3.client('sns')
ec2 = boto3.client('ec2')
# 通知先SNSトピックのARNを取得
TOPIC_ARN = os.environ['SNS_TOPIC']
def lambda_handler(event, context):
volumes = collect_ebs_volume_status_available()
if volumes:
publish_sns_message(volumes)
else:
print("すべてのEBSボリュームが利用されています")
def collect_ebs_volume_status_available():
response = ec2.describe_volumes(Filters=[{'Name': 'status', 'Values': ['available']}])
volumes = response['Volumes']
return volumes
def publish_sns_message(volumes):
message = "利用されていないEBSボリュームが存在します\n\nEBSボリュームID"
volume_id = ""
for volume in volumes:
volume_id += volume['VolumeId'] + "\n"
content = message + "\n" + volume_id
sns.publish(
TopicArn=TOPIC_ARN,
Message=content
)
CloudFormation
前項に載せた構成図にあるリソースをすぐに構築できるよう、CloudFormationスクリプトを作成しました。
CloudFormationスクリプト
AWSTemplateFormatVersion: "2010-09-09"
Description: "不要EBS検知"
Parameters:
EndPointEmail:
Description: Enter the email address to be notified
Type: String
LambdaCodeS3Backet:
Description: Enter the S3 bucket with Lambda code
Type: String
Resources:
LambdaFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: AvailableEBSChecker
Handler: DataCheckAvailableEBS.lambda_handler
Role: !GetAtt LambdaExecutionRole.Arn
Runtime: python3.12
Timeout: 3
Code:
S3Bucket: !Ref LambdaCodeS3Backet
S3Key: DataCheckAvailableEBS.zip
Environment:
Variables:
SNS_TOPIC: !Ref SNSTopic
SNSTopic:
Type: AWS::SNS::Topic
Properties:
DisplayName: DataCheckAvailableEBS
EmailSubscription:
Type: AWS::SNS::Subscription
Properties:
Protocol: email
TopicArn: !Ref SNSTopic
Endpoint: !Ref EndPointEmail
LambdaExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: LambdaPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- ec2:DescribeVolumes
- ec2:DescribeVolumeStatus
- ec2:DescribeVolumeAttribute
- sns:Publish
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
- logs:FilterLogEvents
Resource: '*'
LambdaLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: /aws/lambda/DataCheckAvailableEBS
RetentionInDays: 1
ErrorLogFilter:
Type: AWS::Logs::MetricFilter
Properties:
LogGroupName: !Ref LambdaLogGroup
FilterPattern: "ERROR"
MetricTransformations:
- MetricName: /aws/lambda/DataCheckAvailableEBSError
MetricNamespace: DataCheckAvailableEBS
MetricValue: 1
DefaultValue: 0
ErrorAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: DataCheckAvailableEBSErrorAlarm
AlarmDescription: "Lambda Error Alarm"
Namespace: DataCheckAvailableEBS
MetricName: /aws/lambda/DataCheckAvailableEBSError
Statistic: Maximum
Period: 300
EvaluationPeriods: 1
Threshold: 1
ComparisonOperator: GreaterThanOrEqualToThreshold
AlarmActions:
- !Ref SNSTopic
EventBridgeRule:
Type: AWS::Events::Rule
Properties:
Name: DataCheckAvailableEBS
Description: "Lambdaを毎日5:00に実行"
ScheduleExpression: cron(0 20 * * ? *) #UTC指定のため20時実行に設定
State: ENABLED
Targets:
- Arn: !GetAtt LambdaFunction.Arn
Id: DataCheckAvailableEBS
PermissionForEventsToInvokeLambda:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !Ref LambdaFunction
Action: lambda:InvokeFunction
Principal: events.amazonaws.com
SourceArn: !GetAtt EventBridgeRule.Arn
やってみる
前項に記載のLambdaスクリプトとCloudFormationを使用して、実際にステータスが"available"のEBSを検知し、メール通知がされるか試してみます。
実施の流れはこちらとなります。
- Lambdaコード記載のzipファイルをアップロードするS3バケットを作成する。
- Lambdaコード記載のzipファイルをアップロードする。
- CloudFormationスタックを作成。(Lambdaコード保存S3バケット、通知メールアドレスを指定)
- 指定のEmailアドレスで受信したSNSトピックを承認する。
- 作成されたLambdaをTest実行し、意図した挙動を確認する。
S3バケット作成 / zipファイルアップロード
任意の命名でS3バケットを作成し、Lambdaソースコードをzipにしてアップロードします。
ここで二点注意点があります。
- zipファイルはバケット直下に配置してください。
- zipファイル名をDataCheckAvailableEBS.zipにしてください。
※zipファイル名は前項のCloudFormationスクリプトで固定していました。今後パラメータから任意の命名を指定できるように修正しようと思います。
CloudFormationスタックの作成
CloudFormationスタックを作成します。
CloudFormationの画面から[スタックを作成]を押下し、CloudFormationスクリプトをアップロードしてください。
スタック名、メール通知に使用するメールアドレス、1で作成したS3バケット名を入力します。
その他はデフォルトの設定値でスタックを作成します。
指定のEmailアドレスで受信したSNSトピックを承認する。
CloudFomationスタックの作成が完了すると、その中で作成されたSNSTopicからメールアドレスの確認メールが飛んできます。ここで"Confirm subscription"を押下し確認します。
SNSTopicの画面でステータス"確認済み"と表示されていれば成功です。
テスト用EBS作成
検知対象となるEBSを作成します。
検知テスト用なので、ボリュームサイズは最小の1GiBとします。
※日本語表記なのでステータス「利用可能」と表示されていますが、これが検知対象のステータス「available」です。
検知テスト
Lambda画面からTestを実行します。
無時検知メールを受信しました!
次はEventBridgeでAM5時に実行されるかの確認です。
翌日確認すると・・・
AM5時に検知メールを受信していました!
まとめ
今回紹介した仕組みを取り入れると、削除忘れのEBSの存在に気づくことができます。
ちなみに、EBSの削除漏れの原因の一つには、インスタンス終了時に削除する項目が無効化されている場合があります。この場合、EC2は削除したのにEBSは残っていてその分の料金が引き続き発生してしまいます。
EBSの料金は東京リージョン1GB/月あたりgp2ボリュームで0.12USD、gp3ボリュームで0.096USDのため、仮に1000GBのEBSの削除漏れがあるだけで、月約100USDの損失につながります。棚卸されない時間が続き、こういったリソースが増えることによって、この損失分がかなり高額になることも考えられます。
そのため、最低限ここの無駄には気づきたい!という部分だけでも、検知できる仕組みを構築してみてはいかがでしょうか。
We're Hiring!
DELTAではチームの一員になっていただける仲間を募集中です!
下記フォームよりお気軽にご連絡ください!
Discussion