こんにちは。イタンジ株式会社でインフラエンジニアを担当している李(イ)です。 AWS、GCP、Azureなどのクラウド基盤でイタンジのサービスのインフラ構築および運用を主な仕事としています。
サービスのインフラをAWSで構築する場合、弊社ではECS(Elastic Container Service)を利用するのが一般的です。その際にログの保存をどうするかの問題について、ECSだとCloudWatchにログを流すのが一般的なので弊社もそうしてきましたが、サービスの成長に伴ってCloudWatchによるログ出力及び保存のコストが年々上がっているという課題がありました。
言うまでもなく、サービスを運用する側にとってコストはあまり高くしたくないものですね。ログのように、サービスの成長に伴ってスケールするものなら尚更です。
今回は、AWS ECSでアプリケーションを運用する場合にログ保存のコストを削減する方法を紹介します。
ECSにおけるログの出力、保存とは
ECSログの出力、保存は
- コンテナからログを「収集」する
- 収集したログを保存先に「転送」する
- 受け取ったログを「保存」しておく
と段階を分けて考えられます。CloudWatchを使用する場合は、ログを保存するロググループを指定するだけで収集から保存までを自動的に処理してくれるので便利です。
しかし、大量のログを生成するアプリケーションや、多数のコンテナを実行する環境では、これらの費用が予想以上に高額になることがあります。弊社の場合だと、全体のコストのうち約3%がCloudWatchへのログ出力で発生していました。
この課題を解決するために、より費用対効果の高いログ保存の仕組みが必要となりました。そこで登場するのがAWS FireLensです。
AWS FireLensとは
AWS FireLensとはAWS ECSで利用できるログルーターのことです。ログルーターを使用することでコンテナで収集したログをCloudWatch以外の宛先に転送することが可能になります。また、ログルーターの設定を活用することでログを加工することもできます。
AWS FireLensはオープンソースのログルーターであるFluentdまたはFluent Bitとして動作します。なので、これらのプロジェクトがサポートする宛先であればログを転送することができます。AWS公式ブログではリソース使用量の側面でFluent Bitの方を推奨していたので、今回はFluent Bitをログルーターとして採用しました。
AWS FireLensを使用したログ保存の仕組み
まずはログ収集と保存でどういうサービスを利用するかを決めて、それを基にインフラの仕組みを考えてみましょう。前述のとおり、ECSログの保存は「収集」、「転送」、「保存」と段階を分けて考えられます。この中で、収集はFireLens(Fluent Bit)を使用すると決めたので、残りの2つをどうするかを考えてみましょう。
ログの保存
まず、ログの保存先に関しては原則S3が良いと考えています。CloudWatchと比べてデータ保存のコストが安くて、ライフサイクルでログを保存してから経過した期間に応じて低コストのストレージクラスに変更したりログファイルを破棄したりすることが可能なので、データ保存コストを更に抑えることが可能になります。
S3に保存したログは、GlueとAthenaというAWSサービスを使用して確認することができます。お客様のお問い合わせ対応などでログを確認しなければならない場面が出てくるはずなので、S3のログが確認できる仕組みも事前に考えておきましょう。
ログの転送
保存先が決まったら、次はログの転送です。Fluent Bitをログルーターとして使用するので、Fluent Bitで対応している転送先であればどこでもログを流すことができます。AWSサービスと統合するなら、「CloudWatchに流す」、「直接S3に流す」、「Kinesis Data Firehoseを介してS3に流す」の方法が考えられます。
「先ほどCloudWatchは高いと言わなかった?」と思われるかもしれませんが、CloudWatchは必ずしも悪いとは限りません。設定がシンプルでログの確認が便利だというメリットは変わりません。そのため、エラーログのように素早く確認したいデータは引き続きCloudWatchに流すことにしました。
「直接S3に流す」方法はシンプルかつ非常に安いコストでログの保存ができます。その代わりに、コンテナの破棄やS3の障害などでログが欠損する可能性があります。商用システムにおいてログが消える可能性は許されないので、今回の対応では使用しないことにしました。
「Kinesis Data Firehoseを介してS3に流す」方法はFluent BitからFirehoseデータストリームにログを流して、またFirehoseがS3にログを流す方法です。直接S3にログを流す方法と比べて構成が複雑で追加コストがかかりますが、ログの転送にAWSマネージドサービスを利用するのでデータの欠損なく安定してログを流せるようになります。ログ保存のコストのほとんどは通常のアクセスログで発生するので、アクセスログの保存にこの方法を使用することにしました。
紹介した方法それぞれのメリット、デメリット、ユースケースをまとめると以下になります。
| CloudWatch | 直接S3に流す | Kinesis Data Firehoseを介してS3に流す | |
|---|---|---|---|
| メリット | 設定がシンプルでログの確認が便利 | 非常に安いコストで運用可能 | CloudWatchと比べて安価で欠損なくログの保存が可能 |
| デメリット | コストが高い | ログが欠損する可能性がある | 構成が多少複雑になり、Firehoseの追加コストがかかる |
| ユースケース | エラーログのように素早く確認したいログの保存 | - | アクセスログの保存 |
構成図

