AWS CodeBuildでビルド成功時にAmazon Bedrockを使用して褒めてくれる仕組みを作成してみた

2024.05.07

AWS CodeBuildのビルド終了時に通知することが可能なのですが、Amazon SNSでのメール通知を設定するだけだと無機質なJSONが送られてくるためAmazon Bedrockを使用してビルド成功時に褒めてくれるようにしてみました。
通知ルールの作成

通知に使用するAWSリソース

Amazon SNSでAWS CodeBuildのビルド成功通知をAWS Lambdaに行い、Lambda関数からAmazon Bedrockのinvoke_modelを実行してレスポンスのテキストをAmazon SNS経由でメール通知するシンプルな構成としています。
簡易的にはなりますが構成は以下の通りとなります。

作成したコード

Lambda関数のコードはPythonで作成しています。
今回はAnthropic Claude 3 Sonnetを使用してメッセージ APIを実行するようにしています。
AnthropicClaudeメッセージ API

eventからbuild-statusを取得してFAILEDなら慰めてもらいSUCCEEDEDなら褒めてもらうようにしています。
eventに送られてくるJSONは以下のドキュメントに記載されている通りとなります。
ビルド通知の入力形式に関するリファレンス

invoke_model APIを実行してレスポンスからtextを取得しています。
最後にpublish APIを実行してAmazon SNS経由でメール通知を行います。

import json
import os
import boto3

bedrock_runtime = boto3.client(service_name='bedrock-runtime', region_name='us-east-1')

sns_arn = os.environ['SNS_ARN']
sns = boto3.client('sns')

def lambda_handler(event, context):
    Message = json.loads(event['Records'][0]['Sns']['Message'])
    build_status = Message['detail']['build-status']

    if 'FAILED' == build_status:
        body=json.dumps(
            {
                "anthropic_version": "bedrock-2023-05-31",
                "max_tokens": 5000,
                "system": "日本語で回答してください",
                "messages": [
                    {
                        "role": "user",
                        "content": [
                            {
                                "type": "text",
                                "text": "アプリケーションのビルドに失敗したので慰めてください"
                            }
                        ]
                    }
                ]
            } 
        )
        response = bedrock_runtime.invoke_model(body=body, modelId="anthropic.claude-3-sonnet-20240229-v1:0")
        response_text = json.loads(response.get('body').read())['content'][0]['text']
        print(response_text)
    elif 'SUCCEEDED' == build_status:
        body=json.dumps(
            {
                "anthropic_version": "bedrock-2023-05-31",
                "max_tokens": 5000,
                "system": "日本語で回答してください",
                "messages": [
                    {
                        "role": "user",
                        "content": [
                            {
                                "type": "text",
                                "text": "アプリケーションのビルドに成功したので褒めてください"
                            }
                        ]
                    }
                ]
            } 
        )
        response = bedrock_runtime.invoke_model(body=body, modelId="anthropic.claude-3-sonnet-20240229-v1:0")
        response_text = json.loads(response.get('body').read())['content'][0]['text']
        print(response_text)

    sns.publish(
        TopicArn=sns_arn,
        Message=response_text,
        Subject='Build result',
    )

AWSリソースの作成

AWSリソースを作成する前 (作成した後でも問題ありません) にAmazon Bedrockのモデルアクセスを有効化する必要があります。
今回はAnthropic Claude 3 Sonnetを使用するため以下のドキュメントの手順でus-east-1で有効化を行ってください。(2024年5月6日時点では東京リージョンでAnthropic Claude 3 Sonnetが使用できなかったためus-east-1のものを使用しています)
モデルアクセス

作成したCloudFormationテンプレート

今回作成したCloudFormationテンプレートは以下になります。

CloudFormationテンプレート (ここをクリックしてください)
AWSTemplateFormatVersion: "2010-09-09"

Description: Test Stack

