こんにちは!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 がグローバルサービスのためです。
CloudFormationテンプレート
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 ブログ