본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 26. 09:34

실제로 계속 성공(Green) 상태를 유지하는 GitHub Actions 크론(Cron) 작업

요약

GitHub Actions 크론 작업이 종료 코드 0을 반환하며 조용히 실패하는 문제를 해결하는 방법을 다룹니다. 예외 처리 방식 개선, 저수위 경보 설정, 작업 전 상태 확인(Health check) 도입을 통해 안정적인 자동화 운영 전략을 제시합니다.

핵심 포인트

  • 종료 코드 0이 작업의 실제 성공을 보장하지 않음을 인지해야 함
  • 예외를 무조건 삼키지 말고 실패 플래그를 설정하여 에러를 명시해야 함
  • 작업 완료 후가 아닌 작업 전 상태 확인(Health check) 단계가 필수적임
  • 큐(Queue)의 잔량을 모니터링하는 저수위 경보 시스템 구축 권장
  • 7개의 일일 크론(Cron), 재작성을 유발한 2건의 자원 고갈(Starvation) 사고
  • 작업 후가 아닌 작업 전의 상태 확인(Health checks)이 조용한 실패를 잡아낸다
  • 큐(Queue) 저하 알람은 0개가 아니라 5개일 때 울려야 한다
  • 크론(Cron)을 3주 동안 무시할 수 있으려면 실패가 명확하게 드러나야 한다

저는 매일 7개의 GitHub Actions 크론(Cron)을 실행하며, 지난 두 달 동안 한 번도 그것들을 들여다보지 않았습니다. 그러다 콘텐츠 큐(Queue)가 조용히 고갈되었고, 알아차리기 전까지 4일 동안 아무것도 게시하지 못했습니다. 크론(Cron)이 계속 성공(Green) 상태를 유지하면서도 3주 연속으로 무시해도 될 만큼 안정적으로 운영될 수 있도록 제가 무엇을 변경했는지 소개합니다.

재작성을 강제한 두 가지 사고

첫 번째 자원 고갈 사고는 화요일에 발생했습니다. 저의 이미지 생성 크론(Cron)은 큐(Queue)에서 프롬프트(Prompt)를 가져와 에셋(Asset)을 만들고, 이를 게시 큐(Publish queue)로 푸시합니다. 이미지 API가 429(Rate limited, 속도 제한)를 반환했고, 작업은 초록색 체크 표시와 함께 깔끔하게 종료되었습니다. GitHub Actions는 성공을 보고했습니다. 워크플로(Workflow) 로그에는 제가 읽지 않았던 한 줄에 "0 prompts processed"라고 적혀 있었습니다. 4일 동안 게시 큐(Publish queue)는 비어갔고 아무것도 채워지지 않았습니다. 한 팔로워가 왜 소식이 없느냐고 물어본 후에야 이 사실을 알게 되었습니다.

두 번째 사고는 더 교묘했습니다. 외부 API를 호출하는 크론(Cron)이 만료된 인증 토큰(Auth token)에 부딪혔습니다. 스크립트는 에러를 포착하여 로그를 남겼지만, 제가 모든 것을 삼켜버리는 try/except 구문으로 전체를 감싸 놓았기 때문에 종료 코드(Exit code) 0을 반환했습니다. 초록색 체크가 떴지만, 작업은 수행되지 않았습니다. 이 사고는 다른 디버깅 세션 중에 발견하기 전까지 6일 동안 지속되었습니다.

두 실패는 하나의 근본 원인을 공유하고 있었습니다. GitHub Actions의 초록색 체크 표시는 프로세스가 종료 코드 0을 반환했다는 의미이지, 작업이 수행되었다는 의미가 아닙니다. 이 둘은 완전히 다른 주장입니다. 스스로 에러를 포착하고 깔끔하게 종료되는 크론(Cron)은 가능한 가장 정중한 방식으로 당신에게 거짓말을 하고 있는 것입니다.

두 번째 사고 이후, 저는 제가 실제로 원하는 것이 무엇인지 적어 내려갔습니다. 저는 무언가 잘못되지 않는 한 이 워크플로(Workflow)들을 절대 들여다보고 싶지 않았습니다. 저는 "잘못된 상황"이 요란하게 드러나기를 원했습니다. 그리고 그 요란함이 피해가 발생한 후가 아니라, 발생하기 전에 도착하기를 원했습니다.

그것은 세 가지 변화를 의미했습니다. 첫째, 종료 코드 (exit code)가 실제 작업 내용을 반영해야 했으므로, 삼켜진 예외 (swallowed exceptions)는 다시 발생시키거나 실패 플래그 (failure flag)를 설정해야 했습니다. 둘째, 큐 (queue) 자체에 대응할 시간이 남아 있을 때 작동하는 저수위 경보 (low-water alarm)가 필요했습니다. 셋째, 모든 크론 (cron) 작업에는 실제 작업 전에 실행되는 상태 확인 (health check)이 필요했습니다. 그래야 만료된 토큰 (token)이나 죽은 API가 조용한 무작위 동작 (no-op)이 아닌 실패한 작업으로 드러날 수 있기 때문입니다.

