概要
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
{
"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のコンソールのコード
の一番下にあるレイヤーのレイヤーの追加
を押します
ARNを指定
を選択し、requestsのArnを直接指定します
以下のようにrequestsのLayerが追加されたら成功です
コードの実装
GitHub Actionsの使用状況をSlackで通知するロジックをlambda_function.py内に記載します
仕組みは簡単でGitHubのAPIから無料枠と当月の使用時間を取得し、当月の使用時間/無料枠の値ごとにSlackへ通知するメッセージを変えてます
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の使用量の通知に成功しました"
環境変数の設定
設定>環境変数から環境変数のキーと値を設定します
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を実行します
以下のようにSlackの通知が送信されたら成功です
EventBridgeの設定
EventBridgeを使ってJSTの平日朝10時にLambdaを実行するよう指定します
以下のように設定できたら成功です
環境変数をパラメータストアで管理したい時
Lambda内の環境変数をよりセキュアに管理したいのでパラメータストアから参照するようにします
環境変数の作成
環境変数をパラメータストア内に作成していきます
Lambdaの修正
以下のようにパラメータストアの環境変数を取得するよう修正します
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を追加します
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バケットを先に作成します
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ファイルを格納します
LambdaとEvent Bridgeの作成
以下のようにLambdaとEvent Bridgeを作成します
S3内のzipファイルからデプロイするので
- LambdaArchiveBucketName(バケット名)
- LambdaArchiveBucketObjectKey(zipファイルのパス、今回だとnotice-github-actions-usage/lambda_function.py.zip)
をパラメータに指定します
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
以上です
参考