본문 바로가기

AWS

[ECS] FireLens로 ECS Fargate 컨테이너 로그 ElasticSearch로 보내기

반응형

1. 개요

ECS Fargate를 사용하다보면 Log를 어떻게 보내야할지에 대해 고민하게 된다.

Fargate이기 때문에 별도 서버는 없고, Log는 수집해야 하는데 어떻게 보내야할지에 대해서 막막할 수 있는데 이러한 고민을 덜기 위해 AWS에서는 FireLens라는 기능을 출시하였다.

내부적으로 Fluentd 혹은 FluentBit 이미지를 가지고 AWS가 목적에 맞게 커스터마이징하여 개발한 Container Image라고 생각하면 되며 Task Definition에 Value만 전달하여 Log를 전달할 수도 있다.

좀 더 Custom하게 사용하고 싶다면 S3에 fluent-bit.yml 파일을 올려서 설정 값을 전달할 수도 있고

Image를 Pull하여 Container에 접속하여 설정값을 변경한 뒤 Push하여 그 이미지를 사용할 수도 있고 이 부분은 선택하여 사용하면 된다.

⇒ 이미지를 다시 빌드해서 사용하는 방법도 존재

 

Fluentd: 플러그인 구조를 갖고 있는 로그 라우팅, 정제, 변환, 수집을 담당하는 툴

Fluent Bit: Fluentd의 경량화 버전

 

 

사용법

아래의 3개 문서를 보면 좀 더 수월하게 이해할 수 있다.

 

[1] FireLens GitHub

https://github.com/aws-samples/amazon-ecs-firelens-examples

 

GitHub - aws-samples/amazon-ecs-firelens-examples: Sample logging architectures for FireLens on Amazon ECS and AWS Fargate.

Sample logging architectures for FireLens on Amazon ECS and AWS Fargate. - GitHub - aws-samples/amazon-ecs-firelens-examples: Sample logging architectures for FireLens on Amazon ECS and AWS Fargate.

github.com

 

[2] Docker Log Driver

https://docs.docker.com/config/containers/logging/configure/

 

Configure logging drivers

 

docs.docker.com

 

[3] FluentBit - Out - ElasticSearch

https://docs.fluentbit.io/manual/v/0.12/output/elasticsearch

 

Elasticsearch - Fluent Bit: Official Manual

Some input plugins may generate messages where the field names contains dots, since Elasticsearch 2.0 this is not longer allowed, so the current es plugin replaces them with an underscore, e.g:

docs.fluentbit.io

 

2.  동작 원리

 

"logConfiguration": {     
    "logDriver": "awsfirelens",     
    "options": {         
          "Name": "firehose",         
          "region": "us-west-2",         
          "delivery_stream": "my-stream"     
    } 
}

 

위 설정에서 볼 수 있듯이 Task Definition에서 설정이 가능하다.

Application Container와 FireLens Container는 SideCar 형태로 1개의 Task에서 같이 띄워지며,

Application Container에서 Fluentd Docker Logging Driver를 사용하여 TCP Socket을 맺어 stdout, stderr 로그를 전송하게 된다.

 

FireLens는 Fluentd 혹은 FluentBit 컨테이너로 동작하게 되며 사용자가 기입한 설정값에 의거하여 목적지로 Log를 보내게 된다.

 

 

