AWS

StackSetsで予算設定・通知機能を作ってみた!

consider about costs.
hiropy

はじめに

みなさんこんにちは。最近椎茸チップスにどハマりしているhiropyです。
早速ですが、現在社内のサンドボックス環境を整備しており、その中で「アカウントごとに予算を設定して、予算を超えそうになったら通知する機能が欲しい」という要望がありました。要望を叶えるべく、CloudFormationのStackSetsを使って実装しましたのでその方法を記事にしたいと思います。

作成するリソース

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

image.png (179.1 kB)

CloudFormationを使用して、以下のリソースをデプロイします。

  • 予算を設定するためのAWS Budgets
  • BudgetsからのアラートをLambdaに送るためのAmazon SNS
  • Slackに通知するためのLambda

前提

以下のリソースの準備ができていることが前提となります。

  • ControlTowerのセットアップが完了していること
  • デプロイ先のOUが存在していること
  • 通知用チャンネルのWebhookが設定されていること

やってみる

それでは早速やっていきましょう!

まずはyamlファイルの内容に書かれているコードをコピーし、手元にyamlファイルを作成してください。

Q
yamlファイルの内容
AWSTemplateFormatVersion: '2010-09-09'
Description: Stack that creates an AWS budget, notifications, and a Lambda function that will shut down EC2 instances

Parameters:
  BudgetAmount:
    Type: Number
    Description: Maximum permissible spend for the month
  Email:
    Type: String
    Description: Email address to deliver notifications to
  WarningThreshold:
    Type: Number
    Description: Percentage of forecast monthly spend for the warning notification
    Default: 80
  WebhookUrl:
    Type: String
    Description: Webhook URL for send message to slack
  SlackUser:
    Type: String
    Description: Name for mension for slack

Outputs:
  BudgetId:
    Value: !Ref Budget

Resources:
  WarningTopic:
    Type: AWS::SNS::Topic

  WarningTopicPolicy:
    Type: AWS::SNS::TopicPolicy
    Properties:
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Action: sns:Publish
            Resource: "*"
            Principal:
              Service: budgets.amazonaws.com
      Topics:
        - !Ref WarningTopic

  Budget:
    Type: AWS::Budgets::Budget
    Properties:
      Budget:
        BudgetLimit:
          Amount: !Ref BudgetAmount
          Unit: USD
        TimeUnit: MONTHLY
        BudgetType: COST
      NotificationsWithSubscribers:
        - Notification:
            NotificationType: ACTUAL
            ComparisonOperator: GREATER_THAN
            Threshold: !Ref WarningThreshold
          Subscribers:
            - SubscriptionType: EMAIL
              Address: !Ref Email
            - SubscriptionType: SNS
              Address: !Ref WarningTopic

  BudgetLambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: /
      Policies:
        - PolicyName: BudgetLambdaExecutionRolePolicy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource: arn:aws:logs:*:*:log-group:/aws/lambda/*-BudgetLambdaFunction-*:*

  BudgetLambdaFunction:
    Type: AWS::Lambda::Function
    Properties:
      Description: Lambda function to be called after a critical budget threshold has been exceeded
      Handler: index.lambda_handler
      Role: !GetAtt BudgetLambdaExecutionRole.Arn
      Runtime: python3.9
      Timeout: 20
      Environment:
        Variables:
          WEBHOOK_URL : !Ref WebhookUrl
          USER : !Ref SlackUser
      Code:
        ZipFile: |
          import boto3
          import os
          import json
          import urllib

          def send_slack():
              user_name = os.environ['USER']
              send_data = {
                  "text": f"<@{user_name}>\nサンドボックス環境が予算を超過しそうです。\nリソースの整理をお願いします。",
              }
              send_text = json.dumps(send_data)
              request = urllib.request.Request(
                  os.environ['WEBHOOK_URL'], 
                  data=send_text.encode('utf-8'), 
                  method="POST"
              )
              with urllib.request.urlopen(request) as response:
                  response_body = response.read().decode('utf-8')
              return 

          def lambda_handler(event, context):
              # TODO implement
              try:
                  send_slack()
              except Exception as e:
                  print(e)
                  raise e

  WarningTopicSubscription:
    Type: AWS::SNS::Subscription
    Properties:
      Protocol: lambda
      TopicArn: !Ref WarningTopic
      Endpoint: !GetAtt BudgetLambdaFunction.Arn

  BudgetLambdaFunctionPermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !Ref BudgetLambdaFunction
      Principal: sns.amazonaws.com
      SourceArn: !Ref WarningTopic

yamlファイルの作成が完了しましたら、ControlTowerの管理者アカウントでCloudFormationコンソールを開き、StackSetsタブを選択してから「StackSetsを作成」をクリックします。

実行ロールなどはそのままにし、「テンプレートの指定」で「テンプレートファイルのアップロード」から先ほどのyamlファイルをアップロードします。

image.png (251.0 kB)

StackSet名や説明は任意のものを入力し、その下に表示される「パラメータの入力」に進みます。

image.png (174.2 kB)

ここがポイントです。
ここに表示される値は、yamlファイルのParameters以下の部分に当たります。

各パラメータについて説明すると、

  • BudgetAmount: 各アカウントに設定する予算を入力します。単位はUSDです。
  • Email: 予算を超過しそうになった時に通知するメールアドレスです。
  • WarningThreshold: 予算のどれくらいの割合を超過した時に通知するかを設定します。値は%で設定します。(例:80)デフォルトで80が指定されています。
  • WebhookUrl: Slackで通知するためのWebhookのURLを指定します。ここを変更することで通知先のチャンネルを変更することができます。
  • SlackUser: メンバーにメンションするためのメンバーIDを設定します。メンバーIDは、メンバーのプロフィールを表示 -> 3点マークをクリック -> Copy memberIDで確認できます。

チャンネル全体に通知したい場合は、yamlファイルの113行目、<@{user_name}><!channel>のように変更してください。

これらのパラメータを入力したら、次の画面に進みます。
「StackSet オプションの設定」は特に変更せず次に進みます。(必要に応じてタグを付与してください)

デプロイオプションの設定では、「アカウント」セクションでアカウント単位でデプロイするかOU単位でデプロイするかを選択できます。

image.png (211.6 kB)

リージョンは任意のものを設定、そのほかの設定は変更せず次へ進みます。

レビュー画面で内容を確認し送信を押すとスタックのデプロイが開始されます。

これでデプロイが完了すれば、アカウントまたはOU内の各アカウントにリソースが作成されます!

確認

デプロイが完了したら動作確認をしてみましょう!
例えば予算を0.01USDにして、価格の高いインスタンスを立ててみます。

すると以下のようにSlackに通知が飛ぶことが確認できるかと思います。

image.png (52.8 kB)

最後に

いかがでしたでしょうか?
CloudFormationを使えば、複数のアカウントに簡単にリソースをデプロイできるので、CT環境管理の一環として、ぜひ活用していただければ幸いです。

参考

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