はじめに
CloudFormationでFn::ForEachを使ったループ処理が2023年6月から利用出来るようになり、革命の予感を感じたので色々試行錯誤してみました。
結果から言うと、求めてるレベルの利便性は得られなかったですが、今までよりは確実に少しの作業で複数のログ転送設定ができるようになりました。
試行錯誤した内容については最後に記載しています。
ループ機能で CloudFormation のオーサリング体験を加速させましょう
余談ですが、最近Kinesis Data Firehose → Data Firehoseへ改名があったので、
本ブログではData Firehoseといたします。
Amazon Data Firehose (旧 Amazon Kinesis Data Firehose) のご紹介
CloudWatch Logs → S3について
前提としてCloudWatch LogsからS3にログを継続的に転送するためにはData Firehoseを間に挟む必要があります。
その際、基本的には1つのロググループに対して1つのData Firehoseが必要となります。
大量のロググループがある場合、サブスクリプションフィルターとData Firehoseの設定作業は無限に続きます。
ということで今回のループ処理と相性が良さそうだと思い、ループ処理で作ってみました。
登場サービス
CloudFormationで作成されるもの
- ロググループ
- サブスクリプションフィルター
- Data Firehose
- IAMポリシー/IAMロール(ロググループからData Firehoseに出力する)
- IAMポリシー/IAMロール(Data FirehoseからS3に出力する)
事前に用意するもの
- ログ出力先のS3
CloudFormation
001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042 043 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 062 063 064 065 066 067 068 069 070 071 072 073 074 075 076 077 078 079 080 081 082 083 084 085 086 087 088 089 090 091 092 093 094 095 096 097 098 099 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 | AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::LanguageExtensions Description: 'Kinesis Data Firehose and CloudWatch Subscription Filters' Parameters: CwogsPolicyName: Type: String Default: "cwlogs-policy" CwogsRoleName: Type: String Default: "cwlogs-role" KdfPolicyName: Type: String Default: "kdf-policy" KdfRoleName: Type: String Default: "kdf-role" S3BucketArn: Type: String CreateLogGroup: #ロググループを作成するかどうか Type: String Description: CreateLogGroup AllowedValues: [ 'yes' , 'no' ] Default: 'yes' LogsParameters: #mapを呼び出すためにループで回すリスト Type: CommaDelimitedList Description: Edit with yaml Default: LogPara1, LogPara2, LogPara3 Mappings: # ループコンテンツのパラメータ LogsMappings: # firehoseでは「/」などの特殊文字は使えないのでlognameとfirehosenameは別々で定義 LogPara1: LogName: "/aws/ec2/var/log/messages" firehoseName: "kinesis-ec2-messages-delivery-stream" LogPara2: LogName: "/aws/ec2/var/log/audit/audit.log" firehoseName: "kinesis-ec2-audit-delivery-stream" LogPara3: LogName: "/aws/ec2/var/log/secure" firehoseName: "kinesis-ec2-secure-delivery-stream" Conditions: ShouldCreateLogGroup: !Equals [!Ref CreateLogGroup, 'yes' ] Resources: # CloudWatch Logs To Kinesis Data Firehose Policy CWLogsPolicy: Type: AWS::IAM::ManagedPolicy Properties: ManagedPolicyName: !Sub ${CwogsPolicyName} PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - 'firehose:*' Resource: - !Sub "arn:aws:firehose:${AWS::Region}:${AWS::AccountId}:*" - Effect: Allow Action: - 'iam:PassRole' Resource: - !Sub "arn:aws:iam::${AWS::AccountId}:role/${CwogsRoleName}" # CloudWatch Logs To Kinesis Data Firehose Role CWLogsRole: Type: AWS::IAM::Role Properties: RoleName: !Sub ${CwogsRoleName} AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - !Sub "logs.${AWS::Region}.amazonaws.com" Action: - 'sts:AssumeRole' Path: / ManagedPolicyArns: - !Ref CWLogsPolicy # Kinesis Data Firehose To S3 Policy KdfPolicy: Type: AWS::IAM::ManagedPolicy Properties: ManagedPolicyName: !Sub ${KdfPolicyName} PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: 'glue:GetTableVersions' Resource: "*" - Effect: Allow Action: - 's3:PutObject' - 's3:PutObjectAcl' - 's3:GetObject' - 's3:ListBucketMultipartUploads' - 's3:AbortMultipartUpload' - 's3:ListBucket' - 's3:GetBucketLocation' Resource: - !Sub ${S3BucketArn} - !Sub ${S3BucketArn}/* # Kinesis Data Firehose To S3 Role KdfRole: Type: AWS::IAM::Role Properties: RoleName: !Sub ${KdfRoleName} AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - firehose.amazonaws.com Action: 'sts:AssumeRole' Condition: StringEquals: sts:ExternalId: !Sub "${AWS::AccountId}" Path: / ManagedPolicyArns: - !Ref KdfPolicy # 以下ループ 'Fn::ForEach::LogGroupLoop' : - LogParameter - !Ref LogsParameters - '${LogParameter}' : #ロググループ作成 Type: AWS::Logs::LogGroup Condition: ShouldCreateLogGroup Properties: LogGroupName: !FindInMap [LogsMappings, !Ref LogParameter, LogName] RetentionInDays: 1 # KinesisFirehose 'Kdf${LogParameter}' : Type: AWS::KinesisFirehose::DeliveryStream Properties: DeliveryStreamName: !FindInMap [LogsMappings, !Ref LogParameter, firehoseName] DeliveryStreamType: DirectPut S3DestinationConfiguration: BucketARN: !Sub "${S3BucketArn}" Prefix: !Join [ "" , [ "cw_logs/" , !FindInMap [LogsMappings, !Ref LogParameter, LogName], "/" ] ] CloudWatchLoggingOptions: Enabled: false RoleARN: !GetAtt KdfRole.Arn # SubscriptionFilter 'SubscriptionFilter${LogParameter}' : Type: AWS::Logs::SubscriptionFilter Properties: LogGroupName: !FindInMap [LogsMappings, !Ref LogParameter, LogName] FilterName: ALL DestinationArn: !GetAtt - !Sub 'Kdf${LogParameter}' - Arn RoleArn: !GetAtt CWLogsRole.Arn Distribution: ByLogStream FilterPattern: "" |
S3のarnとログ転送したいロググループ名、firehose名を指定すれば利用可能な内容としています。
また、必要に応じてLogsParameters、LogsMappingsを変更すれば何件分でも対応可能です。
IAM部分は1つあれば十分なので、ループ処理からは除外。
ループ処理を行う際、渡す値はmapと組み合わせるのが現状一番良い気がします。
なぜわざわざここでリストとMapを使っているかというと、今回で一番苦労した点で以下の制限があるためです。
論理 ID は英数字(A-Za-z0-9)とし、テンプレート内で一意である必要があります。
対してロググループには「_」,「-」,「/」などの記号が入っていることが一般的であり、ループ処理内で変数を違う値を差し込む必要があります。
この条件から与える値を「論理IDで使用する変数」と「指定するロググループの文字列」を分ける必要がありました。
ちなみに今回は値が少ないのでmapの恩恵は少ないですが、他で利用する際まとめて複数の値を渡すことができるので応用が効きやすい利点があります。
ループのコード
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | # 以下ループ 'Fn::ForEach::LogGroupLoop' : - LogParameter - !Ref LogsParameters - '${LogParameter}' : #ロググループ作成 Type: AWS::Logs::LogGroup Condition: ShouldCreateLogGroup Properties: LogGroupName: !FindInMap [LogsMappings, !Ref LogParameter, LogName] RetentionInDays: 1 # KinesisFirehose 'Kdf${LogParameter}' : Type: AWS::KinesisFirehose::DeliveryStream Properties: DeliveryStreamName: !FindInMap [LogsMappings, !Ref LogParameter, firehoseName] DeliveryStreamType: DirectPut S3DestinationConfiguration: BucketARN: !Sub "${S3BucketArn}" Prefix: !Join [ "" , [ "cw_logs/" , !FindInMap [LogsMappings, !Ref LogParameter, LogName], "/" ] ] CloudWatchLoggingOptions: Enabled: false RoleARN: !GetAtt KdfRole.Arn # SubscriptionFilter 'SubscriptionFilter${LogParameter}' : Type: AWS::Logs::SubscriptionFilter Properties: LogGroupName: !FindInMap [LogsMappings, !Ref LogParameter, LogName] FilterName: ALL DestinationArn: !GetAtt - !Sub 'Kdf${LogParameter}' - Arn RoleArn: !GetAtt CWLogsRole.Arn Distribution: ByLogStream FilterPattern: "" |
今回のメインはこちらの処理。
コード自体はシンプルで、ロググループ,Data Firehose,サブスクリプションフィルターを作る処理をループ化しています。
LogsParametersの数だけループし、LogsParametersの値を参照して、LogsMappingsの値を呼び出すようにしました。
ロググループ作成分岐
CreateLogGroupの値を使って分岐を作っています。
- Yes:新規でロググループごと作成する場合
- No:既存ロググループにログ転送処理を作成する場合
試してみたけどだめだったこと
- Length関数の利用
一般的なプログラム言語のようにリストの配列数を取得して、それを論理IDの変数として回せないか。
そもそもCloudFormationでカウントを進めるような記述ができないからボツ。
- ロググループの作成分岐
指定したロググループ作成する際、既に存在している場合は作成しないように自動的に分岐させたかったのですが、ループ内の処理に組み込むと結構厄介なことになったので選択式としました。
こちらは検証しきれてないので、なにかしら手段はあるかもしれません。