動いた処理を、あとから分解してみる
第1回では、公式ニュースの収集から日報生成、Web公開、メール送信までを一つにつなげました。第2回と第3回では、その途中で起きたS3、Secrets Manager、SES、Schedulerの問題を追っています。
作っている最中は「Lambdaが日報を作る仕組み」と考えていました。間違いではありませんが、実際にエラーを追うと、その説明だけでは足りません。Lambdaを起動するもの、秘密を渡すもの、結果を保存するもの、読者へ届けるものは、それぞれ別です。
今回は手順をもう一度並べるのではなく、なぜこのサービスを使い、どこまでを任せたのかを整理します。なお、EventBridge Schedulerは毎朝5時、Asia/Tokyoで有効化済みですが、最初の定刻実行結果はまだ確認前です。手動実行でページとメールが届いた事実と、定刻実行の設定を分けて扱います。
現在の構成
EventBridge Scheduler(毎朝5:00 / Asia/Tokyoで設定)
↓
Lambda(samekoro-daily)
├─ 公式ニュースを収集
├─ OpenAI Responses APIへ日報生成を依頼
├─ コード内でJSON Schemaを検証
├─ S3へ日報・HTML・stateを保存
├─ CloudFront Invalidationを依頼
└─ SESからメールを送信
Secrets Manager ── OpenAI APIキーをLambdaへ渡す
S3 ── CloudFront ── samekorolab.com/daily/
Lambda ── CloudWatch Logsへ実行記録を出す
SES ── 受信箱へメールを届ける
図にすると、Lambdaから線が何本も伸びます。ただし、Lambdaがすべてを持っているわけではありません。処理の順序を決めて各サービスを呼び出す役で、保存や配信、メール送信そのものはAWSの各サービスへ任せています。
EventBridge Schedulerは「始める時刻」だけを持つ
EventBridge Schedulerに任せたのは、毎朝5時にLambdaを呼ぶことです。タイムゾーンはAsia/Tokyo、再試行は2回、イベントの最大保持時間は1時間で設定しています。初期設定ではDLQを付けていません。
ここへニュース収集やメール送信の判断まで入れなかったのは、時刻と処理内容を分けたかったからです。配信時刻を変えるときはSchedulerを見て、日報の作り方を変えるときはLambda側を見る。その境界があると、変更箇所を絞れます。
第3回で確認したとおり、今回の実機画面ではSchedulerがLambdaのトリガー一覧へ表示されていませんでした。そのため、Scheduler側のターゲットARN、実行ロール、入力、状態を照合しました。定刻実行の成否は、実行時刻後のCloudWatch Logs、受信箱、日報ページで別に確認します。
Lambdaは処理をつなぐ中心にした
Lambdaの中では、最初に対象日のstateをS3で確認し、同じ日報を重ねて処理しないようにします。そのあと、公式フィードを収集し、前回の日報や記事情報も読み込み、OpenAI Responses APIへ生成を依頼します。
返ってきたJSONは、そのまま公開しません。JSON Schemaに合うかをコードで検証し、Web用とメール用のHTMLを別々に作ります。ここでのSchema検証はAWSの独立したサービスではなく、Lambdaで動くDailyのコードです。
生成、検証、保存、配信を一つの関数から順番に呼ぶ構成は、大規模な処理なら分割を検討するところです。今回は個人開発で、処理量も限られています。まず実行順とstateを一か所で追えることを優先しました。
Secrets Managerは秘密をコードから外す
OpenAI APIキーはSecrets Managerから実行時に取得します。コードや記事、環境変数の一覧へ値を直接書かないためです。実装はSSM Parameter Storeや環境変数にも対応していますが、現在の環境で使っているのはSecrets Managerです。
第2回で AccessDenied を追ったとき、Lambdaの実行ロールが正しいシークレットARNを参照しているかが問題になりました。構成図では一本の矢印でも、実際には「どの実行ロールが、どのResourceを、どのActionで読むか」が必要です。秘密を置く場所と、読む許可は別でした。
S3は公開物と処理状態を残す
S3には、日報のJSON、Webページ、メールHTML、日付別の出力、処理状態を保存します。stateには公開やメール送信がどこまで進んだかが残るため、失敗後の再実行でも「何をもう一度行ってよいか」を判断できます。
保存先には、既存のSAMEKORO LABで使っているS3を再利用しました。日報のために別の静的サイト基盤を増やすより、daily/ 配下へ出力する方が、今の規模では管理しやすいと考えたからです。
ただし、サイトのドメイン名とS3バケット名は同じとは限りません。第2回の404では、ここを取り違えました。図で「S3」と一箱にすると見落としやすいので、実際の切り分けではLambdaの環境変数、ログに出た保存先、AWS上の実バケットを照合します。
CloudFrontは保存したページを読者へ届ける
S3へHTMLを書いただけでは、CloudFrontのキャッシュに以前の内容が残る場合があります。そこで保存後に /、/index.html、/daily/* を対象としたInvalidationを依頼します。
CloudFrontとS3は、すでにSAMEKORO LABを公開していた構成を使いました。日報用に新しい配信経路を増やさず、既存サイトの一部として公開しています。S3は置き場所、CloudFrontは読者へ届ける入口、と分けると役割を追いやすくなりました。
SESは公開処理のあとにメールを送る
WebページをS3へ保存し、公開済みのstateへ進めたあと、SESからメールを送ります。メール内のリンクを開いたときに、先にページが存在する順番にしたかったからです。
送信前にstateを sending へ進め、成功後に完了を記録します。送信を試した直後に結果が不明になった場合も別状態として残します。これは、再実行で同じメールを安易に二重送信しないためです。
SESのIdentityやIAM Resourceで起きた403は第3回で扱ったため、ここでは繰り返しません。構成上のポイントは、Lambdaが本文を作り、SESが配送を担当することです。生成成功とメール送信成功は別の状態として確認します。
CloudWatch Logsは「どこまで進んだか」を残す
LambdaのログはCloudWatch Logsへ出ます。S3への保存、OpenAI呼び出し、CloudFront、SESのどこで止まったかを追う入口です。
実際のトラブルでは、HTTPステータスだけでなく、API名、対象パス、Resourceをログから読みました。一つ直すたびにエラーの出る場所が次のサービスへ移ったため、ログは単なるエラー一覧ではなく、処理がどこまで進んだかの記録になりました。
この構成にした理由
最初から最小サービス数を目指したわけではありません。既存のS3とCloudFrontを使い、定刻開始はScheduler、実行はLambda、秘密はSecrets Manager、送信はSES、記録はCloudWatch Logsへ任せました。
企業の運用なら、処理の分割、DLQ、監視通知、権限のさらに細かな分離も検討します。今回は個人開発なので、まず自分が追える構成を優先しました。ただし、秘密をコードへ置かないこと、実行ロールを分けること、stateで二重処理を避けることは、小さくても省きませんでした。
今回ハマったポイント
構成図の最初の案では、Lambdaの箱の中に収集、生成、保存、公開、送信を全部書き、ほかのサービスを付属物のように並べていました。最初に疑ったのは、単に図の情報量が多すぎることです。
しかし、第2回と第3回のログを処理順に置くと、問題は情報量だけではありませんでした。S3へ書く、Secrets Managerから読む、SESで送る、Schedulerから呼ぶという境界ごとに、別の権限と確認画面があります。そこで「始める」「つなぐ」「秘密を渡す」「保存する」「届ける」「送る」「記録する」へ役割を分けました。
次回から構成を確認するときは、サービス名の一覧ではなく、データが次へ渡る地点を見ます。エラーが出たら、その矢印の送信元、送信先、権限、ログを順に照合します。
今回の学び
SAMEKORO DailyはLambda一つで完成した仕組みではありませんでした。Lambdaは中心ですが、その役割は各サービスを決めた順序でつなぐことです。境界を図にしたことで、設定を変える場所と、障害時に見る場所が分かれました。
構成図は完成した姿をきれいに見せるためだけではなく、次に詰まったときの地図になります。毎朝5時の設定が本当に一巡したかは、最初の定刻実行後にCloudWatch Logs、S3、CloudFront、受信箱を同じ日付で確認して、この図へ結果を戻します。