TOP>コラム一覧>CodePipeline と CodeBuild で開発のスタイルに合わせた CI/CD 環境実現する ~ S3 Event と Pipeline の Event 編 ~

CodePipeline と CodeBuild で開発のスタイルに合わせた CI/CD 環境実現する
~ S3 Event と Pipeline の Event 編 ~

はじめに

こんにちは、丸山です。

前回のコラムでは、Git-flow のブランチ戦略に対応するワークフローを実現するために CodeBuild を使って GitHub の Push イベントを検出する部分についてご紹介しました。

今回は、Pipeline の起動のトリガーとして使用している S3 Event Notification と CodePipeline の状態変化を EventBridge の Rule で検出して Lambda を起動している部分の解説をします。

前回のコラムは下記を御覧ください。

https://www.ctc-g.co.jp/solutions/cloud/column/article/83.html

構成のおさらい

前回のコラムにも掲載しましたが、今回作成した CI/CD パイプラインの概要図です。

今回着目するのは S3 から Lambda を起動する部分で使用している S3 Event Notification と、CodePipeline の status change に合わせて Lambda を起動するために使用している EventBridge の Rule (旧 CloudWatch Events Rule) についてです。

S3 の Object PUT イベントの取得

今回の構成では、CodeBuild が S3 に対して zip ファイルを PUT するので、そのイベントに連動させて Lambda を起動したいという要件がありました。

現在 S3 の Object PUT イベントを検出してなにか処理を行うためには以下の何れかの方法を取ることができます。

  • CloudTrail + EventBridge Rule
  • S3 Event Notification
  • S3 Event Notification with EventBridge

今回は 1 つの Lambda に S3 の PUT Event を渡すことができれば良いため S3 Event Notification を使用しましたが、同じイベントで複数のサービスに連携したい場合や Event の内容をフィルタリングして使いたい場合などは 3 点目に挙げた S3 Event Notification with EventBridge を使用する必要があります。

AWS の公式のブログに S3 Event Notification with EventBridge の記事がありましたのでリンクを記載しておきます。

https://aws.amazon.com/jp/blogs/news/new-use-amazon-s3-event-notifications-with-amazon-eventbridge/

今回は S3 Event Notification を使用していることは既に説明しましたが、以前から存在している CloudTrail + EventBridge Rule のパターンを採用した場合に Event の内容がどのように設定されるのか気になったので確認してみました。

以下にそれぞれのイベントの例を紹介します。

※ ID などの情報については別の文字列で置き換えておりますので、発生したイベントをそのまま掲載しているわけではありません。

CloudTrail + EventBridge Rule で呼び出された場合の Event

{
    "version": "0",
    "id": "00000000-0000-0000-0000-000000000000",
    "detail-type": "AWS API Call via CloudTrail",
    "source": "aws.s3",
    "account": "account-id",
    "time": "The time, in ISO-8601 format",
    "region": "aws-region",
    "resources": [],
    "detail": {
      "eventVersion": "1.09",
      "userIdentity": {
        "type": "AssumedRole",
        "principalId": "principal-id",
        "arn": "ARN",
        "accountId": "account-id",
        "accessKeyId": "access-key",
        "sessionContext": {
          "sessionIssuer": {
            "type": "Role",
            "principalId": "principal-id",
            "arn": "ARN",
            "accountId": "account-id",
            "userName": "user-name"
          },
          "attributes": {
            "creationDate": "The time, in ISO-8601 format",
            "mfaAuthenticated": "false"
          }
        }
      },
      "eventTime": "The time, in ISO-8601 format",
      "eventSource": "s3.amazonaws.com",
      "eventName": "PutObject",
      "awsRegion": "aws-region",
      "sourceIPAddress": "ip-address",
      "userAgent": "user-agent",
      "requestParameters": {
        "bucketName": "s3-bucket-name",
        "Host": "host-name",
        "key": "latest.zip"
      },
      "responseElements": {
        "x-amz-server-side-encryption": "AES256",
        "x-amz-version-id": "version-id"
      },
      "additionalEventData": {
        "SignatureVersion": "SigV4",
        "CipherSuite": "ECDHE-RSA-AES128-GCM-SHA256",
        "bytesTransferredIn": 0,
        "SSEApplied": "Default_SSE_S3",
        "AuthenticationMethod": "AuthHeader",
        "x-amz-id-2": "amz-id-2",
        "bytesTransferredOut": 0
      },
      "requestID": "request-id",
      "eventID": "event-id",
      "readOnly": false,
      "resources": [
        {
          "type": "AWS::S3::Object",
          "ARN": "arn:aws:s3:::〈bucket-name〉/latest.zip"
        },
        {
          "accountId": "account-id",
          "type": "AWS::S3::Bucket",
          "ARN": "arn:aws:s3:::〈bucket-name〉"
        }
      ],
      "eventType": "AwsApiCall",
      "managementEvent": false,
      "recipientAccountId": "account-id",
      "vpcEndpointId": "vpce-id",
      "eventCategory": "Data",
      "tlsDetails": {
        "tlsVersion": "TLSv1.2",
        "cipherSuite": "ECDHE-RSA-AES128-GCM-SHA256",
        "clientProvidedHostHeader": "host-header"
      }
    }
  }