제가 이러한 자동화 (automations)를 어떻게 구성하는지에 대한 더 깊은 맥락을 알고 싶다면, 제가 이런 종류의 작업에서 의존하는 전체 에이전트 (agent) 설정을 다루는 Claude Blueprint를 참조하세요.

상태 확인은 마지막이 아니라 가장 먼저 실행한다

가장 큰 패턴의 변화는 모든 워크플로 (workflow)의 맨 앞으로 상태 확인 (health check)을 옮긴 것이었습니다. 이전의 순서는 '작업을 수행한 다음, 그것이 제대로 작동했는지 확인하는 것'이었습니다. 새로운 순서는 '작업을 수행할 수 있음을 증명한 다음, 수행하는 것'입니다.

작업 상단에 위치한 상태 확인은 5초짜리 사전 점검 (pre-flight)입니다. API 의존형 크론 (cron)의 경우, 저렴한 읽기 전용 호출로 엔드포인트 (endpoint)에 핑 (ping)을 보내고 200 상태 코드를 확인합니다. 토큰 의존형 크론의 경우, whoami 엔드포인트를 통해 토큰을 검증합니다. 큐 의존형 크론의 경우, 큐 깊이 (queue depth)를 계산하여 처리할 대상이 있는지 확인합니다. 이 중 하나라도 실패하면 작업은 즉시 0이 아닌 종료 코드 (non-zero exit code)로 종료되며, GitHub Actions는 이를 빨간색으로 표시하고 저는 이메일을 받게 됩니다.

상태 확인을 앞당기는 것이 중요한 이유는 다음과 같습니다. 만약 상태 확인이 마지막에 실행된다면, API 오류가 발생했을 때 작업은 이미 절반쯤 실행된 상태가 됩니다. 부분적인 상태 (partial state), 절반만 비워진 큐, 생성된 3개의 자산과 누락된 7개의 자산 같은 상황이 발생합니다. 이를 정리하는 것은 원래의 실패보다 더 나쁩니다. 반면 상태 확인이 먼저 실행되고 실패한다면, 아무 일도 일어나지 않습니다. 큐는 건드려지지 않은 상태입니다. 토큰을 수정하고 다시 실행하면 끝입니다.

저는 또한 엄격한 규칙을 하나 추가했습니다. 종료 코드 (exit code)를 삼켜버리는 포괄적인 catch-all 구문으로 전체 작업을 감싸지 않는 것입니다. 각 단계 (step)는 고유하고 타겟팅된 에러 핸들링 (error handling)을 가집니다. 제가 예상하지 못한 무언가가 깨진다면, 그것이 요란하게 충돌 (crash)하기를 원합니다. 충돌은 정보입니다. 삼켜진 예외 (swallowed exception)는 침묵이며, 그 침묵 때문에 저는 처음 발생했을 때 4일을 허비했습니다.

헬스 체크 (health check)는 또한 제가 2초 만에 훑어볼 수 있도록 작업 로그에 한 줄 요약을 작성합니다: "API ok, token valid, queue depth 14, processing 5." 제가 실행 기록 (runs)을 살펴볼 때가 있다면, 그 한 줄이 모든 것을 말해줍니다. 200줄에 달하는 단계 (step) 출력물을 스크롤하며 확인할 필요가 없습니다.

효과를 본 작은 디테일 하나는, 헬스 체크 자체를 "preflight"와 같이 명확한 이름을 가진 별도의 작업 단계 (job step)로 만든 것입니다. GitHub Actions UI에서 실패한 단계는 정확히 그 라벨과 함께 빨간색으로 표시되므로, 이메일 제목과 실행 페이지 모두 제가 무엇이 고장 났는지 확인하기 위해 무엇인가를 열어보기 전에 미리 알려줍니다. 사람을 위해 단계 (steps)의 이름을 짓는 것은 비용이 들지 않으며, 실행 기록을 열었을 때 어디서 멈췄는지 몰라 당황하는 상황을 방지해 줍니다.

큐 비움 실패보다 큐 부족 알람이 낫다

기아 (starvation) 현상들을 겪으며 저는 큐가 비어버리는 것은 너무 늦다는 것을 배웠습니다. 큐가 0에 도달했을 때쯤이면, 이미 피해는 발생하기로 예정된 상태입니다. 저에게는 여유가 남아 있을 때 울리는 알람이 필요했습니다.

