はじめに
こんにちは、インフラエンジニアのhogeです。 この記事では、AWSを使用してSPAアプリケーションを公開する手順を解説します。
構成

使用しているサービスのざっくりとした説明
Route 53
- DNSサービス
- ユーザーがウェブサイトにアクセスすると、Route 53がリクエストを適切なサーバーまたはAWSサービスにルーティングする。
- 今回の構成の場合、エイリアスレコードを利用して CloudFront のディストリビューションに紐づいたドメイン名(例:xxxxx.cloudfront.net)にルーティングする。
ACM (AWS Certificate Manager):
CloudFront:
- AWSのCDN(コンテンツ配信ネットワーク)サービス。
- 世界中に分散されたエッジロケーションにコンテンツをキャッシュし、ユーザーにより近い場所からコンテンツを提供することで、レイテンシを低減し、ロード時間を短縮する。
- 細かなキャッシュの制御が可能。例えば、特定のパスをキャッシュから除外したり、クエリ文字列パラメータに基づいてコンテンツをキャッシュしたりすることができる。
S3
- オブジェクト ストレージ サービス。
- 実際のウェブサイトのファイル(HTML、CSS、JavaScriptなど)が格納されている
- この構成では、バケットはオリジンとして機能し、静的ファイルをホスティングする。
クライアントがアクセスし、レスポンスが返されるまでの流れ
- クライアントがブラウザでサイトにアクセスする。
- ドメイン名がRoute 53で処理され、レコードに基づいてCloudFrontのドメイン名にリダイレクトされる。
- ACMで検証されたSSL/TLS証明書を使ってセキュアな通信が確立される。
- CloudFrontはリクエストを受け、キャッシュされたコンテンツがエッジロケーションにあるか確認する。
- キャッシュされたコンテンツが利用可能な場合、CloudFrontはそれをクライアントに返す。 キャッシュされていない場合は、S3オリジンからコンテンツを取得し、クライアントに配信する前にキャッシュする。
S3の前段にCloudfrontを設置するメリット
Cloudfrontを前段に設置するメリットは3点ほどあります。
- CloudFrontのキャッシュを使ったレスポンス高速化
- 独自ドメインの証明書を適用
- コスト効率の向上
- キャッシュによりオリジンへのリクエスト数が減少するため、オリジンサーバーの使用量とそれに伴うコストが削減される(S3よりCloudfrontへのアクセス料の方が安い)
構築してみる
S3バケットを作成する
CloudFrontを経由してからのみS3にアクセスできるようにするため、ブロックパブリックアクセスのバケット設定に全てチェックを入れます。

S3にSPAサイトをアップロードする
buildしたファイルをアップロードします
パブリックアクセスを許可していないため、S3に直接アクセスすると、Access Deniedエラーが出ます。

ACMでSSL証明書を発行する
注意点として、Cloudfrontに関連づけるACMはバージニア北部で発行されたものしか使えません。そのため、リージョンをバージニア北部に切り替えておきましょう。

「完全装飾ドメイン名」にドメイン名を入力して、そのほかはデフォルトのまま「リクエスト」します。

Cloudfrontディストリビューションを作成する
先ほど作成したS3バケットをオリジンに設定する
OAC(CloudFront へのアクセスのみを制限する設定)を設定します。
OACを新規作成します。
ディストリビューション作成後、バケットポリシーをコピーできるので、コピーする。
コピーしたポリシードキュメントをS3バケットに設定する。これによってCloudfrontを経由してS3オブジェクトにアクセスできるようになる。ここでミスすると、Cloudfront + S3間でアクセスができなくて403エラーが出る。

Cloudfrontからアクセスできるか確認してみる
赤枠の部分がデプロイ中ではなくなったら、cloudfrontの設定が完了になります。

https://ディストリビューションドメイン名/index.htmlにアクセスし、サイトが表示できるかを確認します。

アクセスできました

Route53でサブドメインの設定を行う
Recordをルーティング先がCloudfrontのディストリビューションのエイリアスレコードに設定します。
少し待ってからカスタムドメインにアクセスすると、SPAサイトが表示されます(反映までに少し数分かかります)

