iimon TECH BLOG

iimonエンジニアが得られた経験や知識を共有して世の中をイイモンにしていくためのブログです

SPAサイトをS3 + Cloudfrontにデプロイする方手順を解説する

はじめに

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

構成

使用しているサービスのざっくりとした説明

Route 53

ACM (AWS Certificate Manager):

  • SSL/TLS証明書を管理し、ユーザーとサーバー間の通信を暗号化する。CloudFrontと連携して、安全なHTTPS接続を提供する。

CloudFront:

  • AWSCDNコンテンツ配信ネットワーク)サービス。
  • 世界中に分散されたエッジロケーションにコンテンツをキャッシュし、ユーザーにより近い場所からコンテンツを提供することで、レイテンシを低減し、ロード時間を短縮する。
  • 細かなキャッシュの制御が可能。例えば、特定のパスをキャッシュから除外したり、クエリ文字列パラメータに基づいてコンテンツをキャッシュしたりすることができる。

S3

  • オブジェクト ストレージ サービス。
  • 実際のウェブサイトのファイル(HTML、CSSJavaScriptなど)が格納されている
  • この構成では、バケットはオリジンとして機能し、静的ファイルをホスティングする。

クライアントがアクセスし、レスポンスが返されるまでの流れ

  1. クライアントがブラウザでサイトにアクセスする。
  2. ドメイン名がRoute 53で処理され、レコードに基づいてCloudFrontのドメイン名にリダイレクトされる。
  3. ACMで検証されたSSL/TLS証明書を使ってセキュアな通信が確立される。
  4. CloudFrontはリクエストを受け、キャッシュされたコンテンツがエッジロケーションにあるか確認する。
  5. キャッシュされたコンテンツが利用可能な場合、CloudFrontはそれをクライアントに返す。 キャッシュされていない場合は、S3オリジンからコンテンツを取得し、クライアントに配信する前にキャッシュする。

S3の前段にCloudfrontを設置するメリット

Cloudfrontを前段に設置するメリットは3点ほどあります。

  • CloudFrontのキャッシュを使ったレスポンス高速化
    • CloudFrontはCDNサービスのため静的コンテンツをキャッシュして2回目以降のアクセスにキャッシュした静的コンテンツを返すことで、高速化することができる。
    • CloudFrontはグローバルに分散されたエッジロケーション(CDN)を使用してコンテンツをキャッシュし、ユーザーに最も近いエッジロケーションからコンテンツを配信するため、レイテンシーが削減される。
  • 独自ドメインの証明書を適用
    • S3のみでは、S3に割り当てられたURLしか使うことができないが、CloudFrontでは独自ドメインACMを設定することができるため、CloudFrontとS3を組み合わせることで独自ドメインに証明書適用することができる。
  • コスト効率の向上
    • キャッシュによりオリジンへのリクエスト数が減少するため、オリジンサーバーの使用量とそれに伴うコストが削減される(S3よりCloudfrontへのアクセス料の方が安い)

構築してみる

S3バケットを作成する

CloudFrontを経由してからのみS3にアクセスできるようにするため、ブロックパブリックアクセスのバケット設定に全てチェックを入れます。

S3にSPAサイトをアップロードする

buildしたファイルをアップロードします

パブリックアクセスを許可していないため、S3に直接アクセスすると、Access Deniedエラーが出ます。

ACMSSL証明書を発行する

注意点として、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を独自ドメインでホスティングする

CloudFrontのデフォルトルートオブジェクト設定に対する私的まとめ | DevelopersIO

AWSにおける静的コンテンツ配信パターンカタログ(アンチパターン含む) | DevelopersIO