ぱろっと・すたじお

技術メモなどをまったりと / my site : http://parrot-studio.com/

async/awaitを投入したら便利だった件(あるいはAWS Lambdaで画像変換しようとしたら罠にはまった件)

今回は完全にメモ書きだし、たいしたことは書いてないのですが、
私自身がこれを書き残さないと後でまたはまりそうなので、
特に読まなくてもOKですΣ(・ω・ノ)ノ

API Gateway + Lambdaの画像変換ではまった話

本当に今さらなんですが、社内インフラで画像変換させるのが重すぎて、
他のサービスにも影響しそうだし、それ用のサーバ立てるのもめんどいってことで、
AWSに流しちゃえって話になったわけです

今だと「Lambda@Edge」の話ばかり出てくるのですが、
これは「CDNからLambdaをキック(して画像を変換する)」って仕組みなので、
社内と連携するなら「API Gateway+Lambda」という旧来の構成がいいかなと

構築したのは、API GatewayAPIとしてパラメータを受け取り、
Lambdaにeventとしてパラメータを流し、レスポンスをAPIとして返し、
エラーだったら500エラーで返す・・・みたいな標準的なやつです

これ自体はググれば山ほどやり方が出てくるので、
今回はどうでもいいのですが、
問題はLambda環境のimagemagickです

サンプルコードからしてJSというかES2016(=ES7)なので、
当然のようにES2016でコードを書いていて、手元の環境で動くことも確認したのに、
なぜかLambdaにデプロイすると動かないという・・・Σ(・ω・ノ)ノ

変換の手前までは動くことはログで確認できたので、
どうもimagemagick自体が動かないんじゃないか・・・と思ったら、
デフォルトのNode.js10系環境にはimagemagickが入ってなかったようで*1

LambdaのNode.jsを8.xにしたらあっさり動いたのですが、
これに気づくまで半日かかったのです
今さらこの仕組みを構築するって方は気をつけていただきたいщ(゚Д゚щ)


なお、API+Lambdaの環境だと、APIタイムアウトが29秒なので、
Lambdaの処理が29秒を超えると、応答がタイムアウトしてしまいます

現状だとAPI Gateway側のタイムアウトをこれ以上伸ばせないので、
結局、S3にアップロードされたことをトリガーにする仕組みも書いたのですが、
こちらはLambda側にテンプレートが用意されているのです

で、その環境のNode.jsが8.x指定なんですよね・・・
検証手順が逆ならば時間を無駄にしなかったのに・・・(´-ω-)

async/awaitが便利って話

ある意味ここまで前置きですΣ(・ω・ノ)ノ

ES6自体は以前からccptsで使っているわけですが・・・

github.com

ccpts.parrot-studio.com

・・・ES2016(かつてES7と呼ばれていたもの)から導入された、
「async/await」を使う機会がありませんでした

「イベントを数珠つなぎに処理する」のではなく、
「各componentがBusからイベントを勝手に受け取って動く」という疎結合な作りなので、
「連続した非同期処理を書く」ということが、あまりなかったのです*2

しかし、今回の画像変換は以下の3つの非同期処理をつなぐ必要があります

  • S3から画像をダウンロードする
  • 画像を加工する
  • 画像をS3にアップロードする

