[Terraform]ECS FargateでFireLensを使って複数サービスにログ出力する

FireLensをTerraformでコード化し、検証中のFAQをまとめました!
2021.11.07

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは!コンサル部のinomaso(@inomasosan)です。

ECS FargateでFireLensからCloudWatch LogsやKinesis Data Firehoseへのログ出力を、Terraformでコード化したので紹介します。

検証にあたり、弊社の以下ブログを参考にしました。

そもそもFireLensって何?

FireLensは、複数のAWSサービスやAWSパートナーネットワーク(Datadog等)にログ出力することができます。 ECSタスク定義でサイドカーとして起動し、他のコンテナからログドライバーとして使用します。

コンテナイメージにはFluentdFluent Bitを選択可能です。
今回の検証では、リソース使用率が低く、ログルータに推奨されているFluent Bitを使用します。

2021/11/6時点でFluent Bitでは、以下のAWSサービスにログを出力することができます。

  • Amazon CloudWatch
  • Amazon Kinesis Data Firehose
  • Amazon Kinesis Data Streams
  • Amazon S3

FireLens(Fluent Bit)の新旧プラグインに注意

FireLens(Fluent Bit)のプラグインにはC言語のプラグインと、GO言語のプラグインがあります。
新プラグインはC言語のプラグインの方なので、S3以外の対応するAWSサービスはプラグイン指定に注意が必要です。

プラグイン指定はECSタスク定義か、Fluentd又はFluent Bitの設定ファイルNameに設定します。

新旧プラグインの指定方法の比較表は以下の通りです。

サービス 新プラグイン 旧プラグイン
Amazon CloudWatch cloudwatch_logs cloudwatch
Amazon Kinesis Data Firehose kinesis_firehose firehose
Amazon Kinesis Data Streams kinesis_streams kinesis

Fluent BitのオフィシャルマニュアルにあるAmazon CloudWatchには、以下のような記載があるので、新規構築する場合は基本的に新プラグインを選択して頂くのが良いかと思います。

The Golang plugin was named cloudwatch; this new high performance CloudWatch plugin is called cloudwatch_logs to prevent conflicts/confusion. Check the amazon repo for the Golang plugin for details on the deprecation/migration plan for the original plugin.

構成図

今回はECS Fargateで起動したApache httpdコンテナで検証してみました。
Terraformで構築する全体構成図は以下の通りです。

ログ出力の詳細

ログ出力の詳細な図となります。

各ログ毎の補足事項を、以下にまとめました。

httpd Log

FireLens経由で、CloudWatch LogsやKinesis Data Firehoseへログ出力します。
FireLensのFluent BitアプリにIAM権限が必要なため、ECSタスクロールに権限が必要です。

Fluent Bit Log

FireLensのFluent Bitコンテナ自身のログを、ログ配信の障害切り分けのために取得します。 awslogsログドライバーでCloudWatch Logsへログを送信するため、ECSタスク実行ロールに権限が必要です。

ちなみに今回の検証とは別に、awslogsログドライバーのみ使用する場合は、FireLensは不要となります。

Delivery Error log

Kinesis Data FirehoseからS3へ配信する際のエラーログとなります。
障害切り分けのために、Kinesis Data Firehoseのエラーログ記録を有効化します。

検証環境

今回実行した環境は以下の通りです。

Terraform関連

項目 バージョン
macOS BigSur 11.6
Terraform 1.0.7
AWSプロバイダー 3.63.0

コンテナ

項目 バージョン
httpd latest(2.4.51)
amazon/aws-for-fluent-bit latest(2.21.1)
Fluent Bit 1.8.9
Docker Desktop 4.1.1

ざっくり設計方針

今回コードを書くにあたって、特に意識した方針は以下の通りです。

  • 検証環境のコンテナのイメージタグはlatestタグとする。
  • FireLens用のイメージ作成とECRへプッシュも、Terraformのnull_resourceで実施する。
  • httpd Log用のCloudWatch Logsのログストリーム作成は、Fluent Bitのカスタム設定ファイルで定義し、Fluent Bitから実施できるようにする。
  • IAMロールにアタッチするIAMポリシーはFull権限を避け、必要最低現にする。
    • ただし、Fluent Bitのカスタム設定ファイルによるログストリーム作成については、Resources*で定義し、イメージデプロイ担当者に裁量を持たせる。

フォルダ構成

