[AWS CFn] Cloudwatch LogsのログをFirehose経由でS3に転送する

Cloudwatch LogsのログをKinesisのサブスクリプションフィルタを使って(ほぼリアルタイムに)S3に転送するCFnを作りました。



### ■ このテンプレートで作成するもの
※ ロググループが2つの場合

- S3：ログ転送先
- IAMロール：
    - subscription_filter_role：CWLからFirehoseデータストリームへ書き込み権限与えたもの
    - firehose_delivery_role：S3にデータを転送するのに必要な権限与えたもの
- Firehoseデータストリーム (×2)
- CWLサブスクリプションフィルタ (×2)


### ■ テンプレート
（パラメータの値をいい感じに変えると汎用的に使えるはずです。）

```yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: CloudwatchLogs to S3
Metadata: 
  AWS::CloudFormation::Interface: 
    ParameterGroups: 
      - Label:
          default: "S3"
        Parameters:
          - S3BucketName
          - S3ExpireDate
      - Label:
          default: "LogGroupNames"
        Parameters:
          - LogGroupName01
          - LogGroupName02
      - Label:
          default: "CWL"
        Parameters:
          - SizeInMBs
          - IntervalInSeconds
          - CompressionFormat

Parameters:
# CWL
  SizeInMBs:
    Description: "The size of the buffer, in MBs, that Kinesis Data Firehose uses for incoming data before delivering it to the destination."
    Type: Number
    Default: 5
    MinValue: 1
    MaxValue: 128
  IntervalInSeconds:
    Description: The length of time, in seconds, that Kinesis Data Firehose buffers incoming data before delivering it to the destination. 
    Type: Number
    Default: 300
    MinValue: 60
    MaxValue: 900
  CompressionFormat:
    Description: "The type of compression that Kinesis Data Firehose uses to compress the data that it delivers to the Amazon S3 bucket. "
    Type: String
    Default: 'UNCOMPRESSED'
# S3
  S3ExpireDate:
    Description: Number of days to keep S3 file
    Type: String
    Default: 730
  S3BucketName:
    Description: Name of the S3 bucket
    Type: String
    Default: ''
# LogGroupNames
  LogGroupName01:
    Description: The log group to associate with the subscription filter01
    Type: String
    Default: ''
  LogGroupName02:
    Description: The log group to associate with the subscription filter02
    Type: String
    Default: ''



Resources:
#--------------------------------------------------#
# subscription_filter_role
#--------------------------------------------------#
  SubscriptionFilterRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: subscription_filter_role
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: !Sub 'logs.${AWS::Region}.amazonaws.com'
            Action:
              - sts:AssumeRole
      Path: /
      Policies:
        - PolicyName: subscription_filter_policy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - firehose:PutRecord
                  - firehose:PutRecords
                Resource: !Sub 'arn:aws:firehose:${AWS::Region}:${AWS::AccountId}:deliverystream/*'

#--------------------------------------------------#
# firehose_delivery_role
#--------------------------------------------------#
  DeliveryPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyName: firehose_delivery_policy
      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:::${S3bucket}'
              - !Sub 'arn:aws:s3:::${S3bucket}*'
      Roles:
        - !Ref 'DeliveryRole'

  DeliveryRole:
    Type: AWS::IAM::Role
    Properties: 
      RoleName: firehose_delivery_role
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Sid: ''
            Effect: Allow
            Principal:
              Service: firehose.amazonaws.com
            Action: sts:AssumeRole
            Condition:
              StringEquals:
                sts:ExternalId: !Ref 'AWS::AccountId'

#--------------------------------------------------#
# SubscriptionFilter
#--------------------------------------------------#
  SubscriptionFilter01:
    Type: AWS::Logs::SubscriptionFilter
    Properties: 
      DestinationArn: !GetAtt 'DeliveryStream01.Arn'
      FilterPattern: ''
      LogGroupName: !Ref 'LogGroupName01'
      RoleArn: !GetAtt 'SubscriptionFilterRole.Arn'

  SubscriptionFilter02:
    Type: AWS::Logs::SubscriptionFilter
    Properties: 
      DestinationArn: !GetAtt 'DeliveryStream02.Arn'
      FilterPattern: ''
      LogGroupName: !Ref 'LogGroupName02'
      RoleArn: !GetAtt 'SubscriptionFilterRole.Arn'


#--------------------------------------------------#
# Firehose DeliveryStream
#--------------------------------------------------#
  DeliveryStream01:
    Type: AWS::KinesisFirehose::DeliveryStream
    Properties:
      DeliveryStreamName: !Sub '${LogGroupName01}-stream' 
      DeliveryStreamType: 'DirectPut'
      S3DestinationConfiguration:
        BucketARN: !Sub 'arn:aws:s3:::${S3bucket}'
        BufferingHints:
          SizeInMBs: !Ref SizeInMBs
          IntervalInSeconds: !Ref IntervalInSeconds
        CompressionFormat: !Ref CompressionFormat
        Prefix: !Sub ${LogGroupName01}/
        RoleARN: !GetAtt 'DeliveryRole.Arn'

  DeliveryStream02:
    Type: AWS::KinesisFirehose::DeliveryStream
    Properties:
      DeliveryStreamName: !Sub '${LogGroupName02}-stream' 
      DeliveryStreamType: 'DirectPut'
      S3DestinationConfiguration:
        BucketARN: !Sub 'arn:aws:s3:::${S3bucket}'
        BufferingHints:
          SizeInMBs: !Ref SizeInMBs
          IntervalInSeconds: !Ref IntervalInSeconds
        CompressionFormat: !Ref CompressionFormat
        Prefix: !Sub ${LogGroupName02}/
        RoleARN: !GetAtt 'DeliveryRole.Arn'


#--------------------------------------------------#
# S3
#--------------------------------------------------#
  S3bucket:
    Type: AWS::S3::Bucket
    DeletionPolicy: Delete
    Properties:
      BucketName: !Ref 'S3BucketName'
      LifecycleConfiguration:
        Rules:
          - Id: !Sub 'ExpirationIn-${S3ExpireDate}Days'
            Status: Enabled
            ExpirationInDays: !Ref 'S3ExpireDate'

```

