[アップデート]EventBridgeのクイックスタートでGitHub、Stripe、Twilioとのイベント連携が簡単に作れるようになりました!

EventBridgeのクイックスタートでGitHub、Stripe、Twilioとのイベント連携が簡単に作れるようになりました!実際にGitHubと連携する際の動きを確認しています、
2022.08.13

はじめに

CX事業本部の佐藤智樹です。

今回は、EventBridgeのクイックスタートでSaaS(GitHub、Stripe、Twilio)とAWSサービスの連携が簡単に作れるようになったので紹介します。全体の流れはほぼ共通ですがSaaSごとにシークレットの登録などは異なるため、本記事ではGitHubとの連携を中心的に紹介します。

具体的な構成

CloudFormationによってSaaSの Webhook に対応するLambdaとFunction URLsが生成でき、EventBridgeへの連携が簡単にできるようになります。(画像はAWSコンソールのEventBridgeクイックスタートより引用)

やってみる

それでは実際に動かしてみます。細かい手順は以下に記載されています。

今回はGitHubの例で実際にどのような動きになるのか確認してみます。

GitHub上でシークレットの作成

以下の手順を元に、GitHubのシークレットを作成します。

リポジトリの設定から「Secrets」->「Actions」を選択して、以下の画像のように「New repository secrets」を選択して作成します。

シークレット名はルールを守るように名付けすれば大丈夫です。小文字は全て大文字に変換されます。値はuuidgenコマンドで生成したUUIDなど推察されにくいもので設定します。

ここで生成したSecretの値はメモしておきます。

AWS側でIncomming Webhookの作成

EventBridgeのクイックスタートを開くと以下のように「Lambda fURL を使用したインバウンドウェブフック」という表記がでるので、「使用を開始する」を押下します。

画面をスクロールすると、「CloudFormation スタックテンプレート」に「GitHub」があるので「設定」を押下します。(ここでStripeやTwilioを選択すればそれぞれのSaaS用のテンプレートが使えます)

Webhookの設定画面に移行します。「ステップ 1: イベントバスを選択する」を今回はdefaultのままで変更せず、「ステップ 2: CloudFormation を使用してセットアップする」から「新しいGitHubウェブフック」を押下します。

すると警告画面が出ます。パブリックに公開されるURLが生成されるためセキュリティチームへの確認などが推奨されます。問題なければチェックを入れて、「確認」を押下します。

スタックの展開画面に遷移します。公開されたテンプレートのURLが表示されます。またパラメータにGithubWebhookSecretが書かれているのでここに前の手順で取得したシークレットを設定します。

設定後、チェックなどを確認後つけて「スタックの作成」を押下します。

スタック作成後、以下の「FunctionUrlEndpoint」にIncoming Webhook用のURLが生成されるのでメモします。

展開されるテンプレートとLambdaのコードの中身

展開されるテンプレートは現状以下のようになっています。GitHubのSecretsはパラメータに指定できますが、ちゃんとNoEchoで残らないよう設定されています。リソースとしては、SecretManager、Lambda、CloudWatch Alarmが生成されています。SecretManagerはGitHub Webhookのシークレット保存用、CloudWatch AlarmはLambda URLに過剰なアクセスがあるかの監視、Lambdaは処理の本体になってます。

AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: >
  github-webhook

  Amazon EventBridge Inbound webhooks using lambda fURLs CFNs Template.

Parameters:
  GithubWebhookSecret:
    Type: String
    Description: Github webhook secret
    NoEcho: true
    AllowedPattern: ".+"

  EventBusName:
    Type: String
    Description: EventBridge event bus name
    Default: default

  LambdaInvocationThreshold:
    Type: String
    Description: Innovation Alarm Threshold for number of events in a 5 minute period. 
    Default: 2000

