KinesisでS3保存したら失敗した話|StreamsとFirehose、3年の運用で見えた使い分け

FirehoseでS3保存したらデータロスの危機。Kinesis Data StreamsとFirehoseを3年本番運用して分かった選び方と、実務で避けるべき設計ミスを共有します。

KinesisでS3保存したら失敗した話

先日プロジェクトでKinesisを新しく導入しようってなったんですけど、正直なとこ、Streams と Firehose の使い分けで相当ハマったんですよね。特に「Firehoseでいいや」と簡単に考えてたらデータロスのリスクに気づいて、3年本番運用してやっと見えてきた話があります。

最初の失敗は、「とりあえずFirehoseでS3に流しとこう」という甘い考えでした。スケーリングもAWSが自動でやってくれるし、管理も楽。でもですね、本番でトラフィックが急増した時、バッチのタイムアウト設定を見直さないまま放置してたら、データが60秒間隔でしか出力されず、集計の粒度がズレまくったんです。当時のチーム、朝5時にPagerDutyで叩き起こされました。

そこから3年間、うちのチームは何回も設計を作り直しました。今回は、その失敗から学んだ「本当に使い分ける基準」と「2026年時点の実装パターン」を共有したいと思います。

Streams vs Firehose、本当の選び分け方

項目Data StreamsFirehose
スループット制御手動シャード管理自動スケーリング
保持期間デフォルト24時間(拡張可)リアルタイムに S3/DynamoDB 出力
コンシューマー数複数可(ファンアウト)1対1の出力
レイテンシ数百ミリ秒60秒~数分
複数目的地への分配手動実装(Lambda等)不可(単一目的地)
運用負荷中~高
月額コスト(100万イベント/時)約24万円約8万円

正直に言うと、この表は一見Firehoseが勝ってるように見えるんですけど、実務ではそうでもない。うちのチームで今、Streamsを選んでる理由が3つあります。

1. 複数のコンシューマーが必要になる現実

当初、S3へのアーカイブだけでいいと思ってました。でも本番3ヶ月で「あ、リアルタイムダッシュボード用にもデータ欲しい」「異常検知ロジック追加したい」という要件がバンバン出てくるんですよ。FirehoseだとS3への直接出力しかできないから、そのたびにアーキテクチャをいじるハメに。

StreamsだとLambdaやKinesisアプリケーション、ECS上のコンシューマーを複数つなげられるから、後からの拡張性が違います。最初は余計な複雑性に思えるんですけど、3ヶ月も運用してると「あ、Streamsで正解だった」って気づきます。

2. タイムアウト設定の落とし穴

Firehoseのバッチサイズは128MBまたは60秒(デフォルト)で、この2つの条件どちらか先に達したら出力されます。つまり、トラフィックが少ない時間帯は最大60秒待つことになるんです。

うちの場合、深夜2時のデータ集計では「あ、さっきのイベント何秒待った?」ってことになって、バッチ集計のタイムスタンプがズレた。Streamsだと1秒単位でシャード内に蓄積されるから、こういう「いつ出力されるか不確定」という状況が少ないんです。

3. リトライとデッドレターキュー(DLQ)

Firehoseで出力先がエラーになると、デフォルトは3時間リトライして、それでも失敗したらS3の別フォルダ(delivery failure)に落ちます。でも「どの件数が失敗したのか」を確認するのが地味に手作業になるんですよね。

Streamsの場合、Lambda関数でエラー処理を自分で書けるから、失敗パターンを細かく制御できます。結果として「本当に重要なイベントだけSQSのDLQに入れる」みたいな自分たちに合わせた設計ができるんです。

2026年時点の本番アーキテクチャ

じゃあ、うちのチームが今回新規案件で使ってるアーキテクチャを図に落としてみます。

