この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
ECSのタスク(コンテナ)のログ出力先を変更できるFireLens機能があります。本記事ではFluent Bitを利用したカスタムログルーティングでCloudWatch Logsへアプリケーションコンテナのログを送ります。CloudWatch Logsへ送信するための最小権限と、送信先のCloudWatch Logsのロググループ名を限定したIAMポリシーを作成します。動作検証しましたのでまとめます。
-
Icons made by Freepik from www.flaticon.com
FireLensイメージの準備
Fluent Bitでカスタムログルーティングするための設定ファイルを作成します。設定ファイルを同梱したFireLensイメージを作成しECRにプッシュします。
- すべてのログをCloudWatch Logsへ送る
- ロググループ名は
/ecs/logs/sample-test
- ここで指定したロググループ名は後で必要になります。
- ログストリーム名は
webapp
- ロググループが存在していない場合は新規作成する
extra.conf
[SERVICE]
Flush 1
Grace 30
Log_Level info
[OUTPUT]
Name cloudwatch_logs
Match *
region ap-northeast-1
log_group_name /ecs/logs/sample-test
log_stream_name webapp
auto_create_group true
完成イメージ
ベースのイメージはAmazonが配布しているAWS用のプラグインが同梱されたFluent Bitのイメージを利用します。カスタムログルーティング用の設定ファイルをイメージ内にコピーします。
Dockerfile
FROM amazon/aws-for-fluent-bit:2.18.0
COPY ./extra.conf /fluent-bit/etc/extra.conf
設定ファイル込みのイメージをECRへプッシュして準備完了です。
docker build -t sample-test-custom-firelens:v1 .
docker tag sample-test-custom-firelens:v1 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/sample-test-custom-firelens:v1
docker push 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/sample-test-custom-firelens:v1
CloudFormation
テンプレートを実行すると以下のFargateでの動作確認環境を構築できます。VPCは事前に用意してください。
-
Icons made by Freepik from www.flaticon.com
テンプレート
折りたたみ
AWSTemplateFormatVersion: "2010-09-09"
Description: Create Fargate*1, Firelens*1
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
- Label:
default: Common Settings
Parameters:
- ProjectName
- Environment
- Label:
default: ECS VPC Settings
Parameters:
- VPCID
- PublicSubnet1
- PublicSubnet2
- PrivateSubnet1
- PrivateSubnet2
Parameters:
ProjectName:
Description: Project Name
Type: String
Default: unnamed
Environment:
Description: Environment
Type: String
Default: dev
AllowedValues:
- prod
- dev
- stg
- test
VPCID:
Type: AWS::EC2::VPC::Id
PublicSubnet1:
Description: "Web App Subnet 1st"
Type: AWS::EC2::Subnet::Id
PublicSubnet2:
Description: "Web App Subnet 2nd"
Type: AWS::EC2::Subnet::Id
PrivateSubnet1:
Description: "ECS Subnet 1st"
Type: AWS::EC2::Subnet::Id
PrivateSubnet2:
Description: "ECS Subnet 2nd"
Type: AWS::EC2::Subnet::Id
DesiredCount:
Type: Number
Default: 1
ClusterName:
Type: String
Default: cluster
AppName:
Type: String
Default: webapp
ServiceName:
Type: String
Default: service
TaskDefinitionName:
Type: String
Default: taskdefinition
ImageNameWebApp:
Description: "Web Application Repository Name also Need to TagName"
Type: String
Default: "public.ecr.aws/nginx/nginx:latest"
ImageNameFirelens:
Description: "Firelens Repository Name also Need to TagName"
Type: String
Default: "public.ecr.aws/aws-observability/aws-for-fluent-bit:latest"
Resources:
# --------------------------------------------
# ELB
# --------------------------------------------
ELB1:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Type: "application"
Name: !Sub ${ProjectName}-${Environment}-elb
Scheme: "internet-facing"
SecurityGroups:
- !Ref SecurityGroup2
Subnets:
- !Ref PublicSubnet1
- !Ref PublicSubnet2
IpAddressType: "ipv4"
Tags:
- Key: Name
Value: !Sub ${ProjectName}-${Environment}-elb
ELBListener1:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
DefaultActions:
- TargetGroupArn: !Ref TargetGroup1
Type: "forward"
LoadBalancerArn: !Ref ELB1
Port: 80
Protocol: "HTTP"
TargetGroup1:
Type: "AWS::ElasticLoadBalancingV2::TargetGroup"
Properties:
VpcId: !Ref VPCID
Name: !Sub ${ProjectName}-${Environment}-tg
Protocol: "HTTP"
HealthCheckPath: "/"
Port: 80
TargetType: ip
HealthCheckIntervalSeconds: 10 # Default is 30.
HealthyThresholdCount: 2 # Default is 5.
HealthCheckTimeoutSeconds: 5
UnhealthyThresholdCount: 2
TargetGroupAttributes:
- Key: "stickiness.enabled"
Value: "false"
- Key: deregistration_delay.timeout_seconds
Value: "60" # default is 300.
- Key: "stickiness.type"
Value: "lb_cookie"
- Key: "stickiness.lb_cookie.duration_seconds"
Value: "86400"
- Key: "slow_start.duration_seconds"
Value: "0"
- Key: "load_balancing.algorithm.type"
Value: "round_robin"
# --------------------------------------------
# CloudWatch Logs Group
# --------------------------------------------
# FireLens Stdout
FireLensLogGroup:
Type: "AWS::Logs::LogGroup"
Properties:
LogGroupName: !Sub "/ecs/logs/${ProjectName}-${Environment}-firelens"
# --------------------------------------------
# ECS Fargate
# --------------------------------------------
# Cluster
ECSCluster:
Type: "AWS::ECS::Cluster"
Properties:
ClusterName: !Sub "${ProjectName}-${Environment}-${ClusterName}"
CapacityProviders:
- "FARGATE_SPOT"
- "FARGATE"
# Service
ECSService:
Type: "AWS::ECS::Service"
Properties:
ServiceName: !Sub ${ProjectName}-${Environment}-${ServiceName}
Cluster: !Ref ECSCluster
LaunchType: "FARGATE"
PlatformVersion: "1.4.0"
DesiredCount: !Ref DesiredCount
LoadBalancers:
- TargetGroupArn: !Ref TargetGroup1
ContainerName: !Ref AppName
ContainerPort: 80
NetworkConfiguration:
AwsvpcConfiguration:
AssignPublicIp: "DISABLED"
SecurityGroups:
- !Ref SecurityGroup1
Subnets:
- !Ref PrivateSubnet1
- !Ref PrivateSubnet2
TaskDefinition: !Ref ECSTaskDefinition
DependsOn: ELBListener1
# ECS TaskDefinition
ECSTaskDefinition:
Type: "AWS::ECS::TaskDefinition"
Properties:
Family: !Sub "${ProjectName}-${Environment}-${AppName}-${TaskDefinitionName}"
TaskRoleArn: !GetAtt ECSTaskRole1.Arn
ExecutionRoleArn: !GetAtt ECSTaskExecutionRole1.Arn
NetworkMode: "awsvpc"
RequiresCompatibilities:
- "FARGATE"
Cpu: "256"
Memory: "512"
ContainerDefinitions:
- Essential: true
Name: !Ref AppName
Image: !Ref ImageNameWebApp
LogConfiguration:
LogDriver: "awsfirelens"
PortMappings:
- ContainerPort: 80
HostPort: 80
Protocol: "tcp"
- Essential: true
Name: "log_router"
Image: !Ref ImageNameFirelens
LogConfiguration:
LogDriver: "awslogs"
Options:
awslogs-group: !Ref FireLensLogGroup
awslogs-region: !Ref AWS::Region
awslogs-stream-prefix: "firelens"
FirelensConfiguration:
Type: "fluentbit"
Options:
config-file-type: "file"
config-file-value: "/fluent-bit/etc/extra.conf"
User: "0"
# --------------------------------------------
# Security Group
# --------------------------------------------
# Security Group for WebApp
SecurityGroup1:
Type: AWS::EC2::SecurityGroup
Properties:
GroupName: !Sub ${ProjectName}-${Environment}-${AppName}-sg
GroupDescription: Web App Security Group
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
SourceSecurityGroupId: !Ref SecurityGroup2
Description: "Access from ELB"
VpcId: !Ref VPCID
Tags:
- Key: Name
Value: !Sub ${ProjectName}-${Environment}-${AppName}-sg
# Security Group for ELB
SecurityGroup2:
Type: AWS::EC2::SecurityGroup
Properties:
GroupName: !Sub ${ProjectName}-${Environment}-elb-sg
GroupDescription: ELB Security Group
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: "0.0.0.0/0"
Description: "Access from Public"
VpcId: !Ref VPCID
Tags:
- Key: Name
Value: !Sub ${ProjectName}-${Environment}-elb-sg
# --------------------------------------------
# IAM Role
# --------------------------------------------
# ECS Task Execution Role
ECSTaskExecutionRole1:
Type: "AWS::IAM::Role"
Properties:
RoleName: !Sub ${ProjectName}-${Environment}-${AppName}-ECSTaskExecutionRole
Path: "/"
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service: ecs-tasks.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
# ECS Task Role
ECSTaskRole1:
Type: "AWS::IAM::Role"
Properties:
Path: "/"
RoleName: !Sub ${ProjectName}-${Environment}-${AppName}-ECSTaskRole
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service: ecs-tasks.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- !Ref ECSExecPolicy
- !Ref SentCloudWatchLogsPolicy
# --------------------------------------------
# IAM Policy
# --------------------------------------------
# Allowed ECS Exec for Task Role
ECSExecPolicy:
Type: "AWS::IAM::ManagedPolicy"
Properties:
ManagedPolicyName: !Sub "${ProjectName}-${Environment}-ECSExecPolicy"
Path: "/"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- ssmmessages:CreateControlChannel
- ssmmessages:CreateDataChannel
- ssmmessages:OpenControlChannel
- ssmmessages:OpenDataChannel
Resource: "*"
# Sent CloudWatch Logs for Task Role
SentCloudWatchLogsPolicy:
Type: "AWS::IAM::ManagedPolicy"
Properties:
ManagedPolicyName: !Sub "${ProjectName}-${Environment}-SentCloudWatchLogsPolicy"
Path: "/"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- logs:CreateLogStream
- logs:CreateLogGroup
- logs:DescribeLogStreams
- logs:PutLogEvents
Resource: !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/ecs/logs/sample-test:*
検証環境
項目 | バージョン |
---|---|
aws-for-fluent-bit | 2.18.0 |
Fluent Bit | 1.8.2 |
Fargate platform | 1.4.0 |
ポイント
- ELB経由でNginxコンテナにアクセスすることで、アクセスログがFireLensコンテナ経由でCloudWatch Logsへ保存
- CloudWatch Logsへログを送るために必要なタスクロールの権限をできる限り制限
- FireLensのイメージは事前にカスタムログルーティングの設定ファイルを同梱して準備が必要
- Nginxコンテナは素のイメージをそのまま使用するので準備不要
- セキュリティグループが0.0.0.0/0で解放されています、必要に応じてアクセス元を制限してください
FireLensイメージのパラメータ指定
FireLensのイメージ名はECRへプッシュした設定ファイルが同梱したイメージを指定してください。デフォルト値は素のFireLens(Fluent Bit)のイメージになっています。
FilreLensの設定
FireLensのログ設定周りを見ていきます。
アプリコンテナ
- LogCongurationのLogDriverの指定は
awsfirelens
Name: !Ref AppName
Image: !Ref ImageNameWebApp
LogConfiguration:
LogDriver: "awsfirelens"
PortMappings:
- ContainerPort: 80
HostPort: 80
Protocol: "tcp"
FireLensコンテナ
- LogConfigurationはFireLensコンテナの標準出力内容をデバッグ用にCloudWatch Logsへ送ります。
- FirelensConfigurationにはカスタム設定ファイルをDockerイメージ内にコピー先のパスを指定します。
Name: "log_router"
Image: !Ref ImageNameFirelens
LogConfiguration:
LogDriver: "awslogs"
Options:
awslogs-group: !Ref FireLensLogGroup
awslogs-region: !Ref AWS::Region
awslogs-stream-prefix: "firelens"
FirelensConfiguration:
Type: "fluentbit"
Options:
config-file-type: "file"
config-file-value: "/fluent-bit/etc/extra.conf
タスクロール用のIAMポリシー設定
FireLensからCloudWatch Logsへログを送るためにはタスクロールに権限が必要です。Actionは最低限必要な権限に絞り、Resourceで送信先のロググループも限定します。
Resource: !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/ecs/logs/sample-test:*
の/ecs/logs/sample-test
が注意するところです。ここはロググループ名を指しており、FireLensからの送信先ロググループ名はFluent Bitの設定ファイル(extra.conf)でロググループ名を決めています。
# Sent CloudWatch Logs for Task Role
SentCloudWatchLogsPolicy:
Type: "AWS::IAM::ManagedPolicy"
Properties:
ManagedPolicyName: !Sub "${ProjectName}-${Environment}-SentCloudWatchLogsPolicy"
Path: "/"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- logs:CreateLogStream
- logs:CreateLogGroup
- logs:DescribeLogStreams
- logs:PutLogEvents
Resource: !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/ecs/logs/sample-test:*
FireLensイメージ内に同梱した設定ファイルを再掲します。IAMポリシーでResourceも制限するときはlog_group_name
で指定した名前を確認しましょう。
extra.conf
[SERVICE]
Flush 1
Grace 30
Log_Level info
[OUTPUT]
Name cloudwatch_logs
Match *
region ap-northeast-1
log_group_name /ecs/logs/sample-test
log_stream_name webapp
auto_create_group true
Resourceは制限しないで、Actionだけを純粋にみると以下のIAMポリシーになります。一部Createが必要になるのはFluent Bitの設定ファイルでauto_create_group
がtrueだと、指定したロググループと、ログストリームを自動的に新規作成してくれるためです。
CloudWatch Logs用のIAMポリシー
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": [
"logs:CreateLogStream",
"logs:CreateLogGroup",
"logs:DescribeLogStreams",
"logs:PutLogEvents"
],
"Resource": "*"
}]
}
完成イメージ
テンプレートを実行をすると以下のIAMポリシーが生成されます。
Resource制限付きCloudWatch LogsのIAMポリシー
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": [
"logs:CreateLogStream",
"logs:CreateLogGroup",
"logs:DescribeLogStreams",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:ap-northeast-1:123456789012:log-group:<loggroup-name>:*"
}]
}
動作確認
ELBのDNS名にアクセスするとNginxのページが返ってきます。
Nginxのログ確認
アプリケーションコンテナのログはFluent Bitの設定ファイルで指定したロググループにログが保存される予定です。今回の検証環境では/etc/logs/sample-test/webapp
にログが保存されます。
ELBのヘルスチェックのログを確認できます。検証の目的であったタスクロールのIAMポリシー設定に問題ないことを確認できました。
タスクロールの権限を間違った場合
CloudWatch Logsへ送信用のActionsとResource指定に問題ないことを確認できました。仮にResourceの指定を誤っていた場合、切り分けするにはどこを確認したらよいのか確認しておきます。
Resourceで指定したCloudWatch Logsのロググループ名を適当な名前に変更しました。先ほど送信していたロググループ名へアクセスできない状態にしました。
FireLens経由で送信されるCloudWatch Logsのロググループ(/etc/logs/sample-test/webapp
)のログが止まりました。時刻はIAMポリシーを変更して間もない22:00:12で止まっています。ログが止まったということはわかりました。なにが原因で止まったのかはアプリケーションコンテナのログからでは判斷する材料がありません。
FireLensの標準出力結果を送信しているCloudWatch Logsのロググループ(/etc/logs/sample-test-firelens
)を確認します。
アプリケーションコンテナのログが止まった直後の22:00:13にAccessDeniedExceptionのメッセージ確認できました。アクセス権がないということから、タスクロールのIAMポリシーなど権限周りを疑うことができます。
FireLensコンテナのログ設定はテンプレートの以下の箇所で指定しています。明示的にCloudWatch Logsのロググループをテンプレート内で作成しています。
# FireLens Stdout
FireLensLogGroup:
Type: "AWS::Logs::LogGroup"
Properties:
LogGroupName: !Sub "/ecs/logs/${ProjectName}-${Environment}-firelens"
...snip...
Name: "log_router"
Image: !Ref ImageNameFirelens
LogConfiguration:
LogDriver: "awslogs"
Options:
awslogs-group: !Ref FireLensLogGroup
awslogs-region: !Ref AWS::Region
awslogs-stream-prefix: "firelens"
FirelensConfiguration:
Type: "fluentbit"
Options:
config-file-type: "file"
config-file-value: "/fluent-bit/etc/extra.conf
アプリケーションコンテナのログ送信先のロググループ名はCloudFormationのテンプレートからは作成していません。Fluent Bitの設定ファイルで対象のロググループ名がない場合は新規作成できるため、Fluent Bitの設定ファイルに寄せています。
おわりに
IAMポリシーのActionはよいとして、Resourceの指定はFluent Bitから作成されるリソース(CloudWatch Logsのロググループ)をCloudFormationのテンプレートで作成するIAMポリシーを指定しています。テンプレートを一発実行して何か問題にならないのか気になったので検証環境を作成しました。どなたかの参考になれば幸いです。