FireLensを使用してECS FargateでホストしているアプリケーションのログをS3とCloudWatch Logsロググループに出力してみた

FireLensを使用してECS FargateでホストしているアプリケーションのログをS3とCloudWatch Logsロググループに出力してみた

2025.10.01

ECS Fargateでホストしているアプリケーションログをエラーログとそれ以外のログでS3とCloudWatch Logsに振り分ける設定をFireLensで行ってみました。

構成

今回作成するAWSの構成は以下のようになります。
Firelens構成図

通常のECSクラスターとログの出力先としてS3を作成します。
また、AWS for Fluent Bitの設定ファイルの置き場所用のS3も作成します。
Fargateの場合、設定ファイルの置き場所としてS3を使用できないのですが、こちらのブログで紹介されているようにAWS for Fluent Bitのイメージタグに"init-"がついているものを使用すればS3内に配置した設定ファイルを参照させることができます。
https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/firelens-taskdef.html

AWS Fargate でホストされるタスクは、file 設定ファイルタイプのみをサポートします。

リソース作成

今回使用するアプリケーションはFlaskを使用します。
Flaskはuwsgiを使用してNginxを経由して配信するためWebサーバー用のコンテナイメージも作成します。
サンプルのアプリケーションは以下のGitHubリポジトリに置いてあります。
https://github.com/Kobayashi-Riku0226/Flask

事前準備

事前準備としてアプリケーション用とNginx用のコンテナイメージを作成します。
以下のCloudFormationテンプレートを使用してECRを作成してください。

			
			AWSTemplateFormatVersion: "2010-09-09"
Description: ECR

Metadata:
# ------------------------------------------------------------#
# Metadata
# ------------------------------------------------------------# 
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label: 
          default: Parameters for env Name
        Parameters:
          - env

Parameters:
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------# 
  env:
    Type: String
    Default: dev
    AllowedValues:
      - dev

Resources:
# ------------------------------------------------------------#
# ECR
# ------------------------------------------------------------# 
  ECRApp:
    Type: AWS::ECR::Repository
    Properties:
      EmptyOnDelete: true
      EncryptionConfiguration:
        EncryptionType: AES256
      RepositoryName: !Sub ecr-app-${env}

  ECRWeb:
    Type: AWS::ECR::Repository
    Properties:
      EmptyOnDelete: true
      EncryptionConfiguration:
        EncryptionType: AES256
      RepositoryName: !Sub ecr-web-${env}

		

ECRの作成後、CloudShellから以下のコマンドを実行してコンテナイメージを作成してください。

			
			git clone https://github.com/Kobayashi-Riku0226/Flask.git
cd Flask/
cd app/
aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin AWSアカウントID.dkr.ecr.ap-northeast-1.amazonaws.com
docker build -t ecr-app-dev .
docker tag ecr-app-dev:latest AWSアカウントID.dkr.ecr.ap-northeast-1.amazonaws.com/ecr-app-dev:latest
docker push AWSアカウントID.dkr.ecr.ap-northeast-1.amazonaws.com/ecr-app-dev:latest

cd ../web/
docker build -t ecr-web-dev .
docker tag ecr-web-dev:latest AWSアカウントID.dkr.ecr.ap-northeast-1.amazonaws.com/ecr-web-dev:latest
docker push AWSアカウントID.dkr.ecr.ap-northeast-1.amazonaws.com/ecr-web-dev:latest

		

CloudWatch Logsに全てのログを出力

まずは全てのログをCloudWatch Logsに出力する設定を行ってみます。
以下のCloudFormationテンプレートを使用してリソースを作成します。

			
			AWSTemplateFormatVersion: "2010-09-09"
Description: ECS

Metadata:
# ------------------------------------------------------------#
# Metadata
# ------------------------------------------------------------# 
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label: 
          default: Parameters for env Name
        Parameters:
          - env
      - Label:
          default: Parameters for Network
        Parameters:
          - VPCCIDR
          - PublicSubnet01CIDR
          - PublicSubnet02CIDR
          - PrivateSubnet01CIDR
          - PrivateSubnet02CIDR

Parameters:
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------# 
  env:
    Type: String
    Default: dev
    AllowedValues:
      - dev

  VPCCIDR:
    Default: 192.168.0.0/16
    Type: String

  PublicSubnet01CIDR:
    Default: 192.168.0.0/24
    Type: String

  PublicSubnet02CIDR:
    Default: 192.168.1.0/24
    Type: String

  PrivateSubnet01CIDR:
    Default: 192.168.2.0/24
    Type: String

  PrivateSubnet02CIDR:
    Default: 192.168.3.0/24
    Type: String

