Tech Do | メディアドゥの技術ブログ

株式会社メディアドゥのエンジニアによるブログです。

Fargate+FireLens+EFSでログ管理

f:id:ogady:20200805115422j:plain

はじめに

こんにちは、バックエンドエンジニアのogadyです。

みなさん、AWS FargateのEFSマウント使っていますか??

この機能は、2020年4月にリリースされた機能で、これによって冗長化したFargateタスク同士でも共有のファイルストレージ領域を持てるようになりました。

Amazon ECS および AWS Fargate による Amazon EFS ファイルシステムのサポートの一般利用を開始

この機能追加によりFargateでできることがかなり増え、今まで共有ファイルストレージを使う為にECS on EC2を選択するしかなかったところ、晴れてECS Fargateを選択することができるようになった訳です!

この機能を利用して、弊社のオンプレシステムのクラウドリフトに向けてFargate + EFSを使用したログ転送の仕組みを検討しましたので、本ブログで共有しようと思います。

背景

現在メディアドゥでは、オンプレに構築されている電子書籍取次システムをAWS上に移行する計画を進めています。

現行オンプレシステムではアプリケーションログをファイル形式で出力しており、それをログ管理サーバーに転送して保持しています。

今回のクラウドリフトでは、メインの取次システムはECS Fargateで運用する方針となっています。この時、できるだけ既存のシステム改修コストを少なくしたいため、既存のログ出力モジュールをそのままに、SREが用意するログ分析基盤に転送していくような作りを検討してました。

この時、Fargateで動作しているアプリケーションログをコンテナボリューム内にファイル出力する形式とすると、コンテナが破棄されたときにログが欠損してしまうため、EFSマウントしてそこに出力させればいいんじゃないか?

ということで試してみました!

ログ転送の検証

今回はログの出力転送の部分を検証するために、FargateにマウントしたEFSへログを出力します。 EFSに出力されたログの収集・ルーティングにはFireLensを使用し、CloudWatch Logsに転送するフローを検証しました。

※クラウド移行本番では、Kinesisを経由してS3に保存することも検討していますが、今回は検証ということでCloudWatch Logsを使用して検証します。

FireLensとは

Fargateは通常、awslogs driverを使用して、標準出力をCloudWatch Logsに出力します。

FireLensを使用すると、ログ転送先やログフィルターなどをFluentd/Fluent Bitの定義を使用してカスタマイズできます。 この際、提供されているAWS for Fluent Bitイメージだけでなく、自身で定義したFluentd/Fluent Bitイメージも使用できます。

カスタムログルーティング

Fargate(FireLens) + EFSでログ転送してみる

サンプルアプリ

今回はEFSにログファイル出力できればいいので、 サンプルとして、goを使って一定時間でログをファイルに出力するアプリケーションを作成します。

ロギングライブラリはgo.uber.org/zapを使用し、/var/log配下にログファイルを出力するようにしています。

package main

import (
    "fmt"
    "time"

    "go.uber.org/zap"
    "go.uber.org/zap/zapcore"
)

func main() {

    level := zap.NewAtomicLevel()
    level.SetLevel(zapcore.InfoLevel)

    zapConf := zap.Config{
        Level:    level,
        Encoding: "json",
        EncoderConfig: zapcore.EncoderConfig{
            TimeKey:        "Time",
            LevelKey:       "Level",
            NameKey:        "Name",
            CallerKey:      "Caller",
            MessageKey:     "Msg",
            StacktraceKey:  "St",
            EncodeLevel:    zapcore.CapitalLevelEncoder,
            EncodeTime:     zapcore.ISO8601TimeEncoder,
            EncodeDuration: zapcore.StringDurationEncoder,
            EncodeCaller:   zapcore.ShortCallerEncoder,
        },
        OutputPaths:      []string{"/var/log/app.log"},
        ErrorOutputPaths: []string{"/var/log/err.log"},
    }

    logger, err := zapConf.Build()
    if err != nil {
        fmt.Println(err)
    }
    for {
        logger.Info("Hello zap", zap.String("key", "value"), zap.Time("now", time.Now()))
        time.Sleep(time.Second * 30)
    }
}