そもそも、Lambdaのテンプレートに、ご丁寧にも「async」が宣言されており、
これはもう使うしかないなと(`・ω・´)

ということで、「async/await」について細かく調べてみたわけですが、
新しいパラダイムを持ち込んだ難しい話ではなく、
「Promiseを使った非同期処理をいい感じに書ける仕様」でしかありません

もちろん、「いい感じに書けること」が最大のポイントでして、
そもそもPromise自体、「callback地獄をいい感じにしたい」から導入されたものですよね

qiita.com

ぶっちゃけ、これを読んでもらえれば、
もう私が書くことはないくらいなのですが、
Promiseの仕組み自体はjQuery全盛の時代から実装されています

  • 非同期処理でちゃんと正常系と異常系を処理したい
  • 非同期処理を連鎖させたい
  • 連鎖の途中でエラーになった場合に特別な制御をしたい

この要件を満たそうと、functionにfunctionを複数渡すことをネストさせた結果、
ぱっと見てわけがわからないコードができあがりました(lll゚Д゚)

それを改善したのがPromiseではあったのですが、
「つーかPromiseってなに? resolveとかrejectとかワカンネ」という、
概念としてやや複雑なものではありました

しかも、結局のところthenの中で別なfunctionを呼んでつなぐ構造は変わっておらず、
それぞれの処理を独立して「認識」できるかというと、結構面倒だったわけです

それが、「await」を使うだけでここまで単純化されます

try{
  // S3からダウンロード
  const image = await download(bucket, origin);
  // 画像変換
  const resized = await resize(image, ext, width, height);
  // S3に書き戻す
  const result = await upload(bucket, resized, image.ContentType, output);
} catch(e) {
  // エラーレスポンスを返す
}

一見、同期的なコードの羅列に見えますが、
裏ではちゃんと非同期的なcallbackで動いてブロックしないようになっており、
まさに「いい感じに書ける」仕様でございます( ゚д゚)o彡゚

要は「Promiseを返すと、resolveかrejectされるまで待って、
resolveした値を返し、rejectされたら例外をあげる」というだけの仕組みです
なので、function側はPromiseを返す仕組みにさえしておけば「いい感じ」になります

しかしですね・・・これだけだと
「そもそもPromiseがわかんないんだよぉぉぉぉっ」って問題を解決できません
そこで出てくるのが「async」です

「async」を宣言したfunctionは、暗黙的にPromiseに変換される・・・と書くと面倒そうですが、
要するにasyncを宣言して「普通に」書けばawaitできる、というだけです

// asyncをつけると、Promiseにラップしてくれる
async function resize(image) {

  // なんかresizeする処理

  if (err) {
    throw err; // 例外はrejectされる
  } else {
    return resizedImage; // 通常のreturnはresolveされる
  }
}

// resizeが完了するまで待って、終わったら結果がresizedに代入され、エラーなら例外
const resized = await resize(image);

Primiseの概念がわからなかったとしても、
「asyncつきでなんか非同期処理を書けば、いい感じにawaitできる」と覚えておけば、
それだけで直感的なコードになるわけです(`・ω・´) b

「直感的である」「わかりやすい」ということは、
「間違いを見つけやすくなる(ので品質が上がる)」ということでもあります
なので、積極的に採用していきたいところです

強いていえば、ES2016が動く環境か、
トランスパイラ経由のデプロイが整備された環境でしか使えないので、
サーバサイドエンジニアとしては、そこをどうにか頑張っていきたいところですね(`・ω・´)

おまけ:API GatewayとS3、どちらがトリガーとして最適か?

S3の方がトリガーとしてシンプルだし、Lambda@Edgeもその延長線なのだから、
そっちが本命・・・というのは事実だと思います

ただ、「画像をアップロードした」がトリガーだと、
「どうやって」とか「どこに」という変換の付帯情報が渡せないわけです(´-ω-)

変換処理ごとにLambdaを立てるってのが、
マイクロアーキテクチャ的には適切なのですが、
細かくたくさんの環境を作ると、管理できなくなると思うわけです

その点、APIであれば細かなパラメータを外部からコントロールでき、
Lambdaに情報を引き渡すことができるので、
汎用的に使うのには楽だと思います(`・ω・´)

一方で、わざわざhttpで通信するオーバーヘッドはあるし、
なにより(現時点では)タイムアウトの問題もあるので、
「汎用的な画像変換API」自体が適切か・・・というと、難しいところですね

結局のところ、どういうフローの中でどう使うか・・・なので、
「Lambdaを使えば画像変換が簡単」といっても、
考えることはたくさんありますね(´-ω-)

*1: サンプルコードにあるgmモジュールを、わざわざ手動でuploadしないといけないから、おかしいな・・・と思ってはいたのですが・・・(´-ω-)

*2: 詳しくはFRPについて書いた記事にて(´・ω・)っ https://parrot.hatenadiary.jp/entry/2015/11/29/175113