TerraformでECS FargateなコンテナにFireLensを適用する

# はじめに
FargateのログはデフォルトだとCloudWatch Logsに収集される。
このままでも良いのだけど、加工したり転送したりと色々やりたいときに手間にならない方法としてFireLensが使えるらしいので、試してみる。

[以前書いた記事](https://qiita.com/neruneruo/items/d5e58b4013debdf9ebbe)をベースに、以下の記事と同様のことをTerraformで実装する。

【Developers.IO】[[アップデート] ECS/Fargateでログ出力先をカスタマイズできる「FireLens」機能がリリースされました](https://dev.classmethod.jp/articles/ecs-firelens/)

# 事前準備
上記の記事の構成でFireLensを使うのに必要なのは以下。
まずはこれを用意していく。

- ログの書き込み先となるFirehoseの準備
- ECSのタスクロール(Notタスク実行ロール)へのFirehoseへの書き込み権限のアタッチ
- Firehoseに対する書き込み先のS3の書き込み権限のアタッチ

これまでの記事に書いているように、```local```なリソースは名前の定義程度なので、テキトーに良い感じな名前を振ってもらえれば良い。

まずは、以下のような感じでログの書き込み先を作成する。
```s3_configuration```の```cloudwatch_logging_options```は有ってもなくても良い。
エラー時の原因調査にログが必要であればつけておこう(これがないと、本当に何の手掛かりもなくログが出力されないことになる)
エラー時に出力する```log_group_name```は、別のリソースで作っておく。

```HCL:loggroup.tf
################################################################################
# CloudWatch Logs                                                              #
################################################################################
resource "aws_cloudwatch_log_group" "firelenstest"{
  name = "${local.loggroup_name}"
}
```

```HCL:kinesis.tf
################################################################################
# Kinesis Firehose                                                             #
################################################################################
resource "aws_kinesis_firehose_delivery_stream" "firelenstest" {
  name        = "${local.stream_name}"
  destination = "s3"

  s3_configuration {
    role_arn   = "${data.aws_iam_role.firehose.arn}"
    bucket_arn = "${data.aws_s3_bucket.firelenstest.arn}"

    cloudwatch_logging_options {
      enabled         = "true"
      log_group_name  = "${data.aws_cloudwatch_log_group.firelenstest.name}"
      log_stream_name = "kinesis_error"
    }
  }
}
```

次に、ECSのタスクロールにFirehoseへの書き込み権限をアタッチする。
タスク実行ロールではなくてタスクロールの信頼ポリシーの設定なのに、Principalsが```ecs-tasks.amazonaws.com```なのは不思議だが、どうやらこれでOKらしい。

```HCL:iam.tf
################################################################################
# IAM Role for ECS Task                                                        #
################################################################################
resource "aws_iam_role" "ecs_task" {
  name               = "${local.ecs_task_role_name}"
  assume_role_policy = "${data.aws_iam_policy_document.ecs_task_trust.json}"
}

data "aws_iam_policy_document" "ecs_task_trust" {
  statement {
    effect = "Allow"

    actions = [
      "sts:AssumeRole",
    ]

    principals {
      type = "Service"
      identifiers = [
        "ecs-tasks.amazonaws.com",
      ]
    }
  }
}

resource "aws_iam_role_policy_attachment" "ecs_task" {
  role       = "${aws_iam_role.ecs_task.name}"
  policy_arn = "${aws_iam_policy.ecs_task_custom.arn}"
}

resource "aws_iam_policy" "ecs_task_custom" {
  name        = "ecs-task-role-policy"
  description = "ECS Task Role Policy for Firehose"
  policy      = "${data.aws_iam_policy_document.ecs_task_custom.json}"
}

data "aws_iam_policy_document" "ecs_task_custom" {
  version   = "2012-10-17"
  
  statement {
    sid = "FirehosePutRecord"
  
    effect = "Allow"
    actions = [
      "firehose:PutRecordBatch",
    ]

    resources = [
      "*",
    ]
  }
}
```

最後に、Firehoseに対する書き込み先のS3の書き込み権限のアタッチをすれば準備完了だ。
書き込み先のS3もここでリソースを準備してしまおう。
こんかい、エラー時にCloudWatch Logsにも出力するつもりなので、```logs:PutLogEvents```のアクセス権限をFirehoseにアタッチしている。CloudWatch Logsに出さないという強気戦略であれば、不要だろう。
↑の準備とiam.tfで名前が重複しているので、コンカチするか分けるかはお任せする。やりやすい方で良いと思う。

```HCL:s3.tf
################################################################################
# S3 Bucket                                                                    #
################################################################################
resource "aws_s3_bucket" "firelenstest" {
  bucket = "${local.bucket_name}"
  acl    = "private"
}
```

```HCL:iam.tf
################################################################################
# IAM Role for Kinesis Firehose                                                #
################################################################################
resource "aws_iam_role" "firehose" {
  name               = "${local.firehose_role_name}"
  assume_role_policy = "${data.aws_iam_policy_document.firehose_trust.json}"
}

data "aws_iam_policy_document" "firehose_trust" {
  statement {
    effect = "Allow"

    actions = [
      "sts:AssumeRole",
    ]

    principals {
      type = "Service"
      identifiers = [
        "firehose.amazonaws.com",
      ]
    }
  }
}

resource "aws_iam_role_policy_attachment" "firehose" {
  role       = "${aws_iam_role.firehose.name}"
  policy_arn = "${aws_iam_policy.firehose_custom.arn}"
}

resource "aws_iam_policy" "firehose_custom" {
  name        = "firehose-role-policy"
  description = "Firehose Role Policy"
  policy      = "${data.aws_iam_policy_document.firehose_custom.json}"
}

data "aws_iam_policy_document" "firehose_custom" {
  version   = "2012-10-17"
  
  statement {
    sid = "S3Access"
  
    effect = "Allow"
    actions = [
      "s3:AbortMultipartUpload",
      "s3:GetBucketLocation",
      "s3:GetObject",
      "s3:ListBucket",
      "s3:ListBucketMultipartUploads",
      "s3:PutObject"
    ]

    resources = [
      "arn:aws:s3:::${aws_s3_bucket.firelenstest.bucket}",
      "arn:aws:s3:::${aws_s3_bucket.firelenstest.bucket}/*",
    ]
  }
  statement {
    sid = "CloudWatchLogsAccess"
  
    effect = "Allow"
    actions = [
      "logs:PutLogEvents"
    ]

    resources = [
      "${aws_cloudwatch_log_group.firelenstest.arn}"
    ]
  }
}
```

# ECSサービスとタスク定義の修正
ECSサービスとタスク定義は以下の通りに修正する。

ポイントは以下の通り。

- ```aws_ecs_task_definition```にFireLensのコンテナ(Fluentbitのコンテナ)を仕込んでいるので、通常のHTTPサーバとは構成が異なる可能性あり
- コンテナイメージのパスは[公式のAWS Fargate 用ユーザーガイド](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/userguide/using_firelens.html```となって)に書かれている値を設定する。他所のリポジトリから取得することでイメージが未来永劫同じであることの担保がされなくなってしまうため、それを嫌うのであれば、自分のECRリポジトリにコピーを行い、安定板として自分で管理するのが良い。が、その後のメンテ性とは裏返しになるので、注意が必要な部分ではある。

ポイントになるのは以下の部分。
デフォルトでは```awslogs```で定義する部分を以下のように変更する。
```options```で指定できるプロパティは、[AWS Fargate用ユーザーガイド](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/userguide/using_firelens.html)によると、

> Fluentd または Fluent Bit 出力設定の生成に使用されます。

らしい。が、Fluent Bitのドキュメントには言及がない。おそらく[この辺](https://github.com/aws/amazon-kinesis-firehose-for-fluent-bit)が使えるのだろうが、イマイチよく分からないな……。

```HCL
      "logConfiguration": {
        "logDriver": "awsfirelens",
        "options": {
          "Name": "firehose",
          "region": "${data.aws_region.current.name}",
          "delivery_stream": "${aws_kinesis_firehose_delivery_stream.firelenstest.name}"
        }
      }
```

また、以下の部分で、Fluent Bitをサイドカーコンテナとして起動している。
imageのパスは、↑のユーザーガイドに書かれているものを設定する。リージョンごとに違うので注意。Fluent Bitそのものが出力するログは、awslogsでCloudWatch Logsに転送する(自分のストリームにはログは出せないというか、出してしまうと障害解析ができない)。

```HCL
    {
      "name" : "log_router",
      "image": "906394416424.dkr.ecr.ap-northeast-1.amazonaws.com/aws-for-fluent-bit:latest",
      "firelensConfiguration": {
        "type": "fluentbit"
      },
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "${data.aws_cloudwatch_log_group.firelenstest.name}",
          "awslogs-region": "${data.aws_region.current.name}",
          "awslogs-stream-prefix": "ecs"
        }
      }
    }
```

ECSを定義するHCLの全文は以下。
<details><summary>ecs.tf</summary><div>

```HCL:ecs.tf
resource "aws_ecs_cluster" "firelenstest" {
  name = "${local.ecs_cluster_name}"
}

resource "aws_ecs_task_definition" "firelenstest" {
  family                   = "${local.ecs_family_name}"
  task_role_arn            = "${data.aws_iam_role.ecs_task.arn}"
  execution_role_arn       = "${data.aws_iam_role.task_execution.arn}"
  network_mode             = "awsvpc"
  cpu                      = "256"
  memory                   = "1024"
  requires_compatibilities = [
    "FARGATE",
  ]
  container_definitions    = <<EOF
  [
    {
      "name" : "${local.ecs_container_name}",
      "image": "${data.aws_ecr_repository.firelenstest.repository_url}:${local.container_image_tag}",
      "cpu": 0,
      "memoryReservation": 512,
      "portMappings": [
        {
          "containerPort": 9000,
          "hostPort": 9000,
          "protocol": "tcp"
        }
      ],
      "logConfiguration": {
        "logDriver": "awsfirelens",
        "options": {
          "Name": "firehose",
          "region": "${data.aws_region.current.name}",
          "delivery_stream": "${aws_kinesis_firehose_delivery_stream.firelenstest.name}"
        }
      }
    },
    {
      "name" : "log_router",
      "image": "906394416424.dkr.ecr.ap-northeast-1.amazonaws.com/aws-for-fluent-bit:latest",
      "firelensConfiguration": {
        "type": "fluentbit"
      },
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "${data.aws_cloudwatch_log_group.firelenstest.name}",
          "awslogs-region": "${data.aws_region.current.name}",
          "awslogs-stream-prefix": "ecs"
        }
      }
    }
  ]
  EOF
}

