GitHub Actions의 일일 CI 트리거를 cron-job.org로 이전하고 Render Keep-Alive Cron 추가하기
요약: 불안정한 GitHub Actions 스케줄을 신뢰할 수 있는 cron-job.org 트리거로 대체하고, Render 서비스용 경량 keep-alive 엔드포인트를 추가했습니다. 이 변경으로 인해 누락되던 빌드가 사라지고, GitHub 사용 한도가 줄었으며, 비업무 시간 동안 Render 컨테이너가 잠드는 것을 방지할 수 있었습니다.
문제점
저희 모노레포 VS는 S3에 정적 에셋을 게시하고 몇 가지 캐시 키를 새로 고치는 일일 빌드를 실행합니다. 원래 이것은 schedule 트리거를 사용하는 GitHub Actions 워크플로우(.github/workflows/daily.yml)로 구동되었습니다:
on:
schedule:
- cron: '0 3 * * *' # UTC 기준 오전 3시
한 달 후, 저희는 간헐적인 실패를 보기 시작했습니다:
Error: Resource not found
at /home/runner/work/_temp/...
근본 원인은 두 가지였습니다:
- GitHub Actions 사용 한도: 저희 조직이 예약된 작업의 무료 티어 한도에 도달하여, 워크플로우가 명확한 오류 없이 건너뛰어졌습니다.
- 시간대 드리프트(Timezone drift): cron 표현식은 UTC로 해석되었지만, 저희 팀은 이 작업이 CST 기준 오전 3시에 실행되기를 기대했기 때문에 6시간의 오프셋이 발생했고 간헐적인 캐시 만료가 있었습니다.
저희는 로컬 시간대를 존중하고 GitHub 사용 할당량에 포함되지 않는 더 신뢰할 수 있는 스케줄러가 필요했습니다.
제가 처음 시도한 것들
제 첫 번째 시도는 GitHub Actions 워크플로우를 유지하되, 건너뛰어졌을 때 수동으로 다시 실행할 수 있도록 workflow_dispatch 트리거를 추가하는 것이었습니다:
on:
schedule:
- cron: '0 3 * * *'
...
또한 저는 concurrency 한도를 늘리려고 시도했고, 별도의 워크플로우 파일에 대체 cron을 추가했습니다. 두 접근 방식 모두 실패했습니다:
- 수동 재실행: 사람이 필요하여 자동화의 목적을 달성하지 못했습니다.
- 중복 워크플로우: 경쟁 조건(race conditions)을 일으켜, 때로는 두 개의 빌드가 연속으로 실행되어 서로의 아티팩트를 덮어썼습니다.
이 시점에서 저는 스케줄러 자체가 약한 고리임을 깨달았습니다.
구현 과정
1. 경량 keep-alive 스크립트 생성
Render는 비활성 상태가 15분 동안 지속되면 컨테이너를 자동으로 절전 모드(sleeps)로 전환합니다. 저희의 일일 빌드는 빌드 스크립트에서 사용되는 작은 API를 제공하기 때문에 Render 서비스가 깨어 있도록 유지해야 했습니다. 그래서 저는 10분마다 /health 엔드포인트에 요청을 보내는 아주 작은 TypeScript 스크립트를 추가했습니다.
파일: scripts/keep-alive.ts
import fetch from 'node-fetch';
const URL = process.env.RENDER_HEALTH_URL ?? 'https://my-app.onrender.com/health';
...
이를 package.json에 추가했습니다:
{
"scripts": {
"keep-alive": "ts-node scripts/keep-alive.ts"
...
2. keep-alive 스크립트를 cron-job.org로 배포하기
cron-job.org에서 다음 설정으로 새로운 크론 작업을 생성했습니다:
- URL:
https://api.myapp.com/run-keep-alive
- Method:
POST
- Headers:
Content-Type: application/json
- Body:
{ "secret": "RENDER_KEEP_ALIVE_TOKEN" }
- Schedule:
*/10 * * * * (10분마다)
Render 측에서는 단순히 keep-alive 스크립트를 실행하는 새로운 엔드포인트(/run-keep-alive)를 추가했습니다:
파일: src/routes/keepAlive.ts
import { Router } from 'express';
import { exec } from 'child_process';
...
이 라우트를 메인 서버에 추가했습니다:
import keepAliveRouter from './routes/keepAlive';
app.use(keepAliveRouter);
3. 일일 CI 트리거를 cron-job.org로 이전하기
.github/workflows/daily.yml에서 schedule 블록을 제거하여 GitHub Actions 스케줄링을 비활성화하고, 디버깅용 수동 트리거만 유지했습니다:
다음으로, cron-job.org에서 GitHub API를 통해 GitHub Actions 워크플로우를 호출하는 새로운 크론 작업을 생성했습니다. 이를 위해서는 repo 스코프를 가진 개인 액세스 토큰이 필요했습니다.
Cron‑job.org 설정:
- URL:
https://api.github.com/repos/roblo/vs/actions/workflows/daily.yml/dispatches
- Method:
POST
- Headers:
Authorization: token $GITHUB_TOKEN
Accept: application/vnd.github.v3+json
- Body:
{ "ref": "main" }
- Schedule:
0 3 * * * (CST 기준 오전 3시 → UTC 기준 0 9 * * *)
저는 이 토큰을 cron‑job.org의 Secure Variables에 GITHUB_TOKEN으로 저장했습니다.
4. 새로운 흐름 확인하기
새로운 트리거가 작동하는지 확인하기 위해, 워크플로우 시작 부분에 간단한 로깅 단계를 추가했습니다:
jobs:
build:
runs-on: ubuntu-latest
...
첫 실행 후, 워크플로우 로그에는 다음 내용이 표시되었습니다:
Triggered by workflow_dispatch at Thu Oct 22 09:00:01 UTC 2026
빌드는 성공적으로 완료되었고, Render 서비스는 밤새 깨어 있었습니다 (Render 대시보드의 로그에서 확인 가능).
5. 오래된 파일 정리하기
이제 사용하지 않는 cron 설정을 package.json에서 제거하고, 구식인 scripts/old-scheduler.sh 파일을 삭제했습니다. 이 PR(#30 chore(cron))의 최종 diff는 다음과 같습니다:
--- a/.github/workflows/daily.yml
+++ b/.github/workflows/daily.yml
@@
...
핵심 정보 (Key)
저의 Build in Public 시리즈의 일부입니다 — 멕시코 플라야 델 카르멘에서 Building PlayaMXCRM을 구축하는 실제 과정을 공유합니다.
레포: zaerohell/VS · 2026-10-20
#playadev #buildinpublic