この記事は公開されてから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
に出力します。
server.go
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
を追加しアプリケーションを起動します。
Dockerfile
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
を作成します。※ログ出力用バケットは別途作成してください。
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
を書き換えます。
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
を追加します。
Dockerfile
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となっておりログを格納するには物足りないサイズであるため、実際に運用する場合はログのローテーションやガーベージにも気をつけください。