AI 블로그 자동화가 4일간 조용히 멈춰 있었다: exit(1) 하나가 부른 연쇄 장애 복구기
지금 읽고 계신 이 블로그의 포스트는 매일 AI 파이프라인이 자동으로 작성합니다. 그런데 그 파이프라인이 4일 동안 완전히 멈춰 있었고, 저는 그 사실을 전혀 몰랐습니다. 이 글은 그 장애의 원인 진단부터 복구까지의 실제 기록이며, "부가 기능의 실패가 핵심 기능을 죽이는 구조"와 "침묵 장애(silent failure)"라는 두 가지 흔한 함정에 대한 포스트모템입니다.
1. 발견: 장애는 소리 없이 온다
발견 경위부터 이상적이지 않았습니다. 모니터링 알림이 울린 게 아니라, 서버의 불필요한 API 키를 정리하는 감사 작업을 하다가 우연히 크론 로그에서 4일치 연속 실패를 발견했습니다. 매일 새벽 자동 발행되어야 할 포스트가 7일 연속으로 발행일 기준 4일째 하나도 올라오지 않고 있었던 겁니다.
로그에 남은 에러는 이것 하나였습니다.
❌ Vertex AI Init Failed: 403 Publisher Model
`publishers/google/models/imagen-3.0-generate-001`
is not visible to the current project
흥미로운 점: 이건 이미지 생성 모델의 에러입니다. 이미지가 없으면 스톡 이미지라도 쓰고 글은 발행됐어야 정상인데, 왜 발행 자체가 멈췄을까요?
2. 원인은 하나가 아니었다: 겹쳐 있던 세 개의 문제
문제 ① 텍스트 모델의 조용한 폐기 (404)
파이프라인은 텍스트 생성에 Vertex AI의 gemini-2.0-flash-001을 쓰고 있었습니다. 스냅샷 버전(-001)을 고정해서 쓰면 재현성이 좋다는 장점이 있지만, 클라우드 제공사가 해당 스냅샷을 폐기(retire)하는 순간 코드는 그대로인데 404가 나기 시작합니다. 정확히 그 일이 일어났습니다. 해결은 간단했습니다 — 현행 모델(gemini-2.5-flash)로 교체.
문제 ② 진짜 범인: 결합된 초기화와 exit(1)
원래 코드는 대략 이런 구조였습니다.
# Before: 텍스트와 이미지를 하나의 try로 초기화
try:
text_model = GenerativeModel("...") # 핵심 기능
image_model = ImageGenerationModel.from_pretrained("...") # 부가 기능
except Exception as e:
logging.error(f"Init Failed: {e}")
exit(1) # ← 이미지만 죽어도 전체가 죽는다
이미지 모델 초기화가 403으로 실패하자 exit(1)이 텍스트 생성까지 포함한 프로세스 전체를 죽였습니다. 코드에는 이미지 실패 시 스톡 이미지(Unsplash)로 대체하는 폴백 경로까지 이미 구현되어 있었는데, 폴백 코드에 도달하기도 전에 프로세스가 종료된 겁니다. 부가 기능(이미지)의 장애가 핵심 기능(글 발행)을 동반 사살한 전형적인 결합 사고입니다.
# After: 초기화 분리 + 부가 기능은 non-fatal
try:
text_model = GenerativeModel("gemini-2.5-flash")
except Exception as e:
logging.error(f"Text Init Failed: {e}")
exit(1) # 핵심 기능만 fail-fast
try:
image_model = init_image_model()
except Exception as e:
logging.warning(f"Image Init Failed (fallback to stock): {e}")
image_model = None # 부가 기능은 폴백으로 계속
문제 ③ Imagen 403의 정체: 프로젝트 게이팅
이미지 쪽 403은 더 파고들 가치가 있었습니다. "결제 문제겠지"라는 첫 가설부터 하나씩 배제해 나갔습니다.
- 결제? 아님 — 같은 프로젝트·같은 자격증명으로 텍스트 모델(유료 호출)은 정상 작동.
- 특정 모델 버전 폐기? 아님 — Imagen 3, 3-fast, 4, 구세대 네이밍까지 전 버전을 테스트해도 전부 동일한 403.
- 리전 문제? 아님 — 미국·유럽·아시아 6개 리전 전수 테스트, 전부 동일.
- 서비스 계정 권한? 아님 — 프로젝트 소유자 계정으로 콘솔에 직접 들어가도 Model Garden에서 Imagen 카드 자체가 보이지 않았고, 모델 메타데이터 조회 API(
GetPublisherModel)의 오류율이 100%였습니다.
결론: 해당 Google Cloud 프로젝트에서 Imagen 계열 모델 전체가 프로젝트 레벨에서 비노출 처리(게이팅)된 상태였습니다. 액세스 요청 버튼조차 없어서 콘솔에서 풀 방법이 없었습니다. 어느 날 갑자기, 공지 없이.
3. 해결: 같은 모델, 다른 문으로 — API 표면 우회
여기서 중요한 발견: Google의 생성 모델은 두 개의 API 표면으로 제공됩니다. Vertex AI(GCP 프로젝트·서비스계정 기반)와 Gemini API(AI Studio, API 키 기반). 한쪽 문이 막혀도 다른 쪽 문은 열려 있을 수 있습니다.
실제로 Gemini API 쪽 키로 모델 목록을 조회하니 imagen-4.0-fast-generate-001이 멀쩡히 보였고, 테스트 생성도 즉시 성공했습니다. 이미지 생성부를 Vertex SDK에서 REST 한 방으로 교체했습니다.
resp = requests.post(
"https://generativelanguage.googleapis.com/v1beta/models/"
"imagen-4.0-fast-generate-001:predict",
params={"key": API_KEY},
json={
"instances": [{"prompt": prompt}],
"parameters": {"sampleCount": 1, "aspectRatio": "16:9"},
},
timeout=120,
)
img_bytes = base64.b64decode(
resp.json()["predictions"][0]["bytesBase64Encoded"])
부수 효과로 이미지 모델이 Imagen 3에서 Imagen 4로 세대 업그레이드됐고, 비용은 fast 모델 기준 장당 약 $0.02 — 하루 2포스트면 월 1,500원 수준입니다. 이 포스트 상단의 이미지가 바로 그 복구된 파이프라인이 생성한 결과물입니다.
4. 이 사건이 남긴 교훈
- 부가 기능과 핵심 기능의 초기화를 분리하라. fail-fast는 핵심 기능에만. 부가 기능은 warning + 폴백으로 강등(graceful degradation). 폴백 코드가 있어도 거기 도달하지 못하면 없는 것과 같다.
- 침묵 장애가 최악의 장애다. 크론이 4일 연속 실패해도 아무도 모른다면 모니터링이 없는 것이다. 성공 알림이 아니라 실패 알림(또는 "N일간 산출물 없음" 감시)을 걸어야 한다.
- 고정된 모델 스냅샷은 만료된다.
-001같은 스냅샷 고정은 재현성을 주지만, 제공사의 폐기 일정에 노출된다. 폐기 공지 모니터링이 없다면 현행 별칭 사용을 고려하라. - 가설은 매트릭스로 배제하라. "결제 문제겠지"라고 짐작만 했다면 헛다리를 짚었을 것이다. 버전×리전 전수 테스트 스크립트 하나가 결제/폐기/리전/권한 가설을 한 번에 배제해 줬다.
- 단일 벤더 안에도 우회로가 있다. 같은 모델이라도 API 표면(Vertex vs Gemini API)이 다르면 접근 정책도 다르다. 한쪽이 막히면 다른 표면을 먼저 테스트해 보라 — 마이그레이션보다 훨씬 싸다.
5. FAQ
- Q: Vertex AI 쪽 게이팅의 정확한 원인은 밝혀졌나?
A: 아니요. 공식 에러 메시지는 "not visible to the current project"가 전부이고, 콘솔에도 액세스 요청 경로가 없었습니다. 지원 티켓으로 풀 수도 있겠지만, 우회로가 완전히 동작하는 상황에서 그 비용을 지불할 이유가 없다고 판단했습니다. - Q: Gemini API 키 방식이 서비스계정보다 보안상 불리하지 않나?
A: 키 유출 리스크는 실재합니다. 키를 API 제한(Generative Language API 전용)으로 잠그고, 환경변수로만 주입하며, 과금 이상 감지를 걸어두는 것으로 보완했습니다. - Q: 침묵 장애 감시는 구체적으로 어떻게?
A: 가장 값싼 방법은 "산출물 기반 감시"입니다. 프로세스 성공 여부 대신 결과물(최신 포스트의 날짜)을 검사해서 N일 이상 갱신이 없으면 알리는 것. 파이프라인 내부가 어떻게 실패하든 잡아냅니다.
6. 결론
이번 장애의 근본 원인은 Google의 모델 폐기도, 수수께끼의 403도 아니었습니다. "이미지가 없으면 글도 없다"고 코드 구조가 강제하고 있었다는 것, 그리고 그 실패를 나흘간 아무도 몰랐다는 것입니다. 외부 의존성은 언젠가 반드시 예고 없이 변합니다. 그때 당신의 파이프라인이 우아하게 강등되는지, 통째로 침묵하는지는 전적으로 try/except 블록 하나의 설계에 달려 있습니다. 오늘 당장 자신의 자동화 코드에서 exit(1)을 검색해 보세요 — 그것이 정말 전체를 죽여야 할 실패인지.
