BitbucketPipelinesでCI/CDに挑戦する　その2：テスト結果をS3にアップロードする

## 概要
既にある程度の開発が進んでいるBitbucket上のプロジェクトにPipelinesを使って継続的あれこれする機能を追加しようという、解説というよりも私の挑戦の記録です。

[前回](https://qiita.com/talesleaves/items/99a75ca0b831f92be34f)は初のPipelinesの利用として「手動でテストを実行」しました。
その2では「オープンソースの開発で、生成されたテスト結果を開発リソースとして共有する」シーンをを想定して、テスト結果をAWS S3にアップロードするまでを扱います。記事の分量から、CloudFrontでの配信やビルドステータスへのアーティファクトの公開URLの追加は次回以降に扱います。

### 記事内で使用している環境
- Java17
- Gradle7.6
- JUnit,JaCoCoでテストのレポートを作成
- BitbucketのリポジトリでPipelinesを有効化している（[その1](https://qiita.com/talesleaves/items/99a75ca0b831f92be34f)を読んでいる）
- AWSの管理者アカウントを持っている

## Pipelinesと生成物
Pipelines上で実行されたビルドも、ビルドツールの指定通りに成果物を生成します。生成場所はGradleの標準的な設定であれば`build`ディレクトリ内となります。
この生成物をダウンロードしたり、Pipelinesの別ステップで利用できるようにするためには、yml内で「[アーティファクト](https://support.atlassian.com/ja/bitbucket-cloud/docs/use-artifacts-in-steps/)」として指定する必要があります。

アーティファクトの指定には、`step`プロパティ内で`artifacts`を使用して生成物が配置されるディレクトリを指定します。
この指定はリポジトリのルートからの相対指定となります。また、[globパターン](https://support.atlassian.com/ja/bitbucket-cloud/docs/use-glob-patterns-on-the-pipelines-yaml-file/)を使用して複数のファイル、ディレクトリに一致させることが出来ますが、**`.`や`..`は使用できません。つい`./build/**`などとしたくなりますが、この制約のために機能しません。**
Gradleでの標準的な設定での指定の例は下記のようになります。

```yaml
#前略 stepのみ抜粋
  - step
      scripts:
        - ./gradlew test
      artifacts:
        - build/reports/tests/** #JUnitのテストレポートすべて
        - build/libs/** #Jar
```

正しく指定できていれば、アーティファクトを含むステップの終了時にそれが保存され、パイプラインのログ画面の`Artifacts`タブから確認とダウンロードができます。
個人のリポジトリであればこれだけでも十分ともいえるのですが、**アーティファクトの保存期間は14日間、1容量GB以下という制限**もありますので、過去のバージョンを保存しておく必要がある、などの場合には別のストレージにアップロードするなどの手段が必要になります。

## ファイル展開用にS3をセットアップする
そこでこの記事ではS3にテストレポートをアップロードして、後にブラウザから閲覧できるようにしてみます。
ここからはしばらく、AWSコンソールでの作業になります。

### 採用する構成
ストレージにはS3を使用しますが、Jarなど比較的大きなバイナリを含み、プロジェクトが人気になると膨大なアクセスが発生することも想定して、CloudFrontからファイルを配信します。
検索するとS3にhtmlを配置する場合の様々な記事が出てきますが、現在[^now]はこの組み合わせを利用するのが一番シンプルに実現できるようです。
[^now]: 2023年1月末

PipelinesからS3にアップロードする処理にはAtlassian公式から[aws-s3-deployパイプ](https://bitbucket.org/atlassian/aws-s3-deploy/src/master/)が用意されていますので、これを使用します。
そしてこのパイプの使用法に[Bitbucket Pipelines OpenID Connect を使用して AWS にデプロイする](https://support.atlassian.com/ja/bitbucket-cloud/docs/deploy-on-aws-using-bitbucket-pipelines-openid-connect/)が紹介されていますので、これを組み合わせて使用します。

この組み合わせにすることでアクセスキーとシークレットの脆弱性を避けることが出来る上、AWS側ではBitbucketPipelinesとCloudFrontに対してそれぞれロールを割り当てるだけでアクセス管理をできるようになりますので、煩雑な設定がほとんどなくなります。

### S3バケットを作成する
それではまず、AWSでS3のバケットを作成します。
S3のコンソールから、「バケットを作成」をクリックします。

名前、リージョン等、多数の設定項目が表示されますが、ここでは名前とリージョン以外のバケットの設定はすべてデフォルトで問題ありません。
記事のタイトルにもあるようにCloudFrontから配信するため、「[静的ウェブサイトホスティング](https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/WebsiteHosting.html)」や「[パブリックアクセスの設定](https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/access-control-block-public-access.html)」は変更しません。
Pipelinesからのアップロードを行うための権限は、IAMで設定します。

### IAMでIDプロバイダにBitbuketPipelineを登録する
[Bitbucket Pipelines OpenID Connect を使用して AWS にデプロイする](https://support.atlassian.com/ja/bitbucket-cloud/docs/deploy-on-aws-using-bitbucket-pipelines-openid-connect/)の手順に従って、リポジトリのPipelinesがS3を利用できるようにしていきます。

IAMのサイドメニューから「IDプロバイダ」開きます。プロバイダのタイプで「OpenID Connect」を選択すると下図のような画面になります。
![IAM-ID-Providers.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357184/289fcf8f-6ba1-4cd1-d607-03a5004de6a1.png)
この画面に設定する内容は、Bitbucketのリポジトリ設定＞OpenID Connectから確認できます。ここで表示される`Identity provider URL`と`Audience`がIAM側の`プロバイダのURL`と`対象者`に相当します。
プロバイダのURLを張り付けたあと、「サムプリントを取得」で表示されるサムプリントは`a031c46782e6e6c662c2c87c76da9aa62ccabd8e`となります。確認出来たら対象者も張り付け、「プロバイダを追加」で登録完了です。

### 作成したプロバイダにロールを作成して割り当てる
引き続き、作成したプロバイダ情報とS3へのアクセス許可を持ったロールを紐づけます。

作成されたプロバイダを選択するとプロバイダの詳細画面へ移動します。
プロバイダ名の下に「ロールの割り当て」ボタンが表示されていますので、それをクリックします。
ダイアログが表示され「新しいロール作成」を選択すると、プロバイダを選択する画面に移動します。
先ほど作成したPipelinesのプロバイダと`Audience`（なぜかここは翻訳されてない）を選択して、「次のステップ」へ進みます。
![IAM-S3-Policy.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357184/c069f41f-882d-8d91-da25-1af6907c6c4c.png)
上図のようにフィルタに`S3`と入力し、`AmazonS3FullAccess`にチェックを入れ「次のステップ」に進みます。
タグの付与は任意ですが、プロジェクト名などをタグとして追加しておくことをお勧めします。
最後にロールの名前と説明を入力しますが、**説明にも一部の記号と英数字しか入力できない**ことに注意してください。「信頼されたエンティティー」が設定するリポジトリのパイプラインであること、ポリシーがS3FullAccessであることを確認して、「ロールの作成」で完了です。

これでS3にファイルをアップロードするためのAWS側の作業は完了しました。

## Pipelinesにパイプを追加する
Bitbucketのリポジトリに戻って、S3へのアップロードを行うようにパイプラインを編集します。
こちらにもいくつかの準備が必要となり、また、今後を見据えてymlの構造の変更も行います。

### Pipelinesの変数、リポジトリ変数
[パイプ](https://support.atlassian.com/ja/bitbucket-cloud/docs/what-are-pipes/)の使用には、パイプが指定する変数に値を設定する必要があります。
この変数はyml内部で直接指定することも可能ですが、リポジトリやワークスペースに変数を登録することが可能です。

これらの変数はパイプラインを実行するコンテナの環境変数として読み込まれ、値だけを変更するような場合にコミットを行う必要がなくなります。また、この変数に「Secured」を指定することで、ログや画面上に値が表示されないようにできます。他システムとの連携時には特に重宝する機能です。

このほか、[変数とシークレット](https://support.atlassian.com/ja/bitbucket-cloud/docs/variables-and-secrets/)には、デフォルトで使用できる変数のリストが掲載されています。ビルド番号やパイプラインが起動されたブランチなどを識別することが可能なため、積極的に利用していきましょう。

### aws-s3-deployパイプで使用される変数
[aws-s3-deployパイプ](https://bitbucket.org/atlassian/aws-s3-deploy/src/master/)から、必要な変数を確認します。`*`のついた変数は「パイプを使用する際に常に指定する変数」`**`のついた変数は「必須だが、アカウント、リポジトリ、環境変数に指定されていれば自動的に読み取られる変数。実行時に上書き可能」となっています。つまり、`*`はymlに直接記入、`**`はリポジトリ変数に登録する変数とするとよいでしょう。
aws-s3-deployパイプの[Advanced example:](https://bitbucket.org/atlassian/aws-s3-deploy/src/master/#:~:text=%3A%20%27build%27-,Advanced,-example%3A)に、OpenID Connectを使用する例もありますので、これを参考にします。

まず、リポジトリ変数に`AWS_DEFAULT_REGION`と`AWS_OIDC_ROLE_ARN`を登録します。
リポジトリ設定の下部に「PIPELINES」のセクションがあり、その中に「Repository variables」のページがあります。
変数の追加は名前と値を入力して「add」を押すだけです。アクセスキーなど値が見えてはいけない変数にはSecuredのチェックを入れてから「add」します。
`AWS_DEFAULT_REGION`の値はS3バケットを作成したリージョン名、`AWS_OIDC_ROLE_ARN`の値は[作成したプロバイダにロールを作成して割り当てる](#作成したプロバイダにロールを作成して割り当てる)で作成したロールのARNとなります。

また、これ自体は必須の変数ではありませんが、S3で作成したバケット名を`BUCKET_NAME`として登録しておきます。これは、このリポジトリに設定された他のパイプラインでS3を利用するときに、パイプの必須変数である`S3_BUCKET`に指定するパス（正確にはプレフィックス）の生成に利用する目的と、以降にバケット自体が変更になる可能性もあるためです。

これらを利用してaws-s3-deployパイプをパイプラインに追加していきます。

## bitbucket-pipelines.ymlの編集
例によって[バリデーター](https://bitbucket-pipelines.atlassian.io/validator)を開いてそこで編集していきます。[^branch]
バリデーターに前回のymlをコピペします。

[^branch]:IDEやyamlの編集に慣れている方はローカルで編集してpushしてももちろん問題ありません。
```yaml:bitbucket-pipelines.yml
image: eclipse-temurin:17

definitions:
  caches:
    gradlewrapper: ~/.gradle/wrapper

pipelines:
  custom:
    manualTest:
      - step:
          caches:
            - gradle
            - gradlewrapper
          script:
            - ./gradlew test
```

### テスト結果をアーティファクトに指定する
[前述](#pipelinesと生成物)のように、stepの終了時に生成物は破棄されてしまいますので、testタスクを実行している`step`に`artifacts`を追加します。
このとき、あくまでもパイプラインの動作確認であれば`build/**`という指定もあり得ますが、ここでアーティファクトに指定したディレクトリは以降のすべてのstepで読み込まれるため、少しでも処理を軽くしたいのであれば必要なディレクトリのみを指定した方が効率的です。ここではJUnitの標準である`build/reports/tests/test/**`[^JUnitDest]とします。
[^JUnitDest]:JUnitのデフォルトは`build/reports/tests/＜テストタスク名＞`です。ここではtestタスクであるため、このディレクトリとなります。

build.gradleで別の場所を指定している場合は、当然それに合わせて変更が必要です。また、私の環境ではJaCoCoでカバレッジのレポートも生成していますが、それもtestタスクに組み込んでいるため、同じディレクトリ内に`coverage`ディレクトリを指定して出力させています。
アーティファクトを指定したymlは下記のようになります。

```yaml:bitbucket-pipelines.yml
image: eclipse-temurin:17

definitions:
  caches:
    gradlewrapper: ~/.gradle/wrapper

pipelines:
  custom:
    manualTest:
      - step:
          caches:
            - gradle
            - gradlewrapper
          script:
            - ./gradlew test
          artifacts:
            - build/reports/tests/test/** #JUnitの出力先ディレクトリ内をアーティファクトに指定
```

### アップロードするstepの作成
まず簡単な変更ですが、ステップが増えたので各`step`に`name`プロパティを追加して名前を付けます。ステップの名前はパイプラインの実行時のログや結果表示に使用されます。
ここでは、先に作成済みのtestタスク実行部分は`Test`、パイプの実行部分は`Upload Reports`とします。

パイプの実行には[パイプの使用法](https://bitbucket.org/atlassian/aws-s3-deploy/src/master/)の通り、必要な変数を宣言してパイプに渡します。
また、OpenID Connectを利用したaws-s3-deployパイプの使用では、使用する`step`に`oidc`プロパティを追加し、`true`を設定する必要があります。

必須の変数のうち、`AWS_DEFAULT_REGION`はパイプの必須変数でリポジトリ変数に登録してあるため、記述を省略できます。
`AWS_OIDC_ROLE_ARN`もリポジトリ変数に登録していますが、こちらはパイプの必須の変数ではないため宣言が必要です。少し不思議な感じがしますが、パイプの変数にリポジトリ変数を展開させます。
`S3_BUCKET`には、どのビルドのテスト結果がわかるようなパスを指定します。ここでは、テスト結果はビルドの番号に依存するため、ビルドごとにバケット内に`build_＜ビルド番号＞`のフォルダを作ってアップロードするようにします。。リポジトリ変数に登録したバケット名と、ビルド番号を表す`BITBUCKET_BUILD_NUMBER`を組み合わせて、`$BUCKET_NAME/build_$BITBUCKET_BUILD_NUMBER`と指定しました。
`LOCAL_PATH`はアーティファクトに指定したフォルダを指定します。ここでは`build/reports/tests/test`です。

```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 #アーティファクトの存在するディレクトリ
```

### `deployment`プロパティ
このプロパティはBitbucketに生成物のデプロイを追跡させます。デフォルトで指定可能な値は`test`,`staging`,`production`です。デプロイの性質によって分けて追跡できるようになっています。
ここではテストのレポートですので`test`を指定していますが、リポジトリ設定から別のデプロイを設定することもできます。
この指定が存在することで、リポジトリのサイドメニュー「Deployments」から、デプロイを行ったコミット・ビルドの情報や、デプロイの履歴を確認できます。
デプロイの詳細は[デプロイを設定、監視する](https://support.atlassian.com/ja/bitbucket-cloud/docs/set-up-and-monitor-deployments/)を参照してください。

## ymlの変更をコミットして実行してみる
バリデーター上でエラーがなければ、リポジトリの「ソース」からymlを表示し、「編集」ボタンからエディタを有効にしてバリデーターの内容をコピーして、コミットします。[^branch]

手動でのパイプラインの実行はサイドメニュー「パイプライン」でパイプラインの履歴を表示し、「Run pipeline」からブランチと実行するパイプライン（ここではmanualTest）を選択して「Run」です。

前回と同様、コンソール風の画面にログを表示しながらパイプラインが実行されます。今回はステップがふたつあるため、左側のステータス表示が2段になり、「Test」と「Upload Reports」になっているはずです。
ふたつとも無事に緑になったら、S3のコンソールからバケットのオブジェクトを確認してください。`S3_BUCKET`に指定した名前のフォルダが生成されていれば、アップロードは成功です。

## 次の記事
ここまでご閲覧いただきありがとうございます。
[次の記事](https://qiita.com/talesleaves/items/7427fe30f63ceb6e855b)ではCloudFrontを通じてレポートをブラウザに表示する、ビルドステータスにレポートへのリンクを追加する、など、今回の処理に付随する機能の設定を行います。