2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

お題は不問!Qiita Engineer Festa 2024で記事投稿!
Qiita Engineer Festa20242024年7月17日まで開催中!

AWS LambdaとEventBridgeを使ってOrganization内のGitHub Actionsの使用状況をSlackで通知するには

Last updated at Posted at 2024-06-18

概要

AWS LambdaとEventBridgeを使ってOrganization内のGitHub Actionsの使用状況をSlackで通知する方法について解説します
記事の後半ではCloudFormationを使ってLambdaとEventBridgeを構築する方法についても解説します

前提

  • Pythonを使用
  • GitHubのAPIを使用するため、GitHub Tokenを発行済み
  • Incomming Webhookを作成済み

Organization内のGitHub Actionsの使用状況を取得するAPI

今回は以下のAPIを使って毎月の無料枠(分)と当月使用したGitHub Actionsの使用時間(分)を取得します
orgにはOrganization名を指定します

GET /orgs/{org}/settings/billing/actions
response
{
  "total_minutes_used": 305,
  "total_paid_minutes_used": 0,
  "included_minutes": 3000,
  "minutes_used_breakdown": {
    "UBUNTU": 205,
    "MACOS": 10,
    "WINDOWS": 90
  }
}

Lambdaの設定

本Lambdaを実装するには

  • Layer
  • Lambda本体のソースコード
  • 環境変数
  • IAM Role

が必要になるので順番に解説します

Layerの指定

Pythonのrequestsなどの外部パッケージを使用する際はLayerを設定する必要があります
今回は以下のリポジトリからLayerのArnを取得します

{
    "packageVersion": "2.32.3",
    "arn": "arn:aws:lambda:ap-northeast-1:770693421928:layer:Klayers-p312-requests:5",
    "package": "requests"
}

Layerを追加する際はLambdaのコンソールのコードの一番下にあるレイヤーのレイヤーの追加を押します

スクリーンショット 2024-06-18 9.55.39.png

ARNを指定を選択し、requestsのArnを直接指定します
スクリーンショット 2024-06-18 9.54.05.png

以下のようにrequestsのLayerが追加されたら成功です
スクリーンショット 2024-06-06 9.44.20.png

コードの実装

GitHub Actionsの使用状況をSlackで通知するロジックをlambda_function.py内に記載します
仕組みは簡単でGitHubのAPIから無料枠と当月の使用時間を取得し、当月の使用時間/無料枠の値ごとにSlackへ通知するメッセージを変えてます

lambda_function.py
import json
import logging
import os

import requests

def lambda_handler(event, context):
    headers = {"Authorization": f"Bearer {os.environ.get("GITHUB_TOKEN")}"}
    url = os.environ.get("GITHUB_ACTIONS_BILLING_API")
    response = json.loads(
        requests.get(url, headers=headers).content.decode()
    )
    total_minutes_used = response["total_minutes_used"]
    included_minutes = response["included_minutes"]
    usage = total_minutes_used / included_minutes * 100
    if usage >= 80:
        usage_msg = f":alert: 現在のGitHub Actionsの使用状況は{usage}%です\n 不要なワークフローがないか見直しましょう"
    else:
        usage_msg = f"現在のGitHub Actionsの使用状況は{usage}%です"
    url = os.environ.get("NOTICE_GITHUB_ACTIONS_USAGE_SLACK_WEBHOOK_URL")
    post_json = {'text': usage_msg}
    requests.post(url, data = json.dumps(post_json))
    return "GitHub Actionsの使用量の通知に成功しました"


環境変数の設定

設定>環境変数から環境変数のキーと値を設定します

スクリーンショット 2024-06-18 10.26.13.png

IAM Role

Lambda用のIAM Roleを作成します

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": "logs:CreateLogGroup",
            "Resource": "arn:aws:logs:ap-northeast-1:XXXXXXXXXXXX:*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:ap-northeast-1:XXXXXXXXXXXX:log-group:/aws/lambda/notice-github-actions-usage:*",
            "Effect": "Allow"
        }
    ]
}

実際に実行してみよう!

コンソールからLambdaを実行します

スクリーンショット 2024-06-18 12.59.37.png

