0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rails + Lambda + DynamoDB非同期ジョブのエラーハンドリング|AI実務ノート 編集部

Last updated at Posted at 2026-01-03

1. 結論(この記事で得られること)

この記事では、Rails から Lambda を呼び出し、DynamoDB を使った非同期ジョブで確実にエラーをハンドリングし、リトライ・通知・運用監視まで完結させる実装パターンを解説します。

得られるもの:

  • Lambda 側の例外を Rails 側でどう捕捉すべきか(同期/非同期呼び出しの違い)
  • DynamoDB を使ったジョブステータス管理の実装
  • Dead Letter Queue(DLQ)と Slack 通知の連携
  • AI を使った障害切り分けの最速フロー
  • テスト・監視のチェックリスト

私自身、最初は「Lambda のエラーは CloudWatch に出てるからいいか」と放置して、本番で無音障害を起こした経験があります。この記事はその反省を込めた実務ガイドです。

2. 前提(環境・読者層)

想定環境

  • Rails 7.x(API モード or フルスタック)
  • AWS Lambda(Ruby 3.2 or Python 3.11)
  • DynamoDB(ジョブステータス・ログ保存用)
  • EventBridge or 直接 Lambda 呼び出し

読者層

  • Rails から非同期処理を外部に切り出したい人
  • Sidekiq / ActiveJob では対応しきれないスケールや実行環境制約がある人
  • すでに動いているがエラーハンドリングが不安な人

3. Before:よくあるつまずきポイント

3-1. 同期呼び出しと非同期呼び出しの混同

# ❌ 非同期呼び出しなのに rescue で捕まえようとする
begin
  lambda_client.invoke(
    function_name: 'MyFunction',
    invocation_type: 'Event', # 非同期
    payload: JSON.generate(user_id: user.id)
  )
rescue Aws::Lambda::Errors::ServiceError => e
  # ここには来ない!非同期なので invoke は即座に 202 を返す
  logger.error(e)
end

落とし穴:「invocation_type: 'Event'」 の場合、Lambda の実行結果を待たずに成功ステータスが返ります。つまり Rails 側では Lambda 内のエラーを直接 rescue できません。

3-2. DynamoDB への状態書き込み忘れ

Lambda 側でエラーが起きても、DynamoDB に 「status: 'failed'」 を書かないと Rails 側が検知できません。

# ❌ Lambda 内でエラーが起きても何も残らない
def lambda_handler(event, context):
    user_id = event['user_id']
    # 何か処理
    send_email(user_id)  # ここで失敗しても DynamoDB 更新なし

3-3. リトライ設計の欠如

Lambda のデフォルトリトライは非同期呼び出しで 2 回ですが、それを超えたらエラーは闇に消えます。DLQ を設定していないと、障害に気づくのは「ユーザーから問い合わせが来たとき」です。

4. After:基本的な解決パターン

4-1. Rails 側:ジョブ登録 + 非同期呼び出し

# app/services/async_job_invoker.rb
class AsyncJobInvoker
  def self.call(job_type:, payload:)
    # 1. DynamoDB にジョブレコード作成
    job_id = SecureRandom.uuid
    dynamodb_client.put_item(
      table_name: 'async_jobs',
      item: {
        job_id: job_id,
        job_type: job_type,
        status: 'pending',
        payload: payload.to_json,
        created_at: Time.current.iso8601,
        updated_at: Time.current.iso8601
      }
    )
 
    # 2. Lambda 非同期呼び出し
    lambda_client.invoke(
      function_name: ENV['LAMBDA_FUNCTION_NAME'],
      invocation_type: 'Event',
      payload: JSON.generate(job_id: job_id, job_type: job_type, payload: payload)
    )
 
    job_id
  rescue Aws::DynamoDB::Errors::ServiceError, Aws::Lambda::Errors::ServiceError => e
    # invoke 前のエラーは Rails 側で捕捉可能
    Rails.logger.error("Failed to invoke job: #{e.message}")
    raise
  end
 
  private
 
  def self.dynamodb_client
    @dynamodb_client ||= Aws::DynamoDB::Client.new(region: ENV['AWS_REGION'])
  end
 
  def self.lambda_client
    @lambda_client ||= Aws::Lambda::Client.new(region: ENV['AWS_REGION'])
  end
end

4-2. Lambda 側:ステータス更新 + エラーハンドリング

# lambda_function.py
import boto3
import json
from datetime import datetime
 
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('async_jobs')
 
def lambda_handler(event, context):
    job_id = event['job_id']
 
    try:
        # ステータスを processing に更新
        table.update_item(
            Key={'job_id': job_id},
            UpdateExpression='SET #status = :status, updated_at = :updated_at',
            ExpressionAttributeNames={'#status': 'status'},
            ExpressionAttributeValues={
                ':status': 'processing',
                ':updated_at': datetime.utcnow().isoformat()
            }
        )
 
        # 実際の処理
        result = process_job(event['payload'])
 
        # 成功時
        table.update_item(
            Key={'job_id': job_id},
            UpdateExpression='SET #status = :status, result = :result, updated_at = :updated_at',
            ExpressionAttributeNames={'#status': 'status'},
            ExpressionAttributeValues={
                ':status': 'completed',
                ':result': json.dumps(result),
                ':updated_at': datetime.utcnow().isoformat()
            }
        )
 
    except Exception as e:
        # 失敗時
        table.update_item(
            Key={'job_id': job_id},
            UpdateExpression='SET #status = :status, error_message = :error, updated_at = :updated_at',
            ExpressionAttributeNames={'#status': 'status'},
            ExpressionAttributeValues={
                ':status': 'failed',
                ':error': str(e),
                ':updated_at': datetime.utcnow().isoformat()
            }
        )
        raise  # DLQ に送るため再 raise

4-3. DLQ + SNS で Slack 通知

Lambda の設定で Dead Letter Queue(SQS or SNS)を指定します。

# serverless.yml の例
functions:
  asyncJob:
    handler: lambda_function.lambda_handler
    onError: arn:aws:sns:ap-northeast-1:123456789012:lambda-dlq-topic
    maximumRetryAttempts: 2

SNS トピックから Slack に通知する Lambda を別途用意:

# dlq_notifier.py
import json
import urllib.request
 
def lambda_handler(event, context):
    message = event['Records'][0]['Sns']['Message']
    payload = {
        "text": f"🚨 Lambda DLQ Alert\n```{message}```"
    }
    req = urllib.request.Request(
        os.environ['SLACK_WEBHOOK_URL'],
        data=json.dumps(payload).encode('utf-8'),
        headers={'Content-Type': 'application/json'}
    )
    urllib.request.urlopen(req)
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?