はじめに
2023年10月24日に発表されたAWS CodePipelineのV2タイプに対して2024年2月9日にもトリガーフィルターの新機能が追加された。これにより、GitHubやGitLab(セルフマネージド含む)のソースプロバイダに対して、
- リポジトリへのPush契機だけではなく、プルリクエスト発行契機でも作成が可能になる
⇒ プルリク時にテストだけ流してパスしたことを確認してからマージすることが容易になる - ファイルパスで対象ファイルをフィルタすることが可能になる
⇒ モノレポで構成されるプロジェクトの特定領域の更新時だけビルドをすることが容易になる
といったことができるようになり、より柔軟にパイプラインが構成できるようになった。
TerraformのAWS Providerも追従してトリガーフィルターが利用可能になっているため、今回は、プルリクエストのフィルタをIaCで作成してみる。
本記事の前提となる知識は、主に以下だ。
- TerraformでAWS CodePipelineを実装したことがある
なお、TerraformのAWS CodePipelineの実装サンプルそのものは過去の記事で書いているので、実装経験のない人は参考にしていただきたい。
トリガーフィルターは、AWS CodePipelineのソースプロバイダがAWS CodeCommitの場合は利用できない。AWS本家のマネージドサービスなのになぜ……という気はするが、ひとまず、外部のソースプロバイダを設定するにはAWS CodeStar Connections改めAWS CodeConnectionsを設定する必要がある。
AWS CodeConnectionを外部のプロバイダに設定する方法は、こちらの記事に記載しているので、この設定が済んでいる前提で以降の説明を記載する。
Terraformでのトリガーフィルター設定方法
パイプラインは以下のように設定をする。
なお、今回は、以下の設定を事前に行っている。
プロバイダ | GitLabセルフマネージド |
グループ | example_group |
リポジトリ | example_repository |
マージ先のブランチ | main |
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ユーザーガイドとTerraformドキュメントのtriggerブロックの項を参照。
プルリクエストは、events
属性で指定し、OPEN
, UPDATE
, CLOSE
の契機でパイプラインを起動することができる。配列なので、OPEN, UPDATE両方を指定しておき、プルリクエストで差し戻しになった後に更新した場合に、再度パイプラインをトリガすることも可能だ。
その他、Pushのフィルタについては、ファイルパス以外にtags
を指定することでタグでフィルタすることもできる。
ファイルパス、タグ、ブランチ名にはワイルドカードも使えるので、例えば、ブランチ指定でFeature/*
とすることで、Featureブランチすべてを対象にパイプラインを実行することも可能になる。
AWS CodeBuildでGitLabのメタ情報を参照する方法
さて、AWS CodeBuildでは、予約された環境変数を使うことで様々なメタ情報を取得することができる。
せっかくGitLabのプルリクエストが扱えるようになったのだから、GitLabのプルリクエスト関連のAPIを実行して、GitLab側にビルド結果を書き込むことで、マージまでの流れにおける開発者体験が向上するだろう。
ということで、環境変数にどのような情報が設定されるか、以下のようにBuildspecを作成して確認してみよう。
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
……なんと、欲しい情報が無い!これではやりたいことが実現できないではないか!
と思ってマネコンを探して、
ここから……
見つけた!
ソースステージの出力に入っているということは、AWS CodePipelineの変数を使えばパイプラインの他のステージでも取得ができる!
ということで、以下のようにTerraformのコードを見直そう。あるステージの出力を後方のステージで参照する場合、namespace
の設定をしないと取得できないということに気を付けよう。
################################################################################
# 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を以下のように見直す。
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のドキュメントを見ながら作っていく。
GitLabのAPIは、Notesへの初回書き込みはPOSTリクエスト、更新はPUTリクエストなので、NotesのIDが渡された場合で動作を変更している。
NoteIdの持ち回りはスクリプトの終了コードを使用しているので、NoteIdが128を超えるとうまく動かなくなる。
実運用で128を超えることが想定される場合は、ファイル出力をする等、別の方法を検討しよう。
エラーハンドリングはしていないので、実際に利用する場合は適宜追記をしていただきたい。
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に該当バケットへのアクセス権はつけておこう。
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に格納しておこう。
今回は紹介しないが、さらに後続のステージでGitLabのNotesを更新できるように、namespace
を設定しておくと良い。
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フェーズで値を設定することで持ち回りが可能だ。
NOTEID=$?
は、コマンド行を分けず一息で書く必要がある。CodeBuildは、コマンドの終了コードが0以外の場合にビルドが停止してしまうため、終了コードを退避することと、これにより終了コードを0で上書きする二重の効果を期待する。
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に以下のようにビルド結果が表示される。
実際は、更新を書けているので、段々と⭕が増えていくのが見える。
これで、マージリクエストの結果でいちいちCodeBuildの様子を見に行かなくても状況を把握できるようになる!