料金表の比較
以下の比較は東京リージョンの利用料金を基にしたものです。
| CloudWatch | FireLens + Kinesis Data Firehose + S3 | |
|---|---|---|
| 収集 | USD 0.76/GB (スタンダードの場合) |
FireLensコンテナの稼働料金 CPUやメモリ使用量が少ないため、コストへの影響は軽微 |
| 転送 | - | Kinesis Data Firehoseを使用 処理されたGBあたり USD 0.032 S3オブジェクト1000個あたり USD 0.008 |
| 保存 | 月間 USD 0.033/圧縮されたGBあたり | S3を使用 標準クラスの場合、月間 USD 0.023/GB |
やってみる
1. Fluent Bitの設定ファイルを用意する
ログの性質に応じて送信先を分岐するため、まずはFluent Bitの設定ファイルを用意します。 以下のような操作が可能です:
- ログを検査して特定のログは出力しないようにする
- ログの内容を加工する
- タグをつけてログの転送先を分岐する
上記以外でもFluent Bitを利用すると様々なログ処理が可能になるので、詳しくはマニュアルをチェックしてみてください。
[SERVICE]
Flush 1
Daemon Off
Log_Level $${LOG_LEVEL}
Grace 30
HTTP_Server On
HTTP_Listen 0.0.0.0
HTTP_PORT 2020
Health_Check On
HC_Errors_Count 5
HC_Retry_Failure_Count 5
HC_Period 5
# ---------------------------------------------------------------
# FILTER: Grepフィルタ
# 目的:
# - ヘルスチェック用ログ(ELB-HealthCheckerのログ)を除外
# - 空白行を除外
# - log-routerコンテナ自身のログを除外(これにより、内部ログの流出を防ぐ)
# 対象:タグが "*-firelens-*" にマッチするログ全般
# ---------------------------------------------------------------
[FILTER]
Name grep
Match *-firelens-*
Exclude log ^(?=.*ELB-HealthChecker\/2\.0).*$
Exclude log ^$
Exclude container_name ^log-router$
# ---------------------------------------------------------------
# FILTER: rewrite_tagフィルタ(アプリケーションログの識別)
# 目的:
# - 複数ルールでアプリケーションログを識別:
# 1. $progname が "access-log" の場合 → タグ "app-logs"
# 2. container_name が "app" の場合 → タグ "app-cw-logs"
# - いずれも "false" で元レコードは破棄する
# ---------------------------------------------------------------
[FILTER]
Name rewrite_tag
Match *-firelens-*
Rule $progname ^access-log$ app-logs false
Rule $container_name ^app$ app-cw-logs false
# ---------------------------------------------------------------
# FILTER: rewrite_tagフィルタ(アクセスログ中のエラー/警告の抽出)
# 目的:
# - すでに "app-logs" タグが付与されたログについて、さらに詳細な解析を行い、
# ・ログ内に "level" が ERROR, FATAL, CRITICAL, WARN の場合
# ・または "status" が 4xx, 5xx の場合
# これらの条件にマッチする場合は、タグ "app-cw-logs" を追加
# ---------------------------------------------------------------
[FILTER]
Name rewrite_tag
Match app-logs
Rule $log .*\"level\":\"(ERROR|FATAL|CRITICAL|WARN)\" app-cw-logs true
Rule $log .*\"status\":(4|5)\d{2}.* app-cw-logs true
# ---------------------------------------------------------------
# OUTPUT: CloudWatch Logs 出力(アクセスログ以外のアプリケーションログ、エラーレベルのアクセスログ)
# 目的:
# - タグが "app-cw-logs" のログを CloudWatch Logs に送信
# ---------------------------------------------------------------
[OUTPUT]
Name cloudwatch_logs
Match app-cw-logs
region ap-northeast-1
log_group_name example-log-group
log_stream_name app/$${ECS_TASK_ID}
auto_create_group false
# ---------------------------------------------------------------
# OUTPUT: Kinesis Firehose 出力(通常のアクセスログ)
# 目的:
# - タグが "app-logs" のログを Kinesis Firehose を通じてS3に送信
# ---------------------------------------------------------------
[OUTPUT]
Name kinesis_firehose
Match app-logs
region ap-northeast-1
delivery_stream example-stream
2. Fluent Bitの設定ファイルをS3に保存する
FireLensでカスタムの設定ファイルを使用するには
- Fluent Bitのコンテナイメージの内部に設定ファイルを入れる
- S3に設定ファイルを保存して、Fluent Bitのコンテナから参照する
の方法があります。1の方法だと設定ファイルを修正した際にそれを適用するにはFluent BitイメージのビルドとECSのデプロイを再度行う必要があり、運用上の負荷が高くなるため、S3から参照させる方法を使用します。
弊社はサイドカーコンテナの設定ファイルを中央管理する専用のバケットを作って、そこにFluent Bitの設定ファイルを保存しました。
3. タスクロールに権限を付与する
Fluent Bitのコンテナがログの転送先にアクセスできる必要があるため、その権限をタスクロールに付与します。タスクロールに必要な権限としては
- Fluent Bitの設定ファイルを保存したS3からオブジェクトを取得する権限
- ログを転送したいCloudWatchロググループにログを流す権限
- ログを転送したいKinesis Data Firehoseストリームにレコードを書き込む権限
があります。これらの権限を付与するためにタスクロールにアタッチしているポリシーを修正します。
{ "Version": "2012-10-17", "Statement": [ { "Sid": "AllowFirehosePutRecordBatch", "Effect": "Allow", "Action": "firehose:PutRecordBatch", "Resource": "arn:aws:firehose:ap-northeast-1:xxxxxxxxxxxx:deliverystream/example-stream" }, { "Sid": "AllowCloudWatchPutRecords", "Effect": "Allow", "Action": [ "logs:CreateLogStream", "logs:CreateLogGroup", "logs:DescribeLogStreams", "logs:PutLogEvents", "logs:PutRetentionPolicy" ], "Resource": [ "arn:aws:logs:ap-northeast-1:xxxxxxxxxxxx:log-group:example-log-group", "arn:aws:logs:ap-northeast-1:xxxxxxxxxxxx:log-group:example-log-group:*" ] }, { "Sid": "AllowS3ListBucket", "Effect": "Allow", "Action": ["s3:ListBucket", "s3:GetBucketLocation"], "Resource": "arn:aws:s3:::example-bucket" }, { "Sid": "AllowS3GetObject", "Effect": "Allow", "Action": "s3:GetObject", "Resource": "arn:aws:s3:::example-bucket/*" } ] }
4. Fluent BitのイメージをECRリポジトリにpushしておく
プライベートのECRリポジトリを作成して、そこにFluent Bitのイメージをpushしておきます。 AWSのパブリックECRギャラリーにAWS公式のFluent Bitリポジトリがあるので、そのイメージをそのままプライベートリポジトリにpushしてください。
パブリックECRにアップされているイメージをそのまま使っても動きはしますが、プライベートサブネットで稼働しているECSの場合だとパブリックECRからイメージをpullするにはNATゲートウェイを経由しなくてはいけません。現在ECRのVPCエンドポイントがパブリックリポジトリに対応していないのがその理由です。
現在、VPC エンドポイントは Amazon ECR パブリックリポジトリをサポートしていません。プルスルーキャッシュルールを使用して、VPC エンドポイントと同じリージョンにあるプライベートリポジトリでパブリックイメージをホストすることを検討してください。
NATゲートウェイの料金もそこそこ高いので、NATの通信は可能なら利用しない方向で対応したいですね。なので、プライベートリポジトリを作ってそこにFluent Bitのイメージをpushしておくのです。
5. タスクにログルーターのサイドカーコンテナを追加する
FireLensはタスク定義の中でサイドカーコンテナとして配置して、他のコンテナからはログドライバーとして使用します。 タスク定義を修正したあとにECSサービスをデプロイすることで、ログルーターを使用したログの転送が開始されます。
{ "family": "sample-app", "taskRoleArn": "arn:aws:iam::xxxxxxxxxxxx:role/example-task-role", "executionRoleArn": "arn:aws:iam::xxxxxxxxxxxx:role/example-task-exec-role", "networkMode": "awsvpc", "containerDefinitions": [ { "name": "log-router", "image": "xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/aws-for-fluent-bit:latest", "essential": false, "firelensConfiguration": { "type": "fluentbit", "options": { "enable-ecs-log-metadata": "true" } }, "memoryReservation": 64 }, { "name": "app", "image": "xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/example-app:latest", "essential": true, "portMappings": [ { "containerPort": 3000, "hostPort": 3000, "protocol": "tcp" } ], "logConfiguration": { "logDriver": "awsfirelens" }, "environment": [ { "name": "RAILS_ENV", "value": "production" } ], "secrets": [], "memoryReservation": 1024, "healthCheck": { "command": [ "CMD-SHELL", "curl -f http://localhost:3000/health || exit 1" ], "interval": 30, "timeout": 5, "retries": 3, "startPeriod": 60 }, "ulimits": [ { "name": "nofile", "softLimit": 65536, "hardLimit": 65536 } ] } ] }
実際の削減効果
ログ出力のコストが高いサービスの中で1つにこの仕組みを試験的に入れてみたところ、約50%~60%のコストが削減できました。🎉

まとめ
ECSでログを保存する際、デフォルトのCloudWatchはシンプルで便利ですが、ログ量が増えるとコストが高くなる問題があります。本記事では、AWS FireLens(Fluent Bit)を活用して、ログの種類に応じて転送先を分けることでコスト削減を実現する方法を紹介しました。
具体的には、アクセスログはFireLens経由でKinesis Data Firehoseを通してS3に保存し、エラーログのみCloudWatchに流すことで、ログ保存のコストを半額以上削減できました。実装には、Fluent Bitの設定ファイル作成、S3への保存、適切な権限設定、そしてECSタスク定義の更新が必要ですが、一度導入すれば大きなコストメリットが得られます。
サービスの成長に伴いログの量も増えていく中で、このようなコスト最適化は大きな価値があります。また、S3に保存したログはGlueとAthenaを使って分析することも可能で、単にコスト削減だけでなく、ログデータの活用範囲も広がります。