以下のようにSlackの通知が送信されたら成功です

  • 80%を下回った時
    スクリーンショット 2024-06-06 9.48.51.png

  • 80%を超えた時
    スクリーンショット 2024-06-06 10.41.59.png

EventBridgeの設定

EventBridgeを使ってJSTの平日朝10時にLambdaを実行するよう指定します

スクリーンショット 2024-06-06 9.43.41.png

以下のように設定できたら成功です

スクリーンショット 2024-06-06 9.43.59.png

環境変数をパラメータストアで管理したい時

Lambda内の環境変数をよりセキュアに管理したいのでパラメータストアから参照するようにします

環境変数の作成

環境変数をパラメータストア内に作成していきます

スクリーンショット 2024-06-18 13.02.35.png

Lambdaの修正

以下のようにパラメータストアの環境変数を取得するよう修正します

lambda_function.py
import json
import logging
import os

import requests
import urllib

def lambda_handler(event, context):
    token = getParameterStoreValue(os.environ.get("GITHUB_TOKEN"))
    headers = {"Authorization": f"Bearer {token}"}
    url = getParameterStoreValue(os.environ.get("GITHUB_ACTIONS_BILLING_API"))
    response = json.loads(
        requests.get(url, headers=headers).content.decode()
    )
    total_minutes_used = response["total_minutes_used"]
    included_minutes = response["included_minutes"]
    usage = total_minutes_used / included_minutes * 100
    if usage >= 80:
        usage_msg = f":alert: 現在のGitHub Actionsの使用状況は{usage}%です\n 不要なワークフローがないか見直しましょう"
    else:
        usage_msg = f"現在のGitHub Actionsの使用状況は{usage}%です"
    url = getParameterStoreValue(os.environ.get("NOTICE_GITHUB_ACTIONS_USAGE_SLACK_WEBHOOK_URL"))
    post_json = {'text': usage_msg}
    requests.post(url, data = json.dumps(post_json))
    return "GitHub Actionsの使用量の通知に成功しました"

def getParameterStoreValue(parameter_path: str):
    """パラメータストアに設定されている値を取得する
    パラメータストアのパスを指定して、対応する設定値を取得する
    前提:
        - AWS-Parameters-and-Secrets-Lambda-Extension レイヤーが存在する
    参考:
        - https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/ps-integration-lambda-extensions.html
    Args:
        parameter_path (str): SSMパラメータストアのパス
    Returns:
        _type_: パラメータストアに登録されている値
    """
    port = "2773"
    encoded_parameter_path = urllib.parse.quote_plus(parameter_path)
    parameter_store_url = (
        "http://localhost:"
        + port
        + "/systemsmanager/parameters/get/?name="
        + encoded_parameter_path
        + "&withDecryption=true"
    )
    aws_session_token = os.environ.get("AWS_SESSION_TOKEN")
    headers = {"X-Aws-Parameters-Secrets-Token": aws_session_token}
    r = requests.get(parameter_store_url, headers=headers)
    return json.loads(r.text)["Parameter"]["Value"]

Layerの追加

パラメータストア用のLayerを追加します

スクリーンショット 2024-06-18 13.05.30.png

IAM Roleの修正

IAM Roleにパラメータストアの取得とKMSによる複合化の権限を付与します

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "",
            "Effect": "Allow",
            "Action": [
                "ssm:GetParameter",
                "kms:Decrypt"
            ],
            "Resource": [
                "arn:aws:ssm:ap-northeast-1:XXXXXXXXXXXX:parameter/GITHUB_TOKEN",
                "arn:aws:ssm:ap-northeast-1:XXXXXXXXXXXX:parameter/GITHUB_ACTIONS_BILLING_API",
                "arn:aws:ssm:ap-northeast-1:XXXXXXXXXXXX:parameter/NOTICE_GITHUB_ACTIONS_USAGE_SLACK_WEBHOOK_URL",
                "arn:aws:kms:ap-northeast-1:XXXXXXXXXXXX:key/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
            ]
        }
    ]
}

CloudFormationを使って構築

CloudFormationを使うことでLambdaとEvent Bridgeをコードで管理することができます
今回は

  • Lambdaを格納するS3バケット
  • LambdaとEventBridge

用のテンプレートを作成します

Lambdaを格納するS3バケット

