Amazon Auroraの各種ログをCloudWatch LogsからS3に連携してみた

2019.09.09

こんにちは、崔です。

Aurora MySQLの各種ログを出力し、そのログファイルをCloudWatch LogsからKinesis Data Firehoseを用いて、S3まで連携する機会がありましたので、参考までにCloudFormationのテンプレートを紹介させていただきます。 今回試したAuroraのエンジンバージョンは5.7.mysql_aurora.2.04.5です。

ログ出力の設定

まずAuroraの各種ログ出力を設定していきます。

監査ログの設定

最初にAurora MySQLにて監査ログを設定します。 監査ログの設定はクラスタパラメータグループで設定します。 以下のパラメータを設定します。

パラメータ名 備考
server_audit_events CONNECT,QUERY,TABLE 監査対象のイベント
server_audit_logging 1 監査ログを有効にする
server_audit_excl_users <なし> 監査対象外とするユーザ
server_audit_incl_users <なし> 監査対象とするユーザ

CloudFormationのテンプレートは下記のように記載します。

  DBClusterParameterGroup:
    Type: AWS::RDS::DBClusterParameterGroup
    Properties:
      Family: aurora-mysql5.7
      Parameters:
        server_audit_events: 'CONNECT,QUERY,TABLE'
        server_audit_logging: 1

パラメータの設定内容を補足します。 server_audit_eventsには記録するイベントをカンマ区切り、大文字で指定します。 次のイベントの任意の組み合わせを記録できます。

イベント名 備考
CONNECT 成功した接続と失敗した接続の両方、および切断を記録。このイベントにはユーザ情報が含まれる。
QUERY すべてのクエリをプレーンテキストで記録。構文またはアクセス権限エラーで失敗したものも含む。
QUERY_DCL QUERYイベントと同様だが、GRANT,REVOKEなどのデータ制御言語(DCL)クエリのみ返す。
QUERY_DDL QUERYイベントと同様だが、CREATE,ALTERなどのデータ定義言語(DDL)クエリのみ返す。
QUERY_DML QUERYイベントと同様だが、INSERT,UPDATE,SELECTなどのデータ操作言語(DML)クエリのみ返す。
TABLE クエリ実行の影響を受けたテーブルを記録する。

今回は全てを記録するために、CONNECT,QUERY,TABLEを指定します。

また、server_audit_excl_usersには監査対象外とするユーザをカンマ区切りで指定します。 server_audit_excl_usersを指定した場合は、そのユーザ以外が監査対象となります。 server_audit_incl_usersには監査対象とするユーザをカンマ区切りで指定します。 今回はどちらも指定しません。この場合、全てのユーザが監査対象となります。 どちらのパラメータにも同じユーザを指定した場合は、server_audit_incl_usersが優先されるため、監査対象となります。

一般ログ、スロークエリログの設定

一般ログ、スロークエリログはパラメータグループで設定します。 以下のパラメータを設定します。

パラメータ名 備考
general_log 1 一般ログを有効にする
slow_query_log 1 スロークエリログを有効にする
long_query_time 1 スロークエリの閾値(秒)

long_query_timeには各システムの閾値となる秒数を指定して下さい。 CloudFormationのテンプレートは下記のように記載します。

  RDSDBParameterGroup:
    Type: AWS::RDS::DBParameterGroup
    Properties:
      Family: aurora-mysql5.7
      Parameters:
        general_log: '1'
        slow_query_log: '1'
        long_query_time: '1'

CloudWatch Logsへの出力

各種ログを設定した後、AuroraからCloudWatch Logsにログを出力します。 ログ出力先であるCloudWatch Logsのロググループ名、ログストリーム名は以下の形式となります。

ログ種別 ロググループ名 ログストリーム名
監査ログ /aws/rds/cluster/<クラスタ名>/audit <DBインスタンス名>.audit.log.n.YYYY-MM-DD-HH-MI.n.n
エラーログ /aws/rds/cluster/<クラスタ名>/error <DBインスタンス名>
一般ログ /aws/rds/cluster/<クラスタ名>/general <DBインスタンス名>
スロークエリログ /aws/rds/cluster/<クラスタ名>/slowquery <DBインスタンス名>

CloudWatch Logsへの出力設定は、CloudFormationのテンプレートでは、DBクラスタ内で設定します。

  RDSCluster:
    Type: AWS::RDS::DBCluster
    Properties:
      EnableCloudwatchLogsExports:
        - audit
        - general
        - error
        - slowquery

