첫 번째 Cloudflare 작업자: 가져오기 핸들러, Wrangler 및 배포
Cloudflare 작업자를 생성, 테스트, 배포하는 방법에 대한 단계별 가이드 0: 가져오기 핸들러에서 랭글러 배포까지, 환경 변수를 통과하고, 비밀과 고급 라우팅.
제로에서 생산 작업자로
이전 기사에서 우리는 V8 분리와 감기를 제거하는 이유를 이해했습니다. 시작합니다. 이 글에서 우리는 구체적인 것을 구축합니다. 라우팅하고, 환경 변수를 읽고, 비밀을 사용하고, JSON으로 응답합니다. 마지막에 Cloudflare 계정에 작업자가 배포되어 전 세계적으로 접근할 수 있습니다. 시작 1ms 미만.
무엇을 배울 것인가
- Wrangler CLI 및 TypeScript를 사용한 개발 환경 설정
- 파일 구조
wrangler.toml그리고 모든 주요 필드 - 가져오기 핸들러: request, env, ExecutionContext 세부정보
- 복잡한 URL 패턴에 대한 수동 및 itty-router 라우팅
- 환경 변수, 비밀 및 유형이 안전한 TypeScript에서 이에 액세스하는 방법
- 로컬 테스트
wrangler dev그리고 핫 리로드 - 프로덕션 및 환경 관리에 배포(스테이징/프로덕션)
- 사용자 정의 도메인 및 경로 패턴
전제 조건 및 초기 설정
무료 Cloudflare 계정과 Node.js 18 이상이 필요합니다. 무료 등급 하루에 100,000개의 요청이 포함되어 있으며 이는 개발에 충분한 양이며 개인 프로젝트.
# Installa Wrangler globalmente (CLI ufficiale Cloudflare)
npm install -g wrangler
# Verifica l'installazione
wrangler --version
# wrangler 3.x.x
# Autenticati con il tuo account Cloudflare
# Apre il browser per il login OAuth
wrangler login
# Verifica che l'autenticazione abbia funzionato
wrangler whoami
# Logged in as: tuo@email.com
# Account ID: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
작업자 프로젝트 만들기
랭글러는 명령을 제공합니다 create 누가 프로젝트를 진행하는지
TypeScript 및 필요한 모든 종속성:
# Crea un nuovo Worker con template TypeScript
npm create cloudflare@latest mio-primo-worker -- --type worker
# Oppure con Wrangler direttamente
wrangler init mio-primo-worker --type worker
# Struttura del progetto generata:
mio-primo-worker/
├── src/
│ └── index.ts # Entry point del Worker
├── test/
│ └── index.spec.ts # Test con Vitest (opzionale)
├── wrangler.toml # Configurazione principale
├── tsconfig.json # TypeScript config
├── package.json
└── .gitignore
cd mio-primo-worker
npm install
wrangler.toml: 중앙 구성
파일 wrangler.toml 이는 작업자 구성의 핵심입니다.
각 필드는 배포 동작에 영향을 미칩니다.
# wrangler.toml
# Nome del Worker (deve essere unico nel tuo account)
name = "mio-primo-worker"
# Versione del formato di configurazione
main = "src/index.ts"
# Compatibilita: usa sempre una data recente per avere le ultime API
compatibility_date = "2025-11-01"
# Flags per abilitare funzionalita sperimentali/stabili
compatibility_flags = ["nodejs_compat"]
# Workers paused: false (default)
# workers_dev = true # Abilita il sottodominio .workers.dev (default true)
# Variabili d'ambiente (visibili nel codice, NON secret)
[vars]
ENVIRONMENT = "production"
API_BASE_URL = "https://api.esempio.it"
MAX_RETRIES = "3"
# Route patterns: il Worker risponde a questi URL
# (richiede un dominio nel tuo account Cloudflare)
# [[routes]]
# pattern = "esempio.it/api/*"
# zone_name = "esempio.it"
# Ambiente di staging (sovrascrive i valori di root)
[env.staging]
name = "mio-primo-worker-staging"
vars = { ENVIRONMENT = "staging" }
# Binding KV Namespace (vedi articolo 3)
# [[kv_namespaces]]
# binding = "CACHE"
# id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# preview_id = "yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"
가져오기 핸들러: 완전한 해부학
각 작업자는 메서드를 사용하여 기본 객체를 내보내야 합니다. fetch.
각 매개변수를 자세히 살펴보겠습니다.
// src/index.ts
export interface Env {
// Variabili definite in [vars] di wrangler.toml
ENVIRONMENT: string;
API_BASE_URL: string;
MAX_RETRIES: string;
// Secrets aggiunti con wrangler secret put
DATABASE_URL: string;
API_KEY: string;
// Binding KV (se configurato)
// CACHE: KVNamespace;
}
export default {
/**
* Fetch handler: chiamato per ogni richiesta HTTP
*
* @param request - La richiesta HTTP in arrivo (Request standard WhatWG)
* @param env - I binding: variabili, secrets, KV, R2, D1, Durable Objects, AI
* @param ctx - Execution context: waitUntil() per operazioni post-risposta
*/
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
// Informazioni Cloudflare sulla richiesta
const cf = request.cf;
console.log({
country: cf?.country, // "IT", "US", "DE", ...
city: cf?.city, // "Milan", "New York", ...
datacenter: cf?.colo, // "MXP" (Milano), "LHR" (Londra), ...
asn: cf?.asn, // Numero ASN dell'ISP
tlsVersion: cf?.tlsVersion, // "TLSv1.3"
});
const url = new URL(request.url);
const { pathname, searchParams } = url;
// Routing semplice basato sul pathname
if (pathname === '/') {
return handleRoot(request, env);
}
if (pathname.startsWith('/api/')) {
return handleApi(request, env, ctx, pathname);
}
if (pathname === '/health') {
return new Response(JSON.stringify({ status: 'ok', env: env.ENVIRONMENT }), {
headers: { 'Content-Type': 'application/json' },
});
}
return new Response('Not Found', {
status: 404,
headers: { 'Content-Type': 'text/plain' },
});
},
};
async function handleRoot(request: Request, env: Env): Promise<Response> {
const html = `
<!DOCTYPE html>
<html>
<head><title>Mio Primo Worker</title></head>
<body>
<h1>Hello from ${env.ENVIRONMENT}!</h1>
<p>Cloudflare Worker is running.</p>
</body>
</html>
`;
return new Response(html, {
headers: { 'Content-Type': 'text/html; charset=utf-8' },
});
}
async function handleApi(
request: Request,
env: Env,
ctx: ExecutionContext,
pathname: string,
): Promise<Response> {
// Verifica l'API key (dal header Authorization)
const authHeader = request.headers.get('Authorization');
if (!authHeader || authHeader !== `Bearer ${env.API_KEY}`) {
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
status: 401,
headers: { 'Content-Type': 'application/json' },
});
}
// Proxy verso il backend con retry logic
const maxRetries = parseInt(env.MAX_RETRIES);
const backendUrl = `${env.API_BASE_URL}${pathname}`;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch(backendUrl, {
method: request.method,
headers: request.headers,
body: request.method !== 'GET' ? request.body : undefined,
});
if (response.ok || attempt === maxRetries - 1) {
return response;
}
} catch (error) {
if (attempt === maxRetries - 1) {
return new Response(JSON.stringify({ error: 'Backend unavailable' }), {
status: 502,
headers: { 'Content-Type': 'application/json' },
});
}
// Exponential backoff (attenzione: consuma CPU time)
await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 100));
}
}
return new Response(JSON.stringify({ error: 'Max retries exceeded' }), { status: 502 });
}
itty-router를 사용한 고급 라우팅
경로가 많은 애플리케이션의 경우 수동 라우팅 작성이 장황해집니다. 도서관 이티 라우터 근로자를 위해 특별히 설계되었습니다. 소문자(1KB 미만)이며 동일한 표준 웹 API를 사용합니다.
# Installa itty-router
npm install itty-router
// src/index.ts con itty-router
import { Router, error, json, withParams } from 'itty-router';
export interface Env {
ENVIRONMENT: string;
API_KEY: string;
}
const router = Router();
// Middleware globale: aggiunge params al request object
router.all('*', withParams);
// Middleware di autenticazione
const authenticate = (request: Request & { params: Record<string, string> }, env: Env) => {
const apiKey = request.headers.get('x-api-key');
if (apiKey !== env.API_KEY) {
return error(401, 'Invalid API key');
}
// Ritornare undefined continua al prossimo handler
};
// Route pubbliche
router.get('/', () => new Response('Hello from Workers!'));
router.get('/health', (req: Request, env: Env) =>
json({ status: 'ok', environment: env.ENVIRONMENT, timestamp: Date.now() })
);
// Route con parametri URL
router.get('/users/:userId', authenticate, async (request, env: Env) => {
const { userId } = request.params;
// Fetch dall'origine con parametri
const user = await fetchUser(userId, env);
if (!user) {
return error(404, `User ${userId} not found`);
}
return json(user);
});
// Route con query parameters
router.get('/search', async (request: Request) => {
const url = new URL(request.url);
const query = url.searchParams.get('q');
const page = parseInt(url.searchParams.get('page') ?? '1');
const limit = Math.min(parseInt(url.searchParams.get('limit') ?? '10'), 100);
if (!query) {
return error(400, 'Query parameter "q" is required');
}
return json({
query,
page,
limit,
results: [], // qui andrebbero i risultati reali
});
});
// POST con body JSON
router.post('/users', authenticate, async (request: Request, env: Env) => {
let body: unknown;
try {
body = await request.json();
} catch {
return error(400, 'Invalid JSON body');
}
// Validazione base
if (typeof body !== 'object' || body === null || !('name' in body)) {
return error(422, 'Field "name" is required');
}
// Logica di business...
return json({ created: true, data: body }, { status: 201 });
});
// Catch-all per 404
router.all('*', () => error(404, 'Route not found'));
async function fetchUser(userId: string, env: Env) {
// Esempio: fetch da un'API esterna
const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
if (!response.ok) return null;
return response.json();
}
export default {
fetch: router.fetch,
};
환경 변수 및 비밀 관리
작업자는 두 가지 유형의 구성 값을 구별합니다. 변수 (코드와 Wrangler에서 볼 수 있으며 암호화되지 않음) e 기미 (암호화되어 런타임 시 작업자에서만 볼 수 있으며 대시보드에서는 지워지지 않습니다)
# Aggiungere un secret (interattivo: chiede il valore)
wrangler secret put DATABASE_URL
# Enter a secret value: [digitare il valore, non visibile]
# Successfully created secret DATABASE_URL
# Aggiungere un secret da stdin (utile in CI/CD)
echo "postgres://user:password@host:5432/db" | wrangler secret put DATABASE_URL
# Listare i secrets (mostra solo i nomi, non i valori)
wrangler secret list
# [
# { "name": "DATABASE_URL", "type": "secret_text" },
# { "name": "API_KEY", "type": "secret_text" }
# ]
# Eliminare un secret
wrangler secret delete DATABASE_URL
# Aggiungere secrets per ambiente specifico
wrangler secret put DATABASE_URL --env staging
wrangler secret put DATABASE_URL --env production
변수와 비밀: 언제 무엇을 사용해야 하는가
- 변수([vars]): 공개 API URL, 기능 플래그, 구성 환경의. Wrangler.toml에 표시되므로 민감한 데이터에는 사용하지 마세요. 그리고 Cloudflare Dashboard에 있습니다.
- 기미: API 키, 데이터베이스 비밀번호, JWT 토큰, 모든 데이터 민감하다. 암호화되어 로그에도 공개되지 않습니다.
Wrangler 개발자를 통한 로컬 개발
wrangler dev 작업자 런타임을 시뮬레이션하는 로컬 서버를 시작합니다.
자동 핫 리로드:
# Avvia il server di sviluppo locale
npm run dev # equivale a: wrangler dev
# Output tipico:
# ⛅️ wrangler 3.x.x
# -------------------
# Using vars defined in .dev.vars
# Your Worker has access to the following bindings:
# - Vars:
# - ENVIRONMENT: "development"
# - API_BASE_URL: "https://api.esempio.it"
# ⎔ Starting local server...
# [wrangler:inf] Ready on http://localhost:8787
# In un altro terminale, testa le route:
curl http://localhost:8787/health
# {"status":"ok","environment":"development","timestamp":1742000000000}
curl -H "x-api-key: test-key" http://localhost:8787/users/1
# {"id":1,"name":"Leanne Graham","username":"Bret",...}
로컬 비밀의 경우 파일을 만듭니다. .dev.vars (추가 예정
.gitignore!):
# .dev.vars - NON committare questo file!
# Questi valori sovrascrivono [vars] in wrangler.toml durante lo sviluppo locale
DATABASE_URL=postgres://localhost:5432/dev_db
API_KEY=dev-test-key-12345
STRIPE_WEBHOOK_SECRET=whsec_test_xxxxx
# Per usare .dev.vars anche per variabili non-secret:
ENVIRONMENT=development
API_BASE_URL=http://localhost:3000
프로덕션에 배포
배포는 TypeScript를 컴파일하여 종속성을 번들로 묶는 단일 명령입니다. esbuild를 사용하여 결과를 300개 이상의 Cloudflare PoP에 업로드합니다.
# Deploy all'ambiente di default (production)
wrangler deploy
# Output:
# ⛅️ wrangler 3.x.x
# -------------------
# Total Upload: 45.32 KiB / gzip: 12.18 KiB
# Uploaded mio-primo-worker (1.23 sec)
# Published mio-primo-worker (0.45 sec)
# https://mio-primo-worker.tuo-account.workers.dev
# Current Deployment ID: xxxxxxxxxx
# Current Version ID: yyyyyyyyyy
# Deploy su ambiente specifico
wrangler deploy --env staging
# Preview del deploy senza pubblicare
wrangler deploy --dry-run
# Verificare il Worker deployato
curl https://mio-primo-worker.tuo-account.workers.dev/health
사용자 정의 도메인 및 경로 패턴
작업자를 사용자 지정 도메인(Cloudflare에 있어야 함)과 연결하려면 다음을 구성하세요.
의 경로 wrangler.toml:
# wrangler.toml - configurazione route con dominio custom
name = "api-gateway"
main = "src/index.ts"
compatibility_date = "2025-11-01"
# Opzione 1: route pattern (piu flessibile)
# Corrisponde a: esempio.it/api/ e tutti i sottopercorsi
[[routes]]
pattern = "esempio.it/api/*"
zone_name = "esempio.it"
# Opzione 2: custom domain diretto (Workers For Platforms)
# [env.production]
# routes = [{ pattern = "api.esempio.it/*", zone_name = "esempio.it" }]
# Opzione 3: Worker Routes nel dashboard Cloudflare
# (utile per pattern complessi o gestione manuale)
오류 관리 및 응답 도우미
강력한 오류 처리 패턴은 전역 래퍼를 사용합니다.
// src/utils/error-handler.ts
export function handleError(error: unknown): Response {
if (error instanceof WorkerError) {
return new Response(JSON.stringify({
error: error.message,
code: error.code,
}), {
status: error.status,
headers: { 'Content-Type': 'application/json' },
});
}
// Errore non atteso: non esporre dettagli in produzione
console.error('Unexpected error:', error);
return new Response(JSON.stringify({ error: 'Internal server error' }), {
status: 500,
headers: { 'Content-Type': 'application/json' },
});
}
export class WorkerError extends Error {
constructor(
message: string,
public readonly status: number = 500,
public readonly code: string = 'INTERNAL_ERROR'
) {
super(message);
this.name = 'WorkerError';
}
static notFound(resource: string): WorkerError {
return new WorkerError(`${resource} not found`, 404, 'NOT_FOUND');
}
static unauthorized(): WorkerError {
return new WorkerError('Unauthorized', 401, 'UNAUTHORIZED');
}
static badRequest(message: string): WorkerError {
return new WorkerError(message, 400, 'BAD_REQUEST');
}
}
// src/index.ts - integrazione con il fetch handler
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
try {
return await handleRequest(request, env, ctx);
} catch (error) {
return handleError(error);
}
},
};
CORS: 작업자의 교차 출처 관리
다른 도메인의 브라우저에서 호출해야 하는 API의 경우 CORS를 구성하세요. 작업자에서 직접:
// src/middleware/cors.ts
const ALLOWED_ORIGINS = [
'https://esempio.it',
'https://www.esempio.it',
'http://localhost:3000', // solo in development
];
export function corsHeaders(request: Request, origin?: string): HeadersInit {
const requestOrigin = origin ?? request.headers.get('Origin') ?? '';
const allowed = ALLOWED_ORIGINS.includes(requestOrigin) ? requestOrigin : '';
return {
'Access-Control-Allow-Origin': allowed,
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization, x-api-key',
'Access-Control-Max-Age': '86400',
'Vary': 'Origin',
};
}
// Gestisci il preflight OPTIONS
export function handleCors(request: Request): Response | null {
if (request.method === 'OPTIONS') {
return new Response(null, {
status: 204,
headers: corsHeaders(request),
});
}
return null; // Continua con il normale handling
}
// Integrazione nel fetch handler
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const corsResponse = handleCors(request);
if (corsResponse) return corsResponse;
const response = await router.fetch(request, env);
// Aggiungi headers CORS a tutte le risposte
const newResponse = new Response(response.body, response);
Object.entries(corsHeaders(request)).forEach(([key, value]) => {
newResponse.headers.set(key, value);
});
return newResponse;
},
};
결론 및 다음 단계
이제 강력한 TypeScript 작업자를 생성하기 위한 모든 도구가 있습니다.
itty-router, 변수 및 비밀 관리, 로컬 개발 wrangler dev
프로덕션에 배포합니다. 다음 단계는 작업자를 상태 저장 상태로 만드는 것입니다.
Cloudflare가 제공하는 다양한 스토리지 계층을 사용합니다.
시리즈의 다음 기사
- 제3조: 에지 지속성 - KV, R2 및 D1 - 글로벌 캐싱에 Workers KV를 사용하고 객체 스토리지에 R2를 사용하는 시기와 방법 송신 수수료 없이 S3와 호환되며 관계형 SQL 쿼리를 위한 D1입니다.
- 제4조: 지속성 객체 — 기본 상태 강력한 일관성과 엣지의 WebSocket.
- 제9조: Miniflare 및 Vitest를 사용한 테스트 — 단위 테스트 배포 없이 작업자를 위한 통합 테스트.