% tree
.
├── aws_alb.tf
├── aws_cloudwatch.tf
├── aws_ecr.tf
├── aws_ecs.tf
├── aws_iam_ecs.tf
├── aws_iam_kinesis.tf
├── aws_kinesis.tf
├── aws_s3_alblog.tf
├── aws_s3_firelens.tf
├── aws_sg_alb.tf
├── aws_sg_ecs.tf
├── aws_vpc.tf
├── fluentbit
│   ├── Dockerfile
│   └── extra.conf ##Fluent Bitのカスタム設定ファイル
├── httpd
│   └── container_definitions.json ##ECSコンテナ定義
├── outputs.tf
├── provider.tf
└── version.tf

Terrraformコード

各コードは折りたたんで記載してあります。

プロバイダー設定

provider.tf
Terraformで使用するプロバイダー及びリージョンを指定します。

provider "aws" {
  ##東京リージョン
  region  = "ap-northeast-1"
}

バージョン設定

version.tf
Terraform本体のバージョンを固定します。

terraform {
  ## バージョンを固定
  required_version = "1.0.7"
}

VPC

aws_vpc.tf
Terraform RegistryのModuleを使用してVPCを作成します。今回の検証ではALBとECSを同じサブネットに作成します。

# Terraform Registry
# AWS VPC Terraform module
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "3.10.0"

  name = "my-vpc"
  cidr                 = "10.0.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support   = true

  azs             = ["ap-northeast-1a", "ap-northeast-1c"]
  public_subnets  = ["10.0.11.0/24", "10.0.12.0/24"]

  #デフォルトセキュリティグループのルール削除
  manage_default_security_group  = true
  default_security_group_ingress = []
  default_security_group_egress  = []
}

ALB

aws_alb.tf
インターネットからのHTTP通信をECSに振り分けるロードバランサーを作成します。

resource "aws_lb" "alb" {
  name               = "inomaso-dev-alb"
  load_balancer_type = "application"
  internal           = false
  idle_timeout       = 60
  # ALB削除保護無効
  # 本番はtrue推奨
  enable_deletion_protection = false
  security_groups            = [aws_security_group.alb.id]
  subnets                    = module.vpc.public_subnets
}

resource "aws_alb_listener" "alb_http" {
  load_balancer_arn = aws_lb.alb.arn
  port              = "80"
  protocol          = "HTTP"
  default_action {
    target_group_arn = aws_lb_target_group.alb_tg.arn
    type             = "forward"
  }
}

resource "aws_lb_target_group" "alb_tg" {
  name        = "inomaso-dev-alb-tg"
  port        = 80
  protocol    = "HTTP"
  vpc_id      = module.vpc.vpc_id
  target_type = "ip"

  health_check {
    path                = "/"
    port                = "traffic-port"
    protocol            = "HTTP"
    timeout             = 5
    interval            = 10
    healthy_threshold   = 2
    unhealthy_threshold = 2
    matcher             = 200
  }
}
aws_sg_alb.tf
ALB用のセキュリティグループを作成します。インターネットからのHTTP通信を全許可しています。

resource "aws_security_group" "alb" {
  name        = "inomaso-dev-alb-sg"
  description = "inomaso-dev-alb-sg"
  vpc_id      = module.vpc.vpc_id
  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "inomaso-dev-alb-sg"
  }
}
outputs.tf
Terraformのtfstateファイルから、ALBのDNS名をコンソールに出力します。

output "alb_dns_name" {
  description = "ALB DNS Name"
  value       = aws_lb.alb.dns_name
}

ECR関連

fluentbit/Dockerfile
Fluent Bitのカスタム設定ファイルをイメージ内にコピーしています。2021/11/6時点でFargateでホストする場合はカスタム設定ファイルをS3から取得できないので、イメージ作成時に対応する必要があります。

FROM amazon/aws-for-fluent-bit:latest
COPY fluentbit/extra.conf /fluent-bit/etc/extra.conf
fluentbit/extra.conf
Fluent Bitのカスタム設定ファイルで、httpdログの保存先を指定しています。CloudWatch Logsは auto_create_group を true にすることで、コンテナデプロイ時にロググループを作成することができます。

一点注意なのが、この方法でロググループを作成した場合は、Terraformのtfstateファイルの管理対象外となるため、terraform destroyしてもリソースは残り続けます。

[OUTPUT]
    Name   cloudwatch_logs
    Match  *
    region ap-northeast-1
    log_group_name /ecs/firelens/httpd
    log_stream_prefix from-
    auto_create_group true
    log_retention_days 90

