🐢

Amazon ECS on FargateでS3とNew Relicに同時にログを送る方法

2023/12/07に公開

はじめに

この記事はNew Relic 使ってみた情報をシェアしよう! by New Relic Advent Calendar 2023
のシリーズ3、7日目のものです😎

クルマ🚖のサブスクの会社でインフラ屋さんをしている@exitjukiです

ECS利用時にS3とNew Relicに同時にアプリケーションログを送りたいケースは多いと思います

色々方法はありますが、今回はNew Relicでのログ/トレース/メトリクスを紐づけることを前提としたNew Relicへのログ送信方法について以下の2つの方法を紹介します

  • New Relic Agent + Fluent Bitを利用した方法
  • Fluent Bit に集約して送信する方法

以下補足です。

  • Java 17を利用
  • New Relic Agentを利用
    • 今回はJava Agentを利用しています

https://docs.newrelic.com/jp/docs/new-relic-solutions/new-relic-one/install-configure/compatibility-requirements-new-relic-agents-products/

  • Fluent BitはECSのサイドカーとして動作

New Relic Agent + Fluent Bitを利用した方法

New Relic Agent + Fluent Bitを利用した方法

New Relic Agentでアプリケーションログ、メトリクス、トレースを送信しつつFluent Bitでアプリケーションの標準出力を拾ってS3とその他に送る方法です
Fluent BitそのもののログはCloudWatch Logsのみに送ってます

注意点としてはNew Relic Agent経由で送信したログとFluent Bitで送信したログの属性が異なることです
New Relic Agent経由のログはNew Relic側で属性をカスタマイズされるためアプリケーション側で付与した属性が削られてしまうことがあります
https://newrelic.com/jp/blog/best-practices/patterns-using-logs-in-context

対策としては後述のFluent Bitに集約する方法もしくはAPM Agentの設定変更になります。Javaの場合は以下のドキュメントを参考にしてください。Agentに手を入れる場合、設定を管理する箇所が一つ多くなってしまうことには注意してください
https://docs.newrelic.com/jp/docs/logs/logs-context/java-configure-logs-context-all/

New Relicではmessage部分のみ確認できればOKということであればこのままの利用でも問題ないです

New Relic Java Agentをインストールする

今回はNew Relicドキュメントを参考にGradleでインストールしました
https://docs.newrelic.com/jp/install/java/?deployment=gradle

  • build.gradleの設定

    • エージェントダウンロード用のプラグインを追加
    plugins {
        id "de.undercouch.download" version "5.3.0"
    }
    
    • エージェントのダウンロードと解凍
     task downloadNewrelic(type: Download) {
         mkdir 'newrelic'
         src 'https://download.newrelic.com/newrelic/java-agent/newrelic-agent/current/newrelic-java.zip'
         dest file('newrelic')
     }
     task unzipNewrelic(type: Copy) {
         from zipTree(file('newrelic/newrelic-java.zip'))
         into rootDir
     }
    
    
    • jibでコンテナ化、最後にエージェントのダウンロード部分との依存関係の設定もしておく
      jib {
          from {
              image = "eclipse-temurin:17-alpine"
          }
          container {
              mainClass = 'com.hoge'
              jvmFlags = [
                  '-javaagent:/app/newrelic/newrelic.jar'
              ]
          }
          extraDirectories {
              paths {
                  path {
                      from = 'newrelic/newrelic'
                      into = '/app/newrelic'
                  }
              }
          }
      }
    
      // New Relic Agentインストールを依存づける
      tasks.jib.dependsOn unzipNewrelic
    

ビルドしたコンテナはECRへアップロードしておきます

https://docs.aws.amazon.com/ja_jp/AmazonECR/latest/userguide/docker-push-ecr-image.html

ECSのタスク定義の設定

コンテナ部分にピックアップします

  • applications部分
        {
            "name": "application",
            "image": "11112222333.dkr.ecr.us-west-2.amazonaws.com/hoge/application:latest",
            "cpu": 128,
            "memoryReservation": 256,
            "portMappings": [
                {"containerPort": 8080, "hostPort": 8080, "protocol": "tcp"},
                {"containerPort": 8081, "hostPort": 8081, "protocol": "tcp"}
            ],
            "essential": true,
            "environment": [
                {"name": "NEW_RELIC_APP_NAME", "value": "hoge-app"}
                {"name": "NEW_RELIC_AGENT_ENABLED", "value": "true"}
            ],
            "secrets": [
                {"name": "NEW_RELIC_LICENSE_KEY", "valueFrom": "arn:aws:ssm:us-west-2:11112222333:parameter/new_relic_license_key"}
            ],
            "mountPoints": [],
            "volumesFrom": [],
            "logConfiguration": {"logDriver": "awsfirelens"}
        },