Cloudfrontにデフォルトのルートオブジェクトを指定する
現状、ルートにアクセスすると、index.htmlではなく、S3バケットのルート直下の存在しないオブジェクトにアクセスするため、403エラーを返します。
そのため、cloudfrontにデフォルトのルートオブジェクトを指定し、/index.htmlにリダイレクトするように設定します。
ディストリビューションの設定からデフォルトルートオブジェクトにindex.htmlを指定し、変更します
デプロイが完了したら、再度ルートにアクセスし、/index.htmlにアクセスされることを確認します。

デプロイ
react + viteでアプリケーションを作っている想定でデプロイします
アプリケーションをbuildします。
yarn build
buildしたファイルをs3にアップロードします
(「S3にSPAサイトをアップロードする」手順と同様のことを行なっています)
aws s3 cp --recursive --region ap-northeast-1 dist/ s3://バケット名
S3バケットに新しいファイルはアップロードされましたが、cloudfrontにキャッシュが残っているため、アプリケーションは更新されません。
そのため、cloudfrontのキャッシュを削除する必要があります。
aws cloudfront create-invalidation --distribution-id ディストリビューションID --paths "/*"
キャッシュ削除のステータスは以下から確認できます。
キャッシュ削除後、オリジンサーバからリソースが取得され、更新された画面が表示されます。


デプロイをGithub Actionsで自動化する
この部分を作っていきます。
### OIDC認証用IAMロールを作成
AWSのリソースをGithub actions上から操作するにあたって、OIDCを使用してAWS認証を行います。OIDCを使う場合、IAMロールに紐づいた一時クレデンシャルを利用してAWSリソースへアクセスします。そのため、万が一悪意ある第三者に窃取されたとしても、永続的に利用可能なIAMユーザのアクセスキーに比べて危険性は低くなります。
以下のようなIDプロバイダーを作成。
各項目は次のように入力する。
| 項目 | 値 |
|---|---|
| プロバイダのタイプ | OpenID Connect |
| プロバイダの URL | https://token.actions.githubusercontent.com( 入力後、 サムプリントを取得 をクリックします。 ) |
| 対象者 | sts.amazonaws.com |
IAMポリシーを作成します。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:ListBucket",
"cloudfront:CreateInvalidation"
],
"Resource": [
"arn:aws:cloudfront::<アカウントID>:distribution/<ディストリビューションID>",
"arn:aws:s3:::<バケット名>/*",
"arn:aws:s3:::<バケット名>"
]
}
]
}
次にGithub Actionsで利用するIAMロールを作成します。
エンティティタイプはカスタム信頼ポリシーです
ポリシードキュメントには以下の内容を記述します。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "",
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::<AWSアカウントID>:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:<GitHubユーザー名>/<GitHubリポジトリ名>:ref:refs/heads/<ブランチ名>"
}
}
}
]
}
Secretsを設定
github actions内から読み取る機密情報を設定します

Github actionsのworkflowを作成
プロジェクトルートにworkflowを追加します。
mkdir -p .github/workflows
workflowファイルを作成します。
touch .github/workflows/main.yml
on:
push:
env:
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v1
- uses: aws-actions/configure-aws-credentials@v2
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/<IAMロール名>
aws-region: ap-northeast-1
- name: Install Dependencies
run: yarn install
- name: build
run: yarn build
- name: deploy
run: aws s3 cp --recursive --region ap-northeast-1 dist/ s3://<バケット名>
- name: Clear cache
run: aws cloudfront create-invalidation --distribution-id <ディストリビューションID> --paths "/*"
手順は以上です。
AWSのリソースの操作関連で失敗した場合はIAMの権限が足りていなかったり、Github ActionsでAssumeRoleできていない可能性が高いので、IAMRole・Policyを確認すると良いかと思います。

まとめ
今回の記事はフロントエンドエンジニア向けに書いたので、イメージしやすいようにあえてAWSマネジメントコンソールでリソースを作成してみました!本記事がフロントエンドのインフラの構築やデプロイでエラーが出た際のトラブルシューティングなど、何かしら皆さんに何か役立つことがあれば幸いです。
参考
GitHub Actions で OIDC を使用して AWS 認証を行う
CloudFront + S3 + Route53でReactのSPAを独自ドメインでホスティングする