また、CloudWatch Logsの保存期間を設定する場合は、ロググループを下記のように設定します。 今回は保存期間90日で設定します。 (ログ出力時にロググループは自動作成されますので、明示的に作成しなくても良いです。が、今回は保存期間やS3への連携を設定するために作成します。)

  RDSauditLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub "/aws/rds/cluster/${DBInstanceId}-cluster/audit"
      RetentionInDays: 90
  RDSerrorLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub "/aws/rds/cluster/${DBInstanceId}-cluster/error"
      RetentionInDays: 90
  RDSgeneralLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub "/aws/rds/cluster/${DBInstanceId}-cluster/general"
      RetentionInDays: 90
  RDSslowqueryLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub "/aws/rds/cluster/${DBInstanceId}-cluster/slowquery"
      RetentionInDays: 90

S3へ転送

CloudWatch Logsへの出力設定のあとは、Kinesis Data Firehoseを用いたS3への転送設定を行います。 こちらのブログを参考に設定できます。

CloudWatchLogsのログをFirehose経由でS3に出力してみた

S3バケットの作成、IAMポリシー/IAMロールの作成、Kinesis Data Firehoseの転送ストリームを作成します。 S3では90日でIntelligent-Tieringへの移動、365日で削除としました。 (ひとまず監査ログの転送ストリームになります。)

  S3bucket:
    Type: AWS::S3::Bucket
    DeletionPolicy: Delete
    Properties:
      BucketName: !Sub ${DBInstanceId}-log-${AWS::AccountId}
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      LifecycleConfiguration:
        Rules:
          - Id: AutoDelete
            Status: Enabled
            ExpirationInDays: 365
            Transitions:
              - StorageClass: INTELLIGENT_TIERING
                TransitionInDays: 90

  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:::${DBInstanceId}-log-${AWS::AccountId}'
              - !Sub 'arn:aws:s3:::${DBInstanceId}-log-${AWS::AccountId}*'
      Roles:
        - !Ref 'deliveryRole'

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

  AuditLogstream:
    Type: AWS::KinesisFirehose::DeliveryStream
    DependsOn: S3bucket
    Properties:
      ExtendedS3DestinationConfiguration:
        BucketARN: !Sub 'arn:aws:s3:::${DBInstanceId}-log-${AWS::AccountId}'
        BufferingHints:
          IntervalInSeconds: '60'
          SizeInMBs: '50'
        CompressionFormat: GZIP
        Prefix: audit/
        RoleARN: !GetAtt 'deliveryRole.Arn'
        ProcessingConfiguration:
          Enabled: 'false'
        CloudWatchLoggingOptions:
          Enabled: 'true'
          LogGroupName: !Sub '/aws/firehose/${DBInstanceId}-auditlog-deliverystream'
          LogStreamName: 'AuditLogDelivery'

  AuditLogstreamlogstream:
    Type: AWS::Logs::LogStream
    Properties:
      LogGroupName: !Sub '/aws/firehose/${DBInstanceId}-auditlog-deliverystream'
      LogStreamName: 'AuditLogDelivery'
      
  AuditSubscriptionFilter:
    Type: AWS::Logs::SubscriptionFilter
    Properties: 
      DestinationArn: !GetAtt 'AuditLogstream.Arn'
      FilterPattern: ''
      LogGroupName: !Sub "/aws/rds/cluster/${DBInstanceId}-cluster/audit"
      RoleArn: !GetAtt 'LogsRole.Arn'

  LogsRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: !Sub 'logs.${AWS::Region}.amazonaws.com'
            Action:
              - sts:AssumeRole
      Path: /
      Policies:
        - PolicyName: root
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - firehose:PutRecord
                  - firehose:PutRecords
                Resource:
                  - !GetAtt 'AuditLogstream.Arn'
                  - !GetAtt 'ErrorLogstream.Arn'
                  - !GetAtt 'GeneralLogstream.Arn'
                  - !GetAtt 'SlowqueryLogstream.Arn'

これでCloudWatch Logsに出力されたログが、S3に出力されるようになります。

まとめ

