728x90

1. 왜 지금 이 얘기가 나오나

요즘 옵저버빌리티 벤더들 데모를 보면 흐름이 다 비슷하다. 경보가 뜨면 AI가 알아서 관련 메트릭·로그·트레이스를 훑고, 최근 배포 내역과 연결하고, "○○ 서비스의 커넥션 풀 고갈로 보입니다, 3시간 전 배포 커밋 abc123이 유력합니다"라고 슬랙에 카드 하나를 던진다. 심하면 롤백 버튼까지 같이 붙여준다.

실무자 입장에서 이건 솔직히 매력적이다. 새벽 3시에 페이저 울리고, 눈 비비며 Grafana 대시보드 12개를 순회하고, 배포 채널 스크롤 올려가며 "누가 뭘 올렸지" 찾는 그 20분이 통째로 사라지니까. MTTR 그래프는 예쁘게 내려간다.

그런데 이번에 공유된 글이 찌르는 지점은 여기다. 그 20분은 낭비가 아니라 학습 시간이었다는 것. 대시보드를 헤매는 동안 엔지니어는 "아, 이 서비스는 DB 커넥션이 이렇게 흐르는구나", "이 큐가 밀리면 저쪽 API가 먼저 죽는구나"를 몸으로 익힌다. AI가 그 구간을 대신 걸어주면, 결과는 얻지만 지도는 안 남는다.

그리고 결국 AI가 못 푸는 장애 — 처음 보는 형태, 여러 시스템이 얽힌 것, 데이터 정합성이 깨진 것 — 만 사람에게 넘어온다. 그때 넘겨받는 사람이 시스템 감각이 없는 상태라면? 그게 이 글의 문제 제기다. 참고로 원문 전문을 확보하지 못해 세부 논거나 인용된 사례는 원문 확인이 필요하다.

2. 자동화의 아이러니: 40년 전에 이미 나온 얘기

이 현상은 새로운 게 아니다. 인간공학 쪽에서 Lisanne Bainbridge가 1983년에 쓴 "Ironies of Automation"이라는 유명한 논문이 있다. 핵심 주장 두 가지가 지금 AI SRE 상황에 그대로 꽂힌다.

  • 아이러니 1: 자동화는 쉬운 일부터 가져간다. 그래서 사람에게는 자동화가 못 하는, 가장 어려운 일만 남는다.
  • 아이러니 2: 평소에 손을 안 쓰면 기술이 녹슨다. 그런데 정작 사람이 개입해야 하는 순간은 시스템이 최악으로 망가진 순간이다.

항공 쪽 비유가 제일 와닿는다. 오토파일럿이 순항을 다 해주는 조종사는 수동 조작 감각이 무뎌진다. 그리고 오토파일럿이 해제되는 순간은 대체로 기상이 최악이거나 센서가 이상할 때다. 즉 가장 어려운 조건에서, 가장 준비 안 된 사람이 조종간을 잡는다.

온콜에 대입하면 이렇다. AI SRE가 P3~P4 수준 반복 장애(디스크 풀, 파드 OOMKilled, 특정 배포 후 5xx 증가)를 다 처리한다. 주니어는 1년 동안 온콜을 서도 실제로 손을 댄 장애가 서너 건뿐이다. 그러다 어느 날 리전 단위 네트워크 파티션이 터지고 AI가 "관련 신호가 여러 서비스에 분산되어 단일 원인을 특정할 수 없습니다"를 뱉는다. 이때 조종간을 잡을 사람이 없다.

더 고약한 건 조직 차원의 지식이 문서가 아니라 AI 컨텍스트 안에만 쌓인다는 점이다. 예전엔 장애 하나 겪으면 위키에 런북이 늘고, 사람 머릿속에 아키텍처 그림이 갱신됐다. 지금은 그 지식이 벤더 SaaS의 임베딩 인덱스 안에 들어간다. 계약이 끝나거나 모델이 바뀌면 조직에 뭐가 남는지 냉정하게 봐야 한다.

3. 실무 관점: 뭘 맡기고 뭘 쥐고 있을 건가

맡겨도 되는 것 / 쥐고 있어야 하는 것

