🦎
AWSの利用料を毎日Slackに通知するシステムをコマンドのみで構築する
使っていないAWSのリソースを削除し忘れを防ぐために、Slackで毎日通知するシステムを構築しました。
このシステムは、AWS CloudFormationを用いてテンプレート化することで、他のチームメンバーにも簡単に共有し再利用できる環境を提供します。
やってみた結果
CloudFormationを使ってインフラをコード化することで、インフラの管理が容易になりました。
コードベースで共有することで、誰もが同じ環境を再現可能にすることが大きなメリットです。
また、将来的にはSlackから直接、使用していないStackを削除できるように拡張することで、さらに管理の効率を上げることが可能だと感じました。
インフラ構成
事前準備
以下のツールが必要です。これらは、スクリプトの実行や環境構築の過程で利用します。
ツール | 説明 |
---|---|
peco | コマンドラインでのインタラクティブフィルタリングツール。 |
awscli | AWSサービスをコマンドラインから操作するためのツール。 |
jq | JSONデータを処理するためのコマンドラインツール。 |
rain | AWS CloudFormationのための開発用ツール。 |
実際に動かしてみる
以下の手順でシステムをセットアップし、動作させます。
- SlackのWebhook URLを取得し、設定ファイルに記入します。
- リポジトリをクローンします。
git clone git@github.com:yoppyDev/iac-templates.git
- 環境変数が記載された
.env
ファイルが必要に応じて修正します。 - シェルスクリプトを実行して環境をデプロイします。
sh utils.sh
コードの説明
aws-billing-to-slack-notification.yaml
aws-billing-to-slack-notification.yaml
# 「aws:cloudformation:stack-name」タグが付与されたリソースのコストを取得し、Slackに通知する
AWSTemplateFormatVersion: '2010-09-09'
Description: Get AWS billing information and send it to Slack
Parameters:
SlackWebhookUrl:
Type: String
Description: Slack Incoming Webhook URL
Resources:
LambdaExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Principal:
Service: "lambda.amazonaws.com"
Action: "sts:AssumeRole"
Policies:
- PolicyName: "lambda-billing-policy"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Action:
- "cloudwatch:GetMetricStatistics"
- "cloudwatch:ListMetrics"
- "ce:GetCostAndUsage"
- "ce:GetCostForecast"
- "cloudformation:ListStacks"
Resource: "*"
- Effect: "Allow"
Action:
- "logs:CreateLogGroup"
- "logs:CreateLogStream"
- "logs:PutLogEvents"
Resource: "arn:aws:logs:*:*:*"
- PolicyName: "EventBridgeLambdaInvocationPolicy"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Action:
- "lambda:InvokeFunction"
Resource: "*"
LambdaFunction:
Type: AWS::Lambda::Function
Properties:
Handler: "index.handler"
Role: !GetAtt LambdaExecutionRole.Arn
Code:
ZipFile: |
import json
import os
import requests
import datetime
import boto3
import logging
import calendar
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def get_all_stack_names():
client = boto3.client('cloudformation', region_name='ap-northeast-1')
paginator = client.get_paginator('list_stacks')
stack_names = []
for page in paginator.paginate(StackStatusFilter=['CREATE_COMPLETE', 'UPDATE_COMPLETE']):
for stack in page['StackSummaries']:
stack_names.append(stack['StackName'])
return stack_names
def get_cost_for_stack(stack_name, start_date, end_date):
client = boto3.client('ce', region_name='ap-northeast-1')
response = client.get_cost_and_usage(
TimePeriod={
'Start': start_date.strftime('%Y-%m-%d'),
'End': end_date.strftime('%Y-%m-%d')
},
Granularity='MONTHLY',
Filter={
'Tags': {
'Key': 'aws:cloudformation:stack-name',
'Values': [stack_name]
}
},
Metrics=["UnblendedCost"]
)
results_by_time = response.get('ResultsByTime')
if results_by_time:
total_cost = sum(float(day['Total']['UnblendedCost']['Amount']) for day in results_by_time)
else:
total_cost = 0
return total_cost
def handler(event, context):
today = datetime.datetime.today()
start_of_month = datetime.datetime(today.year, today.month, 1)
end_of_month = datetime.datetime(today.year, today.month, calendar.monthrange(today.year, today.month)[1])
ce_client = boto3.client('ce', region_name='ap-northeast-1')
forecast_response = ce_client.get_cost_forecast(
TimePeriod={
'Start': today.strftime('%Y-%m-%d'),
'End': end_of_month.strftime('%Y-%m-%d')
},
Metric='UNBLENDED_COST',
Granularity='MONTHLY'
)
client = boto3.client('cloudwatch', region_name='us-east-1')
get_metric_statistics = client.get_metric_statistics(
Namespace='AWS/Billing',
MetricName='EstimatedCharges',
Dimensions=[
{
'Name': 'Currency',
'Value': 'USD'
}
],
StartTime=today - datetime.timedelta(days=1),
EndTime=today,
Period=86400,
Statistics=['Maximum']
)
current_cost = get_metric_statistics['Datapoints'][0]['Maximum']
forecast_cost = float(forecast_response['Total']['Amount'])
stack_names = get_all_stack_names()
messages = [f"当月の現費用:${current_cost:.2f}", f"当月の予測費用:${forecast_cost:.2f}\n"]
for stack_name in stack_names:
cost = get_cost_for_stack(stack_name, start_of_month, end_of_month)
messages.append(f"{stack_name}: ${cost:.2f}")
headers = {'Content-Type': 'application/json'}
response = requests.post(os.environ.get('SLACK_WEBHOOK_URL'), data=json.dumps({'text': "\n".join(messages)}), headers=headers)
return {
'statusCode': response.status_code,
'body': response.text
}
Runtime: "python3.7"
Environment:
Variables:
SLACK_WEBHOOK_URL: !Ref SlackWebhookUrl
Timeout: 30
LambdaPermission:
Type: AWS::Lambda::Permission
Properties:
Action: "lambda:InvokeFunction"
FunctionName: !GetAtt LambdaFunction.Arn
Principal: "events.amazonaws.com"
SourceArn: !GetAtt EventBridgeRule.Arn
EventBridgeRule:
Type: AWS::Events::Rule
Properties:
ScheduleExpression: "cron(0 21 * * ? *)" # 6 AM JST
Targets:
- Arn: !GetAtt LambdaFunction.Arn
Id: "TriggerLambdaDaily"
Input: "{}"
Outputs:
LambdaFunctionArn:
Value: !GetAtt LambdaFunction.Arn
Description: "ARN of the Lambda Function"
Parameters
- SlackWebhookUrl: SlackのIncoming Webhook URLを指定するパラメータです。これにより、Lambda関数がコスト情報をSlackチャンネルに送信できます。
Resources
LambdaExecutionRole
-
概要: Lambda関数がAWSリソースにアクセスするために必要な権限を持つIAMロールです。
-
ポリシー:
-
lambda-billing-policy: AWSコストデータを取得し、CloudWatchからメトリクス情報を取得するためのポリシー。
- cloudwatch:GetMetricStatisticsおよびcloudwatch:ListMetrics: CloudWatchからメトリクス情報を取得するための権限。
- ce:GetCostAndUsageおよびce:GetCostForecast: AWS Cost Explorerを使用してコストデータおよび予測を取得するための権限。
- cloudformation:ListStacks: スタックのリストを取得するための権限。
- ログ関連のアクション: Lambda実行ログをCloudWatch Logsに記録するための権限。
-
EventBridgeLambdaInvocationPolicy: EventBridgeからLambda関数を呼び出すためのポリシー。
- lambda:InvokeFunction: 任意のLambda関数を実行するための権限。
-
LambdaFunction
- 概要: 実際にAWSのコスト情報を取得し、Slackに通知するLambda関数です。
-
実装詳細:
-
get_all_stack_names
: CloudFormationからすべてのスタック名を取得します。 -
get_cost_for_stack
: 指定されたスタックの指定期間内のコストをAWS Cost Explorerから取得します。 -
handler
: 現在の月のコストと予測コストを定期的に取得し、それらをSlackに通知します。
-
- 使用されるAWSサービス: Boto3ライブラリを使用し、CloudFormation、AWS Cost Explorer、CloudWatchを操作します。
-
環境変数:
SLACK_WEBHOOK_URL
を利用し、通知を送信するSlackのWebhook URLを設定します。
LambdaPermission
- 概要: Lambda関数をEventBridgeから呼び出す権限を付与します。
-
設定:
- Action: "lambda:InvokeFunction"。EventBridgeからLambda関数を実行するための権限。
- Principal: "events.amazonaws.com"。EventBridgeが実行するためのプリンシパル。
- SourceArn: EventBridgeのルールのARNを指定します。
EventBridgeRule
- 概要: AWS EventBridgeを使用し、Lambda関数を毎日UTCの21時(日本時間で翌日の6時)に自動的にトリガーします。
-
設定: スケジュール式は
cron(0 21 * * ? *)
に設定し、Lambda関数を定期実行します。
おわりに
このプロジェクトを通じて、インフラをコードとして管理する重要性を改めて実感しました。
これからも、インフラ管理の自動化と最適化を進め、運用の効率化を図っていきたいと思います。
今回作ったリポジトリは以下です。ぜひご参照ください。
Discussion