ECS FargateでSidecarのFluentdでログをS3に送る構成をCloudFormationで構築する

awsfluentd

DAEMONを動かすことはできず、 fluentd logdriverもサポートされていないFargateで、 サイドカーとしてFluentdのコンテナを動かしてアプリケーションのログをS3に送る。 全体のコードはGitHubにある。

FargateでECSを使う - sambaiz-net

Kubernetesの1PodでAppとfluentdコンテナを動かしてBigQueryに送る - sambaiz-net

Fluentd

必要なプラグインと設定ファイルを入れたイメージを作る。

FROM fluent/fluentd:v1.4-1

USER root

COPY ./fluent.conf /fluentd/etc/

# install plugin
RUN apk add --update-cache --virtual .build-deps sudo build-base ruby-dev \
    && gem install fluent-plugin-s3 -v 1.0.0 --no-document \
    && gem install uuidtools \
    && gem sources --clear-all \
    && apk del .build-deps \
    && rm -rf /var/cache/apk/* \
    /home/fluent/.gem/ruby/*/cache/*.gem

# set timezone (Alpine)
RUN apk --update-cache add tzdata && \
    cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime && \
    apk del tzdata && \
    rm -rf /var/cache/apk/*

fluent.confはこんな感じ。

<source>
    @type tail
    format json
    path /var/log/test.log
    pos_file /var/log/test.log.pos
    tag test
    <parse>
      @type json
      time_type string
      time_format '%Y-%m-%dT%H:%M:%S%:z'
      keep_time_key true
    </parse>
</source>

<match test>
  @type s3

  s3_bucket my-test-logs-bbbbbb
  s3_region ap-northeast-1
  path test-log/
  time_slice_format ymd=%Y-%m-%d/hour=%-H/
  s3_object_key_format %{path}%{time_slice}%{uuid_flush}.json.%{file_extension}

  <buffer tag,time>
    @type file
    path /var/log/fluent/test
    timekey 60
    timekey_wait 60
    chunk_limit_size 30m
  </buffer>
  <format>
    @type json
  </format>
</match>

pushするとCircleCIでビルドしECRにpushされるようにした。

CircleCI 2.1からのOrbでdocker buildしてECRにpushし、Slackに通知させる - sambaiz-net

workflows:
  build-push:
    jobs:
      - aws-ecr/build_and_push_image:
          name: 'build-latest'
          executor: default
          repo: '${ECR_REPO}'
          tag: latest

Role

ECRからPullできるExecutionRoleとS3にPutできるTaskRoleを作る。

ExecutionRole:
  Type: 'AWS::IAM::Role'
  Properties:
    RoleName: 'test-execution-role'
    AssumeRolePolicyDocument: 
      Version: '2012-10-17'
      Statement: 
        - Effect: 'Allow'
          Principal:
            Service: 'ecs-tasks.amazonaws.com'
          Action: 
            - 'sts:AssumeRole'
    Policies: 
      - PolicyName: 'test-execution-role'
        PolicyDocument: 
          Version: '2012-10-17'
          Statement: 
            - Effect: 'Allow'
              Action:
                - ecr:GetAuthorizationToken
                - ecr:BatchCheckLayerAvailability
                - ecr:GetDownloadUrlForLayer
                - ecr:BatchGetImage
                - logs:CreateLogStream
                - logs:PutLogEvents
              Resource: '*'
TaskRole:
  Type: 'AWS::IAM::Role'
  Properties:
    RoleName: 'test-task-role'
    AssumeRolePolicyDocument: 
      Version: '2012-10-17'
      Statement: 
        - Effect: 'Allow'
          Principal:
            Service: 'ecs-tasks.amazonaws.com'
          Action: 
            - 'sts:AssumeRole'
    Policies: 
      - PolicyName: 'test-task-role'
        PolicyDocument: 
          Version: '2012-10-17'
          Statement: 
            - Effect: 'Allow'
              Action: 's3:ListBucket'
              Resource: !GetAtt Bucket.Arn
            - Effect: 'Allow'
              Action: 
                - 's3:GetObject'
                - 's3:PutObject'
              Resource: !Join [ '/', [ !GetAtt Bucket.Arn, '*' ] ]

TaskDefinition

Fargateで動かすのでNetworkModeはawsvpcになる。K8sのときと同様にvolumeをmountしてファイルを共有する。

TaskDefinition:
  Type: AWS::ECS::TaskDefinition
  Properties: 
    RequiresCompatibilities:
      - 'EC2'
      - 'FARGATE'
    NetworkMode: 'awsvpc'
    ExecutionRoleArn: !Ref ExecutionRole
    TaskRoleArn : !Ref TaskRole
    Cpu: '256'
    Memory: '512'
    ContainerDefinitions: 
      - Name: 'app'
        Image: 'busybox'
        EntryPoint: 
          - 'sh'
          - '-c'
        Command: 
          - 'while true; do echo "{\"foo\":1000,\"time\":\"2019-05-09T20:00:00+09:00\"}" >> /var/log/test.log; sleep 1; done'
        Essential: 'true'
        LogConfiguration:
          LogDriver: 'awslogs'
          Options:
            awslogs-group: !Ref LogGroup
            awslogs-region: 'ap-northeast-1'
            awslogs-stream-prefix: 'app'
        Environment:
          - Name: 'TZ'
            Value: 'Asia/Tokyo'
        MountPoints: 
          - SourceVolume: "varlog"
            ContainerPath: "/var/log"
      - Name: 'fluentd'
        Image: !Join [ '/', [ '<your_account_id>.dkr.ecr.ap-northeast-1.amazonaws.com', !Ref Repository ] ]
        Essential: 'true'
        LogConfiguration:
          LogDriver: 'awslogs'
          Options:
            awslogs-group: !Ref LogGroup
            awslogs-region: 'ap-northeast-1'
            awslogs-stream-prefix: 'fluentd'
        MountPoints: 
          - SourceVolume: "varlog"
            ContainerPath: "/var/log"
    Volumes: 
      - Name: 'varlog'

Service

VPCやサブネットはこのスタックでは作らない。

CloudFormationでVPCを作成してLambdaをデプロイしAurora Serverlessを使う - sambaiz-net

Service:
  Type: AWS::ECS::Service
  Properties:
    Cluster: !Ref Cluster
    LaunchType: FARGATE
    DesiredCount: 1
    TaskDefinition: !Ref TaskDefinition
    NetworkConfiguration:
      AwsvpcConfiguration:
        # If private subnet is placed, set 'DISABLED' to pull images
        AssignPublicIp: 'ENABLED'
        Subnets: !Ref Subnets
    ServiceName: 'test-service'

確認

スタックを上げるとServiceからTaskが動き、S3の正しいパスにログが保存される。 保存されないときはfluent.confとCloudWatchにあるFluentdのログを、ymd=1970-01-01になってしまった場合はtime_formatの内容や場所を、 時間がずれている場合はFluentdコンテナのタイムゾーンが正しいか確認する。

S3に保存されているログ