[OUTPUT]
    Name   kinesis_firehose
    Match  *
    region ap-northeast-1
    delivery_stream inomaso-dev-kinesis-firehose
aws_ecr.tf
ECRを作成し、Fluent Bitのコンテナイメージをプッシュします。

resource "aws_ecr_repository" "fluentbit" {
  name = "inomaso-dev-ecr-fluentbit"
  ## 同じタグを使用した後続イメージのプッシュによるイメージタグの上書き許可
  image_tag_mutability = "MUTABLE"

  ## プッシュ時のイメージスキャン
  image_scanning_configuration {
    scan_on_push = true
  }
}

# AWSアカウント情報取得
data "aws_caller_identity" "my" {}

# terraform apply時にFluent Bitのコンテナイメージプッシュ
resource "null_resource" "fluentbit" {
  ## 認証トークンを取得し、レジストリに対して Docker クライアントを認証
  provisioner "local-exec" {
    command = "aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin ${data.aws_caller_identity.my.account_id}.dkr.ecr.ap-northeast-1.amazonaws.com"
  }

  ## Dockerイメージ作成
  provisioner "local-exec" {
    command = "docker build -f fluentbit/Dockerfile -t inomaso-dev-fluentbit ."
  }

  ## ECRリポジトリにイメージをプッシュできるように、イメージにタグ付け
  provisioner "local-exec" {
    command = "docker tag inomaso-dev-fluentbit:latest ${aws_ecr_repository.fluentbit.repository_url}:latest"
  }

  ## ECRリポジトリにイメージをプッシュ
  provisioner "local-exec" {
    command = "docker push ${aws_ecr_repository.fluentbit.repository_url}:latest"
  }
}

ECS

aws_ecs.tf
ECS Fargateを作成します。各コードの補足はコード内のコメントに記載しました。

# タスク定義
resource "aws_ecs_task_definition" "task" {
  depends_on = [null_resource.fluentbit]

  family             = "httpd-task"
  task_role_arn      = aws_iam_role.ecs_task.arn
  execution_role_arn = aws_iam_role.ecs_task_exec.arn
  #0.25vCPU
  cpu = "256"
  #0.5GB
  memory                   = "512"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  ## templatefile関数でFluent BitのイメージがプッシュされたECRリポジトリURLを、imageurl変数で引き渡し
  container_definitions = templatefile("httpd/container_definitions.json", {
    imageurl = "${aws_ecr_repository.fluentbit.repository_url}:latest"
  })
}

# クラスター
resource "aws_ecs_cluster" "cluster" {
  name = "httpd-cluster"
}

# サービス
resource "aws_ecs_service" "service" {
  #depends_on = [aws_cloudwatch_log_group.firelens]

  name             = "httpd-service"
  cluster          = aws_ecs_cluster.cluster.arn
  task_definition  = aws_ecs_task_definition.task.arn
  desired_count    = 2
  launch_type      = "FARGATE"
  platform_version = "1.4.0"
  
  health_check_grace_period_seconds = 60

  network_configuration {
    assign_public_ip = true
    security_groups  = [aws_security_group.ecs.id]
    subnets = module.vpc.public_subnets
  }

  ## ALBのターゲットグループに登録する、コンテナ定義のnameとportMappings.containerPortを指定
  load_balancer {
    target_group_arn = aws_lb_target_group.alb_tg.arn
    container_name   = "httpd"
    container_port   = 80
  }

  ## デプロイ毎にタスク定義が更新されるため、リソース初回作成時を除き変更を無視
  lifecycle {
    ignore_changes = [task_definition]
  }
}
httpd/container_definitions.json
Apache httpdとFirelensのコンテナ定義です。Firelensの設定ファイルは、Fluent Bitのイメージ作成時に使用した /fluent-bit/etc/extra.conf を指定しています。

[
  {
    "essential": true,
    "name": "httpd",
    "image": "httpd:latest",
    "portMappings": [
      {
        "protocol": "tcp",
        "containerPort": 80
      }
    ],
    "memoryReservation": 100,
    "logConfiguration": {
      "logDriver": "awsfirelens"
    }
  },
  {
    "essential": true,
    "name": "log_router",
    "image": "${imageurl}",
    "memoryReservation": 50,
    "logConfiguration": {
      "logDriver": "awslogs",
      "options": {
        "awslogs-group": "/ecs/firelens",
        "awslogs-region": "ap-northeast-1",
        "awslogs-stream-prefix": "httpd-sidecar"
      }
    },
    "firelensConfiguration": {
      "type": "fluentbit",
      "options": {
        "config-file-type": "file",
        "config-file-value": "/fluent-bit/etc/extra.conf"
      }
    }
  }
]
aws_sg_ecs.tf
ECSサービス用のセキュリティグループを作成します。ALBからのHTTP通信のみ許可しています。