S3 Event Notification で呼び出された場合の Event

{
    "Records": [
      {
        "eventVersion": "2.2",
        "eventSource": "aws:s3",
        "awsRegion": "us-west-2",
        "eventTime": "The time, in ISO-8601 format, for example, 1970-01-01T00:00:00.000Z, when Amazon S3 finished processing the request",
        "eventName": "event-type",
        "userIdentity": {
          "principalId": "Amazon-customer-ID-of-the-user-who-caused-the-event"
        },
        "requestParameters": {
          "sourceIPAddress": "ip-address-where-request-came-from"
        },
        "responseElements": {
          "x-amz-request-id": "Amazon S3 generated request ID",
          "x-amz-id-2": "Amazon S3 host that processed the request"
        },
        "s3": {
          "s3SchemaVersion": "1.0",
          "configurationId": "ID found in the bucket notification configuration",
          "bucket": {
            "name": "bucket-name",
            "ownerIdentity": {
              "principalId": "Amazon-customer-ID-of-the-bucket-owner"
            },
            "arn": "bucket-ARN"
          },
          "object": {
            "key":"object-key",
            "size":"object-size in bytes",
            "eTag":"object eTag",
            "versionId":"object version if bucket is versioning-enabled, otherwise null",
            "sequencer": "a string representation of a hexadecimal value used to determine event sequence, only used with PUTs and DELETEs"
          }
        },
        "glacierEventData": {
          "restoreEventData": {
            "lifecycleRestorationExpiryTime": "The time, in ISO-8601 format, for example, 1970-01-01T00:00:00.000Z, of Restore Expiry",
            "lifecycleRestoreStorageClass": "Source storage class for restore"
          }
        }
      }
    ]
  }

Event の引用元: https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/notification-content-structure.html

また、今回は実際のイベントは取り上げませんでしたが、S3 Event Notification with EventBridge パターンについては公式のドキュメントに Event のサンプルが掲載されておりましたので下記を参照ください。

https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/ev-events.html

それぞれの Event の詳細を見ると、CloudTrail + EventBridge Rule により呼び出された場合の Event 内容のほうが呼び出し元に関する詳しい情報が含まれていることがわかります。

今回は S3 に PUT されたというトリガーと、どの CodeBuild Container により呼び出されたのかを把握できれば良かったので、設定がより簡単な S3 Event Notification を利用しました。

以下に CDK を使って Event Trigger を設定する例を掲載します。

CloudTrail + EventBridge Ruleの場合

const s3EventTrail = new Trail(this, 'S3EventTrail');

  // sourceCodeStoreBucket は S3 Bucket instance です
  s3EventTrail.addS3EventSelector([
    {
      bucket: props.sourceCodeStoreBucket,
    },
  ]);
  
  const rule = props.sourceCodeStoreBucket.onCloudTrailPutObject('SourceCodeStoreOnPutTrailEvent');
  
  rule.addTarget(new targets.LambdaFunction(pipelineTriggerFunc));

S3 Event Notificationの場合

// sourceCodeStoreBucket は S3 Bucket instance です
  props.sourceCodeStoreBucket.addEventNotification(
    EventType.OBJECT_CREATED_PUT,
    new LambdaDestination(pipelineTriggerFunc)
  );

CDK を使うと暗黙的に設定されるパラメータが存在するため、CloudFormation よりも記載すべきコードの量は減りますが、S3 Event Notification の場合は S3 Bucket とそれにより呼び出される Lambda Function を CDK 上で生成しておけば良いため、非常に簡単です。

CloudTrail + EventBridge Rule の場合は、S3 のイベントも検出するように設定した Trail とそのデータを保管する S3 Bucket が別に必要になりますので、生成されるリソースの量としても S3 Event Notification のほうが有利のようです。
(コード上では sourceCodeStoreBucket しか記載がありませんが、Trail を生成する際に CDK が自動的に S3 Bucket を生成します)

※ CDK による詳細なリソース定義方法については以下の Build note にサンプルコードへのリンクを掲載しておりますので、興味を持っていただけましたら下記もご覧いただければ幸いです。

https://note.com/build_service/n/n2fa797d82270

CodePipeline の状態変化を検出する

あとは CodePipeline で実行されるワークフローの成功/失敗などの Pipeline 状態変化に合わせて Lambda を起動することができれば、GitHub Checks API に CI/CD の結果を送信することができるようになりますので、どのように Lambda をトリガーするのか見てみましょう。

まず CDK のコードからです。