그래서 저는 하한선 (low-water mark)을 설정했습니다. 제 게시 큐 (publish queue)는 0이 아니라 잔여 항목이 5개 남았을 때 알람을 울립니다. 저의 게시 주기 (posting cadence)를 고려할 때, 5개는 대략 2일 정도의 여유분입니다. 이는 서비스가 중단되기 전 주말 내내 경고를 받을 수 있는 시간을 줍니다. 이 알람은 크론 (cron) 작업 내의 하나의 단계 (step)일 뿐이며, 큐 깊이 (queue depth)를 확인하고 임계값 (threshold) 미만일 경우 "Queue low: 5 items, refill within 48h"와 같은 제목으로 GitHub 이슈 (issue)를 생성합니다.

이슈 (issue)가 핵심입니다. 저는 무시하게 될 또 다른 이메일을 원하지 않았습니다. 제가 해결할 때까지 제 눈앞에 머물러 있는 무언가를 원했습니다. GitHub 이슈는 열린 상태로 유지됩니다. 제 리포지토리 (repo)에 숫자로 나타납니다. 계속 신경 쓰이게 만듭니다. 큐를 다시 채우면 이슈를 닫고 카운트는 다시 0으로 돌아갑니다. 열려 있는 이슈의 개수는 "무언가 기아 상태에 빠졌는가"를 확인하는 저만의 단일 대시보드가 되었습니다.

또한 저는 알람을 멱등적 (idempotent)으로 만들었습니다. 큐가 3일 연속으로 낮다면, 중복된 이슈 3개를 원하지 않습니다. 크론 (cron)은 새 이슈를 생성하기 전에 동일한 라벨을 가진 기존의 열린 이슈가 있는지 확인합니다. 해결될 때까지 하나의 알람, 하나의 이슈만 유지됩니다. 이를 통해 신호 (signal)를 깨끗하게 유지할 수 있었습니다. 열려 있는 큐 부족 (queue-low) 이슈를 보면, 그것이 실제 상황이며 현재 진행 중임을 알 수 있습니다.

임계값(thresholds)은 보기보다 더 중요합니다. 저는 반응 시간(reaction time)을 기준으로 역산하여 임계값을 조정했습니다. 저는 스스로에게 질문합니다. '만약 금요일 밤에 이 알람이 울린다면, 스트레스 없이 일요일까지 해결할 수 있는가?' 게시 큐(publish queue)의 경우, 5개의 항목이 저에게 그 여유를 줍니다. 더 빠르게 비워지는 큐의 경우에는 임계값을 더 높게 설정했습니다. 이 숫자는 마법 같은 것이 아니라, 그저 "평정심을 유지할 수 있는 충분한 활주로"일 뿐입니다.

만약 큐 위에서 소셜 스케줄링(social scheduling)을 실행한다면, Buffer가 실제 게시 주기(posting cadence)를 처리하므로 크론(cron)은 콘텐츠 파이프라인(content pipeline)을 가득 채우는 일에만 집중하면 됩니다. "생성(generate)"과 "게시(post)"를 분리함으로써, 생성 실패가 더 이상 게시를 중단시키지 않게 되었습니다. Buffer가 이미 예약된 항목들을 계속 게시하기 때문입니다. 이러한 분리만으로도 기아(starvation) 현상의 한 부류를 완전히 제거할 수 있었습니다. 이와 관련된 더 광범위한 큐 및 캐시(queue-and-cache) 사고방식에 대해서는 Claude Blueprint에서 다루었습니다.

데드 레터(Dead-Letter) 동작 및 장애 모드(Failure-Mode) 이슈

마지막 단계는 작업이 실행 도중 실패했을 때 어떻게 처리할지 결정하는 것이었습니다. 메시지 큐(message-queue) 용어로 이는 데드 레터(dead-letter) 패턴이며, 크론(cron)에도 자체적인 버전의 이 패턴이 필요합니다.

제 이미지 크론(image cron)이 프롬프트(prompt)를 가져왔는데 생성이 실패할 경우, 이전 코드는 프롬프트를 그냥 버려버렸습니다. 프롬프트는 사라졌고, 큐는 다음으로 넘어갔으며, 저는 특정 항목이 실패했다는 사실조차 알 수 없었습니다. 이제 실패한 항목은 폐기되는 대신 데드 레터 리스트(dead-letter list)로 이동합니다. 크론은 한 번 시도하고, 실패하면 해당 항목을 별도의 "실패(failed)" 큐로 이동시키고 그 이유를 로그로 남깁니다. 메인 큐는 계속 흐릅니다. 실패한 항목은 저를 기다립니다.

