Nextjsで作成したWebサイトをCloudFront+S3で公開する

# はじめに
今回はNextjsを静的出力し、S3に格納、CloudFront経由で公開までをやってみようと思います。
基本的にAWSの操作はマネジメントコンソール上で行い、CICDの設定や、CloudFormationなどは行いません。

# Nextjsのプロジェクトを作成

サクッとNextjsのプロジェクトを作成します。

```
npx create-next-app@latest --typescript

What is your project named? ... static-app
Would you like to use ESLint with this project? ... Yes
Would you like to use `src/` directory with this project? ... Yes
Would you like to use experimental `app/` directory with this project? ... No
What import alias would you like configured? ... @/*
```

これでstatic-appが作成されました。

# 静的出力の設定

Nextjsをビルドした際に出力されるファイルを設定します。  
CloudFrontの仕様上、このまま出力した場合、うまく行かないため、next.config.jsに下記オプションを追加します。  

```js:next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  trailingSlash: true,
}

module.exports = nextConfig

```

https://nextjs.org/docs/api-reference/next.config.js/trailing-slash  

また、静的出力機能を利用するために、package.jsonも少し変更します。

```json:package.json
  "scripts": {
    "dev": "next dev",
    "build": "next build && next export",
    "start": "next start",
    "lint": "next lint"
  },
```

これで、CloudFrontで利用できる形になりました。

# ページ作成

今回は動くことを目標とするため、画面の作りこみは行いません。
デフォルトのフォルダやファイルに少し手を加えて、簡易的なページを作成し、正しく表示されるか確認したいと思います。

```
├─pages
│  │  index.tsx
│  │  _app.tsx
│  │  _document.tsx
│  │
│  └─about
│          index.tsx
│
└─styles
        globals.css
        Home.module.css
```

```tsx:index.tsx
import Head from "next/head";
import { Inter } from "@next/font/google";
import styles from "@/styles/Home.module.css";

const inter = Inter({ subsets: ["latin"] });

export default function Home() {
  return (
    <>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <main className={styles.main}>
        <p className={inter.className}>ルートページです。</p>
      </main>
    </>
  );
}

```


```tsx:about.tsx
import Head from "next/head";
import { Inter } from "@next/font/google";
import styles from "@/styles/Home.module.css";

const inter = Inter({ subsets: ["latin"] });

export default function About() {
  return (
    <>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <main className={styles.main}>
        <p className={inter.className}>アバウトページです。</p>
      </main>
    </>
  );
}

```

# 動作確認

コマンドを入力して、ビルド＆静的出力を行います。

```
npm run build
```

すると、static-app/outフォルダが生成されます。  
このフォルダの中身をS3にデプロイし、CloudFrontを利用して公開していきます。

# S3にデプロイ
まずはS3にデプロイするためにバケットを作成します。

バケットを作成を押下し、必要な項目を適当に埋めていきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1240861/0383c621-8c05-e536-9b8c-f9ebf2d5b77e.png)

なお、今回はCloudFrontを経由させるため、ブロックパブリックアクセスの設定は不要です。
CloudFrontを経由させず、直接バケットをWebサイトとして公開する場合、こちらの内容を設定する必要があります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1240861/3b8eda35-00de-50f8-3343-b7b684419071.png)

バケットが作成されたら、ここに先ほどNextjsで作成したWebサイトをデプロイします。
static-app/out内のものをすべてドラッグ＆ドロップです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1240861/caeee40c-d001-785b-7505-f2967f31b291.png)

# CloudFrontの作成
CloudFrontを作成していきます。

オリジンドメインからS3のバケットを選択することができますので、
先ほど作ったS3バケットを選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1240861/a83f495d-8c28-dfe1-3824-624e007b3b7c.png)


次にCloudFrontからS3バケットにアクセスできるようにするために、S3バケットアクセスの設定を行います。

`Origin access control settings (recommended)`にチェックを入れ、プルダウンより既存の`Origin access control`を選択します。
新規作成する場合は、`コントロール設定を作成`を押下します。

`署名リクエスト (推奨)`にチェックを入れ、`作成`を押下します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1240861/c6a9eb34-8e84-6e60-bc06-2ec3f289ee63.png)

