티스토리 뷰

개요

사실 로그 파이프라인은 인프라 초기 설계 단계에서 함께 고민했어야 했지만, ECS 기반 인프라를 구성할 당시에는 방법을 몰라 한동안 손대지 못하고 있었다. 그 대신 임시 방편으로 로그를 데이터베이스에 저장하고, 단순히 스택 트레이스를 확인할 수 있게만 만들어둔 상태였다.

 

하지만 로그를 DB에 저장하는 방식은 여러모로 바람직하지 않다.
첫째, 서비스 로직에 필요한 DB 커넥션 풀을 로그 기록 때문에 점유하게 된다.
둘째, 불필요한 DB I/O가 발생해 비용 낭비로 이어진다.
셋째, 로그는 개발자나 운영자를 위한 정보인데, 이를 위해 운영 데이터 저장소의 용량을 소모하게 된다.
그리고 마지막으로, 검색과 필터링도 DB 쿼리로 해야 하기 때문에, 로그 시스템 고유의 장점인 빠른 탐색/집계 기능을 기대하기 어렵다.

 

이런 문제들을 해결하고자, ECS 로그 파이프라인을 제대로 구축해보기로 했다. 인프라 구성은 당연하게도 Terraform을 사용해 작업을 진행했다.

 

전체 구조도

처음에는 Firelens를 써서 로그를 수집하고, 그걸 CloudWatch로 보내는 구조를 생각했었다. 왜 그렇게 구성했냐고 하면, 그냥 막연하게 N개의 서버에서 로그를 수집하려면 로그 수집기가 있어야 한다고 생각했던 것 같다.

 

그리고 그 로그들을 바탕으로 Grafana 인스턴스를 띄워서, 다양한 서비스 관련 지표를 대시보드 형태로 제공하려고도 했었다. 하지만 서비스 지표 수집을 GA(Google Analytics)나 Firebase 같은 도구로 해결하려는 움직임이 있어서, 우리도 가능하면 최소한의 구성으로 최대한의 효과를 낼 수 있는 방향을 고민하게 됐다.

 

조금 찾아보니, ECS의 task definition에 CloudWatch 로그 그룹만 명시하면, 별다른 로그 수집기 없이도 바로 로그를 보낼 수 있다는 걸 알게 됐다. 생각보다 간단했고, 테스트해보니 잘 작동해서 바로 도입했다.

 

Firelens 같은 중간단계들이 제외되면서 전체 구조도가 매우 단순해졌다. 생각해보니 task-definition도 필요하니 이것부터 정리하면 될 것 같다.

 

Task definition

https://docs.aws.amazon.com/ko_kr/AmazonECS/latest/developerguide/specify-log-config.html

{
    "family": "your-task-family-name",
    "networkMode": "awsvpc",
    "containerDefinitions": [
        {
			...
            "logConfiguration": {
                "logDriver": "awslogs",
                "options": {
                    "awslogs-group": "/ecs/polanotes-backend-app-log",
                    "awslogs-region": "<region>",
                    "awslogs-stream-prefix": "backend"
                }
            }
        }
    ],
	...
}

 ECS는 AWS에서 자체제공하는 서비스라 그런지 문서화가 되게 잘 되어있으니, 위 링크 문서를 참고해서 작성하면 된다.

 

Terraform

task-definition에 명시한다고해서, CloudWatch의 로그 그룹이 자동생성되진 않는다. 그래서 로그 그룹부터 만들어줘야한다.

resource "aws_cloudwatch_log_group" "ecs_log_group" {
  name              = "/ecs/polanotes-backend-app-log"
  retention_in_days = 7
}

 

CloudWatch에 너무 많은 데이터가 저장되는 것을 막기 위해 저장기간은 7일로 지정했다. CloudWatch를 사용할 때의 장점 중 하나는 별도의 보안그룹을 지정하지 않아도 되는 것이다.

 

그리고 로그의 장기 저장을 위해 S3를 만든다. S3로 로그의 저장기간을 1년으로 설정했다.

