소개
지원 팀의 상태 모니터에는 같은 간단한 점검을 두 가지 방식으로 실행하는 기능이 필요합니다. 사용자가 요청한 직후 실행하는 방식과 사용자 요청 없이 주기적으로 실행하는 방식입니다. 두 방식을 모두 구현하고, 응답 확인과 작업 완료를 구분하며, 로컬에서 이벤트 수명을 테스트하고 실제 예약된 클라우드 호출을 확인합니다.
이 독립 VM 은 /home/labex/project/task-monitor에서 시작하며 Node.js 22.22.0, 프로젝트 로컬 Wrangler 4.131.1, 격리된 평가에 사용하는 Miniflare 4.20260730.0 및 모의 내부 상태 서비스 픽스처를 제공합니다. 이전에 사용한 서비스 바인딩 및 디바이스 인증 기술을 다시 사용하지만, 이전 VM 이나 리소스는 사용하지 않습니다. 본인의 학습 계정을 사용합니다. 도메인, 스토리지 제품 또는 유료 업그레이드는 필요하지 않습니다. 요청과 예약 호출은 일반적인 계정 사용량에 포함됩니다.
터미널 하나를 계속 열어 둡니다. 1 분 주기의 짧은 일정은 폐기 가능한 테스트용입니다. 마지막에는 로그 스트림을 중지하고, 인증된 상태에서 두 Workers 를 삭제한 뒤, 삭제 여부를 확인하고 로그아웃합니다. 이 실습의 백그라운드 작업은 범위가 제한되고 영속적이지 않습니다. 안정적인 큐 전달을 보장하는 기능으로 사용하지 마세요.
백그라운드 작업이 끝나기 전에 반환하기
이 단계에서는 공개 엔드포인트가 간단한 상태 점검 요청을 확인하는 동안, 제공된 내부 서비스가 비동기 작업을 수행합니다. 이 VM 은 독립적으로 제공되며, 내부 서비스는 이전 실습의 리소스가 아닌 새 픽스처입니다.
cd /home/labex/project/task-monitor
node --version
npx wrangler --version
cat health/index.js
픽스처는 250 밀리초 동안 기다린 다음 모의 JSON 을 반환합니다. 외부 요청을 보내지 않으며 데이터를 저장하지도 않습니다. 고유한 기본 이름을 생성한 다음 공개 Worker 와 내부 HEALTH 서비스를 구성합니다. 이는 앞에서 사용한 표준 서비스 바인딩입니다.
WORKER_NAME="labex-tasks-$(openssl rand -hex 6)"
cat > wrangler.jsonc <<CONFIG
{
"name": "$WORKER_NAME",
"main": "src/index.js",
"compatibility_date": "2026-07-30",
"workers_dev": true,
"preview_urls": false,
"services": [
{
"binding": "HEALTH",
"service": "$WORKER_NAME-health"
}
],
"triggers": {
"crons": []
}
}
CONFIG
cat > health/wrangler.jsonc <<CONFIG
{
"name": "$WORKER_NAME-health",
"main": "index.js",
"compatibility_date": "2026-07-30",
"workers_dev": false,
"preview_urls": false
}
CONFIG
cat > src/index.js <<'JS'
async function checkHealth(env, details) {
const url = new URL('https://health.internal/health');
url.searchParams.set('probe', details.probe);
const response = await env.HEALTH.fetch(url, {signal: AbortSignal.timeout(3000)});
if (!response.ok) throw new Error('health_service_unavailable');
const data = await response.json();
if (data.status !== 'ok' || data.service !== 'labex-health-fixture' || data.probe !== details.probe) {
throw new Error('unexpected_health_response');
}
console.log(JSON.stringify({event: 'health_check', ...details, status: data.status, service: data.service}));
}
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
if (url.pathname === '/health' && request.method === 'GET') {
return Response.json({status: 'ok'}, {headers: {'Cache-Control': 'no-store'}});
}
if (url.pathname !== '/checks') return Response.json({error: 'not_found'}, {status: 404});
if (request.method !== 'POST') {
return Response.json({error: 'method_not_allowed'}, {status: 405, headers: {Allow: 'POST'}});
}
const probe = url.searchParams.get('probe') || '';
if (!/^[a-z0-9-]{1,48}$/.test(probe)) {
return Response.json({error: 'invalid_probe'}, {status: 400});
}
ctx.waitUntil(checkHealth(env, {source: 'request', probe}).catch(() => {
console.error(JSON.stringify({event: 'health_check_failed', source: 'request', probe}));
}));
return Response.json({accepted: true, probe}, {status: 202, headers: {'Cache-Control': 'no-store'}});
}
};
JS
checkHealth는 실제 의존 서비스의 응답을 검증하고 간단한 구조화 결과를 출력합니다. 3 초 타임아웃 신호가 요청에 걸리는 시간을 제한합니다. 공개 핸들러는 프로미스를 ctx.waitUntil에 전달한 뒤 HTTP 202 를 즉시 반환합니다. 202 는 짧은 작업 시도를 수락했다는 의미일 뿐, 영속적인 전달을 보장하지 않습니다. catch는 원시 예외, 요청 또는 자격 증명을 출력하지 않고 실패를 기록합니다. 여기에서 표시한 모의 probe 값만 사용합니다.
npx wrangler dev -c wrangler.jsonc -c health/wrangler.jsonc --ip 0.0.0.0 --port 8080 > dev.log 2>&1 &
프롬프트가 나타날 때까지 기다린 다음 로그를 확인합니다. 공개 개발 서버가 8080 포트에서 준비되었다는 내용이 확인되면 계속합니다. 아직 시작 중이면 잠시 기다렸다가 로그를 다시 읽습니다.
cat dev.log
curl -i http://127.0.0.1:8080/health
curl -i -X POST "http://127.0.0.1:8080/checks?probe=manual-one"
cat dev.log
포그라운드 상태 확인 경로는 200 과 status ok를 반환합니다. POST 요청은 accepted true와 probe manual-one이 포함된 202 를 반환합니다. 의존 서비스 작업이 끝나면 로그에 event health_check, source request, 동일한 probe 및 status ok가 포함됩니다. 로그를 너무 빨리 확인했다면 잠시 후 다시 읽습니다. 응답이 나중에 기록된 로그보다 먼저 도착했다는 사실은 관찰 결과일 뿐, 정확한 성능 측정값이 아닙니다.
서버가 실행 중인 상태에서 검증을 실행합니다. 격리된 런타임은 제어된 게이트 뒤에 자체 상태 픽스처를 두고, 포그라운드 응답이 먼저 도착한 뒤에야 해당 게이트를 열도록 요구합니다. 그런 다음 백그라운드 작업이 완료되었는지도 확인합니다. 공개 상태 경로와 거부되는 메서드 및 probe 사례도 검사합니다. 이 테스트는 Cloudflare 를 호출하지 않습니다.
예약 핸들러를 로컬에서 호출하기
이 단계에서는 Cron 트리거 핸들러에서 상태 점검 작업을 다시 사용합니다. 코드를 변경하기 전에 실제 개발 작업을 확인하고 중지합니다. 예시와 현재 작업 번호가 다르면 현재 작업 번호로 바꿉니다.
jobs
kill %1
cat > src/index.js <<'JS'
async function checkHealth(env, details) {
const url = new URL('https://health.internal/health');
url.searchParams.set('probe', details.probe);
const response = await env.HEALTH.fetch(url, {signal: AbortSignal.timeout(3000)});
if (!response.ok) throw new Error('health_service_unavailable');
const data = await response.json();
if (data.status !== 'ok' || data.service !== 'labex-health-fixture' || data.probe !== details.probe) {
throw new Error('unexpected_health_response');
}
console.log(JSON.stringify({event: 'health_check', ...details, status: data.status, service: data.service}));
}
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
if (url.pathname === '/health' && request.method === 'GET') {
return Response.json({status: 'ok'}, {headers: {'Cache-Control': 'no-store'}});
}
if (url.pathname !== '/checks') return Response.json({error: 'not_found'}, {status: 404});
if (request.method !== 'POST') {
return Response.json({error: 'method_not_allowed'}, {status: 405, headers: {Allow: 'POST'}});
}
const probe = url.searchParams.get('probe') || '';
if (!/^[a-z0-9-]{1,48}$/.test(probe)) {
return Response.json({error: 'invalid_probe'}, {status: 400});
}
ctx.waitUntil(checkHealth(env, {source: 'request', probe}).catch(() => {
console.error(JSON.stringify({event: 'health_check_failed', source: 'request', probe}));
}));
return Response.json({accepted: true, probe}, {status: 202, headers: {'Cache-Control': 'no-store'}});
},
async scheduled(controller, env) {
await checkHealth(env, {
source: 'scheduled',
probe: `cron-${controller.scheduledTime}`,
cron: controller.cron,
scheduledTime: controller.scheduledTime
});
}
};
JS
cat > wrangler.jsonc <<CONFIG
{
"name": "$WORKER_NAME",
"main": "src/index.js",
"compatibility_date": "2026-07-30",
"workers_dev": true,
"preview_urls": false,
"services": [
{
"binding": "HEALTH",
"service": "$WORKER_NAME-health"
}
],
"triggers": {
"crons": [
"* * * * *"
]
}
}
CONFIG
5 개 필드로 구성된 표현식 * * * * *은 UTC 기준으로 1 분마다 실행한다는 뜻입니다. 이처럼 자주 실행되는 일정은 짧은 시간 동안만 사용하는 폐기 가능한 실험용입니다. scheduled는 상태 점검 작업이 끝날 때까지 기다리므로 호출 결과에 완료 여부가 반영됩니다. HTTP 응답을 반환하지는 않습니다. 로그에는 트리거의 실제 cron 표현식과 예약 시간이 기록됩니다.
npx wrangler dev -c wrangler.jsonc -c health/wrangler.jsonc --ip 0.0.0.0 --port 8080 > dev.log 2>&1 &
프롬프트가 나타날 때까지 기다린 다음 로그를 확인합니다. 공개 개발 서버가 8080 포트에서 준비되었다는 내용이 확인되면 계속합니다. 아직 시작 중이면 잠시 기다렸다가 로그를 다시 읽습니다.
cat dev.log
curl -i "http://127.0.0.1:8080/cdn-cgi/local/scheduled?cron=*+*+*+*+*&time=1700000000000&format=json"
cat dev.log
로컬 트리거는 outcome ok를 반환해야 하며, 애플리케이션 로그에는 source scheduled, cron * * * * * 및 scheduledTime 1700000000000이 포함되어야 합니다. 이 과거 타임스탬프는 의도적으로 입력한 모의 테스트 값이며, 현재 클라우드에서 실행되었다는 증거가 아닙니다. 검증을 사용해 다른 제어된 예약 시간을 실행하고 상태 서비스가 호출되었는지 확인합니다.
HTTP 호출의 경우 waitUntil은 응답이 전송되거나 클라이언트 연결이 끊긴 뒤 최대 30 초 동안 작업을 계속 실행할 수 있습니다. 이 허용 시간은 해당 요청의 백그라운드 프로미스가 공유합니다. 이는 영속적인 큐나 재시도 보장이 아닙니다. 이 실습의 3 초 제한 상태 점검 작업은 이러한 방식에 적합합니다. 안정적인 전달 또는 재시도가 필요한 작업은 이 실습의 범위를 벗어난 적절한 큐 또는 워크플로 설계를 사용해야 합니다. 호출 수명 주기가 어떻게 다른지 알아보려면 context API 및 scheduled handler 문서를 참조하세요.
배포하고 요청 백그라운드 작업 관찰하기
이 단계에서는 본인의 학습 계정에 새 Worker 두 개를 배포합니다. 실제 로컬 작업을 중지한 다음, 익숙한 디바이스 인증 흐름을 사용해 이 새 VM 을 인증합니다.
jobs
kill %1
npx wrangler login --device --browser=false --scopes account:read user:read workers_scripts:write workers_tail:read
표시된 디바이스 링크를 로그인된 브라우저에서 열고 코드를 입력한 다음, 요청된 액세스 권한을 검토하고 학습 계정을 선택합니다. 터미널에 성공 메시지가 나타날 때까지 기다립니다.
npx wrangler whoami --json
계정이 하나만 표시되더라도 계정 이름을 확인합니다. 아래 두 구성에서 YOUR_ACCOUNT_ID를 실제 ID 로 바꿉니다. 앞에서 생성한 Worker 이름과 1 분 주기를 그대로 유지합니다. 공개 Worker 에서는 Workers Logs 도 활성화하므로 해당 Worker 의 호출 로그와 애플리케이션 로그를 Dashboard 의 Observability에서 확인할 수 있습니다. 이는 실시간 wrangler tail 연결과 별개입니다. 이 폐기 가능한 실습은 모의 상태 점검 데이터만 출력합니다. 자격 증명은 절대 로그에 기록하지 마세요. 자세한 내용은 Workers Logs 문서를 참조하세요.
cat > wrangler.jsonc <<CONFIG
{
"name": "$WORKER_NAME",
"main": "src/index.js",
"compatibility_date": "2026-07-30",
"workers_dev": true,
"preview_urls": false,
"services": [
{
"binding": "HEALTH",
"service": "$WORKER_NAME-health"
}
],
"triggers": {
"crons": [
"* * * * *"
]
},
"observability": {
"enabled": true
},
"account_id": "YOUR_ACCOUNT_ID"
}
CONFIG
cat > health/wrangler.jsonc <<CONFIG
{
"name": "$WORKER_NAME-health",
"main": "index.js",
"compatibility_date": "2026-07-30",
"workers_dev": false,
"preview_urls": false,
"account_id": "YOUR_ACCOUNT_ID"
}
CONFIG
npx wrangler deploy -c health/wrangler.jsonc
npx wrangler deploy
공개 Worker 의 바인딩이 내부 서비스를 확인할 수 있도록 내부 상태 서비스를 먼저 배포합니다. 공개 Worker 의 배포 출력에 schedule: * * * * *가 포함되어 있는지 확인합니다. 아래에 실제 workers.dev 주소를 복사합니다.
APP_URL="https://YOUR_WORKER.YOUR_SUBDOMAIN.workers.dev"
curl -i "$APP_URL/health"
npx wrangler tail --format pretty > events.log 2> tail-errors.log &
tail 연결이 초기화되도록 몇 초 기다린 다음 모의 점검을 하나 보냅니다.
sleep 5
curl -i -X POST "$APP_URL/checks?probe=remote-one"
cat events.log
POST 이벤트와 함께 source request, probe remote-one이 포함된 health_check 로그를 찾습니다. 아직 이벤트가 보이지 않으면 tail-errors.log를 확인하고 잠시 기다린 다음 요청을 다시 보내고 events.log를 다시 읽습니다. 이전 실습에서 사용한 로그 파일은 이 배포의 증거가 아닙니다.
Dashboard 에서 이 계정의 Compute → Workers & Pages 를 엽니다. 정확한 이름 두 개와 공개 Worker 주소, 그리고 일치하는 내부 서비스에 연결된 HEALTH 바인딩을 확인합니다. 검증을 사용합니다. 검증은 인증된 소유권과 엔드포인트/바인딩 메타데이터를 확인한 다음 별도의 짧은 실시간 tail 세션을 열고 새 probe 를 보내 백그라운드 작업이 완료되는지 확인합니다. 평가자는 사용자의 events.log를 증거로 인정하지 않습니다. 다음 단계까지 학습용 tail 작업을 실행한 상태로 둡니다.
실제 Cron 실행 관찰하기
이 단계에서는 배포 구성과 실행을 구분합니다. Dashboard 에서 공개 Worker 를 열고 Settings → Trigger events → Cron triggers 로 이동합니다. 일정이 Every minute 으로 표시되는지 확인합니다. Next time 은 예측값이지 완료된 실행을 뜻하지 않습니다. 일정이 구성되어 있다는 사실만으로 핸들러가 실행되었다고 볼 수 없습니다.
아래 예시는 지원되는 가장 짧은 주기인 * * * * *, 즉 1 분에 한 번 실행하는 설정입니다. Worker 이름을 본인의 구성과 비교합니다. 이름과 표시 시간은 예시이므로, Wrangler 가 이미 구성한 경우 Dashboard 에서 트리거를 추가할 필요가 없습니다.