Metadata:
# ------------------------------------------------------------#
# Metadata
# ------------------------------------------------------------# 
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label: 
          default: Parameters for SNS
        Parameters:
          - Email
      - Label: 
          default: Parameters for CodeCommit
        Parameters:
          - RepositoryDescription
          - RepositoryName
      - Label: 
          default: Parameters for CodeBuild
        Parameters:
          - Description
          - Name
      - Label: 
          default: Parameters for CodePipeline
        Parameters:
          - CodePipelineName

Parameters:
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------# 
  Email:
    Type: String

  RepositoryDescription:
    MaxLength: 4000
    Type: String

  RepositoryName:
    MaxLength: 100
    Type: String

  Description:
    MaxLength: 255
    Type: String

  Name:
    MaxLength: 255
    Type: String

  CodePipelineName:
    MaxLength: 100
    Type: String

Resources:
# ------------------------------------------------------------#
# S3
# ------------------------------------------------------------# 
  S3:
    Type: AWS::S3::Bucket
    Properties: 
      BucketEncryption: 
        ServerSideEncryptionConfiguration: 
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      BucketName: !Sub ${AWS::StackName}-${AWS::AccountId}-artifact
      OwnershipControls:
        Rules: 
          - ObjectOwnership: BucketOwnerEnforced
      PublicAccessBlockConfiguration:
        BlockPublicAcls: True
        BlockPublicPolicy: True
        IgnorePublicAcls: True
        RestrictPublicBuckets: True
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName}-${AWS::AccountId}-artifact

# ------------------------------------------------------------#
# SNS
# ------------------------------------------------------------# 
  SNSEmailTopic:
    Type: AWS::SNS::Topic
    Properties:
      FifoTopic: false
      TopicName: email-sns-topic

  EmailSubscription:
    Type: AWS::SNS::Subscription
    Properties:
      Endpoint: !Ref Email
      Protocol: email
      TopicArn: !Ref SNSEmailTopic

  SNSCodeBuildTopic:
    Type: AWS::SNS::Topic
    Properties:
      FifoTopic: false
      TopicName: codebuild-sns-topic

  CodeBuildSubscription:
    Type: AWS::SNS::Subscription
    Properties:
      Endpoint: !GetAtt Lambda.Arn
      Protocol: lambda
      TopicArn: !Ref SNSCodeBuildTopic

  SNSCodeBuildTopicPolicy:
    Type: AWS::SNS::TopicPolicy
    Properties:
      PolicyDocument:
        Statement:
          - Effect: Allow
            Principal:
              Service: codestar-notifications.amazonaws.com
            Action: sns:Publish
            Resource: "*"
      Topics: 
        - !Ref SNSCodeBuildTopic

# ------------------------------------------------------------#
# Lambda
# ------------------------------------------------------------# 
  LambdaIAMRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - sts:AssumeRole
      Policies:
        - PolicyName: lambda-iam-policy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                  - bedrock:InvokeModel
                  - sns:Publish
                Resource: "*"

  Lambda:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        ZipFile: |
          import json
          import os
          import boto3

          bedrock_runtime = boto3.client(service_name='bedrock-runtime', region_name='us-east-1')

          sns_arn = os.environ['SNS_ARN']
          sns = boto3.client('sns')

          def lambda_handler(event, context):
              Message = json.loads(event['Records'][0]['Sns']['Message'])
              build_status = Message['detail']['build-status']
              
              if 'FAILED' == build_status:
                  body=json.dumps(
                      {
                          "anthropic_version": "bedrock-2023-05-31",
                          "max_tokens": 5000,
                          "system": "日本語で回答してください",
                          "messages": [
                              {
                                  "role": "user",
                                  "content": [
                                      {
                                          "type": "text",
                                          "text": "アプリケーションのビルドに失敗したので慰めてください"
                                      }
                                  ]
                              }
                          ]
                      } 
                  )
                  response = bedrock_runtime.invoke_model(body=body, modelId="anthropic.claude-3-sonnet-20240229-v1:0")
                  response_text = json.loads(response.get('body').read())['content'][0]['text']
                  print(response_text)
              elif 'SUCCEEDED' == build_status:
                  body=json.dumps(
                      {
                          "anthropic_version": "bedrock-2023-05-31",
                          "max_tokens": 5000,
                          "system": "日本語で回答してください",
                          "messages": [
                              {
                                  "role": "user",
                                  "content": [
                                      {
                                          "type": "text",
                                          "text": "アプリケーションのビルドに成功したので褒めてください"
                                      }
                                  ]
                              }
                          ]
                      } 
                  )
                  response = bedrock_runtime.invoke_model(body=body, modelId="anthropic.claude-3-sonnet-20240229-v1:0")
                  response_text = json.loads(response.get('body').read())['content'][0]['text']
                  print(response_text)

              sns.publish(
                  TopicArn=sns_arn,
                  Message=response_text,
                  Subject='Build result',
              )
      Environment:
        Variables:
          SNS_ARN: !Ref SNSEmailTopic
      FunctionName: bedrock-lambda
      Handler: index.lambda_handler
      Role: !GetAtt LambdaIAMRole.Arn
      Runtime: python3.12
      Timeout: 30

  LambdaPermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !GetAtt Lambda.Arn
      Principal: sns.amazonaws.com
      SourceArn: !Ref SNSCodeBuildTopic