Resources:
  WebhookSecretsManager:
    Type: AWS::SecretsManager::Secret
    Properties:
      Name: !Sub WebhookSecret-${AWS::StackName}
      Description: Secrets Manager for storing Webhook Secret
      SecretString: !Ref GithubWebhookSecret

  LambdaInvocationsAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmDescription: !Sub Alarm for ${AWS::StackName} - InboundWebhook Lambda for traffic spikes
      AlarmName: !Sub InboundWebhook-Lambda-Invocation-Alarm-${AWS::StackName}
      MetricName: Invocations
      Namespace: AWS/Lambda
      Statistic: Sum
      Period: "300"
      EvaluationPeriods: "2"
      Threshold: !Ref LambdaInvocationThreshold
      Dimensions:
        - Name: FunctionName
          Value: !Ref WebhookFunction
      ComparisonOperator: GreaterThanThreshold

  WebhookFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    DependsOn: WebhookSecretsManager
    Properties:
      FunctionName: !Sub [
          "InboundWebhook-Lambda-${ID}",
          ID: !Select [2, !Split ["/", !Ref AWS::StackId]],
        ] # Append the stack UUID
      CodeUri:
        Bucket: !Sub 'eventbridge-inbound-webhook-templates-prod-${AWS::Region}'
        Key: 'lambda-templates/github-lambdasrc.zip'
      Handler: app.lambda_handler
      Runtime: python3.7
      ReservedConcurrentExecutions: 10
      Environment:
        Variables:
          GITHUB_WEBHOOK_SECRET_ARN: !Ref WebhookSecretsManager
          EVENT_BUS_NAME: !Ref EventBusName
      MemorySize: 128
      Timeout: 100
      FunctionUrlConfig:
        AuthType: NONE
      Policies:
        - EventBridgePutEventsPolicy:
            EventBusName: !Ref EventBusName
        - Version: "2012-10-17"
          Statement:
            - Effect: Allow
              Action:
                - secretsmanager:DescribeSecret
                - secretsmanager:GetSecretValue
              Resource: !Ref WebhookSecretsManager

Outputs:
  FunctionUrlEndpoint:
    Description: "Webhhook Function URL Endpoint"
    Value: !GetAtt WebhookFunctionUrl.FunctionUrl

  LambdaFunctionName:
    Value: !Ref WebhookFunction

  LambdaFunctionARN:
    Description: Lambda function ARN.
    Value: !GetAtt WebhookFunction.Arn

Lambdaに関しても、CodeUriのURLからコードが確認できます。少し長くなるのでapp.handlerと実際のリクエストの整形の部分のみ記載します。コード量は多くないですが、細かいツールが増えると保守が大変なので今回のクイックスタートに任せたり、このコードの内容を参考に独自で拡張して作ることも可能かと思います。

app.py

~~
def lambda_handler(event, _context):
    """Webhook function"""
    headers = event.get('headers')

    # Input validation
    try:
        json_payload = get_json_payload(event=event)
    except ValueError as err:
        print_error(f'400 Bad Request - {err}', headers)
        return {'statusCode': 400}
    except BaseException as err:  # Unexpected Error
        print_error('500 Internal Server Error\n' +
                    f'Unexpected error: {err}, {type(err)}', headers)
        return {'statusCode': 500}

    detail_type = headers.get('x-github-event', 'github-webhook-lambda')
    try:
        if not contains_valid_signature(event=event):
            print_error('401 Unauthorized - Invalid Signature', headers)
            return {'statusCode': 401}

        response = forward_event(json_payload, detail_type)

        if response['FailedEntryCount'] > 0:
            print_error('500 Internal Server Error - Failed to forward message to EventBridge\n' +
                        str(response['Entries'][0]), headers)
            return {'statusCode': 500}

        return {'statusCode': 202}

    except BaseException as err:  # Unexpected Error
        print_error('500 Internal Server Error\n' +
                    f'Unexpected error: {err}, {type(err)}', headers)
        return {'statusCode': 500}
~~
def forward_event(payload, detail_type):
    """Forward event to EventBridge"""
    return event_bridge_client.put_events(
        Entries=[
            {
                'Source': 'github.com',
                'DetailType': detail_type,
                'Detail': payload,
                'EventBusName': event_bus_name
            },
        ]
    )

GitHub Webhook の設定

話を画面の設定に戻します。GitHubを開いてWebhookを設定します。公式の情報だと以下が参考になります。

作成したGitHubのシークレットとFunctionのURLを設定します。設定後今回は、issueに反応してWebhookを送信するように設定してみます。

適当なissueをGitHubのリポジトリ内で作成してみます。

LambdaのCloudWatch Dashboardを確認するとLambdaが実行されたことが分かります。Webhook作成時に1回実行されるので合計2回Lambdaが実行されてます。イベントの内容は異なるので反応させたいイベントをEventBridgeのルールで設定すれば大丈夫です。

