• トップ
  • ブログ一覧
  • ECS サイドカーで特定ログを CloudWatch に分離出力する方法
  • ECS サイドカーで特定ログを CloudWatch に分離出力する方法

    はじめに

    今回は AWS ECS において、アプリケーションの通常ログとは別に特定のログファイルを別の CloudWatch Logs グループ に出力する「サイドカー構成」について紹介します。

    ログの分離出力は、開発・運用時のトラブルシューティングやログ保守・可視化の観点で非常に有用です。今回は、Railsで外部サービスとの連携ログをexternal_service.logに出力し、これをサイドカーのFluent BitコンテナでCloudWatch Logsに送信する構成を作ってみました。

    サイドカーとは?どんな時に使う?

    サイドカーコンテナとは、メインのアプリケーションコンテナと一緒に ECS タスク内で起動する補助的なコンテナのことです。

    以下のようなケースで使われることが多いです:

    • アプリとは別のログやメトリクスを処理したい
    • 定期的なバッチ処理やクリーンアップ処理を並列で実行したい
    • セキュリティ用の監視やエージェントを分離したい
    • Proxy / Sidecar Pattern(Istio など)でのマイクロサービス連携

    メインコンテナのサポート的な役割ですね!並走して走るのでサイドカー。

    サイドカーのコストは?

    ECS Fargate では、1 タスクに複数コンテナを定義できますが、課金対象はタスク数そのものです!

    つまり、、、、

    • サイドカーコンテナを追加してもタスク数が増えない
    • 現在の CPU / メモリに余裕があるなら追加コストは基本的に発生しない

    という点も嬉しいポイントです。

    今回の実装だとログ転送量次第では CloudWatch コストが少し増える可能性はあります。とはいえ通常の運用では 数百円/月 程度で収まるケースがほとんどです。

    やりたいことを整理

    • 通常ログは STDOUT → CloudWatch(デフォルト設定)
    • 外部連携ログは external_service.logに出力
    • Fluent Bit がそれを監視し CloudWatch に送信

    ECS タスク定義の変更

    Terraformで記述していきます!

    Fluent Bitサイドカーについて

    今回はFluent BitをサイドカーとしてECSタスクに追加することで、特定ログファイル(external_service.log)をCloudWatch Logsに転送する構成をとります。

    Fluent Bitは軽量なログ転送エージェントで、AWS公式が提供するイメージamazon/aws-for-fluent-bit:stableを使うことで、CloudWatch Logsへの出力がすぐに可能です。

    ちなみに自分が少しハマったポイントでもあるのですが、latestタグではCloudWatch出力に失敗するケースがありました。CloudWatchプラグインが含まれる安定版(stable)を明示的に指定するのが安心です。

    まずはCloudWatch Logsのロググループを作成します。

    1resource "aws_cloudwatch_log_group" "fluent_bit" {
    2  name              = "/ecs/external-service-log"
    3  retention_in_days = 30
    4}

    続いて、ECSタスク定義でFluent Bitをサイドカーとして追加します。

    最終的なタスク定義(Terraform)

    1resource "aws_ecs_task_definition" "ecs_task_definition" {
    2  family                   = var.name
    3  execution_role_arn       = var.ecs_role_arn
    4  task_role_arn            = var.ecs_task_role_arn
    5  network_mode             = "awsvpc"
    6  cpu                      = var.cpu_unit
    7  memory                   = var.memory_mb
    8  requires_compatibilities = ["FARGATE"]
    9
    10  container_definitions = <<CONTAINERS
    11[
    12  {
    13    "name": "${var.name}",
    14    "image": "${var.ecr_repository_url}:latest",
    15    "portMappings": [
    16      {
    17        "hostPort": ${var.port},
    18        "containerPort": ${var.port}
    19      }
    20    ],
    21    "logConfiguration": {
    22      "logDriver": "awslogs",
    23      "options": {
    24        "awslogs-group": "${var.cloud_watch_name}",
    25        "awslogs-region": "ap-northeast-1",
    26        "awslogs-stream-prefix": "${var.name}"
    27      }
    28    },
    29    "mountPoints": [
    30      {
    31        "sourceVolume": "shared-logs",
    32        "containerPath": "/shared-logs"
    33      }
    34    ]
    35  },
    36  {
    37    "name": "fluent-bit",
    38    "image": "amazon/aws-for-fluent-bit:stable",
    39    "logConfiguration": {
    40      "logDriver": "awslogs",
    41      "options": {
    42        "awslogs-group": "${var.external_log_group}",
    43        "awslogs-region": "ap-northeast-1",
    44        "awslogs-stream-prefix": "fluentbit"
    45      }
    46    },
    47    "mountPoints": [
    48      {
    49        "sourceVolume": "shared-logs",
    50        "containerPath": "/shared-logs"
    51      }
    52    ],
    53    "command": [
    54      "/fluent-bit/bin/fluent-bit",
    55      "-i", "tail",
    56      "-p", "path=/shared-logs/external_service.log",
    57      "-p", "tag=external_service",
    58      "-p", "read_from_head=false",
    59      "-p", "db=/fluent-bit/tail-external-service.db",
    60      "-o", "cloudwatch_logs",
    61      "-p", "region=ap-northeast-1",
    62      "-p", "log_group_name=${var.external_log_group}",
    63      "-p", "log_stream_prefix=${var.env}-external-",
    64      "-p", "auto_create_group=true"
    65    ],
    66    "essential": false
    67  }
    68]
    69CONTAINERS
    70
    71  volume {
    72    name = "shared-logs"
    73  }
    74}

    Fluent Bitのcommand解説

    Fluent Bitのcommandでは、tail プラグインでログファイルを監視し、CloudWatch Logsに送信する設定をしています。

    ポイントとしてはread_from_head=false で、コンテナ起動以降のログのみを対象にする という設定です。これにより、過去の古いログを大量に取り込むことを防げます。

    カスタムイメージで設定ファイルを使う方法(応用編)

    今回紹介した方法ではcommandにFluent Bitの設定を直接書いていますが、カスタムイメージを使う方法もあります!それぞれのメリット・デメリットです。

    1. メリット
      • 設定ファイルを細かく制御できる
      • 長い設定をcommandにベタ書きしなくて済む
    2. デメリット
      • Dockerfileとfluent-bit.confを自前で管理する必要がある

    Dockerfileの例

    1FROM amazon/aws-for-fluent-bit:stable
    2COPY fluent-bit.conf /fluent-bit/etc/fluent-bit.conf

    fluent-bit.confの例

    1[INPUT]
    2    Name tail
    3    Path /shared-logs/external_service.log
    4    Tag external_service
    5    DB /fluent-bit/tail-external-service.db
    6    Refresh_Interval 5
    7    Read_from_Head true
    8
    9[OUTPUT]
    10    Name cloudwatch_logs
    11    Match external_service
    12    region ap-northeast-1
    13    log_group_name ${EXTERNAL_LOG_GROUP}
    14    log_stream_prefix ${ENV_NAME}-external-
    15    auto_create_group true

    ECRにビルド&プッシュ

    1docker build -t YOUR_ECR_URL/fluent-bit:latest .
    2docker push YOUR_ECR_URL/fluent-bit:latest

    Terraform側(環境変数を渡す)

    1{
    2  "name": "fluent-bit",
    3  "image": "${var.ecr_fluent_bit_repository_url}:latest",
    4  "environment": [
    5    {
    6      "name": "SF_LOG_GROUP",
    7      "value": "${var.sf_cloud_watch_name}"
    8    }
    9  ]
    10}

    どっちを選ぶ?

    項目commandベースカスタムイメージ
    設定の柔軟性△ 少し大変◎ かなり柔軟
    実装のシンプルさ◎ 手軽に始められる△ 管理コストあり
    Terraformの見やすさ△ commandが長くなりがち◎ スッキリ

    今回は簡易的な構成のためcommandベースにしましたが、今後Fluent Bitによるフィルタやルール追加、複数ログ対応が必要になってきたら、このカスタムイメージ方式が有効だと思います!

    Rails側の設定

    Rails側ではRestforceのログ出力を環境によって切り替えます。

    config/initializers/external_service_logger.rb

    1ExternalService.log = true
    2ExternalService.configure do |config|
    3  if Rails.env.production?
    4    config.logger = Logger.new("/shared-logs/external_service.log", shift_age = 0, shift_size = 10 * 1024 * 1024)
    5  else
    6    config.logger = Logger.new("log/external_service.log")
    7  end
    8  config.log_level = :debug
    9end

    Fluent Bitはログファイルをtailしているため、ログファイルが肥大化しないようにshift_sizeを設定しましょう。

    Dockerfileの変更

    Fluent Bitサイドカーとログを共有するため、RailsアプリのDockerfileでも/shared-logsディレクトリを作成しておく必要があります。

    1# Fluent Bitサイドカーと共有するログディレクトリの作成
    2RUN mkdir -p /shared-logs && chown -R nobody:nogroup /shared-logs

    結果の確認

    CloudWatch Logsにロググループ/ecs/external-service-logが作成され、/shared-logs/external_service.logに出力された外部サービスのログが確認できるようになります。

    CloudWatch上では次のような形で確認できます:

    • /aws/ecs/your-service-name:アプリログ(デフォルト)
    • /aws/ecs/your-env-external:external_service.logのみ(分離)

    まとめ

    今回は、ECSのサイドカー構成を使って、特定ログファイルをCloudWatch Logsに分離出力する方法を紹介しました。

    アプリケーションが複数の外部サービスと連携するようになると、通常ログと分離してログを管理したくなる場面が増えてきます。今回のような構成を取り入れることで、障害調査やモニタリング、ログ保守が格段にやりやすくなります。

    また、ECSタスク数を増やすことなく、低コストでログ基盤を柔軟に強化できる点も大きなメリットです。アプリケーション側の修正も最低限で済み、既存サービスへの影響も最小限に抑えることができます。

    今後もログ可視化や集約のニーズが高まる中で、こうした柔軟なアーキテクチャの選択肢を持っておくことは、インフラ設計の幅を広げる上で非常に有効だと感じています!

    ライトコードでは、エンジニアを積極採用中!

    ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。

    採用情報へ

    あだっちー(エンジニア)
    あだっちー(エンジニア)
    Show more...

    おすすめ記事

    エンジニア大募集中!

    ライトコードでは、エンジニアを積極採用中です。

    特に、WEBエンジニアとモバイルエンジニアは是非ご応募お待ちしております!

    また、フリーランスエンジニア様も大募集中です。

    background