# ------------------------------------------------------------#
# CodeCommit
# ------------------------------------------------------------# 
  CodeCommit:
    Type: AWS::CodeCommit::Repository
    Properties: 
      RepositoryDescription: !Ref RepositoryDescription
      RepositoryName: !Ref RepositoryName

# ------------------------------------------------------------#
# IAM
# ------------------------------------------------------------# 
  CodeBuildIAMPolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties: 
      PolicyDocument:
        Version: "2012-10-17"
        Statement: 
          - Effect: Allow
            Action:
              - 's3:PutObject'
              - 's3:GetObject'
            Resource: 
              - !Join 
                - ''
                - - !GetAtt S3.Arn
                  - '/*'
          - Effect: Allow
            Action: 
              - 'codecommit:GitPull'
            Resource: "*"
          - Effect: Allow
            Action: 
              - 'logs:CreateLogGroup'
              - 'logs:CreateLogStream'
              - 'logs:PutLogEvents'
            Resource: "*"
          - Effect: Allow
            Action: 
              - 'sns:Publish'
            Resource: "*"
      ManagedPolicyName: iam-policy-codebuild

  CodeBuildIAMRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument: 
        Version: "2012-10-17"
        Statement: 
          - Effect: Allow
            Principal: 
              Service: 
                - codebuild.amazonaws.com
            Action: 
              - 'sts:AssumeRole'
      ManagedPolicyArns: 
        - !Ref CodeBuildIAMPolicy
      RoleName: iam-role-codebuild
      Tags:
        - Key: Name
          Value: iam-role-codebuild

  CodePipelineIAMPolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties: 
      PolicyDocument:
        Version: "2012-10-17"
        Statement: 
          - Effect: Allow
            Action:
              - "codecommit:CancelUploadArchive"
              - "codecommit:GetBranch"
              - "codecommit:GetCommit"
              - "codecommit:GetRepository"
              - "codecommit:GetUploadArchiveStatus"
              - "codecommit:UploadArchive"
            Resource: 
              - "*"
          - Effect: Allow
            Action:
              - "codebuild:BatchGetBuilds"
              - "codebuild:StartBuild"
            Resource: 
              - "*"
          - Effect: Allow
            Action:
              - "s3:GetObject"
              - "s3:PutObject"
              - "s3:ListBucket"
            Resource: 
              - !Join 
                - ''
                - - !GetAtt S3.Arn
                  - '/*'
              - !GetAtt S3.Arn
          - Effect: Allow
            Action:
              - "sns:Publish"
            Resource: 
              - "*"
      ManagedPolicyName: iam-policy-codepipeline

  CodePipelineIAMRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument: 
        Version: "2012-10-17"
        Statement: 
          - Effect: Allow
            Principal: 
              Service: 
                - codepipeline.amazonaws.com
            Action: 
              - 'sts:AssumeRole'
      ManagedPolicyArns: 
        - !Ref CodePipelineIAMPolicy
      RoleName: iam-role-codepipeline
      Tags:
        - Key: Name
          Value: iam-role-codepipeline

