サーバーレスなDropboxアプリの構築例 "LightFile for Dropbox"

# LightFile for Dropbox

Jpeg画像を劣化を感じない範囲でいい感じに再圧縮してDropboxの空き容量を稼ごうぜ、という処理を自動で行うDropboxサーバーアプリです。

[LightFile for Dropbox](https://dropbox.lightfile.net/)
<a href="https://dropbox.lightfile.net/"><img width="320" alt="screenshot.png" src="https://qiita-image-store.s3.amazonaws.com/0/35178/112c717f-b7bf-787c-3c77-f23144f08dd5.png"></a>

Dropboxアプリというとスマホクライアントアプリを想像しますが、豊富なAPIを使ってDropboxをファイルシステムとするサーバーアプリも独自に開発することができます。

LightFile for Dropboxではこれを**サーバーレス**で**クラウドネイティブ**に構築しました。

**急にユーザーが増えたら自動的にインフラが拡張されるし、誰も使わなければコストがかからない。しかも僕らは何も保守しなくていい**ともうエンジニアをダメにする楽ちんさです。

ほとんど定石ではあるんですが、利用技術を紹介します。**Dropbox上のファイルを自動処理するアプリや、サーバーレスなWebアプリ**を設計する上での参考になれば幸いです。

# 利用技術

* インフラ
    * `処理系` [AWS Lambda](https://aws.amazon.com/jp/lambda/)
    * `Webゲートウェイ` [API Gateway](https://aws.amazon.com/jp/api-gateway/)
    * `DNS` [Route53](https://aws.amazon.com/jp/route53/)
    * `SSL証明書` [Certificate Manager](https://aws.amazon.com/jp/certificate-manager/)
    * `データベース` [DynamoDB](https://aws.amazon.com/jp/dynamodb/)
    * `分散処理のためのメッセージング` [SNS](https://aws.amazon.com/jp/sns/)
  * `Dropboxのファイル変更検知` [Dropbox Webhooks](https://www.dropbox.com/developers/reference/webhooks)
  * `開発環境でWebhookを受信` [ngrok](https://ngrok.com/)
* 開発言語(JavaScript)
  * `実行環境`[　Node.js](https://nodejs.org/ja/)
  * `Dropbox SDK` [dropbox](https://www.npmjs.com/package/dropbox)
  * `Webアプリケーションフレームワーク` [Express](https://expressjs.com/ja/)
  * `テンプレートエンジン` [Pug](https://pugjs.org/api/getting-started.html)
  * `サーバーレスフレームワーク` [Serverless](https://serverless.com/)
    * `開発用Webサーバ` [serverless-offline](https://www.npmjs.com/package/serverless-offline)
    * `Expressバインド` [serverless-express](https://www.npmjs.com/package/serverless-express)

# 処理の概要

大まかな処理の流れです。

## ログイン

OAuth2によるごく普通の認証・認可のフローです。Dropbox側にアプリケーションを登録し、アプリのIDとシークレットを用意する必要があります。

![login-flow.png](https://qiita-image-store.s3.amazonaws.com/0/35178/972e6081-1b16-a62e-d736-454ad2e6bb05.png)

## ファイルの変更検知と処理

ファイルの変更はWebhookｓで受信できます。後述しますが、Webhooks通知には速攻でレスポンスする必要があるので、時間のかかるファイル処理は`SNS`を経由して別のLambdaプロセスを起動します。

![file-flow.png](https://qiita-image-store.s3.amazonaws.com/0/35178/4436bd74-afa1-27b9-aebc-7962856285a4.png)

# インフラ

AWSのマネージドサービスのみで構成されています。

いわゆるLAMPと対比するとこんな感じです。

* Linux → `AWS`
* Apache → `API Gateway`
* MySQL → `DynamoDB`
* PHP → `Lambda` + `NodeJS`
* cron(非同期処理) → `SNS`

## 処理系 `Lambda`

言わずと知れたFaaS、サーバーレスアプリケーションの実行環境です。
プログラムを書いてデプロイすれば後は勝手に運用してくれる楽ちんさは麻薬的です。

## Webゲートウェイ `API Gateway`

`Lambda`自体はOSでいうスクリプト言語に相当する処理系なので、HTTP経由でそれを公開するにはApacheやCGIのようなレイヤーが必要です。
それに該当するのが`API Gateway`です。

REST APIの提供が主要な目的となるのでApacheといろいろ異なりますが、カスタムドメイン機能を用いてWebサーバー的な使い方もできます。

`Lambda`と`API Gateway`の操作は、`Serverlessフレームワーク`がいい感じにやってくれます。

## DNS `Route53`

AWSのDNSです。
任意のドメインから`API Gateway`のカスタムドメインへのエイリアスを貼ることで、独自ドメインでのサービス展開が可能になります。

## SSL証明書 `Certificate Manager`

AWSで稼働するサービス向けに、無償でSSL証明書を準備してくれます。
`API Gateway`でカスタムドメインを用意するときに証明書が必要になります。
最終的にサービスを提供する独自ドメインをコモンネームとした証明書を取得します。

## データベース `DynamoDB`

AWSが提供するスケーラブルなNoSQLです。今回のアプリでは、次の用途に使っています。

* DropboxのユーザーIDとアクセストークンの保管
* 処理済みファイルの再処理を防止したり変更を検知するためのリビジョンの保管

サーバーレスとはいえ、アクセストークンの保存に何らかのデータストアが必要です。

## ファイル変更検知 `Dropbox Webhooks`

Dropbox上で予めURLを登録しておくと、ユーザーによるファイル操作を通知を受けることができます。
これを`API Gateway`で受信し、`Lambda`のプログラムを起動します。

## 非同期処理のためのメッセージング `SNS`

API Gateway経由でLambdaを実行する場合は、**タイムアウトが30秒間**という制約を受けます。
画像ファイルの処理に30秒間は心許なく、Dropboxにフォルダを追加されたときなどは大量のタスクが発生することがあります。

また、それ以前に(出典を失念してしまいましたが)**Dropbox Webhooks側にも時間的な制約**がありました。

更には**Dropbox側でもWebhooksの送信先URLのヘルスチェックを実施**しているようで、Webhooks URLが何度か**応答しないと通知が停止**される使用になっているようです。
実際に開発中に通知がこないなーと思ったら停止されていたことがありました。もちろんユーザー向けの管理画面から再度、有効にすることができます。

以上の理由で、Webhooksの受信時は速攻で応答しなければなりません。

そこで今回、実際にファイル操作を行う処理は、`SNS`を経由して非同期に起動することにしました。`SNS`のメッセージを通して、実際にファイルの変換処理を行う`Lambda`ファンクションを起動します。

最近、Lambdaが[SQS](https://aws.amazon.com/jp/sqs/)にも対応したのでそちらでもよいと思います。

# JavaScript

## 実行環境 `Node.js`

AWS Labmdaが伝統的にサポートする`JavaScript`と`Node.js`を利用します。

自分が`Python`にあまり詳しくないだけで、もちろん選択肢としてありです。むしろ`Dropbox SDK`のサポートは`Python`の方が充実しているかもしれません。

## Dropbox SDK `dropbox`

その名も`dropbox`というNPMモジュールが提供されています。

ユーザーのログインにより認証と認可を行う`OAuth2`の処理は他のモジュールで頑張らなければならないかなと思っていたら、その辺の処理もしっかりサポートしてくれます(リダイレクト先のURLやcodeからのアクセストークンの取得など)。便利！

このモジュールを利用してDropbox上のファイルのダウンロードや、処理したファイルのアップロードを行います。

## Webアプリケーションフレームワーク `Express`

`Node.js`でお馴染みのWebアプリケーションフレームワークです。

`Lambda`と`API Gateway`でHTTPアクセスには対応できるのですが、往年のCGIのようなプリミティブさしかありません。

ヘッダーが、Cookieが、ファイルアップロードが、HTMLテンプレートが、といった処理を自力で頑張るのはしんどいし危険な香りがします。なので`Express`を利用します。

## テンプレートエンジン `Pug`

これは好みですが、テンプレートエンジンには記述量が少ないPugを使用します。[EJS](http://ejs.co/)などでももちろん問題ありません。

#サーバーレスフレームワーク `Serverless`

権限管理、イベント設定、コードの継続的なデプロイなど、`Lambda`を使うには付随して必要なことや手間が多く意外と大変です。

ユースケースを`CloudFormation`に落とし込んでまるっとサポートしてくれるのがサーバーレスフレームワーク`Serverless`です。

最近は[SAM](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/serverless_app.html)の方がトレンドなんですかね、ちょっとキャッチアップできていませんが。

## 開発用Webサーバ `serverless-offline`

`Serverless`のプラグインです。
`API Gateway`の動作をシミュレートする簡易的なWebサーバーを立ち上げてくれます。開発時に必須のプラグインです。

## Expressバインド `serverless-express`

`Express`のミドルウェアが期待する入出力と、`API Gateway` + `Lambda`のそれは大きく異なります。その変換を行う仕組みが必要です。

言ってもオブジェクト間のマッピングなので自分で頑張ることもできますが、既存のモジュールを使った方がよいでしょう。

このモジュールを使うと、**Expressアプリケーションをラッピングした、API Gateway向けのLambdaハンドラを自動生成する**ことができます。それをエクスポートするだけなので、Lambdaファンクションのハンドラファイルはかなりシンプルに記述できます。