매일 도는 배치 파이프라인을 만들 때 흔한 고민이 "이걸 어디서 돌리지"입니다. 상주 서버를 두자니 관리와 비용이 붙고 개인 PC 크론은 꺼지면 끝입니다. 정책 공고 수집 파이프라인에서는 GitHub Actions 크론을 실행 기반으로 선택해 서버 없이 매일 새벽 수집·AI 분류·중복 통합까지 무인으로 돌렸습니다. 이 글은 그 구성과 운영에서 배운 것들입니다.
왜 GitHub Actions였나
이 파이프라인의 실행 특성은 명확했습니다. 하루 한 번, 수십 분 안에 끝나고 상태는 전부 외부 DB에 있습니다. 상주 프로세스가 필요 없는 전형적인 배치입니다. 이런 작업에 서버를 두는 것은 과잉이고 GitHub Actions는 정확히 이 지점에 맞습니다.
- 코드와 스케줄이 같은 저장소에 있어 배포라는 단계 자체가 없습니다. 워크플로 파일을 머지하면 그게 곧 운영 반영입니다.
- 실행 로그와 이력, 수동 재실행 버튼(workflow_dispatch)이 기본 제공됩니다. 배치 운영에서 따로 만들면 은근히 큰 부분입니다.
- 시크릿 저장소가 내장되어 API 키를 코드 밖에서 관리합니다.
물론 전제가 있습니다. 실행 시간이 러너 한도 안에 들어와야 하고 정확한 초 단위 정시성이 필요 없어야 합니다. 크론 스케줄은 정시 보장이 아니라 "그 무렵 실행"에 가깝고 혼잡 시간대에는 대기가 생길 수 있습니다. 우리는 스케줄을 정각 대신 어중간한 분(새벽 4시 17분)에 걸었는데, 정각에 몰리는 예약 혼잡을 피하는 흔한 요령입니다.
파이프라인은 한 덩어리가 아니라 5단계로
워크플로 안에서 파이프라인은 다섯 단계로 나뉘어 순서대로 실행됩니다. 수집, 대표 이미지 추출(일반), 이미지 추출(동적 렌더링 사이트 전용), AI 분류, 의미 중복 통합입니다. 각 단계는 별도 프로세스로 격리되어 한 단계의 비정상 종료가 다른 단계의 상태를 오염시키지 않습니다.
단계를 나누는 기준은 실패의 성격입니다. 수집 실패와 AI 분류 실패는 원인도 대응도 다릅니다. 붙여 놓으면 "어디서부터 다시 돌릴지"가 애매해지지만 나눠 놓으면 실패한 단계만 수동 재실행하면 됩니다. 이런 단계 분리와 오케스트레이션의 일반론은 마이크로서비스 오케스트레이션에서도 같은 원리로 다룬 적이 있습니다.
실패 대응도 같은 구조 덕에 단순해집니다. 어느 단계가 실패하면 워크플로 화면에서 해당 실행의 로그를 열어 원인을 확인하고 수동 트리거로 그 단계부터 다시 돌리면 됩니다. 재실행이 버튼 하나라는 사실은 무인 운영의 심리적 부담을 크게 줄여 줍니다.
무인 운영을 지키는 세 가지 가드
사람이 지켜보지 않는 실행에는 자동 안전장치가 필요합니다. 우리가 넣은 것은 세 가지입니다.
- 시크릿 유출 가드: 워크플로의 첫 단계가 저장소에 평문 키가 섞여 있지 않은지 검사합니다. 무인 파이프라인은 커밋이 곧 운영이므로, 실수로 커밋된 키가 로그와 히스토리에 퍼지기 전에 실행을 차단합니다.
- AI 비용 상한: AI 호출이 들어가는 단계에는 실행당 비용 상한을 겁니다. 데이터 이상으로 처리량이 폭증하면 파이프라인이 조용히 지갑을 태우는 대신 상한에서 멈춥니다.
- 실행 감사 로그: 실행마다 소스별 성공·실패와 수집 전후의 행 수 변화를 DB에 남깁니다. "성공으로 끝났지만 들어온 데이터가 0건"인 조용한 고장을 잡는 것은 에러 로그가 아니라 이 델타 기록입니다.
시간대 함정 하나
GitHub Actions 크론은 UTC 기준입니다. 한국 새벽 4시대에 돌리려면 전날 UTC 19시대로 적어야 합니다. 사소해 보이지만 서머타임이 없는 한국과 달리 UTC 오프셋이 바뀌는 지역 기준으로 생각하다 스케줄이 한 시간 밀리는 실수는 흔합니다. 워크플로 주석에 의도한 현지 시각을 함께 적어 두는 것만으로 예방됩니다.
on:
schedule:
- cron: "17 19 * * *" # KST 04:17 (UTC 19:17)
workflow_dispatch: {} # 수동 재실행용이 배치가 실행하는 파이프라인의 내용, 즉 무엇을 수집하고 어떻게 분류·통합하는지는 공공데이터 크롤링 자동화 사례에 정리했습니다. 배치가 상대하는 공공 API의 함정들은 공공데이터 API 연동 함정에서 이어집니다.
자주 묻는 질문
GitHub Actions로 배치를 돌리면 서버가 정말 필요 없나요?
실행 자체에는 필요 없습니다. 하루 한두 번 유한하게 끝나고 상태를 외부 DB에 두는 배치라면 러너가 실행 환경을 대신합니다. 다만 데이터가 머무는 DB나 스토리지는 별도로 있어야 하고 상시 대기해야 하는 서비스에는 적합하지 않습니다.
크론이 정확한 시간에 실행되지 않는다는데 문제없나요?
분 단위 정시성이 필요한 작업에는 문제가 됩니다. 반대로 "새벽 중 한 번 돌면 되는" 일일 배치에는 수 분의 지연이 무해합니다. 혼잡을 줄이려면 정각 대신 어중간한 분에 스케줄을 걸고 지연에 민감한 작업이라면 전용 스케줄러를 검토하는 것이 맞습니다.
API 키 같은 민감 정보는 어떻게 관리하나요?
저장소 시크릿에 넣고 워크플로에서 환경변수로 주입합니다. 여기에 더해 저장소에 평문 키가 커밋되지 않았는지 검사하는 가드를 워크플로 첫 단계에 두면, 키가 히스토리에 퍼지는 사고를 실행 시점에 차단할 수 있습니다.
실행 시간이 러너 한도에 가까워지면 어떻게 하나요?
단계를 분리해 둔 구조라면 대응이 쉽습니다. 오래 걸리는 단계를 별도 워크플로로 떼어 다른 시간대에 돌리거나 처리량에 상한을 두어 실행당 시간을 예측 가능하게 묶으면 됩니다. 우리도 이미지 추출처럼 무거운 단계에 건수 상한을 두었습니다.
무인 배치가 실패하면 어떻게 알 수 있나요?
두 겹으로 잡습니다. 워크플로 실패는 GitHub이 알려 주고 데이터 수준의 이상은 실행마다 남기는 소스별 감사 로그와 행 수 델타로 드러납니다. 특히 에러 없이 수집량만 주는 고장은 델타 기록 없이는 발견이 늦어집니다.