resource "aws_ecs_service" "firelenstest" {
  name = "${local.ecs_service_name}"
  cluster = "${aws_ecs_cluster.firelenstest.id}"
  launch_type = "FARGATE"
  task_definition = "${aws_ecs_task_definition.firelenstest.arn}"
  desired_count = 1

  load_balancer {
    target_group_arn = "${data.aws_alb_target_group.firelenstest.arn}"
    container_name   = "${var.prefix}-Container"
    container_port   = 9000
  }

  deployment_controller {
    type = "CODE_DEPLOY"
  }

  network_configuration {
    subnets = flatten(["${data.aws_subnet_ids.my_vpc.ids}",])

    security_groups = [
      "${data.aws_security_group.firelenstest.id}",
    ]

    assign_public_ip = "true"
  }
}
```

</div></details>

# 出力の確認
これを動かすと、S3に
![キャプチャ3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/2cbaf30f-4969-d215-7a37-daf460230977.png)
こんな感じで出力される。

①…時間単位でディレクトリが作られる
②…kinesis.tfで```s3_configuration```のデフォルト設定を適用すると、5MiBまたは5分単位で出力される。

ログ分割に関する時間やバッファリングのチューニングは↑この設定で行える。

ログ出力そのものの設定は、Fluent Bit側の設定になる。詳細は[ユーザーガイド](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/userguide/using_firelens.html)の『カスタム設定ファイルの指定』を参照。