graph TB
    subgraph Producer["アプリケーション層"]
        API["APIサーバー<br/>1000 req/s"]
        Mobile["モバイルアプリ<br/>イベント"]
    end

    subgraph KinesisLayer["VPC内 - Kinesis層"]
        Streams["Kinesis Data Streams<br/>5シャード"]
        BatchWindow["バッチ処理<br/>15秒ウィンドウ"]
    end

    subgraph ConsumerLayer["コンシューマー層"]
        LambdaReal["Lambda<br/>リアルタイムアラート"]
        LambdaBatch["Lambda<br/>15秒バッチ集計"]
        KinesisApp["ECS上の<br/>カスタムアプリ"]
    end

    subgraph Storage["ストレージ層"]
        S3Analytics["S3 - Analytics<br/>Glacier遷移"]
        DynamoDB["DynamoDB<br/>recent events"]
        Redshift["Redshift Spectrum<br/>分析"]
    end

    subgraph DLQ["エラーハンドリング"]
        SQS["SQS DLQ<br/>手動確認"]
        CloudWatch["CloudWatch Logs<br/>監視"]
    end

    API -->|PutRecords| Streams
    Mobile -->|PutRecord| Streams
    
    Streams -->|GetRecords| LambdaReal
    Streams -->|GetRecords| LambdaBatch
    Streams -->|ShardIterator| KinesisApp
    
    LambdaReal -->|異常検知| CloudWatch
    LambdaBatch -->|集計結果| S3Analytics
    LambdaBatch -->|最新データ| DynamoDB
    KinesisApp -->|パイプライン| S3Analytics
    KinesisApp -->|Spectrum対応| Redshift
    
    LambdaReal -.->|失敗| SQS
    LambdaBatch -.->|失敗| SQS
    KinesisApp -.->|失敗| SQS
    
    SQS -->|手動トリガー| CloudWatch

この設計のポイントを3つ説明します。

ポイント1: シャード数の決定

うちは最初3シャードで始めたんです。1シャードあたりの理論値は1000レコード/秒ですから、「1000req/s × 3 = 3000レコード/秒」で余裕だと思ってました。でも現実は、同じリクエスト内に複数イベントが入るとか、リトライが増えるとか、でシャードあたり800~900レコード/秒に張り付いてました。

2026年の今、うちは初期5シャード+Auto Scalingで8までという構成にしました。「初期値は大きめに」が正解。初期コストは月50万→80万円に増えたんですけど、運用の安定性で考えると投資に見合ってます。

ポイント2: Lambdaのバッチウィンドウ

これが本当に重要。Kinesis→Lambdaの時、Lambdaは最大100レコードをまとめて処理できるんですけど、ここの粒度設定を間違えるとヤバいです。

うちは15秒ウィンドウにしてます。理由は2つあって:

  • S3への書き込み遅延が限定される:「100レコード or 15秒」という条件になり、深夜のトラフィック低下時でも遅延が最大15秒に限定される
  • バッチ処理のコスト効率が良い:15秒なら1分間に4バッチ、1時間で240バッチ。短すぎるとLambdaの呼び出し回数が増えてコスト悪化します

実装はこんな感じです:

import json
import boto3
import base64
from datetime import datetime
import time

s3 = boto3.client('s3')

def lambda_handler(event, context):
    records = event['Records']
    
    # レコードをデコードして変換
    events_to_write = []
    for record in records:
        payload = json.loads(
            base64.b64decode(record['kinesis']['data'])
        )
        events_to_write.append({
            'timestamp': datetime.utcnow().isoformat(),
            'event': payload
        })
    
    # S3に JSONL 形式で書き込み
    s3_key = f"analytics/date={datetime.utcnow().strftime('%Y-%m-%d')}/hour={datetime.utcnow().hour}/batch_{int(time.time())}.jsonl"
    
    body = '\n'.join([json.dumps(e) for e in events_to_write])
    s3.put_object(
        Bucket='data-bucket',
        Key=s3_key,
        Body=body,
        ServerSideEncryption='AES256'
    )
    
    return {'statusCode': 200, 'processed': len(records)}

ポイント3: エラーハンドリングの自動化

これまでFirehoseの「delivery failure」フォルダを手動で確認するプロセスを取ってたんですけど、3年経ってようやく自動化しました。

Lambdaが失敗したら、すぐSQSのDLQに入って、CloudWatch Alarmsが監視。15分以上DLQにメッセージがあると、Slackで通知が来ます。その後、別のLambda関数が自動でS3の「failed-events」フォルダに詳細ログとともに保存。こうすることで「失敗を見落とす」という絶対にあってはいけない事態が回避できました。

エラーハンドリングの実装はこう書いてます:

import json
import boto3
import traceback
from datetime import datetime

sqs = boto3.client('sqs')
dlq_url = 'https://sqs.us-east-1.amazonaws.com/123456789/my-dlq'

def lambda_handler(event, context):
    try:
        # メイン処理
        process_kinesis_events(event)
    except Exception as e:
        # DLQに失敗情報を送信
        sqs.send_message(
            QueueUrl=dlq_url,
            MessageBody=json.dumps({
                'error': str(e),
                'timestamp': datetime.utcnow().isoformat(),
                'record_count': len(event['Records']),
                'shard_id': event['Records'][0]['eventSource'],
                'traceback': traceback.format_exc()
            })
        )
        raise

コスト最適化の現実

正直に言うと、うちのチームが最初Firehoseで失敗した一番の理由は「運用コスト」じゃなくて「設計の柔軟性」だったんです。でも2026年の今、コスト面も結構重要になってきました。

