Fargateで起動するコンテナのログをFluentd経由でS3に保存してみた
現状、Fargate上のコンテナが利用できるログドライバーはawslogsのみです。つまり、アプリケーションログ(標準出力)の集約先としてはCloudWatch Logsしかありません。
「それはわかっている。でもどうしてもFluentd経由で外部にログを送信したいんだ。。」
そんなことがありました。ということで今回はFargate上で起動するコンテナのログをFluentd経由で外部に送信する方法を紹介したいと思います。
ログ転送イメージ
アプリケーションコンテナおよびFluentdコンテナで共有ボリュームをマウントします。アプリケーションコンテナはそのボリューム上にログを出力し、Fluentdコンテナはそのログ参照します。
ECSの言葉でいうと、1つのTaskにアプリケーションコンテナ、Fluentdコンテナ、およびVolumesを定義し、両コンテナのMountPointsで同一のVolume指定します。
では早速やっていきましょう!
以下のリソースは本記事の本質ではないのでCloudFormationでサクッと構築します。
- ネットワーク
- ECSクラスタ
- ECR
- ALB
コンテナイメージ作成
アプリケーションコンテナイメージの作成
簡単なアプリケーションを作成します。
$ tree . . ├── Dockerfile └── server.go
アプリケーションは8080
ポートでhttpリクエストを受け付け、リクエストの内容を/var/log/development.log
に出力します。
package main import ( "fmt" "log" "net/http" "os" ) func main() { logPath := "/var/log/development.log" httpPort := 8080 openLogFile(logPath) log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "healthy!") }) err := http.ListenAndServe(fmt.Sprintf(":%d", httpPort), logRequest(http.DefaultServeMux)) if err != nil { log.Fatal(err) } } func logRequest(handler http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { log.Printf("%s %s %s\n", r.RemoteAddr, r.Method, r.URL) handler.ServeHTTP(w, r) }) } func openLogFile(logfile string) { if logfile != "" { lf, err := os.OpenFile(logfile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0640) if err != nil { log.Fatal("OpenLogfile: os.OpenFile:", err) } log.SetOutput(lf) } }
アプリケーションコンテナのベースイメージはgolang:alpine
とします。このベースイメージにserver.go
を追加しアプリケーションを起動します。
FROM golang:alpine ADD . /go/src/ EXPOSE 8080 CMD ["/usr/local/go/bin/go", "run", "/go/src/server.go"]
アプリケーションコンテナイメージを作成しECRにPushします。
$ docker build . -t sampleapp $ docker tag sampleapp:latest XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/fargate-fluentd-ecrapp:latest $ docker push XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/fargate-fluentd-ecrapp:latest
Fluentdコンテナイメージの作成
公式リポジトリを参考にFluentdコンテナイメージを作成します。
$ tree . ├── Dockerfile ├── entrypoint.sh └── fluent.conf
アプリケーションコンテナが出力した/var/log/development.log
をtailし、差分をS3に送信する定義ファイルfluent.conf
を作成します。※ログ出力用バケットは別途作成してください。
<source> @type tail path /var/log/development.log pos_file /var/log/development.log.pos read_from_head true tag log.access <parse> @type none </parse> </source> <filter **> @type stdout </filter> <match log.access> @type s3 s3_bucket XXXX-bucket s3_region ap-northeast-1 time_slice_format %Y%m%d%H%M time_slice_wait 1m flush_at_shutdown true </match>
Fluentdはコンテナ上にバッファファイルを出力する必要があります。デフォルトの設定(fluentdユーザー)では書き込み権限がないためrootユーザでFluentdを実行するようにentrypoint.sh
を書き換えます。
#!/usr/bin/dumb-init /bin/sh # If the user has supplied only arguments append them to `fluentd` command if [ "${1#-}" != "$1" ]; then set -- fluentd "$@" fi # If user does not supply config file or plugins, use the default if [ "$1" = "fluentd" ]; then if ! echo $@ | grep ' \-c' ; then set -- "$@" -c /fluentd/etc/fluent.conf fi if ! echo $@ | grep ' \-p' ; then set -- "$@" -p /fluentd/plugins fi fi exec su-exec root "$@"
entrypoint.sh
に実行権限を付与します。
$ chmod +x entrypoint.sh
Fluentdのベースイメージはfluent/fluentd:v0.14.25
とします。このベースイメージにfluent.conf
とentrypoint.sh
を追加します。
FROM fluent/fluentd:v0.14.25 RUN gem install fluent-plugin-s3 -v 1.0.0 --no-document COPY fluent.conf /fluentd/etc/ COPY entrypoint.sh /bin/entrypoint.sh
Fluentdコンテナイメージを作成しECRにPushします。
$ docker build . -t samplefluentd $ docker tag samplefluentd:latest XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/fargate-fluentd-ecrfluentd:latest $ docker push XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/fargate-fluentd-ecrfluentd:latest
Task定義作成
Task定義用のCloudFormationを作成しコンソールより実行します。
このCloudFormationでは以下のリソースを作成します。
- ECSタスク実行ロール(S3へのPut権限を付与)
- Task定義
- アプリケーションコンテナ、Fluentdコンテナ、Volumesを定義
- 両コンテナのMountPointsで同一のVolume指定
AWSTemplateFormatVersion: '2010-09-09' Description: ecs task definition Parameters: projectName: Type: String Default: 'fargate-fluentd' Resources: ecsTaskExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Principal: Service: - "ecs-tasks.amazonaws.com" Action: - "sts:AssumeRole" ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy - arn:aws:iam::aws:policy/AmazonS3FullAccess RoleName: !Sub ${projectName}-ecsTaskExecutionRole Taskdefinition: Type: AWS::ECS::TaskDefinition DependsOn: ecsTaskExecutionRole Properties: Family: !Sub ${projectName}-task RequiresCompatibilities: - FARGATE Cpu: 256 Memory: 512 NetworkMode: awsvpc ExecutionRoleArn: !GetAtt ecsTaskExecutionRole.Arn TaskRoleArn: !GetAtt ecsTaskExecutionRole.Arn ContainerDefinitions: - Name: !Sub ${projectName}-app Image: !Sub ${AWS::AccountId}.dkr.ecr.ap-northeast-1.amazonaws.com/fargate-fluentd-ecrapp:latest PortMappings: - ContainerPort: 8080 LogConfiguration: LogDriver: awslogs Options: awslogs-region: !Ref AWS::Region awslogs-group: !ImportValue appLogGroup awslogs-stream-prefix: !Sub ${projectName}-app MountPoints: - SourceVolume: "my-vol" ContainerPath: "/var/log/" - Name: !Sub ${projectName}-fluentd Image: !Sub ${AWS::AccountId}.dkr.ecr.ap-northeast-1.amazonaws.com/fargate-fluentd-ecrfluentd:latest LogConfiguration: LogDriver: awslogs Options: awslogs-region: !Ref AWS::Region awslogs-group: !ImportValue fluentdLogGroup awslogs-stream-prefix: !Sub ${projectName}-fluentd MountPoints: - SourceVolume: "my-vol" ContainerPath: "/var/log/" Volumes: - Name: "my-vol" Outputs: ecsTaskExecutionRole: Value: !Ref ecsTaskExecutionRole Export: Name: ecsTaskExecutionRole ecsTaskExecutionRoleArn: Value: !GetAtt ecsTaskExecutionRole.Arn Export: Name: ecsTaskExecutionRoleArn taskdefinition: Value: !Ref Taskdefinition Export: Name: frontendTaskdefinition
サービス作成
サービス用のCloudFormationを作成しコンソールより実行します。
サービスは以下のように定義します。
- Task定義を1つ起動
- ALBからのリクエストをアプリケーションコンテナでリッスンする
AWSTemplateFormatVersion: '2010-09-09' Description: internal service front Parameters: projectName: Type: String Default: 'fargate-fluentd' Resources: serviceFrontendSecuritygroup: Type: AWS::EC2::SecurityGroup Properties: GroupName: !Sub ${projectName}-service-front-sg GroupDescription: !Sub ${projectName}-service-front-sg Tags: - Key: Name Value: !Sub ${projectName}-service-front-sg VpcId: !ImportValue vpc SecurityGroupIngress: - IpProtocol: tcp FromPort: '8080' ToPort: '8080' SourceSecurityGroupId: !ImportValue albSecuritygroup frontendService: Type: AWS::ECS::Service Properties: Cluster: !ImportValue cluster DeploymentConfiguration: MaximumPercent: 200 MinimumHealthyPercent: 50 DesiredCount: 1 LaunchType: FARGATE LoadBalancers: - ContainerName: !Sub ${projectName}-app ContainerPort: 8080 TargetGroupArn: !ImportValue frontendTargetGroup NetworkConfiguration: AwsvpcConfiguration: AssignPublicIp: DISABLED SecurityGroups: - !Ref serviceFrontendSecuritygroup Subnets: - !ImportValue protectedSubnet1 - !ImportValue protectedSubnet2 ServiceName: !Sub ${projectName}-service-flontend TaskDefinition: !ImportValue frontendTaskdefinition Outputs: frontendService: Value: !Ref frontendService Export: Name: frontendService serviceFrontendSecuritygroup: Value: !Ref serviceFrontendSecuritygroup Export: Name: serviceFrontendSecuritygroup
サービスが正常にデプロイされるとECSは以下のような状態となります。
ログ確認
Fluentdコンテナの標準出力確認
まずは、Fluentdコンテナの標準出力を確認してみましょう。CloudWatchLogsからfargate-fluentd-fluentd-log
ロググループを選択します。
Fluentdが正常に起動していることが確認できます。
S3のログ確認
では最後にFluentd経由で外部(S3)にログが送信されていることを確認しましょう。ALBではヘルスチェックが定期的に実行されているので何もしなくてもアクセスログは保存されているはずです。
ログが保存されていますね。ファイルの中身も想定通りの結果となっています。
2019-01-22T15:41:06+00:00 log.access {"message":"2019/01/22 15:41:06 server.go:30: 10.0.0.12:29886 GET /"} 2019-01-22T15:41:36+00:00 log.access {"message":"2019/01/22 15:41:36 server.go:30: 10.0.0.12:29906 GET /"}
確認が終わったらCloudFormationのスタックを削除しておきましょう。
まとめ
Fargateで起動するアプリケーションのログをFluentd経由で外部に送信してみました。
コンテナ化したアプリケーションは、基本的にログを標準出力に流すべきだと思います。ですが、「どうしてもFargateでFluentdを利用したい!!ローカルファイルを読み込みたい!!今すぐにだ!!」という場合はこの方式を利用しても良いかもしれません。
共有ディスクは4Gとなっておりログを格納するには物足りないサイズであるため、実際に運用する場合はログのローテーションやガーベージにも気をつけください。