Streamlining NDJSON Processing with Kinesis Firehose Dynamic Partitioning
Kinesis Data Firehose recently introduced support for dynamic partitioning. With this, developers no longer require Lambda functions just to convert to NDJSON (Newline Delimited JSON).
Kinesis Data Firehose recently introduced support for dynamic partitioning. With this, developers no longer require Lambda functions just to convert to NDJSON (Newline Delimited JSON).
Building
The dynamic partitioning configuration lives in the CloudFormation template below.
AWSTemplateFormatVersion: 2010-09-09Description: Kinesis Data Firehose streaming NDJSON sample with dynamic partitioningResources: KinesisFirehoseDeliveryStream: Type: AWS::KinesisFirehose::DeliveryStream Properties: DeliveryStreamName: ndjson-firehose DeliveryStreamType: DirectPut ExtendedS3DestinationConfiguration: BucketARN: !GetAtt S3Bucket.Arn BufferingHints: IntervalInSeconds: 60 DynamicPartitioningConfiguration: Enabled: true Prefix: "success/user_id=!{partitionKeyFromQuery:user_id}/year=!{timestamp:yyyy}/month=!{timestamp:MM}/day=!{timestamp:dd}/hour=!{timestamp:HH}/" ErrorOutputPrefix: "error/year=!{timestamp:yyyy}/month=!{timestamp:MM}/day=!{timestamp:dd}/hour=!{timestamp:HH}/type=!{firehose:error-output-type}/" ProcessingConfiguration: Enabled: true Processors: - Type: AppendDelimiterToRecord Parameters: - ParameterName: Delimiter ParameterValue: '\\n' - Type: MetadataExtraction Parameters: - ParameterName: MetadataExtractionQuery ParameterValue: '{user_id: .user_id}' - ParameterName: JsonParsingEngine ParameterValue: JQ-1.6 RoleARN: !GetAtt IAMRoleKinesisFirehose.Arn
S3Bucket: Type: AWS::S3::Bucket Properties: BucketName: !Sub ${AWS::Region}-${AWS::AccountId}-ndjson-s3 BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: AES256 PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true
IAMRoleKinesisFirehose: Type: AWS::IAM::Role Properties: RoleName: ndjson-firehose-role AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: firehose.amazonaws.com Action: sts:AssumeRole MaxSessionDuration: 3600 Policies: - PolicyName: policy1 PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - glue:GetTable - glue:GetTableVersion - glue:GetTableVersions Resource: - !Sub arn:aws:glue:${AWS::Region}:${AWS::AccountId}:catalog - !Sub arn:aws:glue:${AWS::Region}:${AWS::AccountId}:database/%FIREHOSE_POLICY_TEMPLATE_PLACEHOLDER% - !Sub arn:aws:glue:${AWS::Region}:${AWS::AccountId}:table/%FIREHOSE_POLICY_TEMPLATE_PLACEHOLDER%/%FIREHOSE_POLICY_TEMPLATE_PLACEHOLDER% - Effect: Allow Action: - s3:AbortMultipartUpload - s3:GetBucketLocation - s3:GetObject - s3:ListBucket - s3:ListBucketMultipartUploads - s3:PutObject Resource: - !GetAtt S3Bucket.Arn - !Sub ${S3Bucket.Arn}/* - Effect: Allow Action: - lambda:InvokeFunction - lambda:GetFunctionConfiguration Resource: !Sub arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:%FIREHOSE_POLICY_TEMPLATE_PLACEHOLDER% - Effect: Allow Action: - kms:GenerateDataKey - kms:Decrypt Resource: - !Sub arn:aws:kms:${AWS::Region}:${AWS::AccountId}:key/%FIREHOSE_POLICY_TEMPLATE_PLACEHOLDER% Condition: StringEquals: kms:ViaService: !Sub s3.${AWS::Region}.amazonaws.com StringLike: kms:EncryptionContext:aws:s3:arn: - arn:aws:s3:::%FIREHOSE_POLICY_TEMPLATE_PLACEHOLDER%/* - arn:aws:s3:::%FIREHOSE_POLICY_TEMPLATE_PLACEHOLDER% - Effect: Allow Action: - logs:PutLogEvents Resource: - !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:%FIREHOSE_POLICY_TEMPLATE_PLACEHOLDER%:log-stream:*Deploy the stack:
aws cloudformation deploy \ --template-file stack.yaml \ --stack-name firehose-ndjson-sample \ --s3-bucket <YOUR_S3_BUCKET> \ --s3-prefix firehose-ndjson-sample/$(date +%Y/%m/%d/%H) \ --capabilities CAPABILITY_NAMED_IAMTesting
Data blobs must be Base64 encoded before being sent described in the official documentation.
The data blob, which is base64-encoded when the blob is serialized. The maximum size of the data blob, before base64-encoding, is 1,000 KiB.
Encode your sample JSON as follows:
$ echo -n '{"user_id": 1, "message": "Hello"}' | base64eyJ1c2VyX2lkIjogMSwgIm1lc3NhZ2UiOiAiSGVsbG8ifQ==
$ echo -n '{"user_id": 1, "message": "World"}' | base64eyJ1c2VyX2lkIjogMSwgIm1lc3NhZ2UiOiAiV29ybGQifQ==The encoded records can then be sent to the Firehose stream via the AWS CLI:
echo '{ "DeliveryStreamName": "ndjson-firehose", "Records": [ {"Data": "eyJ1c2VyX2lkIjogMSwgIm1lc3NhZ2UiOiAiSGVsbG8ifQ=="}, {"Data": "eyJ1c2VyX2lkIjogMSwgIm1lc3NhZ2UiOiAiV29ybGQifQ=="} ]}' > input.jsonaws firehose put-record-batch --cli-input-json file://~/input.jsonCheck the objects in your S3 bucket:
$ aws s3 ls --recursive s3://<AWS_REGION>-<AWS_ACCOUNT>-ndjson-s3/success/2022-07-26 23:52:47 69 success/user_id=1/year=2022/month=07/day=26/hour=14/ndjson-firehose-4-2022-07-26-14-50-22-b95618c0-e518-3b66-b06c-693b059cc751
$ aws s3 cp s3://<AWS_REGION>-<AWS_ACCOUNT>-ndjson-s3/success/user_id=1/year=2022/month=07/day=26/hour=14/ndjson-firehose-4-2022-07-26-14-50-22-b95618c0-e518-3b66-b06c-693b059cc751 ./download: s3://<AWS_REGION>-<AWS_ACCOUNT>-ndjson-s3/success/user_id=1/year=2022/month=07/day=26/hour=14/ndjson-firehose-4-2022-07-26-14-50-22-b95618c0-e518-3b66-b06c-693b059cc751 to ./ndjson-firehose-4-2022-07-26-14-50-22-b95618c0-e518-3b66-b06c-693b059cc751
$ cat -n ndjson-firehose-4-2022-07-26-14-50-22-b95618c0-e518-3b66-b06c-693b059cc751 1 {"user_id": 1, "message": "Hello"} 2 {"user_id": 1, "message": "World"}Cleaning Up
Remove the resources provisioned by this example with:
aws s3 rm --recursive s3://<AWS_REGION>-<AWS_ACCOUNT>-ndjson-s3/aws cloudformation delete-stack --stack-name firehose-ndjson-sampleConclusion
Enabling dynamic partitioning on a Firehose delivery stream produced partitioned, newline-delimited JSON in S3 without a transformation Lambda in the pipeline. DynamicPartitioningConfiguration: Enabled: true combined with the MetadataExtraction processor is what lets the Prefix template reference !{partitionKeyFromQuery:user_id} directly, pulling the partition key out of each record’s JSON body via the JQ query {user_id: .user_id} rather than requiring a Lambda function to compute it beforehand. Pairing that with the AppendDelimiterToRecord processor’s \\n delimiter covers the other half of what a transformation Lambda used to be needed for — producing clean NDJSON output. Since a MetadataExtractionQuery that doesn’t match the actual record shape routes silently to the error prefix instead of raising an error, it’s worth checking the error/ path in S3 alongside success/ the first time real traffic flows through.
Related posts
Configuring Record Separators for Kinesis Firehose in AWS IoT Core
Configuring a record separator on an IoT Core topic rule's Firehose action so records land in S3 newline-delimited instead of concatenated.
Querying S3 Logs with Athena and Kinesis Data Firehose
Piping logs into S3 through Kinesis Data Firehose and querying them with Athena, using custom prefixes so partitions register automatically.
Streaming OPC UA Data to Kinesis via SiteWise Edge Gateway
Bridging OPC UA telemetry to Kinesis Data Streams through SiteWise Edge Gateway and a custom Greengrass component.
Kinesis Shard Separation Using AWS IoT Topic Payloads
Routing IoT topic payloads to specific Kinesis shards by customer ID, keeping each customer's records in order downstream.
Sign in with Slack Using Cognito User Pools and OIDC
Federating Cognito user pools with Slack over OIDC and wiring "Sign in with Slack" into a Next.js app with Amplify.
