HerokuからGoogle App EngineのImage APIを使ってGoogle Cloud Storageにある画像を動的変換する

はじめに

Chief Technology Officerの高丸です。

シンガポールは11-1月は雨季と言われますが、12月に入ってからは全然雨が降らなくなりました。(降ることは降るのですが、夜にさっと降ったりする程度です。)

梅雨明け太夫が、梅雨明け宣言したんですね。

日本のみなさん、シンガポールは暖かいですよ〜。

Heroku で画像変換

さて、HerokuなどのPaaSで運用している場合、たくさんのアドオンが用意されているので、基本、機能実装に困ることはないのですが、
ちょっとサーバーサイドで凝ったことをやろうとしたり、コスト安く済ませようとするとどうしようか考えることがあります。

その一つに画像変換があると思います。

Cloudinaryのアドオンを使えば、500MBまでであれば無料で使えますが、
それを超えると、次は$50/月(10GBまで)のプランになり、継続的に使っていくとコストに響いてきます。

f:id:takamario:20161217144345p:plain

画像変換のインフラ構成例

通常の構成例

前提

「アップロードされた画像を変換し、アプリケーションで使う」として話を進めます。

まず、画像アップロード先の容量をできるだけ気にしなくて良いようにするのがポイントだと思います。
となると、Amazon S3Google Cloud Storage(GCS)をストレージに選ぶことが一般的でしょうか。

(a) 画像変換サーバーを立てる

Amazon EC2Google Compute Engine(GCE)で、そこにApacheやNginxなどの画像変換サーバー(w/ small_lightモジュール)を構築し、S3やGCSから画像を返す、といったやり方が多くあると思います。

f:id:takamario:20161217160337j:plain

  • メリット
    • EC2/GCEインスタンスはスケールできる
  • デメリット
    • インスタンスの利用コスト
    • 画像変換サーバーの構築・管理が必要

(b) 事前に変換済み画像を用意しておく

最近だと、サーバーレスアーキテクチャということで、Lambdaファーストな構成が取られることも非常に多くなってきました。 LambdaのインスタンスにはImageMagickがインストールされているので、変換処理を書いてあげるだけです。

イベント駆動で非同期に処理できるのが魅力的で、画像処理といったものにも向いていたりします。

画像がアップロードされた時に発生するイベントをもとに、AWS LambdaまたはGoogle Cloud Functions(GCF)が動き、アプリケーションで必要な画像に変換します。

f:id:takamario:20161217161346j:plain

ただ、これは事前に作っておくタイプなので、動的と呼ぶには違う感じですね。

  • メリット
    • Lambdaはほぼ無料
    • Lambdaインスタンスはスケールできる
    • 画像変換サーバー構築不要(Lambda Function用のコーディングは必要)
  • デメリット
    • 使う前に、変換済のものを作成しておかなければならない
    • 変換パターンが増えると、既にアップロードされている画像は再作成必要

(c) LambdaをAPIとして使う(*2017-12-18追記)

このパターンを忘れていたので、追記しました。 LambdaのインスタンスにはImageMagickがインストールされているので、APIとして変換した画像を返すことも可能です。 おそらく、Amazon API Gatewayなんかと一緒に使うか、LambdaのInvoke APIをコールすることになると思います。 GCPでは、Cloud Endpoints→Cloud Functionsの連携がまだできないようで、HTTP Triggersを使って、Cloud Functionsをコールすることで、同様のことができそうです。

f:id:takamario:20161218165119j:plain

  • メリット
    • Lambdaはほぼ無料
    • Lambdaインスタンスはスケールできる
    • 画像変換サーバー構築不要(Lambda Function用のコーディングは必要)
    • API GatewayでAPIの認証が可能
  • デメリット
    • API Gatewayの利用料がかかる
    • 画像変換のURLパラメーター処理を自分で実装する必要がある

デメリット少なそうですね。

(d) Google App Engine(GAE)のImage APIを使う

Google App Engine、日本にもやってきましたね。アツいんです!

GAEのインスタンスからコールできるImage APIというものがあり、それがなんと無料で画像変換を提供してくれています!

f:id:takamario:20161217162529j:plain

まず、(1)は通常のアップロードですが、(2)でGAE経由でImage APIをコールします。そうすると、ユニークなURL(ドメインはGoogleのCDN?)が発行され、このURLにパラメータを付けることで、(3)の動的変換が可能になります。