resource "aws_security_group" "ecs" {
  name        = "inomaso-dev-ecs-sg"
  description = "inomaso-dev-ecs-sg"
  vpc_id      = module.vpc.vpc_id
  ingress {
    from_port       = 80
    to_port         = 80
    protocol        = "tcp"
    security_groups = [aws_security_group.alb.id]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "inomaso-dev-ecs-sg"
  }
}
aws_iam_ecs.tf
ECSタスクロールとECSタスク実行ロールを作成します。ECSタスクロールの権限は最低限の Actions のみ許可しています。ただCloudWatchLogsのロググループを自由に作成できるように Resouces は特に制限していません。

# ECSタスクロール作成
resource "aws_iam_role" "ecs_task" {
  name               = "inomaso-dev-ecs-task-role"
  assume_role_policy = data.aws_iam_policy_document.ecs_task_assume.json
}

# ECSタスクロールを引き受けるための信頼関係を設定
data "aws_iam_policy_document" "ecs_task_assume" {
  statement {
    effect  = "Allow"
    actions = ["sts:AssumeRole"]
    principals {
      type        = "Service"
      identifiers = ["ecs-tasks.amazonaws.com"]
    }
  }
}

# ECSタスクロール用のIAMポリシーを作成し、IAMロールにアタッチ
resource "aws_iam_role_policy" "ecs_task" {
  name   = "inomaso-dev-ecs-task-role-policy"
  role   = aws_iam_role.ecs_task.id
  policy = data.aws_iam_policy_document.ecs_task_access.json
}

# ECSタスクロール用のIAMポリシーJSON
data "aws_iam_policy_document" "ecs_task_access" {
  version = "2012-10-17"

  statement {
    sid = "CloudWatchLogsAccess"

    effect = "Allow"
    actions = [
      "logs:CreateLogStream",
      "logs:CreateLogGroup",
      "logs:DescribeLogStreams",
      "logs:PutLogEvents",
      "logs:PutRetentionPolicy"
    ]
    resources = [
      "*"
    ]
  }
  statement {
    sid = "FirehoseAccess"

    effect = "Allow"
    actions = [
      "firehose:PutRecordBatch"
    ]
    resources = [
      "${aws_kinesis_firehose_delivery_stream.firelens.arn}"
    ]
  }
}

# ECSタスク実行ロール作成
resource "aws_iam_role" "ecs_task_exec" {
  name               = "inomaso-dev-ecs-task-exec-role"
  assume_role_policy = data.aws_iam_policy_document.ecs_task_exec_assume.json
}

# ECSタスク実行ロールを引き受けるための信頼関係を設定
data "aws_iam_policy_document" "ecs_task_exec_assume" {
  statement {
    effect  = "Allow"
    actions = ["sts:AssumeRole"]
    principals {
      type        = "Service"
      identifiers = ["ecs-tasks.amazonaws.com"]
    }
  }
}

# ECSタスク実行ロールに既存IAMポリシーをアタッチ
resource "aws_iam_role_policy_attachment" "ecs_task_exec" {
  role       = aws_iam_role.ecs_task_exec.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}

Kinesis Data Firehose

aws_kinesis.tf
FireLens用のKinesis Data Firehoseを作成します。エラーログ記録の有効化は cloudwatch_logging_options で設定しますが、ロググループとログストリームは自動で作成されないため、事前に作成する必要があります。

resource "aws_kinesis_firehose_delivery_stream" "firelens" {
  name        = "inomaso-dev-kinesis-firehose"
  destination = "s3"

  s3_configuration {
    role_arn           = aws_iam_role.firehose.arn
    bucket_arn         = aws_s3_bucket.firelens.arn
    buffer_size        = 1
    buffer_interval    = 60
    compression_format = "GZIP"

    cloudwatch_logging_options {
      enabled         = "true"
      log_group_name  = aws_cloudwatch_log_group.firelens.name
      log_stream_name = "kinesis_error"
    }
  }
}
aws_iam_kinesis.tf
Kinesis Data Firehoseから、S3バケットとCloudWatch Logsへの配信するためのIAMロールを作成します。IAMロールの権限は最低限の Actions のみ許可しています。また特定のS3バケットとCloudWatch Logsにのみ Actions を許可るすため Resources も制限しています。

