ブログを AWS からホスティングする仕組みにした【CloudFront + Amazon S3】

AWS

当ブログを WordPress から NextJS 製に変更しました。 前々から WordPress 煩わしいなあと感じていて、1年以上前からコツコツ開発を進めていたのですが、それがようやく形になったという感じです。

そのついでと言ってはなんですが、今まで一般的なレンタルサーバーから配信していた当ブログを AWS の CloudFront + Amazon S3 から配信する仕組みに変更。

next export で静的コンテンツとしてビルドしたあと、Amazon S3 にデプロイ、CloudFront から配信しています。

AWS と GCP、それぞれのクラウドについて

上の通り、AWS を使った配信方法に変更しました。

はじめは学習がてら GCP 使ってやってみたいなーと思って進めていたのですが、最終的に AWS を選択。

GCP は Google アカウントにログインしていればさっさとコンソール画面に入れて楽ちんだったんですけど、ググってもあまり情報が見つからない。情報量などの観点からみても AWS がやっぱり一番いいのかなって感じです。

GCP は静的ホスティングでもロードバランサが必要

今回のブログリニューアルには、 サーバー側で動作させる必要のない静的コンテンツのみでの配信でブログを成り立たせたいという目的があります。

しかし、Cloud Storage を使った静的ホスティングであっても、Cloud CDN だけでなくロードバランサを通す必要があり、結構な利用料金かかってしまっていたみたいです。


Amazon S3 + CloudFront の構成の場合、こんなにも費用がかかるということは個人ブログだとまず考えられないので、候補から外さざるを得なかったですね。

この画像はステージング環境としてホストしていたときのものなのですが、GCP のアカウントを作成してすぐの無料キャンペーンの期間中だったので、一銭の料金もかからず済みました。すぐ気がついてよかったです。危ない危ない。

CDN の評価 圧縮設定などに関しても AWS に軍配があがる

CloudFront は静的コンテンツを圧縮するかどうかを選択できて、よしなに gzip だったり Brotli 圧縮してくれるんですけど、GCP の Cloud CDNにはそういう機能は見当たりませんでした。

そのため Node.js で html,css,js,json を圧縮する処理を自前で書いて、そのあとデプロイフローに実行コマンドを入れ込み、 Cloud Storage にアップロード。そのあと Content-Type などのメタデータの設定を追記するというなかなか面倒なことをしていました。

また「Cloud Storageや IP アドレスの直接アクセスができてしまう」という問題がありました。ドメイン名でなくてもコンテンツが見えてしまうというのは少々気持ちが悪いです。

上記の複数の理由から、GCP の採用を諦めて、AWS を採用したという感じです。

GitHub Actions でデプロイ

NextJS を使ったこのブログでは、Markdown を HTML 化して出力するという仕組みを採用して、GitHub リポジトリで記事データを管理しています。

この手のブログを作る人はみな同じことを考えると思うのですが、やっぱりデータベースだけでなく、GitHub のリモートリポジトリ、自分のパソコンのローカルリポジトリ、サーバー側の複数ヶ所に記事データが保管される安心感はいいですね。

話が少しそれましたが、デプロイにも GitHub の CI/CD 機能である GitHub Actions を使うことにしました。採用理由に深い理由はなく、ただ単に使い慣れているからです。

どうせこのブログのバケットにしか操作をおこなわないので、デプロイするための IAM ユーザーは、インラインポリシーで作成しました。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "S3RimaneBlogObjectCURD",
            "Effect": "Allow",
            "Action": [
                "s3:*Object"
            ],
            "Resource": "arn:aws:s3:::example-backet-name/*"
        },
        {
            "Sid": "ListBucket",
            "Effect": "Allow",
            "Action": "s3:ListBucket",
            "Resource": "arn:aws:s3:::example-backet-name"
        }
    ]
}

sync コマンドを使ってアップロードしているので、ListBucket アクションも設定しています。

