왜 배포 이벤트 추적이 필요한가
장애 대응 회의에서 가장 먼저 나오는 질문은 항상 같다. "방금 뭐 배포했어요?" 메트릭에 스파이크가 생기고 에러율이 튀는데, 그 시점에 어떤 서비스의 어떤 커밋이 나갔는지 아무도 모르면 원인 규명에 몇십 분이 그냥 사라진다. CI/CD 로그는 파이프라인 안에 갇혀 있고, 모니터링 대시보드는 지표만 보여줄 뿐 배포라는 사건과 연결되지 않는다. 이 단절이 MTTR(평균 복구 시간)을 늘리는 가장 흔한 원인이다.
해결의 핵심은 단순하다. 배포를 "이벤트"로 만들어 관측 시스템 타임라인 위에 얹는 것이다. 그러면 그래프 위에 세로선 하나로 "여기서 v1.42.0이 나갔다"가 표시되고, 상관관계 추적이 눈으로 가능해진다.
어떤 필드를 남겨야 하는가
이벤트 스키마를 처음부터 표준화해두지 않으면 나중에 서비스마다 제각각이 되어 조인이 불가능해진다. 최소한 다음 필드는 고정하자.
| 필드 | 설명 | 예시 |
|---|---|---|
| service | 배포 대상 서비스명 | payment-api |
| version | 릴리스 버전/태그 | v1.42.0 |
| revision | 커밋 SHA | a1b2c3d |
| environment | 배포 환경 | production |
| status | started / success / failed / rolled_back | success |
| actor | 실행 주체 | ci-bot / user |
| timestamp | UTC 기준 발생 시각 | ISO 8601 |
특히 revision과 environment는 나중에 로그·트레이스와 조인하는 키가 되므로 절대 빼지 말자.
CI에서 이벤트 전송하기
가장 간단한 방식은 파이프라인 마지막 단계에서 관측 백엔드로 이벤트를 쏘는 것이다. 아래는 GitHub Actions에서 배포 완료 시점에 마커를 전송하는 예시다.
- name: Send deploy marker
if: always()
run: |
STATUS=${{ job.status == 'success' && 'success' || 'failed' }}
curl -sf -X POST "$OBS_ENDPOINT/api/v1/deployments" \
-H "Authorization: Bearer $OBS_TOKEN" \
-H "Content-Type: application/json" \
-d @- <
if: always()로 실패 케이스도 반드시 잡는다. 실패 배포가 기록되지 않으면 "왜 이 시점부터 트래픽이 절반만 처리됐지?" 같은 미스터리가 생긴다.
OpenTelemetry 이벤트로 표준화하기
벤더에 묶이지 않으려면 OTel의 로그/이벤트로 보내는 것이 낫다. 속성 이름은 시맨틱 컨벤션(service.name, deployment.environment)을 따르면 대시보드 쪽 조인이 쉬워진다.
from opentelemetry import trace
from opentelemetry.sdk._logs import LoggerProvider
import logging
logger = logging.getLogger("deploy")
def emit_deploy_event(version, revision, status, env="production"):
logger.info(
"deployment",
extra={
"service.name": "payment-api",
"deployment.environment": env,
"deployment.version": version,
"vcs.revision": revision,
"deployment.status": status,
},
)
emit_deploy_event("v1.42.0", "a1b2c3d", "success")
이렇게 표준 속성으로 남기면 트레이스의 service.name과 자연스럽게 묶여, 배포 직후 특정 엔드포인트의 지연시간 변화를 바로 겹쳐볼 수 있다.
배포와 지표를 연결해 조회하기
이벤트가 쌓이면 배포 전후 비교 쿼리가 가능해진다. 예를 들어 배포 이벤트를 저장한 테이블과 에러 카운트를 조인해 "배포 후 5분간 에러 증가"를 뽑을 수 있다.
SELECT d.service, d.version, d.timestamp AS deployed_at,
COUNT(e.id) AS errors_5min
FROM deployments d
LEFT JOIN error_events e
ON e.service = d.service
AND e.ts BETWEEN d.timestamp AND d.timestamp + INTERVAL '5 minutes'
WHERE d.environment = 'production'
AND d.status = 'success'
GROUP BY d.service, d.version, d.timestamp
HAVING COUNT(e.id) > 0
ORDER BY d.timestamp DESC;
이 결과를 자동 롤백 트리거나 배포 게이트의 판단 근거로 쓰면, 사람이 그래프를 노려보며 판단하던 일을 규칙으로 옮길 수 있다.
주의점
첫째, 시계는 반드시 UTC로 통일한다. 로컬 타임존이 섞이면 타임라인 정렬이 어긋나 상관관계 자체가 틀어진다.
둘째, 이벤트 전송 실패가 배포를 막으면 안 된다. 마커 전송은 best-effort로 두되, 실패는 별도 로그로 남겨 누락을 감지하자.
셋째, 카나리·블루그린처럼 점진 배포는 "시작"과 "전량 전환"이 다른 시점이다. 두 이벤트를 구분해 남겨야 실제 영향 구간을 정확히 짚는다.
넷째, 버전·커밋·환경은 저카디널리티 태그로만 쓰고, 요청 ID 같은 고카디널리티 값을 배포 이벤트 태그에 섞지 말자. 스토리지 비용과 쿼리 성능이 함께 무너진다.