Resources:
# ------------------------------------------------------------#
# S3
# ------------------------------------------------------------# 
  S3LogBucket:
    Type: AWS::S3::Bucket
    Properties: 
      BucketEncryption: 
        ServerSideEncryptionConfiguration: 
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      BucketName: !Sub s3-log-${AWS::StackName}-${AWS::AccountId}
      PublicAccessBlockConfiguration:
        BlockPublicAcls: True
        BlockPublicPolicy: True
        IgnorePublicAcls: True
        RestrictPublicBuckets: True

  S3ConfBucket:
    Type: AWS::S3::Bucket
    Properties: 
      BucketEncryption: 
        ServerSideEncryptionConfiguration: 
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      BucketName: !Sub s3-conf-${AWS::StackName}-${AWS::AccountId}
      PublicAccessBlockConfiguration:
        BlockPublicAcls: True
        BlockPublicPolicy: True
        IgnorePublicAcls: True
        RestrictPublicBuckets: True
# ------------------------------------------------------------#
# IAM
# ------------------------------------------------------------# 
  TaskExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      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
      RoleName: !Sub iam-${env}-ecs-tast-execution-role

  TaskRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - ecs-tasks.amazonaws.com
            Action:
              - sts:AssumeRole
      RoleName: !Sub iam-${env}-ecs-tast-role

  TaskRolePolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyName: !Sub iam-${env}-ecs-task-role-policy
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Action:
              - "s3:PutObject"
              - "s3:GetObject"
              - "s3:GetBucketLocation"
              - "logs:CreateLogStream"
              - "logs:PutLogEvents"
              - "logs:DescribeLogStreams"
              - "logs:CreateLogGroup"
              - "ssmmessages:CreateControlChannel"
              - "ssmmessages:CreateDataChannel"
              - "ssmmessages:OpenControlChannel"
              - "ssmmessages:OpenDataChannel"
            Resource:
              - "*"
      Roles:
        - !Ref TaskRole

# ------------------------------------------------------------#
# VPC
# ------------------------------------------------------------# 
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VPCCIDR
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags: 
        - Key: Name
          Value: !Sub vpc-${env}

# ------------------------------------------------------------#
# InternetGateway
# ------------------------------------------------------------# 
  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags: 
        - Key: Name
          Value: !Sub igw-${env}

  InternetGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId: !Ref InternetGateway
      VpcId: !Ref VPC

# ------------------------------------------------------------#
# Subnet
# ------------------------------------------------------------# 
  PublicSubnet01:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1a
      CidrBlock: !Ref PublicSubnet01CIDR
      MapPublicIpOnLaunch: true
      Tags: 
        - Key: Name
          Value: !Sub subnet-${env}-pub1
      VpcId: !Ref VPC

  PublicSubnet02:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1c
      CidrBlock: !Ref PublicSubnet02CIDR
      MapPublicIpOnLaunch: true
      Tags: 
        - Key: Name
          Value: !Sub subnet-${env}-pub2
      VpcId: !Ref VPC

  PrivateSubnet01:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1a
      CidrBlock: !Ref PrivateSubnet01CIDR
      MapPublicIpOnLaunch: true
      Tags: 
        - Key: Name
          Value: !Sub subnet-${env}-prv1
      VpcId: !Ref VPC

  PrivateSubnet02:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1c
      CidrBlock: !Ref PrivateSubnet02CIDR
      MapPublicIpOnLaunch: true
      Tags: 
        - Key: Name
          Value: !Sub subnet-${env}-prv2
      VpcId: !Ref VPC

# ------------------------------------------------------------#
# RouteTable
# ------------------------------------------------------------# 
  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: rtb-${env}-pub

  PublicRouteTableRoute:
    Type: AWS::EC2::Route
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway
      RouteTableId: !Ref PublicRouteTable

  PublicRtAssociation1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref PublicSubnet01

  PublicRtAssociation2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref PublicSubnet02

  PrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: rtb-${env}-prv

  PrivateRtAssociation1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      SubnetId: !Ref PrivateSubnet01

  PrivateRtAssociation2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      SubnetId: !Ref PrivateSubnet02

