AWS FargateでFireLens(Fluent Bit)を最小構成で起動し、ECS ExecでFireLensコンテナに入ってみる

FireLensの動作検証環境をサクッと作れるようにまとめました。
2021.07.18

ECSのタスク(コンテナ)のログ出力先を変更できるFireLens機能があります。 AWS FargateでFireLensの動作検証のため、最小限の設定でFireLensを起動させて動作確認する機会がありました。また使う場面がありそうなのでCloudFormationのテンプレートにまとめました。

Fluent Bitでのカスタムログルーティング設定は行いません。FireLensにはオリジナルのaws-for-fluent-bitイメージをそのまま起動します。FireLens自身のログはlog driverをawslogsに設定し、CloudWatch Logsへ直接送ることができます。FireLensをデバッグするには必要ですね。

もしカスタムログルーティングの設定を行いたい場合は以下のブログポストを参照ください。

本当に最小限の設定にしたいとなった結果、アプリケーションコンテナのログはFireLensコンテナの標準出力へ返すことになりました。本来FireLensコンテナのログ確認用にlog driverをawslogs設定でCloudWatch Logsへ送るところへ、アプリケーションログも載せてしまいます。 この設定はFluent Bitのカスタムログルーティングと一般的に呼ばれているものとは異なります。FireLensの動作検証の参考にしてください。

CloudFormation

テンプレートを実行すると約5分で以下の動作確認環境を構築できます。

Icons made by Freepik from www.flaticon.com

ポイント

  • NginxコンテナのパブリックIPにアクセスすることで、アクセスログがFireLensコンテナ経由でCloudWatch Logsへ保存
    • ELBは作成しません
  • 動作検証用にECS Exec用のタスクロール設定済み
  • パブリックのコンテナイメージをそのまま利用するため、自前でECRにイメージの準備不要
  • セキュリティグループが0.0.0.0/0で解放されています、必要に応じてアクセス元を制限してください

前提

VPCは準備済みのものを間借りします。

  • VPC作成済み
  • パブリックサブネットが2個あること

テンプレート

折り畳み

fargateWithFirelens.yml

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

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
  DesiredCount:
    Type: Number
    Default: 1
  ClusterName:
    Type: String
    Default: cluster
  AppName:
    Type: String
    Default: webapp
  ServiceName:
    Type: String
    Default: service
  TaskDefinitionName:
    Type: String
    Default: taskdef
  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:
  # --------------------------------------------
  # ECS Fargate
  # --------------------------------------------
  # Cluster
  ECSCluster:
    Type: "AWS::ECS::Cluster"
    Properties:
      ClusterName: !Sub "${ProjectName}-${Environment}-${ClusterName}"
      CapacityProviders:
        - "FARGATE_SPOT"
        - "FARGATE"

  ECSLogGroup:
    Type: "AWS::Logs::LogGroup"
    Properties:
      LogGroupName: !Sub "/ecs/logs/${ProjectName}-${Environment}-${TaskDefinitionName}"
  # Service
  ECSService:
    Type: "AWS::ECS::Service"
    Properties:
      ServiceName: !Sub ${ProjectName}-${Environment}-${ServiceName}
      Cluster: !Ref ECSCluster
      LaunchType: "FARGATE"
      PlatformVersion: "1.4.0"
      DesiredCount: !Ref DesiredCount
      DeploymentConfiguration:
        MaximumPercent: 200
        MinimumHealthyPercent: 100
        DeploymentCircuitBreaker:
          Enable: false
          Rollback: false
      NetworkConfiguration:
        AwsvpcConfiguration:
          AssignPublicIp: "ENABLED"
          SecurityGroups:
            - !Ref SecurityGroup1
          Subnets:
            - !Ref PublicSubnet1
            - !Ref PublicSubnet2
      TaskDefinition: !Ref ECSTaskDefinition
  # 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
          Image: !Ref ImageNameWebApp
          LogConfiguration:
            LogDriver: "awsfirelens"
            Options:
              Name: "stdout"
          Name: !Ref AppName
          PortMappings:
            - ContainerPort: 80
              HostPort: 80
              Protocol: "tcp"
        - Essential: true
          FirelensConfiguration:
            Type: "fluentbit"
          Image: !Ref ImageNameFirelens
          LogConfiguration:
            LogDriver: "awslogs"
            Options:
              awslogs-group: !Ref ECSLogGroup
              awslogs-region: !Ref AWS::Region
              awslogs-stream-prefix: "firelens"
          Name: "log_router"
          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
          CidrIp: "0.0.0.0/0"
          Description: "Access from Public"
      VpcId: !Ref VPCID
      Tags:
        - Key: Name
          Value: !Sub ${ProjectName}-${Environment}-${AppName}-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

  # --------------------------------------------
  # IAM Policy
  # --------------------------------------------
  # Allowed ECS Exec
  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": "*"
            }
          ]
        }

