ECSにおけるログ運用の基本
はじめに
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 のほとんどを必要とする場合は、ブロッキングモードを使用することが通常は必要になります。その理由は、ネットワーク障害などによってドライバーがログをエンドポイントに配信できなくなった場合に、リングバッファが十分な量のメモリを利用できないからです。
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に転送できる。このとき、タスク実行ロールにログストリーム作成および書き込み権限があることを確認する。
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
へのシンボリックリンクとなっていることがわかる。
$ 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に出力されるのがわかる。
$ 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にログ転送する場合は対応する権限も必要)
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: '*'
実装方法
FireLensを使うときのTips
CWLにログを送信する場合、CWLのログストリームのprefixには使用しているイメージのタグをつけるのが良い。参照
FireLensを使うとき、extra.conf
の設定に$(tag)
を使うと上記可能になる。参照
また、タスクごとにCWLの出力先ログストリームを分ける必要もある。参照
その他
(非推奨)EFSにログを保存する
ログがコンテナ内の標準出力 (/dev/stdout
)と標準エラー出力(/dev/stderr
)に出力されない場合、EFSを使ってログを管理できる。参照
ただし、EFSはログのような細かいサイズの大量書き込み処理には向いておらず推奨されない。参照
(非推奨)コンテナ内でCloudWatchエージェントを動かす
あまりメリットなさそうだが出来ないこともない。参照
参考
Discussion