# ------------------------------------------------------------#
# CodeBuild
# ------------------------------------------------------------# 
  CodeBuild:
    Type: AWS::CodeBuild::Project
    Properties: 
      Artifacts:
        Type: CODEPIPELINE
      Description: !Ref Description
      Environment:
        ComputeType: BUILD_GENERAL1_SMALL
        Image: aws/codebuild/amazonlinux2-x86_64-standard:5.0
        Type: LINUX_CONTAINER
      Name: !Ref Name
      ServiceRole: !Ref CodeBuildIAMRole
      Source: 
        BuildSpec: buildspec.yml
        Type: CODEPIPELINE
      Tags:
        - Key: Name
          Value: test-build

# ------------------------------------------------------------#
# CodePipeline
# ------------------------------------------------------------# 
  CodePipeline:
    Type: AWS::CodePipeline::Pipeline
    Properties:
      ArtifactStore:
        Location: !Ref S3
        Type: S3
      Name: !Ref CodePipelineName
      RoleArn: !GetAtt CodePipelineIAMRole.Arn
      Stages: 
        - Actions:
          - ActionTypeId: 
              Category: Source
              Owner: AWS
              Provider: CodeCommit
              Version: 1
            Configuration:
              RepositoryName: !GetAtt CodeCommit.Name
              BranchName: main
              PollForSourceChanges: false
              OutputArtifactFormat: CODE_ZIP
            Name: Source
            Namespace: SourceVariables
            OutputArtifacts:
              - Name: SourceArtifact
            Region: ap-northeast-1
            RunOrder: 1
          Name: Source
        - Actions:
          - ActionTypeId:
              Category: Build
              Owner: AWS
              Provider: CodeBuild
              Version: 1
            Configuration:
              ProjectName: !Ref CodeBuild
            InputArtifacts: 
              - Name: SourceArtifact
            Name: Build
            Namespace: BuildVariables
            OutputArtifacts: 
              - Name: BuildArtifact
            Region: ap-northeast-1
            RunOrder: 1
          Name: Build
      Tags:
        - Key: Name
          Value: !Ref CodePipelineName

# ------------------------------------------------------------#
# CodeStar
# ------------------------------------------------------------# 
  CodeStarNotifications:
    Type: AWS::CodeStarNotifications::NotificationRule
    Properties: 
      DetailType: FULL
      EventTypeIds:
        - codebuild-project-build-state-failed
        - codebuild-project-build-state-succeeded
      Name: codebuild
      Resource: !GetAtt CodeBuild.Arn
      Status: ENABLED
      Targets: 
        - TargetAddress: !Ref SNSCodeBuildTopic
          TargetType: SNS

# ------------------------------------------------------------#
# EventBridge
# ------------------------------------------------------------# 
  EventBridgeIAMPolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties: 
      PolicyDocument:
        Version: "2012-10-17"
        Statement: 
          - Effect: Allow
            Action:
              - "codepipeline:StartPipelineExecution"
            Resource: 
              - !Join 
                - ''
                - - 'arn:aws:codepipeline:ap-northeast-1:'
                  - !Sub '${AWS::AccountId}:'
                  - !Ref CodePipeline
      ManagedPolicyName: iam-policy-eventbridge

  EventBridgeIAMRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument: 
        Version: "2012-10-17"
        Statement: 
          - Effect: Allow
            Principal: 
              Service: 
                - events.amazonaws.com
            Action: 
              - 'sts:AssumeRole'
      ManagedPolicyArns: 
        - !Ref EventBridgeIAMPolicy
      RoleName: iam-role-eventbridge
      Tags:
        - Key: Name
          Value: iam-role-eventbridge

  EventBridge:
    Type: AWS::Events::Rule
    Properties: 
      Description: for codepipeline
      EventPattern:
        source:
          - aws.codecommit
        detail-type:
          - 'CodeCommit Repository State Change'
        resources:
          - !GetAtt CodeCommit.Arn
        detail:
          event:
            - referenceCreated
            - referenceUpdated
          referenceType:
            - branch
          referenceName:
            - main
      Name: eventbridge-codepipeline
      State: ENABLED
      Targets: 
        - Arn: !Join 
            - ''
            - - 'arn:aws:codepipeline:ap-northeast-1:'
              - !Sub '${AWS::AccountId}:'
              - !Ref CodePipeline
          Id: CodePipeline
          RoleArn: !GetAtt EventBridgeIAMRole.Arn