Amazon Auroraの各種ログ、特に監査ログは、セキュリティ要件によっては、ある程度の期間保存しておくケースがあると思います。 今回のように、CloudWatch Logsには一部の期間のログだけを保存し、S3に長期間分を保存しておくことで、コスト面でのメリットが見出だせます。

最後にテンプレートを貼り付けておきます。ご参考まで。

Auroraログ転送用テンプレート
AWSTemplateFormatVersion: '2010-09-09'
Description: Build Database RDS Aurora template.
Parameters:
  DBInstanceId:
    Description: RDS DB InstanceId.
    Type: String
  DataBaseName:
    Description: RDS database name.
    Type: String
  RDSDbMasterUserName:
    Description: RDS DB Master User Name.
    Type: String
  RDSDbMasterPassword:
    Description: RDS DB Master Password.
    Type: String
    NoEcho: 'TRUE'
  ProtectedSubnet1:
    Description: The SubnetId of Availability Zone 1
    Type: AWS::EC2::Subnet::Id
  ProtectedSubnet2:
    Description: The SubnetId of Availability Zone 2
    Type: AWS::EC2::Subnet::Id
  RDSInstanceClass:
    Type: String
    Description: Database InstanceType
  SecurityGroupId:
    Description: SecurityGroupID attache Aurora.
    Type: AWS::EC2::SecurityGroup::Id
  KmsKeyId:
    Type: String
    Description: KmsKeyID