### ■ CFn作成時のポイント

#### Point1 : サブスクリプションフィルターの確認とディレクトリ
ロググループ単位でフィルタをかけるので、ロググループ単位でS3内にディレクトリが作成されます。
Cloudwatchのコンソールから、サブスクリプションフィルターがロググループにアタッチされていることを確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/491542/a3276c51-3d84-7769-d22c-0b7c57f685f4.png)

#### Point2 : プレフィックス
送信先のディレクトリ構成はYYYY/MM/DD/HH” (UTC)形式のプレフィックスが自動的作成されます。
`AWS::KinesisFirehose::DeliveryStream`でPrefixを指定することでこれより上の階層のディレクトリを指定できます。今回はロググループ名をプレフィックスにしているので`LogGroupName/YYYY/MM/DD/HH/各ファイル`になっています。

#### Point3 : ファイルの形式
CWLからFirehoseに送信されるデータはgzipに圧縮されます。CompressionFormatでzipなど指定してしまうと、S3からダウンロードファイルが解凍したときに文字化けしてしまうので注意です。（パラメータで書かなくてもよかったな。。）

> CloudWatch Logs から Amazon Kinesis Data Firehose に送信されたデータは、すでに gzip レベル 6 圧縮で圧縮されているため、Kinesis Data Firehose 配信ストリーム内で圧縮を使用する必要はありません。
[CloudWatch Logs サブスクリプションフィルタの使用 - Amazon CloudWatch Logs](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/logs/SubscriptionFilters.html#FirehoseExample)

### ■ おわり
設定したバッファーの時間に応じてS3に出力されるタイミングは変わってきますが、ほぼリアルタイムでCloudwatch LogsのログをS3に転送することができました。

### ■ 参考サイト
- [サブスクリプションを使用したログデータのリアルタイム処理 - Amazon CloudWatch Logs](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/logs/Subscriptions.html)
- [CloudWatch LogsのログデータをKinesis Data Firehose経由でS3に出力する | Developers.IO](https://dev.classmethod.jp/articles/cloudwatch-logs-to-s3-via-kinesis-data-firehose/)
- [Kinesis Firehoseを使ってCloudWatch Logをs3へ出力してみた | CCI TECH BLOG](https://tech-cci.io/archives/3561)