```
S3 バケットポリシーを更新する必要があります
CloudFront は、ディストリビューションの作成後にポリシーステートメントを提供します。
```
と表示されていますが、一旦このままで大丈夫です。
この後、設定します。
また、残りの項目はデプロイを優先するためにデフォルトのまま進めていきます。
`ディストリビューションを作成`を押下することで、ディストリビューションが作成されます。

# S3 バケットポリシーの更新

画面が遷移すると画面上部にS3 バケットポリシーについての案内が来るので、`ポリシーをコピー`からコピーしてS3 バケットポリシーを更新します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1240861/61cdf2ca-d309-d971-6316-8e7b760d4727.png)

あとからコピーしたい場合や、コピーし逃した場合であっても
ディストリビューションを選択して、オリジンタブからオリジンを編集を押下すると、同じものがコピーすることができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1240861/2350cc73-5e9a-34b6-0268-e795e3ba1e43.png)

内容はCloudFrontのディストリビューションからS3バケットに対してのアクセス許可なので、自分で書いてしまっても問題ないです。

```json
{
        "Version": "2008-10-17",
        "Id": "PolicyForCloudFrontPrivateContent",
        "Statement": [
            {
                "Sid": "AllowCloudFrontServicePrincipal",
                "Effect": "Allow",
                "Principal": {
                    "Service": "cloudfront.amazonaws.com"
                },
                "Action": "s3:GetObject",
                "Resource": "arn:aws:s3:::next-static-web-site/*",
                "Condition": {
                    "StringEquals": {
                      "AWS:SourceArn": "arn:aws:cloudfront::自分のID:distribution/EGDE7KD6UY0RT"
                    }
                }
            }
        ]
      }
```

これをS3のバケットポリシーにあてていきます。
Amazon S3のバケットからアクセス許可のバケットポリシーを編集から、コピーしたものを入力します。
これで、CloudFrontからS3 バケットにアクセスできるようになります。

# 実際にアクセスしてみる
CloudFrontよりディストリビューションを選び、ディストリビューションドメイン名に表示されているURLにアクセスします。
なお、ルートにアクセスしても`index.html`を返却するようにしていないため、正しく表示されません。
一旦`/index.html`を付けてアクセスします。

![スクリーンショット 2023-03-02 134741.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1240861/e1e4b3b1-2a0f-8d52-bab7-d0d93e66cfb7.png)

表示されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1240861/ff8f5d08-047f-ae76-93e4-65d569f4088b.png)

続いて`/about/index.html`を付けてアクセスします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1240861/20306a7b-b385-ccd4-a8ad-1307b013c4bd.png)

問題なく公開されていそうです。

# CloudFrontの関数を定義する
アクセスした際に、わざわざ`index.html`をつけるのは面倒なので、
自動で`index.html`が付与されるようにCloudFrontに関数を定義します。

CloudFront内の左のメニューより関数を選択し、`関数を作成`を押下します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1240861/fd01abf5-346f-9ac1-b3a1-c388261a0007.png)

名前を付け、関数を新規作成すると、関数コードがあるのでそこに下記Javasctiptを記載し、`変更を保存`を押下します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1240861/1341ee0a-b1e5-55a9-cea1-313bcda2bd6a.png)
```js
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;
}
```
発行タブから`関数を発行`を押下して、関数を発行します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1240861/b4af299e-1972-5146-ff47-63fcf285d2df.png)

ディストリビューション一覧画面に戻って対象のディストリビューションを選択し、
ビヘイビアタブからデフォルトのものを選択し、編集を押下します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1240861/567f9c61-6c9b-8799-d90b-7d2cca6e5edc.png)

関数の関連付けで、ビューワーリクエストから関数タイプ`CloudFront Functions`を選択し、先ほど作った関数を選択、変更を保存します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1240861/f70aded2-b80f-c220-5e72-205fbdcfa3c5.png)

これでindex.htmlなしでもindex.htmlが表示されるようになりました。

# まとめ

CloudFrontとS3を利用することで、キャッシュを効かせながらサーバレスで静的サイトを簡単に公開できました。
次はGithubからS3バケットに自動デプロイできるようなCICDや、S3とCloudFrontの設定をCloudFormationで構築してみたいと思いました。

# 参考
https://zenn.dev/hamo/articles/0a96c4d27097bd

https://penpen-dev.com/blog/s3-cloudfront-index/