# firehose用IAMロール作成
resource "aws_iam_role" "firehose" {
  name               = "inomaso-dev-firehose-role"
  assume_role_policy = data.aws_iam_policy_document.firehose_assume.json
}

# firehose用IAMロールを引き受けるための信頼関係を設定
data "aws_iam_policy_document" "firehose_assume" {
  statement {
    effect  = "Allow"
    actions = ["sts:AssumeRole"]
    principals {
      type        = "Service"
      identifiers = ["firehose.amazonaws.com"]
    }
  }
}

# firehose用IAMロール用のIAMポリシーを作成し、IAMロールにアタッチ
resource "aws_iam_role_policy" "firehose" {
  name   = "inomaso-dev-firehose-role-policy"
  role   = aws_iam_role.firehose.id
  policy = data.aws_iam_policy_document.firehose_access.json
}

# firehose用IAMロールのIAMポリシーJSON
data "aws_iam_policy_document" "firehose_access" {
  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.firelens.bucket}",
      "arn:aws:s3:::${aws_s3_bucket.firelens.bucket}/*",
    ]
  }
  statement {
    sid = "CloudWatchLogsDeliveryErrorLogging"

    effect = "Allow"
    actions = [
      "logs:PutLogEvents"
    ]
    resources = [
      "${aws_cloudwatch_log_group.firelens.arn}:log-stream:*"
    ]
  }
}

CloudWatch Logs

aws_cloudwatch.tf
FireLensとKinesis Data Firehoseの障害切り分け用のロググループを作成します。また、Kinesis Data Firehose用のログストリームは事前に作成する必要があるので、注意が必要です。

resource "aws_cloudwatch_log_group" "firelens" {
  name = "/ecs/firelens"
  #retention_in_days = 90
}

resource "aws_cloudwatch_log_stream" "kinesis" {
  name           = "kinesis_error"
  log_group_name = aws_cloudwatch_log_group.firelens.name
}

S3

aws_s3_firelens.tf
Kinesis Data Firehoseから配信するためのS3バケットを作成します。

resource "aws_s3_bucket" "firelens" {
  ## bucketを指定しないと[terraform-xxxx]というバケット名になる
  bucket = "inomaso-dev-firelens"
  acl    = "private"

  ## S3バケットにオブジェクトがあっても削除
  ## 本番はfalse推奨
  force_destroy = true
  ## SSE-S3で暗号化
  server_side_encryption_configuration {
    rule {
      apply_server_side_encryption_by_default {
        sse_algorithm = "AES256"
      }
    }
  }

  lifecycle_rule {
    id      = "Delete-After-90days"
    enabled = true

    expiration {
      days = 90
    }
  }
}

resource "aws_s3_bucket_public_access_block" "firelens" {
  bucket = aws_s3_bucket.firelens.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

ログ出力の確認

httpd Log

ECSで実行中のタスク数だけ、ログストリームが作成されてることを確認できました。

またALBからのヘルスチェックのリクエストログを確認できました。

{
    "source": "stdout",
    "log": "10.0.12.144 - - [07/Nov/2021:09:14:02 +0000] \"GET / HTTP/1.1\" 200 45",
    "container_id": "5c5c639cc38042649f84d55c5df013dd-3386804179",
    "container_name": "httpd",
    "ecs_cluster": "httpd-cluster",
    "ecs_task_arn": "arn:aws:ecs:ap-northeast-1:[AccountID]:task/httpd-cluster/5c5c639cc38042649f84d55c5df013dd",
    "ecs_task_definition": "httpd-task:72"
}

S3にもログが出力されていることを確認できました。

S3 Selectでログの内容をクエリーしてみました。

{
  "log": "10.0.12.144 - - [07/Nov/2021:09:13:52 +0000] \"GET / HTTP/1.1\" 200 45",
  "container_id": "5c5c639cc38042649f84d55c5df013dd-3386804179",
  "container_name": "httpd",
  "source": "stdout",
  "ecs_cluster": "httpd-cluster",
  "ecs_task_arn": "arn:aws:ecs:ap-northeast-1:[AccountID]:task/httpd-cluster/5c5c639cc38042649f84d55c5df013dd",
  "ecs_task_definition": "httpd-task:72"
}

Fluent Bit Log

httpdのサイドカーとして起動しているので、ECSで実行中のタスク数だけ、ログストリームが作成されてることを確認できました。

ログの内容も確認できました。 infoレベルのログのみなので、特に問題がないことがわかります。

Delivery Error log

FireLensとは直接関係ないですが、Kinesis Data Firehoseのエラーログも見ていきます。 ロググループとログストリームは事前に作成した通りに作成されていることを確認できました。

特に問題なければエラーログは作成されないので、試しにIAMポリシーのresourcesを存在しないS3バケットに変更してみます。

aws_iam_kinesis.tf

data "aws_iam_policy_document" "firehose_access" {
  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.firelens.bucket}:errortest",
      "arn:aws:s3:::${aws_s3_bucket.firelens.bucket}:errortest/*",
    ]
  }
  statement {
    sid = "CloudWatchLogsDeliveryErrorLogging"

    effect = "Allow"
    actions = [
      "logs:PutLogEvents"
    ]
    resources = [
      "${aws_cloudwatch_log_group.firelens.arn}:log-stream:*"
    ]
  }
}