EventBridgeまで通知がされているのか確認するため、EventBridgeのルールを作成してます。今回はEventBridgeにルールに該当するものがあればイベントをSNS経由でメールに送信するよう設定します。先にSNSの画面でトピックを作成してください。

作成後、「サブスクリプションの設定」からプロトコルをEメールで選択し、エンドポイントに受信可能なメールアドレスを設定します。設定後、SNSからメールアドレス宛に「AWS Notification - Subscription Confirmation」という件名のメールが来るので、「Confirm subscription」を押下して許可設定を行います。 これで受信の準備は完了です。

次はEventBridgeのルールの「ルールを作成」から以下のように適当なルール名をつけます。

イベントソースを「その他」で設定し、「サンプルイベント」に以下を設定してみます。

{
  "version": "0",
  "id": "d85hoge-fugo-3712-abxx-1234567ee4e9",
  "detail-type": "issues",
  "source": "github.com",
  "account": "123456789012",
  "time": "2019-12-05T16:50:53Z",
  "region": "ap-northeast-1",
  "resources": [],
  "detail": {}
}

次にイベントパターンで「ご自身名前を入力」を選択し以下を設定します。こちらが実際にイベントを引っ掛ける際のルールになります。テストパターンのボタンから設定がうまくいっているか確認できます。

{
  "source": ["github.com"]
}

例ですが以下のようなイベントにすればissueのオープンのイベントに対してマッチングしてターゲットの実行ができます。

{
  "source": ["github.com"],
  "detail-type": ["issues"],
    "detail": {
    "action": ["opened"]
  }
}

ターゲットタイプにSNSを選択し、先ほど作成したトピックを設定します。ターゲットを変更すればCodePiplineやLambdaの実行などができます。後の設定はデフォルトのままで完了まで進めます。

上記で設定は完了です。再度GitHub上でissueを作成すると以下のようなイベントがメールに送信されてきます。

{
  "version": "0",
  "id": "d85hoge-ebae-1694-76d4-1234567ee4e9",
  "detail-type": "issues",
  "source": "github.com",
  "account": "123456789012",
  "time": "2022-08-12T14:36:07Z",
  "region": "ap-northeast-1",
  "resources": [],
  "detail": {
    "action": "opened",
    "issue": {
      "url": ""
      ...
    }
  }
}

おまけ:今までのGitHubとCodePipeline

個人的になぜこのアップデートが良かったのかについて記載してます。AWSとGitHubの連携は、ソースをビルド/デプロイするためCodeBuildとCodePipelineとの連携がメインになってたかと考えます。ここではCodePipelineとの関係に注目します。CodePipelineとGitHubを接続するためにはGitHub認証のv2かv1を使う必要があります。v2では基本的なイベントであるマージやプッシュなどによるイベントフックには対応していますが、細かいイベント(PRの作成、タグ作成など)によるパイプラインの実行ができませんでした。

v1の場合は、設定することである程度細かいイベントによる実行はできました。しかしながら、モノレポなどで特定のディレクトリの変更にだけ反応させたいなどさらに細かいイベントに対応したい場合は対応できませんでした。なので、以下のようにAPI GatewayにGitHubのWebhookを送る方法がブログに書かれていました。

ただ細かい実装方法やCloudFormationやCDKのテンプレートは公開されていないので実現するのは少し手間でした。今回のクイックスタートを使うとここを始めるためのハードルが下がると期待できます。またEventBridgeと連携することで、単にCodePipelineの実行だけでなく他のサービスも合わせた独自のパイプラインを組むこともできます。

所感

GitHubにメインで注目した内容になりました。CodePipelineなどを使わなければならない環境では、複雑なWebhookが多少作りやすくなり良いアップデートかと思いました。個人的にはEventBridgeのルール作成の方が細かくエラーが出なく難しいのでこちらもテンプレートがあるとありがたいかなと感じます。余談ですがLambda FunctionsのURL機能は、 Lambda fURLと略すみたいです。

Lambda fURLは公開設定になるので、セキュリティポリシーに合わせて使用を検討してください。おそらくですが、認証部分だけ変更すればLambdaの内容は基本使いまわせるかと思うので試してみてください。どなたかの参考になれば幸いです。