5년 굴려보면서 정리한 기준은 "되돌릴 수 있냐"와 "판단이 필요하냐" 두 축이다.

  • AI에 맡겨도 되는 것: 컨텍스트 수집. 관련 대시보드 링크 모으기, 최근 30분 배포·설정 변경 목록, 동일 알럿 과거 발생 이력, 관련 서비스 의존성 그래프. 이건 사람이 해도 기계적인 작업이고 실수만 늘어난다.
  • AI가 초안만 내고 사람이 결정할 것: 원인 가설. "이게 원인입니다"가 아니라 "가설 3개와 각각의 근거·반증 방법"을 내게 만들어야 한다. 단일 정답을 주는 순간 사람은 검증을 안 한다.
  • 사람이 쥐고 있어야 하는 것: 상태를 변경하는 모든 액션. 롤백, 스케일 조정, 트래픽 차단, DB 작업. 특히 되돌릴 수 없는 것(데이터 삭제, 마이그레이션, DNS TTL 긴 변경)은 무조건 사람.

여기서 흔한 오해 하나. "그럼 자동 롤백도 하지 말라는 거냐?" 아니다. 자동 롤백은 해도 된다. 다만 조건이 명시적으로 코드에 박혀 있고 사람이 그 조건을 읽을 수 있어야 한다. LLM이 상황 봐서 판단해서 롤백하는 것과, 카나리 에러율 임계치 초과 시 롤백하는 규칙은 완전히 다른 얘기다. 후자는 사람이 규칙을 이해하고 튜닝하지만, 전자는 왜 롤백됐는지 아무도 모른다.

실행 가능한 예제 1: AI가 손대지 못할 영역을 RBAC로 못 박기

AI 에이전트에 쿠버네티스 접근을 줄 때, "일단 편의상 cluster-admin" 하고 나중에 줄이겠다는 계획은 100% 안 지켜진다. 처음부터 읽기 전용으로 시작하는 게 맞다.

# ai-sre-readonly.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: ai-sre-investigator
rules:
- apiGroups: ["", "apps", "batch"]
  resources: ["pods", "pods/log", "events", "deployments", "replicasets", "jobs"]
  verbs: ["get", "list", "watch"]      # create/delete/patch 없음
- apiGroups: ["metrics.k8s.io"]
  resources: ["pods", "nodes"]
  verbs: ["get", "list"]

적용하고 실제로 막히는지 확인한다. kubectl auth can-i는 이럴 때 제일 빠르다.

$ kubectl auth can-i get pods/log \
    --as=system:serviceaccount:observability:ai-sre -n prod
yes

$ kubectl auth can-i delete pods \
    --as=system:serviceaccount:observability:ai-sre -n prod
no

$ kubectl auth can-i patch deployments \
    --as=system:serviceaccount:observability:ai-sre -n prod
no

이 세 줄이 "AI는 조사만 하고 손은 사람이 댄다"는 정책의 실체다. 문서에 써놓는 것보다 이게 훨씬 강력하다.

실행 가능한 예제 2: 온콜이 손으로 밟는 조사 경로 남기기

AI가 다 해주더라도, 신규 입사자가 손으로 따라갈 수 있는 최소 경로는 스크립트로 유지해야 한다. 우리 팀은 이런 걸 ops/drill/ 밑에 둔다.

#!/usr/bin/env bash
# ops/drill/trace-5xx.sh  — 5xx 급증 시 사람이 밟는 최소 경로
set -euo pipefail
SVC="$1"; NS="${NS:-prod}"

echo "== 1) 최근 배포 =="
kubectl -n "$NS" rollout history deploy/"$SVC" | tail -3

echo "== 2) 재시작/OOM 파드 =="
kubectl -n "$NS" get pods -l app="$SVC" \
  -o custom-columns='POD:.metadata.name,RS:.status.containerStatuses[0].restartCount,REASON:.status.containerStatuses[0].lastState.terminated.reason'

echo "== 3) 최근 5분 에러율 (PromQL) =="
curl -sG "$PROM_URL/api/v1/query" \
  --data-urlencode "query=sum(rate(http_requests_total{service=\"$SVC\",code=~\"5..\"}[5m]))
    / sum(rate(http_requests_total{service=\"$SVC\"}[5m]))" \
  | jq -r '.data.result[0].value[1] // "no data"'
$ NS=prod ./ops/drill/trace-5xx.sh checkout-api
== 1) 최근 배포 ==
REVISION  CHANGE-CAUSE
41        image: checkout-api:1.22.3
42        image: checkout-api:1.23.0
== 2) 재시작/OOM 파드 ==
POD                              RS   REASON
checkout-api-7c9f4d8b6-2xk4t     3    OOMKilled
checkout-api-7c9f4d8b6-9jm2p     0    
== 3) 최근 5분 에러율 (PromQL) ==
0.0731