検証環境

項目 バージョン
aws-for-fluent-bit 2.17.0
Fluent Bit 1.8.1
Fargate platform 1.4.0
Rain 1.2.0

rain deploy時のパラメータ指定。VPC ID、サブネットIDはご自身の環境に合わせ変更してください。

rain deploy ./fargateWithFirelens.yml fargateWithFirelens-stack --params \
        AppName=webapp,\
        ClusterName=cluster,\
        DesiredCount=1,\
        Environment=test,\
        ImageNameFirelens=public.ecr.aws/aws-observability/aws-for-fluent-bit:latest,\
        ImageNameWebApp=public.ecr.aws/nginx/nginx:latest,\
        ProjectName=minimum,\
        PublicSubnet1=subnet-043566448c316b46a,\
        PublicSubnet2=subnet-00a24cb4e0d180ffe,\
        ServiceName=service,\
        TaskDefinitionName=taskdef,\
        VPCID=vpc-05d62f9c3d68253a2

FireLens動作確認

最小構成でFireLensが確実に起動できることを目的とした環境です。

タスク定義

CloudFormationで生成されたタスク定義を確認します。設定ポイントを抜粋して説明します。

アプリケーションコンテナの設定

アプリコンテナのlogConfigurationのlog driverはawsfirelensを指定し、optionで最もシンプルな設定だと思われるNameキーにstdoutの値を指定しています。これによりアプリコンテナのログがFireLensコンテナの標準出力へ送られます。