LambdaをCloudFormationで作成する際に必要なので以下のようにLambdaを格納するS3バケットを先に作成します

s3-factory-stacksets.yml
AWSTemplateFormatVersion: 2010-09-09
Description: "S3 Bucket Stack For Account Setup"

# -------------------------------------
# Metadata
# -------------------------------------
Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: "Project Configuration"
        Parameters:
          - ProjectName

# -------------------------------------
# Parameters
# -------------------------------------
Parameters:
  ProjectName:
    Description: "Enter the project name (ex: my-project)"
    Type: String
    MinLength: 1
    ConstraintDescription: "ProjectName must be entered"
    Default: my-project

# -------------------------------------
# Resources
# -------------------------------------
Resources:
  # -------------------------------------
  # S3
  # -------------------------------------
  # For Lambda Archive
  LambdaArchiveBucket:
    DeletionPolicy: Retain
    UpdateReplacePolicy: Retain
    Type: AWS::S3::Bucket
    Properties:
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      BucketName: !Sub ${ProjectName}-lambda-archive-${AWS::Region}
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      VersioningConfiguration:
        Status: Enabled
      LifecycleConfiguration:
        Rules:
          - Id: ExpiresAfter365DaysFor4thOlderVersion
            Status: Enabled
            NoncurrentVersionExpiration:
              NewerNoncurrentVersions: 3
              NoncurrentDays: 365

# -------------------------------------
# Outputs
# -------------------------------------
Outputs:
  LambdaArchiveBucketName:
    Value: !Ref LambdaArchiveBucket
  LambdaArchiveBucketArn:
    Value: !GetAtt  LambdaArchiveBucket.Arn

作成完了後、notice-github-action-usageのフォルダ内にLambda関数を圧縮したzipファイルを格納します

スクリーンショット 2024-06-18 13.07.13.png

LambdaとEvent Bridgeの作成

以下のようにLambdaとEvent Bridgeを作成します
S3内のzipファイルからデプロイするので

  • LambdaArchiveBucketName(バケット名)
  • LambdaArchiveBucketObjectKey(zipファイルのパス、今回だとnotice-github-actions-usage/lambda_function.py.zip)

をパラメータに指定します

notice-github-actions-usage.yml
AWSTemplateFormatVersion: 2010-09-09
Description: "Lambda Function Stack For Notice GitHub Actions Usage"

# -------------------------------------
# Metadata
# -------------------------------------
Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: "Project Configuration"
        Parameters:
          - ProjectName
      - Label:
          default: "EventBridge Configuration"
        Parameters:
          - EventBridgeRuleNameForNoticeGitHubActionsUsage
      - Label:
          default: "Lambda Configuration"
        Parameters:
          - LambdaArchiveBucketName
          - LambdaArchiveBucketObjectKey
          - PythonRequestsLambdaExtensionArn
          - ParametersSecretsLambdaExtensionArn
          - LambdaHandler
          - LambdaMemorySize
          - LambdaTimeout
          - LambdaRuntime

# -------------------------------------
# Parameters
# -------------------------------------
Parameters:
  ProjectName:
    Description: "Enter the project name (ex: my-project)"
    Type: String
    MinLength: 1
    ConstraintDescription: "ProjectName must be enter"
    Default: my-project
  EventBridgeRuleNameForNoticeGitHubActionsUsage:
    Description: "Enter the EventBridge rule name of Lambda function for notice github actions usage (ex: notice-github-actions-daily-jst-1000)"
    Type: String
  LambdaArchiveBucketName:
    Type: String
    Description: "Enter the S3 Bucket name for Lambda zip archive"
  LambdaArchiveBucketObjectKey:
    Type: String
    Description: "Enter the S3 Bucket object key for Lambda zip archive"
  PythonRequestsLambdaExtensionArn:
    Type: String
    Description: "Enter the Python 3.12 Request Module Extension ARN"
    Default: arn:aws:lambda:ap-northeast-1:770693421928:layer:Klayers-p312-requests:5
  ParametersSecretsLambdaExtensionArn:
    Type: String
    Description: "Enter the Lambda Extension ARN for AWS Parameters and Secrets"
    Default: arn:aws:lambda:ap-northeast-1:133490724326:layer:AWS-Parameters-and-Secrets-Lambda-Extension:11
  LambdaHandler:
    Type: String
    Description: "Enter the Lambda function handler (default: lambda_function.lambda_handler)"
    Default: lambda_function.lambda_handler
  LambdaMemorySize:
    Type: Number
    Description: "Enter the Lambda function memory size (MiB) (default: 128)"
    Default: 128
    MinValue: 128
    MaxValue: 10240
  LambdaTimeout:
    Type: Number
    Description: "Enter the Lambda function timeout second (default: 30)"
    Default: 30
    MinValue: 1
    MaxValue: 900
  LambdaRuntime:
    Type: String
    Description: "Enter the Lambda function runtime (default: python3.12)"
    AllowedValues:
      - python3.12
    Default: python3.12