핵심은 스크립트 자체가 아니라 "5xx가 뜨면 배포 → 파드 상태 → 에러율 순으로 본다"는 사고 순서를 코드로 박제한다는 점이다. AI가 답을 먼저 줘도, 온콜은 이 순서를 밟아 교차 검증하게 만든다.

흔한 함정

함정 1: 권한을 줄였더니 AI가 "원인 불명"만 뱉는다. 위 RBAC를 적용하고 나서 십중팔구 이런 걸 만난다.

Error from server (Forbidden): events is forbidden: User
"system:serviceaccount:observability:ai-sre" cannot list resource "events"
in API group "" in the namespace "kube-system"

ClusterRole은 만들었는데 특정 네임스페이스에 RoleBinding이 안 걸렸거나, ClusterRoleBinding이 아니라 네임스페이스 한정 바인딩을 걸어놨을 때 나온다. 여기서 귀찮다고 cluster-admin을 붙이는 순간 앞의 설계가 다 무너진다. 필요한 네임스페이스를 열거해서 명시적으로 여는 게 맞다.

함정 2: 그럴듯한 오답. LLM 기반 분석은 근거가 부족해도 자신 있게 결론을 낸다. 실제로 겪은 패턴은 이렇다 — DB 응답이 느려진 진짜 원인은 배치 잡이 테이블 락을 잡은 건데, AI는 같은 시각에 나간 무관한 프론트엔드 배포를 범인으로 지목한다. 시간적 상관관계만 보고 인과로 단정한 것이다. 온콜이 시스템 감각이 있으면 "프론트 배포가 왜 DB를 느리게 해?"라고 되묻지만, 감각이 없으면 그대로 롤백하고 30분을 날린다.

대응은 하나뿐이다. AI 출력에 신뢰도와 반증 방법을 같이 쓰게 강제하는 것. 프롬프트/템플릿 레벨에서 "가설, 지지 근거, 이 가설이 틀렸다면 관측될 신호"를 세 칸으로 나눠 출력하게 한다. 세 번째 칸이 없으면 검증이 안 된다.

함정 3: 알럿 다이어트를 AI가 대신하게 두는 것. 시끄러운 알럿 100개를 AI가 잘 요약해주니까 알럿 정리를 안 하게 된다. 이건 부채를 이자만 내며 굴리는 거다. AI 도입 전에 알럿을 SLO 기반으로 줄이는 작업이 먼저다. 안 그러면 AI는 노이즈를 학습해서 노이즈를 요약해준다.

함정 4: 포스트모템이 AI 요약본으로 대체되는 것. 이게 제일 조용히 진행된다. AI가 타임라인을 자동 생성해주니 사람이 쓰는 부분이 "액션 아이템" 세 줄로 줄어든다. 포스트모템의 가치는 문서가 아니라 여러 명이 모여서 "왜 이게 안 잡혔지"를 따지는 30분에 있는데, 그 자리가 사라진다.

학습이 남는 온콜을 만드는 실무 장치

  • AI 결론 지연 공개(delayed reveal): 훈련 목적 세션에서는 AI 분석 결과를 5~10분 뒤에 공개한다. 온콜이 먼저 자기 가설을 슬랙에 적고, 그다음 AI 답과 비교한다. 이 diff가 그대로 학습 자료다.
  • 섀도 온콜: 주니어가 실제 페이저를 받되, 시니어가 뒤에 붙는다. AI가 이미 답을 냈어도 주니어는 위 trace-5xx.sh 같은 경로를 직접 밟는다. 복구는 빠르게, 학습은 별도로 분리하는 게 핵심.
  • 런북은 절차가 아니라 모델을 담는다: "1. 파드 재시작 2. 확인" 같은 런북은 AI가 대체하면 그만이다. 대신 "이 서비스는 커넥션 풀 크기가 N이고, 업스트림 타임아웃이 M초라 M초를 넘기면 앞단에서 먼저 터진다" 같은 시스템의 작동 원리를 적어야 사람에게 남는다.
  • 게임데이: AI 보조를 꺼놓고 하는 장애 훈련을 분기 1회라도 넣는다. 오토파일럿 끄고 수동 착륙 연습하는 것과 같은 이유다.
  • 지표를 MTTR 하나로 두지 않기: MTTR만 보면 AI 의존도를 올리는 게 항상 정답이 된다. "사람이 직접 조사한 인시던트 비율", "온콜 1인당 신규 유형 장애 경험 건수" 같은 걸 같이 본다. 조직마다 정의는 달라야 하니 그대로 베끼지 말고 팀 상황에 맞춰라.

