概要
既にある程度の開発が進んでいるBitbucket上のプロジェクトにPipelinesを使って継続的あれこれする機能を追加しようという、解説というよりも私の挑戦の記録です。
前回(その3)はS3にアップロードしたテストレポートをCloudFrontを通してブラウザで閲覧できるようにしました。
今回はブランチやタグごとに別の動作を設定して、masterにタグがpushされたらリリース、を実現したいと思います。
運用形式の決定
BitbucketPipelinesのトリガーは基本、push時または手動です。スケジュールで設定もできますが、ここでは割愛します。
どのブランチにpushされたときにどんな作業が必要になるかを決めることで、様々な作業を自動化できます。
大人数の開発だとfeatureブランチの開発にもプルリクエストを使用したり、デプロイ環境ごとにブランチを切ったりするかと思いますが、ここでは、下記のような運用を想定して設定していきます。
- featuer/*ブランチで開発作業を行う
- featuer/*にpushしてテストを通過したらmasterにプルリクエストを出す
- 担当者がプルリクエストの内容を確認してマージ
- masterブランチに*-SNAPSHOTのタグがpushされたらstagingデプロイ
- masterブランチに*-RELEASEのタグがpushされたらproductデプロイ
masterにマージされるときには必ずテストは完了(通過)しているものと考えれば、テストとその結果の処理は前回までで対応済みです。
JarやJavadocのデプロイについても、アップロード作業そのものはJUnitのレポートの公開の応用です。
これを踏まえて、bitbucket-pipelines.ymlを編集していきます。
デプロイの設定の共通化
デプロイの手順はaws-s3-deployパイプラインを使用してアーティファクトをS3にアップロードし、CloudFrontでのURLをビルドステータスAPIに追加するというものでした。
これの流れそのものは、前述のstagingデプロイやproductデプロイでも使用できます。
環境に合わせてプロパティに設定する値を変更する必要がありますが、これを実現するためにリポジトリ設定内のDeploymentsで設定できるデプロイメント変数と、YAMLアンカーを使用します。
YAMLアンカー
YAMLアンカーはYAMLファイル内での記述の重複を避けるための機能です。ある場所で宣言したブロックを別の個所から参照できます。まず前回の最終的なbitbucket-pipelines.ymlを確認しましょう。
image: eclipse-temurin:17
definitions:
caches:
gradlewrapper: ~/.gradle/wrapper
pipelines:
custom:
manualTest:
- step:
name: Test #ステップが増えるので名前を付けて区別
caches:
- gradle
- gradlewrapper
script:
- ./gradlew test
artifacts:
- build/reports/tests/test/** #JUnitの出力先ディレクトリをアーティファクトに指定
- step:
name: Upload Reports #作業内容がわかる名前を
deployment: test
oidc: true
script:
- pipe: atlassian/aws-s3-deploy:1.1.0 #パイプの実行
variables: #パイプに渡す変数
AWS_OIDC_ROLE_ARN: $AWS_OIDC_ROLE_ARN #必須ではないため宣言が必要
S3_BUCKET: $BUCKET_NAME/build_$BITBUCKET_BUILD_NUMBER #例えばbucket/build_10などに展開される
LOCAL_PATH: build/reports/tests/test #アーティファクトの存在するディレクトリ
- export FRONT_URL="https://${FROMT_DOMAIN}/build_${BITBUCKET_BUILD_NUMBER}/"
- export BUILD_STATUS="{\"key\":\"TEST_REPORT\", \"state\":\"SUCCESSFUL\", \"name\":\"Test report\", \"url\":\"${FRONT_URL}\"}"
- curl -H "Content-Type:application/json" -X POST --user "${DEPLOY_AUTH}" -d "${BUILD_STATUS}" "https://api.bitbucket.org/2.0/repositories/${BITBUCKET_REPO_OWNER}/${BITBUCKET_REPO_SLUG}/commit/${BITBUCKET_COMMIT}/statuses/build"
これに*-SNAPSHOTタグのpushで起動するstagingデプロイをべた書きで追加します。
image: eclipse-temurin:17
definitions:
caches:
gradlewrapper: ~/.gradle/wrapper
pipelines:
custom:
manualTest:
- step:
name: Test #ステップが増えるので名前を付けて区別
caches:
- gradle
- gradlewrapper
script:
- ./gradlew test
artifacts:
- build/reports/tests/test/** #JUnitの出力先ディレクトリをアーティファクトに指定
- step:
name: Upload Reports #作業内容がわかる名前を
deployment: test
oidc: true
script:
- pipe: atlassian/aws-s3-deploy:1.1.0 #パイプの実行
variables: #パイプに渡す変数
AWS_OIDC_ROLE_ARN: $AWS_OIDC_ROLE_ARN #必須ではないため宣言が必要
S3_BUCKET: $BUCKET_NAME/build_$BITBUCKET_BUILD_NUMBER #例えばbucket/build_10などに展開される
LOCAL_PATH: build/reports/tests/test #アーティファクトの存在するディレクトリ
- export FRONT_URL="https://${FROMT_DOMAIN}/build_${BITBUCKET_BUILD_NUMBER}/"
- export BUILD_STATUS="{\"key\":\"TEST_REPORT\", \"state\":\"SUCCESSFUL\", \"name\":\"Test report\", \"url\":\"${FRONT_URL}\"}"
- curl -H "Content-Type:application/json" -X POST --user "${DEPLOY_AUTH}" -d "${BUILD_STATUS}" "https://api.bitbucket.org/2.0/repositories/${BITBUCKET_REPO_OWNER}/${BITBUCKET_REPO_SLUG}/commit/${BITBUCKET_COMMIT}/statuses/build"
#タグのpushで起動するパイプラインを追加
tags:
'*-SNAPSHOT':
- step:
name: Sataging Deploy
deployment: staging
oidc: true
script:
- ./gradlew shadowJar javadoc #テストではなくFatJarの生成とJavaDocの生成
- pipe: atlassian/aws-s3-deploy:1.1.0
variables:
AWS_OIDC_ROLE_ARN: $AWS_OIDC_ROLE_ARN
S3_BUCKET: $BUCKET_NAME/build_$BITBUCKET_BUILD_NUMBER
LOCAL_PATH: build #デフォルトならbuild直下にlibsとdocsが生成されてその下にFatJarやJavaDocが生成される
EXTRA_ARGS: --exclude=* --include=docs/* --include=libs/* #アップするディレクトリをフィルタリング
- export FRONT_URL="https://${FROMT_DOMAIN}/build_${BITBUCKET_BUILD_NUMBER}/"
#JavaDoCのステータス生成
- export DOC_STATUS="{\"key\":\"DOC\", \"state\":\"SUCCESSFUL\", \"name\":\"JavaDoc\", \"url\":\"${FRONT_URL}/dos/javadoc\"}"
- curl -H "Content-Type:application/json" -X POST --user "${DEPLOY_AUTH}" -d "${BUILD_STATUS}" "https://api.bitbucket.org/2.0/repositories/${BITBUCKET_REPO_OWNER}/${BITBUCKET_REPO_SLUG}/commit/${BITBUCKET_COMMIT}/statuses/build"
#FatJarのステータス生成
- export JAR_NAME=$(ls ${BITBUCKET_CLONE_DIR}/build/libs/*-all.jar | xargs basename)
- export JAR_STATUS="{\"key\":\"JAR\", \"state\":\"SUCCESSFUL\", \"name\":\"Executable Jar\", \"url\":\"${FRONT_URL}/libs/${JAR_NAME}\"}"
- curl -H "Content-Type:application/json" -X POST --user "${DEPLOY_AUTH}" -d "${BUILD_STATUS}" "https://api.bitbucket.org/2.0/repositories/${BITBUCKET_REPO_OWNER}/${BITBUCKET_REPO_SLUG}/commit/${BITBUCKET_COMMIT}/statuses/build"
ご覧のように、一部の値が異なる以外は同じ記述の繰り返しとなってしまいます。
これを解消するためにYAMLアンカーによる参照を導入して、重複を解消します。
アンカーを使用するには、参照されるブロックに&
で名前を付けて、参照する側には*
と名前を記述します。また、参照される側は参照する側よりも前に記述されている必要があります。このことから、bitbucket-pipelines.ymlではdefinitions
の中に参照される側を記述するのが良い方法だと思います。
アンカーを使って書き換える
YAMLアンカーを使って上記を書き換えたものが下記になります。要点にはコメントで番号を振って、後ほど解説します。
image: eclipse-temurin:17
definitions:
caches:
gradlewrapper: ~/.gradle/wrapper
pipes:
s3: &S3Deploy #1:S3へのアップロードを参照先として切り出し
pipe: atlassian/aws-s3-deploy:1.1.0
variables: &S3Variables #2:パイプの変数を参照可能にする
AWS_OIDC_ROLE_ARN: ${AWS_DEPLOY_ROLE_ARN}
S3_BUCKET: ${BUCKET_NAME}/${S3_PATH} #3:デプロイメント変数で環境ごとに書き換え可能にする
LOCAL_PATH: ${LOCAL_PATH} #3
scripts: #4:シェルスクリプトをすべて切り出す
- &S3Path export S3_PATH="${BITBUCKET_DEPLOYMENT_ENVIRONMENT}/build_${BITBUCKET_BUILD_NUMBER}" #3
- &FrontURL export FRONT_URL="https://${FROMT_DOMAIN}/${S3_PATH}"
- &JunitStatus export STATUS="{\"key\":\"JUNIT\", \"state\":\"SUCCESSFUL\", \"name\":\"JUnit report\", \"description\":\"junit test report\", \"url\":\"${FRONT_URL}\"}"
- &PostStatus curl -H "Content-Type:application/json" -X POST --user "${WRITE_AUTH}" -d "${STATUS}" "https://api.bitbucket.org/2.0/repositories/${BITBUCKET_REPO_OWNER}/${BITBUCKET_REPO_SLUG}/commit/${BITBUCKET_COMMIT}/statuses/build"
- &JacocoStatus export STATUS="{\"key\":\"JACOCO\", \"state\":\"SUCCESSFUL\", \"name\":\"JaCoCo report\", \"url\":\"${FRONT_URL}/coverage/\"}"
- &DocStatus export STATUS="{\"key\":\"DOC\", \"state\":\"SUCCESSFUL\", \"name\":\"Javadoc\", \"url\":\"${FRONT_URL}/docs/javadoc/\"}"
- &JarName export JAR_NAME=$(ls ${BITBUCKET_CLONE_DIR}/build/libs/*-all.jar | xargs basename) #12:生成したJarの検出
- &JarStatus export STATUS="{\"key\":\"JAR\", \"state\":\"SUCCESSFUL\", \"name\":\"Executable Jar\", \"url\":\"${FRONT_URL}/libs/${JAR_NAME}\"}"
steps:
- step: &Test #5:テストデプロイを切り出す
name: Test
deployment: test
oidc: true
caches:
- gradle
- gradlewrapper
artifacts:
- build/reports/tests/test/**
script:
- ./gradlew test
- *S3Path
- *S3Deploy
- *FrontURL
- *JunitStatus
- *PostStatus
- *JacocoStatus
- *PostStatus
pipelines:
custom:
manualTest:
- step: *Test #6:参照を使用する
manualBuild:
- step: &Build_and_deploy
<<: *Test #7:「マップのマージ」を利用した上書き
name: Build
deployment: staging
artifacts:
- build/docs/javadoc/**
- build/libs/**
script:
- ./gradlew shadowJar javadoc #12
- *S3Path
- <<: *S3Deploy #8:マージの癖のある仕様
variables:
<<: *S3Variables #8
EXTRA_ARGS: --exclude=* --include=docs/* --include=libs/* #9:アップロードするディレクトリをフィルタリング
- *FrontURL
- *DocStatus
- *PostStatus
- *JarName
- *JarStatus
- *PostStatus
branches: #10:ブランチを指定してpushで起動するパイプライン
feature/*:
- step: *Test
tags: #11:タグのpushで起動するパイプライン
'*-SNAPSHOT':
- step:
<<: *Build_and_deploy #7
name: Snapshot deploy
deployment: staging
'*-RELEASE':
- step:
<<: *Build_and_deploy #7
name: Product Release
deployment: production
#1:S3へのアップロードを参照先として切り出し
definitions
にpipes
という独自のプロパティを追加して、aws-s3-deployパイプを呼び出せるようにしています。アンカーで名前を付けた「値」が参照個所に呼び出されます。アンカー以降のブロックが参照先に挿入されると考えると理解しやすいです。この中で、環境変数を利用して、環境ごとに別のURLを生成できるように工夫しています。
#2:パイプの変数を参照可能にする
パイプに使用する変数は変更される可能性が高く、環境ごとの差異が発生しやすいのでこちらも参照できるようにしておきます。パイプの使用全体がすでに参照されていますが、#7でこれが有効になる場面があります。
#3:デプロイメント変数で環境ごとに書き換え可能にする
BitbucketからPipelinesの環境変数として設定される変数はリポジトリ変数のほかに、デプロイメント変数が存在します。デプロイメント変数はstep
で宣言されているdeployment
の値に応じて設定される変数で、リポジトリ変数を上書きできます。
この例ではアップロード先のパスを決めるS3_PATH
とアップロードするディレクトリを指定するLOCAL_PATH
を環境別に指定した環境変数を使用するようにしています。
S3_PATH
ではデフォルトで使用できる変数を組み合わせて使用しています。
このデフォルト変数はビルドの実行時にならないと値が存在しないため、シェルスクリプトでコンテナ内の環境変数として<CloudFrontのドメイン>/<デプロイ環境名>/build_<ビルド番号>
を生成しています。
LOCAL_PATH
ではデプロイ変数を利用してアップロードするファイルのパスの値を変更します。
test環境ではテスト結果のみのアップロードでしたのでbuild/tests/test
を指定していましたが、その他の環境でのデプロイは(デフォルト設定であれば)build
ディレクトリ直下で複数のディレクトリに生成物が分かれてしまうため、build
を指定して、aws-s3-deployパイプのフィルタ機能でアップロードするディレクトリ、ファイルを制御します。
#4:シェルスクリプトをすべて切り出す
そこまでするのであればシェルスクリプトファイルを書いた方がよいのでは、という意見もあるかと思いますが、ここではYAML内にとどめます。
シェルコマンドひとつひとつを参照できるようにして、実際のscript
内から参照します。特にビルドステータスAPIにポストするコマンドは複数回使用されますので参照できるようにする価値が大きいです。
#5:テストデプロイを切り出す
前述の通り、この例でのテストのデプロイとその他の環境でのデプロイの差はパスなどの値が大部分でした。
そのため、テストのデプロイ手順を参照できるようにすることで、その他の環境でのデプロイで利用することが出来ます。#7で活用します。
#6:参照を使用する
手動テストの内容を切り出したテストデプロイを参照します。ここでは&Test
以降の各プロパティがそのままstep
内に展開されます。
#7:「マップのマージ」を利用した上書き
YAMLアンカーの使用法で最も重要なのが「マップのマージ」です。値を呼び出すときに<<:
を値のレベルのインデントに合わせて配置します。このインデントがずれると値が正しく呼び出されない1ため、YAMLパーサー2で展開後の値を確認するとエラーの解明に非常に役立ちます。
この例の場合、step
の値に&Test
を呼び出しています。これに対して同じレベルでプロパティ(キー)と値を記述すると、キーが参照に存在しない場合は挿入、存在する場合は上書きされます。この辺りはJavascriptでのオブジェクトの動作と同様です。ここではname
など、環境ごとに異なる値を上書きしています。
#8:マージの癖のある仕様
まず、step
のシークエンスの要素として、aws-s3-deployパイプを参照します。
ここではvariables
にEXTRA_ARGS
という値を追加してパイプでアップロードするディレクトリをフィルタリングする必要があります。この時、マージを利用していても下記のようには記述できません。
- step:
- <<: *S3Deploy
variables:
EXTRA_ARGS: --exclude=* --include=docs/* --include=libs/*
上記だとvariables
をEXTRA_ARGS
のみを持ったマップで差し替えてしまいます。これを避けるには、variables
の値を別途、参照してマージする必要があります。そのため、#2で別途に参照を設定しています。
また、この機能でマージできるのは「マップ」であることに注意してください。シークエンスはマージできないため、下記のような記述はできません。
seq-a: &a-ref
- a
- b
seq-b:
<<: *a-ref #エラー!
seq-c:
- *a-ref
- c #エラーではないがseq-cの値として出来上がるのは[[a,b],c]となって[a,b,c]ではない
この仕様のため、script
は要素のひとつひとつで値(つまりコマンドそのもの)を参照できるようにして、シークエンスの各要素から値を参照する必要があります。
#9:アップロードするディレクトリ
aws-s3-deployパイプは、AWSのcliをラップしたものです。このため、S3のコマンドで使用できるオプションをEXTRA_ARGS
にセットすることでディレクトリやファイルを制御できます。ここではいったんbuild
内のすべてを除外して、libs
とdocs
の2つのディレクトリ内のすべて、を含める指定を行っています。
より詳しくは、オプションの詳細を参照してください。
#10:ブランチを指定してpushで起動するパイプライン
branches
プロパティでブランチ名を指定して、特定のブランチにpushされたときに起動するパイプラインを記述できます。ここではfeature/*
として、feature/で始まる名前の機能開発ブランチを指定しています。
運用形式の決定で決めた通り、このブランチではテストのみ行います。&Test
で指定したテスト結果のデプロイを参照しています。
#11:タグのpushで起動するパイプライン
考え方はbranches
と同様で、指定した名前のタグがpushされたら起動するパイプラインです。
ここでは*-SNAPSHOT
と*-RELEASE
を指定しています。それぞれ、stagingとproductデプロイとして、生成物をアップロードします。
このタグの指定をシングルクォートで括っているのは、先頭のアスタリスクをYAMLアンカーの参照と区別するためだと思われます。
デプロイ環境と名前だけ上書きして、実行しているのは&Build_and_deploy
と同じ内容です。
ここを見ると、アンカーのありがたみを感じると思います。
#12:生成したJarの検出
これはシェルコマンドの領域になるため最後に回しましたが、gradlewで生成したJarの名前を実行時に特定して変数にエクスポートしています。
これはGradleのプロジェクト、タスクの設定により、生成されるファイル名が変わることと、生成されるJarがひとつに限定されるという条件を利用して簡易的に実装しています。
lsコマンドで得られたJarファイルのパスからbasenameでファイル名のみを取り出して$(...)
で変数に渡す、という処理を行っています。
lsコマンドでは、Pipelinsのコンテナ内では.
を使用したカレントディレクトリの指定ができないため、${BITBUCKET_CLONE_DIR}
を使用してコンテナ内でのパスを特定しています。
また、ファイル名を*-all.jar
として検索しているのはGradleの「Shadowプラグイン」を使用してshadowJar
コマンドで実行可能Jarを生成しているためです。仮にSpringBootでbootJar
コマンドを使用するのであれば、単に*.jar
で問題ありません。ここはプロジェクトの設定次第ですので、適合する指定に読み替えてください。
取得したファイル名は、ビルドステータスAPIのurl
の指定に使用しています。
次の記事?
ここまでご閲覧いただきありがとうございました。
ひとまずこのシリーズの開始時に目標にしていた自動でテストを行い結果をアップロードする、リリース時に実行可能Jarを公開するという目標は果たせました。
しかしながら、改善点はいくつか残っています。
- シェルコマンドの実行結果が常に成功と決め込んでいる
- ビルドステータスAPIはリポジトリのメンバーにしか公開されないため、実行可能Jarが公開できたといってよいのか微妙→WikiやConfluenceを利用する?
- パイプラインの実行時間の短縮ができるのではないか
この辺りは気になっているところですが、今後時間をとって調査できればと考えています。
ひとまずは目標達成、次の記事は未定として、一旦終了とします。
ありがとうございました。
-
値を参照したいキーと同じレベルに記述してしまい、
{値を参照したいキー: null, 参照内容のキー1: 値1, 参照内容のキー2: 値2,…}
と展開されてしまうとバリデーターでは検出できません。 ↩