Fastly の Image Optimizer と VCLによる制御


```
特に記載がない限り本記事の記載内容は Fastly のデフォルト設定での挙動となります。
Fastly の正式なサポート内容ついては以下のドキュメントをご参照下さい。
サポートページ: https://docs.fastly.com/
日本語(一部のみ): https://docs.fastly.com/ja/
```



# この記事の内容について

Fastly のオプション機能であるイメージオプティマイザー（以降 IO）を利用すると画像に Fastly 側で様々な処理を行うことが可能となります。
代表的な処理としてはサイズ、品質の変換やクロッピング(指定した箇所を画像から切り出し)などがあります。Chrome などの Webp に対応しているブラウザに対して自動的に webp フォーマットに変換することなども可能です。
この記事では Fastly IO の基本的な使い方と、VCL を利用した変換のテクニックをご紹介したいと思います。

なお、Fastly IO は有償のオプションサービスとなっています。開発者アカウントではご利用できません。詳細については japan@fastly.com までお問い合わせ下さい。

# 利用手順
詳細な利用手順は[オフィシャルドキュメント](https://docs.fastly.com/ja/guides/setting-up-image-optimization)に書かれているので、ここではざっくりと必要な作業を説明します。

1. IO を有効化したいサービスに[オリジンシールド](https://docs.fastly.com/ja/guides/shielding)を設定し有効化(Activate)
2. Fastly に有効化したいサービスの ID を連絡
2. 処理対象の画像のリクエストに IO ヘッダーを付与する設定を追加
3. 処理対象の画像のリクエストに処理内容を指示するクエリパラメータ(ex, width=300)を付与する

※ イメージオプティマイザーを利用するには[オリジンシールド](https://docs.fastly.com/ja/guides/shielding#%E3%82%AA%E3%83%AA%E3%82%B8%E3%83%B3%E3%82%B7%E3%83%BC%E3%83%AB%E3%83%89%E3%82%92%E6%9C%89%E5%8A%B9%E3%81%AB%E3%81%99%E3%82%8B) が有効化されている必要があります。詳細は[イメージオプティマイザーの設定](https://docs.fastly.com/ja/guides/setting-up-image-optimization)をご参照下さい。


付与可能なクエリパラメーターの一覧は以下のページから確認できます。
IO API: https://docs.fastly.com/ja/api/imageopto/


# 基本的な使い方

それでは [Fastly のデモページ](https://io.fastlydemo.net/io) から実際に処理された画像を見てみましょう。このデモページでは画面からダイナミックに出力サイズや品質を変更した画像を表示することが出来ます。

![Fastly_Image_Optimizer_Demo___.jpg](https://qiita-image-store.s3.amazonaws.com/0/128569/a5b19c38-7ad3-d484-4072-3965c727f7fd.jpeg)


オリジナル画像のURL: https://www.domfee.com/img/fastly-io/img-1.io.jpg
IO処理後のURL: https://www.domfee.com/img/fastly-io/img-1.io.jpg?width=893&quality=75&format=pjpg


ここでは、オリジナルの画像は 4000 x 3000 で 3.08MB の JPEG です。
その画像に対して `width=893&quality=75&format=pjpg` というクエリパラメーターを付与してリクエストすることで画像のサイズと品質を変換し、83.18KBのサイズに変換されています。



このように画像をリクエストする際に、クエリパラメーターで処理内容を指示するというのが Fastly IO の基本的な使い方になります。
イメージ変換サービスを提供している会社は色々とありますが Fastly と同様にクエリパラメーターで変換内容を指定するサービスが一般的です。

ただ、業界標準の指定方法があるわけではないので、例えばA社では横幅指定をする際には `w` と指定、B社では `width` と指定というようにサービスの提供業者によってパラメーターの名前は異なる可能性がある点は注意が必要です。

#  Image Optimizer を適用する対象を指定

イメージオプティマイザーを適用するにはリクエストに `http.x-fastly-imageopto-api` ヘッダを付与します。UI からも設定可能ですが、より細かい設定が可能ですので以下のようなコードを VCL Snippet で設定するのがおすすめです。


```:vcl_recv
if (req.url.ext ~ "(?i)^(gif|png|jpe?g|webp)$" || req.url.path ~ "^/image/") {
	set req.http.x-fastly-imageopto-api = "fastly";
}
```

この例では指定されたイメージの拡張子を持つ、もしくはリクエストされたパスが `/image/` で始まる場合にヘッダーを付与しています。

サービスの Activate が完了すると、対象のリクエストはクエリパラメータで指定した値に応じて画像が変更されます。


# VCL を使って変換内容を制御

Fastly ではクエリパラメーターを利用するのではなく、VCL を利用して画像の変換内容を制御することが出来ます。

変換内容を VCL でコントロールする場合、クエリパラメーターで指定する方法と比較して以下のようなメリットが考えられます。

 - **エンドユーザー側で勝手に意図しないクエリパラメーターをつけられても問題にならない**
 - **画像リンクにクエリパラメーターを付与する必要がないため URL がシンプルになる**
 - **デザイナーや開発者は画像の URL を指定するだけで良いので開発時のミスと運用負荷の軽減**
 - **変換内容を一元的に管理できるので、将来的に画質を変えたくなった場合にも簡単に対応が可能**
 - **IO 導入前に外部に配信した画像リンクに対しても画像変換の適用が可能**

変換するための条件は User-Agent の内容、またはリクエストのパスなど色々と考えられると思いますが、ここでは User-Agent からデスクトップ向け、モバイル向けに自動に最適なサイズに画像を変換するケースを考えてみます。

考え方としてはシンプルです。Fastly がユーザーからリクエストを受けたタイミングで Uesr-Agent を確認し、デバイスのタイプに合わせたクエリパラメーターを設定する流れになります。具体的には以下のようなコードで実現できます。

```:vcl_recv
if (req.url.ext ~ "(?i)^(gif|png|jpe?g|webp)$" || req.url.path ~ "^/image/") {
  set req.http.x-fastly-imageopto-api = "fastly";

  if(fastly.ff.visits_this_service == 0) {
    if(client.platform.mobile) {
      set req.url = querystring.set(req.url, "width", "400");
    } else {
      set req.url = querystring.set(req.url, "width", "800");
    }
  }
}
```

まず最初に IO を有効化する対象をイメージ拡張子、`image` パス配下に指定してヘッダーを追加しています。

その後、処理が エッジ POP で1回だけ実施されるように `fastly.ff.visits_this_service == 0` を指定し、続いてリクエストに付与されている User-Agent からモバイルかどうかを判断しています。
`client.platform.mobile` は User-Agent から判断したデバイスが mobile だった場合に true になります。

`querystring.add` はクエリパラメーターを追加するための関数です。

モバイルからのアクセスと判断された場合 `width=400` を、それ以外の場合は `width=800` というクエリパラメーターを追加しています。

つまり、元々のURLが `/image/aaa.jpg` だった場合、この処理が実施されるとリクエストされた URL は以下のように変換され、Fastly IO で画像の変換処理が実施されます。

モバイルの場合: `/image/aaa.jpg?width=400`
それ以外: `/image/aaa.jpg?width=800`


※ その他のクエリパラメーターを操作するための関数は [Query string manipulation VCL features](https://docs.fastly.com/vcl/query-string-manipulation/) をご参照下さい。

なお、上述のサンプルではスマートフォンからのアクセスを識別するために `client.platform.mobile` ファンクションを利用していますが、以下のようにモバイルを識別するための文字列を VCL で直接指定することも可能です。

```:vcl_recv
if (req.url.ext ~ "(?i)^(gif|png|jpe?g|webp)$" || req.url.path ~ "^/image/") {
  set req.http.x-fastly-imageopto-api = "fastly";

  if(req.http.User-Agent ~ "(?i)(Android|iPhone|mobile)") {
    set req.url = querystring.set(req.url, "width", "400");
  } else {
    set req.url = querystring.set(req.url, "width", "800");
  }
}
```

この例では User-Agent に大文字小文字を問わずに Android, iPhone, mobile などの文字列が含まれる場合にモバイルと判断してモバイル用の横幅のクエリパラメタを設定しています。

# Edge Dictionary を利用して変換内容をダイナミックに制御

先程のサンプルでは VCL のコードの中に変換するサイズを直接指定していました。変換パターンが少なかったり変換サイズが将来的に変更にならないのであればよいですが、もっと変換のパターンが多かったり、変換パターンを一元的に管理したい場合は [Edge Dictionary](https://docs.fastly.com/guides/edge-dictionaries/) を利用すると便利です。

Edge Dictioanry は Key と Value を含むことが出来るテーブルのようなもので、VCL の各サブルーチンから Value を呼び出して利用することが出来ます。また、Edge Dictionary に含まれる内容は UI または API を通じて Fastly Service のバージョン変更なしに書き換えることが出来るので、頻繁に変更がある内容や特定の処理を一元的に管理したい場合などに役立ちます。

それでは実際のコードを見てみます。まずは Fastly のコントロールパネルの UI などを通じて以下の内容を含む Edge Dictionary テーブルを作成します。

"mobile_width": "200",
"other_width": "500",

VCL には以下のようなコードが追加されます。

```:vcl_init
table auto_io {
    "mobile_width": "200",
    "other_width": "500",
}
```

vcl_recv に含めるコードは先程のコードとほぼ同じです。ただし、ここでは横幅の指定を直接しているするのではなく Edge Dictioanry のテーブルから呼び出します。
テーブルから呼び出す場合は `table.lookup(<table_name>, "<key>")` というようにテーブル名と Key を指定します。上記のテーブルから呼び出すコードは以下のようになります。


```:vcl_recv
if (req.url.ext ~ "(?i)^(gif|png|jpe?g|webp)$" || req.url.path ~ "^/image/") {
  set req.http.x-fastly-imageopto-api = "fastly";

  if(fastly.ff.visits_this_service == 0) {
    if(client.platform.mobile) {
      set req.url = querystring.set(req.url, "width", table.lookup(auto_io, "mobile_width"));
    } else {
      set req.url = querystring.set(req.url, "width", table.lookup(auto_io, "other_width"));
    }
  }
}

```

こうすることで width に設定される内容が Edge Dictionary から取得されます。Edge Dictionary の内容は簡単に UI や API を通じて変更することが出来ます。変更後もサービスのバージョンは変わらず、変更内容は通常10秒程度でネットワークに反映されます。

このサンプルでは User-Agent の内容でシンプルに2種類の変換を指定していますが、リクエストパスとの組み合わせやユーザーの種別（例えばゲストユーザーと有料ユーザー）により表示する画像の画質を変換するなどと行った処理も実装することが可能です。

Edge Dictionary を利用するデメリットとしてはコードが若干ですが複雑になります。ただコードは一度作成してしまえば変更は必要ありませんので、変換のパターンが数種類以上あるのであれば Edge Dictionry の利用を検討する方が将来的には管理が簡単になるのではないかと思います。

# AVIF を WebP よりも優先

AVIF フォーマットに対応しているクライアントには WebP ではなく AVIF フォーマットに変換した画像を返却します

```:vcl_recv
  # Set format prioritize avif > webp
  if (req.http.Accept ~ "\bimage/avif\b") {
    set req.url = querystring.set(req.url, "auto", "avif");
  } elsif (req.http.Accept ~ "\bimage/webp\b") {
    set req.url = querystring.set(req.url, "auto", "webp");
  }
```

# その他のパラメータの取得方法

ここまではイメージ変換のパラメータを VCL で指定していますが、たとえばファイル名やパスの情報から取得して設定することも可能です。

### パラメータをファイル名の一部から取得
例えば画像のファイル名が `sXXX_filename.jpg` というような形式で `XXX` に画像の横幅が含まれている場合、以下のように正規表現を利用して数字を取得してリクエストファイル名を再構築することが出来ます。

```:vcl_recv
  if (req.url.basename ~ "^s(\d+)_(.*)$") {
    set req.url = req.url.dirname "/" re.group.2;
    set req.url = querystring.set(req.url, "width", re.group.1);
  }
```

変更前: /images/s800_filename.jpg
変更後: /images/filename.jpg?width=800

### パラメータをリクエストパスから取得

続いてパラメータの値をパス情報から取得するサンプルです。このコードではパスの2階層目をリクエスト URL から取り除き、取り出した値をクエリパラメータ width として設定しています。


```:vcl_recv
  if (req.url ~ "^(/[^/]*)/([^/]*)(.*)") {
      set req.url = re.group.1 re.group.3;
      set req.url = querystring.set(req.url, "width", re.group.2);
  }
```
変更前: /image/500/filename.jpg
変更後: /image/filename.jpg?width=500

このように正規表現を利用することでリクエストに含まれる情報から自由に値を取得して IO のパラメータとして利用することが出来ます。

# VCL コードサンプル

Fastly の開発者ツールである Fastly Fiddle で IO の動作を確認するサンプルを用意しました。実際の変換結果や、挙動のコントロールを行う VCL を確認することが出来ます。

サンプルには以下の様な挙動が設定されています。
・変換サイズなどは Edge Dictionary(INIT内のコード）で管理
・User-Agent がモバイルかどうかで横幅を変更
・Edge Dictionary の copyrigt が `true` の場合はオーバーレイ機能を利用して画像にコピーライトを表示
・copyright 用の画像の path も Edge Dictionary で指定
・リクエストされた画像のファイル名が Edge Dictionary に存在する場合は指定された横幅を使用
・変換に利用されたパラメータやロジックの情報をレスポンスヘッダーとして返却




https://fiddle.fastlydemo.net/fiddle/d9414d1d

右上の **RUN** をクリックするとリクエストが送られ以下の3種類の変換結果が表示されます。

1. 通常の User-Agent でのアクセス
User-Agent がデスクトップと判断され、Edge Dictionary からデスクトップ用の横幅が設定されます。 

2. モバイルの User-Agent でのアクセス
User-Agent が mobile と判断され、Edge Dictionary からモバイル用の横幅が設定されます。 

3. ファイル名マッチ
リクエストされた画像ファイルの名前が Edge Dictionary に指定されているものと一致するので、そのファイル用に指定された横幅が設定されます。

上記の Fiddle ページは変更出来ませんが、右上のハンバーガーメニューから Clone することでリクエストパスや VCL コードを書き換えてテストすることが出来ます。

# 変換結果の確認
IO で処理された画像のレスポンスには、変換前後のフォーマットやサイズなどの情報を含む `fastly-io-info` ヘッダーが付与されています。:


```
fastly-io-info: ifsz=108501 idim=827x788 ifmt=jpeg ofsz=13066 odim=300x286 ofmt=jpeg
```

インプットファイルの
ifmt:フォーマット
idim: 画角
ifsz: サイズ(bytes)

アウトプット（変換後）ファイルの
ofmt: フォーマット
odim: 画角
ofsz: サイズ(bytes)

このヘッダーを確認することで変換が意図した通りに行われているかや、圧縮効率を確認することが出来ます。

# まとめ

Fastly の VCL を利用すると画像変換を柔軟にコントロールすることが可能です。Fastly IO を実装する際はクエリパラメーターで指定する方法とあわせて VCL で変換内容を制御する方法もニーズにあわせてご検討頂ければと思います。

また、このページでは VCL で変換内容を指定する方法をご紹介しましたが、例えば別のサービスや自前で変換を行っていた場合にも VCL を利用して古いパラメーター指定フォーマットを利用したまま Fastly IO に切り替えることも可能です。
VCL を利用したパスやパラメーターの書き換えは正規表現を利用することが多いです。手順については[Fastly で正規表現を使った書き換え処理](https://qiita.com/AtsushiFukuda/items/a8e655d5d4bc2565d6e3)を参照してみて下さい。