Fluent 설정

  1. Unix, TCP Socket 연결
  2. ECS Metadata 추가 (이 부분 때문에 어떤 Task에서 발생한 Log인지 확인하기 용이해진다. ⇒ 어떤 Task인지, 어떤 Cluster 인지, 어떤 Container Name인지 등에 대해서 자세히 확인할 수 있다.
  3. 추가적인 사용자 설정
  4. Log sink(목적지) 설정

아래는 일반적인 fluent 설정 파일의 위치이다.

 

Fluentd: /fluentd/etc/fluent.conf

Fluent Bit: /fluent-bit/etc/fluent-bit.conf

 

 

 

https://github.com/aws-samples/amazon-ecs-firelens-examples

 

GitHub - aws-samples/amazon-ecs-firelens-examples: Sample logging architectures for FireLens on Amazon ECS and AWS Fargate.

Sample logging architectures for FireLens on Amazon ECS and AWS Fargate. - GitHub - aws-samples/amazon-ecs-firelens-examples: Sample logging architectures for FireLens on Amazon ECS and AWS Fargate.

github.com

 

AWS Firelens는 다양한 Log 목적지를 지원하는데 sumologic, datadog, newrelic, s3, opensearch, kinesis, .. 등을 지원한다.

필자는 amazon-opensearch로 Log를 보낼 예정이다.

 

 

3. 사용 및 설정 방법

필자는 Terraform으로 ECS Fargate를 구성하였기 때문에 Terraform으로 설명함.

 

우선 Amazon ElasticSearch를 구성한다.

 

ECS Cluster 및 Service를 생성하는 부분은 생략하고 작업 정의 부분만 설명하도록 하겠음

 

 

1. Amazon ElasticSearch 구성

 

주의 사항으로 ES Inbound : 443 Port가 Task로부터 허용되도록 SG를 설정해주어야 한다.

//////////////////////////////////
// Elastic Search Domain
//////////////////////////////////
resource "aws_iam_service_linked_role" "es" {
  aws_service_name = "es.amazonaws.com"
  description      = "AWSServiceRoleForAmazonElasticsearchService Service-Linked Role"
}

resource "aws_elasticsearch_domain" "this" {
  domain_name = "${local.name_prefix}-es-domain"

  elasticsearch_version = local.es.version

  cluster_config {
    instance_count         = local.es.instance_count
    instance_type          = local.es.instance_type
    zone_awareness_enabled = false
  }

  ebs_options {
    ebs_enabled = true
    volume_size = local.es.volume_size
    volume_type = local.es.volume_type
  }

  vpc_options {
    subnet_ids         = [local.es.subnet_ids[0]]
    security_group_ids = [aws_security_group.this.id]
  }

  domain_endpoint_options {
    enforce_https       = true
    tls_security_policy = local.es.custom_endpoint.tls_policy

    custom_endpoint_enabled         = local.es.custom_endpoint.enabled
    custom_endpoint_certificate_arn = local.es.custom_endpoint.enabled ? local.acm_arn : null
    custom_endpoint                 = local.es.custom_endpoint.enabled ? local.es.custom_endpoint.domain_name : null
  }

  access_policies = local.es.access_policies

  tags = merge(
    local.tags,
    {
      Name = "${local.name_prefix}-es-domain"
    }
  )
  depends_on = [aws_iam_service_linked_role.es]
}

 

2. Task

 

[1] Task Policy : es에 대한 Policy를 Task에 설정해주어야 한다.
(참고 : ssmmessages는 ecsexec를 사용하기 위한 policy임)

 

[2] 작업 정의 : Terraform
aws_cloudwatch_log_group 는 firelens container에 대한 Log를 수집하기 위한 cloudwatch log group이다.

 

resource "aws_cloudwatch_log_group" "main" {
  name = "/ecs/${local.env}/service/${local.service_name}"

  retention_in_days = local.cw_retention_in_days

  tags = {
    Name = "${local.name_prefix}-${local.service_name}"
  }
}

resource "aws_ecs_task_definition" "main" {
  family                   = "${local.name_prefix}-${local.service_name}"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  cpu                      = local.task.cpu
  memory                   = local.task.memory
  execution_role_arn       = local.ecs_task_execution_role_arn
  task_role_arn            = local.ecs_task_role_arn

  # Container 정의
  container_definitions = jsonencode(
    [
      # Application Container
      {
        essential = true

        name        = "${local.name_prefix}-${local.service_name}"
        image       = "${local.task.image}:${local.task.tag}"
        environment = local.task.environments
        portMappings = [
          {
            protocol      = "tcp"
            containerPort = local.service_port
            hostPort      = local.service_port
          }
        ]

        # ecsexec 사용을 위해 추가 (Option 이지만, Docs에서 나오므로 추가하였음)
        linuxParameters = {
          initProcessEnabled = true
        },

        logConfiguration : {
          logDriver : "awsfirelens",
          options : {
            Name : "es",
            Host : local.es.host,
            Port : "443",
#            Index : local.service_name,
            Logstash_Prefix : local.service_name
            Logstash_Format : "On",
            Logstash_DateFormat : "%Y-%m-%d",
            Type : local.es.type,
            Aws_Auth : "On",
            Aws_Region : local.region,
            tls : "On"
          }
        }
      },

      # FireLens Container
      {
        essential : true,

        image : "906394416424.dkr.ecr.us-west-2.amazonaws.com/aws-for-fluent-bit:latest",
        name : "log_router",
        firelensConfiguration : {
          "type" : "fluentbit"
        },
        logConfiguration : {
          logDriver : "awslogs",
          options : {
            awslogs-group : "/ecs/${local.env}/service/${local.service_name}/firelens-container",
            awslogs-region : local.region,
            awslogs-create-group : "true",
            awslogs-stream-prefix : "firelens"
          }
        },
        memoryReservation : 50
      }
    ]
  )

  tags = merge(
    {
      Name = "${local.name_prefix}-${local.service_name}"
    },
    local.tags
  )

  lifecycle {
    ignore_changes = [
      container_definitions
    ]
  }

}

 

아래가 LogConfiguration인데, Index는 주석으로 설정하였다.

그 이유로는 ElasticSearch에서 Index 기반으로 ISM(Index State Management)를 설정하기 위해 Logstash_Format을 On으로 하였고,

Logstash_Prefix를 ECS Service Name으로 하여 구분하는 용도로 설정하였다.

 

이렇게 하면 Index Name은 아래와 같이 날짜별로 생성될 것이다.

indexName-2022-05-02

indexName-2022-05-03

..

 

이렇게 설정하면 추후 ISM에 의해 자동 인덱스 삭제 설정이 가능해진다.

(인덱스 이름이 indexName 이라면 오래된 인덱스를 ElasitcSearch의 Delete API를 통해 크론잡으로 삭제해야 할텐데 굳이...?)

        logConfiguration : {
          logDriver : "awsfirelens",
          options : {
            Name : "es",
            Host : local.es.host,
            Port : "443",
#            Index : local.service_name,
            Logstash_Prefix : local.service_name
            Logstash_Format : "On",
            Logstash_DateFormat : "%Y-%m-%d",
            Type : local.es.type,
            Aws_Auth : "On",
            Aws_Region : local.region,
            tls : "On"
          }
        }
      },

 