# ------------------------------------------------------------#
# NAT Gateway
# ------------------------------------------------------------# 
  NatGatewayEIP:
    Type: AWS::EC2::EIP
    DependsOn: InternetGatewayAttachment
    Properties:
      Domain: vpc
      Tags:
        - Key: Name 
          Value: !Sub eip-${env}-nat

  NatGateway:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt NatGatewayEIP.AllocationId
      SubnetId: !Ref PublicSubnet01
      Tags:
        - Key: Name
          Value: !Sub ngw-${env}

  PrivateRouteTableRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NatGateway

# ------------------------------------------------------------#
# SecurityGroup
# ------------------------------------------------------------# 
  ALBSG:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: for alb
      GroupName: !Sub securitygroup-${env}-alb
      SecurityGroupEgress: 
        - CidrIp: 0.0.0.0/0
          FromPort: -1
          IpProtocol: -1
          ToPort: -1
      SecurityGroupIngress:
        - FromPort: 80
          IpProtocol: tcp
          CidrIp: 0.0.0.0/0
          ToPort: 80
      Tags: 
        - Key: Name
          Value: !Sub securitygroup-${env}-alb
      VpcId: !Ref VPC

  ECSSG:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: for ecs
      GroupName: !Sub securitygroup-${env}-ecs
      SecurityGroupEgress: 
        - CidrIp: 0.0.0.0/0
          FromPort: -1
          IpProtocol: -1
          ToPort: -1
      SecurityGroupIngress:
        - FromPort: 80
          IpProtocol: tcp
          SourceSecurityGroupId: !Ref ALBSG
          ToPort: 80
      Tags: 
        - Key: Name
          Value: !Sub securitygroup-${env}-ecs
      VpcId: !Ref VPC

  VPCEndpointSG:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: for vpc endpoint
      GroupName: !Sub securitygroup-${env}-vpc-endpoint
      SecurityGroupEgress: 
        - CidrIp: 0.0.0.0/0
          FromPort: -1
          IpProtocol: -1
          ToPort: -1
      SecurityGroupIngress:
        - FromPort: 443
          IpProtocol: tcp
          SourceSecurityGroupId: !Ref ECSSG
          ToPort: 443
      Tags: 
        - Key: Name
          Value: !Sub securitygroup-${env}-vpc-endpoint
      VpcId: !Ref VPC

# ------------------------------------------------------------#
# VPC Endpoint
# ------------------------------------------------------------# 
  S3Endpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties: 
      RouteTableIds: 
        - !Ref PrivateRouteTable
      ServiceName: !Sub com.amazonaws.${AWS::Region}.s3
      VpcEndpointType: Gateway
      VpcId: !Ref VPC

  ECRdkrEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcEndpointType: Interface
      PrivateDnsEnabled: true
      ServiceName: !Sub com.amazonaws.${AWS::Region}.ecr.dkr
      VpcId: !Ref VPC
      SubnetIds: 
        - !Ref PrivateSubnet01
        - !Ref PrivateSubnet02
      SecurityGroupIds:
        - !Ref VPCEndpointSG

  ECRapiEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcEndpointType: Interface
      PrivateDnsEnabled: true
      ServiceName: !Sub com.amazonaws.${AWS::Region}.ecr.api
      VpcId: !Ref VPC
      SubnetIds:
        - !Ref PrivateSubnet01
        - !Ref PrivateSubnet02
      SecurityGroupIds:
        - !Ref VPCEndpointSG

  LogsEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcEndpointType: Interface
      PrivateDnsEnabled: true
      ServiceName: !Sub com.amazonaws.${AWS::Region}.logs
      VpcId: !Ref VPC
      SubnetIds: 
        - !Ref PrivateSubnet01
        - !Ref PrivateSubnet02
      SecurityGroupIds:
        - !Ref VPCEndpointSG

# ------------------------------------------------------------#
# ALB
# ------------------------------------------------------------# 
  ALB:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      IpAddressType: ipv4
      LoadBalancerAttributes:
        - Key: deletion_protection.enabled
          Value: false
      Name: !Sub alb-${env}-ecs
      Scheme: internet-facing
      SecurityGroups:
        - !Ref ALBSG
      Subnets: 
        - !Ref PublicSubnet01
        - !Ref PublicSubnet02
      Tags: 
        - Key: Name
          Value: !Sub alb-${env}-ecs
      Type: application

  TargetGroup1:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      HealthCheckEnabled: true
      HealthCheckIntervalSeconds: 30
      HealthCheckPath: /
      HealthCheckPort: traffic-port
      HealthCheckProtocol: HTTP
      HealthCheckTimeoutSeconds: 5
      HealthyThresholdCount: 5
      IpAddressType: ipv4
      Matcher:
        HttpCode: 200
      Name: !Sub tg-${env}-01
      Port: 80
      Protocol: HTTP
      ProtocolVersion: HTTP1
      Tags: 
        - Key: Name
          Value: !Sub tg-${env}-01
      TargetType: ip
      UnhealthyThresholdCount: 2
      VpcId: !Ref VPC

  ALBHTTPListener1:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      DefaultActions:
        - TargetGroupArn: !Ref TargetGroup1
          Type: forward
      LoadBalancerArn: !Ref ALB
      Port: 80
      Protocol: HTTP

