재하의 개발 블로그
인프라 10 분 소요

ECS 사이드카 패턴으로 Loki에 로그 전송하기 — FireLens, Fluent Bit, Alloy 비교

aws ecs loki fluent-bit firelens grafana logging

ECS에서 컨테이너 로그를 Grafana Loki로 전송하는 방법을 정리한다. Fargate와 EC2 launch type의 구조적 차이, FireLens 사이드카 설정, Grafana Alloy를 파이프라인에 끼워넣는 방법까지 다룬다.

왜 사이드카 패턴인가

ECS는 컨테이너 로그를 stdout/stderr로 출력하면 자동으로 처리해주는 구조다. 기본 로그 드라이버는 awslogs(CloudWatch)지만, 외부 시스템(Loki 등)으로 보내려면 로그 라우터 컨테이너를 같은 Task Definition 안에 함께 띄우는 사이드카 패턴을 쓴다.

ECS는 firelensConfiguration이 선언된 컨테이너가 앱보다 먼저 시작되고 나중에 종료되도록 의존성을 자동으로 관리해준다. 로그 손실을 방지하는 핵심 메커니즘이다.


방법 1: FireLens + Fluent Bit → Loki 직접 전송 (권장)

가장 단순한 구조다. grafana/fluent-bit-plugin-loki 이미지를 사용하면 Loki 플러그인이 사전 설치되어 있어 별도 설정 없이 바로 쓸 수 있다.

Task Definition 구조

{
  "family": "my-app-task",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "512",
  "memory": "1024",
  "executionRoleArn": "arn:aws:iam::<account-id>:role/ecsTaskExecutionRole",
  "containerDefinitions": [
    {
      "name": "log_router",
      "image": "grafana/fluent-bit-plugin-loki:latest",
      "essential": true,
      "firelensConfiguration": {
        "type": "fluentbit",
        "options": {
          "enable-ecs-log-metadata": "true"
        }
      },
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/firelens-log-router",
          "awslogs-region": "ap-northeast-2",
          "awslogs-create-group": "true",
          "awslogs-stream-prefix": "firelens"
        }
      },
      "memoryReservation": 50
    },
    {
      "name": "app",
      "image": "your-app:latest",
      "essential": true,
      "logConfiguration": {
        "logDriver": "awsfirelens",
        "options": {
          "Name": "grafana-loki",
          "Url": "http://your-loki-host:3100/loki/api/v1/push",
          "Labels": "{job=\"firelens\", env=\"prod\"}",
          "LabelKeys": "container_name,ecs_task_definition,ecs_cluster",
          "RemoveKeys": "container_id,ecs_task_arn",
          "LineFormat": "json"
        }
      }
    }
  ]
}

핵심 옵션 설명

옵션설명
enable-ecs-log-metadata: trueECS 메타데이터(클러스터명, 태스크 정의 등)를 레이블에 자동 추가
LabelKeysLoki 레이블로 사용할 필드 지정
RemoveKeys레이블에서 제외할 고카디널리티 필드
LineFormat: json로그 본문을 JSON 형태로 유지

보안 주의사항

FireLens는 포트 24224를 리스닝한다. awsvpc 네트워크 모드를 사용하는 경우 Task에 연결된 Security Group에서 24224 인바운드를 허용하면 안 된다. Task 내부 컨테이너끼리는 localhost로 통신하므로 SG 규칙이 필요 없다.


방법 2: FireLens + Fluent Bit → Alloy → Loki

로그 파이프라인 중간에 변환, 필터링, 레이블 보강, 샘플링 등 처리 로직이 필요할 때 Alloy를 미들웨어로 끼워넣는 구조다. Alloy가 OTLP receiver를 열고, Fluent Bit가 OTel 플러그인으로 Alloy에 포워딩하는 방식이다.

Alloy config.alloy

// Fluent Bit에서 OTLP로 수신
otelcol.receiver.otlp "default" {
  grpc {
    endpoint = "0.0.0.0:4317"
  }
  output {
    logs = [otelcol.processor.batch.default.input]
  }
}
 
// 배치 처리 (네트워크 효율화)
otelcol.processor.batch "default" {
  output {
    logs = [otelcol.exporter.loki.default.input]
  }
}
 
// OTel 로그 → Loki 형식 변환
otelcol.exporter.loki "default" {
  forward_to = [loki.write.default.receiver]
}
 