기존 학습용 tail 연결을 계속 실행합니다. 실제 예약 이벤트가 발생할 때까지 기다린 다음 같은 로그 파일을 확인합니다.
sleep 60
cat events.log
읽기 쉬운 tail 출력에서 cron 표현식과 실행 시간으로 식별되는 성공적인 scheduled 호출을 찾습니다. 해당 health_check 로그에는 source scheduled, cron * * * * *, status ok, service labex-health-fixture 및 scheduledTime이 포함되어야 합니다. probe 는 cron- 뒤에 해당 scheduledTime이 이어진 형태입니다. 독립 검증기는 Cloudflare 의 구조화된 이벤트 메타데이터와 이 애플리케이션 로그를 별도로 비교합니다. POST 이벤트, 로컬에서 생성한 모의 타임스탬프 또는 비어 있는 로그만으로는 이 결과를 확인할 수 없습니다.
브라우저에서 해당 출력을 확인하려면 같은 Worker 의 Observability → Events 페이지를 엽니다. 기다리는 동안에는 Live를 사용하거나, 배포 시간이 포함된 시간 범위로 저장된 이벤트 쿼리를 새로 고칩니다. 이 핸들러의 호출 행에는 * * * * *가 표시됩니다. 하나를 확장하고 View invocation을 선택한 다음 연결된 애플리케이션 로그 행을 확장해 상태 점검 필드를 확인합니다. 구조화된 애플리케이션 로그는 Message 셀이 비어 있을 수 있습니다. 데이터가 없다고 판단하지 말고 행을 확장하세요. 읽는 동안 실시간 표시를 일시 중지할 수 있습니다.
이 실제 예시에서 source는 scheduled, status는 ok, service는 labex-health-fixture입니다. cron-... probe 는 애플리케이션 로그의 밀리초 단위 scheduledTime과 일치합니다. Dashboard 는 표시된 시간대 (여기서는 GMT+8) 로 보이는 타임스탬프를 표시하지만, Cron 일정은 UTC 를 사용합니다. Worker 이름, 호출 ID 및 시간은 사용자 환경에서 달라집니다. 이 필드를 호출 결과와 함께 확인하세요. 스크린샷만으로는 완료를 증명할 수 없습니다.