# ------------------------------------------------------------#
# ECS
# ------------------------------------------------------------# 
  ECSCluster:
    Type: AWS::ECS::Cluster
    Properties:
      CapacityProviders:
        - FARGATE
      ClusterName: !Sub ecs-${env}-cluster
      DefaultCapacityProviderStrategy:
        - CapacityProvider: FARGATE
          Weight: 1

  ECSTaskDef:
    Type: AWS::ECS::TaskDefinition
    Properties:
      ContainerDefinitions:
        - Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/ecr-web-${env}:latest
          Essential: true
          LogConfiguration:
            LogDriver: awsfirelens
            Options: 
              log_group_name: !Sub /ecs/${env}/web-log
              log_stream_prefix: web/
              region: !Ref "AWS::Region"
              auto_create_group: "true"
              Name: cloudwatch_logs
          Name: !Sub task-web-${env}
          PortMappings:
            - ContainerPort: 80
              HostPort: 80
        - Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/ecr-app-${env}:latest
          Essential: true
          LogConfiguration:
            LogDriver: awsfirelens
            Options: 
              log_group_name: !Sub /ecs/${env}/app-log
              log_stream_prefix: app/
              region: !Ref "AWS::Region"
              auto_create_group: "true"
              Name: cloudwatch_logs
          Name: !Sub task-app-${env}
          PortMappings:
            - ContainerPort: 3031
              HostPort: 3031
        - Image: public.ecr.aws/aws-observability/aws-for-fluent-bit:stable
          Essential: false
          environment:
            - name: ENV
              value: !Ref env
            - name: AWS_REGION
              value: !Ref "AWS::Region"
            - name: AWS_ACCOUNT_ID
              value: !Ref "AWS::AccountId"
          FirelensConfiguration:
            Type: fluentbit
          LogConfiguration:
            LogDriver: awslogs
            Options: 
              awslogs-group: /ecs/ecs-aws-firelens-sidecar-container
              mode: non-blocking
              awslogs-create-group: "true"
              max-buffer-size: 25m
              awslogs-region: ap-northeast-1
              awslogs-stream-prefix: firelens
          Name: !Sub task-firelens-${env}
      Cpu: 256
      ExecutionRoleArn: !Ref TaskExecutionRole
      TaskRoleArn: !Ref TaskRole
      Family: !Sub task-${env}
      Memory: 512
      NetworkMode: awsvpc
      RequiresCompatibilities:
        - FARGATE

  ECSService:
    Type: AWS::ECS::Service
    DependsOn: 
      - ALBHTTPListener1
    Properties:
      Cluster: !Ref ECSCluster
      DesiredCount: 1
      EnableExecuteCommand: true
      LoadBalancers:
        - ContainerName: !Sub task-web-${env}
          ContainerPort: 80
          TargetGroupArn: !Ref TargetGroup1
      NetworkConfiguration:
        AwsvpcConfiguration:
          SecurityGroups:
            - !Ref ECSSG
          Subnets:
            - !Ref PrivateSubnet01
            - !Ref PrivateSubnet02
      ServiceName: !Sub service-${env}
      TaskDefinition: !Ref ECSTaskDef
      PlatformVersion: LATEST
      DeploymentConfiguration:
        DeploymentCircuitBreaker:
          Enable: true
          Rollback: true
      DeploymentController: 
        Type: ECS