단계별 도입 순서

한 번에 다 하려다 망하는 걸 여러 번 봤다. 순서는 이렇게 잡는 게 안전하다.

  1. 0단계: 알럿 정리와 서비스 소유권 명확화. AI 붙이기 전에 끝내야 한다.
  2. 1단계: 읽기 전용 컨텍스트 수집만. 알럿에 관련 링크·배포 이력을 자동으로 붙이는 수준.
  3. 2단계: 가설 제안. 단 복수 가설 + 반증 방법 형식 강제.
  4. 3단계: 명시적 규칙 기반 자동 조치(카나리 롤백 등). LLM 판단이 아니라 규칙.
  5. <
728x90
728x90

1. 무슨 일이 있었나 — 그리고 왜 남 얘기가 아닌가

GitHub Actions와 GitHub Pages가 6시간 넘게 대규모 장애·성능 저하를 겪었다. 증상은 대략 이렇게 정리된다.

  • 워크플로우 실행 실패와 지연, 큐 적체
  • 웹훅 처리량이 정상 대비 약 15% 수준까지 떨어져 push/PR 기반 CI 트리거 자체가 안 걸림
  • 자체 호스팅(셀프호스티드) 러너까지 작업 할당이 마비

세 번째 항목이 핵심이다. "우리는 러너를 우리 EC2/온프렘에 띄워놨으니 GitHub 죽어도 빌드는 돌겠지"라고 믿고 있던 팀들이 그날 같이 멈췄다. 나도 예전 팀에서 비용 절감 목적으로 셀프호스티드로 옮기면서 은근히 "가용성도 같이 좋아지겠네"라고 생각했었는데, 그건 착각이었다.

Hacker News 논의에서 나온 배경 설명도 흥미롭다. AI 코딩 에이전트의 자동 커밋·PR·백그라운드 폴링이 폭증하면서 시스템 용량 한계를 넘었다는 분석, 그리고 GitHub COO 발언 인용으로 Actions 주간 사용량이 2023년 5억 분에서 2026년 21억 분으로 늘었다는 수치가 언급됐다. Azure 인프라 이관 과정의 병목과 컨트롤 플레인 집중 구조에 대한 비판도 함께 나왔다. 다만 이건 커뮤니티 분석이고, 공식 포스트모템 기준의 근본 원인은 GitHub 상태 페이지/인시던트 리포트 확인이 필요하다.

어쨌든 실무자 입장에서 결론은 하나다. CI/CD가 외부 SaaS 단일 컨트롤 플레인에 물려 있으면, 그건 우리 배포 파이프라인의 SPOF다. 이 글에서는 왜 셀프호스티드 러너까지 죽는지 구조를 뜯어보고, 우리 파이프라인의 SPOF를 진단하는 방법과 장애 중에도 배포를 이어가는 현실적 우회 전략을 정리한다.

2. 셀프호스티드 러너가 같이 죽는 이유 — 컨트롤 플레인 의존 구조

오해부터 풀자. 셀프호스티드 러너는 "내 서버에서 도는 빌드 머신"이지 "독립적인 CI 시스템"이 아니다. 러너 프로세스가 하는 일을 순서대로 보면 이렇다.

  1. 러너가 GitHub의 Actions 서비스에 롱폴링으로 붙어서 대기한다 (아웃바운드 HTTPS)
  2. 이벤트(push/PR/schedule)가 발생하면 GitHub 쪽 스케줄러가 잡을 큐에 넣고 러너에 할당한다
  3. 러너가 잡 메시지를 받아 워크플로우 YAML을 해석하고 스텝을 실행한다
  4. 로그 스트림, 스텝 상태, 아티팩트 업로드를 계속 GitHub API로 올린다