이것으로 부분적 실패(partial-failure) 문제를 해결했습니다. 하나의 잘못된 프롬프트가 전체 실행을 망치지 않게 되었습니다. 크론은 나머지 4개의 항목을 처리하고, 고장 난 항목은 따로 떼어 놓으며, 수행할 수 있는 작업에 대해서는 성공(green) 상태를 유지하면서 수행할 수 없었던 작업에 대해서는 플래그(flag)를 표시합니다. 실행이 끝났을 때 데드 레터 리스트가 늘어났다면, 크론은 다음과 같이 장애 모드(failure-mode) 이슈를 생성합니다: "2개 항목 데드 레터 처리됨, 사유: 1개 속도 제한(rate limit), 1개 잘못된 프롬프트(malformed prompt)."

실패 모드(Failure-mode) 문제는 큐 부족(queue-low) 문제와는 다릅니다. 큐 부족은 "나에게 데이터를 달라"고 말하는 것이라면, 실패 모드는 "무언가 구체적으로 고장 났으며, 그 이유는 이것이다"라고 말하는 것입니다. 저는 이 둘을 다르게 라벨링하여, 저의 오픈 이슈(open-issue) 대시보드가 "굶주림(starving)"과 "질식(choking)"을 구분할 수 있도록 했습니다. 둘 다 시끄럽고, 둘 다 성가시게 굴지만, 둘 다 조용히 넘어가지는 않습니다.

또한 데드 레터 큐(dead-letter queue) 자체에 지수 백오프(retry-with-backoff)를 추가했습니다. 하루에 한 번 별도의 작은 크론(cron) 작업이 데드 레터 처리된 항목들을 재시도하는데, 이는 대부분의 항목이 속도 제한(rate limits)과 같은 일시적인 오류(transient errors)로 인해 실패했기 때문입니다. 데드 레터 처리된 항목의 약 80%는 재시도 단계에서 성공합니다. 계속 실패하는 나머지 20%는 실제 버그(real bugs)이며, 오직 이들만이 제 눈에 띄게 됩니다. 이 비율 덕분에 전체 시스템을 무시할 수 있게 됩니다. 저는 50개의 일시적인 튀는 현상(transient blips)을 검토하는 것이 아니라, 일주일에 단 2개의 진짜 문제를 검토하고 있는 것입니다.

특히 이미지 측면에서는, Magnific가 해당 파이프라인의 업스케일링(upscaling) 단계를 처리하는데, 이를 데드 레터 패턴 뒤에 배치함으로써 업스케일 타임아웃(upscale timeout)이 발생했을 때 배치(batch) 전체를 중단시키는 대신 깔끔하게 재시도할 수 있게 되었습니다. 데드 레터 처리와 일일 재시도의 결합은 저의 가장 불안정했던 크론(cron) 작업을 가장 지루한 작업으로 바꾸어 놓았으며, 이것이 바로 정확한 목표였습니다.

결론 (Bottom Line)

초록색 체크표시는 성공 신호가 아니라 종료 코드(exit-code) 신호입니다. 그리고 이 둘 사이의 간극 때문에 두 차례의 사고 동안 8일간의 조용한 실패(silent failure)를 겪어야 했습니다. 해결책은 세 가지 패턴이었습니다. 첫째, 헬스 체크(Health checks)를 가장 먼저 실행하여 토큰이 고장 났을 경우 작업이 시작되기 전에 명확하게 실패를 알리도록 했습니다. 둘째, 큐 부족(Queue-low) 알람은 여유를 두고 5개 항목이 남았을 때 작동하며, 잊혀지기 쉬운 이메일 대신 성가시게 챙겨야 할 GitHub 이슈를 생성합니다. 셋째, 데드 레터 처리(Dead-letter handling)는 실패한 항목을 따로 분류하여 하루에 한 번 재시도하고, 실제 버그인 20%만을 드러냅니다.

이러한 변경 이후, 저는 진심으로 3주 동안 Actions 탭을 열지 않았습니다. 아무것도 굶주리지 않았고, 아무것도 조용히 질식하지 않았으며, 발생한 두 개의 진짜 문제는 제가 놓칠 수 없는 오픈 이슈(open issues) 형태로 전달되었습니다. 이것이 바로 '무시할 수 있다(ignorable)'는 말의 의미입니다. 아무것도 고장 나지 않는다는 뜻이 아니라, 고장이 났을 때 침착함을 유지할 수 있도록 제때 알려준다는 뜻입니다.

이러한 자동화의 전체 설정 과정을 알고 싶다면, Claude Blueprint에서 제가 에이전트 (agents), 큐 (queues), 그리고 알람 (alarms)을 어떻게 서로 연결하는지 자세히 설명합니다. 하나의 크론 (cron) 작업으로 시작하여, 먼저 상태 확인 (health check) 기능을 추가한 다음, 그로부터 점진적으로 확장해 나가세요.

이 글에는 제휴 링크가 포함되어 있습니다. 해당 링크를 통해 가입하시면, 귀하에게 추가 비용이 발생하지 않으면서도 저에게 소정의 수수료가 지급될 수 있습니다. (광고)

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0