문제: 서비스가 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"]
}
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 = "*"
}
]
})
}
SourceAccount 조건으로 다른 AWS 계정에서 이 역할을 사용하는 것을 방지한다. Grafana 서비스 프린시펄만 assume 가능하다.
대시보드 설계 — 7개 대시보드 체계
대시보드를 용도별로 나눴다. "하나의 대시보드에 전부 넣기" 유혹을 이기고, 관심사 분리를 적용했다:
📊 대시보드 체계
├── API Overview # 전체 서비스 한 눈에 (첫 화면)
├── ALB Traffic # 트래픽 패턴 + 에러율
├── ECS Metrics # 서비스별 CPU/메모리 상세
├── Aurora DB # 데이터베이스 성능
├── Redis Cache # 캐시 히트율 + 메모리
├── EC2 Infrastructure # 인스턴스 레벨 메트릭
└── ECS Logs Explorer # 로그 검색 + 에러 추적
장애 시 동선: 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"
}]
}
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"
}]
}
트래픽 대시보드에서 가장 중요한 건 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"
}
]
}
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)"
}
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)
// 최근 에러 로그 100건 — 원인 파악
fields @timestamp, @message, @logStream
| filter @message like /(?i)(error|exception|fail|critical)/
| sort @timestamp desc
| limit 100
서비스 선택 드롭다운과 키워드 검색 변수를 추가해서, 특정 서비스의 특정 에러를 바로 필터링할 수 있다:
{
"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": "" }
}
]
}
}
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% 초과 시 경고
# ...
}
핵심 설계 결정:
-
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
}
효과: 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)