# ------------------------------------------------------------#
# AutoScaling
# ------------------------------------------------------------# 
  ServiceAutoScalingRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub iam-${env}-ecs-autoscaling-role
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Principal:
              Service: application-autoscaling.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: !Sub iam-${env}-ecs-autoscaling-policy
          PolicyDocument:
            Statement:
              - Effect: Allow
                Action:
                  - application-autoscaling:*
                  - cloudwatch:DescribeAlarms
                  - cloudwatch:PutMetricAlarm
                  - ecs:DescribeServices
                  - ecs:UpdateService
                Resource: "*"

  ServiceScalingTarget:
    Type: AWS::ApplicationAutoScaling::ScalableTarget
    Properties:
      MinCapacity: 1
      MaxCapacity: 4
      ResourceId: !Sub service/${ECSCluster}/${ECSService.Name}
      RoleARN: !GetAtt ServiceAutoScalingRole.Arn
      ScalableDimension: ecs:service:DesiredCount
      ServiceNamespace: ecs

  ServiceScaleOutPolicy:
    Type: AWS::ApplicationAutoScaling::ScalingPolicy
    Properties:
      PolicyName: ecs-autoscaling-policy
      PolicyType: TargetTrackingScaling
      ScalingTargetId: !Ref ServiceScalingTarget
      TargetTrackingScalingPolicyConfiguration:
        TargetValue: 70.0
        ScaleInCooldown: 300
        ScaleOutCooldown: 300
        PredefinedMetricSpecification:
          PredefinedMetricType: ECSServiceAverageCPUUtilization

		

とりあえずCloudWatch Logsに全てのログを出すだけであれば以下のようにタスク定義内のコンテナ定義で設定することが可能です。
指定できるオプションはこちらのドキュメントに記載されています。

			
			          LogConfiguration:
            LogDriver: awsfirelens
            Options: 
              log_group_name: !Sub /ecs/${env}/web-log
              log_stream_prefix: web/
              region: !Ref "AWS::Region"
              auto_create_group: "true"
              Name: cloudwatch_logs

		

上記のCloudFormationテンプレートが正常にデプロイできると「/ecs/dev/app-log」と「/ecs/dev/web-log」というロググループ内にログが出力されていきます。
試しに以下のようにALB経由でアクセスするとエラーログが「/ecs/dev/app-log」に出力されることが確認できます。

			
			http://ALBのDNS名/err

		

以下のようにログが確認できます。
タスクのARNやタスク定義のバージョンなども確認ができます。

			
			{
    "source": "stdout",
    "log": "[2025-09-29 07:40:57,705] ERROR in app: test err access",
    "container_id": "c98113c7df794737895671db6784c2c6-3222487192",
    "container_name": "task-app-dev",
    "ecs_cluster": "ecs-dev-cluster",
    "ecs_task_arn": "arn:aws:ecs:ap-northeast-1:AWSアカウントID:task/ecs-dev-cluster/c98113c7df794737895671db6784c2c6",
    "ecs_task_definition": "task-dev:33"
}

		

S3とCloudWatch Logsに振り分ける設定

まずはFluent Bitの設定ファイルを作成します。
今回作成した設定ファイルは以下になります。

			
			[FILTER]
    Name rewrite_tag
    Match task-app-${ENV}-firelens*
    Rule $log ^(?=.*ERROR).*$ task-app-${ENV}-err-log-$container_id false
    Rule $log ^(?!.*ERROR).*$ task-app-${ENV}-not-err-log-$container_id false

[OUTPUT]
    Name cloudwatch_logs
    Match task-web-${ENV}-firelens*
    auto_create_group true
    log_group_name /ecs/${ENV}/web-log
    log_stream_prefix web/
    region ${AWS_REGION}

[OUTPUT]
    Name cloudwatch_logs
    Match task-app-${ENV}-err-log*
    auto_create_group true
    log_group_name /ecs/${ENV}/app-log
    log_stream_prefix app/
    region ${AWS_REGION}

[OUTPUT]
    Name s3
    Match task-app-${ENV}-not-err-log*
    bucket s3-log-ecs-${AWS_ACCOUNT_ID}
    region ${AWS_REGION}
    total_file_size 100M
    s3_key_format /app/$TAG/%Y/%m/%d/%H_%M_%S/$UUID.gz
    compression gzip

		

Filterセクションでrewrite_tagを使いアプリケーションのログから文字列にERRORが含まれているかいないかでエラーログ (app-err-log) とそれ以外のログ (app-not-err-log) でタグを付けています。
https://docs.fluentbit.io/manual/data-pipeline/filters/rewrite-tag

Matchについては「<コンテナ名>-firelens*」という形式で一致するようなので「task-app-${ENV}-firelens*」という形にしています。
https://aws.amazon.com/jp/blogs/news/under-the-hood-firelens-for-amazon-ecs-tasks/

