はじめに
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
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の恩恵は少ないですが、他で利用する際まとめて複数の値を渡すことができるので応用が効きやすい利点があります。
ループのコード
# 以下ループ
'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でカウントを進めるような記述ができないからボツ。
- ロググループの作成分岐
指定したロググループ作成する際、既に存在している場合は作成しないように自動的に分岐させたかったのですが、ループ内の処理に組み込むと結構厄介なことになったので選択式としました。
こちらは検証しきれてないので、なにかしら手段はあるかもしれません。