1~2번이 전부 GitHub 컨트롤 플레인이다. 비유하자면 셀프호스티드 러너는 내 차고에 세워둔 차인데 시동 키는 본사에 있는 구조다. 차는 멀쩡한데 키 발급 서버가 죽으면 그냥 주차장 장식이다. 웹훅 처리량이 15%로 떨어졌다는 건 애초에 "출발 지시" 자체가 안 나갔다는 뜻이고, 스케줄러가 흔들리면 지시가 나가도 러너에 배정이 안 된다.

러너가 살아 있는지, 컨트롤 플레인과 붙어 있는지는 러너 호스트에서 바로 확인할 수 있다.

# 러너 서비스 상태와 최근 로그
$ sudo systemctl status actions.runner.myorg-myrepo.runner-01
● actions.runner.myorg-myrepo.runner-01.service - GitHub Actions Runner
     Active: active (running) since Tue 2026-02-10 09:12:03 KST; 3h 41min ago

$ tail -n 5 ~/actions-runner/_diag/Runner_*.log
[INFO] Listening for Jobs
[WARN] Retrying connection to the server. Attempt 3
[ERR ] GitHub Actions is temporarily unavailable. Retrying in 30 seconds.

여기서 Listening for Jobs가 찍혀 있는데도 잡이 안 내려오면 러너 문제가 아니라 상류 문제다. 이때 러너를 재시작하는 건 아무 소용이 없다(오히려 등록 API까지 흔들리면 재등록이 안 돼서 상황이 더 나빠진다). 장애 중 흔한 자해 행위 1순위가 "일단 러너 재시작"이다.

상태를 스크립트로 물어볼 때는 GitHub 상태 API를 쓰는 게 정확하다.

$ curl -s https://www.githubstatus.com/api/v2/components.json \
  | jq -r '.components[] | select(.name|test("Actions|Pages|Webhooks")) 
           | "\(.name)\t\(.status)"'
Actions         major_outage
Webhooks        degraded_performance
Pages           partial_outage

이 한 줄을 슬랙 알림에 붙여두면, 장애 때 "우리 코드가 문제인가?"로 낭비하는 30분을 아낄 수 있다. 실제로 장애 초기에 팀 채널에 가장 많이 올라오는 질문이 "제 PR만 안 도는 건가요?"다.

3. 우리 파이프라인의 SPOF 진단과 내구성 설계

3-1. SPOF 체크리스트

배포 경로를 단계별로 쪼개서, 각 단계가 GitHub에 얼마나 물려 있는지 표로 그려보길 권한다. 실제로 그려보면 생각보다 GitHub 의존이 촘촘하다.

  • 트리거: push/PR 웹훅. → GitHub 의존 100%. 웹훅이 안 오면 아무것도 시작 안 됨
  • 소스: actions/checkout이 github.com에서 clone. → 미러 레포가 없으면 의존 100%
  • 빌드 캐시: actions/cache는 GitHub 캐시 서비스. → 캐시 미스 시 빌드 시간 폭증
  • 아티팩트: upload/download-artifact, GHCR(ghcr.io). → GHCR도 GitHub 도메인
  • 시크릿: Repository/Org Secrets. → 여기가 진짜 위험. GitHub이 죽으면 수동 배포 시 시크릿 값을 꺼낼 방법이 없다
  • 배포 승인: Environments의 required reviewers. → GitHub UI 의존

이 중 우선순위를 딱 하나만 고르라면 시크릿이다. 빌드는 로컬에서 어떻게든 돌리지만, DB 접속 정보나 배포 키가 GitHub Secrets에만 있으면 긴급 배포 자체가 불가능하다. Secrets는 쓰기 전용이라 UI에서 값을 다시 볼 수도 없다. Vault, AWS Secrets Manager, GCP Secret Manager 중 아무거나 SoT(Source of Truth)로 두고 GitHub Secrets는 거기서 동기화된 사본으로 취급하는 게 맞다.

3-2. 아티팩트·레지스트리 이중화