ライセンスキーはパラメータストアから取得するようにしています。New Relicで使用する環境変数は最小限にしてます。NEW_RELIC_APP_NAMEはNew Relicで表示されるアプリケーション名なので識別しやすい名前がおすすめです

これだけでNew Relicでログ/トレース/メトリクスを取得することができます

Fluent BitでS3にログを送る部分

今回はAWS提供のイメージを使用します
https://gallery.ecr.aws/aws-observability/aws-for-fluent-bit
まずはECSのタスク定義でコンテナを追加します

  • log部分
        {
            "name": "log",
            "image": "public.ecr.aws/aws-observability/aws-for-fluent-bit:init-latest",
            "cpu": 64,
            "memoryReservation": 128,
            "portMappings": [],
            "essential": true,
            "environment": [
                {"name": "LOG_BUCKET_NAME", "value": "hogehoge-application-bucket"},
                {"name": "UPLOAD_TIMEOUT", "value": "1m"},
                {"name": "S3_KEY_FORMAT", "value": "/app/%Y/%m/%d/%H/%M/%S"},
                {"name": "CONTAINER_NAME", "value": "application"},
                {"name": "REGION_NAME", "value": "us-west-2"},
                {
                    "name": "aws_fluent_bit_init_s3_1",
                    "value": "arn:aws:s3:::hogehoge-application-bucket/fluent-bit-custom.conf"
                }
            ],
            "mountPoints": [],
            "volumesFrom": [],
            "user": "0:1337",
            "logConfiguration": {
                "logDriver": "awslogs",
                "options": {
                    "awslogs-group": "/app/log",
                    "awslogs-region": "us-west-2",
                    "awslogs-stream-prefix": "firelens"
                }
            },
            "firelensConfiguration": {
                "type": "fluentbit"
            }
        }

init*の付くイメージを使用することでFluent BitのコンフィグファイルをS3から読み込めるようになるので、それを利用するようにしています
なお、タスクロールでS3へのアクセス権限が必要なのでそこは付与しておく必要があります

{
  "actions": [
    "s3:GetObject",
    "s3:GetBucketLocation"
  ],
  "resources": [
    "arn:aws:s3:::*"
  ]
}

ECSの環境変数もそのまま利用できるため以下のようにS3出力部分を設定します

[OUTPUT]
    Name s3
    Match *
    bucket ${LOG_BUCKET_NAME}
    region ${REGION_NAME}
    upload_timeout ${UPLOAD_TIMEOUT}
    s3_key_format ${S3_KEY_FORMAT}
    compression gzip
    use_put_object true

これでNew Relicにログ/トレース/メトリクスを送りながら、S3にもログを送ることができました

ただしこれではNew RelicとS3とで送られるログに違いが出てきてしまうので少し気持ち悪さが残ります。そのため、次にFluent Bitでまとめて送る方法について紹介します

Fluent Bit に集約して送信する方法

New RelicはECSからNew Relicにログを送るサイドカーコンテナを用意してくれています
https://docs.newrelic.com/jp/docs/logs/forward-logs/aws-firelens-plugin-log-forwarding/
しかしこのコンテナはNew Relic専用であり、S3など他の場所に同時にログを送ることはできません

そのため、
New Relic提供のFluent Bitに設定を追加してS3にもログを送れるようにします。
https://github.com/newrelic/newrelic-fluent-bit-output

New RelicとS3にログを送るFluent Bitの用意

色々やる方法はあるかと思いますが、今回はレポジトリをフォークして直接ビルドして利用することにしました

フォークした後にDockerfileのCOPYでコンフィグを一つ追加します

FROM golang:1.20.5-buster AS builder

WORKDIR /go/src/github.com/newrelic/newrelic-fluent-bit-output

COPY Makefile go.* *.go /go/src/github.com/newrelic/newrelic-fluent-bit-output/
COPY config/ /go/src/github.com/newrelic/newrelic-fluent-bit-output/config
COPY nrclient/ /go/src/github.com/newrelic/newrelic-fluent-bit-output/nrclient
COPY record/ /go/src/github.com/newrelic/newrelic-fluent-bit-output/record
COPY utils/ /go/src/github.com/newrelic/newrelic-fluent-bit-output/utils

ENV SOURCE docker

# Not using default value here due to this: https://github.com/docker/buildx/issues/510
ARG TARGETPLATFORM
ENV TARGETPLATFORM=${TARGETPLATFORM:-linux/amd64}
RUN echo "Building for ${TARGETPLATFORM} architecture"
RUN make ${TARGETPLATFORM}

FROM amazon/aws-for-fluent-bit:latest

# RUN yum update -y \
#     && yum clean all

COPY --from=builder /go/src/github.com/newrelic/newrelic-fluent-bit-output/out_newrelic-linux-*.so /fluent-bit/bin/out_newrelic.so
COPY config/fluent-bit-custom.conf /fluent-bit/etc
COPY config/plugins.conf /fluent-bit/etc

コンフィグの中身ではS3とNew RelicのOUTPUTを設定しています

