AWS ECS上にfluentdクラスタを構築し別のVPCからログを送りつける

この記事は scouty Advent Calendar 2018 の17日目です.

fluentdに関して,勢い余って今年こういう発表をした.

speakerdeck.com

完全に酔っていたので勢いだけなのだが,その後マトモなfluentdクラスタができたので書こうと思う.

もう二度とchefで作ろうとは思うまい.

ログ収集基盤にfluentdを使いたい

例えばAWSに絞るのであれば,別にkinesisを使っても構築は可能だと思う.が,できればfluentdを使いたい.

これについては今回も悩んで,結局fluentdを採用した.

fluentdだとVPCの制約がキツイ?

当初,fluentdよりkinesisの方がいいと思っていた. これには明確な理由があって,VPCによる制約だ.

fluentdは,基本的にVPC内での通信に閉ざしておき,認証等は掛けずに使っていた. ただ,その状態だと,もちろんインターネット上に公開することはできない.なので,VPCをまたいだ通信というのがやりにくくなる. 方法としては,

という手は考えられる.

VPC Peeringを使える状況は限定的

Network Load Balancerが登場して以来,ECSでfluentdをホスティングして,NLBにロードバランスさせるという構成がいいのではないかと思っていた. AWS上でDocker化させるならこれが一番ラクだし,流石に複数台用意してロードバランスしたいだろう.

しかし,internalなNLBにVPC Peering越しに通信するのは不可能だった(かつては!これについては後述).

そのため,この方法でfluentdを運用するのであれば,NLB + ECSという組み合わせを諦めて,単一ホスト + EIPのような構成をとらざるを得なかった.

secure forwardプラグインを使う

それもそれで手段としてはあり.だが,internalな通信だけで済むならそれに越したことはないので,実はあまり検討していない.

というわけでkinesisを考えていた

kinesisであれば,VPCによる制約は受けない.また,kinesisへのアクセス権は完全にIAM Roleで制限できる.

ログを送りつけることだけを考えると最良の手段に見える.

が,ログを集約する側を考えると,ログを増やすたびにlambdaを書くのはだるい. ログの送り先をS3やRedshiftに絞るのであれば,firehoseを使えばいいのだが…….

それにしてもログを増やすたびに,kinesisを増やしたり,subscriberを増やしたりすのは若干手間だ.

エンジニア的にはfluentdがイイ

それに対して,fluentdは,なんかそれっぽい設定を追記するだけでよい

これはかなりでかくて,俺自身はkinesisやfirehoseに慣れているからいいとしても,アプリケーション内にログを仕込むエンジニアは,普段アプリケーションを書いているエンジニアだったりする.そうすると,毎回kinesis増やしてね,とかlambda新しく書いてね,というのは結構なハードルになり得る.

ログなんていらなければ捨てればいいわけで,むしろ大抵の場合は分析したいと思ったときに足らないことの方が多いんだから,気軽にサクサク取れたほうがいいじゃない?

Private Link それだよ!!

と,ここまでは昔の話.

dev.classmethod.jp

AWSのNitro世代インスタンスでは,VPC Peering経由でNLBにアクセスできるようになっている

ちなみにこれは送信側だけの問題なので,fluentdの集約サーバはどんなインスタンスタイプであっても問題ない.

アプリケーションログを送信する側をNitro世代インスタンスにすればいいだけだ.

これがあれば,別VPCのfluentdからログを送りつけて,受け取る側はECS上に構築したfluentdクラスタで,NLBによるロードバランスが可能になる.

というわけでこんな感じになった.

fluentd-cluster

sidecar(送信側)

送信する側はsidecarコンテナとして建てた.

<source>
  @type tail
  path "#{ENV['TD_AGENT_CUSTOM_LOG_PATH']}"
  pos_file /var/tmp/service.log.pos
  format json
  time_key timestamp
  time_format "%Y-%m-%dT%H:%M:%S%z"
  keep_time_key true
  tag "#{ENV['SERVICE_ROLE']}.#{ENV['SERVICE_ENV']}.*"
</source>