FireLens 컨테이너는 아래와 같이 설정하였다.

 

type은 fluentbit, fluentd 2가지 설정이 있는데 경량화 버전인 fluentbit을 사용하였다.

 

그리고 log_router란 이름으로 만든 firelens container에 대해서도 log를 수집해야 하기 때문에 awslogs 드라이버를 통해 로그를 보내주었다.

      # FireLens Container
      {
        essential : true,

        image : "906394416424.dkr.ecr.us-west-2.amazonaws.com/aws-for-fluent-bit:latest",
        name : "log_router",
        firelensConfiguration : {
          "type" : "fluentbit"
        },
        logConfiguration : {
          logDriver : "awslogs",
          options : {
            awslogs-group : "/ecs/${local.env}/service/${local.service_name}/firelens-container",
            awslogs-region : local.region,
            awslogs-create-group : "true",
            awslogs-stream-prefix : "firelens"
          }
        },
        memoryReservation : 50
      }

 

이렇게 terraform apply를 수행하면 아래와 같이 container가 배포된다.

 

Logs는 awslogs (cloudwatch log group)을 통해 수집되는 로그를 보여주는데, firelens가 정상적으로 기동된 로그를 확인할 수 있다.

 

Fargate Container이기 때문에 ECSExec를 통해 FireLens Container 내부로 접속해보면 작업 정의에서 설정한 내용들이 OUTPUT 섹션에 설정이 되어 있다.

 

추가로 Record에 어떤 ecs cluster인지, 어떤 task인지 등을 기록해준다.

 

 

Index 패턴을 Log Stash Format에 맞게 생성해주고 Discover로 접속해보면 Log가 정상적으로 전달되는 것을 확인할 수 있다.

 

Index 또한 {Prefix}-년월일 로 생성된 것을 확인할 수 있다.

 

State Management Policy를 통해 오래된 인덱스는 지울 예정이다.

 

[참고]

https://docs.aws.amazon.com/ko_kr/opensearch-service/latest/developerguide/ism.html

 

Amazon OpenSearch Service의 인덱스 상태 관리 - Amazon OpenSearch Service

Amazon OpenSearch Service의 인덱스 상태 관리 Amazon OpenSearch Service에서 인덱스 상태 관리(ISM)를 사용하면 주기적으로 수행되는 작업을 자동화하도록 사용자 지정 관리 정책을 정의하고 해당 정책을 인

docs.aws.amazon.com

 

아래는 예시인데, 본인 환경에 맞게 수정하면 된다.

{
    "policy_id": "delete_old_indexes_30Days",
    "description": "Delete old indexes over 30 days.",
    "default_state": "hot",
    "states": [
        {
            "name": "hot",
            "actions": [],
            "transitions": [
                {
                    "state_name": "delete",
                    "conditions": {
                        "min_index_age": "30d"
                    }
                }
            ]
        },
        {
            "name": "delete",
            "actions": [
                {
                    "delete": {}
                }
            ],
            "transitions": []
        }
    ],
    "ism_template": [
        {
            "index_patterns": [
                "{PREFIX}-*"
            ],
            "priority": 0
        }
    ]
}

 

날짜에 맞게 인덱스가 생성되면 자동으로 ism_template의 index_patterns에 의해 Policy가 적용된다.

아래 Index는 30일 후에 자동 삭제될 것이다.

 

 

이렇게 간단하게 Firelens를 통해 ECS Fargate Container에서 수집되는 로그를 ElasticSearch로 보내는 방법에 대해 알아보았다.

 

 

반응형