[SFDC][S3]Salesforce – AmazonS3連携を検討する

aws
SalesforceとAmazon S3の連携方法を検討・検証する機会がありまして、「現状だとこのやり方になるよね…」と自分なりに腹落ちしたものがあるで、それをメモしておきますという内容です。※ほんとに字ばっかりのただのメモです。あといろいろ課題アリ

要件:標準の添付ファイル機能をS3に置き換える

Salesforceには「添付ファイル」という形で、オブジェクトのレコードに紐づけてファイルをアップロードする仕組みが用意されています。
レコードの詳細画面に「メモ & 添付ファイル」の一覧があり、ここからレコードに紐づけてファイルをアップロード(添付)できます。
アップロードされたファイルはAttachmentオブジェクトのレコードとしてSalesforceのファイルストレージ領域に保存されます。

便利な機能ではありますが、ファイルストレージの容量と1ファイルあたりのサイズにそれぞれ制限がありますので、例えば「5MB以上のファイルをレコードに添付したい」とか「数十GB単位のファイルを蓄積したい」とかいった場合には標準の添付ファイル機能を利用するのは難しくなります。
[参考]
・Salesforce の機能とエディションの制限
・リソースの監視
そこで、ファイルの保存先をForce.comのストレージではなくS3にしようというアイデアが生まれます。保存先をS3に変更するだけで、使い勝手は標準の添付ファイル機能と同等のものを目指します。
ファイルの「アップロード」およびレコードへの「添付」、そしてそのファイルの「参照」、最後にファイルの「削除」について、それぞれ検討しました。
なお、動作環境としてはIE7,8,9あたりもターゲットに入っています。

アップロードと添付

UI概要

標準の添付ファイルと同等のUIを実現します。つまり、オブジェクトの詳細画面に「ファイル添付」ボタンを置き、ボタン押下でファイルアップロード画面に遷移し、遷移先の画面でファイルをアップロードします。アップロード画面はvisualforceでオブジェクトへ依存しない形で作成します。ファイル添付ボタンのリンクURLにVFページの相対パスを指定し、オブジェクトIDなど必要なパラメータを付加して遷移するようにします。
※VFページを相対パスで指定するやり方は名前空間プレフィックスを設定すると破綻するのですが、今回は特定ユーザ向けの開発という想定のもと許容します(何かうまい方法はないでしょうか…)。

アップロード方法

visualforce画面からS3にファイルをアップロードする方法を検討します。S3の提供するファイルアップロード方法のうち以下の3つくらいのやり方が候補にあがりますが、SFDCの制約等を加味し、今回は「ブラウザベースアップロード」を選択しました。

  1. REST or SOAP API
  2. S3のRESTまたはSOAP APIを利用してファイルアップロードする方法です。Apexコード内からS3のAPIをコールアウトします。ファイルサイズが3MBを超えるとApexの実行時ガバナ制限に抵触するため、この方法は採用できません。

  3. HTML5 File API & CORS(Ajax REST)
  4. S3のCORSサポートを受けて可能になりました。JavascriptでS3のREST APIを叩いてクライアントからS3に直接ファイルアップロードする方法です。サーバを介さずアップロード処理が完結するため、Apexのガバナ制限から開放されます。アップロード失敗時のエラーハンドリングも容易であるため、作り込み如何によっては優れたインタフェースのアップロード画面を実現できそうです。
    しかし、オリジンをまたいだajax処理を行い、またJavaScriptからローカルファイルにアクセスするため、XHR2およびHTML5のFile APIを利用します。これら機能はモダンブラウザは軒並みサポートしているものの、IE9以前では使えません。残念ながら今回は採用を見送りました。状況が許すならこちらの方法を採るのが良いのではないですかね。簡単でスマートだし。
    やり方はクラスメソッドの素晴らしいエントリにまとまっています。→「WebブラウザからAmazon S3に直接ファイルをアップロードする」

  5. ブラウザベースアップロード
  6. 今回採用した方法です。visualforceページで作ったアップロードformのaction先にS3を指定して直接アップロードする方法です。Apexを利用しないためガバナ制限に抵触することはなく、またJavaScriptを利用しないためレガシーなブラウザでも動作可能です。
    頑張りどころとしては、form内に認証用のシグネチャを出力する必要があるのですが、シグネチャにはファイルのMIMEタイプを含める必要があるため、HTML出力時に計算できません。よって、今回はアップロードボタン押下時にJavaScript remote actionを呼びサーバにMIMEタイプ等の情報を送信、Apex内でシグネチャを計算してクライアントに返却し、コールバックの関数内でformにシグネチャを埋め込んでsubmitする、みたいなことをしました。
    ブラウザベースアップロードの制約として、S3の処理後のリダイレクトURLとして成功時のものしか指定できないというのがあります。アップロード処理に失敗した際には、エラーメッセージが設定されたXMLが送り返されるのみでまともなエラー処理ができません。これは大きな課題と思います。また、画面全体が更新されるため、UI的にもあまりイケてない感じにならざるを得ません。自画面遷移&アップロード待ち時に半透明のローディングエリアをかぶせるみたいなことをしてAjaxっぽさを演出したりはしてみましたが…。あとjsでファイルにアクセスできない関係上、MIMEタイプを拡張子から判断するようにしました。js内に拡張子-MIMEのマッピングを持つみたいな。筋悪感まんさい…。

  7. 未検証:AWS SDK for JavaScript in the Browser???
  8. そういえば最近AWS SDK for JavaScript in the Browserというのがリリースされたようです。上記二番目に紹介したAjax RESTを行うためのAmazon謹製のjsライブラリって感じなのでしょうか。認証方法として、クライアントに認証情報を書くやり方のほかにFacebook認証?なんかもサポートされているそうで面白そうです。やはり今回の要件にはマッチしなさそうですが。

