DEV Community

Artistrator
Artistrator

Posted on

Terraform으로 AWS Managed Grafana 모니터링 체계 구축하기 — MSA 6개 서비스를 한 눈에

문제: 서비스가 6개인데 모니터링이 없다

MSA로 전환한 뒤 가장 먼저 부딪힌 현실이 있다. "지금 어떤 서비스가 문제인지 모른다."

Gateway, AI 처리, 문서 생성, 데이터 파이프라인, 관리자 대시보드, 레거시 서비스 — 6개 ECS 서비스가 돌아가고, 각각이 Aurora PostgreSQL, Redis, ALB를 공유한다. 장애가 나면 AWS 콘솔에서 CloudWatch → ECS → RDS → ElastiCache를 돌아다니며 원인을 찾아야 했다. 서비스 하나 확인하는 데 5분, 전체 파악에 30분.

"대시보드 하나에서 전부 보고 싶다." 이 단순한 요구가 Grafana 도입의 시작이었다.

왜 AWS Managed Grafana인가

선택지 장점 단점
CloudWatch 대시보드만 추가 비용 없음 커스터마이징 한계, 대시보드 간 이동 불편
자체 호스팅 Grafana 완전한 커스터마이징 서버 관리, 업그레이드, 보안 패치
AWS Managed Grafana 관리 부담 없음 + SSO 연동 월 비용 발생
Datadog/New Relic 강력한 APM 비용이 서비스 수에 비례해서 증가

5명 팀에서 Grafana 서버를 직접 운영하는 건 과하다. AWS Managed Grafana는 SSO 인증, 데이터 소스 연결, 업그레이드를 AWS가 처리하므로 대시보드 설계에만 집중할 수 있다.

결정적으로, CloudWatch와 X-Ray를 네이티브로 지원한다. 별도 에이전트 없이 기존 AWS 메트릭을 그대로 시각화할 수 있다.

Terraform으로 전체 구성하기

모니터링 인프라도 코드로 관리한다. Grafana 워크스페이스 + IAM 역할 + 대시보드 JSON을 Terraform으로 프로비저닝한다.

워크스페이스 + IAM

# Grafana 워크스페이스
resource "aws_grafana_workspace" "main" {
  name                     = "${var.project}-${var.environment}"
  account_access_type      = "CURRENT_ACCOUNT"
  authentication_providers = ["AWS_SSO"]  # IAM Identity Center 연동
  permission_type          = "SERVICE_MANAGED"

  configuration = jsonencode({
    unifiedAlerting = { enabled = true }
    plugins         = { pluginAdminEnabled = false }  # 보안: 플러그인 설치 차단
  })

  data_sources = ["CLOUDWATCH", "XRAY"]
}
Enter fullscreen mode Exit fullscreen mode

Grafana가 AWS 리소스의 메트릭을 읽으려면 IAM 역할이 필요하다. 최소 권한 원칙을 지키면서도 필요한 서비스를 모두 커버해야 한다:

resource "aws_iam_role" "grafana" {
  name = "${var.project}-grafana-role"

  assume_role_policy = jsonencode({
    Statement = [{
      Effect = "Allow"
      Principal = { Service = "grafana.amazonaws.com" }
      Action = "sts:AssumeRole"
      Condition = {
        StringEquals = {
          "aws:SourceAccount" = data.aws_caller_identity.current.account_id
        }
      }
    }]
  })
}

# 읽기 전용 정책 — 메트릭 조회만 허용
resource "aws_iam_role_policy" "grafana_permissions" {
  role = aws_iam_role.grafana.id

  policy = jsonencode({
    Statement = [
      {
        Effect = "Allow"
        Action = [
          # CloudWatch 메트릭
          "cloudwatch:DescribeAlarms",
          "cloudwatch:GetMetricData",
          "cloudwatch:GetMetricStatistics",
          "cloudwatch:ListMetrics",
          # CloudWatch Logs
          "logs:DescribeLogGroups",
          "logs:GetQueryResults",
          "logs:StartQuery",
          "logs:StopQuery",
          # ECS 서비스 상태
          "ecs:ListClusters",
          "ecs:DescribeServices",
          "ecs:ListTasks",
          # RDS + Redis
          "rds:DescribeDBClusters",
          "elasticache:DescribeCacheClusters",
          # ALB
          "elasticloadbalancing:DescribeTargetHealth",
          # X-Ray 트레이싱
          "xray:BatchGetTraces",
          "xray:GetServiceGraph",
          "xray:GetTimeSeriesServiceStatistics",
        ]
        Resource = "*"
      }
    ]
  })
}
Enter fullscreen mode Exit fullscreen mode

