この記事で解決すること
SAMEKORO DailyをAWS上で初めて動かした時、LambdaはS3へのstate保存で404になり、その次はSecrets Managerの AccessDenied で止まりました。直して終わりと思ったら、前回の失敗stateが残り、再実行は already-processed です。
結論から書くと、原因は一つではありませんでした。
- Webサイトのドメイン名をS3バケット名だと思っていた
- Lambda環境変数とIAMポリシーが同じ誤ったバケットを見ていた
- IAMポリシー内のAWSアカウントIDを誤っていた
- Secrets ManagerのARN末尾に付く6文字のサフィックスを許可へ含めていなかった
- 原因を直したあとも、同日実行を止めるstateが残っていた
一つの大きな障害ではなく、小さな設定違いをログの順番に潰していく作業でした。
まず、自分のAdministratorAccessとLambdaの権限は別だった
構築中の自分は、IAM Identity Centerから AdministratorAccess を使ってAWSへ入っています。コンソールでS3もSecrets Managerも開けるため、最初は権限不足が少し不思議でした。
ここで混ざっていたのは、自分がコンソールで使う権限と、Lambdaが実行時に使う権限です。Lambdaは自分のサインイン状態を引き継ぎません。関数へ設定した実行ロール SamekoroDailyLambdaRole の権限で動きます。
そのため、実行時に必要な操作を SamekoroDailyRuntimePolicy へ書きました。S3の読み書き、Secrets Managerからの読み取り、SES送信、CloudFrontのInvalidationを対象リソースへ絞っています。手早く Resource: "*" へ広げるのではなく、実際に使う範囲を一つずつ合わせる方針です。
S3 PUTが404になった
起きたこと
最初の手動実行は、stateを保存するところで止まりました。ログの要点は次のとおりです。
S3 API PUT /samekorolab.com/_daily/state/2026-07-22.json
HTTP 404
403ではなく404だったため、「権限がない」より「その保存先は本当に存在するか」を先に見る必要がありました。
最初に疑ったこと
Lambdaの実行ロールを作った直後だったので、最初は s3:PutObject が足りないのだと思いました。IAMポリシーを見てもPutObjectは入っています。それでもIAM側を見続けていましたが、ログを読み返すと、404より先にPUT先として samekorolab.com が出ています。そこで初めて、権限ではなく保存先の名前を疑いました。
実際の原因
DAILY_BUCKET_NAME に、Webサイトのドメイン名 samekorolab.com を入れていました。しかし、CloudFrontがオリジンとして使っているS3バケットは別の名前です。
公開用ルールに合わせ、実名は記事では次のようにマスクします。
samekorolab-prod-origin-<AWS_ACCOUNT_ID>-ap-northeast-1-an
ブラウザで見えるのは samekorolab.com ですが、Lambdaが書き込む相手はCloudFrontの奥にあるS3バケットです。このサイトではS3を直接公開せず、CloudFrontとOACを通しているため、普段のブラウザ操作では実バケット名が表に出ません。人が見るURLとAWS内部の保存先を同じ名前だと思っていたのが原因です。
第1回と第2回でRoute 53、CloudFront、S3の役割は整理していました。それでも環境変数へ具体的な値を入れる場面では、サイトの名前として見慣れたドメインを選んでしまいました。初心者が混同しやすいのは、どちらも「サイトの置き場所」を指しているように見えるのに、Lambdaでは物理的な保存先の名前が必要になるからです。
どう切り分けたか
CloudWatch LogsのPUT先と、Lambdaの DAILY_BUCKET_NAME、CloudFrontのオリジンドメイン、S3コンソールのバケット一覧を並べました。
ログにはドメイン名がバケットとして出ています。一方、CloudFrontのオリジンは samekorolab-prod-origin-... です。この二つが一致していないことから、コードではなく渡している値の問題だと判断できました。
どう直したか
直した場所は一か所ではありません。
- Lambda環境変数
DAILY_BUCKET_NAME SamekoroDailyRuntimePolicyのS3 ARN
環境変数だけを直すとLambdaは正しいバケットへ向きますが、IAMポリシーは古いバケットだけを許可したままです。逆にポリシーだけ直しても、Lambdaは存在しない名前へPUTします。実行先と許可先を同じ値へそろえて、もう一度実行しました。
次回の確認ポイント
次に同じ構成を作るなら、ドメイン名ではなくCloudFrontのオリジンからS3バケット名を控えます。そのうえで、環境変数とIAMポリシーの両方へ同じ値が入っているかを先に比較します。
次はSecrets ManagerのAccessDeniedで止まった
起きたこと
S3の保存先を直すと、処理は一つ先へ進みました。今度はOpenAI APIキーを読むところで止まります。
secretsmanager:GetSecretValue is not authorized
resource: arn:aws:secretsmanager:...:secret:<secret-name>-******
ひとつ直すたび次のエラーが出るので、「まだ終わらないのか」と思いました。ただ、エラーが変わったことは、前の問題を越えた証拠でもあります。
最初に疑ったこと
まず疑ったのは、シークレット名と OPENAI_API_KEY_SECRET_ID です。名前を間違えると取得できないため、Lambdaの環境変数とSecrets Managerの画面を見比べました。
シークレット名は合っています。そこで、エラーに出ているリソースARNとIAMポリシーのResourceを比べました。
実際の原因
IAMポリシーのARNへ入れたAWSアカウントIDが誤っていました。公開記事では12桁の値を載せませんが、作業記録では、誤った値と実際の値で先頭と並びが異なることを確認しています。
この誤りはSecrets Managerだけではなく、同じ置換値を使ったSESとCloudFrontのARNにも入っていました。一つの AccessDenied から、ほかのサービスでも後から止まる設定違いを先に見つけられました。
もう一つ確認したのが、Secrets ManagerのシークレットARNです。ARNの末尾には、シークレット名に続いてAWSが付ける6文字のサフィックスがあります。AWS公式のIAMポリシー例にも、6文字だけに一致させる ?????? の指定が示されています。ポリシーのResourceは次の形で、その部分を含めました。
arn:aws:secretsmanager:<region>:<account-id>:secret:<secret-name>-??????
どう切り分けたか
見る場所を、次の三つに絞りました。
- AccessDeniedに出た実際のリソースARN
SamekoroDailyRuntimePolicyのSecrets Manager Resource- Secrets Manager画面のシークレットARN
サービス名、リージョン、アカウントID、シークレット名、末尾サフィックスを区切って比較します。ARN全文を雰囲気で眺めるより、どの部分が違うかを見つけやすくなりました。
どう直したか
IAMポリシー内のアカウントIDを正しい値へそろえ、Secrets ManagerのResourceへ6文字サフィックスを含むパターンを指定しました。同じ誤りが入っていたSESとCloudFrontのARNも一緒に直しています。
ポリシーを保存したあと、Lambdaの実行ロールへ更新後のポリシーが付いていることも確認しました。自分の権限ではなく、実行ロールから見た許可を最後まで追います。
次回の確認ポイント
次回はAWSアカウントID、リージョン、バケット名、CloudFrontディストリビューション、SES Identity、シークレットARNを最初に一覧へ控えます。テンプレートの置換後に元のプレースホルダーが残っていないかも検索します。
直して終わりと思ったら、already-processedになった
起きたこと
アクセス権を直して同じ日付で再実行すると、今度はエラー処理へ進まず daily.already_processed で終了しました。前回の実行で _daily/state/2026-07-22.json が作られていたからです。
最初は、失敗したのだから何度でもやり直せると思っていました。しかしこのLambdaは、同じ日の二重公開と二重送信を避けるため、stateがあれば先へ進みません。安全のための冪等性に、再試験を止められた形です。
実際の原因
原因修正とは別に、前回のstateが残っていました。ここでstateを無条件に消すのは危険です。サイト公開後やメール送信直後だった場合、再実行で同じ日報を公開したり、同じメールを二度送ったりする可能性があります。
どう切り分けたか
S3のstateを開き、最低限次の値を確認しました。
statuspublishedmailStatuserror.message
判断の順番は、原因修正より先にstateを消すのではなく、原因を直す → stateを開く → published と mailStatus を確認する → 削除可否を決める、です。削除を検討できるのは、published=false かつ mailStatus=not-sent と確認できた場合だけです。
この二つがそろわない場合は、対象日のstateを削除しません。published=true なら公開済みファイルを、mailStatus=sent なら受信結果を確認します。mailStatus=unknown-after-send-attempt は、SESが受け付けた直後にLambdaが止まった可能性があるため、SESの記録と受信箱を先に見ます。
どう直したか
今回の再試験では、公開もメール送信もしていない組み合わせだと確認してから、対象日のstateだけを削除し、手動実行しました。state全体のプレフィックスや別の日付は触っていません。
次回の確認ポイント
再実行の前にstateを見る、という順番を固定します。エラー原因を直すことと、同じ日を再試験してよいかを判断することは別です。
修正後の動作確認
バケット名、IAMポリシー、Secrets ManagerのResource、stateを順番に見直したあと、Lambdaはニュース生成と公開処理へ進みました。最終的なメール送信では別のSES 403が出ましたが、それはS3とSecrets Managerの問題ではありません。
エラーが変わるたび、前の修正が正しかったかをログで確認し、次のサービスへ進みました。全部を一度に変更しなかったことで、どの修正でどこまで進んだかを残せました。
今回ハマったポイント
S3 PUTの404を見た時点では、最初に s3:PutObject を疑いました。実際は、ログのPUT先にドメイン名が出ており、Lambda環境変数とIAMポリシーが実在しない同じ保存先を見ていました。両方を実バケット名へそろえると、処理はSecrets Managerまで進みました。
次のAccessDeniedでは、エラーに出たARNとIAM Resourceを区切って比較し、AWSアカウントIDとシークレットARN末尾の範囲を直しました。さらに再実行は、前回の失敗stateによって already-processed で止まりました。ここは権限ではなく、published と mailStatus を確認してから対象日のstateだけを扱う問題でした。
次回は、CloudWatch LogsのAPI名、パス、Resourceを先に読みます。そのあと環境変数の実行先、IAM Resource、AWSコンソールの実リソースを同じ順で比べ、原因を一つ直すたびにログがどこまで進んだかを確認します。
今回の学び
AWSのエラーは、HTTPステータスだけを見ると確認先を間違えます。404ではPUT先、AccessDeniedでは操作名だけでなくResource ARN、再実行ではstateの中身まで読む必要がありました。
一番効いたのは、一つ直すたびにログがどこまで進んだかを確認したことです。サービス間の権限では、許可するActionより先に、Lambdaが実際に見ているリソースが合っているかを確かめるべきだと分かりました。
次回は、最後に残ったSES 403をどう追い、EventBridge Schedulerを毎朝5時の設定へつないだかを振り返ります。