[SERVICE]
    HTTP_Server  On
    HTTP_Listen  0.0.0.0
    HTTP_PORT    2020

[OUTPUT]
    Name s3
    Match *
    bucket ${LOG_BUCKET_NAME}
    region ${REGION_NAME}
    upload_timeout ${UPLOAD_TIMEOUT}
    s3_key_format ${S3_KEY_FORMAT}
    compression gzip
    use_put_object true

[OUTPUT]
    Name newrelic
    Match *
    licenseKey ${NRIA_LICENSE_KEY}

これをビルドしECRにアップロードしておきます

ECSのタスク定義の編集

このまま起動してしまうと、ログがNew Relic AgentとFluent Bitからの二重送信状態になってしまうため、New Relic Agentからのログ出力を止める必要があります
環境変数でコントロールします

  • applications部分
        {
            "name": "application",
            "image": "11112222333.dkr.ecr.us-west-2.amazonaws.com/hoge/application:latest",
            "cpu": 128,
            "memoryReservation": 256,
            "portMappings": [
                {"containerPort": 8080, "hostPort": 8080, "protocol": "tcp"},
                {"containerPort": 8081, "hostPort": 8081, "protocol": "tcp"}
            ],
            "essential": true,
            "environment": [
                {"name": "NEW_RELIC_APP_NAME", "value": "hoge-app"}
                {"name": "NEW_RELIC_AGENT_ENABLED", "value": "true"}
                {"name": "NEW_RELIC_APPLICATION_LOGGING_ENABLED", "value": "true"}
                {"name": "NEW_RELIC_APPLICATION_LOGGING_LOCAL_DECORATING_ENABLED", "value": "true"}
                {"name": "NEW_RELIC_APPLICATION_LOGGING_FORWARDING_ENABLED", "value": "false"}

            ],
            "secrets": [
                {"name": "NEW_RELIC_LICENSE_KEY", "valueFrom": "arn:aws:ssm:us-west-2:11112222333:parameter/new_relic_license_key"}
            ],
            "mountPoints": [],
            "volumesFrom": [],
            "logConfiguration": {"logDriver": "awsfirelens"}
        },

NEW_RELIC_APPLICATION_LOGGING_LOCAL_DECORATING_ENABLEDではNew Relicの情報をFluent Bitに付与しています
NEW_RELIC_APPLICATION_LOGGING_FORWARDING_ENABLEDでログを止めています

https://docs.newrelic.com/jp/docs/logs/logs-context/java-configure-logs-context-all/

ログ部分についてもNew Relicのライセンスキーの情報が必要になるため変更をします
S3のコンフィグもビルド時に直接指定しているため環境変数から外します

  • log部分
        {
            "name": "log",
            "image": "11112222333.dkr.ecr.us-west-2.amazonaws.com/hoge/log:latest",
            "cpu": 64,
            "memoryReservation": 128,
            "portMappings": [],
            "essential": true,
            "environment": [
                {"name": "LOG_BUCKET_NAME", "value": "hogehoge-application-bucket"},
                {"name": "UPLOAD_TIMEOUT", "value": "1m"},
                {"name": "S3_KEY_FORMAT", "value": "/app/%Y/%m/%d/%H/%M/%S"},
                {"name": "CONTAINER_NAME", "value": "application"},
                {"name": "REGION_NAME", "value": "us-west-2"}
            ],
            "secrets": [
                {"name": "NEW_RELIC_LICENSE_KEY", "valueFrom": "arn:aws:ssm:us-west-2:11112222333:parameter/new_relic_license_key"}
            ],
            "mountPoints": [],
            "volumesFrom": [],
            "user": "0:1337",
            "logConfiguration": {
                "logDriver": "awslogs",
                "options": {
                    "awslogs-group": "/app/log",
                    "awslogs-region": "us-west-2",
                    "awslogs-stream-prefix": "firelens"
                }
            },
            "firelensConfiguration": {
                "type": "fluentbit"
            }
        }

これでFluent Bit経由でS3 + New Relicにログを飛ばせるようになります
トレースも紐づいているはずです

New Relic上でのログの見え方について(小ネタ)

Fluent Bitから送られたログのnewrelic.sourceはapi.logs

New Relic Agentから送られたログのnewrelic.sourceはlogs.APM

可用性を持たせるためにFirehoseの導入

今回はライトな感じの紹介になってしまいましたが、本番ワークロードにおいては以下のような構成がおすすめです

New RelicもS3もKinesis Data Firehoseに対応しているので、ログのバーストなども考慮すると導入したほうがいいとおもいます

https://docs.newrelic.com/jp/docs/logs/forward-logs/stream-logs-using-kinesis-data-firehose/

参考

https://kakakakakku.hatenablog.com/entry/2023/05/29/094701
https://qiita.com/hir00/items/e11b41c47be3c26adff7

Discussion