# -------------------------------------
# Resources
# -------------------------------------
Resources:
  # -------------------------------------
  # Lambda Function
  # -------------------------------------
  NoticeGitHubActionsUsageLambda:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        S3Bucket: !Ref LambdaArchiveBucketName
        S3Key: !Ref LambdaArchiveBucketObjectKey
      FunctionName: !Sub ${ProjectName}-notice-github-actions-usage
      Description: "GitHub Actionsの利用状況をSlackで通知するための Lambda 関数"
      Handler: !Ref LambdaHandler
      MemorySize: !Ref LambdaMemorySize
      Role: !GetAtt NoticeGitHubActionsUsageLambdaExecutionRole.Arn
      Runtime: !Ref LambdaRuntime
      Timeout: !Ref LambdaTimeout
      Layers:
        - !Ref ParametersSecretsLambdaExtensionArn
        - !Ref PythonRequestsLambdaExtensionArn
      Environment:
        Variables:
          GITHUB_ACTIONS_BILLING_API: GITHUB_ACTIONS_BILLING_API
          GITHUB_TOKEN: GITHUB_TOKEN
          NOTICE_GITHUB_ACTIONS_USAGE_SLACK_WEBHOOK_URL: NOTICE_GITHUB_ACTIONS_USAGE_SLACK_WEBHOOK_URL
      PackageType: Zip
  NoticeGitHubActionsUsageFunctionPermissions:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !Ref NoticeGitHubActionsUsageLambda
      Principal: events.amazonaws.com
      SourceArn: !GetAtt NoticeGitHubActionsUsageEventRule.Arn

  # -------------------------------------
  # EventBridge
  # -------------------------------------
  NoticeGitHubActionsUsageEventRule:
    Type: AWS::Events::Rule
    Properties:
      Name: !Ref EventBridgeRuleNameForNoticeGitHubActionsUsage
      Description: !Sub "毎日 (JST) 10:00 の時間検知 Event をトリガーに Lambda function (${NoticeGitHubActionsUsageLambda}) を起動"
      ScheduleExpression: cron(0 1 ? * MON-FRI *)
      State: ENABLED
      Targets:
        - Arn: !GetAtt NoticeGitHubActionsUsageLambda.Arn
          Id: NoticeGitHubActionsUsageLambda

  # -------------------------------------
  # IAM Role
  # -------------------------------------
  NoticeGitHubActionsUsageLambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub LambdaRoleForNoticeGitHubActionsUsage-${ProjectName}
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      Path: /service-role/
      Policies:
        - PolicyName: !Sub LambdaRoleForNoticeGitHubActionsUsage-${ProjectName}
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action: logs:CreateLogGroup
                Resource: !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*
              - Effect: Allow
                Action:
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource: !Sub
                  - arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${LambdaFunctionName}:*
                  - {LambdaFunctionName: !Sub "${ProjectName}-notice-github-actions-usage"}
              - Effect: Allow
                Action:
                  - ssm:GetParameter
                  - kms:Decrypt
                Resource:
                  - !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/GITHUB_TOKEN
                  - !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/GITHUB_ACTIONS_BILLING_API
                  - !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/NOTICE_GITHUB_ACTIONS_USAGE_SLACK_WEBHOOK_URL
                  - !Sub arn:aws:kms:${AWS::Region}:${AWS::AccountId}:key/XXXXXXXXXXXX:key/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

以上です

参考

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?