Nov 13, 20269 min read

Amazon Kinesis Data Streams with Terraform: On-Demand, Encryption and Consumers

Create an on-demand Kinesis stream with a customer-managed KMS key, a seven-day retention period, a registered enhanced fan-out consumer, producer and consumer IAM, and an iterator-age alarm, then the same fan-out job as a topic in Encore.

Illustration for Amazon Kinesis Data Streams with Terraform: On-Demand, Encryption and Consumers

Amazon Kinesis Data Streams keeps records in an ordered log for as long as the retention period allows, and Terraform can create that stream, its encryption key and the IAM grants producers and consumers need. The defaults are easy to miss: encryption is off, retention is 24 hours, and on-demand mode starts at 4 MB/s of write throughput.

This guide writes a Terraform configuration for an on-demand, KMS-encrypted stream that holds document events for seven days, registers one enhanced fan-out consumer and alarms when iterator age grows, then shows how Encore expresses the common fan-out case as a topic and subscriptions in Go.

What a Kinesis Terraform setup needs

PieceWhat it controlsTypical miss
Capacity modeOn-demand scaling, or a fixed shard_countOn-demand starts at 4 MB/s write, 8 MB/s read, not unlimited
EncryptionServer-side KMS on every recordencryption_type defaults to NONE
RetentionHow long records can be replayedDefaults to 24 hours; the maximum is 8,760 hours (365 days)
Record sizeMaximum payload per recordThe service cap is 10 MiB; max_record_size_in_kib has to be raised to use it
Producer IAMPutRecord / PutRecords plus KMSWrites fail with a KMS error on an encrypted stream
Consumer IAMGetRecords or SubscribeToShard, plus KMSSame KMS miss on reads
Registered consumerEnhanced fan-out, a dedicated 2 MB/s pipeUp to 20 registered consumers per stream on standard and provisioned modes
Iterator age alarmHow far the slowest consumer is behindA stuck shard iterator is silent without CloudWatch

The Terraform aws_kinesis_stream resource documents the stream arguments, including stream_mode_details, retention_period, encryption_type and max_record_size_in_kib. Limits below come from the Kinesis quotas page and the key concepts page.

The stream, the key and the consumer

The example is a stream of document events: a producer writes one record per document ID, an archiver reads the stream through enhanced fan-out, and a notifier can be added later as a second consumer of the same records. Retention is 168 hours so a failed archiver can catch up across a weekend.