컨테이너 이미지를 GHCR에만 올리고 있다면, 롤백조차 못 하는 상황이 생긴다. 푸시를 두 곳으로 나누는 건 몇 줄이면 된다.

- name: Build and push (multi-registry)
  uses: docker/build-push-action@v6
  with:
    push: true
    tags: |
      ghcr.io/${{ github.repository }}:${{ github.sha }}
      ${{ secrets.ECR_REGISTRY }}/myapp:${{ github.sha }}

비용은 스토리지 두 벌 + 푸시 트래픽 정도. 롤백 가능성을 사는 값으로는 싸다. 여기서 트레이드오프는 태그 정합성인데, 한쪽 푸시만 성공하고 다른 쪽이 실패하는 부분 성공 케이스를 어떻게 처리할지 정해둬야 한다. 우리는 "ECR 푸시 실패는 파이프라인 실패"로 잡고, 대신 재시도 3회를 걸어뒀다.

3-3. 백업 파이프라인은 "존재"가 아니라 "훈련"이 중요하다

GitLab CI나 Jenkins를 백업으로 두는 팀은 꽤 있는데, 1년에 한 번도 안 돌려본 백업 파이프라인은 장애 때 반드시 깨져 있다. 크리덴셜 만료, 러너 이미지 노후화, 빌드 스크립트 드리프트가 쌓인다. 최소한 주 1회 스케줄로 백업 파이프라인에서 빌드까지만이라도 돌려 녹슬지 않게 해야 한다. 배포까지는 안 해도 된다. 빌드가 성공하는지만 보면 드리프트의 90%는 잡힌다.

원문에서 언급된 Forgejo, GitLab, Woodpecker CI, Depot 같은 대안 이관 움직임도 같은 맥락이다. 다만 전면 이관은 러너 관리·시크릿·권한 체계를 통째로 옮기는 일이라 만만치 않다. 현실적으로는 완전 이관보다 핵심 서비스만 이중 파이프라인이 비용 대비 효율이 좋다고 본다.

3-4. 흔한 함정 — 실제로 만나는 에러들

함정 1: 재시도를 걸었는데 큐만 더 막는다. 장애 중 if: failure()로 재실행을 자동화해두면, 복구 직후 몰린 잡이 한꺼번에 터지면서 동시성 한도를 넘긴다. 그때 나오는 에러가 이거다.

The job was not started because the runner group does not have 
enough capacity. Waiting for a runner to pick up this job...

##[error]The self-hosted runner: runner-01 lost communication with the server. 
Verify the machine is running and has a healthy network connection. 
Anything in your workflow that terminates the runner process, 
starves it for CPU/Memory, or blocks its network access can cause this error.

lost communication with the server 메시지가 특히 헷갈린다. 문구만 보면 우리 머신 네트워크 문제 같은데, 컨트롤 플레인 장애 때도 똑같이 뜬다. 러너 호스트에서 ping, curl https://api.github.com이 멀쩡한지부터 확인하고, 멀쩡하면 상류 장애로 판단하고 손을 떼는 게 낫다.

함정 2: 웹훅 유실은 조용히 지나간다. 웹훅 처리량이 15%로 떨어졌다는 건, 나머지 85%의 push가 실패 표시도 없이 그냥 CI가 안 돈다는 뜻이다. PR 화면에 체크가 안 뜨는 게 아니라 아예 아무 체크도 안 붙는다. 브랜치 보호에서 required status check을 걸어놨다면 머지가 전부 막히고, 안 걸어놨다면 테스트 없이 머지된 커밋이 흘러 들어간다. 후자가 더 무섭다. 복구 후에 반드시 해당 시간대 머지 커밋을 훑고 워크플로우를 수동 재실행해야 한다.

# 장애 시간대 이후 커밋에 워크플로우 수동 재실행
$ gh run list --branch main --limit 5
STATUS  NAME       WORKFLOW  BRANCH  EVENT  ID
X       fix: cache CI        main    push   1029384756
-       chore: bump CI       main    push   1029384700

$ gh workflow run ci.yml --ref main -f reason="post-incident replay"
✓ Created workflow_dispatch event for ci.yml at main

이걸 하려면 워크플로우에 workflow_dispatch: 트리거가 미리 들어 있어야 한다. 없으면 장애 중에 트리거를 추가하는 커밋을 푸시해야 하는데, 그 푸시의 웹훅도 안 갈 수 있다. 지금 당장 모든 배포 워크플로우에 workflow_dispatch를 넣어두자. 비용 0, 효과 확실.

