Kinesis Data Streams が S3 汎用バケットへの直接配信をサポートしたので試してみた
はじめに
2026年8月のアップデートで、Kinesis Data Streams から S3 の汎用バケットへ直接データを配信できるようになりました。これまで S3 へアーカイブするには Amazon Data Firehose を経由するのが一般的でしたが、サービス実行ロールを用意し、ストリームにチャネルを1つ作るだけで配信できます。
従来の構成と直接配信を並べると、次のようになります。
| 項目 | KDS → Firehose → S3 | KDS → S3 直接配信 |
|---|---|---|
| 追加で作るリソース | Firehose ストリーム | チャネル |
| データ変換・形式変換 | できる | できない |
| 最小バッファ | 60 秒(ゼロバッファリングも選択可) | 300 秒 |
| 配信に伴う追加料金(単価) | data-out $0.040/GB + ingestion $0.029/GB | S3 delivery $0.0275/GB |
直接配信の料金は、既存の KDS 料金(per-stream / data-in / retention)に追加でかかります。Firehose の ingestion は 5KB 単位に切り上げて課金され、$0.029/GB は月間の最初の 500 TB までに適用される単価です。単価は Kinesis Data Streams の料金(S3 delivery タブ)と Amazon Data Firehose の料金に載っている US East の計算例の値です。最小バッファは、直接配信が配信のクォータと制約、Firehose がバッファリングのヒントに記載の値です。
この記事では、On-Demand Standard のストリームから SSE-S3 のバケットへ GZIP / JSON で配信する構成を作ります。配信の作成手順、S3 のキー構造、配信されたオブジェクトの中身、最初のレコードが届くまでの経過時間、不正なレコードの行き先を確認します。
検証内容
ap-northeast-1 で、On-Demand Standard のストリームにチャネルを作成しました。
検証環境
操作はすべて AWS CLI で実行しました。使ったバージョンは次のとおりです。
aws-cli/2.36.36 Python/3.14.6 Linux/7.1.6-400.asahi.fc44.aarch64+16k docker/aarch64.amzn.2023
2.36.36 では kinesis create-channel を実行できました。コマンドが見つからない場合は CLI を更新します。
実行環境は Docker イメージ public.ecr.aws/aws-cli/aws-cli:2.36.36 です。digest は sha256:7047de9ebb37c39d07f652ff2ff3f6c42308a4e189788aeeae70563b0c82b57d です。
送信元ストリームの設定は次のとおりです。
{
"StreamDescriptionSummary": {
"StreamName": "kds-s3-gp-ods-20260901-2210",
"StreamARN": "arn:aws:kinesis:ap-northeast-1:123456789012:stream/kds-s3-gp-ods-20260901-2210",
"StreamStatus": "ACTIVE",
"StreamModeDetails": {
"StreamMode": "ON_DEMAND"
},
"RetentionPeriodHours": 24,
"StreamCreationTimestamp": "2026-09-01T13:58:21+00:00",
"EncryptionType": "NONE",
"OpenShardCount": 4,
"ConsumerCount": 0,
"MaxRecordSizeInKiB": 1024,
"ChannelCount": 0
}
}
直接配信の対象になるのは On-Demand のストリームです。
配信の作成
先にサービス実行ロールを作ります。信頼する principal は kinesis.amazonaws.com で、送信元アカウントと、チャネル ARN のワイルドカードを条件に付けました。
{
"Role": {
"Path": "/",
"RoleName": "kds-s3-gp-role-20260901-2210",
"Arn": "arn:aws:iam::123456789012:role/kds-s3-gp-role-20260901-2210",
"CreateDate": "2026-09-01T13:58:07+00:00",
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "kinesis.amazonaws.com"
},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"aws:SourceAccount": "123456789012"
},
"ArnLike": {
"aws:SourceArn": "arn:aws:kinesis:ap-northeast-1:123456789012:channel/*"
}
}
}
]
}
}
}
権限ポリシーには、配信先バケットへの書き込みとバケット一覧の取得、DLQ 用のアクションを入れました。この構成で配信が成功しました。マルチパートアップロードの実行自体は s3:PutObject で許可されます(CreateMultipartUpload)。
サービス実行ロールの権限ポリシー
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DeliveryBucketList",
"Effect": "Allow",
"Action": [
"s3:ListBucket",
"s3:ListBucketMultipartUploads"
],
"Resource": [
"arn:aws:s3:::kds-s3-gp-20260901-2210-123456789012",
"arn:aws:s3:::kds-s3-gp-20260901-2210-123456789012/*"
]
},
{
"Sid": "DeliveryBucketWrite",
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:CreateMultipartUpload",
"s3:UploadPart",
"s3:CompleteMultipartUpload",
"s3:ListMultipartUploads",
"s3:ListMultipartUploadParts"
],
"Resource": [
"arn:aws:s3:::kds-s3-gp-20260901-2210-123456789012/*"
]
},
{
"Sid": "DLQBucketAccess",
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:ListBucket",
"s3:ListBucketMultipartUploads"
],
"Resource": [
"arn:aws:s3:::kds-s3-gp-20260901-2210-123456789012",
"arn:aws:s3:::kds-s3-gp-20260901-2210-123456789012/*"
],
"Condition": {
"StringEquals": {
"aws:ResourceAccount": "123456789012"
}
}
}
]
}
配信の設定は入力 JSON にまとめて渡しました。
{
"ChannelName": "kds-s3-gp-channel-20260901-2210",
"ServiceExecutionRoleARN": "arn:aws:iam::123456789012:role/kds-s3-gp-role-20260901-2210",
"StreamConfigurationList": [
{
"StreamARN": "arn:aws:kinesis:ap-northeast-1:123456789012:stream/kds-s3-gp-ods-20260901-2210",
"RecordConfiguration": { "RecordFormatType": "JSON" }
}
],
"S3DestinationConfiguration": {
"DataFreshnessInSeconds": 300,
"DeadLetterQueueS3Configuration": {
"BucketARN": "arn:aws:s3:::kds-s3-gp-20260901-2210-123456789012",
"ExpectedBucketOwner": "123456789012",
"ErrorOutputPrefix": "dlq/"
},
"StorageConfiguration": {
"BucketARN": "arn:aws:s3:::kds-s3-gp-20260901-2210-123456789012",
"ExpectedBucketOwner": "123456789012",
"OutputKeyTemplate": "data/year=!{yyyy}/month=!{MM}/day=!{dd}/hour=!{HH}/records-!{extension}",
"StorageClass": "STANDARD",
"CompressionType": "GZIP"
}
}
}
aws kinesis create-channel \
--cli-input-json file://scripts/params/create-channel.json
DLQ の設定は S3DestinationConfiguration の直下にあり、StorageConfiguration の中ではありません。また、圧縮に GZIP を指定する場合は、キーテンプレートに !{extension} が必要です。暗号化を指定していないので、配信先は S3 既定の SSE-S3 です。
create-channel の応答
{
"ChannelDescription": {
"ChannelName": "kds-s3-gp-channel-20260901-2210",
"ChannelARN": "arn:aws:kinesis:ap-northeast-1:123456789012:channel/xxxxxxxxxxxxxxxxx",
"ChannelId": "xxxxxxxxxxxxxxxxx",
"ChannelStatus": "CREATING",
"ChannelCreationTimestamp": "2026-09-01T13:58:38+00:00",
"ServiceExecutionRoleARN": "arn:aws:iam::123456789012:role/kds-s3-gp-role-20260901-2210",
"StreamConfigurationList": [
{
"StreamARN": "arn:aws:kinesis:ap-northeast-1:123456789012:stream/kds-s3-gp-ods-20260901-2210",
"StreamCreationTimestamp": "2026-09-01T13:58:21+00:00",
"RecordConfiguration": {
"RecordFormatType": "JSON"
}
}
],
"S3DestinationConfiguration": {
"DataFreshnessInSeconds": 300,
"DeadLetterQueueS3Configuration": {
"BucketARN": "arn:aws:s3:::kds-s3-gp-20260901-2210-123456789012",
"ExpectedBucketOwner": "123456789012",
"ErrorOutputPrefix": "dlq/"
},
"StorageConfiguration": {
"BucketARN": "arn:aws:s3:::kds-s3-gp-20260901-2210-123456789012",
"ExpectedBucketOwner": "123456789012",
"OutputKeyTemplate": "data/year=!{yyyy}/month=!{MM}/day=!{dd}/hour=!{HH}/records-!{extension}",
"StorageClass": "STANDARD",
"CompressionType": "GZIP"
}
},
"LoggingConfiguration": {
"CloudWatchLogs": {
"Enabled": false
}
}
}
}
作成直後のステータスは CREATING でした。少し待って取得した応答から、ステータスの部分を抜き出しました。
{
"ChannelDescription": {
"ChannelName": "kds-s3-gp-channel-20260901-2210",
"ChannelStatus": "ACTIVE",
"ChannelCreationTimestamp": "2026-09-01T13:58:38+00:00"
}
}
作成から ACTIVE の観測までは、13:58:38Z から 13:58:46Z までの 8 秒でした。create-channel の応答には、指定したレコード形式・GZIP 圧縮・保存先と DLQ の設定がそのまま反映されていました。
キー構造
{
"Contents": [
{
"Key": "data/year=2026/month=09/day=01/hour=13/records--000000000000000000-000000-9d6757de83b.gz",
"LastModified": "2026-09-01T14:15:52+00:00",
"ETag": "\"dd02b756664cd84f8b3d675727905c1d-1\"",
"ChecksumAlgorithm": [
"SHA256"
],
"ChecksumType": "COMPOSITE",
"Size": 66,
"StorageClass": "STANDARD"
},
{
"Key": "data/year=2026/month=09/day=01/hour=13/records--000000000000100000-000000-9d6757de83b.gz",
"LastModified": "2026-09-01T14:16:17+00:00",
"ETag": "\"2b73988bd978d1be87230ceb356aff6e-1\"",
"ChecksumAlgorithm": [
"SHA256"
],
"ChecksumType": "COMPOSITE",
"Size": 89,
"StorageClass": "STANDARD"
}
],
"RequestCharged": null,
"Prefix": "data/"
}
指定したキーテンプレートのとおり、年・月・日・時を Hive 形式のパーティションとして含むキーになりました。テンプレートで指定したのは records- までで、その後ろの連番とユニークな接尾辞、.gz はサービスが付けます。有効な JSON のレコード(次の節で投入した 3 件)は 2 つのオブジェクトに分かれ、連番を含むキーで区別されました。
配信オブジェクトの中身
有効な JSON 3 件と、JSON ではない文字列 1 件を投入しました。beta のレコードだけ、コロンとカンマの後に空白を入れています。
aws kinesis put-record --stream-name "$STREAM_NAME" --partition-key alpha \
--cli-binary-format raw-in-base64-out \
--data '{"message":"alpha","n":1,"nested":{"z":true}}'
aws kinesis put-record --stream-name "$STREAM_NAME" --partition-key beta \
--cli-binary-format raw-in-base64-out \
--data '{"message": "beta", "n": 2, "array": [1, 2, 3]}'
aws kinesis put-record --stream-name "$STREAM_NAME" --partition-key gamma \
--cli-binary-format raw-in-base64-out \
--data '{"message":"gamma","n":3,"null":null}'
aws kinesis put-record --stream-name "$STREAM_NAME" --partition-key invalid \
--cli-binary-format raw-in-base64-out \
--data 'not-json'
配信された 2 つのオブジェクトを GZIP 解凍して連結すると、こうなりました。1 つ目(連番 000000000000000000)に beta、2 つ目に alpha と gamma が入っています。
{"message": "beta", "n": 2, "array": [1, 2, 3]}
{"message":"alpha","n":1,"nested":{"z":true}}
{"message":"gamma","n":3,"null":null}
区切り文字を確かめるため、alpha と gamma が入ったオブジェクトを od -c で表示しました。
0000000 { " m e s s a g e " : " a l p h
0000020 a " , " n " : 1 , " n e s t e d
0000040 " : { " z " : t r u e } } \r \n {
0000060 " m e s s a g e " : " g a m m a
0000100 " , " n " : 3 , " n u l l " : n
0000120 u l l } \r \n
0000126
今回のオブジェクトでは、レコードの区切りは CRLF で、各レコードの末尾にも CRLF が付いていました。レコード形式に JSON を指定していても、今回の入力では空白の除去やキー順の変更といった再シリアライズは起きず、投入時の表記の差がそのまま保持されました。
配信までの経過時間
チャネルの ACTIVE を観測した時刻、最初のレコードの投入時刻、最初のオブジェクトの LastModified を並べました。
channel_active_observed_utc=2026-09-01T13:58:46+00:00
first_record_put_utc=2026-09-01T13:58:46+00:00
s3_first_object_last_modified_utc=2026-09-01T14:15:52+00:00
4 レコードそれぞれの投入時刻
record_1_put_utc=2026-09-01T13:58:46+00:00
record_2_put_utc=2026-09-01T13:58:47+00:00
record_3_put_utc=2026-09-01T13:58:48+00:00
record_4_put_utc=2026-09-01T13:58:49+00:00
1 つのチャネルで 1 回実行した今回の結果では、最初の PutRecord から最初のオブジェクトまでは約 1,026 秒、およそ 17 分でした。最初の PutRecord は ACTIVE を観測したのと同じ 13:58:46Z なので、チャネルの起動待ち時間を含む値ではありません。DataFreshnessInSeconds には最小値の 300 を設定しましたが、この値どおりのタイミングでは配信されませんでした。2 つ目のオブジェクトは、1 つ目の 25 秒後に作成されました。
DLQ
{
"Contents": [
{
"Key": "dlq/DESERIALIZATION_ERROR/2026/09/01/13/kds-s3-gp-channel-20260901-2210-000000000000000000-000000-9d6757de83b.json",
"LastModified": "2026-09-01T14:15:52+00:00",
"Size": 372,
"StorageClass": "STANDARD"
}
],
"RequestCharged": null,
"Prefix": "dlq/"
}
指定した ErrorOutputPrefix の下に、エラー種別と時刻のパーティションが自動で付きました。オブジェクトの中身は 1 行の JSON です。
{"approximateArrivalTimestamp":1788271129616,"streamArn":"arn:aws:kinesis:ap-northeast-1:123456789012:stream/kds-s3-gp-ods-20260901-2210","shardId":"shardId-xxxxxxxxxxxxxxxxx","sequenceNumber":"xxxxxxxxxxxxxxxxx","errorCode":"Record.InvalidJson","errorMessage":"The record contains invalid JSON. Please verify the record is valid JSON."}
今回確認できたオブジェクトには、stream ARN・シャードID・シーケンス番号・到着時刻・エラー内容が JSON で出力され、レコード本体は含まれていませんでした。DLQ の内容だけでは、元のデータを復元できません。
S3 直接配信を利用できる条件と制約
Kinesis Data Streams から S3 へ直接配信するには、次の条件と制約があります。開発者ガイドと配信のクォータと制約を確認し、手元のストリームが該当するかを先に確認してください。
- 送信元ストリームが AWS マネージドキー(
aws/kinesis)で暗号化されている場合は使えません。CMK への移行が前提になります - キャパシティモードは On-Demand だけが対象で、Provisioned のストリームは対象外です
- 作成後に更新できるのは DataFreshnessInSeconds と LoggingConfiguration だけです。圧縮・キーテンプレート・レコード形式・宛先バケットは変更できないため、変えるには削除して作り直します。既存データのバックフィルもありません
- 1 つのストリームに作れる配信は 2 つまでで、Iceberg 用が 1 つ、汎用 S3 用が 1 つという内訳です。緩和申請はできません
- キーテンプレートで使える変数は時刻・チャネル名・ストリーム名だけです。レコードの内容によるパーティショニングはできません
- クロスリージョン配信はできません。宛先バケットだけは別アカウントを指定できます
Firehose との棲み分け
直接配信を利用できる場合でも、Firehose 側にしかない機能があります。Lambda によるデータ変換、Parquet / ORC への形式変換、レコード内容による動的パーティショニング、60 秒未満の鮮度、1 つのソースから複数の宛先への配信です。逆に直接配信側にしかないものとして、ZSTD 圧縮と、配信先ストレージクラスの指定(STANDARD / INTELLIGENT_TIERING / GLACIER_IR)があります。
まとめ
Kinesis Data Streams から S3 の汎用バケットへ、Firehose を介さず直接配信できました。検証では、Hive 形式のキーでの出力、JSON レコードの配信、無効な JSON レコードの DLQ 出力を確認しました。
Firehose の機能や Lambda による変換を必要としない場合は、今回追加された直接配信を試してみてください。
また、再処理やテストを見据えたアーカイブが目的なら、Kinesis Data Streams の保持期間を延長するよりも、S3 を再処理の起点として設計したほうが扱いやすい場面があります。データの長期保持を目的とする場合にも、直接配信を評価してみてください。







