NetlifyをやめてCloudflare Pagesへ移行する【Hugo SSG】

feature-image

Hugo で生成したコンテンツ(静的ウェブサイト)の公開用サーバーとして Netlify (無料プラン)を利用していましたが、若干アクセスが遅い気がしていたのと、Netlifyへのこだわりが強いわけでもなかったので、お引越し気分で Cloudflare Pages を試してみることにしました。

Netlify は本当に遅いのか

2020年8月時点の分析記事として、以下のブログが参考になります。

Netlifyが日本からだと遅い - id:anatooのブログ

約2年前ということで、今とは状況が違うかもしれません。そして、2021年5月時点、サポートによると東京ロケーションは High-Performance CDN と位置づけられているので(これは、無料枠ではないと解釈。調べていない)、2022年7月現在でも Netlify 無料枠を使う上では遅い解釈で正しいかもしれません。

Is there a list of where Netlify’s CDN pops are located? - Support - Netlify Support Forums

この記事では “Netlify が遅いから引っ越す” というよりも、”Cludflare Pages への好奇心” の方が移行の動機として大きいため、本件にはこれ以上言及しません。

引っ越しの前提条件

既に Hugo を用いてコンテンツは生成してあるので、生成先ディレクトリ(デフォルトでは public/ )を新たなホスティングへ配置し直すだけです。

この記事では2種類の接続方法を記載してます。

  • GitHub と接続し、Commit が発生したら自動で Cloudflare Pages へデプロイする
  • 公式CLI(wrangler cli)を用いて、ローカルのフォルダを Cloudflare Pages へデプロイする

今回は、Hugoの操作方法については記載していません。また、Cloudfareアカウントは作成済みであるものとします。

Cloudflar Pages プロジェクトの作り方

  • GitHubと接続する過程で作成する
  • ファイルをブラウザ経由でアップロードする
  • wrangler cli を使って作成する

今回、最初にGitHubと接続をしたので、そのセットアップ過程でプロジェクトを作成しました。

GitHubへの接続

GitHubへの接続を選択する。アイコンにある通り、GitHubおよびGitLabと接続できるようです。

次に、ここでプロジェクト名を決定します。作成後も変更画面はありました。(試していません)

ここで作成したプロジェクト名が 後に公開されるURLになります。サイトは <your-project>.pages.dev として公開されます。

サイト生成のフレームワークも多数選択できました。

フレームワークを選択した後は、ビルドコマンドを指定します。

  • ビルドコマンド: hugo --minify
  • ビルド出力ディレクトリは Hugo 標準: public (変更なし)

必要に応じて、環境変数もここで追加します。マニュアルには記載が見当たりませんが、指定しないと古いバージョンが使われるとのことなので、ローカルで利用している hugo のバージョンと合わせておきます。バイナリは +extended バージョンが利用されるようです。Hugo Modules を使っている場合も大丈夫とのこと。

(参考 Build configuration · Cloudflare Pages docs - Language support and tools)

そして設定が完了し、デプロイに移ります。

初デプロイが27秒で完了しました。上出来。

ドメイン(レコード)の切り替え

最後に、ドメインをアクティブにします。(デプロイの後にこの画面が勝手に出てきました)

前提として、自サイトでは既に Cloudflare をドメインのネームサーバとして指定していますので、Cloudflare Pages セットアップの流れで切り替え操作まで完了するフローになっています(と思われます)。

試していないので想像ですが、Cloudflare で管理していないドメインの場合は「このレコードに切り替えてね」といったアナウンス画面になるのではと思います。

ただ、設定するレコードは CNAME で *.pages.dev 向けになりますので、Zone Apex(頭にサブドメインを伴わないドメイン自体を指す)の場合は、一般的には CNAMEレコードが設定できません(RFC 1912 - Common DNS Operational and Configuration Errors)。その場合は結局、Cloudflare の管理下に移すことになるでしょう。

Netlify ではまさに、CNAEでNetlifyのロードバランサ用レコードを指す必要があったので、この時に Cloudflare 管理下に置いていました。

参考 CNAMEレコードにZone Apexをマッピングできない件について - サーバーワークスエンジニアブログ

パフォーマンスを軽くベンチマーク

冒頭で「レスポンスは目的ではなかった」と言いつつも関心はありますので評価してみます。自サーバーではなくサービスプロバイダーの設備ですので、攻撃にならないよう控えめの数字で軽くベンチします。