少し時間をまってからエラーログを確認したところ、以下のログが増えていました。

{
    "deliveryStreamARN": "arn:aws:firehose:ap-northeast-1:[AccountID]:deliverystream/inomaso-dev-kinesis-firehose",
    "destination": "arn:aws:s3:::inomaso-dev-firelens",
    "deliveryStreamVersionId": 1,
    "message": "Access was denied. Ensure that the trust policy for the provided IAM role allows Firehose to assume the role, and the access policy allows access to the S3 bucket.",
    "errorCode": "S3.AccessDenied"
}

検証中のFAQ

1. ECSでのログ保存にFireLensは必須なのか?

ECSからのログ出力先がCloudWatch Logsのみで問題なければ、FireLensを無理に使う必要はありません。 ログ出力先にCloudWatch Logs以外も選択したい場合や、ログのフィルタリングをしたい場合にFireLensを検討するのが良いかと思います。

2. FireLens経由でS3バケットへログ保存するのにKinesis Data Firehoseは必要なの?

FireLensのFluent Bitコンテナが予期せぬ停止をしてしまうと、送信頻度によってはログを消失する可能性があります。 Kinesis Data Firehoseを間に挟むことでログのバッファリングが可能なので、高頻度の送信によるニアリアルタイムでログを保管することができます。

3. FireLensとKinesis Data Firehoseの障害切り分け用のロググループも自動作成できなかったの?

awslogsの設定で、LogConfigurationawslogs-create-group: trueを設定すれば可能です。 ただし、タスク実行ロールlogs:CreateLogGroup等の権限が追加で必要となります。 タスク実行ロールのデフォルトIAMポリシーであるAmazonECSTaskExecutionRolePolicyには、上記権限は含まれていません。

今回はタスク実行ロールに余計な権限を与えたくなかったので、Terraformによりロググループを作成することにしました。

4. FireLensの新プラグインだとJSONに順序性がなくなる

プラグインのGithubのissueにもありましたが、JSONはキーの順序付けの概念をサポートしていないようです。 また、Qiitaの記事にも以下のような記載がありました。

JSON に限らずキー、バリューで表現されるデータ形式は順番を保証しないと考えるのがベターようです。

ログの確認はCloudWatch LogsのフィルターやAthenaを使用することになりますが、キーバリューによる検索なので、特に問題はないかと思います。

5. Kinesis Data Firehoseにてエラーログの記録を有効にしたけどログストリームが作成されない

Terraformでエラーログ保存用のロググループまで作成したのですが、ログストリームが自動作成されませんでした。 調べてみたところ弊社のブログに以下の記載があることを見つけました。

Kinesis Data Firehoseにてエラーログの記録を有効にする場合かつ、マネジメントコンソール以外で配信ストリームを作成する場合は、ロググループ、ログストリームを事前に作成する必要があります。

特にログストリームも自動で作成されないのは注意が必要ですね。

まとめ

FireLensの検証はタスク定義方法や、ECSのタスクロールやタスク実行ロールの権限で大分時間がかかってしまいました。 ただ苦労した分、ECSのログ出力についてだいぶ理解することができたので、案件対応にも活かすことができそうです。

この記事が、どなたかのお役に立てば幸いです。それでは!