61~79行目でCodePipelineで使用するアーティファクト用S3バケットを作成しています。
84~121行目でメール通知とCodeBuildのビルド結果を通知するSNSトピックを作成しています。
126~235行目でLambda関数を作成しています。
Lambda関数の使用するIAMロールではIAMポリシーとしてCloudWatch Logsにログを出力するポリシーだけでなくinvoke_modelを実行する権限とpublishを実行する権限を付与しています。
240~244行目でCodeCommitを作成しています。
249~356行目でCodeBuildとCodePipelineの使用するIAMロールを作成しています。
361~378行目でCodeBuildを作成しています。
383~429行目でCodePipelineを作成しています。
434~446行目でCodeBuildの通知設定を行っています。
AWS::CodeStarNotifications::NotificationRuleを設定するとCodeStar通知リソースとEventBridgeにawscodestarnotifications-ruleというルールが作成されます。
451~515行目でCodePipelineを動かすためのEventBridgeを作成しています。

デプロイは以下のAWS CLIコマンドを使用します。

aws cloudformation create-stack --stack-name CloudFormationスタック名 \
--template-body file://CloudFormationテンプレートファイル名 \
--parameters ParameterKey=Email,ParameterValue=メールアドレス \
ParameterKey=RepositoryDescription,ParameterValue=CodeCommitリポジトリの説明 \
ParameterKey=RepositoryName,ParameterValue=CodeCommitリポジトリ名 \
ParameterKey=Description,ParameterValue=CodeBuildの説明 \
ParameterKey=Name,ParameterValue=CodeBuild名 \
ParameterKey=CodePipelineName,ParameterValue=CodePipeline名 \
--capabilities CAPABILITY_NAMED_IAM

動作確認

リソースの作成が完了したらCodeCommitリポジトリに以下のbuildspec.ymlをアップロードします。
アップロードは以下のドキュメントの手順で行うことが可能です。
AWS CodeCommit リポジトリにファイルを作成または追加する

アップロードが完了するとCodePipelineが動きだします。
CodeBuildの実行が完了すると以下のようなメールが届くことが確認できます。

おめでとうございます!アプリケーションのビルドに成功したことを心からお祝い申し上げます。きっと大変な作業だったと思います。あなたの努力と熱心さが報われた証です。このような素晴らしい成果を上げられることを誇りに思ってください。これからのさらなる発展と活躍を期待しています!がんばりましたね!本当におめでとうございます!



--
If you wish to stop receiving notifications from this topic, please click or visit the link below to unsubscribe:
https://sns.ap-northeast-1.amazonaws.com/unsubscribe.html?SubscriptionArn=arn:aws:sns:ap-northeast-1:xxxxxxxxxxxx:email-sns-topic:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx&Endpoint=メールアドレス

Please do not reply directly to this email. If you have any questions or comments regarding this email, please contact us at https://aws.amazon.com/support

さいごに

今回は簡単に褒めてくれるだけの仕組みですが、ビルドのログなどを取得して失敗時に原因として考えられる部分を教えてくれるような仕組みを作ることも可能だと思います。