🦁

Vite + GitHub ActionsでCloudFrontのキャッシュを活用しつつデプロイ後に即時反映する

2023/01/09に公開

はじめに

以前、S3 へデプロイした後に CloudFront のキャッシュを削除する記事を書きました。
https://zenn.dev/keita_hino/articles/a39e98b59b7afb

これにより、デプロイしたら即時反映されるようになりましたが、キャッシュを削除するパスに /* を指定しているため、全てのキャッシュが削除されてしまいます。理想は 変更されたファイルのみオリジン(今回だとS3)から再取得し、変更されていないファイルはキャッシュを使うこと だと思いますが、Vite のビルド周りを調べてみたところ簡単に実現できたので本記事でまとめようと思います。

前提

S3 + CloudFront の環境構築方法は省略します。また、CloudFront のキャッシュを有効にしている前提で進めます。

環境

  • Vite v4.0.4
  • React v18.2.0

CloudFront のキャッシュを活用するには

ユーザーからオブジェクトをリクエストされた場合、ざっくり下記のような流れになります。

  • キャッシュがなかった場合
    • オリジンに転送
    • 返ってきたオブジェクトをユーザーに転送 & キャッシュに追加
  • キャッシュがある場合
    • オリジンに転送せず、CloudFront がオブジェクトを返す

今回やりたいことを実現するには変更されたファイルに対するリクエストの時はキャッシュを使わないようにする必要があります。どのように実現するかですが、ドキュメントに

CloudFront ディストリビューション内の既存のファイルを更新する場合、何らかのバージョン識別名をファイル名またはディレクトリ名に含めて、コンテンツを容易に制御できるようにすることをお勧めします。この識別名には、日付タイムスタンプ、連番など、同じオブジェクトの 2 つのバージョンを区別する方法を使用できます。

とある通り、既存ファイルを更新する時にファイル名 or ディレクトリ名を変えてあげれば良さそうです。ファイル名を変えることで、キャッシュがない状態になるのでオリジンから再取得してくれるようになります。
https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/UpdatingExistingObjects.html#ReplacingObjects

Vite のビルド時に変更されたファイルの名前を変える方法

次は Vite 側で変更されたファイルをビルドする時はファイル名 or ディレクトリ名を変える方法について調べていきます。
...と言いたいところですが、実は 変更されているファイルをビルドするとデフォルトで異なるハッシュ値が付与されたファイル名 になります👏(一部を除き)

Vite + React で検証してみる

ここからは変更されているファイルがある場合にファイル名が変わることを実際に手を動かしながら確認してみます。

実際にサンプルアプリを動かしたい方はこちら
$ yarn create vite sample-app --template react-ts
$ cd sample-app
$ yarn
$ yarn dev

念の為、http://localhost:5173/ を開いていつもの画面が表示されることを確認しておきましょう。

早速ビルドしてみましょう。また、何回ビルドしても結果が変わらないことも確認しておきます。

$ yarn build
vite v4.0.4 building for production...
✓ 34 modules transformed.
dist/index.html                   0.46 kB
dist/assets/react-35ef61ed.svg    4.13 kB
dist/assets/index-3fce1f81.css    1.41 kB │ gzip:  0.73 kB
dist/assets/index-e73c0651.js   143.55 kB │ gzip: 46.18 kB
✨  Done in 1.30s.

次はファイルを変更した状態でビルドしてみます。まずは次のようにファイルを変更します。

src/App.tsx
import { useState } from 'react'
import reactLogo from './assets/react.svg'
import './App.css'

function App() {
  const [count, setCount] = useState(0)

  return (
    <div className="App">
      <div>
        <a href="https://vitejs.dev" target="_blank">
          <img src="/vite.svg" className="logo" alt="Vite logo" />
        </a>
        <a href="https://reactjs.org" target="_blank">
          <img src={reactLogo} className="logo react" alt="React logo" />
        </a>
      </div>
      <h1>Vite + React</h1>
      <div className="card">
-       <button onClick={() => setCount((count) => count + 1)}>
+       <button onClick={() => setCount((count) => count + 2)}>
          count is {count}
        </button>
        <p>
          Edit <code>src/App.tsx</code> and save to test HMR
        </p>
      </div>
      <p className="read-the-docs">
        Click on the Vite and React logos to learn more
      </p>
    </div>
  )
}

export default App

この状態で再度ビルドして結果を見てみましょう。ここで重要なのは、index-XXX.js のファイル名だけが変わっているのと、今回変更していない index-XXX.css などはファイル名が変わっていないということです。

$ yarn build
vite v4.0.4 building for production...
✓ 34 modules transformed.
dist/index.html                   0.46 kB
dist/assets/react-35ef61ed.svg    4.13 kB
dist/assets/index-3fce1f81.css    1.41 kB │ gzip:  0.73 kB
dist/assets/index-1091ba50.js   143.55 kB │ gzip: 46.18 kB
✨  Done in 1.26s.

index-XXX.js などを読み込んでいる dist/index.html の中身を見てみると、スクリプトの読み込み先が追従して変わっているかと思います。ここで重要なのは index.html は中身は変わっているが、ファイル名は変わっていないということです。CloudFront のキャッシュの仕様を思い出すと、ファイル名が同じオブジェクトをリクエストした場合は CloudFront がオブジェクトを返すため、変更後のスクリプトを読み込んでくれません。そのため、/index.html はデプロイ後にキャッシュを削除する必要がありそうです。

dist/index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + React + TS</title>
    <script type="module" crossorigin src="/assets/index-1091ba50.js"></script>
    <link rel="stylesheet" href="/assets/index-3fce1f81.css">
  </head>
  <body>
    <div id="root"></div>
    
  </body>
</html>

ビルドアセットを CloudFront から配信する

次にビルドアセットを CloudFront から配信できるようにします。

vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

+ const isProduction = process.env.NODE_ENV === "production"; 

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
+ // CloudFront のディストリビューションドメイン名を設定
+ base: isProduction ? 'https://XXXX.cloudfront.net/' : "",
})
CloudFront のディストリビューションドメイン名を確認する方法

AWSのコンソールを開き、作成済みの CloudFront のディストリビューションを選択すると表示されます。(赤枠の部分)

GitHub Actions で S3 にデプロイした後、最小限のキャッシュを削除する

Vite のビルドの検証で /index.html のキャッシュだけを消せば良いことがわかったので、実際にワークフローに組み込んでいきます。

name: Deploy

on: push

env:
  AWS_ROLE_ARN: arn:aws:iam::${{secrets.AWS_ACCOUNT_ID}}:role/XXX

permissions:
  id-token: write
  contents: read
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@master
      - uses: aws-actions/configure-aws-credentials@v1
        with:
          role-to-assume: ${{ env.AWS_ROLE_ARN }}
          aws-region: ap-northeast-1
      - name: Install Dependencies
        run: yarn

      - name: Build
        run: yarn build

      - name: Deploy
        run: |
          aws s3 sync dist/ s3://XXX --region ap-northeast-1
      
      - name: Clear cache
        # 元々は paths に /* を指定してキャッシュを全削除していたが、/index.html のみを削除するようにした
        run: |
          aws cloudfront create-invalidation --distribution-id XXXXXXXXXXXXXX --paths "/index.html"

上記のワークフローを実行してしばらく待つと、/index.html のオブジェクトパスのみキャッシュを削除できました!

動作確認

ここまでで一通り作業が終わったので、実際に S3 + CloudFront でホスティングしている静的サイトにアクセスし、動作確認してみます。Chrome の場合、Devtools → Network → 該当のリソースを選択 → x-cache を確認することで、キャッシュを使用しているかを確認できます。Hit from cloudfront の場合はキャッシュを使用しており、Miss from cloudfront の場合はキャッシュは使用せず、オリジン経由で取得しています。

何度かリロードした後、<ディストリビューションドメイン名>.cloudfront.net を見てみると、Hit from cloudfront になっているためキャッシュが使用されているようです。また、index-XXX.js も同様に Hit from cloudfront となっていることを確認しておきましょう。

この状態で、ファイルを変更し再度デプロイしてみます。
再度、<ディストリビューションドメイン名>.cloudfront.net を見てみると、Miss from cloudfront となっているため、オリジン経由で取得されているようです。index-XXX.js も同様に Miss from cloudfront になっており、画面上も変更した内容が反映されていました!

次に変更していない index-XXX.css を確認してみると、こちらは Hit from cloudfront になっていました。このことから、変更されたファイルのみオリジンから再取得し、変更されていないファイルはキャッシュを使うことを実現できたと言えそうです🎉

終わりに

今回は Vite + CloudFront でキャッシュを使いつつ、変更内容を即時に反映する方法をまとめました。もっと良い方法や誤りなどありましたら優しくコメントいただけると助かります!🙏

参考

https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/UpdatingExistingObjects.html
https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/HowCloudFrontWorks.html

Discussion