dockerfile(サンプルアプリ用)

ここは、サンプルアプリ用のdockerfileなのでシンプルです。

FROM golang:1.14

ENV GO111MODULE=on

WORKDIR /go/src
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o main main.go

FROM alpine:3.10
WORKDIR /root/
RUN apk --no-cache add ca-certificates
COPY --from=0 /go/src/main .
ENTRYPOINT ["./main"]

fluent-bit_custom.conf

Fluent Bitの設定ファイルを作成します。 INPUTを以下のように定義することで、Pathのファイルを監視してくれます。(tail -f /usr/local/var/log/app.logのようなイメージですね。)

アウトプット先は、CloudWatch Logsとし、ログフィルタ用のマッチ文字列と、その他出力先のロググループなどを定義するだけです。

マッチ文字列を*にしているため、本番では[FILTER]を設定し、ログのフィルタリング、整形を行う必要があります。

[INPUT]
    Name        tail
    Path        /var/log/app.log

[OUTPUT]
    Name            cloudwatch
    Match           *
    log_key         log
    region          ap-northeast-1
    log_group_name  firelens-container
    log_stream_name firelens

dockerfile(fluent-bitコンテナ用)

Fluent Bit用のDockerコンテナはamazon/aws-for-fluent-bitコンテナをベースに使います。 上で定義したカスタム設定ファイルは/fluent-bit/etc配下に配置します。

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

COPY ./fluent-bit_custom.conf /fluent-bit/etc/fluent-bit_custom.conf

terraform(一部省略)

terraformでEFSとECSを構築します。(IAM Role、VPC、Subnetはあらかじめ作成しています。) タスクロールには、以下の権限を付与しています。

  • elasticfilesystem:ClientMount
  • elasticfilesystem:ClientWrite
  • elasticfilesystem:ClientRootAccess
  • logs:CreateLogStream
  • logs:CreateLogGroup
  • logs:DescribeLogStreams
  • logs:PutLogEvents
# EFS周り
resource "aws_efs_file_system" "efs" {
  creation_token                  = "sample-fargate-efs"
  provisioned_throughput_in_mibps = "50"
  throughput_mode                 = "provisioned"

  tags = {
    Name = "sample-fargate-efs"
  }
}

resource "aws_efs_access_point" "efs_ap" {
  file_system_id = aws_efs_file_system.efs.id
  posix_user {
    gid = 1000
    uid = 1000
  }
  root_directory {
    path = "/log"
    creation_info {
      owner_gid   = 1000
      owner_uid   = 1000
      permissions = 755
    }
  }
}

resource "aws_efs_mount_target" "tg_1a" {
  file_system_id = aws_efs_file_system.efs.id
  subnet_id      = "subnet-XXXXXXXX"
  security_groups = [
    "sg-XXXXXXXXXXXXXX"
  ]
}

### ECS周り
resource "aws_ecs_cluster" "ecs_cluster" {
  name = "sample-cluster"
}

data "template_file" "service_task_definition_json" {
  template = file("./task-definitions/task-definition.json")
}

resource "aws_ecs_task_definition" "ecs_task_definition" {
  family                   = "sample-task"
  memory                   = "1024"
  network_mode             = "awsvpc"
  cpu                      = "512"
  requires_compatibilities = ["FARGATE"]
  container_definitions    = data.template_file.service_task_definition_json.rendered
  task_role_arn            = "arn:aws:iam::XXXXXXXXXXXX:role/ecs-task-role-for-efs"
  execution_role_arn       = "arn:aws:iam::XXXXXXXXXXXX:role/ecsTaskExecutionRole"
  volume {
    name = "sample-fargate-efs"
    efs_volume_configuration {
      file_system_id     = aws_efs_file_system.efs.id
      root_directory     = "/"
      transit_encryption = "ENABLED"
      authorization_config {
        access_point_id = aws_efs_access_point.efs_ap.id
        iam             = "ENABLED"
      }
    }
  }
}

