Firebase Hosting で簡単無料なサーバーレスリダイレクタを設置する

最近 @euxn23 とともに awswakaran.tokyo というコミュニティを立ち上げたのですが、はてなブログ Pro が naked domain に対応しておらず、 https://awswakaran.tokyo/* を https://www.awswakaran.tokyo/* にリダイレクトしたいモチベーションが生まれました。

解決方法自体はいくつかあると思いますが、Firebase Hosting を利用することで簡単手軽サーバーレスな解決ができたのでご紹介します。

## tl;dr

- ドメイン間リダイレクトは Firebase を利用すると最高
  - 安い
  - 早い
  - メンテナンスフリー
  - 柔軟
      - https://awswakaran.tokyo/xxx/ を https://www.awswakaran.tokyo/xxx/ にリダイレクトするまでできる
- ソースコード
  - https://github.com/awswakaran-tokyo/redirector
  - template なので「おまけ」の通りに設定するとすぐ使えます

## Firebase を利用したサーバーレスなリダイレクトシステム

もしかしたら今どきわざわざ www なしから www ありにリダイレクトする機会なんてないよというかたもいらっしゃるかもしれませんが、現にこういう機会はたまに訪れます。

時代が時代なら .htaccess を書き換えたり nginx.conf を書き換えたりして対処していたと思いますが、IaaS を建てないで大抵の問題が解決できる今の時代において、わざわざ Web サーバーを立てるのは得策とは言い難い状況です。

また、 Lambda や Cloud Functions といった FaaS による解決もできますが、コードの記述が必要であったり、デプロイが面倒であったり、リダイレクトを行うというだけの目的にはいささか too much であることは否めません。

そんなときに Firebase Hosting が便利です。Firebase Cloud Functions ではなく、Hosting の方です。

実は Firebase Hosting はただの静的サイトホスティングだけではなく、JSON の設定ファイルを書くだけでリダイレクトや Cloud Functions への接続ができる機能を搭載しています。

有名なものでいうと History API を利用した SPA 向けに、 404 になるものを全部 /index.html を読むように rewrite する設定などですね。

今回はこの Firebase Hosting の設定ファイルを記述することで、プログラムレスでありサーバーレスな、メンテナンスフリーのリダイレクタを作ってみます。

### プロジェクトの立ち上げ

<img width="1372" alt="Screen Shot 2019-07-26 at 16.35.30.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75633/c3b10efd-4be7-8c84-6f43-3e7abe378f26.png">

https://console.firebase.google.com/

事前にプロジェクトを立ち上げて、ホスティングをこの状態にしておいてください。

### Firebase Hosting の愚直な外部リダイレクト

できたら設定ファイルの記述です。

Firebase Hosting の URL 書き換えといえば、先程も紹介した SPA 向けの rewrites が有名な印象がありますが、実は普通に Redirect もできたりしちゃいます。

公式のドキュメントに書いてあるサンプルコードはこんな感じ。

https://firebase.google.com/docs/hosting/full-config#redirects

```js
"hosting": {
  // ...

  // Add the "redirects" attribute within "hosting"
  "redirects": [ {
    // Returns a permanent redirect to "/bar" for requests to "/foo" (but not "/foo/**")
    "source": "/foo",
    "destination": "/bar",
    "type": 301
  }, {
    // Returns a permanent redirect to "/bar" for requests to both "/foo" and "/foo/**"
    "source": "/foo{,/**}"
    "destination": "/bar"
    "type": 301
  }, {
    // Returns a temporary redirect for all requests to files or directories in the "firebase" directory
    "source": "/firebase/**",
    "destination": "https://firebase.google.com/",
    "type": 302
  } ]
}
```

あくまで内部向けのリダイレクトの補助として外向きへのリダイレクトもサポートしているように見えますが、バッチリ `/**` での全指定と 301 / 302 の指定もできることがわかります。

ただ、これだけではまだ不十分です。なぜならこのままの設定の場合、 `https://awswakaran.tokyo/posts/1` が `https://www.awswakaran.tokyo` へとリダイレクトしてしまい、目的を達成できません。

### Firebase Hosting の URL 引き継ぎリダイレクト

じゃあ Firebase Hosting で対応できないのかと言うと、そんな事はありません。

公式のドキュメントを読み進めていくと、「Capture URL segments for redirects」という項目が出てきます。そして、そこには以下のようなサンプルコードが書かれています。

https://firebase.google.com/docs/hosting/full-config#capture_url_segments_for_redirects

```js
"hosting": {
  // ...

  "redirects": [ {
    "source": "/blog/:post*",  // captures the entire URL segment beginning at "post"
    "destination": "https://blog.myapp.com/:post", // includes the entire URL segment identified and captured by the "source" value
    "type": 301
  }, {
    "source": "/users/:id/profile",  // captures only the URL segment "id", but nothing following
    "destination": "/users/:id/newProfile",  // includes the URL segment identified and caputured by the "source" value
    "type": 301
  } ]
}
```

これはもうめちゃくちゃ正解みたいなコードが出てきましたね。もともと `/blog/` でサブディレクトリ形式でサイトを運営していたものを静的サイト + WordPress に切り出したりするシチュエーションを想定しているように見えます。

### 最終的に仕上がる設定

上記を加味して設定を書くと、こんな感じになります。わかりやすいですね。

```js
{
  "hosting": {
    "public": "public",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
    "redirects": [
      {
        "source": "/",
        "destination": "https://www.awswakaran.tokyo/",
        "type": 301
      },
      {
        "source": "/:path*",
        "destination": "https://www.awswakaran.tokyo/:path",
        "type": 301
      }
    ]
  }
}
```