アプリケーションコンテナの設定抜粋

  "executionRoleArn": "arn:aws:iam::123456789012:role/minimum-test-webapp-ECSTaskExecutionRole",
  "containerDefinitions": [
    {
      "dnsSearchDomains": [],
      "environmentFiles": [],
      "logConfiguration": {
        "logDriver": "awsfirelens",
        "secretOptions": [],
        "options": {
          "Name": "stdout"
        }
      },

FireLensコンテナの設定

FireLensコンテナのlogConfigurationのlog dirverはawslogsのままで、FireLensコンテナの標準出力内容をCloudWatch Logsへ送ります。前述のアプリコンテナの設定により、FireLensの標準出力内容にアプリコンテナのログが載かってくるわけです。

firelensConfigurationの設定値はfluentbitを指定し、オプションは未指定です。

FireLensの設定抜粋

      "logConfiguration": {
        "logDriver": "awslogs",
        "secretOptions": [],
        "options": {
          "awslogs-group": "/ecs/logs/minimum-test-taskdef",
          "awslogs-region": "ap-northeast-1",
          "awslogs-stream-prefix": "firelens"
        }
      },
...snip...
      "image": "public.ecr.aws/aws-observability/aws-for-fluent-bit:latest",
      "startTimeout": null,
      "firelensConfiguration": {
        "type": "fluentbit",
        "options": {}
      },

タスク定義全文

折り畳み
{
  "ipcMode": null,
  "executionRoleArn": "arn:aws:iam::123456789012:role/minimum-test-webapp-ECSTaskExecutionRole",
  "containerDefinitions": [
    {
      "dnsSearchDomains": [],
      "environmentFiles": [],
      "logConfiguration": {
        "logDriver": "awsfirelens",
        "secretOptions": [],
        "options": {
          "Name": "stdout"
        }
      },
      "entryPoint": [],
      "portMappings": [
        {
          "hostPort": 80,
          "protocol": "tcp",
          "containerPort": 80
        }
      ],
      "command": [],
      "linuxParameters": null,
      "cpu": 0,
      "environment": [],
      "resourceRequirements": null,
      "ulimits": [],
      "dnsServers": [],
      "mountPoints": [],
      "workingDirectory": null,
      "secrets": [],
      "dockerSecurityOptions": [],
      "memory": null,
      "memoryReservation": null,
      "volumesFrom": [],
      "stopTimeout": null,
      "image": "public.ecr.aws/nginx/nginx:latest",
      "startTimeout": null,
      "firelensConfiguration": null,
      "dependsOn": null,
      "disableNetworking": null,
      "interactive": null,
      "healthCheck": null,
      "essential": true,
      "links": [],
      "hostname": null,
      "extraHosts": [],
      "pseudoTerminal": null,
      "user": null,
      "readonlyRootFilesystem": null,
      "dockerLabels": {},
      "systemControls": [],
      "privileged": null,
      "name": "webapp"
    },
    {
      "dnsSearchDomains": [],
      "environmentFiles": [],
      "logConfiguration": {
        "logDriver": "awslogs",
        "secretOptions": [],
        "options": {
          "awslogs-group": "/ecs/logs/minimum-test-taskdef",
          "awslogs-region": "ap-northeast-1",
          "awslogs-stream-prefix": "firelens"
        }
      },
      "entryPoint": [],
      "portMappings": [],
      "command": [],
      "linuxParameters": null,
      "cpu": 0,
      "environment": [],
      "resourceRequirements": null,
      "ulimits": [],
      "dnsServers": [],
      "mountPoints": [],
      "workingDirectory": null,
      "secrets": [],
      "dockerSecurityOptions": [],
      "memory": null,
      "memoryReservation": null,
      "volumesFrom": [],
      "stopTimeout": null,
      "image": "public.ecr.aws/aws-observability/aws-for-fluent-bit:latest",
      "startTimeout": null,
      "firelensConfiguration": {
        "type": "fluentbit",
        "options": {}
      },
      "dependsOn": null,
      "disableNetworking": null,
      "interactive": null,
      "healthCheck": null,
      "essential": true,
      "links": [],
      "hostname": null,
      "extraHosts": [],
      "pseudoTerminal": null,
      "user": "0",
      "readonlyRootFilesystem": null,
      "dockerLabels": {},
      "systemControls": [],
      "privileged": null,
      "name": "log_router"
    }
  ],
  "placementConstraints": [],
  "memory": "512",
  "taskRoleArn": "arn:aws:iam::123456789012:role/minimum-test-webapp-ECSTaskRole",
  "compatibilities": [
    "EC2",
    "FARGATE"
  ],
  "taskDefinitionArn": "arn:aws:ecs:ap-northeast-1:123456789012:task-definition/minimum-test-webapp-taskdef:1",
  "family": "minimum-test-webapp-taskdef",
  "requiresAttributes": [
    {
      "targetId": null,
      "targetType": null,
      "value": null,
      "name": "com.amazonaws.ecs.capability.logging-driver.awslogs"
    },
    {
      "targetId": null,
      "targetType": null,
      "value": null,
      "name": "ecs.capability.execution-role-awslogs"
    },
    {
      "targetId": null,
      "targetType": null,
      "value": null,
      "name": "com.amazonaws.ecs.capability.docker-remote-api.1.19"
    },
    {
      "targetId": null,
      "targetType": null,
      "value": null,
      "name": "ecs.capability.firelens.fluentbit"
    },
    {
      "targetId": null,
      "targetType": null,
      "value": null,
      "name": "com.amazonaws.ecs.capability.docker-remote-api.1.17"
    },
    {
      "targetId": null,
      "targetType": null,
      "value": null,
      "name": "com.amazonaws.ecs.capability.logging-driver.awsfirelens"
    },
    {
      "targetId": null,
      "targetType": null,
      "value": null,
      "name": "com.amazonaws.ecs.capability.task-iam-role"
    },
    {
      "targetId": null,
      "targetType": null,
      "value": null,
      "name": "com.amazonaws.ecs.capability.docker-remote-api.1.18"
    },
    {
      "targetId": null,
      "targetType": null,
      "value": null,
      "name": "ecs.capability.task-eni"
    }
  ],
  "pidMode": null,
  "requiresCompatibilities": [
    "FARGATE"
  ],
  "networkMode": "awsvpc",
  "cpu": "256",
  "revision": 1,
  "status": "ACTIVE",
  "inferenceAccelerators": null,
  "proxyConfiguration": null,
  "volumes": []
}

ログ出力確認

クラスターが作成されています。

FireLens(log_router)が起動しています。

タスクからパブリックIPを確認します。

WebブラウザからパブリックIPへアクセスすると、Nginxのデフォルトページを確認できます。

Nginxコンテナ(webapp)のログを確認します。log driverはFireLensを指定しているため、ここからはなにも確認できません。

FireLensコンテナ(log_router)のログを確認します。Nginxのアクセスログを確認できます。FireLensのログと同じCloudWatch Logsのロググループにログが保存されています。アクセス元のIPは伏せていますが、Webブラウザでアクセスした時間のログが確認できます。

CloudWatch Logsからも確認してみます。FireLensの起動ログが記録されています。FireLensのデバッグ目的のため、FireLensが起動しない、すぐ落ちるなどあればCloudWatch Logsから確認できます。

起動ログの後半には、さきほどのNginxコンテナのアクセスログも同様に確認できます。

FireLensへECS Exec

ECS ExecのおかげでFargateでも障害調査がはかどるようになりました。ECS Execに必要な権限はCloudFormationで作成済みです。だけど、現状AWS CLIからの操作が必須な箇所あります。詳細は以下のブログポストを参照ください。

やってみる

やや手間な作業ですがIAMロールが正しいか動作確認ためやります。

ECS Exec機能を有効化します。

aws ecs update-service \
    --cluster minimum-test-cluster \
    --service minimum-test-service \
    --enable-execute-command

enableExecuteCommandtrueであることを確認します。

実行結果

                "rolloutState": "COMPLETED",
...skipping...
        "enableECSManagedTags": false,
        "propagateTags": "NONE",
        "enableExecuteCommand": true
    }
}