<match *.production.**>
  @type forward
  send_timeout 60s
  recover_wait 10s
  hard_timeout 60s

  <server>
    name aggregator
    host "#{ENV['AGGREGATOR_HOST']}"
    port 24224
  </server>

  <secondary>
    @type file
    path /var/log/fluent/forward-failed
  </secondary>
</match>

これを建てておくと,TD_AGENT_CUSTOM_LOG_PATH で指定したパスのログファイルをtailで読んでくれる.

なので,ここのパスをvolume mountして,アプリケーションのログを同じ場所に吐き出しておけば良い.

そうすれば, AGGREGATOR_HOST (NLBのホスト名)にログを転送してくれる.

そういうContainerDefinitionを書く.

[
  {
    "name": "application",
    "image": "nginx:latest",
    "essential": true,
    "mountPoints": [
      {
        "sourceVolume": "service-logs",
        "containerPath": "/app/logs",
        "readOnly": false
      }
    ]
  },
  {
    "name": "fluentd",
    "image": "fluent/fluentd:latest",
    "essential": false,
    "environment": [
      {
        "name": "TD_AGENT_CUSTOM_LOG_PATH",
        "value": "/app/logs/*.log"
      },
      {
        "name": "SERVICE_ROLE",
        "value": "application"
      },
      {
        "name": "SERVICE_ENV",
        "value": "production"
      },
      {
        "name": "AGGREGATOR_HOST",
        "value": "fluentd.aggregator.local"
      }
    ],
    "mountPoints": [
      {
        "sourceVolume": "service-logs",
        "containerPath": "/app/logs",
        "readOnly": false
      }
    ]
  }
]

で,TaskDefinitionを作る.

resource "aws_ecs_task_definition" "service" {
  family                = "${var.service}-${var.role}-${var.env}"
  container_definitions = "${file("./container_definitions/service.json")}"

  volume {
    name = "service-logs"

    docker_volume_configuration {
      scope = "task"
    }
  }

  task_role_arn = "${var.task_role_arn}"
  network_mode  = "bridge"
}

こんな感じにしておくと,アプリケーションが /app/logs/*.log に吐き出したログを,sidecarのfluentdが拾ってくれる.

aggregator(集約側)

集約側は特に気をつけることもない.

<source>
  @type forward
  @id input1
  @label @mainstream
  bind 0.0.0.0
  port 24224
</source>

<filter **>
  @type stdout
</filter>

<label @mainstream>
  <match application.production.hogehoge.**>
    # S3に転送するとか,ログごとの個別処理
    ...

    <format>
      @type json
    </format>

    # instance_profile_credentialsだけでECSのTask Roleも認証してくれる
    <instance_profile_credentials>
    </instance_profile_credentials>

    <buffer tag,time>
      @type file
      path /fluentd/buffer/service/hogehoge
      timekey 120
      timekey_wait 1m
      timekey_use_utc true
      # flush_at_shutdownを入れておかないと,デプロイ時にログが飛ぶ可能性がある
      flush_at_shutdown true
    </buffer>
  </match>
</label>

こいつをECS上に立ち上げて,NLBにぶら下げるだけで良い.

ContainerDefinition

[
  {
    "name": "fluentd",
    "image": "fluent/fluentd:latest",
    "essential": true,
    "portMappings": [
      {
        "protocol": "tcp",
        "hostPort": 0,
        "containerPort": 24224
      }
    ],
    "entryPoin": null,
    "command": null
  }
]

TaskDefinition

resource "aws_ecs_task_definition" "fluentd" {
  family                = "${var.service}-${var.role}-${var.env}"
  container_definitions = "${file("./container_definitions/fluentd.json"}"

  task_role_arn = "${var.task_role_arn}"
  network_mode  = "bridge"
}

まとめ

Nitro世代のPrivate Linkが出てきてようやく,ようやくNLBを本番で使えるようになってきた. NLBはSecurity Groupを設定できない等の理由も相まって,できればinternalで使っていきたいと思っていたんだけど,いかんせんVPC Peeringを超えられないのがネックだった.

h3poteto.hatenablog.com

これが解決したことにより,NLBの活用範囲はもっと増えるんじゃないだろうか.

何はともあれ満足のいくfluentdクラスタを構築できるようになった.