この記事で解決すること

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-... です。この二つが一致していないことから、コードではなく渡している値の問題だと判断できました。

どう直したか

直した場所は一か所ではありません。

  1. Lambda環境変数 DAILY_BUCKET_NAME
  2. 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を開き、最低限次の値を確認しました。

  • status
  • published
  • mailStatus
  • error.message

判断の順番は、原因修正より先にstateを消すのではなく、原因を直す → stateを開く → publishedmailStatus を確認する → 削除可否を決める、です。削除を検討できるのは、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 で止まりました。ここは権限ではなく、publishedmailStatus を確認してから対象日のstateだけを扱う問題でした。

次回は、CloudWatch LogsのAPI名、パス、Resourceを先に読みます。そのあと環境変数の実行先、IAM Resource、AWSコンソールの実リソースを同じ順で比べ、原因を一つ直すたびにログがどこまで進んだかを確認します。

今回の学び

AWSのエラーは、HTTPステータスだけを見ると確認先を間違えます。404ではPUT先、AccessDeniedでは操作名だけでなくResource ARN、再実行ではstateの中身まで読む必要がありました。

一番効いたのは、一つ直すたびにログがどこまで進んだかを確認したことです。サービス間の権限では、許可するActionより先に、Lambdaが実際に見ているリソースが合っているかを確かめるべきだと分かりました。


次回は、最後に残ったSES 403をどう追い、EventBridge Schedulerを毎朝5時の設定へつないだかを振り返ります。