SourceAccount 조건으로 다른 AWS 계정에서 이 역할을 사용하는 것을 방지한다. Grafana 서비스 프린시펄만 assume 가능하다.

대시보드 설계 — 7개 대시보드 체계

대시보드를 용도별로 나눴다. "하나의 대시보드에 전부 넣기" 유혹을 이기고, 관심사 분리를 적용했다:

📊 대시보드 체계
├── API Overview          # 전체 서비스 한 눈에 (첫 화면)
├── ALB Traffic           # 트래픽 패턴 + 에러율
├── ECS Metrics           # 서비스별 CPU/메모리 상세
├── Aurora DB             # 데이터베이스 성능
├── Redis Cache           # 캐시 히트율 + 메모리
├── EC2 Infrastructure    # 인스턴스 레벨 메트릭
└── ECS Logs Explorer     # 로그 검색 + 에러 추적
Enter fullscreen mode Exit fullscreen mode

장애 시 동선: API Overview에서 이상 감지 → 해당 영역 대시보드로 드릴다운 → 5분 내 원인 파악.

API Overview — 운영의 첫 화면

가장 중요한 대시보드다. 한 화면에서 전체 시스템의 건강 상태를 파악한다.

// 서비스 상태 카드  떠있는지 죽었는지 즉시 확인
{
  "type": "stat",
  "title": "Gateway",
  "fieldConfig": {
    "defaults": {
      "thresholds": {
        "steps": [
          { "color": "red", "value": null },   // 0 = DOWN
          { "color": "green", "value": 1 }      // 1+ = UP
        ]
      }
    }
  },
  "targets": [{
    "namespace": "ECS/ContainerInsights",
    "metricName": "RunningTaskCount",
    "dimensions": {
      "ClusterName": "my-cluster",
      "ServiceName": "gateway"
    },
    "period": "60",
    "stat": "Average"
  }]
}
Enter fullscreen mode Exit fullscreen mode

6개 서비스의 상태 카드가 한 줄에 나란히 표시된다. 빨간색이 보이면 즉시 대응할 수 있다.

그 아래로:

  • 총 요청 수 (ALB RequestCount, 1분 합계)
  • 응답 시간 (Average + p99, 0.5초 노란색 / 1초 빨간색 임계치)
  • 서비스별 CPU/메모리 (8시간 트렌드, 70% 주의 / 90% 위험)
  • 인프라 요약 (Aurora CPU, DB 커넥션 수, Redis 메모리, 5xx 에러)

ALB Traffic — 트래픽 패턴 읽기

// 5xx 에러   차트로 스파이크 즉시 감지
{
  "type": "barchart",
  "title": "5XX Errors",
  "fieldConfig": {
    "defaults": {
      "color": { "fixedColor": "red", "mode": "fixed" }
    }
  },
  "targets": [{
    "namespace": "AWS/ApplicationELB",
    "metricName": "HTTPCode_ELB_5XX_Count",
    "stat": "Sum",
    "period": "60"
  }]
}
Enter fullscreen mode Exit fullscreen mode

트래픽 대시보드에서 가장 중요한 건 5xx 에러 바 차트다. 타임시리즈보다 바 차트가 스파이크를 잡아내기 좋다. 평소에 막대가 없다가 빨간 막대가 하나라도 나타나면 즉시 눈에 띈다.

서비스별 Healthy Host Count 카드도 배치했다. 배포 중에 한 서비스의 호스트가 0이 되면 즉시 알 수 있다.

Aurora DB — 데이터베이스 병목 추적