余談ですが、僕がハマったポイントとして、List 系の操作をするときは Resource はバケット名そのまま、Object 系の操作をするのときは「バケット名/*」とする必要があるみたいですね。

このハマりポイントからはなかなか抜け出せなくて大変だった……。

さて、ワークフロー用の yml はこんな感じになっています。

name: Build Nextjs on S3

on:
  push:
    branches:
      - publish

    workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Setup Node.js
        uses: actions/setup-node@v2
        with:
          node-version: 16.x

      - name: Output ads.txt
        run: |
          echo ${{secrets.GOOGLE_ADS_TXT}} >> public/ads.txt

      # ビルド
      - name: Install NPM packages
        run: |
          npm i -g yarn
          yarn install

      - name: NextJS Build
        run: yarn build

      # S3にデプロイ
      # cache no-cache
      - name: Deploy HTML
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          AWS_BUCKET_NAME: ${{secrets.AWS_BUCKET_NAME}}
        run: |
          aws s3 sync --delete --exclude "*" \
          --include "*.html" --include "*.xml"  \
          --include "*.txt"  \
          --region ap-northeast-1 \
          out s3://${AWS_BUCKET_NAME} \
          --metadata-directive "REPLACE" \
          --cache-control "no-cache"

      # cache 1 week
      - name: Deploy CSS/JS/JSON
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          AWS_BUCKET_NAME: ${{secrets.AWS_BUCKET_NAME}}
        run: |
          aws s3 sync --delete --exclude "*" \
          --include "*.css" \
          --include "*.js" \
          --include "*.json" \
          --region ap-northeast-1 \
          out s3://${AWS_BUCKET_NAME} \
          --metadata-directive "REPLACE" \
          --cache-control "public, max-age=604800"

      # Cache 1 week
      - name: Deploy Images file
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          AWS_BUCKET_NAME: ${{secrets.AWS_BUCKET_NAME}}
        run: |
          aws s3 sync --delete --exclude "*" \
          --include "*.jpg" --include "*.jpeg" \
          --include "*.png" --include "*.webp" \
          --include "*.avif" --include "*.ico" \
          --region ap-northeast-1 \
          out s3://${AWS_BUCKET_NAME} \
          --metadata-directive "REPLACE" \
          --cache-control "public, max-age=604800"

      # Cache 1 year
      - name: Deploy Web font file
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          AWS_BUCKET_NAME: ${{secrets.AWS_BUCKET_NAME}}
        run: |
          aws s3 sync --delete --exclude "*" \
          --include "*.woff" --include "*.woff2" \
          --region ap-northeast-1 \
          out s3://${AWS_BUCKET_NAME} \
          --metadata-directive "REPLACE" \
          --cache-control "public, max-age=31536000"

プルリク自体は main に投げていきますが、デプロイする際は main ブランチを publish ブランチにマージして、main ブランチからはデプロイしない仕組みです。

まずは Google Adsense 用の ads.txt を出力し、そんでもって NPM をインストールして Next.js でビルド、エクスポート。

ファイルの種類に合わせて Cache-Contorol を設定してアップロードしています。

HTML がキャッシュに左右されると面倒くさいことになりそうなので、HTML などは no-cache にしています。それ以外は基本的に一週間キャッシュ、ウェブフォントは1年キャッシュです。

S3 側でヘッダ情報が指定されている場合、CloudFront はそれを優先してくれるので、CloudFront のキャッシュ期間もオリジンのデータと同じになります。

まだ数日しか様子見していませんが、キャッシュヒット率はこんな感じです。

アクセス頻度が少ないオブジェクトはキャッシュ期間が終わる前にキャッシュが破棄されることがあります。つまりアクセスが減る時間帯はキャッシュヒット率が下がります。

一週間キャッシュが指定されているのに一週間経つ前にキャッシュヒット率が下がっているのはそのためです。

僕のブログの場合、このグラフだと15時付近にヒット率が低下していますが、日本時間に直すと0時付近にということになるので、夜中のアクセスはほとんどなく、キャッシュが破棄されているのだと思います。

ちなみにそれ以外の時間帯で下がっているのは、記事更新などでデプロイし直したタイミングなどです。

DNS (ドメイン) に Cloudflare を使うことにした

DNS には Route 53 ではなく Cloudflare の DNS を使うことにしました。

Route 53 は多機能みたいですが、僕にはよくわかりません。機能を使いこなせていない僕にとって、Route 53 のホストゾーンの料金はちょっと高いです。

とりあえずドメインが使えるようになってくれればいいのと、もとからドメインを所有していたということもあり、バリュードメインから Cloudflare Registrar に移管しました。

Cloudflare Registrar は安い

移管しなくても DNS 自体は使えますが、Cloudflare Registrar はドメインを原価で販売しているのが売りらしく、更新料を安く済ませるためにどうせなら移管もしちゃおうかなという軽いノリでドメイン管理も Cloudflare に任せました。

バリュードメインでは .net ドメインの更新料は 1620円程度だったのが、 Cloudflare では $9.95(1280円)程度になりました。ただしこれは2022年05月の情報です。あなたがこの記事を読んでいるときには変わっている可能性もあります。

ちなみに少し前は移管でしかドメイン管理を Cloudflare に任せることはできなかったみたいですが、今は直接購入することもできるみたいです。

一日二日ちょいちょいっとしかさわっていないのですが、 かなり Cloudflare はわかりやすい感じがします。

ネイキッドドメインも見せかけ上の CNAME が設定できる

ネイキッドドメイン(サブドメイン名とかがついていないドメイン)は 基本的に CNAME が設定できないので、CloudFront のように IP アドレスが変わったりするような仕組みのところにトラフィックを飛ばすのは DNS サービスによっては不可能です。(バリュードメインではできたので僕は困ってなかったです)

しかし、 Cloudflare の DNS サービスは「見かけ上は CNAME だが CNAME ではない」という感じの仕組みが提供されていて、ネイキッドドメインにも CloudFront への値を簡単に設定できます。

なぜCloudflare のプロキシ機能を使っていないのか

DNS で プロキシの設定を ON ににしてステータスを「プロキシ済み」にすると、Cloudflare 側でコンテンツをキャッシュしてくれたり、Gzip や Brtoli 圧縮してくれたり、HTTPS に対応してくれたりと至れり尽くせりで、CloudFront とほぼ同等といってもいいような機能を提供してくれます。

しかし、上記の画像を見て気がついた方もいると思いますが、少なくともこのブログではプロキシ機能は使っていません。

これはなぜかというと、CloudFront を通しているからです。

CloudFront を経由すること自体は問題なくできます。

しかし、ヘッダに Cloudflare の特有の情報が混じってきてしまうことが確認できました。

CloudFront はヘッダ情報が違うだけでも新しいアクセスとみなすため、キャッシュし直す処理が入り、キャッシュヒット率が下がってしまいます。

このあたりの情報はググっても見当たらなかったので、どのように設定するのがより高速に配信することができるのかはわかりませんでした。

Cloudflare -> CloudFront -> Amazon S3 といったようにプロキシを通しつつ CloudFront も使うのか、CloudFront -> Amazon S3 とするのか、はたまた CloudFront なんてそもそも使わずに Cloudflare -> Amazon S3 としたほうが高速なのか……。

Cloudflare のプロキシ機能を ON にすると HTTP/3 で配信できるので、Amazon S3 を直接オリジンにして、CloudFront を使わない手も案外ありかもしれません。

なにはともあれ、すでにCloudFront Function を使っているので、とりあえずといった感じでCloudflare のプロキシ機能は OFF にして様子見しているという状態です。

広告

関連記事

新着記事

広告