前提:Apache Bench コマンドを使いますが、この実行環境の MacBook そのものが無線LAN接続なので、負荷評価用の環境としては非常にお粗末です。軽い参考・目安程度でご覧ください。

Netlifyだった場合のベンチ

ab -c 4 -n 100 https://*********

Server Software:        Netlify
Server Hostname:        *********
Server Port:            443
SSL/TLS Protocol:       TLSv1.2,ECDHE-ECDSA-CHACHA20-POLY1305,256,256
Server Temp Key:        ECDH X25519 253 bits
TLS Server Name:        *********

Document Path:          /
Document Length:        24332 bytes

Concurrency Level:      4
Time taken for tests:   12.878 seconds
Complete requests:      100
Failed requests:        0
Total transferred:      2466308 bytes
HTML transferred:       2433200 bytes
Requests per second:    7.77 [#/sec] (mean)
Time per request:       515.127 [ms] (mean)
Time per request:       128.782 [ms] (mean, across all concurrent requests)
Transfer rate:          187.02 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:      229  275  54.7    244     432
Processing:   146  179 158.6    151    1723
Waiting:       77  123 140.7     86    1458
Total:        379  453 176.1    420    2117

Percentage of the requests served within a certain time (ms)
  50%    420
  66%    453
  75%    474
  80%    493
  90%    528
  95%    544
  98%    583
  99%   2117
 100%   2117 (longest request)
# 2回目(1分後くらいに)

Concurrency Level:      4
Time taken for tests:   11.904 seconds
Complete requests:      100
Failed requests:        0
Total transferred:      2466400 bytes
HTML transferred:       2433200 bytes
Requests per second:    8.40 [#/sec] (mean)
Time per request:       476.148 [ms] (mean)
Time per request:       119.037 [ms] (mean, across all concurrent requests)
Transfer rate:          202.34 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:      231  291  57.8    270     476
Processing:   146  169  41.9    151     353
Waiting:       76  107  40.4     83     256
Total:        378  460  81.3    437     812

Percentage of the requests served within a certain time (ms)
  50%    437
  66%    474
  75%    495
  80%    501
  90%    563
  95%    585
  98%    785
  99%    812
 100%    812 (longest request)

Cloudflare Pagesのベンチ

❯ ab -c 4 -n 100 https://********/

Server Software:        cloudflare
Server Hostname:        ********
Server Port:            443
SSL/TLS Protocol:       TLSv1.2,ECDHE-RSA-CHACHA20-POLY1305,2048,256
Server Temp Key:        ECDH X25519 253 bits
TLS Server Name:        ********

Document Path:          /
Document Length:        23528 bytes

Concurrency Level:      4
Time taken for tests:   6.367 seconds
Complete requests:      100
Failed requests:        0
Total transferred:      2437464 bytes
HTML transferred:       2352800 bytes
Requests per second:    15.71 [#/sec] (mean)
Time per request:       254.672 [ms] (mean)
Time per request:       63.668 [ms] (mean, across all concurrent requests)
Transfer rate:          373.87 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:       40  112  86.6     77     353
Processing:    36  123 123.8     64     628
Waiting:       31   96 105.9     55     516
Total:         82  234 143.5    158     871

Percentage of the requests served within a certain time (ms)
  50%    158
  66%    305
  75%    336
  80%    359
  90%    401
  95%    498
  98%    681
  99%    871
 100%    871 (longest request)
# 2回目

Concurrency Level:      4
Time taken for tests:   14.858 seconds
Complete requests:      100
Failed requests:        0
Total transferred:      2437472 bytes
HTML transferred:       2352800 bytes
Requests per second:    6.73 [#/sec] (mean)
Time per request:       594.337 [ms] (mean)
Time per request:       148.584 [ms] (mean, across all concurrent requests)
Transfer rate:          160.20 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:       37  453 488.3     91    1220
Processing:    34   91  79.8     64     396
Waiting:       33   68  42.2     56     228
Total:         80  545 491.8    254    1466

Percentage of the requests served within a certain time (ms)
  50%    254
  66%   1116
  75%   1133
  80%   1139
  90%   1177
  95%   1260
  98%   1357
  99%   1466
 100%   1466 (longest request)

Connect の時間が大きく改善されました!と言いたいところですが、2回目のブレが多すぎました。

やはり有線LANでやらないとノイズが酷いですが、Netlify の2回平均よりも遥かに良好な値が Cloudflare Pages で一度確認できたので、とりあえず良しとします。

注意

繰り返しますが、とても雑な評価なので参考程度にしてください。Netlifyを不当に悪評価とする意図はありません。

Cloudflare Pages CLI (wrangler) の利用

ローカルディレクトリをアップロードするための操作です。Wrangler そのものは、Cloudflare Workers のためのCLIのようです。

Wrangler is a command-line tool for building Cloudflare Workers

Get started · Cloudflare Workers docs

$ npm install -g wrangler

$ wrangler login
 ⛅️ wrangler 2.0.22 
--------------------
Attempting to login via OAuth...

Successfully logged in.
Would you like to help improve Wrangler by sending usage metrics to Cloudflare? (y/n)
Your choice has been saved in the following file: ./.wrangler/config/metrics.json.

  You can override the user level setting for a project in `wrangler.toml`:

   - to disable sending metrics for a project: `send_metrics = false`
   - to enable sending metrics for a project: `send_metrics = true`

プロジェクト一覧を確認できます。

$ wrangler pages project list
Retrieving cached values for account from node_modules/.cache/wrangler
┌──────────────┬───────────────────────┬──────────────┬────────────────┐
│ Project Name │ Project Domains       │ Git Provider │ Last Modified  │
├──────────────┼───────────────────────┼──────────────┼────────────────┤
│ ***********  │ ***********.pages.dev │ Yes          │ 17 minutes ago │
└──────────────┴───────────────────────┴──────────────┴────────────────┘

アップロードの際は、対象ディレクトリとプロジェクト名を指定して実行。

❯ CLOUDFLARE_ACCOUNT_ID=XXXXXXXXXXXXXXXXX npx wrangler pages publish public --project-name=***********
Retrieving cached values for account_id from node_modules/.cache/wrangler

🌍  Uploading... (287/287)

✨ Success! Uploaded 151 files (136 already uploaded) (3.87 sec)

✨ Deployment complete! Take a peek over at https://d000abcd.***********.pages.dev

たったの4秒で完了しました。Netlifyのときは数十秒かかるケースもあったので、地味に嬉しい。

CLIでアップロードした場合は、Web画面上では以下の通りシンプルな表示に。

Functions

Javascript または Typescript で用意できるみたいです。

Functions (beta) · Cloudflare Pages docs

今回の Hugo コンテンツでも独自のスクリプト(API)を一部で利用していますが、Netlify や Cloudflare Pages などのプロバイダーに依存しないよう別立てしています。おかげで今回、 Netlify への依存機能が無くて移行も1時間とかからなかったので、今後も別のプロバイダーを試す可能性がある場合は、疎結合にしておくのがよいかなと思います。

設定のメモ

最終的に設定した環境変数

本番環境へは HUGO_ENV: production を設定しています。

***.pages.dev の検索エンジンインデックス回避

カスタムドメイン(独自ドメイン)を割り当てて使うケースも多いと思います。

カスタムドメインを割り当てたとしても、デフォルトで割り当てられる Cloudflare Pages のドメイン( ***.pages.dev )は並列で利用できます。万一検索エンジンBOTがアクセスしてきてインデックスされると重複してしまいますので、以下の通り条件を指定して noindex ヘッダーを返すようにします。

_headers ファイルを最上位(index.html等)の場所に配置し、以下の内容を記載するだけです。これにより、 X-Robots-Tag を理解できるロボットのインデックスを回避できます。

https://<your-project-name>.pages.dev/*
  X-Robots-Tag: noindex

robots メタタグの指定 | Google 検索セントラル | ドキュメント | Google Developers

ほかのやり方として、 rel=canonical を設定しておくことでも問題ないでしょう。これならば、今後同等の問題が他のプロバイダーで発生しても問題を回避できますね。

重複した URL を正規タグに統合する | Google 検索セントラル | ドキュメント | Google Developers

まとめ

Netlify から Cloudflare Pages へ移行する経緯を記載しました。

もともと Cloudflare を DNS 環境として利用していてアカウントが整っていたというのもありますが、思い立ってから軽く調べてデプロイの完了まで1時間程度で完了したので、この手軽さは素晴らしいと思いました。

パフォーマンスの理由ももちろんですが、DNS と 静的サイト収容、そしてCDNを同一プロバイダーへまとめることができたので、管理画面が統合されたのも嬉しいです。複数の管理画面を行ったり来たりは面倒ですからね。

その他参考