ブログの画像配信を Google Photos から Cloud Functions + Cloud Storage に変更した


背景

このサイトは Jekyll でビルドした静的 html を Firebase Hosting で配信していて、貼り付けている写真は Google Photos にアップロードした写真の直リンクを Google Picker API で生成し埋め込んでいた。直リンクは path 内で /s1680/ とか /s800/ といった指定をして好きな大きさに動的リサイズでき、 ImageMagick 等が不要で便利だった。

しかし Google 側の仕様変更で直リンク画像が表示できなくなった。はてなブログでも同じ問題が起こっている。

Google Photos の性質からして public に画像配信するものではないし、今後こうした使い方が許されるのも期待できない。大人しく別の配信方法を模索することにした。

画像配信に求めること

以下の要件を満たせる方法を考えた。

  • クラウド上で写真データの管理、リサイズ処理が完結する
    • データ量さえ最適化されたら良いので、動的リサイズは無くても構わない
  • コマンドやブラウザから画像をアップロードできる
  • 自分のサイトくらいのアクセス数であれば無料で使える

流石にニッチだし Google Photos 以外にこれらを満すサービスはなさそうで、自前で簡単な画像配信システムを書くことにした。

Cloud Storage for Firebase を使った画像配信

自分でコードを書かず Firebase に簡単な機能をデプロイする Firebase Extensions というものがある。この中に Resize Images があり、これを使うと Cloud Storage に画像をアップロードすると好きな大きさのリサイズ画像を自動生成できる。

面白そうで調べると同様のことをやっている記事もあり参考にさせてもらった。

Firebaseで作る!リアルタイム画像変換CDN【Firebase Hosting + Cloud Functions】 - AppBrew Tech Blog

全体像はこのようになる。

  • ブログに貼りたい画像を Cloud Storage にアップする(CLI・Web ダッシュボードから簡単にできる)
  • アップが完了したら Resize Images がキックされ、予め設定した大きさのリサイズ画像が生成される
  • 画像には https://horimisli.me/images/sample.jpg のようにブログ配信で使っているドメインでアクセスできるようにする
  • Cloud Storage 上のファイルは直接カスタムドメインでアクセスできないので、 /images/... へアクセスすると Storage 上の画像を返す Cloud Functions 関数を用意する

次に今回行った作業を書く。

Cloud Storage で Resize Images を有効化する

ブログ配信に使っている Firebase Hosting と同じプロジェクトで Resize Images をインストールする。

Firebase Extensions | Resize Images

今回は /images/... へのリクエストで関数を起動させるため Firebase Hosting と Cloud Functions の連携が必要。現状これは us-central1 でしかサポートされていないのでロケーション選択に注意する。

重要: Firebase Hosting は、 us-central1 でのみ Cloud Functions をサポートします。
Cloud Functions を使用した動的コンテンツの配信とマイクロサービスのホスティング  |  Firebase

生成するリサイズ画像は十分な解像度のものをワンサイズだけ用意することにした。例えば 1680x1680 と設定しておくと、アスペクト比を維持しつつ長辺を 1680px 以内におさめてくれる。

Resize Images Extension セットアップ

生成されたファイルはこんな感じ。

Resize Images で生成したファイル

Firebase Hosting、Cloud Functions、Cloud Storage の連携

アップロードした画像は Firebase Hosting にも使っているドメインで https://horimisli.me/images/sample.jpg のようにアクセスしたい。

Firebase Hosting の Rewrite 設定を使うと、 /images 以下へのリクエストを Cloud Functions に流すことができる。この関数内で Cloud Storage Bucket へアクセスし指定された画像を取得、レスポンスで返すようにする。

// firebase.json
{
  "functions": {
    "source": "functions",
    "runtime": "nodejs10"
  },
  "hosting": {
    "public": "_site",
    "rewrites": [
      {
        "source": "/images/**",
        "function": "onRequestResizedImage"
      }
    ]
  }
}

実際にこのブログで動いている関数はこれ。/sample.jpg にアクセスすると、Firebase が生成したリサイズ画像(今回はワンサイズなので固定で sample_1680×1680.jpg)をレスポンスで返す。

'use strict';

const admin = require("firebase-admin");
const functions = require("firebase-functions");
const maxResizedSide = 1680;

admin.initializeApp();

exports.onRequestResizedImage = functions
  .runWith({
    timeoutSeconds: 300,
    memory: "1GB",
  })
  .region("us-central1")
  .https.onRequest((req, res) => {
    // ["filename", "jpg"]
    const fileNameComponents = req.path.substr(1).split("."); 
    // ex.) "filename_1680x1680.jpg"
    const filePath = `${fileNameComponents[0]}_${maxResizedSide}x${maxResizedSide}.${fileNameComponents[1]}`;

    admin
      .storage()
      .bucket()
      .file(filePath)
      .get()
      .then((data) => {
        const file = data[0];
        res.set("Cache-Control", 'public, max-age=604800');
        res.set("Content-Type", file.metadata['contentType']);
        file.createReadStream().pipe(res);
        res.status(200);
      })
      .catch((err) => {
        console.log(err);
        res.status(500).send(err);
      });
  });

Cloud Storage への画像アップロード

Google Cloud SDK に含まれている gsutil を使うのが手軽。画像の元データは Jekyll ソースリポジトリ直下の images/ で管理し、新しく追加したら CLI で Storage にもアップする。

gsutil cp images/sample.png gs://my-blog-project.appspot.com/images/

記事の markdown には、オーソドックスな以下の記法で画像を貼る。

![記事内の画像](/images/sample.jpg)

ここが少しトリックで、ローカルの jekyll サーバで記事プレビューする時は images/ 下の元画像が http://localhost:4000/images/sample.jpg として表示される。記事を書き終え Firebase Hosting にデプロイするとリンク先が https://horimisli.me/images/sample.jpg になり、先に説明した Rewrite 設定で Cloud Functions が起動、 Cloud Storage 上のリサイズ画像が表示される。執筆時点では images/ に画像を置くだけでよくて、公開可能になったタイミングで Storage に画像をアップすればいい。

問題点

Resize Images Extension は画像変換処理を一切書かなくていい手軽さがあるけど、アニメーション gif に対応していないなど不便な所もある。また HEIF 形式は対応しているものの、リサイズ画像も HEIF のままになるのでブラウザでレンダリングできない問題がある。Google Photos は直リンクで HEIF 画像にアクセスすると JPEG ファイルを返してくれるので便利だった。

Cloud Functions は Storage へファイルアップロード完了時に発火する onFinalize があり、これをトリガーに起動する関数で独自のリサイズ処理を書けば解決はできる(Resize Images も同じ仕組みで動く関数になっている)。

Cloud Storage トリガー  |  Firebase

今のところそこまで困っていない問題なので、今回は Resize Images をそのまま使うことにした。

まとめ

Firebase の各種機能を組み合わせて簡単な画像配信システムを組む方法を紹介した。

過去記事に埋め込まれていた googleusercontent.com ドメインの画像も一通り書き換え、画像が表示できるようになった。外部サービスのドメインがたくさん埋め込まれてると置き換えが大変なので、手間でも独自ドメインにしておくと後々便利だなと再確認した。