Terraform を色々見直している中で、CodePipeline の経過を知らせる仕組みに、CodeStar Notifications を使っている箇所がありました。
今回はそれを EventBridge を使う形式に変更した、というか旧式に戻した、地味なあけおめ回です。
変更理由
Terraform やら機能構成そのものを練り直している際に、もっと簡略化とか最適化できないか考える中で、CodeStar Notifications を使ってチャットに通知している箇所がありました。CodeStar 本体は使っておらず、Notifications だけ使っている状態で、リソース構成も正しいとはいえ複雑気味に感じたので、なんとかならないかなーと調べたところ、そもそも 2024年7月31日 でサポートが終了する旨が、ドキュメントやコンソールに表示されていました。
Amazon では、作業の追跡、コードの開発、アプリケーションのビルド、テスト、デプロイを希望するお客様向けに、 CodeCatalyst ソフトウェアプロジェクトを管理するための効率的な開始プロセスと追加機能を提供しています。
CodeStar 本体は 2017年4月に、Notifications は 2019年11月 にリリースされたようで、この手のサービスにしては長く保った感じでしょうか。
後継となる CodeCatalyst を見る感じ、全く同じような通知機能があるわけではなさそうだし、東京リージョンどころかまだ2リージョンしか対応していないしで、原始的(?)な EventBridge 経由に切り替えることにしました。
構成変更
既存の仕組みは、CodeStar Notifications でイベントをキャッチして → SNS → Lambda → チャットAPI という経路になっていました。Pipeline の通知としては世間一般的にも CodeStar を使う情報がほとんどだったし、Terraform としても1つのリソースで対象イベントを配列で指定するだけだったので、わかりやすいっちゃわかりやすかったです。このキャッチする部分を EventRule に変更し、EventTarget で Lambda へ投げるようにします。Rule の記法が若干複雑になりますが、SNS を省略できるし、EventBridge は今後も長く変わらないと思いますので、その辺をイィ感じに使えるようにしておくのも悪くないなと思います。
基本情報としてはこの辺。Pipeline の構造とイベント、イベントルールの書き方、をにらめっこしてれば構成できます。
- CodePipeline パイプライン構造リファレンス – AWS CodePipeline
- Monitoring CodePipeline events – AWS CodePipeline
- Amazon EventBridge イベントパターンでのコンテンツのフィルタリング – Amazon EventBridge
Terraform
EventRule
大元の CodePipeline と、イベント送信先の Lambda はもう存在するとして、Pipeline のイベントをキャッチするルールを作ります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
resource "aws_cloudwatch_event_rule" "pipeline_examplep_notification" { for_each = ... name = format("%s-notify-pipeline", aws_codepipeline.example[each.key].name) description = format("%s Notify Pipeline", aws_codepipeline.example[each.key].name) event_pattern = jsonencode({ "$or": [ { source = ["aws.codepipeline"] detail-type = ["CodePipeline Pipeline Execution State Change"] resources = [aws_codepipeline.example[each.key].arn], detail = { state = ["STARTED", "SUCCEEDED", "FAILED", "SUPERSEDED", "CANCELED", "RESUMED"], } }, { source = ["aws.codepipeline"] detail-type = ["CodePipeline Stage Execution State Change"] resources = [aws_codepipeline.example[each.key].arn], detail = { stage = ["Approval"], state = ["STARTED", "SUCCEEDED", "FAILED", "CANCELED", "RESUMED"], } } ] }) } |
1つ目のルールは Pipeline そのものの、始まりや終わりをキャッチします。あえて STOPPED を省いているのは、通常の終了時には SUCCEEDED か FAILED が必ず返るわけですが、STOPPED も必ず返ってしまうため、普通はその二重通知は鬱陶しくなるからです。
2つ目は任意のステージをキャッチしており、ここでは承認フェーズを対象としています。他に、アクション単位を設定することも可能です。
このような若干複雑気味な複数のルールになる場合は、別リソースで記述するよりも、$or などを使ってまとめる方が気持ちよいので、記法ドキュメントを眺めておくとよいでしょう。
EventTarget
次に、Lambda を送り先とします。rule に引っかかったら arn に送っています。この例では諸事情で文字列にしていますが、aws_lambda_function から値を引っ張れるならそうしましょう。
1 2 3 4 5 6 7 |
resource "aws_cloudwatch_event_target" "pipeline_example_notification" { for_each = ... target_id = "pipeline-notify" arn = "arn:aws:lambda:${local.region}:${local.service_account_id}:function:pipeline-notify" rule = aws_cloudwatch_event_rule.pipeline_example_notification[each.key].name } |
LambdaPermission
EventBridge から Lambda を呼び出すためのオマジナイです。
1 2 3 4 5 6 7 8 9 10 |
resource "aws_lambda_permission" "pipeline_example_notification" { for_each = ... statement_id = "AllowExecutionFromEvent-${each.key}" action = "lambda:InvokeFunction" principal = "events.amazonaws.com" function_name = "pipeline-notify" source_arn = aws_cloudwatch_event_rule.pipeline_example_notification[each.key].arn } |
これらをセットで使うのは、EventRule を schedule_expression で cron 的に動かすのと同じなので、Rule で色々キャッチできますよってのがわかれば難しくないですね。
Lambda
SNS 経由で Lambda に来るのと、EventBridge から Lambda に来るのとでは、最終的に使う情報は同じであるものの、構成が異なるので書き換えるか、両対応にしておく感じになります。SNS からは配列かつ JSON 文字列できますが、EventBridge からはその同じ内容を直接受け取ります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import json def lambda_handler(event, context): details = [] records = event.get('Records') # SNS経由 detail = event.get('detail') # EventBridge経由 if records: for record in records: sns = record.get('Sns') message = sns.get('Message') msg_json = json.loads(message) details.append(msg_json.get('detail')) else: details.append(detail) for detail in details: ... |
実際には detail 以外のデータや、Pipeline や CloudWatchLogs の URL を作成して通知に含めたりして、通知先にリクエストを送って出来上がりです。
CodeCommit
これは EventBridge を使うべき近場のオマケ例です。単純に CodeCommit の更新をトリガーとして Pipeline を動かす方が、定期的にチェックされるより動作が速いということが1つ。
もう1つはイメージのビルドをミドルウェア・アプリケーションと分けた時、ミドルウェアのビルドはそう頻繁に走るものではなくなり、非アクティブとみなされポーリング無効になってしまうからです。
ということで Terraform としては、まずは Pipeline の Source ステージの configuration で、PollForSourceChanges = false に設定してから以下を追加していきます。
EventRule
特定の CodeCommit リポジトリとブランチの更新をキャッチし、
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
resource "aws_cloudwatch_event_rule" "pipeline_example" { for_each = ... name = format("%s-start-pipeline", aws_codepipeline.example[each.key].name) description = format("%s Start Pipeline", aws_codepipeline.example[each.key].name) event_pattern = jsonencode({ source = ["aws.codecommit"] detail-type = ["CodeCommit Repository State Change"] resources = ["arn:aws:codecommit:${local.region}:${local.service_account_id}:${local.service_name}-${each.value["repository"]}"], detail = { event = ["referenceCreated", "referenceUpdated"], referenceType = ["branch"], referenceName = [each.value["branch"]] } }) } |
EventTarget
ルールに引っかかったら、該当 Pipeline を発火します。こちらは Role が必要なので、codepipeline:StartPipelineExecution を付与した events.amazonaws.com の Role を作成して使用します。
1 2 3 4 5 6 7 |
resource "aws_cloudwatch_event_target" "pipeline_example" { for_each = ... arn = aws_codepipeline.example[each.key].arn rule = aws_cloudwatch_event_rule.pipeline_example[each.key].name role_arn = aws_iam_role.pipeline_eventbridge.arn } |
これで適当に対象ブランチを更新して、Pipeline が動けば成功です。
通知やトリガーの実装1つで、何を使ってどう構成するかを考えなくてはいけないってのが、面倒でもあり、調査と判断が楽しいところでもあります。
今年も地味ながらアウトプットを即開始できたので、もしかしたら鉄拳8と両立できるかもしれないと思いつつ、今年もよろしくお願いします:-)