Zoho CRMのWidgetからAWSを呼び出すとき、処理をLambda 1本にまとめない方がよかったです。公開入口、取消しにくい外部処理、定期の結果確認は、失敗時の扱いも認可の置き方も異なります。
本稿では「正本」を書き込み責任があるデータの置き場所、「副作用」を外部送信やPDF保存など取消しにくい処理の意味で使います。
シリーズ「Zoho WidgetとLambdaの連携」の第1回です。ここでは、Lambdaを3つに分けた理由と、各Function URLの公開範囲を説明します。
次回: Zoho WidgetとLambdaの連携 第2回:HMAC署名とDelugeのJSON運搬
この記事の内容
- Lambdaを入口・副作用・確認用に分けた理由
- Function URLごとの公開範囲と認可の置き場所
- 商談レコードと履歴レコードで正本が分かれること
ブラウザに共有鍵を置かない中継としてDelugeを使いました。同等の中継であれば、設計の核は同じです。
構成要素
| 要素 | 役割 |
|---|---|
| Widget | CRMに埋め込むUI。入力・プレビュー・送信操作の起点 |
| Deluge | Zoho側のサーバスクリプト。共有鍵を保持し、外部HTTPへ中継する |
| 入口Lambda | 通常API、許可の発行、Widget配信。公開HTTPS入口を持つ |
| 副作用Lambda | PDF生成・保存、外部送信など取消しにくい処理 |
| 確認用Lambda | 受付後の結果を定期確認する。公開HTTPは持たない |
| Function URL | Lambda用のHTTPSエンドポイント |
| Secrets Manager(相当) | 共有鍵などの秘密の保管場所 |
| 商談レコード | CRM上の案件。フォーム入力の正本 |
| 履歴レコード | PDF・送信状態・進行情報の正本 |
対象にした処理フロー
匿名化した流れは次のとおりです。
- Widgetで商談の入力を集める
- プレビューPDFを画面で確認する
- 確認したPDFを履歴へ保存する
- 保存済みPDFを起点に外部送信を要求する
- 結果を定期ジョブで確認する(ブラウザを閉じても継続)
処理をZohoだけに閉じると制約が強いです。ブラウザからAWSへ直接呼び出すと、共有鍵がiframe側に露出します。AWS側を1関数にまとめると、次の問題が出ます。
Lambdaを1本にまとめると困る理由
UI入口と外部送信を同じ関数にすると、送信の一時エラーを画面操作と同じ感覚で自動retryしたくなります。公開HTTPと定期確認を混ぜると、結果確認まで公開攻撃面になります。入力保存と副作用を同居させると、デプロイ単位と障害影響範囲が同時に広がります。
そのため3関数に分けました。
| 呼び名 | 役割 | Function URL |
|---|---|---|
| 入口Lambda | Widget配信、入力保存、許可の発行、後段呼び出し | あり(公開) |
| 副作用Lambda | PDFの生成・保存、外部送信、履歴の状態更新 | あり(プレビューGET用)。業務の保存・送信POSTは入口経由 |
| 確認用Lambda | 結果の定期確認 | なし(SchedulerからのIAM Invokeのみ) |
入口Lambdaは静的なWidget配信も担当します。画面配信と通常APIが同じ公開エンドポイントになりやすい点に注意してください。
Function URLと認可NONE
Function URLは、Lambdaに付与する専用のHTTPSエンドポイントです。固有URLが発行され、ブラウザやHTTPクライアントから呼び出せます。認可タイプは主に次の2つです。
-
AWS_IAM: IAMで呼び出し元を制限する -
NONE: Function URL手前のIAM認証を付けない
NONE は、URLに到達したリクエストを関数が受け付けうる公開寄りの設定です。URL秘匿を防衛手段とはみなしません。入口の通常APIはHMACや短命tokenで認可します。確認用LambdaにはFunction URLを付けず、SchedulerからのIAM Invokeのみにしました。
どのURLが公開されるか
| 面 | 公開HTTP | 用途 | 業務POST |
|---|---|---|---|
| 入口のFunction URL | あり | 通常API、直送、Widget配信 | する(HMACまたは直送token) |
| 副作用のFunction URL | ありうる | プレビューPDFのGETが主 | しない(サーバ間HMACは入口から) |
| 確認用 | なし | Scheduler起動のみ | なし |
要約すると、入口は公開、副作用URLはプレビューGET用に公開しうる、確認用は公開HTTPなし、です。
CORSとorigin
originはスキーム・ホスト・ポートの組です。ブラウザは、ページのoriginと異なる先への応答読取を、サーバが許可しない限り制限します。その許可仕組みがCORSです。CORSは呼び出し元の本人確認(認証)ではありません。
許可originは、Widgetが動作するexactなHTTPS originのみとし、wildcardは使いません(具体値は記載しません)。直送も入口Function URL配下のため、CORS未設定だとiframeからの直送がブラウザ段階で失敗します。詳細は第3回です。
Widgetからの処理の流れ
画面は導入、入力、確認、送信方法の選択、プレビュー、処理中、完了、と進みます。
通常操作と入力保存は、Deluge経由で入口へHMAC付きPOSTします。プレビューPDFは副作用側URLへGETします。見たPDFの保存は、メタデータをHMAC経路、本体を直送に分けます(第3回)。外部送信は保存済み履歴IDへの要求です(第4回)。最終結果は画面の要求完了表示の外で、確認用Lambdaが履歴を更新します。結果の正本は履歴側です(第5回)。
Widgetは共有鍵も操作者IDの正本も持ちません。DelugeがOrg Variableの鍵でHMACします。共有鍵のもう一端はSecrets Manager(相当)に置き、ブラウザには出しません。
データの正本
| データ | 正本 | 入口Lambda | 副作用/確認用 |
|---|---|---|---|
| 原稿・フォーム入力 | 商談レコード | 読取・書込 | 業務正本としては書かない |
| PDF・送信状態・進行JSON | 履歴レコード | 呼び出し・許可(正本書込なし) | 読取・書込 |
| 画面の一時状態 | ブラウザ | 触らない | 触らない |
| 秘密 | Secrets/env | 使用 | 使用 |
商談は入力データの正本、履歴はPDFと送信進行の正本です。正本が曖昧だと、入口と副作用が同一フィールドを更新し合います。進行管理にDynamoDBを追加する方法もありますが、正本を増やさない方針とし、確認用の進行情報は履歴上のJSONに載せました(第5回)。
商談保存は更新時刻による楽観的ロックで、競合時は上書きしません。履歴更新も競合時は上書きしません(確認用はskip)。楽観的ロックは、読取時点の前提が有効なときだけ更新する方式です。前提が崩れている場合はHTTP 412 Precondition Failedなどで拒否します。
経路ごとの認可
| 経路 | 認可 |
|---|---|
| Widget → Deluge → 入口(通常API) | HMAC(共有鍵とraw body) |
| 大きいPDFの直送 | 短命tokenとSHA-256(第3回) |
| プレビューGET | 業務POSTではない。短命URL(第3回) |
| 入口 → 副作用(サーバ間) | HMAC。ブラウザから業務POSTさせない |
| 確認用 | SchedulerからのIAM Invokeのみ |
AWSではCognitoによる利用者認証や、SigV4(IAM資格情報による署名)も選択肢です。今回はDelugeからそれらを安定実装できない制約が先にあり、採用しませんでした。
WidgetからZoho関数を呼ぶ部分は次の形です。フロント側の責任範囲はここまでです。
ZOHO.CRM.FUNCTIONS.execute(fnName, {
arguments: JSON.stringify(payload),
});
Lambdaへ届く最終文字列はDelugeが組み立てます。共通ライブラリは通信と形式変換までに限定し、状態遷移や送信clientは含めませんでした。
設計上やらないこと
- Delugeから実装困難なAWS認証を前提にしない
- 入口・副作用・定期確認を1関数に混ぜない
- 進行用の正本をAWS側に増やさない
- フロント申告の操作者IDを信用しない
- 巨大PDFをZoho関数に載せない
- 外部送信の一時エラーを自動retryしない
既知の制限(replay台帳なし、プレビューURL漏洩時の読取リスクなど)は第5回末の一覧にまとめています。
次回は入口のHMACです。署名式ではなく、Deluge経由のJSON文字列化が失敗原因だった件を扱います。