AWS

毎月のアカウント利用料を通知する機能をCloudFormationで作ってみた!

cost management
hiropy

はじめに

みなさんこんにちは。最近某コンビニのルイボスティーが大好きなhiropyです。
今回は、「毎月のアカウント利用料を自動でSlackに通知する機能」をCloudFormationを使って実装したのでこちらをご紹介します。

前提

今回は、ControlTower管理下のOU内のアカウントに対してCloudFormationを使ってリソースをデプロイすることを想定しています。

ただし、単一のアカウントに対してスタックでデプロイする場合はControlTowerの設定は必要ありません。

今回は以下のアーキテクチャで実装していこうと思います。

image.png (155.1 kB)

やってみる

早速実装していきましょう!

1. Slackに通知するためのbotを作成する

まずはSlackに通知するためのbotを作成します。

以下のリンクから「Slack API」のページを開き、「Create an app」をクリックします。
https://api.slack.com/

モーダルが表示されるので「From scratch」を選択します。

image.png (245.5 kB)

次の画面でbot名の入力とワークスペースの選択をしてアプリを作成します。

image.png (137.9 kB)

最初の画面で「Permissions」をクリックします。

image.png (213.5 kB)

下にスクロールし、「Scopes」の「Bot token scopes」で「Add an OAuth Scopes」をクリックします。

image.png (132.2 kB)

以下3つの権限を追加してください。

  • chat:write
  • chat:write.public
  • chat:write.customize

次に左メニューの「App Home」からDisplaynameの編集を行います。
画像の「Edit」ボタンをクリックして、DisplayName(Slackで表示されるbot名)、DefaultName(半角英数字)を設定します。

image.png (200.4 kB)

あとはお好みで「Basic Information」からbotの画像などを設定します。

サイドバーで「OAuth & Permissions」を選択し、「Install your Workspace」をクリックしてワークスペースにbotを導入します。

image.png (96.1 kB)

画面にトークンが表示されるのでコピーしておきましょう。

2. 通知するためのStackSetsを作成する

ControlTowerの管理アカウントでStackSetsを作成していきます。

まずは以下のファイルをダウンロードし、s3内の任意のバケットに保存します。
(今回はap-northeast-1に作成してください。CTのデフォルトリージョンが異なる場合は、Lambdaコードのリージョンを指定している箇所を変更してください。)
requests.zip (5.7 MB)

こちらはLambdaで使用するためのレイヤーファイルとなっています。

続いてyamlファイルの内容からコードをコピーし、yamlファイルを作成します。

Q
yamlファイルの内容
AWSTemplateFormatVersion: 2010-09-09

Parameters:
  Token:
    Type: String

Resources:
  LambdaLayer:
    Type: AWS::Lambda::LayerVersion
    Properties:
      CompatibleArchitectures:
        - arm64
        - x86_64
      CompatibleRuntimes:
        - python3.9
      Content:
        S3Bucket: <任意のバケット名>
        S3Key: requests.zip
      LayerName: send-cost-layer
  LambdaFunction:
    Type: AWS::Lambda::Function
    Properties: 
      Handler: index.lambda_handler
      Code:
        ZipFile: !Sub |
          import  os
          from slack_sdk import WebClient
          import boto3
          from datetime import datetime, timedelta, date

          def get_total_billing() -> dict:
              ce = boto3.client('ce', region_name='ap-northeast-1')

              start_date = date.today().replace(day=1).isoformat()
              end_date = date.today().isoformat()

              response = ce.get_cost_and_usage(
                  TimePeriod={
                      'Start': start_date,
                      'End': end_date
                  },
                  Granularity='MONTHLY',
                  Metrics=[
                      'AmortizedCost'
                  ]
              )
              billing = '{:.2f}'.format(float(response['ResultsByTime'][0]['Total']['AmortizedCost']['Amount']))
              return billing

          def lambda_handler(event, context):
              # Web API クライアントを初期化します

              client = WebClient(os.environ["SLACK_BOT_TOKEN"])

              amount = get_total_billing()
              try:
                  # chat.postMessage API を呼び出します
                  response = client.chat_postMessage(
                      channel=f"<任意のチャンネル名>",
                      text=f"今月のサンドボックス利用料をお知らせします :hatching_chick: \n今月の利用料は{amount}USDです。",
                  )
                  print("送信に成功しました")
              except Exception as e:
                  raise e

              return 
      FunctionName: SendMonthlyCostFunction
      Runtime: python3.9
      Environment: 
        Variables:
          SLACK_BOT_TOKEN : !Sub ${Token}
      Layers:
        - !Ref LambdaLayer
      Role: !GetAtt
        - LambdaExecutionRole
        - Arn

  LambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: send-cost-function-execution-role
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: !Sub send-cost-function-policy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Resource: '*'
                Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                  - logs:CreateExportTask
                  - s3:*
                  - ce:GetCostAndUsage
  EventBridgeRule:
    Type: AWS::Events::Rule
    Properties: 
      EventBusName: default
      Name: send-cost-eventbridge-rule
      ScheduleExpression: cron(0 1 L * ? *)
      State: ENABLED
      Targets: 
        - Arn: !GetAtt LambdaFunction.Arn
          Id: LambdaFunction

  PermissionForEventsToInvokeLambda:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !Ref LambdaFunction
      Action: lambda:InvokeFunction
      Principal: events.amazonaws.com
      SourceArn: !GetAtt 'EventBridgeRule.Arn'

今回は「毎月末の10:00に通知する」ような設定にしています。

<任意のバケット名>と書かれているところには、先ほど作成したバケット名を入力します。
<任意のチャンネル名>と書かれているところには、通知を飛ばしたいチャンネル名を設定します(例:#general)。

それではStackSetsをデプロイしていきます。
ControlTowerの管理アカウントからCloudFormationコンソールを開き、StackSetsタブから「Stacksetの作成」をクリックします。

「テンプレートの指定」で先ほど作成したyamlファイルをアップロードします。

image.png (253.8 kB)

Stackset名、説明は任意のものを入力して、パラメータの「Token」でSlack APIのトークンを貼り付けます。

image.png (63.2 kB)

デプロイオプションの設定では、デプロイ先のアカウントIDまたはOU IDを設定し、任意のリージョンを指定します。(今回はap-northeast-1です)

レビュー画面に進み、送信ボタンを押せばOKです!
デプロイが完了するのを待ちましょう。

3. 確認

cronを修正するなどして、今日通知が来るようにすると、画像のようにその月の1日から今日までのアカウント利用料を通知してくれます。

image.png (58.4 kB)

最後に

いかがでしたでしょうか?
サンドボックス環境などを運用していると、その月の利用料は気になってくるところだと思うのでぜひ参考にしていただけますと幸いです!

参考

AUTHOR
hiropy
hiropy
記事URLをコピーしました