
Fargateで起動するコンテナのログをFluentd経由でS3に保存してみた
この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
現状、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となっておりログを格納するには物足りないサイズであるため、実際に運用する場合はログのローテーションやガーベージにも気をつけください。