Resources:
  DBSubnetGroup:
    Type: AWS::RDS::DBSubnetGroup
    Properties:
      DBSubnetGroupDescription: Subnets available for the RDS DB Instance
      DBSubnetGroupName: !Sub "${DBInstanceId}-subnetgroup"
      SubnetIds:
        - !Ref 'ProtectedSubnet1'
        - !Ref 'ProtectedSubnet2'
  RDSCluster:
    Type: AWS::RDS::DBCluster
    Properties:
      BackupRetentionPeriod: 10
      DatabaseName: !Ref 'DataBaseName'
      DBClusterIdentifier: !Sub "${DBInstanceId}-cluster"
      DBClusterParameterGroupName: !Ref 'DBClusterParameterGroup'
      DBSubnetGroupName: !Ref 'DBSubnetGroup'
      Engine: aurora-mysql
      EngineVersion: 5.7.mysql_aurora.2.04.5
      KmsKeyId: !Ref 'KmsKeyId'
      MasterUsername: !Ref 'RDSDbMasterUserName'
      MasterUserPassword: !Ref 'RDSDbMasterPassword'
      PreferredBackupWindow: 18:00-18:30
      PreferredMaintenanceWindow: sun:18:30-sun:19:00
      StorageEncrypted: true
      VpcSecurityGroupIds: 
        - !Ref 'SecurityGroupId'
      EnableCloudwatchLogsExports:
        - general
        - error
        - slowquery
        - audit
  RDSDBInstance1:
    Type: AWS::RDS::DBInstance
    Properties:
      AutoMinorVersionUpgrade: false
      AvailabilityZone: ap-northeast-1a
      DBClusterIdentifier: !Ref 'RDSCluster'
      DBInstanceClass: !Ref 'RDSInstanceClass'
      DBInstanceIdentifier: !Sub "${DBInstanceId}-1"
      DBParameterGroupName: !Ref 'RDSDBParameterGroup'
      DBSubnetGroupName: !Ref 'DBSubnetGroup'
      Engine: aurora-mysql
      OptionGroupName: !Ref 'OptionGroup'
      PreferredMaintenanceWindow: sun:19:00-sun:19:30
      PubliclyAccessible: false
  RDSDBInstance2:
    Type: AWS::RDS::DBInstance
    Properties:
      AutoMinorVersionUpgrade: false
      AvailabilityZone: ap-northeast-1c
      DBClusterIdentifier: !Ref 'RDSCluster'
      DBInstanceClass: !Ref 'RDSInstanceClass'
      DBInstanceIdentifier: !Sub "${DBInstanceId}-2"
      DBParameterGroupName: !Ref 'RDSDBParameterGroup'
      DBSubnetGroupName: !Ref 'DBSubnetGroup'
      Engine: aurora-mysql
      OptionGroupName: !Ref 'OptionGroup'
      PreferredMaintenanceWindow: sun:19:00-sun:19:30
      PubliclyAccessible: false
  DBClusterParameterGroup:
    Type: AWS::RDS::DBClusterParameterGroup
    Properties:
      Description: !Ref 'DBInstanceId'
      Family: aurora-mysql5.7
      Parameters:
        character_set_server: utf8
        character_set_client: utf8
        character_set_connection: utf8
        character_set_results: utf8
        character_set_database: utf8
        time_zone: Asia/Tokyo
        server_audit_logging: 1
        server_audit_events: 'CONNECT,QUERY,TABLE'
  RDSDBParameterGroup:
    Type: AWS::RDS::DBParameterGroup
    Properties:
      Description: !Ref 'DBInstanceId'
      Family: aurora-mysql5.7
      Parameters:
        general_log: '1'
        slow_query_log: '1'
        long_query_time: '1'
  OptionGroup:
    Type: "AWS::RDS::OptionGroup"
    Properties:
      EngineName: "aurora-mysql"
      MajorEngineVersion: "5.7"
      OptionGroupDescription: !Ref 'DBInstanceId'
      Tags:
        - Key: Name
          Value: !Ref 'DBInstanceId'
  RDSauditLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub "/aws/rds/cluster/${DBInstanceId}-cluster/audit"
      RetentionInDays: 90
  RDSerrorLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub "/aws/rds/cluster/${DBInstanceId}-cluster/error"
      RetentionInDays: 90
  RDSgeneralLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub "/aws/rds/cluster/${DBInstanceId}-cluster/general"
      RetentionInDays: 90
  RDSslowqueryLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub "/aws/rds/cluster/${DBInstanceId}-cluster/slowquery"
      RetentionInDays: 90

  S3bucket:
    Type: AWS::S3::Bucket
    DeletionPolicy: Delete
    Properties:
      BucketName: !Sub ${DBInstanceId}-log-${AWS::AccountId}
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      LifecycleConfiguration:
        Rules:
          - Id: AutoDelete
            Status: Enabled
            ExpirationInDays: 365
            Transitions:
              - StorageClass: INTELLIGENT_TIERING
                TransitionInDays: 90
  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:::${DBInstanceId}-log-${AWS::AccountId}'
              - !Sub 'arn:aws:s3:::${DBInstanceId}-log-${AWS::AccountId}*'
      Roles:
        - !Ref 'deliveryRole'

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

  AuditLogstream:
    Type: AWS::KinesisFirehose::DeliveryStream
    DependsOn: S3bucket
    Properties:
      ExtendedS3DestinationConfiguration:
        BucketARN: !Sub 'arn:aws:s3:::${DBInstanceId}-log-${AWS::AccountId}'
        BufferingHints:
          IntervalInSeconds: '60'
          SizeInMBs: '50'
        CompressionFormat: GZIP
        Prefix: audit/
        RoleARN: !GetAtt 'deliveryRole.Arn'
        ProcessingConfiguration:
          Enabled: 'false'
        CloudWatchLoggingOptions:
          Enabled: 'true'
          LogGroupName: !Sub '/aws/firehose/${DBInstanceId}-auditlog-deliverystream'
          LogStreamName: 'AuditLogDelivery'
          
  AuditLogstreamlogstream:
    Type: AWS::Logs::LogStream
    Properties:
      LogGroupName: !Sub '/aws/firehose/${DBInstanceId}-auditlog-deliverystream'
      LogStreamName: 'AuditLogDelivery'

  AuditSubscriptionFilter:
    Type: AWS::Logs::SubscriptionFilter
    Properties: 
      DestinationArn: !GetAtt 'AuditLogstream.Arn'
      FilterPattern: ''
      LogGroupName: !Sub "/aws/rds/cluster/${DBInstanceId}-cluster/audit"
      RoleArn: !GetAtt 'LogsRole.Arn'

  ErrorLogstream:
    Type: AWS::KinesisFirehose::DeliveryStream
    DependsOn: S3bucket
    Properties:
      ExtendedS3DestinationConfiguration:
        BucketARN: !Sub 'arn:aws:s3:::${DBInstanceId}-log-${AWS::AccountId}'
        BufferingHints:
          IntervalInSeconds: '60'
          SizeInMBs: '50'
        CompressionFormat: GZIP
        Prefix: error/
        RoleARN: !GetAtt 'deliveryRole.Arn'
        ProcessingConfiguration:
          Enabled: 'false'
        CloudWatchLoggingOptions:
          Enabled: 'true'
          LogGroupName: !Sub '/aws/firehose/${DBInstanceId}-errorlog-deliverystream'
          LogStreamName: 'ErrorLogDelivery'

  ErrorLogstreamlogstream:
    Type: AWS::Logs::LogStream
    Properties:
      LogGroupName: !Sub '/aws/firehose/${DBInstanceId}-errorlog-deliverystream'
      LogStreamName: 'ErrorLogDelivery'

  ErrorSubscriptionFilter:
    Type: AWS::Logs::SubscriptionFilter
    Properties: 
      DestinationArn: !GetAtt 'ErrorLogstream.Arn'
      FilterPattern: ''
      LogGroupName: !Sub "/aws/rds/cluster/${DBInstanceId}-cluster/error"
      RoleArn: !GetAtt 'LogsRole.Arn'

  GeneralLogstream:
    Type: AWS::KinesisFirehose::DeliveryStream
    DependsOn: S3bucket
    Properties:
      ExtendedS3DestinationConfiguration:
        BucketARN: !Sub 'arn:aws:s3:::${DBInstanceId}-log-${AWS::AccountId}'
        BufferingHints:
          IntervalInSeconds: '60'
          SizeInMBs: '50'
        CompressionFormat: GZIP
        Prefix: general/
        RoleARN: !GetAtt 'deliveryRole.Arn'
        ProcessingConfiguration:
          Enabled: 'false'
        CloudWatchLoggingOptions:
          Enabled: 'true'
          LogGroupName: !Sub '/aws/firehose/${DBInstanceId}-generallog-deliverystream'
          LogStreamName: 'GeneralLogDelivery'

  GeneralLogstreamlogstream:
    Type: AWS::Logs::LogStream
    Properties:
      LogGroupName: !Sub '/aws/firehose/${DBInstanceId}-generallog-deliverystream'
      LogStreamName: 'GeneralLogDelivery'

  GeneralSubscriptionFilter:
    Type: AWS::Logs::SubscriptionFilter
    Properties: 
      DestinationArn: !GetAtt 'GeneralLogstream.Arn'
      FilterPattern: ''
      LogGroupName: !Sub "/aws/rds/cluster/${DBInstanceId}-cluster/general"
      RoleArn: !GetAtt 'LogsRole.Arn'

  SlowqueryLogstream:
    Type: AWS::KinesisFirehose::DeliveryStream
    DependsOn: S3bucket
    Properties:
      ExtendedS3DestinationConfiguration:
        BucketARN: !Sub 'arn:aws:s3:::${DBInstanceId}-log-${AWS::AccountId}'
        BufferingHints:
          IntervalInSeconds: '60'
          SizeInMBs: '50'
        CompressionFormat: GZIP
        Prefix: slowquery/
        RoleARN: !GetAtt 'deliveryRole.Arn'
        ProcessingConfiguration:
          Enabled: 'false'
        CloudWatchLoggingOptions:
          Enabled: 'true'
          LogGroupName: !Sub '/aws/firehose/${DBInstanceId}-slowquerylog-deliverystream'
          LogStreamName: 'SlowqueryLogDelivery'

  SlowqueryLogstreamlogstream:
    Type: AWS::Logs::LogStream
    Properties:
      LogGroupName: !Sub '/aws/firehose/${DBInstanceId}-slowquerylog-deliverystream'
      LogStreamName: 'SlowqueryLogDelivery'

  SlowquerySubscriptionFilter:
    Type: AWS::Logs::SubscriptionFilter
    Properties: 
      DestinationArn: !GetAtt 'SlowqueryLogstream.Arn'
      FilterPattern: ''
      LogGroupName: !Sub "/aws/rds/cluster/${DBInstanceId}-cluster/slowquery"
      RoleArn: !GetAtt 'LogsRole.Arn'

  LogsRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: !Sub 'logs.${AWS::Region}.amazonaws.com'
            Action:
              - sts:AssumeRole
      Path: /
      Policies:
        - PolicyName: root
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - firehose:PutRecord
                  - firehose:PutRecords
                Resource:
                  - !GetAtt 'AuditLogstream.Arn'
                  - !GetAtt 'ErrorLogstream.Arn'
                  - !GetAtt 'GeneralLogstream.Arn'
                  - !GetAtt 'SlowqueryLogstream.Arn'

## 参考 Amazon Aurora MySQL DB クラスターでの高度な監査の使用