はじめに
やりたかったこと:CloudWatchログをS3に保存し、そのログをAthenaでクエリしたかった
上記を実現するために、CloudFormationで、CloudWatch Logs → Subscription Filter → Kinesis Data Firehose → S3 という構成を実装したものの、S3に保存されたファイルがバイナリ形式になり、Athenaでクエリできませんでした。
また、各リソースごとの設定やデフォルト動作も微妙に異なるといったことがあり、この機に全体的に調査をしました。
構成
マネージドサービスのみで、CloudWatch Logsから、S3へ転送する場合、以下の構成になります。
CloudWatch Logs → Subscription Filter → Kinesis Data Firehose → S3
問題:S3のオブジェクトがバイナリ扱いになる
最初にFirehoseでS3にログを転送したところ、S3上のオブジェクトがバイナリファイルとして扱われ、Athenaでクエリできない状態になりました。
※オブジェクトをダウンロードしたときに発覚し、.gz拡張子を付けた場合に正常に開けることを確認しました。
原因の調査
上記構成の場合、Subscription Filterの設定と、Firehoseの設定により、送信されるログには以下の4つのパターンがあることが判明しました。:
- Subscription Filterで圧縮 + Firehose で圧縮 → 2重gzip圧縮
- Subscription Filterで圧縮 + Firehose で圧縮しない → gzip圧縮
- Subscription Filterで圧縮 → Firehose で解凍 → 再圧縮 → gzip圧縮
- Subscription Filterで圧縮 → Firehose で解凍 → 圧縮しない → 未圧縮
- Subscription Filterでは、デフォルトで圧縮が行われます。
- 1, 2, 3の場合はデフォルトでは、ファイル拡張子がつきません。
そのため、全部のパターンで、Athenaでクエリできないじゃないか、、、という話になります。
元々の実装は2番のパターンで、ファイルはgzip圧縮されているものの拡張子がないため、Athenaが圧縮ファイルとして認識できない状態でした。
CSV、TSV、および JSON のデータについては、Athena がファイル拡張子から圧縮タイプを判断します。ファイル拡張子がない場合、Athena はデータを非圧縮のプレーンテキストとして扱います。
解決策:ExtendedS3DestinationConfigurationを使用
この問題の解決策は、最終的なオブジェクトにファイル拡張子を付けることです。
また、上記の変更にあたっては、S3DestinationConfigurationプロパティからExtendedS3DestinationConfigurationプロパティへの変更が必要でした。
変更前(問題のある設定)
S3DestinationConfiguration:
BucketARN: !Sub "arn:aws:s3:::${S3BucketName}"
CompressionFormat: UNCOMPRESSED
# FileExtension指定不可
変更後(解決後の設定)
ExtendedS3DestinationConfiguration: # 変更
BucketARN: !Sub "arn:aws:s3:::${S3BucketName}"
CompressionFormat: UNCOMPRESSED
FileExtension: ".gz" # 変更、また、拡張子指定
この後、Athenaでデータベース等を作成して、実際にクエリできることも確認しました。
重要なポイント
-
S3DestinationConfigurationプロパティではFileExtensionプロパティが使用できない -
ExtendedS3DestinationConfigurationプロパティでのみファイル拡張子の指定が可能 - CloudFormationでの変更は「No interruption」で実行可能
学んだこと
- Athenaでの圧縮ファイル読み込み - ファイル拡張子が必須
- FirehoseのDestination設定 - S3DestinationConfigurationでは拡張子指定不可
- 圧縮パターンの理解 - Subscription FilterとFirehoseの組み合わせによる違い
デモ用CloudFormationテンプレート
完全なCloudFormationテンプレート(クリックして展開)
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
S3BucketName:
Type: String
Description: S3 bucket name for storing logs
LogGroupName:
Type: String
Description: CloudWatch log group name
Resources:
# ------------------------------#
# IAM Policy, Role for Firehose
# ------------------------------#
FirehosePolicy:
Type: AWS::IAM::ManagedPolicy
Properties:
Description: "Allow Firehose to put logs to S3"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- "s3:AbortMultipartUpload"
- "s3:GetBucketLocation"
- "s3:GetObject"
- "s3:ListBucket"
- "s3:ListBucketMultipartUploads"
- "s3:PutObject"
Resource:
- !Sub "arn:aws:s3:::${S3BucketName}"
- !Sub "arn:aws:s3:::${S3BucketName}/*"
FirehoseRole:
Type: AWS::IAM::Role
Properties:
ManagedPolicyArns:
- !Ref FirehosePolicy
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action: sts:AssumeRole
Principal:
Service: firehose.amazonaws.com
# ------------------------------------------#
# IAM Policy, Role for Subscription Filter
# ------------------------------------------#
SubscriptionFilterPolicy:
Type: AWS::IAM::ManagedPolicy
Properties:
Description: "Allow Subscription Filter to put logs to Firehose"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Action:
- "firehose:PutRecord"
- "firehose:PutRecords"
- "firehose:PutRecordBatch"
Resource: !Sub 'arn:aws:firehose:${AWS::Region}:${AWS::AccountId}:deliverystream/*'
Effect: "Allow"
SubscriptionFilterRole:
Type: AWS::IAM::Role
Properties:
ManagedPolicyArns:
- !Ref SubscriptionFilterPolicy
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action: sts:AssumeRole
Principal:
Service: !Sub "logs.${AWS::Region}.amazonaws.com"
# ------------------------------#
# Firehose
# ------------------------------#
Log1Firehose:
Type: AWS::KinesisFirehose::DeliveryStream
Properties:
DeliveryStreamEncryptionConfigurationInput:
KeyType: AWS_OWNED_CMK
DeliveryStreamName: !Sub "firehose${LogGroupName}"
DeliveryStreamType: DirectPut
ExtendedS3DestinationConfiguration:
BucketARN: !Sub "arn:aws:s3:::${S3BucketName}"
BufferingHints:
IntervalInSeconds: 900
SizeInMBs: 128
Prefix: !Sub "firehose${LogGroupName}/"
ErrorOutputPrefix: !Sub "firehose/error${LogGroupName}/"
FileExtension: ".gz"
CompressionFormat: UNCOMPRESSED
CloudWatchLoggingOptions:
Enabled: false
RoleARN: !GetAtt FirehoseRole.Arn
# ------------------------------#
# Subscription Filter
# ------------------------------#
SubscriptionFilter1:
Type: AWS::Logs::SubscriptionFilter
Properties:
DestinationArn: !GetAtt Log1Firehose.Arn
FilterName: !Sub "subscription-filter${LogGroupName}"
FilterPattern: ""
LogGroupName: !Ref LogGroupName
RoleArn: !GetAtt SubscriptionFilterRole.Arn
まとめ
ただ「ログをS3に保存、クエリがしたい」だけでしたが、ファイル拡張子の設定が重要でした。
そもそも、もっと簡単にCloudWatch LogsからS3に転送したり、各リソースからS3に直接送れるようになると嬉しいな・・・