添付の仕組み作り

アップロードしたファイルは、SFDCのレコードと紐付けて管理します。SFDCに「ファイルメタデータ」的なオブジェクトを用意し、S3上のファイルパス(バケット以下のkey)をレコードIDと紐付けて保持するようにしました。メタデータ登録のタイミングは、S3へのアップロードが成功後、リダイレクトURLでアップロード画面に戻ってきた時にしました。
ちなみに、同一レコードに同名のファイルを添付できるように、S3に送信するkeyにはファイル名の前にランダム値を入れるようにしました。

参照(ダウンロード)

ファイル一覧の表示

オブジェクトの詳細画面にそのレコードに添付されたファイルの一覧を表示します。

差し込みVisualforceページで実現しました。よって、各オブジェクトごとにVFページの実装が必要です。HTMLのonLoadでremoteActionを起動してファイル一覧を取得し、ビューを構築するようにしました。この時S3への通信は行わず、前述した「ファイルメタデータ」オブジェクトからファイル情報をとってくるようにしました。
せめてもの共通化、ということで上記の処理をcomponentにまとめ、オブジェクト毎のVFページは簡単なコピペで実装可能なようにしてみましたがまぁ結構微妙です。

※VFページの実装は最小化した。一覧を取得する処理は全てfile_listコンポーネントにまとめている。

非公開ファイルの参照

さて、ここが今回のS3連携における”私的キモ”だったりします。というのも、SFDCとS3の連携と言った時は、ファイルは機密データとして扱うのが普通だと思います。今回の要件もそうです。ファイルを公開してはいけません。S3側でファイルの権限設定ができますので、これをバケットごと「認証ユーザのみ操作可能」設定にしておけば、外部の人間にファイルが見られる心配はありません。と同時に、このままでは見せたい人(SFDC組織のユーザ)に見せることも出来ません…。
REST APIで認証を行いファイルを取得する方法はもちろんありますが、SFDCでやろうとすると、簡単にApexのガバナ制限に抵触してしまいNGです。

うおお困った…、と頭を抱えかけたのですが、Amazonは実にすばらな方法を用意してくれていたのでした。それが「クエリ文字列による代替リクエスト認証」です。ファイルのURLにシグネチャ等をひっつけてリクエストすることで認証できてしまうのです。シグネチャの計算は面倒ですがドキュメントを見れば良いです(いつのまにかS3のドキュメントが日本語化されていてすごい)。
シグネチャの有効期限を指定する必要があるのがミソでしょうか。雰囲気的には以下のような感じです。

ファイル一覧のファイル名の横に「参照」リンクを表示しておき、押下すると例によってremoteActionを起動して上記のシグネチャ計算処理を呼び出し結果を返却します。クライアント側で新しいウインドウを開き、URLにシグネチャ付きのファイルURLをぶち込むようにしました。

削除

削除については、リクエストボディのサイズなど気にする必要はありませんので、普通にREST APIを利用するようにしました。ファイル一覧の「削除」リンクを押下するとremoteActionが動き、Apex内からS3の削除APIをコールアウトします。S3上の削除が成功したらSFDCの「ファイルメタデータ」オブジェクトのレコードも削除するようにしました。

削除機能には大きな課題があります。それは、ファイルの紐づくレコードが削除された時の対応です。同時にS3上のファイルも削除したい所ですが、実現方法は結構悩ましいと思っています。triggerでやっても良いですが、オブジェクト毎にトリガを用意して仕掛けなければならず面倒です。またカスケード削除時(自分の親オブジェクトのレコード削除に伴って自分が削除される時)にはtriggerは動かないので完璧な方法ではありません。
よくよく考えるとレコードの削除とS3の削除をリアルタイムに連動する必要も特に無さそうなので、定期的にバッチを動かし、「ファイルメタデータ」オブジェクト内のデータを舐めてレコードの存在チェック→無ければAPI叩いてS3のファイル削除、みたいなことをすれば良いのではないかと思っています。コールアウトに関する制限もありますので、実行回数とかインターバルとか一リクエスト一ファイル削除ではなくて複数ファイルを同時に削除できるDelete Multiple Objects APIを利用するとかいろいろ気をつけるところはありそうですが。

まとめ

な、長い…。まとめる気にもならん。
ひとまず形にはなりましたがなかなか一筋縄ではいかない感じでした。そしてぜんぜん完璧ではないという。おとなしく各ベンダーさんが出しているサービス使った方が良いのかな、などと思いました。おわり

  


コメントを残す