Cost Optimization HubのレコメンデーションをEventBridge Scheduler+Lambdaで定期通知してみた
こんにちは!AWS事業本部のおつまみです。
みなさん、コスト削減のためのレコメンデーション(推奨事項)を定期的に通知してほしいなと思ったことはありませんか?私はあります。
コスト削減は多くのAWS利用ユーザーにとって重要な課題の1つです。
そこでAWSには、その課題解決をサポートするTrusted Adviser,Compute Optimizerなどの様々なサービスがあります。その中の1つにCost Optimization Hubがあります。
Cost Optimization Hubは、コスト最適化に関する推奨事項(レコメンデーション)を一元的に管理できる非常に優れたサービスです。
しかし、2024/5/27時点ではダッシュボード機能のみの提供となっています。
よって、普段からこのダッシュボードを定期的にチェックしない方にとっては、レコメンデーションを見逃してしまう可能性があります。
そこで、今回はCost Optimization HubのレコメンデーションをEventBridge Scheduler+Lambdaで定期通知する方法をご紹介します!
構成図
今回実装した仕組みです。
EventBridge Schedulerにて指定した間隔でLambdaを呼び出し、Cost Optimization Hubのレコメンデーションを取得します。取得後は、SNS経由でSlack通知するようにします。
構成はこちらのブログを参考にしました。
やってみた
前提条件
今回はCloudFormationを用いるため、AWSコンソールにログインできるAWSアカウントを用いるだけで構築可能です。 事前に通知先となる、Slackアカウントを用意しておいてください。
サービスの有効化
以下のサービスを事前に有効化しておく必要があります。
- Cost Optimization Hub
- Compute Optimizer
公式ドキュメントを参考に、事前に有効化しましょう。
マルチアカウント管理をしている場合は組織設定もこの時点で行うようにしておきましょう。
なおどちらのサービスもレコメンデーションが表示されるまでに24時間以上時間がかかるので、すぐにレコメンデーションが出ないことを念頭に置いておきましょう。
SlackのWebhook URL取得
SlackのWebhook URLを取得する必要があります。
下記を参考に、Slack Appを作成し、Webhook URLを取得しましょう。
CloudFormationテンプレートのデプロイ
以下のCloudFormationテンプレートを用いて、バージニア北部リージョン(us-east-1)でスタックをデプロイします。
バージニア北部リージョン(us-east-1)でデプロイする必要がある理由は、Cost Optimization Hub がグローバルサービスのためです。
AWSTemplateFormatVersion: '2010-09-09' Description: 'Cost Optimization Hub Notification' Parameters: SlackWebHookURL: Type: String Description: 'Slack WebHook URL' SlackChannelName: Type: String Description: 'Slack Channel Name' ScheduleExpression: Type: String Default: cron(0 0 * * ? *) Description: 'Notification Schedule' Resources: # IAM Role for EventBridge Scheduler EventBridgeCostOptimizationHubNotificationRole: Type: AWS::IAM::Role Properties: RoleName: EventBridge_CostOptimizationHub_Notification_Role AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: scheduler.amazonaws.com Action: sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaRole # IAM Role for Lambda LambdaCostOptimizationHubNotificationRole: Type: AWS::IAM::Role Properties: RoleName: Lambda_CostOptimizationHub_Notification_Role AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: lambda.amazonaws.com Action: sts:AssumeRole Policies: - PolicyName: my_inline_policy PolicyDocument: Version: '2012-10-17' Statement: - Action: sns:Publish Effect: Allow Resource: '*' ManagedPolicyArns: - arn:aws:iam::aws:policy/CostOptimizationHubReadOnlyAccess # SNS Topic CostOptimizationHubNotificationTopic: Type: AWS::SNS::Topic Properties: TopicName: CostOptimizationHub-notification-topic SNSTopicPolicy: Type: AWS::SNS::TopicPolicy Properties: Topics: - !Ref CostOptimizationHubNotificationTopic PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: lambda.amazonaws.com Action: sns:Publish Resource: !Ref CostOptimizationHubNotificationTopic # Lambda Function (Cost Optimization Hub Information Retrieval) LambdaCostOptimizationHub: Type: AWS::Lambda::Function Properties: FunctionName: cost-optimization-hub-notification Role: !GetAtt LambdaCostOptimizationHubNotificationRole.Arn Handler: index.lambda_handler Runtime: python3.9 Timeout: 60 Code: ZipFile: | import boto3 import json import os AccountID = os.environ['AccountId'] SNSTopicArn = os.environ['SNSTopicArn'] def lambda_handler(event, context): # Cost Optimization Hubから推奨事項を取得 # クライアント作成 client = boto3.client('cost-optimization-hub') # リクエスト発信 list_response = client.list_recommendations( filter={ accountIds': [AccountID], 'resourceTypes': ['Ec2Instance'] }, maxResults=10 ) # メッセージ生成に必要な情報の抽出 message = [] message.append("Cost Optimization HubのEC2に関するコスト最適化の推奨事項を通知") # recommendationIdを使用して詳細を取得 ec2_recommendations_found = False for recommendation in list_response['items']: recommendation_id = recommendation['recommendationId'] get_response = client.get_recommendation( recommendationId=recommendation_id ) recommendation = get_response['recommendation'] if recommendation['resourceType'] == "Ec2Instance": message.append("EC2インスタンス") message.append(f" アカウントID: {AccountID} 推定削減額: {recommendation['estimatedMonthlySavings']}") if not ec2_recommendations_found: message.append("推奨事項なし") # メッセージをSNSに送信 # 通知を送信するSNSトピックのARNを指定 sns_client = boto3.client('sns') sns_topic_arn = SNSTopicArn sns_client.publish( TopicArn=sns_topic_arn, Message=json.dumps(message, indent=3, ensure_ascii=False) ) Environment: Variables: SNSTopicArn: !Ref CostOptimizationHubNotificationTopic AccountId: !Ref AWS::AccountId # Lambda Function (Slack Notification) LambdaSlack: Type: AWS::Lambda::Function Properties: FunctionName: cost-optimization-hub-to-slack Role: !GetAtt LambdaCostOptimizationHubNotificationRole.Arn Handler: index.lambda_handler Runtime: python3.9 Code: ZipFile: | import json import os import urllib.request def lambda_handler(event, context): # SNSメッセージからCost Optimization Hubの情報を取得 message = event['Records'][0]['Sns']['Message'] # Slackへの通知処理を記述 hook_url = os.environ['HookURL'] channel_name = os.environ['ChannelName'] payload = { 'channel': channel_name, 'text': message } data = json.dumps(payload).encode('utf-8') req = urllib.request.Request(hook_url, data=data, method='POST') with urllib.request.urlopen(req) as res: res.read() Environment: Variables: HookURL: !Ref SlackWebHookURL ChannelName: !Ref SlackChannelName LambdaSlackPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref LambdaSlack Principal: sns.amazonaws.com SourceArn: !Ref CostOptimizationHubNotificationTopic # EventBridge Scheduler CostOptimizationHubNotificationSchedule: Type: AWS::Scheduler::Schedule Properties: Name: cost-optimization-hub-notification-schedule Description: Cost Optimization Hub Notification FlexibleTimeWindow: Mode: "OFF" ScheduleExpression: !Ref ScheduleExpression ScheduleExpressionTimezone: Asia/Tokyo Target: Arn: !GetAtt LambdaCostOptimizationHub.Arn RoleArn: !GetAtt EventBridgeCostOptimizationHubNotificationRole.Arn # SNS Subscription SlackSubscription: Type: AWS::SNS::Subscription Properties: TopicArn: !Ref CostOptimizationHubNotificationTopic Protocol: lambda Endpoint: !GetAtt LambdaSlack.Arn
スタックの詳細画面では、スタック名及びパラメータを指定する必要があります。
- スタック名:任意の名前
- パラメータ
- SlackWebHookURL:
事前に取得したSlackのWebhook URL
- SlackChannelName:
通知するSlackチャンネル名
(Webhook URLを取得したときに指定したSlackチャンネル名を指定) - ScheduleExpression:
通知したい時間をcron式で指定
(デフォルトはcron(0 0 * * ? *)
としており、日本時間の午前9時 (AM9:00) に通知するスケジュールにしています)
- SlackWebHookURL:
参考:cron 式のリファレンス - Amazon EventBridge
パラメータを指定後、スタックをデプロイします。
通知テスト
最後に、デプロイしたシステムがきちんと動くかテストします。
Lambdaコンソールにアクセスし、デプロイしたLambda関数であるcost-optimization-hub-notification
を選択後、テストタブを選択し"テスト"ボタンを押します。
その後、通知先として設定したSlackに、以下のような通知が届く事を確認できればOKです!
現在自分のアカウントはレコメンデーションがなかったため、「推奨事項なし」が表示されています。 ある場合はこのような文言で推奨事項が表示されるようにLambdaを記述しています。
[ "Cost Optimization HubのEC2に関するコスト最適化の推奨事項を通知", "EC2インスタンス", アカウントID: {AccountID}, 推定削減額: {recommendation['estimatedMonthlySavings']} ]
※こちらの文言はBoto3 APIで値を取得しているため、カスタマイズ可能です。
参考:Cost Optimization Hub - Boto3 1.34.113 documentation
あとは指定時刻(今回の場合はAM9:00)まで待機し、同じようなメッセージが届けばSchedulerの動作もOKです!
さいごに
今回はCost Optimization HubのレコメンデーションをEventBridge Scheduler+Lambdaで定期通知する方法をご紹介しました。
これにより、コスト削減の機会を見逃すことなく、効率的にAWSのコストを最適化することができるようになりましたね!
「でも、推奨されたアクションの実行方法がわからない・・・」
という方は、弊社でコスト最適化のご支援も行なっているので、ぜひご活用下さい!(無料相談もやっています!)
最後までお読みいただきありがとうございました!
どなたかのお役に立てれば幸いです。
以上、おつまみ(@AWS11077)でした!
参考
Cost Optimization Hub - AWS コスト管理
Cost Optimization Hub - Boto3 1.34.113 documentation
新しいコスト最適化ハブは、推奨アクションを一元化してコストを節約します | Amazon Web Services ブログ