// ciPipeline は CodePipeline の instance
// pipelineFinTriggerFunc は NodejsFunction の instance です
props.ciPipeline.onEvent('PipelineEvent', {
  eventPattern: {
    source: ['aws.codepipeline'],
    detailType: ['CodePipeline Pipeline Execution State Change'],
    detail: {
      state: ['SUCCEEDED', 'FAILED', 'CANCELED', 'SUPERSEDED'],
      pipeline: [props.ciPipeline.pipelineName],
    },
  },
  target: new targets.LambdaFunction(pipelineFinTriggerFunc),
});

CDK の場合は上記のように、CodePipeline の instance に `onEvent` という method が用意されておりますのでこれを使って設定することができます。

ただし、実際に設定しているものは EventBridge の Rule ですので、`onEvent` method を使わずに定義することも可能です。

今回は Pipeline の成功、失敗、キャンセル、中断をトリガーとして GitHub Checks API に結果を連携しますので、detailType と detail はそれぞれ下記のように設定しました。

  • detailType: 'CodePipeline Pipeline Execution State Change'
  • detail.stat: ['SUCCEEDED', 'FAILED', 'CANCELED', 'SUPERSEDED']

CodePipeline の Event を検出する際に使用する detailType, detail で設定できる値については下記のドキュメントを参照してください。

※ 日本語で下記のドキュメントを閲覧すると detailType に設定する値などが日本語に翻訳されてしまい分かりにくくなるため、英語のドキュメントを参照することをおすすめします。

https://docs.aws.amazon.com/codepipeline/latest/userguide/detect-state-changes-cloudwatch-events.html#detect-state-events-types

今回の Pipeline の全容を振り返る

前回の記事と今回のこれまでの説明で GitHub から Pipeline を起動して、結果を GitHub 側に送信することができるようになりましたので、もう一度全体の構成図を見て振り返ってみます。

まず、Git-flow という Git のブランチ戦略に従った開発をする前提で、機能開発に使用する feature ブランチへの Push や Pull Request 作成をトリガーとして CI/CD を実行したいという要件がありました。

CodePipeline 単体では 2024/2 現在、名前の定まっていないブランチへの変更を検出してワークフローをトリガーすることができませんでしたので、CodeBuild と GitHub を Webhook で連携させてこの問題を解決しました。

その後、CodeBuild の処理の中でソースコードを zip ファイルに圧縮して S3 に PUT します。
S3 にファイルが PUT されると S3 Event Notification により Lambda が起動され、Lambda が CodePipeline の実行を開始します。

※ CodePipeline では S3 を Source とした変更検出をサポートしているため、Lambda を使用せずとも Pipeline 実行を開始できますが、今回は Build ID と Pipeline の Execution ID、 GitHub Check の Run ID を紐づける必要があるため Lambda により CodePipeline の開始を行い、各種 ID と実行ステータスは DynamoDB で管理する構成にしています。

EventBridge の Rule (旧 CloudWatch Events Rule) にて CodePipeline の実行状況に応じて Lambda を起動するように設定したので、CodePipeline でワークフローが成功/失敗すると Lambda が起動され、最終的な CI/CD 結果が GitHub 側に連携されます。

まとめ

前回と今回に分けて Git-flow の開発で活用できる CI/CD Pipeline の構築方法についてご紹介しました。

実際に Git-flow を採用して開発を進める場合には develop、main ブランチへの Push をトリガーにした Pipeline も別途必要になりますが、こちらは CodePipeline が標準でサポートしているトリガーを利用できると思いますので今回の記事で紹介した例より簡単に定義できるのではないかと思います。

Git-flow 向けの CI/CD を作るというテーマでご紹介してきましたが、S3 のイベント通知や EventBridge を使用したイベント駆動システムを作る際には今回ご紹介した記事の内容も参考にしていただけるのではないかと思います。

今回紹介した CodeBuild と CodePipeline 組み合わせる例以外にも、具体的なソースコードを掲載しながら AWS のサービスの使い方や案件で得た知見などを Build サービスの note にて公開しています。興味のある方はこちらの記事もご覧いただければ幸いです。

CTCは、AWSのビジネス利活用に向けて、お客様のステージに合わせた幅広い構築・運用支援サービスを提供しています。
経験豊富なエンジニアが、ワンストップかつ柔軟にご支援します。
ぜひ、お気軽にお問い合わせください。

Build サービス note 「Build の AWS ナレッジ通信」

https://note.com/build_service/m/m8220a76aed15

関連サービス

https://www.ctc-g.co.jp/solutions/build_sales/

お問い合わせ



【著者プロフィール】

丸山 貴嗣(まるやま たかし)

伊藤忠テクノソリューションズ株式会社 ソリューションアーキテクト

自社 Cloud サービスの企画開発からプログラマーに転向し、モバイルアプリ、Web アプリ、API 開発など様々な開発プロジェクトに従事。 現在は Build サービスのソリューションアーキテクトとして Cloud を活用したシステム/アプリの設計/開発の領域で活動中。

丸山 貴嗣(まるやま たかし)

TOP>コラム一覧>CodePipeline と CodeBuild で開発のスタイルに合わせた CI/CD 環境実現する ~ S3 Event と Pipeline の Event 編 ~

pagetop