![f:id:ogady:20200805115422j:plain f:id:ogady:20200805115422j:plain](https://cdn-ak.f.st-hatena.com/images/fotolife/o/ogady/20200805/20200805115422.jpg)
はじめに
こんにちは、バックエンドエンジニアの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のプラットフォームバージョンのlatest
は1.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を確認してみます。 ログが想定通り出力されているのがわかります。
EFSのメトリクスも確認します。 ログを継続的に書き込んでいるのがメトリクスから見て取れます。
後書き
いかがだったでしょうか? 実際のクラウド化案件でこのログ収集方法を使用するかは分かりませんが、アーキテクチャの選択肢として持っておくのは有用だと思ったので共有させていただきました!
今回のようなケースからもわかるように、FargateにEFSをマウントできるようになったことで、Fargateでできることの幅がかなり広がりました。 また、最近LambdaにもEFSマウントを使用できるようにもなったので、今までできなかったワークロードがどんどんできるようになってきました!!
今後も、AWSの新機能はどんどん活用していきたいと思います!
弊社ではエンジニア大募集中ですので、もし興味のあるエンジニアがいらっしゃいましたらぜひこちらからお願いいたします。