BitbucketPipelinesでCI/CDに挑戦する　その3：S3にアップロードしたレポートをCloudFrontで公開する

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

[前回（その2）](https://qiita.com/talesleaves/items/adfd8b673ca6a17ae44c)はテストで生成したレポートをPipelinesでS3へアップロードしました。
今回はアップロードしたHTMLファイルをブラウザで閲覧できるようにCloudFrontを設定する、URLをBitbucketのビルドステータスに追加するなど、やり残した作業を行っていきます。
また、**下記環境に当てはまらない方でも、S3で静的ホスティングでHTMLを公開している方は是非ご一読ください。**

### 記事内で使用している環境
- Java17
- Gradle7.6
- JUnit,JaCoCoでテストのレポートを作成
- BitbucketのリポジトリでPipelinesを有効化している
- AWSの管理者アカウントを持っている
- S3にHTMLがアップロードされている

## HTMLファイルの公開用にCloudFrontを設定する
CloudFrontを設定して行きます。
Web検索ではS3との連携について様々な記事がヒットしますが、AWSの各機能の進化は早く、ベストプラクティスも流動的です。とはいえ、AWS側も利用者の安全性や利便性を考慮してくれていますので、デフォルトや推奨設定に従えば比較的簡単に目的を達成できます。

### 新規ディストリビューションを作成する
CloudFrontの管理コンソールから「ディストリビューション」を選択します。「ディストリビューションを作成」をクリックすると、作成画面へ移動します。

![distribution.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357184/845cf161-0726-e544-4b76-91a1328bbf66.png)

設定できる値やその意味は「[ディストリビューションを作成または更新する場合に指定する値](https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/distribution-web-values-specify.html)」にありますが、S3の静的Webコンテンツを配信する今回のケースに必要な部分だけここで説明します。

`オリジン`のセクションはCloudFrontでキャッシュするデータの「元データ」の指定をします。
「オリジンドメイン」にカーソルを合わせると、アカウント内にCloudFrontと接続できるリソースがある場合、それらがリスト表示されます。ここでは前回作成したS3バケットを選択します。この選択肢と連動して、「名前」も自動で入力されます。
「オリジンパス」はCloudFrontが受け付けたURLをS3に転送する際に付け足すパスです。今回は空白のままにします。詳しくは[オリジンのパス](https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/distribution-web-values-specify.html#DownloadDistValuesOriginPath)を参照してください。

`S3バケットアクセス`はS3バケットへのアクセス制限の設定です。「Public」（S3バケットが公開されている場合）,「Origin access control settings (recommended)」（オリジンアクセスコントロール (OAC)（推奨） ）,「Legacy access identities」（旧設定、オリジンアクセスアイデンティティ (OAI) ）がありますが、推奨となっているOACを選択します。
選択すると新たに`Origin access control`の項目と「コントロール設定を作成」ボタンが表示されます。作成ボタンを押すとダイアログが表示され、推奨設定が表示されます。
![OAC-Create.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357184/5c7fe179-81cb-8b0a-b059-dc8442aec629.png)
「名前」や「説明」に特に変更がなければ、そのまま「作成」します。

`デフォルトのキャッシュビヘイビア`はのディストリビューションの動作の設定です。ここでは`ビューワープロトコルポリシー`を`Redirect HTTP to HTTPS`に変更します。HTTPのリクエストをHTTPSへリダイレクトする設定です。

`キャッシュキーとオリジンリクエスト`はキャッシュする内容やS3へのリクエストのルールです。ここでは`Cache policy and origin request policy (recommended)`を選択し、`キャッシュポリシー`に`CachingOptimized`を設定します。`オリジンリクエストポリシー `、`レスポンスヘッダーポリシー`は設定しません。

以上の設定で残りの項目は飛ばし、ひとまず「ディストリビューションを作成」します。

### ディストリビューションから生成されるポリシーをS3へ適用する
ディストリビューションの一覧画面に戻りますので、作成したディストリビューションを選択します。
`一般`、`オリジン`、`ビヘイビア`、`エラーページ`、`地理的制限`、`キャッシュ削除`、`タグ`のタブから「オリジン」タブに切り替え、接続したS3バケットを選択して「編集」ボタンを押します。

見覚えのある画面の下部が、画像のように変更されています。
![policy-copy.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357184/90f6aefe-f5ac-7aba-fb76-45e90d21e1d7.png)
「ポリシーをコピー」を押すとクリップボードにS3へ設定するべきポリシーがコピーされます。
その下に表示されている「S3 バケットアクセス許可に移動」リンクから、S3のバケットの画面を開きます。

`バケットポリシー`のセクションの「編集」ボタンを押すとJSONエディターが起動しますので、ディストリビューションからコピーした内容を張り付けます。張り付けるとインデントが崩れた状態に見えますが、エラーが検出されていなければそのまま「保存」して大丈夫です。

これで、CloudFrontからS3へのアクセス権が設定されました。
このポリシーの具体的な設定の内容は[Amazon S3 オリジンへのアクセスの制限](https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html)を参照してください。

### ブラウザからアクセスしてみる
CloudFrontを通じてアクセスするためのURLはCloudFrontのディストリビューションの`一般`タブに表示されます。
「ディストリビューションドメイン名」がそのままURLのドメイン部分となりますので、S3バケットのオブジェクトへのパスをつけ足せばアクセス可能なはずです。前回の手順から引き続き作成していれば、例えば`https://x0x0x0x0x0x0.cloudfront.net/build_xx/index.html`のようになります。
無事にテストのレポートが表示されれば成功です。現状ではURLを間違った場合にはステータスコード403が帰りますので、URLに間違いがないか確認の上、間違いがなければS3のバケットポリシーに誤りがないかも確認してください。

## indexの解決とレスポンスの調整
現状、CloudFrontへのHTTPSリクエストがフォルダで終わっている場合や存在しないファイルを指定された場合、S3からステータスコード403が返されます。
Webサーバーの挙動としては前者はindexが存在しないフォルダであれば403、存在すればindexを返すべきですし、存在しないファイル、フォルダであれば404を返すべきです[^permission]。また、エラーのレスポンスボディがXMLで返ってしまうのも望ましくないため、エラーページを指定します。
[^permission]:認証がなく、一般的なディレクトリ設定の場合。

この動作をCloudFrontのディストリビューションで再現する設定を行います。

### CloudFrontでのリダイレクトやヘッダーの処理
CloudFrontでのURLやヘッダーによる処理には「[CloudFront Functions](https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/cloudfront-functions.html)」と「[Lambda@Edge](https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/lambda-at-the-edge.html)」の選択肢があります。
今回はリダイレクトが目的で他のサービスへの接続のような複雑な処理は必要ないため、CloudFront Functionsを使用します。
CloudFront Functionsは実質的にJavaScriptによるコーディングとなります。

### ファイル以外へのアクセスをリダイレクトする関数を作成する
CloudFrontのサイドメニューから「関数[^function]」を選択します。「関数を作成」ボタンを押して、作成画面へ移ります。名前と説明を入力して「作成」すると、「詳細」画面へ移動します。この時点で関数自体は作成されていますが、`開発ステージ`にあってまだ機能していない状態です。
[^function]:機械翻訳されている様子なので用語の統一のために「Funciton」に変わったりするかも

`開発`タブにJavaScriptエディターが表示され、自動生成されたテンプレートが表示されます。これを今回の目的である「ファイル以外へのアクセスをindexへリダイレクトする」ように書き換えます。難しいようですが、これも[ドキュメントに例](https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/example-function-add-index.html)がありますので、それを少し改良するだけです。

ドキュメントの例は下記のようになっています。関数に渡されている`event`の詳細については[CloudFront Functions のイベント構造](https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/functions-event-structure.html)を参照してください。
```javascript:ドキュメントより
function handler(event) {
    var request = event.request;
    var uri = request.uri;

    // Check whether the URI is missing a file name.
    if (uri.endsWith('/')) {
        request.uri += 'index.html';
    }
    // Check whether the URI is missing a file extension.
    else if (!uri.includes('.')) {
        request.uri += '/index.html';
    }

    return request;
}
```

この例では、リクエストのURIを確認してindex.htmlへのパスを追加し、それをリクエストとしてS3へ送出しています。S3へのリクエストは正しいパスを指すのでCloudFrontはS3からindex.htmlを受け取ってブラウザに送り返します。
しかし、ブラウザはこのURIの変更を認識していないめ、返されたindex.html内の相対パスの処理ができません。正しく処理させるにはリダイレクトが必要です。
それを加味して修正したものが下記になります。
```javascript:index-redirect
function handler(event) {
    var request = event.request;
    var uri = request.uri;
    // Check whether the URI is missing a file name.
    if (uri.endsWith('/')) {
        request.uri += 'index.html';
    }
    // Check whether the URI is missing a file extension.
    else if (!uri.includes('.')) {
        request.uri += '/index.html';
    } else { // ファイルへのアクセスと思われるURIならS3へリクエストを送る
        return request;
    }

    // 修正したURLに参照先を変えるようにブラウザにレスポンスを返す
    var response = {
        statusCode: 303,
        statusDescription: 'Your request is not a file. See other.',
        headers:
            { "location": { "value": request.uri } }
        }

    return response;
}
```
ファイルへのリクエストと思われるものはそのままS3へ送り出し、それ以外はS3へはアクセスせずにステータスコード303でindex.htmlを見に行くようにレスポンスを返します。
エディタのコードを編集したら、「変更を保存」で保存します。ヘッダ部分に「保存しました」とメッセージが表示されたら、`開発`タブから`テスト`タブに切り替えます。

### 作成した関数をテストする
`テスト`タブでは、作成した関数の実行テストを行うことが出来ます。
![function-test.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357184/7372447e-ba8d-e14a-517a-f38e21ecefed.png)
`イベントタイプ`は関数に渡される`event`のタイプを選択します。ここでは`Viewer Request`です。
`ステージ`はテストする関数の環境です。保存されてはいるものの「発行」されていない関数は`Development`ステージです。
HTTPメソッドはGET、URLパスは存在しないパスを入力します。「関数をテスト」ボタンを押すと、指定した条件でどのような結果が返るかを確認できます。
![function-test-result1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357184/fadf4123-fba8-3078-68ff-d36157c70749.png)
`出力`を確認すると、意図したとおりindex.htmlを見るように303ステータスを生成しています。ブラウザはこれを受け取って指示されたURLで再度アクセスします。
`コンピューティング使用率`は最大100であらわされる処理の負荷です。情報によると50までは問題ないようです。重すぎる処理にならないよう注意しましょう。

ファイルを指すURLでもテストしてみてください。テストの設定のままのリクエストが出力されていれば成功です。

### 関数を発行してディストリビューションに関連付けする
`発行`タブへ移動して「関数を発行」します。
関数が発行されると、`関連付けられているディストリビューション`の項目が表示され、「関連付けを追加」ボタンが表示されます。
「関連付けを追加」ボタンを押すとダイアログが表示され、対象のディストリビューション、イベントタイプ、キャッシュビヘイビアを指定して関数を関連付けします。関連付けを行ったディストリビューションはデプロイ中となり、関数が実行されるまでには1分前後の時間がかかります。
ディストリビューションのステータスが有効になったのが確認できたら、ブラウザからリダイレクトが機能するかを確認します。

### 存在しないファイルへのアクセスをステータスコード404で処理する
リダイレクトが有効になったことで、アクセス先はファイルに限定されるようになりました。
あとは、S3に存在しないファイルへのアクセスで発生する403ステータスを、404に変更してhtmlを返すように設定します。
これはS3の静的ホスティングの場合とほぼ同様の処理となります。

CloudFrontのサイドメニューから「ディストリビューション」を選択して、作成したディストリビューションの詳細を表示します。
`エラーページ`のタブを選択し、「カスタムエラーレスポンスを作成」ボタンを押します。
![Custom-ErrorResponse-Create.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357184/f071be9f-94a0-0d1d-d998-55429c77aeed.png)
変更したいオリジン（S3）からの`HTTPエラーコード`は403です。
`エラーレスポンスをカスタマイズ`を「はい」にすると、`レスポンスページのパス`、`HTTPレスポンスコード`が表示されます。パスはスラッシュから始める必要があります。ここでは`/404.html`として、レスポンスコードは当然404です。
値を入力したら、「カスタムエラーレスポンスを作成」ボタンを押します。
あとは404.htmlファイルを作成してS3のバケットの直下にアップロードし、存在しないファイルへのURLでアクセスしてみて、アップロードしたhtmlが表示されれば成功です。

xmlでレスポンスが返ることへの対策としてはほかのエラーコードにも対策するべきですが、ここでは省略します。これでテストレポートをブラウザから確認できる状態になりました。

## レポートへのURLをBitbucketのビルドステータスに反映させる
アップロードされたレポートへのURLは、現状、Bitbucketのパイプライン一覧からビルド番号を確認して手入力するしかありません。
これでは不便ですので、[ビルドステータスAPIを使用](https://support.atlassian.com/ja/bitbucket-cloud/docs/publish-and-link-your-build-artifacts/)してコミットのビルドステータスからアップロードしたレポートを閲覧できるようにします。

手順自体は公式ドキュメントの通りなのですが、先ににビルドステータスのプロパティを確認しましょう。

### ビルドステータスのプロパティ
プロパティについては[公式ドキュメントの表](https://ja.confluence.atlassian.com/bitbucket/integrate-your-build-system-with-bitbucket-cloud-790790968.html)を参照します。このうち、いくつかの項目を補足します。

`key`：必須項目です。コミットに対して一意になるように指定します[^status-key]。`name`が指定されていないときは一覧のビルドステータスに表示されるタイトルになります。ここでは値に`TEST_REPORT`を使用します。
`name`：ビルドステータスの名前です。記録したいタスク名やステップ名を指定します。ビルドステータスのタイトルになります。ここでは値に`Test report`を使用します。
`description`：ビルドの詳細です。補足情報が必要な場合に記入します。今回の例では省略します。
`url`：ビルドステータスのタイトルに対するリンクになります。ここではaws-s3-deploymentパイプでS3_BUCKETに渡した値の応用になります。
[^status-key]:POSTで同じキーで呼び出すと２回目以降は上書きとなります。

以上を踏まえて、ビルドステータスAPIでステータスを追加してアーティファクトを確認できるようにします。

### API呼び出し用に認証情報、リポジトリ変数を追加する
まずは**アカウント設定**で[アプリケーションパスワード](https://ja.confluence.atlassian.com/bitbucket/app-passwords-828781300.html#Apppasswords-Createanapppassword)を作成します。必要なアクセスは「リポジトリ」の「書き込み」です。**作成したパスワードは必ず記録しておいてください。**[^pass]
[^pass]:忘れるたびにに削除して新しく作成するというワークアラウンドも。  

そのパスワードを利用して、[リポジトリ変数](https://support.atlassian.com/ja/bitbucket-cloud/docs/variables-and-secrets/#Repository-variables)にAPIへの認証情報を作成します。値には`アカウント名:作成したパスワード`を登録します。これはSecuredにしてください。以後の例ではこの変数の名前を`DEPLOY_AUTH`で登録したものとします。
加えて、CloudFrontのドメインも、リポジトリ変数に登録しておきます。ここでは名前を`FRONT_DOMAIN`とし、今回作成したディストリビューションのドメインを登録します。

### パイプラインにAPI呼び出しを追加する
bitbucket-pipelines.ymlにビルドステータスAPIへの呼び出しを追加します。
公式ドキュメントによる呼び出しの例は以下のような感じです。コメントは私が加筆したものです。

```bash
# S3に直接アクセスするURL
export S3_URL="https://${S3_BUCKET}.s3.amazonaws.com/${S3_KEY_PREFIX}_${BITBUCKET_COMMIT}"

# -dオプションに渡すJSON
export BUILD_STATUS="{\"key\": \"doc\", \"state\": \"SUCCESSFUL\", \"name\": \"Documentation\", \"url\": \"${S3_URL}\"}"

# API呼び出し。--user以外は変更しない
curl -H "Content-Type: application/json" -X POST --user "${BB_AUTH_STRING}" -d "${BUILD_STATUS}"\
"https://api.bitbucket.org/2.0/repositories/${BITBUCKET_REPO_OWNER}/${BITBUCKET_REPO_SLUG}/commit/${BITBUCKET_COMMIT}/statuses/build"
```

今回の例の値に置き換えたものが下記です。

```bash
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"
```

それぞれシェルコマンドですのでyml上では`script`の子要素になります。

```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"
```
オンラインエディタ、オンラインバリデーターで作業すると折り返しが気持ち悪いですが、エラーがなければコミットして、手動でパイプラインを起動させてみます。

成功するでしょうか。成功したら、コミット一覧の右端のbuild欄のチェックをクリックします。
通常のビルド詳細へのリンクのほかに`Test report`のリンクが表示され、リンクをクリックしてJUnitのレポートにアクセスできれば完了です。

### 複数のアーティファクトがある場合
アップロードするアーティファクトが複数ある場合、s3-deployパイプを複数実行するのではなく、一つのディレクトリ内にアーティファクトごとのディレクトリを作成してアップロードします。
ビルドステータスを複数作成すること自体は処理が早いので、そこに子フォルダへのリンクを作成した方がビルド時間の短縮になります。しかし、数が多くなるとやはり不便になってくるので、別なドキュメントにアーティファクトへのリンクを出力したいところです。良い方法があればコメントしてくださると助かります。

## 次の記事
ここまでご閲覧いただきありがとうございます。
次の記事ではymlの整理とpushによる自動デプロイを中心に進めます。~~実際に試しながらの更新になるため、間隔が開くかと思います。~~ 次の記事は[こちら](https://qiita.com/talesleaves/items/282222db3915d83437ce)。