🐙

ECSにおけるログ運用の基本

2023/02/22に公開

はじめに

ECSのログ運用を考える機会があったので、学びをまとめておく。

Dockerコンテナのロギングの基本

ECSに入る前にDockerコンテナのロギングの基本をおさらい。

コンテナのログは/dev/stdout/dev/stderrに出力される

nginxやhttpdやnodeなどの公式で提供されているDockerイメージは、デタッチモードで動いている場合、アプリケーションのログ(Node.jsでいうところのconsole.log()console.error())が標準出力 (/dev/stdout)と標準エラー出力(/dev/stderr)に出力されるように設定されている。参考

コンテナのログはログドライバーを使って処理される

ログドライバーを使って、コンテナのログを整形したり、ホストマシン上のデーモンやクラウドに送信して処理をすることができる。

ログ配信モードを選ぶ

Dockerには、コンテナからログドライバーに向けてのログメッセージ転送に関して2つのモードがある。 参照

  • ブロッキング
    Docker のデフォルトであるブロッキングモードの場合、ドライバーにメッセージを配信するたびにアプリケーションが中断されます。そのため、すべてのメッセージを必ずドライバーに送信できますが、ドライバーが別の処理を行っていると、メッセージが配信されるまでコンテナが他のアプリケーションタスクを処理できないため、アプリケーションのパフォーマンスに遅延が生じる要因になります。
  • ノンブロッキング
    ノンブロッキングを指定した場合、コンテナは最初にメモリ内のリングバッファにログを書き込み、ログドライバーがログを処理できる状態になるまで、リングバッファにログを格納します。ドライバーが別の処理を行っていても、コンテナは即座にアプリケーションからのアウトプットをリングバッファに転送して、アプリケーションの実行を再開します。そのため、処理するログの量が多くても、コンテナで稼働するアプリケーションのパフォーマンスが低下することはありません。

ECSにおいてはブロッキングモード(デフォルト)を使うことが推奨されている。参照

ログをローカルに作成できない場合は、gcplogs や awslogs などのデフォルト以外のドライバーを使用して、ログをリモートの場所に送信する必要があります。アプリケーションが非常に多くのメモリを消費し、コンテナに割り当てられる RAM のほとんどを必要とする場合は、ブロッキングモードを使用することが通常は必要になります。その理由は、ネットワーク障害などによってドライバーがログをエンドポイントに配信できなくなった場合に、リングバッファが十分な量のメモリを利用できないからです。

CFn
  TaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      ...
      ContainerDefinitions:
        - Name: !Ref AppName
          ...
          LogConfiguration:
	    ...
            Options:
              ...
+	      mode: blocking

ECSにおけるロギング

ECSにおいてコンテナは必要に応じて増えたり消えたりする。コンテナ内にログを溜めていても消えてしまうため、ログの永続化のためにCloudWatchLogs(CWL)やS3にログを転送して保存する。

CloudWatchLogs(CWL)をつかったログ運用

例えばCWLにサブスクリプションフィルターを設定すれば、任意の内容を含むログを担当者やSlackに通知することができる。
また、Firehoseと連携してS3に安価にログを保存することも可能。この場合はCWLの保存期限を短く設定できる。

awslogsログドライバーを使う

ロググループを作成し、タスク定義にawslogsログドライバーを設定するだけでログをCWLに転送できる。このとき、タスク実行ロールにログストリーム作成および書き込み権限があることを確認する。

CFn
  AppLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Ref LogGroupName
      
  TaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      ...
+     ExecutionRoleArn: !GetAtt TaskExecutionRole.Arn
      ContainerDefinitions:
        - Name: !Ref AppName
          ...
          LogConfiguration:
+           LogDriver: awslogs
            Options:
              awslogs-group: !Ref AppLogGroup
              awslogs-region: !Ref AWS::Region
              awslogs-stream-prefix: !Ref ImageTagName
	   
  TaskExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Ref TaskExecutionRoleName
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Principal:
              Service: [ecs-tasks.amazonaws.com]
            Action: ['sts:AssumeRole']
      Policies:
        - PolicyName: AllowEcsToCreateLogStreamAndPutLog
          PolicyDocument:
            Statement:
              - Effect: Allow
                Action:
+                 - 'logs:CreateLogStream'
+                 - 'logs:PutLogEvents'
                Resource: '*'

このとき、ログはコンテナ内部の標準出力(/dev/stdout)および標準エラー出力(/dev/stderr)に出力された場合のみ、CWLに転送されることに注意。

awslogs ログドライバーは、Docker から CloudWatch Logs に STDOUT および STDERR I/O ストリームであるコンテナログを渡すだけです。そのため、アプリケーションが STDOUT および STDERR I/O ストリームにログを送信していることを確認してください。参照