Cron 업데이트가 전파되는 데 최대 15 분이 걸릴 수 있습니다. 1 분 대기 및 확인 과정을 반복하되, 성공적으로 배포된 시점부터 최대 17 분까지만 기다립니다. 스트림이 비어 있거나 중지되었다면 tail-errors.log를 확인합니다. 이 제한 시간 안에 일치하는 이벤트가 나타나지 않으면 중지하고 구성, 인증 및 트리거 상태를 진단합니다. 성공했다고 보고하지 마세요. 이는 정확한 실행 지연 시간을 보장하는 과정이 아니라, 범위가 제한된 학습 관찰입니다. Cron Triggers 문서에서 전파 및 UTC 일정에 대해 설명합니다. 저장된 Workers Logs 가 표시되는 데도 잠시 걸릴 수 있으므로 수집 시간을 고려한 뒤 쿼리를 새로 고칩니다. 새 Worker 의 별도 Past Cron Events 기록에는 이벤트가 표시되기까지 최대 30 분이 걸릴 수 있습니다. 기록이 비어 있다고 실패한 것은 아닙니다. 이 실습의 제한 시간 안에서 위의 실시간 관찰 방법을 사용합니다.
실행을 관찰한 다음 검증을 실행합니다. 검증은 배포된 일정을 확인하고 최대 70 초 동안 별도의 실시간 스트림을 감시해 상태 결과가 포함된 실제 예약 이벤트를 찾습니다. 연결이 시작된 뒤 1 분 경계를 충분히 한 번 지나는 시간입니다. 이벤트가 없다는 결과만으로는 판단할 수 없으므로 연결 및 전파 상태를 확인하고 같은 관찰 시간 범위 안에서 다시 시도합니다. 평가자는 예약 이벤트를 생성하지 않습니다. 검증이 성공한 뒤에만 학습용 tail 을 중지합니다. 실제 작업 번호를 사용합니다.
jobs
kill %1
다음 단계에서 폐기 가능한 일정을 즉시 종료합니다. 1 분마다 실행되는 학습용 작업을 방치하지 마세요.
예약 테스트용 Worker 두 개 삭제하기
이 단계에서는 인증된 상태를 유지하면서 공개 Worker 와 해당 트리거를 삭제한 다음, 내부 상태 픽스처를 삭제합니다. 두 구성에 이 실습의 정확한 이름과 대상 계정이 포함되어 있는지 확인합니다.
cat wrangler.jsonc
cat health/wrangler.jsonc
npx wrangler delete
npx wrangler delete -c health/wrangler.jsonc
각 일치하는 이름 확인 프롬프트에서 단일 키 y를 누릅니다. 관련 없는 프로젝트, 계정 및 서브도메인은 유지합니다. 고정된 Wrangler 는 Worker 삭제 후 알려진 레거시 KV 정리 인증 진단 메시지를 출력할 수 있습니다. 해당 메시지나 네트워크 요청 실패만으로는 삭제가 완료되었다고 볼 수 없습니다. 이 진단을 해결하려고 권한을 확대하지 마세요.
Dashboard 를 새로 고친 다음 검증을 사용합니다. 인증된 Worker 목록에 두 이름이 모두 없으면 성공입니다. 이는 배포된 리소스가 제거되었음을 확인하지만, 모든 스케줄러 변경 사항이 전 세계에 즉시 전파되었다는 뜻은 아닙니다. 로그아웃하거나 VM 을 닫는 것만으로는 이 정리가 수행되지 않습니다.
실습 VM 연결 해제하기
이 단계에서는 이 VM 의 연결을 해제하기 전에 tail 작업이 중지되었고 리소스가 삭제되었는지 확인합니다.
jobs
npx wrangler logout
npx wrangler whoami --json
loggedIn: false가 명시적으로 표시되어야 합니다. 구조화된 비인증 명령은 0 이 아닌 종료 코드를 반환할 수 있습니다. 네트워크 오류는 동일한 결과가 아닙니다. 검증을 사용한 다음 VM 을 종료합니다. 브라우저 로그인은 이후의 독립 실습을 위해 유지해도 됩니다.
요약
waitUntil을 사용해 포그라운드에서 요청을 확인한 뒤 제한된 상태 점검 작업이 완료되도록 했고, 같은 작업을 예약 핸들러에서도 다시 사용했습니다. 제어된 로컬 테스트로 응답과 지연된 작업을 분리했으며, 실제 클라우드 이벤트를 통해 배포 후 예약 실행이 이루어졌음을 확인했습니다. 구성, 수동 호출 및 실시간 실행은 서로 다른 유형의 증거를 제공했습니다.
소유권과 서비스 바인딩을 확인하고, 모의 probe 를 구조화된 로그와 대조했으며, 호출 수명 제한을 준수하고, 연결을 해제하기 전에 폐기 가능한 예약 애플리케이션을 삭제했습니다. 안정적인 장기 전달에는 이 단기 백그라운드 작업 패턴과 다른 아키텍처가 필요합니다.

