[BE] 기술 요구사항 5 ‐ 운영 관찰과 대응 - woowacourse-teams/2026-gachi-gacha GitHub Wiki
서버 6대(dev 3 / prod 3)에 지표·로그·알림을 붙이고, 두 환경을 하나의 관측 스택에서 구분해 보도록 구성했습니다.
1. 무엇을 갖췄는가
1-1. 전체 구조
[ 수집 대상 6대 ] [ 수집 ] [ 조회·알림 ]
b2c-prod ─┐
b2c-dev ─┴─ pull ─→ monitoring-app ─┐
(Prometheus) │
├─→ dashboard
postgre-prod ─┐ │ ├ Grafana (대시보드·알림 규칙)
postgre-dev ─┤ │ └ Loki (로그 저장소)
mongo-prod ─┼─ pull ─→ monitoring-db ─┘ ▲
mongo-dev ─┘ (Prometheus) │
│ │
└──────────────── push (Alloy) ───────────────────┘
│
Slack #buster-call
#buster-call-dev
메트릭은 Prometheus가 가지러 가고(pull), 로그는 각 서버의 Alloy가 보내러 옵니다(push). 방향이 반대라 열어야 하는 보안그룹 방향도 반대입니다.
Prometheus를 2대로 나눈 이유는 조회 부하 분산과 역할 분리입니다. 이전에는 Grafana와 Prometheus가 한 대에 있어 세 명이 동시에 대시보드를 열면 서버가 뻗었습니다.
1-2. 사용한 도구
| 구분 | 도구 | 선택 이유 |
|---|---|---|
| 지표 수집 | Prometheus | 사실상 표준. exporter 생태계 |
| 로그 저장 | Grafana Loki | 본문 전체가 아닌 라벨만 색인 → 저사양 서버에 적합. Prometheus와 라벨 체계 공유 가능 |
| 수집 에이전트 | Grafana Alloy | 메트릭·로그를 에이전트 하나로 처리. Promtail은 2026-03 EOL |
| 시각화·알림 | Grafana | 알림 기능 내장 → Alertmanager 컨테이너를 추가하지 않음 |
| 지표 노출 | node_exporter, postgres_exporter, mongodb_exporter, Spring Boot Actuator | — |
| 알림 전달 | Slack Incoming Webhook | — |
1-3. 수집 범위
| 서버 | 환경 | 로그 | 지표 |
|---|---|---|---|
| b2c-prod | prod | O | O |
| postgre-prod | prod | O | O |
| mongo-prod | prod | O | O |
| b2c-dev | dev | O | O |
| postgre-dev | dev | O | O |
| mongo-dev | dev | O | O |
지표 타겟 20개 (monitoring-app 7 + monitoring-db 13), 전부 UP 상태입니다.
보존 기간은 환경별로 다릅니다. prod 로그 30일, dev 로그 7일.
1-4. 대시보드
| 항목 | 값 |
|---|---|
| Grafana | 팀 내부 공유 (계정 필요) |
| 데이터소스 | prometheus-app · prometheus-db · loki |
| 대시보드 | Node Exporter Full (grafana.com ID 1860) |
| 조회 방식 | Explore에서 분할 화면으로 지표·로그 동시 조회 |
1-5. 알림 채널
| 채널 | 받는 것 |
|---|---|
#buster-call |
prod 전용 |
#buster-call-dev |
dev 전용 |
채널을 나눈 이유는 dev 알림이 운영 채널에 섞이면 사람들이 채널을 음소거하게 되고, 그 순간 운영 알림도 같이 죽기 때문입니다.
알림 규칙은 8개입니다. 조건은 2가지(인스턴스 다운 / 디스크 80%)인데, 환경 2개 × 데이터소스 2개로 갈려 8개가 됩니다.
| 규칙 | 조건 | Pending | 채널 |
|---|---|---|---|
| [PROD] 인스턴스 다운 - app | up{env="prod"} == 0 |
2분 | #buster-call |
| [PROD] 인스턴스 다운 - db | up{env="prod"} == 0 |
2분 | #buster-call |
| [PROD] 디스크 80% - app | 루트 사용률 > 80%, env="prod" |
5분 | #buster-call |
| [PROD] 디스크 80% - db | 루트 사용률 > 80%, env="prod" |
5분 | #buster-call |
| [DEV] 인스턴스 다운 - app | up{env="dev"} == 0 |
15분 | #buster-call-dev |
| [DEV] 인스턴스 다운 - db | up{env="dev"} == 0 |
15분 | #buster-call-dev |
| [DEV] 디스크 80% - app | 루트 사용률 > 80%, env="dev" |
5분 | #buster-call-dev |
| [DEV] 디스크 80% - db | 루트 사용률 > 80%, env="dev" |
5분 | #buster-call-dev |
dev의 "인스턴스 다운"만 대기 시간을 15분으로 늘렸습니다. dev는 배포할 때마다 애플리케이션이 재시작하면서 몇 분씩 up == 0이 됩니다. prod와 같은 2분을 쓰면 배포마다 알림이 울려서, 결국 아무도 안 보게 됩니다.
반면 디스크 임계값은 양쪽 모두 80%로 동일합니다. 디스크는 배포와 무관하게 차오르고, 한 번 차면 dev도 완전히 멈추기 때문입니다.
2. dev와 prod 양쪽에서 동작함을 확인하는 방법
2-1. 라벨로 두 환경을 같은 방식으로 부릅니다
모든 지표와 모든 로그에 동일한 라벨 3종이 붙어 있습니다.
| 라벨 | 값 |
|---|---|
env |
prod · dev |
service |
b2c · postgres · mongo |
host |
서버 이름 |
따라서 env 값만 바꾸면 같은 쿼리로 두 환경을 각각 확인할 수 있습니다. 이것이 환경별 동작을 확인하는 가장 직접적인 근거입니다.
2-2. 확인 절차
① 지표 — 수집 중인 타겟 목록
Grafana Explore에서 데이터소스를 prometheus-app 또는 prometheus-db로 두고 실행합니다.
up{env="prod"} → prod 타겟이 전부 1
up{env="dev"} → dev 타겟이 전부 1
up은 Prometheus가 자동 생성하는 지표로, 수집 성공이면 1 / 실패면 0입니다.
② 로그 — 환경별 수집 여부
데이터소스를 loki로 바꾸고 실행합니다.
{env="prod"}
{env="dev"}
양쪽 모두 로그가 흐르는 것으로 확인됩니다. 서버별 로그량은 아래로 볼 수 있습니다.
sum by (host) (count_over_time({env="dev"}[1h]))
③ 지표와 로그를 한 화면에서
Explore 분할 화면에서 좌우에 서로 다른 데이터소스를 놓고 같은 조건으로 조회합니다.
| 왼쪽 (prometheus) | 오른쪽 (loki) |
|---|---|
up{service="b2c", env="dev"} |
{service="b2c", env="dev"} |= "ERROR" |
같은 시간 범위로 묶여서 "지표가 튄 그 시각에 무슨 로그가 찍혔나"를 한 화면에서 볼 수 있습니다. 두 데이터가 같은 라벨을 쓰도록 맞춘 이유가 이것입니다.
④ 알림 — 실제 발화로 검증
설정만 보고 넘어가지 않고, dev 규칙 하나의 임계값을 일시적으로 낮춰 실제로 발화시켰습니다.
결과: #buster-call-dev 에만 도착
#buster-call 에는 오지 않음
확인 후 임계값은 원래대로 되돌렸습니다. 이로써 환경별 알림이 올바른 채널로만 간다는 것이 검증됐습니다.
그리고 dev 알림은 실제 장애로도 검증됐습니다 (아래 4번).
3. 두 환경의 배포 파이프라인 위치
CI와 CD가 별도 트랙으로 운영됩니다. GitHub Actions가 CodePipeline을 호출하는 구조가 아니라, CodePipeline이 소스를 직접 감지합니다.
코드 푸시
│
├─ GitHub Actions ──────── CI (테스트·검증)
│
└─ CodePipeline ──┬─ Source (GitHub)
├─ Build CodeBuild — JAR 빌드·패키징
├─ Artifact S3
└─ Deploy CodeDeploy → EC2
파이프라인은 dev·prod 두 개로 완전히 분리되어 있습니다.
| dev | prod | |
|---|---|---|
| 파이프라인 | CodePipeline (dev) | CodePipeline (prod) |
| 소스 브랜치 | backend-dev |
main |
| 배포 그룹 | dev 전용 | prod 전용 |
| 배포 대상 | b2c-dev | b2c-prod |
서버에서의 배포 구조
CodeDeploy 훅이 아래 구조로 설치합니다.
/opt/gachi-gacha/backend/
├── releases/
│ ├── d-XXXXXX/app.jar (배포할 때마다 하나씩 추가)
│ └── ...
└── current -> releases/d-XXXXXX
애플리케이션은 current가 가리키는 jar을 systemd로 실행합니다.
관측과의 접점
배포 중에는 애플리케이션이 재시작되며 해당 타겟의 up이 일시적으로 0이 됩니다. 이 때문에 dev의 인스턴스 다운 알림은 대기 시간을 15분으로 설정했습니다 (1-5 참고). 15분을 넘겨도 올라오지 않으면 배포가 아니라 사고로 판단합니다.
4. 장애 기록 — mongo-dev 디스크 고갈 (2026-09-30)
언제 · 무엇이
2026-09-30 15:26, dev 알림 규칙([DEV] 디스크 80% - db)을 생성하고 저장한 직후 즉시 Firing이 발생했습니다.
{device="/dev/root", env="dev", host="mongo-dev"} → Firing
$ df -h /
Filesystem Size Used Avail Use% Mounted on
/dev/root 6.8G 5.5G 1.1G 82% /
규칙을 만든 사람이 손댈 새도 없이 걸렸습니다. 즉, 알림이 없던 기간 동안 이 상태가 계속 방치되고 있었다는 뜻입니다.
원인
처음에는 도커 컨테이너 로그가 불어난 것으로 추정했으나, 오진이었습니다.
| 항목 | 용량 |
|---|---|
| 도커 이미지 | 2.266 GB |
/usr |
2.3 GB |
| 컨테이너 json 로그 | 11 MB |
지울 것이 쌓여서가 아니라 볼륨 자체가 작았습니다. 동종 서버와 비교하면 명확했습니다.
| 서버 | 루트 볼륨 |
|---|---|
| postgre-prod | 15–20 GB |
| mongo-prod | 15–20 GB |
| postgre-dev | 15–20 GB |
| mongo-dev | 6.8 GB |
mongo-dev만 초기 생성 시 8 GiB로 만들어졌고 이후 조정된 적이 없었습니다. 정상 상태에서도 80%를 넘는 것이 당연한 구성이었습니다.
조치
EBS 온라인 확장으로 처리했습니다. 컨테이너를 내리지 않았습니다.
1단계 — AWS 콘솔
EC2 → 볼륨 → 수정 → 크기 8 → 16 GiB
2단계 — 서버
sudo growpart /dev/nvme0n1 1 # 파티션을 디스크 끝까지 확장
sudo resize2fs /dev/nvme0n1p1 # 파일시스템을 파티션 크기에 맞춤
곁다리로 발견한 컨테이너 json 로그 방치분(mongo-dev 786 MB, mongo-prod 755 MB)도 함께 비우고, DB 서버 4대 전부에 로그 로테이션을 설정했습니다.
{ "log-driver": "json-file", "log-opts": { "max-size": "50m", "max-file": "3" } }
조치 뒤 확인한 지표
| 지표 | 조치 전 | 조치 후 |
|---|---|---|
| 루트 디스크 사용률 | 82% | 38% |
| 가용 공간 | 1.1 GB | 약 9.9 GB |
| 알림 상태 | Firing | Normal |
| MongoDB 컨테이너 | — | 재시작 없음 (무중단) |
Grafana 대시보드에서 해당 서버의 디스크 사용률 그래프가 80 → 38로 떨어지고, 알림 규칙의 세 인스턴스가 모두 Normal로 돌아온 것을 확인했습니다.
같은 날 발견한 또 하나 (대조군)
같은 날 오전 11:05, b2c-dev도 디스크 92% 상태였습니다. 다만 이쪽은 알림이 아니라 사람이 로그 수집 작업 중 우연히 발견했습니다.
| 1차 (11:05) | 2차 (15:26) | |
|---|---|---|
| 서버 | b2c-dev 92% | mongo-dev 82% |
| 원인 | 배포 릴리스 36개 누적 (3.1 GB) | 볼륨 크기 부족 |
| 발견 경로 | 사람 (우연히) | 알림 (자동) |
| 조치 후 | 55% | 38% |
| 재발 방지 | install.sh에 릴리스 정리 로직 추가 (#179) |
EBS 8 → 16 GiB |
1차는 배포 스크립트에 오래된 릴리스를 삭제하는 로직이 없어 발생했습니다. install.sh에 최근 3개만 유지하는 정리 로직을 추가했고, dev에 실제 배포를 수행해 릴리스가 3개로 유지되고 디스크가 누적되지 않음을 확인했습니다.
같은 종류의 문제인데 발견 경로가 갈렸다는 점이 이번 작업의 필요성을 보여줍니다. 1차는 다른 작업을 하다 우연히 보지 않았다면 배포가 실패하고 나서야 알았을 것입니다.