なので、アップロード後にユニークURLだけをDB等に保存しておけば、アプリケーションの中ではパラメータを付けたURLを作るようにすればOKです。

ざっくりとURLを発行する部分のコード(Go)ですが、

package server

import (
    "fmt"
    "net/http"
    "path/filepath"

    "google.golang.org/appengine"
    "google.golang.org/appengine/blobstore"
    "google.golang.org/appengine/image"
)

func init() {
    http.HandleFunc("/", handler)
}
func handler(w http.ResponseWriter, r *http.Request) {
    bucketName := r.URL.Query().Get("bucket_name")
    objectKey := r.URL.Query().Get("object_key")
    gsPath := filepath.Join("/gs", bucketName, objectKey)

    ctx := appengine.NewContext(r)
    blobKey, err := blobstore.BlobKeyForFile(ctx, gsPath)
    if err != nil {
        fmt.Errorf("%v", err)
    }

    opts := image.ServingURLOptions{Secure: true}
    url, err := image.ServingURL(ctx, blobKey, &opts)
    if err != nil {
        fmt.Errorf("%v", err)
    }

    fmt.Fprint(w, url.String())
}

と、google.golang.org/appengineのパッケージを使ってユニークURLを取得しているだけという、超簡単なものです。これだけで良いのです。

ただ、これは画像URLがGoogleのドメインになってしまうというデメリットもあります。また、画像URLに権限設定はできないので、公開しても良いということが条件になります。(URLはランダムなので推測されることはほぼないが、知られた場合誰でもアクセスできる。)

使い所は限られるかもしれませんが、サーバー構築なくほぼ無料で作成できます。

  • メリット
    • GAEは、28インスタンス時間(インスタンス数 x 時間)まで無料、Image APIも無料
    • GAEインスタンスはスケールできる
    • 画像変換サーバー構築不要(Image APIをコールするアプリケーションは必要)
  • デメリット
    • Image APIが、いつまで無料かは不明
    • Image APIのサービスレベルに依存する
    • URLがGoogleドメインになる
    • GoogleのCDNドメインのサービスレベルに依存する
    • 画像の権限付与は不可、公開前提となる
    • GAE Standard Environmentのため、GAEで動かすアプリは、Go/Python/Java/PHPに限定される(が、難しいコーディングはほぼない)

(もしかして、デメリット多すぎ。。?)

Image APIに関して

Image APIの制限としては、1回の処理で扱えるファイルサイズが32MBまでですが、フルサイズ一眼レフの解像度でなければそこまでは達しないと思うので、普通の規模であれば何も問題ないと思います。

GAEのドキュメントには、リサイズとクロップ程度しかパラメーターは載っていませんが、いくつかあるので以下に載せておきます。

  • パラメータ

    • ハイフンでつなぐ
      • e.g. http://[image-url]=s200-fh-p-b10-c0xFFFF0000
  • リサイズ・クロップ

    • s640 — 長辺を640pxに合わせて、縦横比そのままで、リサイズ
    • s0 — オリジナルサイズ
    • w100 — 幅を100pxに合わせて、リサイズ
    • h100 — 高さを100pxに合わせて、リサイズ
    • s(値なし) — 縦横比変えて、リサイズ(w・hと合わせて使う)
    • c — クロップ(値なしは画像中央正方形切り抜き、w・hと合わせると縦横比は変えずに、指定した幅・高さで切り抜き)
    • p — 顔判定クロップ(プロフィール画像用?)
    • cc — 円形クロップ
    • ci — 短辺に合わせて正方形クロップ
  • 回転

    • fv — 垂直回転
    • fh — 水平回転
    • r{90, 180, 270} — 時計回りに回転
  • フォーマット変換

    • rj — JPEG出力
    • rp — PNG出力
    • rw — WebP出力!
    • rg — GIF出力

おわりに

今回は、QCDでいうCを抑えるべく、お手軽なやり方ということで、GAEを使ってみました。

個人的な経験として、リサイズ程度であれば、動的変換の負荷で困ることはほとんどなく、むしろアプリケーション仕様として、画像サイズのパターンが増え、その対応をひとつひとつやらなければいけないことの方がよっぽど大変でした。

GCPのプロダクト、およびGAEに関しては、また記事を書いていきたいと思います。

参考

suguru03.hatenablog.com kaichu.io stackoverflow.com qiita.com