// Loki로 최종 전송
loki.write "default" {
  endpoint {
    url = "http://your-loki-host:3100/loki/api/v1/push"
  }
  external_labels = {
    env     = "prod",
    cluster = "my-ecs-cluster",
  }
}

Task Definition 구조

{
  "containerDefinitions": [
    {
      "name": "log_router",
      "image": "public.ecr.aws/aws-observability/aws-for-fluent-bit:stable",
      "essential": true,
      "firelensConfiguration": { "type": "fluentbit" },
      "memoryReservation": 50
    },
    {
      "name": "alloy",
      "image": "grafana/alloy:latest",
      "essential": true,
      "command": [
        "run",
        "--server.http.listen-addr=0.0.0.0:12345",
        "/etc/alloy/config.alloy"
      ],
      "portMappings": [
        { "containerPort": 4317, "protocol": "tcp" }
      ],
      "mountPoints": [
        {
          "sourceVolume": "alloy-config",
          "containerPath": "/etc/alloy"
        }
      ],
      "memoryReservation": 128
    },
    {
      "name": "app",
      "image": "your-app:latest",
      "essential": true,
      "logConfiguration": {
        "logDriver": "awsfirelens",
        "options": {
          "Name": "opentelemetry",
          "Host": "localhost",
          "Port": "4317",
          "Tls": "off"
        }
      }
    }
  ]
}

config.alloy 파일은 ECS Volume을 통해 컨테이너에 마운트하거나, 커스텀 Docker 이미지에 포함시키는 방법을 쓴다.


방법 3: Alloy 단독 사이드카 (EC2 launch type 한정)

Fargate에서는 호스트 파일시스템 접근이 불가하므로 Alloy가 다른 컨테이너의 stdout을 직접 읽을 수 없다. EC2 launch type이라면 Docker 소켓을 마운트해서 Alloy가 직접 컨테이너 로그를 수집하는 구조가 가능하다.

config.alloy (EC2 전용)

// Docker 소켓으로 컨테이너 자동 감지
discovery.docker "ecs_containers" {
  host = "unix:///var/run/docker.sock"
}
 
// 감지된 컨테이너에서 로그 수집
loki.source.docker "container_logs" {
  host       = "unix:///var/run/docker.sock"
  targets    = discovery.docker.ecs_containers.targets
  forward_to = [loki.write.default.receiver]
}
 
loki.write "default" {
  endpoint {
    url = "http://your-loki-host:3100/loki/api/v1/push"
  }
}

Task Definition (Docker 소켓 마운트)

{
  "containerDefinitions": [
    {
      "name": "alloy",
      "image": "grafana/alloy:latest",
      "essential": true,
      "mountPoints": [
        {
          "sourceVolume": "docker-sock",
          "containerPath": "/var/run/docker.sock"
        }
      ]
    }
  ],
  "volumes": [
    {
      "name": "docker-sock",
      "host": { "sourcePath": "/var/run/docker.sock" }
    }
  ]
}

Docker 소켓 마운트는 컨테이너에 높은 권한을 주기 때문에 보안 정책에 따라 허용 여부를 검토해야 한다.


방법별 비교

구성FargateEC2 ECS복잡도사용 시점
FireLens + Fluent Bit → Loki낮음로그만 보내면 될 때
FireLens + Fluent Bit → Alloy → Loki중간로그 가공·필터링이 필요할 때
Alloy 단독 (Docker 소켓)중간EC2 기반에서 Alloy만 쓰고 싶을 때

Fargate 환경이라면 Alloy가 직접 stdout을 읽는 구조는 불가능하다. Grafana 공식 문서도 ECS 로그 수집에 FireLens를 프론트로 두는 방식을 권장하고 있다.


Loki 레이블 설계 주의사항

Loki는 레이블 카디널리티가 높아지면 성능이 급격히 나빠진다. ECS 메타데이터를 레이블로 쓸 때 아래 기준을 지키자.

레이블권장 여부이유
ecs_cluster값이 고정
container_name서비스 단위 식별
ecs_task_definition버전 포함이지만 배포 추적에 유용
env값이 고정
ecs_task_arn태스크마다 고유 → 초고카디널리티
container_id컨테이너마다 고유 → 초고카디널리티

ecs_task_arncontainer_idRemoveKeys로 제외하는 것이 기본 설정이다.