terraform { required_version = ">= 1.6" required_providers { aws = { source = "hashicorp/aws" version = "~> 6.0" } } } provider "aws" { region = "eu-central-1" } data "aws_caller_identity" "current" {} variable "producer_role_name" { type = string description = "Name of the IAM role that writes document events" } variable "consumer_role_name" { type = string description = "Name of the IAM role that reads the stream" } data "aws_iam_role" "producer" { name = var.producer_role_name } data "aws_iam_role" "consumer" { name = var.consumer_role_name } resource "aws_kms_key" "documents" { description = "Kinesis document events" enable_key_rotation = true policy = jsonencode({ Version = "2012-10-17" Statement = [ { Sid = "AccountAdmin" Effect = "Allow" Principal = { AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root" } Action = "kms:*" Resource = "*" }, { Sid = "StreamRoles" Effect = "Allow" Principal = { AWS = [data.aws_iam_role.producer.arn, data.aws_iam_role.consumer.arn] } Action = [ "kms:Decrypt", "kms:DescribeKey", "kms:GenerateDataKey", "kms:GenerateDataKeyWithoutPlaintext", ] Resource = "*" Condition = { StringEquals = { "kms:ViaService" = "kinesis.eu-central-1.amazonaws.com" } } }, ] }) } resource "aws_kms_alias" "documents" { name = "alias/kinesis-documents" target_key_id = aws_kms_key.documents.key_id } resource "aws_kinesis_stream" "documents" { name = "document-events" retention_period = 168 encryption_type = "KMS" kms_key_id = aws_kms_key.documents.arn max_record_size_in_kib = 10240 enforce_consumer_deletion = true shard_level_metrics = [ "IncomingBytes", "OutgoingBytes", "IncomingRecords", "IteratorAgeMilliseconds", ] stream_mode_details { stream_mode = "ON_DEMAND" } } # Dedicated 2 MB/s read pipe for the archiver. Standard and provisioned # modes allow up to 20 registered consumers on a stream. resource "aws_kinesis_stream_consumer" "archiver" { name = "archiver" stream_arn = aws_kinesis_stream.documents.arn }

encryption_type has to be set to KMS; leaving it out leaves the stream in the default NONE. max_record_size_in_kib is raised to 10,240 because the service accepts records up to 10 MiB, and the attribute's documented minimum is 1,024 KiB. enforce_consumer_deletion lets Terraform destroy the stream without first deleting every registered consumer by hand.

On-demand mode does not take shard_count. A provisioned stream would set stream_mode = "PROVISIONED" and a shard_count, with each shard limited to 1 MB/s or 1,000 records/s for writes and 2 MB/s for reads. You can switch a given stream between the two modes only twice within 24 hours.

Producer and consumer IAM

The producer writes records; the consumer reads them through enhanced fan-out. Both also need the KMS actions on the key, already granted in the key policy above. The IAM section of the encryption docs is what makes those KMS grants required.

resource "aws_iam_role_policy" "producer_kinesis" { name = "document-events-producer" role = var.producer_role_name policy = jsonencode({ Version = "2012-10-17" Statement = [{ Effect = "Allow" Action = [ "kinesis:PutRecord", "kinesis:PutRecords", "kinesis:DescribeStream", "kinesis:DescribeStreamSummary", "kinesis:ListShards", ] Resource = aws_kinesis_stream.documents.arn }] }) } resource "aws_iam_role_policy" "consumer_kinesis" { name = "document-events-consumer" role = var.consumer_role_name policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Action = [ "kinesis:DescribeStream", "kinesis:DescribeStreamSummary", "kinesis:ListShards", "kinesis:GetShardIterator", "kinesis:GetRecords", "kinesis:SubscribeToShard", "kinesis:DescribeStreamConsumer", ] Resource = [ aws_kinesis_stream.documents.arn, aws_kinesis_stream_consumer.archiver.arn, ] }, ] }) } resource "aws_cloudwatch_metric_alarm" "iterator_age" { alarm_name = "document-events-iterator-age" comparison_operator = "GreaterThanThreshold" evaluation_periods = 3 metric_name = "GetRecords.IteratorAgeMilliseconds" namespace = "AWS/Kinesis" period = 60 statistic = "Maximum" threshold = 60000 treat_missing_data = "notBreaching" dimensions = { StreamName = aws_kinesis_stream.documents.name } }

SubscribeToShard and DescribeStreamConsumer are what the archiver needs for enhanced fan-out; a classic GetRecords consumer would still use GetShardIterator and GetRecords. The alarm fires when the oldest record waiting in a shard iterator is more than 60 seconds old for three minutes, which is the usual first sign that a consumer has stalled. The registered consumer resource notes a limit of 20 consumers per stream, which matches the standard and provisioned quota; on-demand Advantage mode can go higher, as the quotas page describes.

The KMS key policy uses the role ARNs looked up from those names, and the inline policies attach by name.

What stays manual

  • Partition keys. Terraform creates the stream; every PutRecord still has to set a partition key. Records with the same key land in the same shard and stay ordered there. A key that is too hot, such as a single tenant ID that produces most of the traffic, will concentrate writes on one shard even in on-demand mode until the stream splits it.
  • Checkpoints. Kinesis does not track which records a consumer has processed. The archiver has to store its own iterator or sequence number, often in a DynamoDB table used by the Kinesis Client Library, and that table is outside this configuration.
  • Retention cuts. Lowering retention_period deletes records older than the new window immediately. Raise it first if you are unsure, and treat a reduction as a data-loss change.
  • Capacity mode switches. A stream can change between on-demand and provisioned only twice in 24 hours. Plan the change, then leave the mode alone while traffic settles.
  • Lambda or ECS wiring. An event source mapping, a consumer application and its DynamoDB lease table are separate resources. The stream, the consumer registration and the IAM grants above are the parts Terraform can define before that application exists.

How does Encore handle this kind of fan-out from code?

Encore

Encore brings infrastructure from code for the common case this stream is often asked to cover: one event, several independent consumers, and order preserved per document. A topic and its subscriptions are declared in Go, and a deploy to your AWS account provisions SNS and SQS, as the AWS infrastructure docs list. Encore provisions an IAM role for the service. Encore does not provision a Kinesis stream, and it does not keep a replay window of past records.

Application code passes through Encore and infrastructure guardrails to resources provisioned in your own AWS or GCP account.
Encore provisions the infrastructure your application needs in your own AWS or GCP account.

Set up the app

Get started
brew install encoredev/tap/encore &&
encore app create
Copy

Run in your terminal to get started locally.

~
$ encore app create documents --example=hello-world$ cd documents && mkdir documents
// documents/documents.go package documents import ( "context" "time" "encore.dev/beta/errs" "encore.dev/pubsub" ) type DocumentEvent struct { DocumentID string `json:"document_id" pubsub-attr:"document_id"` Kind string `json:"kind"` Body string `json:"body"` } // Messages with the same DocumentID arrive in publish order. var Events = pubsub.NewTopic[*DocumentEvent]("document-events", pubsub.TopicConfig{ DeliveryGuarantee: pubsub.AtLeastOnce, OrderingAttribute: "document_id", }) var _ = pubsub.NewSubscription(Events, "archive", pubsub.SubscriptionConfig[*DocumentEvent]{ Handler: archive, AckDeadline: 30 * time.Second, RetryPolicy: &pubsub.RetryPolicy{MaxRetries: 8}, }) var _ = pubsub.NewSubscription(Events, "notify", pubsub.SubscriptionConfig[*DocumentEvent]{ Handler: notify, AckDeadline: 30 * time.Second, RetryPolicy: &pubsub.RetryPolicy{MaxRetries: 8}, }) type PublishParams struct { DocumentID string `json:"document_id"` Kind string `json:"kind"` Body string `json:"body"` } // Called by the editor service after a document save. // //encore:api private method=POST path=/documents/events func Publish(ctx context.Context, p *PublishParams) error { if p.DocumentID == "" || p.Kind == "" { return &errs.Error{Code: errs.InvalidArgument, Message: "document_id and kind are required"} } _, err := Events.Publish(ctx, &DocumentEvent{ DocumentID: p.DocumentID, Kind: p.Kind, Body: p.Body, }) return err } func archive(ctx context.Context, ev *DocumentEvent) error { // Write the event to long-term storage. Return an error to retry // until MaxRetries, after which the message goes to the subscription DLQ. return nil } func notify(ctx context.Context, ev *DocumentEvent) error { return nil }

Publish is a private endpoint, called by the editor service after a document save, so unauthenticated callers cannot inject events onto the topic. Each subscription gets its own copy of every message, with its own retries and dead-letter queue, which is the SNS plus SQS pattern from the Kinesis vs SQS comparison. Ordered topics on AWS are limited to 300 messages per second, as the Pub/Sub docs state. That is enough for document workflows and far below a clickstream, and a high-volume log with replay belongs on Kinesis.

A coding agent working in this repository adds a subscription in the same change as the handler and publishes events under encore run, where the topic is local and both subscribers receive the events.

Best fit: application events that several services should each process once, with optional per-key ordering, running on SNS and SQS in your own AWS account. Stay with the Terraform stream when consumers must replay a window of past records, when a single payload approaches 10 MiB, or when ingest is measured in megabytes per second.

Good to know

  • A head-to-head of Kinesis and SQS covers replay, retention, shard throughput and when a queue is the better model. This page is the Terraform for the stream itself.
  • On-demand streams in Northern Virginia, Oregon and Ireland can scale to 10 GB/s of write and 20 GB/s of read; in other regions the cap is 200 MB/s write and 400 MB/s read, unless you raise it with a support ticket, per the quotas page.
  • Shard-level metrics have to be listed by name, and the Terraform resource warns not to pass ALL.
  • Destroying a stream without enforce_consumer_deletion fails while a registered consumer still exists.

Frequently asked questions

How do you create a Kinesis Data Stream with Terraform?

Use the aws_kinesis_stream resource with stream_mode_details.stream_mode set to ON_DEMAND or PROVISIONED. Set retention_period between 24 and 8760 hours, set encryption_type to KMS with a kms_key_id, and grant producers and consumers both Kinesis and KMS permissions. The default encryption type is NONE and the default retention period is 24 hours.

Should a Kinesis stream use on-demand or provisioned mode?

On-demand is the simpler starting point: AWS scales the shards, and a new stream begins at 4 MB/s of write and 8 MB/s of read throughput, as the [Kinesis quotas](https://docs.aws.amazon.com/streams/latest/dev/service-sizes-and-limits.html) describe. Provisioned mode sets shard_count yourself, with each shard limited to 1 MB/s or 1,000 records/s for writes. You can switch modes only twice in 24 hours.

How do you encrypt a Kinesis stream in Terraform?

Set encryption_type to KMS and kms_key_id to a customer-managed key, or to the aws/kinesis alias for the AWS-managed key. Producers need kms:GenerateDataKey and kms:Decrypt on that key, and consumers need kms:Decrypt, in addition to their Kinesis permissions, as the [Kinesis encryption docs](https://docs.aws.amazon.com/streams/latest/dev/key-concepts.html#server-side-encryption) require.

How does Encore replace a Kinesis fan-out setup?

Declare a topic with an OrderingAttribute and one subscription per consumer. Encore provisions SNS and SQS in your AWS account, each subscription gets every message, and messages with the same ordering attribute arrive in publish order, with ordered topics limited to 300 messages per second on AWS. Encore does not provision Kinesis, and it does not replay a past window of records.

Deploy to your cloud on AWS and Google Cloud
without managing infrastructure

Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.

~/orders — claude
Claude Code
Claude Code v2.1.180Opus 5.5~/orders