resource "aws_ecs_service" "ecs_service" {
  name             = "sample-service"
  cluster          = aws_ecs_cluster.ecs_cluster.id
  task_definition  = aws_ecs_task_definition.ecs_task_definition.arn
  desired_count    = 1
  launch_type      = "FARGATE"
  platform_version = "1.4.0"

  network_configuration {
    assign_public_ip = true

    subnets = [
      "subnet-XXXXXXXXX"
    ]

    security_groups = [
      "sg-XXXXXXXXXXXXXXX"
    ]
  }
}

現在Fargateのプラットフォームバージョンのlatest1.3.0を指しているので、こちらで1.4.0で明示してあげる必要があります。(2020年7月~9月の間にlatestが1.4.0を指すようになるようです。)

aws_task_ecs_definitionでは、volumeに今回使用するEFSを指定します。

task-definitions

terraform用のタスク定義は以下のようになっています。

上段がFireLensコンテナで、下段がサンプルアプリコンテナのタスク定義です。

FireLens側に、先ほどdockerfile(fluent-bitコンテナ用)で定義したカスタム設定ファイルをオプション項目で指定してます。

サンプルアプリのコンテナでは、log Driverにawsfirelensを指定しています。 一方で、FireLensコンテナはawslogs driverを使用してログ転送を行います。

さらに、両方のコンテナのmountPointsに、今回作成したEFSに/usr/local/var/logをマウントするよう指定しています。

[
    {
        "essential": false,
        "image": "XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/firelens_fluentbit:latest",
        "name": "log_router",
        "portMappings": [],
        "firelensConfiguration": {
            "type": "fluentbit",
            "options": {
                "config-file-type": "file",
                "config-file-value": "/fluent-bit/etc/fluent-bit_custom.conf"
            }
        },
        "logConfiguration": {
            "logDriver": "awslogs",
            "options": {
                "awslogs-group": "firelens-container",
                "awslogs-region": "ap-northeast-1",
                "awslogs-create-group": "true",
                "awslogs-stream-prefix": "firelens"
            }
        },
        "mountPoints": [
            {
                "containerPath": "/var/log",
                "sourceVolume": "sample-fargate-efs"
            }
        ],
        "cpu": 128,
        "memoryReservation": 50
    },
    {
        "essential": true,
        "image": "XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/firelens-sample:latest",
        "name": "app",
        "logConfiguration": {
            "logDriver": "awsfirelens",
            "options": {
                "Name": "cloudwatch",
                "region": "ap-northeast-1",
                "log_group_name": "firelens-testing-fluent-bit",
                "auto_create_group": "true",
                "log_stream_prefix": "from-fluent-bit"
            }
        },
        "mountPoints": [
            {
                "containerPath": "/var/log",
                "sourceVolume": "sample-fargate-efs"
            }
        ],
        "cpu": 128
    }
]

結果確認

CloudWatch Logsを確認してみます。 ログが想定通り出力されているのがわかります。

f:id:ogady:20200730020248p:plain
CloudWatch Logs

EFSのメトリクスも確認します。 ログを継続的に書き込んでいるのがメトリクスから見て取れます。

f:id:ogady:20200730093339p:plain
EFS メトリクス

後書き

いかがだったでしょうか? 実際のクラウド化案件でこのログ収集方法を使用するかは分かりませんが、アーキテクチャの選択肢として持っておくのは有用だと思ったので共有させていただきました!

今回のようなケースからもわかるように、FargateにEFSをマウントできるようになったことで、Fargateでできることの幅がかなり広がりました。 また、最近LambdaにもEFSマウントを使用できるようにもなったので、今までできなかったワークロードがどんどんできるようになってきました!!

今後も、AWSの新機能はどんどん活用していきたいと思います!

弊社ではエンジニア大募集中ですので、もし興味のあるエンジニアがいらっしゃいましたらぜひこちらからお願いいたします。

recruit.mediado.jp