タスク定義から生成されるログアウトプットは、<コンテナ名>-firelens* と <コンテナ名>-firelens** にマッチします。つまり、Fluent Bit を使用していて、コンテナ名が app の場合、マッチパターンは app-firelens* となります。

OUTPUTセクションはNginxのログとアプリケーションのログ用で3つ作成しています。
一つ目がNginxのログ用の設定です。
NginxはシンプルにCloudWatch Logsへ全てのログを出力するようにしています。
ここは、Filterセクションを追加してHTTPステータスコードなどで出力先を振り分けるといったことも可能だと思います。

二つ目がアプリケーションのエラーログをCloudWatch Logsに出力する設定です。
Match部分を「task-app-${ENV}-err-log*」としてFilterセクションで設定したタグに引っかかるようにしています。

三つめがアプリケーションのエラーログ以外をS3に出力する設定です。
Match部分を「task-app-${ENV}-not-err-log*」としてFilterセクションで設定したタグに引っかかるようにしています。
また、出力時にgzip圧縮を行うようにcompressionを設定しています。
他に設定できるパラメータは以下のドキュメントに記載されています。
https://docs.fluentbit.io/manual/data-pipeline/outputs/s3

上記の設定を「firelens.conf」というファイルに保存して「s3-conf-ecs-AWSアカウントID」というバケット名に配置してください。

S3バケットに配置したらCloudFormationテンプレートのタスク定義部分を以下のように変更してください。
FireLensコンテナの環境変数に「firelens.conf」を読み込ませる設定を行っています。

			
			  ECSTaskDef:
    Type: AWS::ECS::TaskDefinition
    Properties:
      ContainerDefinitions:
        - Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/ecr-web-${env}:latest
          Essential: true
          LogConfiguration:
            LogDriver: awsfirelens
          Name: !Sub task-web-${env}
          PortMappings:
            - ContainerPort: 80
              HostPort: 80
        - Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/ecr-app-${env}:latest
          Essential: true
          LogConfiguration:
            LogDriver: awsfirelens
          Name: !Sub task-app-${env}
          PortMappings:
            - ContainerPort: 3031
              HostPort: 3031
        - Image: public.ecr.aws/aws-observability/aws-for-fluent-bit:init-2.34.0
          Essential: false
          environment:
            - name: ENV
              value: !Ref env
            - name: AWS_REGION
              value: !Ref "AWS::Region"
            - name: AWS_ACCOUNT_ID
              value: !Ref "AWS::AccountId"
            - name: aws_fluent_bit_init_s3_1
              value: !Sub arn:aws:s3:::s3-conf-ecs-${AWS::AccountId}/firelens.conf
          FirelensConfiguration:
            Type: fluentbit
          LogConfiguration:
            LogDriver: awslogs
            Options: 
              awslogs-group: /ecs/ecs-aws-firelens-sidecar-container
              mode: non-blocking
              awslogs-create-group: "true"
              max-buffer-size: 25m
              awslogs-region: ap-northeast-1
              awslogs-stream-prefix: firelens
          Name: !Sub task-firelens-${env}
      Cpu: 256
      ExecutionRoleArn: !Ref TaskExecutionRole
      TaskRoleArn: !Ref TaskRole
      Family: !Sub task-${env}
      Memory: 512
      NetworkMode: awsvpc
      RequiresCompatibilities:
        - FARGATE

		

設定ファイルの配置とCloudFormationテンプレートの更新が完了したらECSサービスの更新を行い、新しいタスクを起動させます。
更新時は最新のタスク定義が選択されていることと「新しいデプロイの強制」にチェックを入れてください。
スクリーンショット 2025-10-01 173845

新しいタスクが起動したらALBのDNS名から何度かアクセスを行いCloudWatch Logsにログが出力されることを確認します。
以下の画像の通りエラーログのみCloudWatch Logsに出力できるようになっていることが確認できます。
スクリーンショット 2025-10-01 174119

S3側も「s3-log-ecs-AWSアカウントID」にログが出力されていることが確認できます。
スクリーンショット 2025-10-01 184026

さいごに

FireLensを使用してS3とCloudWatch Logsにログを振り分ける設定を試してみました。
ログの内容で出力先を振り分ける設定は初めてやったので設定ファイルの書き方に戸惑いましたが、慣れればサクッと設定できそうな内容でした。

この記事をシェアする

FacebookHatena blogX

関連記事

FireLensを使用してECS FargateでホストしているアプリケーションのログをS3とCloudWatch Logsロググループに出力してみた | DevelopersIO