実際に 1 からプロジェクトを作って適用する場合はこんな感じでやってやります。
firebase-tools さえあれば完結するため、非常に便利です。

```bash
$ mkdir redirector
$ cd $_
$ yarn init
$ yarn add -D firebase-tools
$ yarn firebase init
$ touch firebase.json # 上記内容を記述
$ yarn firebase deploy --only hosting
```

### 動作確認

実際に動作確認してみます。
今回はこんな感じで curl を叩いてみましたが、ブラウザからでも問題ありません。

```bash
$ curl -L -v https://awswakaran.tokyo
* Rebuilt URL to: https://awswakaran.tokyo/
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0*   Trying 151.101.1.195...
* TCP_NODELAY set
* Connected to awswakaran.tokyo (151.101.1.195) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
# TLS 回りは省略
> GET / HTTP/2
> Host: awswakaran.tokyo
> User-Agent: curl/7.54.0
> Accept: */*
>
* Connection state changed (MAX_CONCURRENT_STREAMS updated)!
< HTTP/2 301
< server: nginx
< content-type: application/octet-stream
< location: https://www.awswakaran.tokyo/
< etag: W/"2c-LHG28cPXsOFuj9wy28J6HSC/mMQ"
< accept-ranges: bytes
< date: Fri, 26 Jul 2019 07:40:32 GMT
< x-served-by: cache-hnd18723-HND
< x-cache: HIT
< x-cache-hits: 1
< x-timer: S1564126832.987797,VS0,VE17
< vary: x-fh-requested-host
< content-length: 44
<
* Ignoring the response-body
{ [44 bytes data]
100    44  100    44    0     0    738      0 --:--:-- --:--:-- --:--:--   733
* Connection #0 to host awswakaran.tokyo left intact
* Issue another request to this URL: 'https://www.awswakaran.tokyo/'
*   Trying 13.115.18.61...
* TCP_NODELAY set
* Connected to www.awswakaran.tokyo (13.115.18.61) port 443 (#1)
* ALPN, offering h2
* ALPN, offering http/1.1
# TLS 回りは省略
> GET / HTTP/1.1
> Host: www.awswakaran.tokyo
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: nginx
< Date: Fri, 26 Jul 2019 07:40:32 GMT
< Content-Type: text/html; charset=utf-8
< Content-Length: 32505
< Connection: keep-alive
< Vary: Accept-Encoding
< Vary: User-Agent, X-Forwarded-Host, X-Device-Type
< Access-Control-Allow-Origin: *
< Content-Security-Policy-Report-Only: block-all-mixed-content; report-uri https://blog.hatena.ne.jp/api/csp_report
< P3P: CP="OTI CUR OUR BUS STA"
< X-Cache-Only-Varnish: 1
< X-Content-Type-Options: nosniff
< X-Dispatch: Hatena::Epic::Web::Blogs::Index#index
< X-Frame-Options: DENY
< X-Page-Cache: hit
< X-Revision: d3420d436d57c5474c042a3d3a49fa3e
< X-XSS-Protection: 1
< X-Runtime: 0.050989
< X-Varnish: 924127069 921277343
< Age: 68
< Via: 1.1 varnish-v4
< X-Cache: HIT
< Cache-Control: private
< Accept-Ranges: bytes
<
{ [15613 bytes data]
100 32505  100 32505    0     0   261k      0 --:--:-- --:--:-- --:--:--  261k
* Connection #1 to host www.awswakaran.tokyo left intact
```

バッチリですね。
一応ブラウザで動かした結果も載せておきます。

[![Image from Gyazo](https://i.gyazo.com/928ad0aeb5f173abdedc278ca9361801.gif)](https://gyazo.com/928ad0aeb5f173abdedc278ca9361801)

これで設定は完了です。
無料で動き、放置でよく、パフォーマンスも良好なリダイレクタが完成しました。

### その他検討したもの

- Netlify
  - はじめは実は Netlify でやってました
  - ただ `www.` も Netlify に向けてくれと言われるので Firebase に
  - レイテンシも Firebase Hosting のほうがない気がする（体感）
- GAE / CloudFunctions
  - 柔軟な設定のために一瞬やっても良いかなと思ったけれどコンピューティングリソース持ちたくないのでなし
  - こういうのは安定していて放置できるのが大切

## おまけ: 読まないで手っ取り早く試したい人向け

今回のリダイレクトツールは GitHub template として登録しているので、これを fork し、 README の通りにリダイレクト設定を書き換えるだけで OK です。

https://github.com/awswakaran-tokyo/redirector

具体的には .firebaserc と firebase.json をこんな感じに書き換えます。


#### .firebaserc

```diff
{
  "projects": {
-    "default": "awswakaran-tokyo"
+    "default": "your-firebase-project-name"
  }
}
```

#### firebase.json

```diff
{
  "hosting": {
    "public": "public",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
    "redirects": [
      {
        "source": "/",
-        "destination": "https://www.awswakaran.tokyo/",
+        "destination": "https://your-redirect-domain.example.com/",
        "type": 301
      },
      {
        "source": "/:path*",
-        "destination": "https://www.awswakaran.tokyo/:path",
+        "destination": "https://your-redirect-domain.example.com/:path",
        "type": 301
      }
    ]
  }
}
```

## 余談

Firebase はプロジェクト数の増枠申請をしていない人は一人あたり 5 プロジェクトまでしか使えません。
プロジェクトの使いすぎにご注意ください。

最近 Firebase Hosting の改修によって 1 つのプロジェクトあたり 36 個まで Web サイトをホスティングできるようになったため、これらをうまく使っていくことを推奨します。

https://firebase.google.com/docs/hosting/multisites?hl=ja