Fargate + Firelens + FluentBit + Firehose で S3(Firehose) と Datadog にログを送る

Firelens + FluentBit を使って Datadog と S3(Firehose) にログを送る方法です。
今までは CloudWatchLogs, Lambda, StepFuntion を組み合わせて Datadog と S3 にログを送っていたのですが、Datadog にログを送る Lambda がたまにコケるので、Firelens と FluentBit を使ってログを送るようにしました。

## Terraform を使って Fargate で Nginx を動かす

まず、Terraform を使って Fargate で Nginx を動かします。
ファイル全文は [こちら](https://gist.github.com/takoikatakotako/efc667393d451c93698f7410448b2e5a) に上げてあります。

```terraform:main.tf
resource "aws_ecs_cluster" "nginx_cluster" {
  name = "nginx-cluster"
}

resource "aws_ecs_service" "ecs_service" {
  name            = "nginx-service"
  cluster         = aws_ecs_cluster.nginx_cluster.name
  launch_type     = "FARGATE"
  desired_count   = "1"
  task_definition = aws_ecs_task_definition.task_definition.arn

  # ECSタスクへ設定するネットワークの設定
  network_configuration {
    subnets          = [aws_subnet.public_subnet_1a.id, aws_subnet.public_subnet_1c.id, aws_subnet.public_subnet_1d.id]
    security_groups  = [aws_security_group.security_group.id]
    assign_public_ip = true
  }
}

resource "aws_ecs_task_definition" "task_definition" {
  family                   = "nginx-task-definition"
  requires_compatibilities = ["FARGATE"]
  cpu                      = "512"
  memory                   = "1024"
  network_mode             = "awsvpc"
  task_role_arn            = aws_iam_role.ecs_app_role.arn
  execution_role_arn       = aws_iam_role.ecs_app_role.arn

  container_definitions = <<EOL
  [
     {
        "essential":true,
        "image":"nginx:latest",
        "name":"nginx",
        "logConfiguration": {
          "logDriver": "awslogs",
          "secretOptions": null,
          "options": {
            "awslogs-group": "${aws_cloudwatch_log_group.nginx_log_group.name}",
            "awslogs-region": "ap-northeast-1",
            "awslogs-stream-prefix": "ecs"
          }
        },
        "memoryReservation":100,
         "portMappings": [
           {
             "containerPort": 80,
             "hostPort": 80
           }
         ]
     }
  ]
EOL
}
```

タスクのパプリックIPにアクセスし、Nginx の起動画面が表示されることを確認してください。


## FluentBit を使って Nginx のログを Datadog に送る

次に FluentBit を使って Nginx のログを Datadog に送ります。
Datadogでログがパースされるために、Key をデフォルトの `log` から `message` に変える必要があったため、FluentBitのカスタムイメージを作ります。

ファイル全文は [こちら](https://gist.github.com/takoikatakotako/73370c82bc2fdd4fd3de5c0306d74ec6) に上げてあります。

カスタムイメージを入れる ECR Repository を作ります。

```terraform:main.tf
# ...略

##############################################################
# ECR
##############################################################
resource "aws_ecr_repository" "ecr_repository" {
  name                 = "my-fluent-bit"
}
```

FluentBit のカスタムイメージを作成し、ECR にプッシュします。


```bash
$ aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/my-fluent-bit
$ docker build -t my-fluent-bit .
$ docker tag my-fluent-bit:latest ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/my-fluent-bit:latest
$ docker push ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/my-fluent-bit:latest
```

```dockerfile:Dockerfile
FROM amazon/aws-for-fluent-bit:latest
ADD fluent-bit-custom.conf /fluent-bit/etc/fluent-bit-custom.conf
```

```fluent-bit-custom.conf
[FILTER]
    Name modify
    Match *
    Rename log message
```

`main.tf` ファイルに手を加えていきます。

Datadog の APIKey をパラメーターストアに保存します。

```terraform:main.tf
#... 略

###############################################
# Parameter Store
###############################################
resource "aws_ssm_parameter" "ssm_parameter" {
  overwrite   = true
  name        = "DATADOG_API_KEY"
  description = "Datadog API Key"
  type        = "SecureString"
  value       = local.datadog_apikey
}
```


Nginx のロググループは削除し、FluentBit のロググループを作成します。

```terraform:main.tf
##############################################################
# Log Group
##############################################################
resource "aws_cloudwatch_log_group" "fluentbit_log_group" {
  name = "/ecs/fluentbit-log"
}
```

タスク定義を変更します。


```terraform:main.tf
#... 略

resource "aws_ecs_task_definition" "task_definition" {
  family                   = "nginx-task-definition"
  requires_compatibilities = ["FARGATE"]
  cpu                      = "512"
  memory                   = "1024"
  network_mode             = "awsvpc"
  task_role_arn            = aws_iam_role.ecs_app_role.arn
  execution_role_arn       = aws_iam_role.ecs_app_role.arn

  container_definitions = <<EOL
  [
     {
        "essential":true,
        "image":"${aws_ecr_repository.ecr_repository.repository_url}:latest",
        "name":"log_router",
        "logConfiguration": {
          "logDriver": "awslogs",
          "secretOptions": null,
          "options": {
            "awslogs-group": "${aws_cloudwatch_log_group.fluentbit_log_group.name}",
            "awslogs-region": "ap-northeast-1",
            "awslogs-stream-prefix": "ecs"
          }
        },
        "firelensConfiguration":{
           "type":"fluentbit",
           "options":{
              "enable-ecs-log-metadata":"false",
              "config-file-type": "file",
              "config-file-value": "/fluent-bit/etc/fluent-bit-custom.conf"
           }
        },
        "memoryReservation":50
     },
     {
        "essential":true,
        "image":"nginx:latest",
        "name":"nginx",
        "logConfiguration":{
           "logDriver":"awsfirelens",
           "options":{
              "Name":"datadog",
              "Host":"http-intake.logs.datadoghq.com",
              "TLS":"on",
              "dd_service":"my-nginx-service",
              "dd_source":"my-nginx-source",
              "dd_tags":"project:my-nginx",
              "format": "none",
              "provider":"ecs"
           },
           "secretOptions": [{
             "name": "apikey",
             "valueFrom": "${aws_ssm_parameter.ssm_parameter.name}"
            }]
        },
        "memoryReservation":100,
         "portMappings": [
           {
             "containerPort": 80,
             "hostPort": 80
           }
         ]
     }
  ]
EOL
}
```

タスクのパプリックIPにアクセスし、Nginx にアクセスすると Datadog にログが入ることを確認してください。


## FluentBit を使って Nginx のログを Datadog と S3(Firehose) に送る

FluentBit（[amazon/aws-for-fluent-bit](https://github.com/aws/aws-for-fluent-bit)） は様々なプラグインが含まれていますが、直接 S3 に送ることはできません。
しかし、Firehose のプラグイン（[amazon-kinesis-firehose-for-fluent-bit](https://github.com/aws/amazon-kinesis-firehose-for-fluent-bit)）は含まれているため、
Firehose 経由で S3 にログを送ります。

ファイル全文は [こちら](https://gist.github.com/takoikatakotako/10f2043b762a5865bfc860422e9bdcac) に上げてあります。


Firehose 用の IAMロールを作ります。
`resources = ["*"]` としていますが、セキュリティ的に良くないので、環境に合わせて修正してください。

```terraform:main.tf
##############################################################
# IAM
##############################################################
# ... 略
# for Firehose
resource "aws_iam_role" "firehose_role" {
  name               = "firehose-role"
  assume_role_policy = data.aws_iam_policy_document.firehose_role_policy_document.json
}

data "aws_iam_policy_document" "firehose_role_policy_document" {
  statement {
    actions = ["sts:AssumeRole"]
    principals {
      type        = "Service"
      identifiers = ["firehose.amazonaws.com"]
    }
  }
}

resource "aws_iam_policy" "firehose_policy" {
  name   = "firehose-policy"
  policy = data.aws_iam_policy_document.firehose_policy_document.json
}

data "aws_iam_policy_document" "firehose_policy_document" {
  statement {
    effect = "Allow"
    actions = [
      "s3:AbortMultipartUpload",
      "s3:GetBucketLocation",
      "s3:GetObject",
      "s3:ListBucket",
      "s3:ListBucketMultipartUploads",
      "s3:PutObject",
      "kinesis:DescribeStream",
      "kinesis:GetShardIterator",
      "kinesis:GetRecords",
      "kinesis:ListShards",
      "kms:Decrypt"
    ]
    resources = ["*"]
  }
}

resource "aws_iam_role_policy_attachment" "firehose_role_policy_attachment" {
  role       = aws_iam_role.firehose_role.name
  policy_arn = aws_iam_policy.firehose_policy.arn
}
```

Firehose からログが送られる S3バケットを作ります。

```terraform:main.tf
##############################################################
# S3
##############################################################
resource "aws_s3_bucket" "log_bucket" {
  bucket = "takoikatakotako-log-bucket"
  acl    = "private"
}
```

Firehose を作ります。

```terraform:main.tf
##############################################################
# Firehose
##############################################################
resource "aws_kinesis_firehose_delivery_stream" "my-firehose" {
  name        = "my-firehose"
  destination = "extended_s3"

  extended_s3_configuration {
    role_arn   = aws_iam_role.firehose_role.arn
    bucket_arn = aws_s3_bucket.log_bucket.arn
  }
}
```


ECS を動かすロールの権限に `firehose:PutRecordBatch` を追加します。


```terraform:main.tf
##############################################################
# IAM
##############################################################
# for ECS
# 略
resource "aws_iam_policy" "ecs_app_policy" {
  name   = "ecs-policy"
  policy = data.aws_iam_policy_document.ecs_policy_document.json
}

data "aws_iam_policy_document" "ecs_policy_document" {
  statement {
    effect = "Allow"
    actions = [
      "ecr:GetAuthorizationToken",
      "ecr:BatchCheckLayerAvailability",
      "ecr:GetDownloadUrlForLayer",
      "ecr:BatchGetImage",
      "logs:CreateLogStream",
      "logs:PutLogEvents",
      "ssm:GetParameters",
      "secretsmanager:GetSecretValue",
      "firehose:PutRecordBatch",
      "kms:Decrypt"
    ]
    resources = ["*"]
  }
}
```

`fluent-bit-custom.conf` ファイルを修正します。

```fluent-bit-custom.conf
[FILTER]
    Name modify
    Match *
    Rename log message

[OUTPUT]
    Name   firehose
    Match  *
    region ap-northeast-1
    delivery_stream my-firehose
```

`--no-cache` オプションを付けて再ビルドし、ECR にプッシュします。

```
$ docker build -t my-fluent-bit . --no-cache
```

ECS のタスクを再起動し、S3 にログが入ることを確認します。


## おまけ、FluentBit の設定に環境変数を使う

`${ENVIRONMENT}` とすることで、環境変数を設定に反映させることができます。

```fluent-bit-custom.conf
[FILTER]
    Name modify
    Match *
    Rename log message

[OUTPUT]
    Name   firehose
    Match  *
    region ap-northeast-1
    delivery_stream ${DELIVERY_STREAM}
```

タスク定義に環境変数を追加する

```
"environment": [
  {
    "name": "DELIVERY_STREAM",
    "value": "my-firehose"
  }
]
```