ECS Exec機能を有効化した後に起動したタスクではじめてECS Execが利用できます。つまり、今起動しているタスク(コンテナ)ではECS Execが使えないです。

さきほどECS Exec機能を有効化しましたが、タスク定義のリビジョンに変更はありません。タスクの新しいデプロイの強制し、ECSのサービス(minimum-test-service)を更新かけます。

aws ecs update-service \
    --cluster minimum-test-cluster \
	--service minimum-test-service \
	--task-definition minimum-test-webapp-taskdef \
	--force-new-deployment

新しいタスクがデプロイ中です。

現在起動しているタスクIDを確認します。タスクは1個しか起動していないため、1つだけ結果が返ってきました。

aws ecs list-tasks \
    --cluster minimum-test-cluster

実行結果

{
    "taskArns": [
        "arn:aws:ecs:ap-northeast-1:123456789012:task/minimum-test-cluster/0575613b8a7a4d0a8845ccd70cef8e7a"
    ]
}

新しいタスクでenableExecuteCommandtrueであることを念のため確認します。

aws ecs describe-tasks \
    --cluster minimum-test-cluster \
    --tasks 0575613b8a7a4d0a8845ccd70cef8e7a

実行結果

            "availabilityZone": "ap-northeast-1a",
...skipping...
            "enableExecuteCommand": true,
            "group": "service:minimum-test-service",
            "healthStatus": "UNKNOWN",

aws-for-fluent-bitのイメージを利用したFireLensです。ベースイメージを確認したところamazonlinuxでした。

Amazon LinuxとのことでBashでログインしてみます。

aws ecs execute-command \
    --cluster minimum-test-cluster \
    --task 0575613b8a7a4d0a8845ccd70cef8e7a \
	--container log_router \
    --interactive \
    --command "/bin/bash"

FIreLens(log_router)コンテナ内

FireLensコンテナに入れました。

Fluent Bitの設定ファイルを直接確認できました。 aws-for-fluent-bitイメージをそのまま起動しているため、カスタムコンフィグの指定がない状態です。

bash-4.2# cat /fluent-bit/etc/fluent-bit.conf

[INPUT]
    Name tcp
    Listen 127.0.0.1
    Port 8877
    Tag firelens-healthcheck

[INPUT]
    Name forward
    unix_path /var/run/fluent.sock

[INPUT]
    Name forward
    Listen 127.0.0.1
    Port 24224

[FILTER]
    Name record_modifier
    Match *
    Record ecs_cluster minimum-test-cluster
    Record ecs_task_arn arn:aws:ecs:ap-northeast-1:123456789012:task/minimum-test-cluster/0575613b8a7a4d0a8845ccd70cef8e7a
    Record ecs_task_definition minimum-test-webapp-taskdef:1

[OUTPUT]
    Name null
    Match firelens-healthcheck

[OUTPUT]
    Name stdout
    Match webapp-firelens*

おわりに

FireLensコンテナへECS Exceすることはなかったので良い機会でした。FireLens起動だけのために必要なタスクロール、タスク実行ロールはないことを確認できました。カスタムログルーティングのためのタスクロール設定は頭にあったのですが、起動にあたり何か必要だったかは意識していなかったので勉強になりました。

参考