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.confentrypoint.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となっておりログを格納するには物足りないサイズであるため、実際に運用する場合はログのローテーションやガーベージにも気をつけください。

参考