// Read/Write Latency 분리  어디서 느린지 파악
{
  "title": "Read / Write Latency",
  "targets": [
    {
      "metricName": "ReadLatency",
      "label": "Read",
      "stat": "Average",
      "period": "60"
    },
    {
      "metricName": "WriteLatency",
      "label": "Write",
      "stat": "Average",
      "period": "60"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

DB 대시보드의 핵심은 Read/Write Latency 분리커넥션 수 추적이다. 멀티테넌트 환경에서 테넌트가 추가될 때마다 커넥션이 늘어나는데, 80개 임계치에 접근하면 알람이 울린다.

Redis Cache — 캐시 효율 모니터링

// 캐시 히트율 계산  Grafana 표현식 활용
{
  "title": "Cache Hit Rate",
  "targets": [
    { "id": "hits", "metricName": "CacheHits", "stat": "Sum" },
    { "id": "misses", "metricName": "CacheMisses", "stat": "Sum" }
  ],
  "expression": "IF(hits + misses > 0, hits / (hits + misses) * 100, 0)"
}
Enter fullscreen mode Exit fullscreen mode

CloudWatch에는 "캐시 히트율" 메트릭이 없다. Hits와 Misses를 가져와서 Grafana Math Expression으로 직접 계산한다. 0 나누기 방지를 위한 IF 조건도 포함.

히트율이 90% 이하로 떨어지면 캐시 키 전략을 재검토해야 한다는 신호다.

ECS Logs Explorer — 에러 추적의 핵심

이 대시보드가 장애 대응에서 가장 많이 쓰인다. CloudWatch Logs Insights 쿼리를 Grafana 패널에 내장했다:

// 5분 단위 에러 카운트 — 스파이크 감지
fields @timestamp, @message
| filter @message like /(?i)(error|exception|fail)/
| stats count() as error_count by bin(5m)
Enter fullscreen mode Exit fullscreen mode
// 최근 에러 로그 100건 — 원인 파악
fields @timestamp, @message, @logStream
| filter @message like /(?i)(error|exception|fail|critical)/
| sort @timestamp desc
| limit 100
Enter fullscreen mode Exit fullscreen mode

서비스 선택 드롭다운키워드 검색 변수를 추가해서, 특정 서비스의 특정 에러를 바로 필터링할 수 있다:

{
  "templating": {
    "list": [
      {
        "name": "service",
        "type": "custom",
        "options": [
          { "text": "Gateway", "value": "/ecs/my-project-gateway" },
          { "text": "Service A", "value": "/ecs/my-project-service-a" },
          { "text": "Service B", "value": "/ecs/my-project-service-b" }
        ]
      },
      {
        "name": "search_keyword",
        "type": "textbox",
        "current": { "text": "", "value": "" }
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

CloudWatch 알람 + Slack 연동

Grafana 대시보드는 사후 분석에 강하다. 사전 감지는 CloudWatch Alarm이 담당한다. 15개 이상의 알람을 Terraform으로 관리한다:

# ECS 서비스별 CPU 알람 (동적 생성)
resource "aws_cloudwatch_metric_alarm" "ecs_cpu" {
  for_each = var.services  # 6개 서비스 자동 생성

  alarm_name          = "${each.key}-high-cpu"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = 2
  metric_name         = "CPUUtilization"
  namespace           = "AWS/ECS"
  period              = 300      # 5분
  statistic           = "Average"
  threshold           = 75       # 75% 초과 시
  treat_missing_data  = "notBreaching"

  dimensions = {
    ClusterName = var.cluster_name
    ServiceName = each.value.name
  }

  alarm_actions = [var.sns_topic_arn]  # Slack 알림
  ok_actions    = [var.sns_topic_arn]  # 복구 알림도 전송
}

# RDS 커넥션 수 알람
resource "aws_cloudwatch_metric_alarm" "rds_connections" {
  alarm_name = "rds-high-connections"
  threshold  = 80  # 멀티테넌트 환경에서 커넥션 폭증 감지
  # ...
}

# Redis 메모리 알람
resource "aws_cloudwatch_metric_alarm" "redis_memory" {
  alarm_name = "redis-high-memory"
  threshold  = 70  # 70% 초과 시 경고
  # ...
}
Enter fullscreen mode Exit fullscreen mode

핵심 설계 결정:

  • for_each로 서비스 추가 시 알람 자동 생성. 수동으로 알람을 만들면 반드시 빠뜨린다.
  • ok_actions에도 SNS를 연결해서 복구 알림도 받는다. "장애 발생" 알림만 오고 "복구됨" 알림이 없으면 불안하다.
  • treat_missing_data = "notBreaching"으로 데이터 없음을 정상으로 취급. 배포 중 메트릭이 잠시 빈다고 알람이 울리면 피로도만 높아진다.

알람 → SNS → Lambda → Slack Webhook 흐름으로 Slack 채널에 실시간 알림이 온다.

로그 분리 라우팅 — 에러만 따로 모으기

6개 서비스의 전체 로그를 뒤지는 건 비효율적이다. 에러 로그만 별도 그룹으로 분기하는 로그 라우터를 구성했다:

# 서비스별 일반 로그 + 에러 전용 로그
resource "aws_cloudwatch_log_group" "service" {
  for_each          = var.services
  name              = "/ecs/${each.key}"
  retention_in_days = 30
}

resource "aws_cloudwatch_log_group" "error" {
  for_each          = var.services
  name              = "/${var.project}/${each.key}/error"
  retention_in_days = 30
}

# error/warn 레벨만 에러 그룹으로 분기
resource "aws_cloudwatch_log_subscription_filter" "error_filter" {
  for_each       = var.services
  name           = "${each.key}-error-filter"
  log_group_name = "/ecs/${each.key}"
  filter_pattern = "?ERROR ?WARN ?error ?warn"
  destination_arn = aws_lambda_function.log_router.arn
}
Enter fullscreen mode Exit fullscreen mode

효과: Grafana Logs Explorer에서 에러 전용 로그 그룹을 선택하면 노이즈 없이 에러만 볼 수 있다. 장애 시 원인 파악 시간이 체감상 절반으로 줄었다.

아쉬웠던 점

  • 대시보드 JSON 관리가 번거롭다. Grafana UI에서 대시보드를 수정한 뒤 JSON을 export해서 Terraform에 반영하는 과정이 수동이다. Grafana API + CI로 자동화하고 싶지만 아직 못 했다.
  • CloudWatch 메트릭의 1분 해상도 한계. 순간적인 스파이크는 1분 평균에 묻힌다. 커스텀 메트릭으로 초 단위 데이터를 보내면 비용이 급증한다.
  • 서비스 간 호출 추적이 부족하다. X-Ray 데이터 소스를 연결해놨지만, NestJS TCP 통신에 X-Ray를 제대로 붙이려면 커스텀 미들웨어가 필요하다. 현재는 서비스별 메트릭만 볼 수 있고 호출 체인을 시각화하진 못한다.
  • 알람 피로도. 초기에 임계치를 낮게 잡았더니 하루에 알람이 10개씩 왔다. 임계치를 올리니 진짜 장애를 놓칠까 불안하다. 적정선을 찾는 게 예상보다 어렵다.

향후 보완할 점

  • Grafana Dashboard as Code 자동화: 대시보드 변경을 Grafana API로 export → Terraform JSON 자동 동기화
  • OpenTelemetry 연동: AsyncLocalStorage 기반 분산 트레이싱을 OTEL로 전환, Grafana Tempo와 연결하여 서비스 간 호출 체인 시각화
  • 비즈니스 메트릭 대시보드 추가: 인프라 메트릭 외에 "분당 처리 건수", "AI 추론 성공률" 같은 비즈니스 KPI 패널
  • 알람 정책 고도화: 정적 임계치 → 이상 탐지(Anomaly Detection) 기반 동적 알람. CloudWatch Anomaly Detection을 활용하면 "평소 대비 비정상" 패턴을 자동 감지할 수 있다

배운 점

  • 대시보드는 관심사별로 분리해야 한다. 하나에 다 넣으면 스크롤 지옥이 된다. Overview → 드릴다운 구조가 장애 대응에 가장 효과적이다.
  • Terraform으로 알람을 관리하면 빠뜨림이 없다. for_each로 서비스 추가 시 알람이 자동 생성되므로 "새 서비스 올렸는데 모니터링이 없었다"는 사고가 발생하지 않는다.
  • 에러 로그 분리는 필수다. 전체 로그에서 에러를 grep하는 것과, 에러만 모인 그룹을 조회하는 것은 장애 시 체감 차이가 크다.
  • 캐시 히트율처럼 CloudWatch에 없는 메트릭은 Grafana Expression으로 만들 수 있다. Math Expression을 적극 활용하면 커스텀 메트릭 비용 없이 파생 지표를 만들 수 있다.
  • ok_actions 설정을 잊지 마라. 장애 알림만 오고 복구 알림이 없으면 팀의 불안감이 불필요하게 높아진다.

AI 활용 포인트

7개 대시보드의 JSON 정의를 Claude Code로 생성했다. "ECS 6개 서비스의 CPU/메모리를 한 패널에, 임계치는 70%/90%로"라고 요청하면 Grafana JSON 스펙에 맞는 패널 정의가 나온다. 특히 Math Expression 문법(Cache Hit Rate 계산)이나 CloudWatch Logs Insights 쿼리 작성에서 AI가 시행착오를 크게 줄여줬다.

Top comments (0)