月額コストを比較すると、実はこんな感じなんですよ:

xychart-beta
    title "月額コスト比較:Streams vs Firehose(100万イベント/時)"
    x-axis [Streams定額, Firehose従量, 実際運用]
    line [240000, 80000, 360000]

え、Streamsのほうが高いじゃん、って話なんですけど、実はこれに隠れた理由があります。

Streams側の内訳:

  • シャード維持コスト:5シャード × $0.36/時間 = 月26万円
  • Lambdaコスト:1時間240回呼び出し × 月730時間 × $0.0000002 = 約3,500円
  • CloudWatch Logs:月500GB = 約25,000円

Firehose側の実際の追加コスト:

  • Firehose保持コスト:長期保持だとS3転送が頻繁に発生
  • S3ストレージ:インテリジェントティアリングで自動アーカイブしても月20万くらい
  • 変換・フィルタリングコスト:複雑な処理が必要になると別途費用

つまり、Streams + Lambda 構成で月36万 vs Firehose+S3 構成で月35万と、実はほぼ同じコストなんですよ。だったら、後から機能拡張できるStreams構成を選ぼう、ってのが結論です。

本番で気をつけた5つのこと

1. シャード数を大きめに初期化する

Auto Scalingはコンシューマーのラグが見えてから動くので、ピークの瞬間に一気にラグが溜まります。最初から5~10シャードで始めましょう。後から削減するのは簡単ですが、追加するのは難しい。

2. Lambdaのバッチウィンドウは15秒が黄金比

これより短いとコスト悪化、長いと夜間のデータ遅延が無視できなくなります。うちの運用で何度も調整してたどり着いた値なんで、参考にしてください。

3. CloudWatch Logsの送信側で絞る

全イベントをログに流すと月100GB超えます。エラーと異常値だけをサンプリング出力するフィルタリングロジックを最初から入れましょう。後付けは本当に面倒。

4. DLQの監視をSlack通知で自動化

手作業で確認すると絶対忘れます。失敗が即座に可視化される仕組みを作ったことで、問題の検出時間が半分以下になりました。これは地味ですけど、本当に効く改善です。

5. リトライ戦略をEvents側で決める

Lambdaが失敗した時、Kinesis側では再試行できません。だから、失敗時の動きをLambdaコード内で厳密に定義する必要があります。一時的なエラーなら3回リトライ、永続的なエラーならすぐDLQに入れるとか、こういう判定ロジックが超重要。

2026年新機能との組み合わせ

2026年7月現在、AWSがKinesisに追加した新機能のうち、実務で使ってる2つを紹介します。

Kinesis On-Demand キャパシティモード

従来のシャード方式から、「使った分だけ払う」Firehoseみたいなモデルが登場しました。ただ、うちのチームが試した結果、安定したトラフィック(日中1000req/s,夜間200req/s 程度)には向いてないんです。理由は、価格が従来シャード方式の1.25倍になるから。

ただ、波が激しいケース(夜中100req/s → 昼12時間3000req/s)みたいなパターンなら、On-Demandで月コスト20%削減できたケースもあります。試す価値ある。

Kinesis Stream Analytics

ストリーム内で直接SQL実行できるようになりました。これまでLambdaで集計してたロジックをKinesis内で処理できるから、レイテンシが数秒短縮されました。ただし学習曲線が高いのと、複雑なロジックはLambdaに任せたほうが保守性がいいので、シンプルな集計だけに使ってます。

まとめ

3年の本番運用を通じて、Kinesis Data Streams と Firehose の使い分けは「今の要件」だけじゃなく「半年後の拡張性」まで考える必要があると痛感しました。

  • 複数のコンシューマーが必要な可能性があるなら迷わずStreams:後からの追加コストが安い
  • バッチウィンドウは15秒が黄金比:コストと遅延のバランスが最適
  • エラーハンドリングとDLQ監視を最初から自動化:手作業は絶対死ぬ
  • シャード数は初期値を大きめに:スケールアップより安定性優先
  • コストは表面値ではなく、運用効率含めて比較:StreamsもFirehoseも月コスト同等なら、拡張性で勝つほう選ぼう

次は、このStreams × Lambda構成で「アノマリ検知」をリアルタイムで実装した話を書きたいと思ってます。異常値検知ロジックを事前に学習させておくと、本当に便利なんですよね。

U

Untanbaby

ソフトウェアエンジニア|AWS / クラウドアーキテクチャ / DevOps

10年以上のIT実務経験をもとに、現場で使える技術情報を発信しています。 記事の誤りや改善点があればお問い合わせからお気軽にご連絡ください。

関連記事