AWS CodePipeline V2のトリガーフィルターをTerraformで扱う

# はじめに
[2023年10月24日に発表されたAWS CodePipelineのV2タイプ](https://aws.amazon.com/jp/about-aws/whats-new/2023/10/aws-codepipeline-parameterized-pipelines/)に対して[2024年2月9日にもトリガーフィルターの新機能](https://aws.amazon.com/jp/about-aws/whats-new/2024/02/codepipeline-trigger-filters-execution-modes/)が追加された。これにより、GitHubやGitLab(セルフマネージド含む)のソースプロバイダに対して、

- リポジトリへのPush契機だけではなく、プルリクエスト発行契機でも作成が可能になる<br>⇒ プルリク時にテストだけ流してパスしたことを確認してからマージすることが容易になる
- ファイルパスで対象ファイルをフィルタすることが可能になる<br>⇒ モノレポで構成されるプロジェクトの特定領域の更新時だけビルドをすることが容易になる

といったことができるようになり、より柔軟にパイプラインが構成できるようになった。

TerraformのAWS Providerも追従してトリガーフィルターが利用可能になっているため、今回は、プルリクエストのフィルタをIaCで作成してみる。

本記事の前提となる知識は、主に以下だ。

- TerraformでAWS CodePipelineを実装したことがある

なお、TerraformのAWS CodePipelineの実装サンプルそのものは[過去の記事](https://qiita.com/neruneruo/items/cda42244ad8e1e2d83b0#terraform%E3%81%AE%E5%AE%9A%E7%BE%A9)で書いているので、実装経験のない人は参考にしていただきたい。

:::note info
トリガーフィルターは、AWS CodePipelineのソースプロバイダがAWS CodeCommitの場合は利用できない。AWS本家のマネージドサービスなのになぜ……という気はするが、ひとまず、外部のソースプロバイダを設定するには[AWS CodeStar Connections改めAWS CodeConnections](https://aws.amazon.com/about-aws/whats-new/2024/03/aws-codeconnections-formerly-codestar-connections/)を設定する必要がある。
AWS CodeConnectionを外部のプロバイダに設定する方法は、[こちらの記事](https://qiita.com/neruneruo/items/1535776e5cab5df115bd)に記載しているので、この設定が済んでいる前提で以降の説明を記載する。
:::

# Terraformでのトリガーフィルター設定方法
パイプラインは以下のように設定をする。
なお、今回は、以下の設定を事前に行っている。

<table>
  <tr><td>プロバイダ</td><td>GitLabセルフマネージド</td></tr>
  <tr><td>グループ</td><td>example_group</td></tr>
  <tr><td>リポジトリ</td><td>example_repository</td></tr>
  <tr><td>マージ先のブランチ</td><td>main</td></tr>
</table>

```diff_HCL
resource "aws_codepipeline" "example" {
  name     = local.codepipeline_pipeline_name
  role_arn = aws_iam_role.codepipeline.arn

+  pipeline_type = "V2"

+  trigger {
+    provider_type = "CodeStarSourceConnection"
+    git_configuration {
+      source_action_name = "Source"
+      pull_request {
+        events = ["OPEN"]
+        branches {
+          includes = ["main"]
+        }
+      }
+    }
+  }

  artifact_store {
    type     = "S3"
    location = aws_s3_bucket.example.bucket
  }

  stage {
    name = "Source"

    action {
      run_order        = 1
      name             = "Source"
      category         = "Source"
      owner            = "AWS"
      provider         = "CodeStarSourceConnection"
      version          = "1"
      output_artifacts = ["SourceArtifact"]
      namespace        = "SourceVariables"

      configuration = {
        ConnectionArn    = aws_codestarconnections_connection.example.arn
        FullRepositoryId = "example_group/example_repository"
        BranchName       = "main"
      }
    }
  }

  stage {
    name = "Build"

    action {
      run_order       = 2
      name            = "Build"
      category        = "Build"
      owner           = "AWS"
      provider        = "CodeBuild"
      version         = "1"
      input_artifacts = ["SourceArtifact"]

      configuration = {
        ProjectName = aws_codebuild_project.example.name
      }
    }
  }
}
```

詳細なトリガーフィルターの仕様は、[AWS公式のAWS CodePipelineユーザーガイド](https://docs.aws.amazon.com/ja_jp/codepipeline/latest/userguide/pipelines-filter.html)と[Terraformドキュメントのtriggerブロックの項](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/codepipeline#trigger)を参照。

プルリクエストは、```events```属性で指定し、```OPEN```, ```UPDATE```, ```CLOSE```の契機でパイプラインを起動することができる。配列なので、OPEN, UPDATE両方を指定しておき、プルリクエストで差し戻しになった後に更新した場合に、再度パイプラインをトリガすることも可能だ。

その他、Pushのフィルタについては、ファイルパス以外に```tags```を指定することでタグでフィルタすることもできる。

ファイルパス、タグ、ブランチ名にはワイルドカードも使えるので、例えば、ブランチ指定で```Feature/*```とすることで、Featureブランチすべてを対象にパイプラインを実行することも可能になる。

# AWS CodeBuildでGitLabのメタ情報を参照する方法
さて、AWS CodeBuildでは、[予約された環境変数](https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/build-env-ref-env-vars.html)を使うことで様々なメタ情報を取得することができる。
せっかくGitLabのプルリクエストが扱えるようになったのだから、GitLabのプルリクエスト関連のAPIを実行して、GitLab側にビルド結果を書き込むことで、マージまでの流れにおける開発者体験が向上するだろう。

ということで、環境変数にどのような情報が設定されるか、以下のようにBuildspecを作成して確認してみよう。

```buildspec.yml
version: 0.2

phases:
  build:
    commands:
      - env | grep CODEBUILD_
```

このBuildspecで実行したAWS Codebuildのログの抜粋は以下。

```
[Container] 2024/03/31 11:55:22.932147 Running command env | grep CODEBUILD_
CODEBUILD_INITIATOR=codepipeline/example-pipeline
CODEBUILD_SRC_DIR=/codebuild/output/srcXXXXXXXXXX/src
CODEBUILD_BUILD_IMAGE=aws/codebuild/amazonlinux2-x86_64-standard:5.0
CODEBUILD_KMS_KEY_ID=arn:aws:kms:ap-northeast-1:XXXXXXXXXXXX:alias/aws/s3
CODEBUILD_GOPATH=/codebuild/output/srcXXXXXXXXXX
CODEBUILD_ACTION_RUNNER_URL=https://codefactory-ap-northeast-1-prod-default-build-agent-executor.s3.ap-northeast-1.amazonaws.com/cawsrunner.zip
CODEBUILD_PROJECT_UUID=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
CODEBUILD_BUILD_ID=example-buildproject:fcd5d542-XXXX-XXXX-XXXX-XXXXXXXXXXXX
CODEBUILD_CI=true
CODEBUILD_RESOLVED_SOURCE_VERSION=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
CODEBUILD_AGENT_ENDPOINT=http://127.0.0.1:7831
CODEBUILD_LAST_EXIT=0
CODEBUILD_BUILD_ARN=arn:aws:codebuild:ap-northeast-1:XXXXXXXXXXXX:build/example-buildproject:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
CODEBUILD_BUILD_NUMBER=6
CODEBUILD_LOG_PATH=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
CODEBUILD_EXECUTION_ROLE_BUILD=true
CODEBUILD_SOURCE_VERSION=arn:aws:s3:::example-bucket/example-codepipelin/SourceArti/XXXXXXX
CODEBUILD_BUILD_URL=https://ap-northeast-1.console.aws.amazon.com/codebuild/home?region=ap-northeast-1#/builds/example-buildproject:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/view/new
CODEBUILD_FE_REPORT_ENDPOINT=https://codebuild.ap-northeast-1.amazonaws.com/
CODEBUILD_BUILD_SUCCEEDING=1
CODEBUILD_BMR_URL=https://CODEBUILD_AGENT:3000
CODEBUILD_AUTH_TOKEN=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
CODEBUILD_CONTAINER_NAME=default
CODEBUILD_START_TIME=1711886110515
```

……なんと、欲しい情報が無い！これではやりたいことが実現できないではないか！

と思ってマネコンを探して、

![キャプチャ1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/d10b06d9-65c5-85d4-4f3a-0a1eaa5b4c90.png)

ここから……

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/532b99e9-f7bb-1562-9128-49af3d1a7e7a.png)

見つけた！

ソースステージの出力に入っているということは、[AWS CodePipelineの変数](https://docs.aws.amazon.com/ja_jp/codepipeline/latest/userguide/reference-variables.html)を使えばパイプラインの他のステージでも取得ができる！

ということで、以下のようにTerraformのコードを見直そう。あるステージの出力を後方のステージで参照する場合、```namespace```の設定をしないと取得できないということに気を付けよう。

```diff_HCL
################################################################################
# CodePipeline                                                                 #
################################################################################
resource "aws_codepipeline" "example" {
  name     = local.codepipeline_pipeline_name
  role_arn = aws_iam_role.codepipeline.arn

  pipeline_type = "V2"

  trigger {
    provider_type = "CodeStarSourceConnection"
    git_configuration {
      source_action_name = "Source"
      pull_request {
        events = ["OPEN"]
        branches {
          includes = ["main"]
        }
      }
    }
  }

  artifact_store {
    type     = "S3"
    location = aws_s3_bucket.example.bucket
  }

  stage {
    name = "Source"

    action {
      run_order        = 1
      name             = "Source"
      category         = "Source"
      owner            = "AWS"
      provider         = "CodeStarSourceConnection"
      version          = "1"
      output_artifacts = ["SourceArtifact"]
+     namespace        = "SourceVariables"

      configuration = {
        ConnectionArn    = aws_codestarconnections_connection.example.arn
        FullRepositoryId = "root/test"
        BranchName       = "main"
      }
    }
  }

  stage {
    name = "Build"

    action {
      run_order       = 2
      name            = "Build"
      category        = "Build"
      owner           = "AWS"
      provider        = "CodeBuild"
      version         = "1"
      input_artifacts = ["SourceArtifact"]

      configuration = {
        ProjectName = aws_codebuild_project.example.name
+       EnvironmentVariables =  jsonencode([
+         {
+           type  = "PLAINTEXT"
+           name  = "CODEPIPELINE_SRCVAR_COMMITID"
+           value = "#{SourceVariables.CommitId}"
+         },
+         {
+           type  = "PLAINTEXT"
+           name  = "CODEPIPELINE_SRCVAR_FULLREPOSITORYNAME"
+           value = "#{SourceVariables.FullRepositoryName}"
+         },
+         {
+           type  = "PLAINTEXT"
+           name  = "CODEPIPELINE_SRCVAR_PULLREQUESTID"
+           value = "#{SourceVariables.PullRequestId}"
+         },
+         {
+           type  = "PLAINTEXT"
+           name  = "CODEPIPELINE_SRCVAR_PULLREQUESTTITLE"
+           value = "#{SourceVariables.PullRequestTitle}"
+         },
+       ])
      }
    }
  }
}
```

これで、Buildspecを以下のように見直す。

```diff_yaml:buildspec.yml
version: 0.2

phases:
  build:
    commands:
-      - env | grep CODEBUILD_
+      - env | grep CODEPIPELINE_SRCVAR
```

AWS CodeBuildのログを取得すると、

```
[Container] 2024/03/31 12:25:27.225154 Running command env | grep CODEPIPELINE_SRCVAR
CODEPIPELINE_SRCVAR_PULLREQUESTID=12
CODEPIPELINE_SRCVAR_PULLREQUESTTITLE=Feature/test3
CODEPIPELINE_SRCVAR_COMMITID=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
CODEPIPELINE_SRCVAR_FULLREPOSITORYNAME=example_group/example_repository
```

取れた！これで、AWS CodeBuildからもGitLabのAPIを実行して情報を書きこむことができるようになった！

# (おまけ)GitLabのマージリクエストに状態を書き込む
AWS CodeBuildからGitLabのAPIを実行して、ビルド状況をリアルタイムにマージリクエストのNotesに反映してくようにしてみよう。

## GitLabのAPIを実行するスクリプト
GitLabの[マージリクエストAPIのドキュメント](https://docs.gitlab.com/ee/api/merge_requests.html)を見ながら作っていく。

:::note info
GitLabのAPIは、Notesへの初回書き込みはPOSTリクエスト、更新はPUTリクエストなので、NotesのIDが渡された場合で動作を変更している。
:::

:::note warn
NoteIdの持ち回りはスクリプトの終了コードを使用しているので、NoteIdが128を超えるとうまく動かなくなる。
実運用で128を超えることが想定される場合は、ファイル出力をする等、別の方法を検討しよう。
:::

:::note warn
エラーハンドリングはしていないので、実際に利用する場合は適宜追記をしていただきたい。
:::

```Python
import json
import requests
import sys

args = sys.argv

print(args)

access_token = args[1]
baseurl      = args[2].replace('https://', '')
reponame     = args[3].replace('/', '%2F') # リポジトリ名はスラッシュが入り得るので、URLエンコードで```%2F```に置換する
mrid         = args[4]

if(len(args) == 6): 
  body = args[5]

  access_url = 'https://' + access_token + '@' + baseurl + '/api/v4/projects/' + reponame + '/merge_requests/' + mrid + '/notes'

  response = requests.post(
    access_url,
    verify=False,
    headers = {
      'Authorization': 'Bearer ' + access_token
    },
    data = {
      'body': body
    },
  )
else:
  noteid = args[5]
  body   = args[6]
 
  access_url = 'https://' + access_token + '@' + baseurl + '/api/v4/projects/' + reponame + '/merge_requests/' + mrid + '/notes/' + noteid

  response = requests.put(
    access_url,
    verify=False,
    headers = {
      'Authorization': 'Bearer ' + access_token
    },
    data = {
      'body': body
    },
  )

response_json = json.loads(response.text)

sys.exit(response_json['id']) # 初回書き込み後は同じNotesを更新していきたいので、終了コードにIDを設定して持ち回れるようにする
```

## CodeBuildからスクリプトを取得できるようにする準備
今回はAmazon S3にスクリプトを入れて置き、AWS CodeBuildから```aws s3 cp```することを想定した作りにする。
なお、CodeBuildに該当バケットへのアクセス権はつけておこう。

```HCL
resource "aws_s3_bucket" "example" {
  bucket = local.s3_bucket_name
}

resource "aws_s3_bucket_ownership_controls" "example" {
  bucket = aws_s3_bucket.example.id

  rule {
    object_ownership = "BucketOwnerEnforced"
  }
}

resource "aws_s3_bucket_public_access_block" "example" {
  bucket = aws_s3_bucket.example.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

resource "aws_s3_object" "example" {
  bucket = aws_s3_bucket.example.id

  source       = "スクリプトへのパス"
  key          = "スクリプト名"

  etag = filemd5("スクリプトへのパス")
}
```

## CodePipelineの準備
CodePipelineでは以下を追加する。
また、GitLabのトークンはシークレット情報なので、AWS Secrets Managerに格納しておこう。

:::note info
今回は紹介しないが、さらに後続のステージでGitLabのNotesを更新できるように、```namespace```を設定しておくと良い。
:::

```diff_HCL
resource "aws_secretsmanager_secret" "example" {
  name = local.secrets_name
}

resource "aws_secretsmanager_secret_version" "example" {
  secret_id     = aws_secretsmanager_secret.example.id
  secret_string = "GitLabで払い出したトークン"
}

resource "aws_codepipeline" "example" {
  # (中略)
  stage {
    name = "Build"

    action {
      run_order       = 2
      name            = "Build"
      category        = "Build"
      owner           = "AWS"
      provider        = "CodeBuild"
      version         = "1"
      input_artifacts = ["SourceArtifact"]
+     namespace       = "BuildVariables"

      configuration = {
        ProjectName = aws_codebuild_project.example.name
        EnvironmentVariables =  jsonencode([
+         {
+           type  = "PLAINTEXT"
+           name  = "CODEPIPELINE_S3_BUCKET_NAME"
+           value = aws_s3_bucket.example.id
+         },
+         {
+           type  = "PLAINTEXT"
+           name  = "CODEPIPELINE_GITLAB_ENDPOINT_URL"
+           value = aws_codestarconnections_host.example.provider_endpoint
+         },
+         {
+           type  = "SECRETS_MANAGER"
+           name  = "CODEPIPELINE_GITLAB_TOKEN"
+           value = aws_secretsmanager_secret.example.name
+         },
          {
            type  = "PLAINTEXT"
            name  = "CODEPIPELINE_SRCVAR_COMMITID"
            value = "#{SourceVariables.CommitId}"
          },
          {
            type  = "PLAINTEXT"
            name  = "CODEPIPELINE_SRCVAR_FULLREPOSITORYNAME"
            value = "#{SourceVariables.FullRepositoryName}"
          },
          {
            type  = "PLAINTEXT"
            name  = "CODEPIPELINE_SRCVAR_PULLREQUESTID"
            value = "#{SourceVariables.PullRequestId}"
          },
          {
            type  = "PLAINTEXT"
            name  = "CODEPIPELINE_SRCVAR_PULLREQUESTTITLE"
            value = "#{SourceVariables.PullRequestTitle}"
          },
        ])
      }
    }
  }
  # (後略)
}
```

## Buildspecの例
AWS CodeBuildのBuildspecは以下のように作成すると良い。
今回のスクリプトは、内部でrequestsモジュールを使用しているため、installフェーズで```pip install```するのを忘れないようにしよう。
```echo test```している行で、実際にテストをするためのコマンド(```npm test```等)を実行することを想定している。

```env.exported-variables```は、ここに書いた変数名でBuildステージの出力をすることができる。
上記の設定とpost_buildフェーズで値を設定することで持ち回りが可能だ。

:::note warn
```NOTEID=$?```は、コマンド行を分けず一息で書く必要がある。CodeBuildは、コマンドの終了コードが0以外の場合にビルドが停止してしまうため、終了コードを退避することと、これにより終了コードを0で上書きする二重の効果を期待する。
:::

```YAML
version: 0.2

env:
  exported-variables:
    - CodeBuildExportNoteId
phases:
  install:
    commands:
      - aws s3 cp s3://${CODEPIPELINE_S3_BUCKET_NAME}/スクリプト名 ./
      - chmod 775 ./スクリプト名
      - pip install requests
  build:
    commands:
      - |
        python ./スクリプト名 ${CODEPIPELINE_GITLAB_TOKEN} ${CODEPIPELINE_GITLAB_ENDPOINT_URL} ${CODEPIPELINE_SRCVAR_FULLREPOSITORYNAME} ${CODEPIPELINE_SRCVAR_PULLREQUESTID} '
        | Phase | Status |
        | --- | --- |
        | test1 | ➖ |
        | test2 | ➖ |
        | Build | ➖ |'; NOTEID=$?
      - echo test1
      - sleep 5
      - |
        python ./スクリプト名 ${CODEPIPELINE_GITLAB_TOKEN} ${CODEPIPELINE_GITLAB_ENDPOINT_URL} ${CODEPIPELINE_SRCVAR_FULLREPOSITORYNAME} ${CODEPIPELINE_SRCVAR_PULLREQUESTID} ${NOTEID} '
        | Phase | Status |
        | --- | --- |
        | test1 | ⭕ |
        | test2 | ➖ |
        | Build | ➖ |'; NOTEID=$?
      - echo test2
      - sleep 5
      - |
        python ./スクリプト名 ${CODEPIPELINE_GITLAB_TOKEN} ${CODEPIPELINE_GITLAB_ENDPOINT_URL} ${CODEPIPELINE_SRCVAR_FULLREPOSITORYNAME} ${CODEPIPELINE_SRCVAR_PULLREQUESTID} ${NOTEID} '
        | Phase | Status |
        | --- | --- |
        | test1 | ⭕ |
        | test2 | ⭕ |
        | Build | ➖ |'; NOTEID=$?
      - echo build
      - sleep 5
      - |
        python ./スクリプト名 ${CODEPIPELINE_GITLAB_TOKEN} ${CODEPIPELINE_GITLAB_ENDPOINT_URL} ${CODEPIPELINE_SRCVAR_FULLREPOSITORYNAME} ${CODEPIPELINE_SRCVAR_PULLREQUESTID} ${NOTEID} '
        | Phase | Status |
        | --- | --- |
        | test1 | ⭕ |
        | test2 | ⭕ |
        | Build | ⭕ |'; NOTEID=$?
  post_build:
    commands:
      - export CodeBuildExportNoteId=${NOTEID}
```

これでマージリクエストを走らせると、Notesに以下のようにビルド結果が表示される。
実際は、更新を書けているので、段々と⭕が増えていくのが見える。

![キャプチャ3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/676eb67d-a08d-ab34-a99f-ca862e405fb6.png)

これで、マージリクエストの結果でいちいちCodeBuildの様子を見に行かなくても状況を把握できるようになる！