EC2上のコンテナから直接CloudWatchLogsにログを出力する方法

前回の記事で構築した環境で、ディスク容量が発生した際の回避策としてログを直接CloudWatchLogsに出力するようにしたのでその備忘録です。

まず、EC2上のコンテナからCloudWatchLogsにログを転送する場合、以下の手段があります。

  1. Docker の logging driverを使う
  2. Fluent Bitを使う
  3. EC2 に出力してCloudWatch Agentで送る

今回のケースの場合、3はディスク容量を逼迫してしまうので選択肢から除外しました。
また、1についてはAmazonLinux2023のDockerではawslogs ドライバが削除されているため除外となり、必然的に手段2で対応しました。

1.アプリ側の標準出力化

まず、webサーバやLaravelのログをファイルに吐かずに標準出力に変更します。

1-1 Apacheの設定

#httpd.conf(VirtualHostを設定している場合はそのconf)に以下を記述

ErrorLog  /dev/stderr
CustomLog /dev/stdout combined

1-2 Laravelの設定

#.envに以下を記述
LOG_CHANNEL=stderr

2.Docker のログローテーション

Docker の既定ログドライバ json-file はローテーション無効=無限増加です。AMI 作成時点で制限を入れておきましょう。

#/etc/docker/daemon.jsonを作成して以下を記述
{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "20m",
    "max-file": "5"
  }
}

#保存したら、以下のコマンドで反映
sudo systemctl restart docker

3.Fluent Bit の導入

次に、fluent-bitの設定を追加します。

3.1 ディレクトリ構成(例)

/home/ec2-user/myrepo/
  ├─ docker-compose.yml
  └─ fluent-bit/
       ├─ fluent-bit.conf
       └─ parsers.conf

3.2 parsers.confの作成

[PARSER]
    Name        docker
    Format      json
    Time_Key    time
    Time_Format %Y-%m-%dT%H:%M:%S.%LZ
    Time_Keep   On

3.3 fluent-bit.confの作成

以下はfluent-bit.confの構成例です。
各環境に合わせてpathなどを調整してください。

[SERVICE]
    Flush         1
    Daemon        Off
    Log_Level     info
    Parsers_File  /fluent-bit/etc/parsers.conf

# ================================================================
# 入力:Docker の json-file
# ================================================================
[INPUT]
    Name                 tail
    Path                 /var/lib/docker/containers/*/*-json.log
    Tag                  raw.docker
    Parser               docker
    Docker_Mode          On
    Docker_Mode_Parser   docker
    Mem_Buf_Limit        200MB
    Skip_Long_Lines      On
    Refresh_Interval     1
    Read_from_Head       Off

# ================================================================
# 0) $log を JSON として解釈
# ================================================================
[FILTER]
    Name           parser
    Match          raw.docker
    Key_Name       log
    Parser         json
    Reserve_Data   On
    Preserve_Key   On

# ================================================================
# 1) Laravel 判定とタグ付与(どちらかがヒットすれば OK)
# ================================================================
# channel が文字列なら Laravel
[FILTER]
    Name    rewrite_tag
    Match   raw.docker
    Rule    $channel ^[A-Za-z0-9._-]+$ laravel.app true

# level_name が Monolog レベル名なら Laravel
[FILTER]
    Name    rewrite_tag
    Match   raw.docker
    Rule    $level_name ^(DEBUG|INFO|NOTICE|WARNING|ERROR|CRITICAL|ALERT|EMERGENCY)$ laravel.app true

# ================================================================
# 2) 残りの raw.docker は stdout/stderr で Apache に振り分け
# ================================================================
[FILTER]
    Name    rewrite_tag
    Match   raw.docker
    Rule    $stream ^stdout$ apache.access true

[FILTER]
    Name    rewrite_tag
    Match   raw.docker
    Rule    $stream ^stderr$ apache.stderr true

# ================================================================
# 3) stderr から Laravel を除外して Apache エラーへ
# ================================================================
[FILTER]
    Name     grep
    Match    apache.stderr
    Exclude  channel ^[A-Za-z0-9._-]+$

[FILTER]
    Name    rewrite_tag
    Match   apache.stderr
    Rule    $log .* apache.error true

# ================================================================
# 4) 出力:CloudWatch Logs
#    - Laravel は JSON で出力
#    - Apache はテキストのまま
# ================================================================
[OUTPUT]
    Name               cloudwatch_logs
    Match              apache.access
    region             ap-northeast-1
    log_group_name     /myrepo/apache
    log_stream_prefix  web-
    auto_create_group  true

[OUTPUT]
    Name               cloudwatch_logs
    Match              apache.error
    region             ap-northeast-1
    log_group_name     /myrepo/apache
    log_stream_prefix  web-
    auto_create_group  true

[OUTPUT]
    Name               cloudwatch_logs
    Match              laravel.app
    region             ap-northeast-1
    log_group_name     /myrepo/laravel
    log_stream_prefix  app-
    auto_create_group  true
    log_format         json

3.4 docker-compose.ymlにコンテナを追加

#既存のymlに以下を追加

  fluent-bit:
    image: public.ecr.aws/aws-observability/aws-for-fluent-bit:2.31.9
    container_name: px-crm-fluent-bit
    restart: always
    user: root
    volumes:
      - /var/lib/containers:/var/lib/containers:ro,Z
      - ./fluent-bit/fluent-bit.conf:/fluent-bit/etc/fluent-bit.conf:ro
      - ./fluent-bit/parsers.conf:/fluent-bit/etc/parsers.conf:ro
    environment:
      - AWS_REGION=ap-northeast-1
    command: ["/fluent-bit/bin/fluent-bit", "-c", "/fluent-bit/etc/fluent-bit.conf"]

ここまでの設定を行ってコンテナを実行すれば、pathなどに問題が無い限りはCloudWatchLogsに出力されていると思います。

もし出力されない場合は、EC2のロールを確認するのと、fluent-bit.confのFILTERの内容と実際のログを比較して確認して見てください。