ECS 사이드카 패턴으로 Loki에 로그 전송하기 — FireLens, Fluent Bit, Alloy 비교
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: true | ECS 메타데이터(클러스터명, 태스크 정의 등)를 레이블에 자동 추가 |
LabelKeys | Loki 레이블로 사용할 필드 지정 |
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 소켓 마운트는 컨테이너에 높은 권한을 주기 때문에 보안 정책에 따라 허용 여부를 검토해야 한다.
방법별 비교
| 구성 | Fargate | EC2 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_arn과 container_id는 RemoveKeys로 제외하는 것이 기본 설정이다.