📖

無駄なクラウドコストを排除するためLambda+EventBridgeで不要リソース自動検知の仕組みを構築してみた

2023/12/22に公開

自己紹介

こんにちは。株式会社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アラームを使用せずにエラー通知できるようです。一通りエラー検知の仕組みを作った後にこの方法に気づいたので、後日この構成に変更しようと思います。
https://dev.classmethod.jp/articles/notification_cloudwatchlogs_subscriptoinfilter/

Lambdaコード

不要リソースを検知するLambdaコードを記載します。
ランタイムはpython3.12です。

SNSTopicのARNは環境変数で設定するようにしています。

DataCheckAvailableEBS.py
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スクリプト

DataCheckEBSsnapshot
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を検知し、メール通知がされるか試してみます。

実施の流れはこちらとなります。

  1. Lambdaコード記載のzipファイルをアップロードするS3バケットを作成する。
  2. Lambdaコード記載のzipファイルをアップロードする。
  3. CloudFormationスタックを作成。(Lambdaコード保存S3バケット、通知メールアドレスを指定)
  4. 指定のEmailアドレスで受信したSNSトピックを承認する。
  5. 作成された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ではチームの一員になっていただける仲間を募集中です!
下記フォームよりお気軽にご連絡ください!

https://docs.google.com/forms/d/e/1FAIpQLSfQuWNU1il5lq2rVdICM0tSK_jTsjqwc52LYEwUxBq7_ImtrQ/viewform

DELTAテックブログ

Discussion