함정 3: Pages를 상태 페이지로 쓰고 있다. 의외로 많다. 서비스 장애 공지 페이지를 GitHub Pages로 호스팅해두면, 이번처럼 Actions와 Pages가 동시에 죽을 때 공지를 띄울 수단이 사라진다. 상태 페이지는 반드시 본 서비스와 다른 사업자에 둬야 한다.

3-5. 장애 중 배포를 이어가는 현실적 우회

순서를 정해두면 당황하지 않는다. 우리 팀 런북은 대략 이 순서다.

  1. 판단(5분): githubstatus API 확인 → 상류 장애면 개별 디버깅 중단, 채널 공지
  2. 동결: 급하지 않은 배포는 전부 홀드. 장애 중 배포는 롤백 수단도 같이 죽어 있다는 걸 잊지 말 것
  3. 핫픽스만 우회: 이미 레지스트리에 올라간 이미지가 있으면 그 태그로 배포. 없으면 로컬 빌드 → 백업 레지스트리 푸시 → 배포 도구(ArgoCD/Helm 등)로 직접 배포
  4. 기록: 우회 배포한 커밋 SHA와 이미지 태그를 채널에 남긴다. 복구 후 정규 파이프라인과 상태를 맞추기 위한 필수 정보다
  5. 복구 후 정합성 확인: 우회 배포분을 정규 파이프라인으로 재빌드해 SHA가 일치하는지 검증

3번에서 로컬 빌드로 배포할 때 가장 자주 사고 나는 게 재현성이다. 평소 CI 러너 이미지와 로컬 환경이 다르면 결과물이 달라진다. 그래서 빌드 환경을 컨테이너로 고정해두는 게 장애 때 빛을 본다. docker run --rm -v $PWD:/src build-env:pinned make build 정도로 CI와 동일한 이미지에서 빌드할 수 있게 만들어두자.

5번의 정합성 확인을 빼먹으면, 몇 주 뒤에 "왜 프로덕션 이미지가 main 브랜치와 다르지?"라는 미스터리로 돌아온다. 실제로 겪어봤고, 원인 찾는 데 하루 날렸다.

4. 정리 — 한 줄 요약과 적용 우선순위

한 줄 요약: 셀프호스티드 러너는 데이터 플레인만 우리 것이고 컨트롤 플레인은 여전히 GitHub이다. 그래서 GitHub이 죽으면 우리 러너도 같이 죽는다.

전면 이중화를 당장 하라는 얘기는 아니다. 팀 규모와 배포 빈도에 따라 필요한 수준이 다르다. 투자 대비 효과 순으로 정리하면 이렇다.

  • 모든 팀 — 오늘 당장(1시간 내): 배포 워크플로우에 workflow_dispatch 추가, githubstatus API 슬랙 알림, 상태 페이지를 Pages 밖으로 이전
  • 배포가 주 1회 이상인 팀 — 이번 스프린트: 시크릿 SoT를 외부 시크릿 매니저로 이전, 컨테이너 레지스트리 이중 푸시, 장애 런북 문서화
  • 배포 중단이 곧 매출 손실인 팀 — 분기 과제: 백업 파이프라인 구축 + 주 1회 자동 검증, 빌드 환경 컨테이너 고정, 소스 미러 레포

반대로 안 해도 되는 것도 짚자. 사내 배포용 툴이나 주 1회 미만 배포하는 레포까지 이중화하는 건 관리 비용만 늘린다. "6시간 멈춰도 되는 것"과 "30분도 안 되는 것"을 먼저 분류하는 게 첫 작업이다.

그리고 이번 장애의 배경으로 지목된 AI 에이전트발 트래픽 폭증은, 사실이라면 앞으로도 반복될 가능성이 높은 종류의 문제다. 우리 조직도 Copilot이나 각종 에이전트를 붙이면서 CI 실행 횟수가 알게 모르게 늘고 있는지, 한 번쯤 사용량을 뽑아볼 만하다. 남의 부하 문제가 아니라 우리도 그 부하의 일부일 수 있다.

참고 자료

728x90

+ Recent posts