# S3 Bucket for Logs
resource "aws_s3_bucket" "logs" {
  bucket        = "your-log-bucket-name"
  force_destroy = true
}

resource "aws_s3_bucket_lifecycle_configuration" "logs_lifecycle" {
  bucket = aws_s3_bucket.logs.id

  rule {
    id     = "expire-logs-after-1-years"
    status = "Enabled"

    expiration {
      days = 365
    }

    filter {
      prefix = ""
    }
  }
}

 

다음은 로그의 장기 저장을 위해 S3로 보내야하는데, Firehose를 통해 별도 로직 없이 AWS 기능만으로 로그를 S3로 전송할 수 있다. Firehose를 사용할때는 Firehose용 보안 그룹 지정이 필요하다. 

# IAM Role for Firehose
resource "aws_iam_role" "firehose_role" {
  name = "your-firehose-role-name"

  assume_role_policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Effect = "Allow",
        Principal = {
          Service = ["firehose.amazonaws.com", "logs.amazonaws.com"]
        },
        Action = "sts:AssumeRole"
      }
    ]
  })
}

resource "aws_iam_role_policy" "firehose_policy" {
  name = "firehose-s3-access"
  role = aws_iam_role.firehose_role.id

  policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Effect = "Allow",
        Action = [
          "s3:PutObject",
          "s3:PutObjectAcl",
          "s3:PutObjectTagging",
          "s3:GetBucketLocation",
          "s3:ListBucket"
        ],
        Resource = "*"
      },
      {
        Effect = "Allow",
        Action = [
          "firehose:DescribeDeliveryStream",
          "firehose:PutRecord",
          "firehose:PutRecordBatch"
        ],
        Resource = "*"
      },
      {
        Effect = "Allow",
        Action = [
          "logs:PutSubscriptionFilter",
          "logs:DescribeLogGroups",
          "logs:GetLogEvents"
        ],
        Resource = "*"
      }
    ]
  })
}

 

여기서 보안 그룹 지정 시, Firehose는 CloudWatch, S3, 그리고 Firehose의 어떤 기능을 사용할 것인가에 대한 권한이 필요하다. 세번째 권한을 빼먹는 경우가 많으니 잘 지정하다. 이것 때문에 진짜 고생했다.

 

Firehose가 생성됐으면 로그그룹에서 어떤 식으로 S3에 로그 스트림을 보내고, 저장할 것인가를 지정하면 된다.

resource "aws_kinesis_firehose_delivery_stream" "log_stream" {
  name        = "your-firehose-stream-name"
  destination = "extended_s3"

  extended_s3_configuration {
    role_arn           = aws_iam_role.firehose_role.arn
    bucket_arn         = aws_s3_bucket.logs.arn
    prefix             = "logs/"
    compression_format = "UNCOMPRESSED"
    buffering_interval = 60     # seconds
    buffering_size     = 5      # MB

    error_output_prefix = "logs-error/"
  }
}

resource "aws_cloudwatch_log_subscription_filter" "ecs_logs_to_firehose" {
  name            = "log-subscription"
  log_group_name  = "/ecs/your-log-group-name"
  filter_pattern  = ""
  destination_arn = aws_kinesis_firehose_delivery_stream.log_stream.arn
  role_arn        = aws_iam_role.firehose_role.arn
}

스트림을 만들때는 검색의 편의성을 위해 prefix를 설정한다. prefix를 설정하면 반드시 error_output_prefix를 설정해야한다.

 

마치며

구성도는 단순해보였나 내부적으론 설정해줘야 하는게 많았다.. 이게 인프라 관리자의 고충이 아닐까 싶다. 

 

다음은 이제 어플리케이션 레벨에서 로그를 어떻게 남길까를 정리해보려고한다.

 

이전에도 로그 관련 글을 여러번 작성했었는데, 이게 사실상 마지막이 아닐까싶다.

 

MSA 환경으로 간다면 조금 더 깊이 있는 내용이 필요하겠지만, 일단 모놀리스에서는 내가 정리한 방법이 최선이 아닐까싶다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/07   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
글 보관함