試しにECS Execを使ってコンテナにログインして標準出力(/dev/stdout)および標準エラー出力(/dev/stderr)を見てみると、それぞれ/proc/self/fd/1および/proc/self/fd/2へのシンボリックリンクとなっていることがわかる。

ECS Exec
$ cd /dev
$ ls -l
lrwxrwxrwx    1 root     root      15 Feb  4 21:22 stderr -> /proc/self/fd/2
lrwxrwxrwx    1 root     root      15 Feb  4 21:22 stdin -> /proc/self/fd/0
lrwxrwxrwx    1 root     root      15 Feb  4 21:22 stdout -> /proc/self/fd/1

そこで、リンク先に直接書き込んでみると、実際にログがCWLに出力されるのがわかる。

ECS Exec
$ echo 1 >>  /proc/1/fd/1
$ echo error!! >> /proc/1/fd/2

CloudWatchLogs(CWL)はログ取り込みコストが高い

ログをCWLに出力する場合、下記のようなメリットとデメリットがある。
特にデータ取り込み時にかかるコスト観点から後述するFireLensを使うことをオススメする。

  • メリット
    • ログドライバを設定するだけで設定が簡単
    • 簡単に分析(Logs Insightsクエリ発行)できる
  • デメリット
    • 標準出力も標準エラー出力も全て同じストリームに配信されてしまう
    • 送信イベントごとに(出力1行ごとに)課金されてしまう部分で高コスト(参照)
    • S3と比較して保存コストも高い

FireLensをつかったログ運用

FireLensを使えば、CWLと比較して下記のようなメリットがあり、柔軟な運用が可能。

  • ログの内容に応じて後処理を振り分けられる
  • CWLに渡す必要のないログをソートできるため、CWLのデータ取り込みコストを削減できる

awsfirelensログドライバーを使う

awsfirelensはDockerのFluentdログドライバーの糖衣構文。参照
タスク内にFluentdもしくはFluentbitのサイドカーコンテナを配置し、アプリケーションコンテナからのログをFargateデータプレーンのDockerデーモンを通じてサイドカーコンテナに送信する。その後サイドカーコンテナからログをS3やCWLに振り分けることができる。

具体的な実装例は下記。参照
このとき、タスクロールにCWLログストリーム作成および書き込み権限があることを確認する。(S3やFirehoseにログ転送する場合は対応する権限も必要)

CFn
  TaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      ...
+     TaskRoleArn: !GetAtt TaskRole.Arn
      ContainerDefinitions:
        - Name: !Ref AppName
          ...
          LogConfiguration:
+           LogDriver: awsfirelens
+       - Name: !Ref SideCarName
+         FirelensConfiguration:
+           Type: fluentbit
+           Options:
+             config-file-type: file
+             config-file-value: /fluent-bit/etc/extra.conf
	   
  TaskRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Ref TaskRoleName
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Principal:
              Service: [ecs-tasks.amazonaws.com]
            Action: ['sts:AssumeRole']
      Policies:
        - PolicyName: AllowTaskToCreateLogStreamAndPutLog
          PolicyDocument:
            Statement:
              - Effect: Allow
                Action:
+                 - 'logs:CreateLogStream'
+                 - 'logs:PutLogEvents'
                Resource: '*'

実装方法

https://github.com/aws-samples/amazon-ecs-firelens-examples#fluent-bit-examples

FireLensを使うときのTips

CWLにログを送信する場合、CWLのログストリームのprefixには使用しているイメージのタグをつけるのが良い。参照
FireLensを使うとき、extra.confの設定に$(tag)を使うと上記可能になる。参照
また、タスクごとにCWLの出力先ログストリームを分ける必要もある。参照

その他

(非推奨)EFSにログを保存する

ログがコンテナ内の標準出力 (/dev/stdout)と標準エラー出力(/dev/stderr)に出力されない場合、EFSを使ってログを管理できる。参照
ただし、EFSはログのような細かいサイズの大量書き込み処理には向いておらず推奨されない。参照

(非推奨)コンテナ内でCloudWatchエージェントを動かす

あまりメリットなさそうだが出来ないこともない。参照

参考

https://zenn.dev/onigiri_w2/articles/e40c5873f7f453

https://qiita.com/sugimount-a/items/975dbee1478d8cf2981b

https://engineering.dena.com/blog/2022/08/firelens/

https://dev.classmethod.jp/articles/storing-error-logs-and-all-logs-separately-in-firelens/

https://rurukblog.com/post/ecs-fluentbit-logs/

https://dev.classmethod.jp/articles/terraform-ecs-fargate-firelens-log-output/

https://mazyu36.hatenablog.com/entry/2023/03/17/184805

https://qiita.com/naomichi-y/items/6eeebd21adf8e771a8e7

Discussion