Workers에서 R2 사용하기 (Use R2 from Workers)
1. C3로 새 애플리케이션 만들기
출처: 문서
본문
C3(create-cloudflare-cli)는 Workers & Pages 애플리케이션을 가능한 한 빨리 Cloudflare에 설정하고 배포할 수 있게 도와주는 명령줄 도구예요.
시작하려면 터미널 창을 열고 다음을 실행해요:
npm create cloudflare@latest -- r2-worker
yarn create cloudflare r2-worker
pnpm create cloudflare@latest r2-worker
설정 시 다음 옵션을 선택해요:
- What would you like to start with? →
Hello World example선택 - Which template would you like to use? →
Worker only선택 - Which language do you want to use? →
JavaScript선택 - Do you want to use git for version control? →
Yes선택 - Do you want to deploy your application? →
No선택(배포 전에 몇 가지를 바꿀 거예요)
그다음 새로 생성된 디렉터리로 이동해요:
cd r2-worker
2. 버킷 만들기
다음을 실행해 버킷을 만들어요:
npx wrangler r2 bucket create <YOUR_BUCKET_NAME>
버킷이 생성됐는지 확인하려면 다음을 실행해요:
npx wrangler r2 bucket list
list 명령을 실행하면 방금 만든 버킷을 포함해 모든 버킷 이름이 보여요.
3. 버킷을 Worker에 바인딩하기
버킷을 Worker에 바인딩해야 해요.
바인딩(Bindings)
바인딩은 Worker가 KV Namespaces, Durable Objects, 또는 R2 Buckets 같은 외부 리소스와 상호작용하는 방식이에요. 바인딩은 Workers 런타임이 코드에 제공하는 런타임 변수예요. Wrangler 파일에 변수 이름을 선언하면 런타임에 이 리소스에 바인딩되고, 그 변수를 통해 상호작용할 수 있어요. 각 바인딩의 변수 이름과 동작은 Worker를 배포할 때 사용자가 결정해요. 자세한 내용은 환경 변수 문서를 참고해 주세요.
바인딩은 Worker 프로젝트 디렉터리의 Wrangler 파일에 정의돼요.
R2 버킷을 Worker에 바인딩하려면 Wrangler 파일에 다음을 추가해요. binding 속성을 유효한 JavaScript 변수 식별자로, bucket_name을 2단계에서 버킷을 만들 때 사용한 <YOUR_BUCKET_NAME>으로 바꿔요:
{
"r2_buckets": [
{
"binding": "MY_BUCKET", // <~ valid JavaScript variable name
"bucket_name": "<YOUR_BUCKET_NAME>"
}
]
}
[[r2_buckets]]
binding = "MY_BUCKET"
bucket_name = "<YOUR_BUCKET_NAME>"
Worker 구성에 대한 더 자세한 정보(예: 관할권을 사용하는 경우)는 Wrangler 구성 문서를 참고해 주세요.
4. Worker에서 R2 버킷에 접근하기
이제 Worker 코드 안에서 버킷이 MY_BUCKET 변수로 사용 가능하고, 그 변수를 통해 상호작용을 시작할 수 있어요.
Wrangler의 로컬 개발 모드
기본적으로 wrangler dev는 로컬 개발 모드로 실행돼요. 이 모드에서 로컬 worker가 수행하는 모든 작업은 컴퓨터의 로컬 스토리지에서 동작해요.
개발 중 수행되는 R2 작업이 실제 R2 버킷에 대해 수행되길 원한다면 R2 바인딩 구성에 "remote" : true를 설정할 수 있어요. 자세한 내용은 원격 바인딩 문서를 참고해 주세요.
R2 버킷은 객체를 READ, LIST, WRITE, DELETE할 수 있어요. Module Worker 문법을 사용한 모든 작업 예시는 아래에서 볼 수 있어요. 프로젝트의 index.js 파일에 다음 스니펫을 추가해요:
import { WorkerEntrypoint } from "cloudflare:workers";
export default class extends WorkerEntrypoint<Env> {
async fetch(request: Request) {
const url = new URL(request.url);
const key = url.pathname.slice(1);
switch (request.method) {
case "PUT": {
await this.env.MY_BUCKET.put(key, request.body, {
onlyIf: request.headers,
httpMetadata: request.headers,
});
return new Response(`Put ${key} successfully!`);
}
case "GET": {
const object = await this.env.MY_BUCKET.get(key, {
onlyIf: request.headers,
range: request.headers,
});
if (object === null) {
return new Response("Object Not Found", { status: 404 });
}
const headers = new Headers();
object.writeHttpMetadata(headers);
headers.set("etag", object.httpEtag);
// When no body is present, preconditions have failed
return new Response("body" in object ? object.body : undefined, {
status: "body" in object ? 200 : 412,
headers,
});
}
case "DELETE": {
await this.env.MY_BUCKET.delete(key);
return new Response("Deleted!");
}
default:
return new Response("Method Not Allowed", {
status: 405,
headers: {
Allow: "PUT, GET, DELETE",
},
});
}
}
};
export default {
async fetch(request, env) {
const url = new URL(request.url);
const key = url.pathname.slice(1);
switch (request.method) {
case "PUT": {
await env.MY_BUCKET.put(key, request.body, {
onlyIf: request.headers,
httpMetadata: request.headers,
});
return new Response(`Put ${key} successfully!`);
}
case "GET": {
const object = await env.MY_BUCKET.get(key, {
onlyIf: request.headers,
range: request.headers,
});
if (object === null) {
return new Response("Object Not Found", { status: 404 });
}
const headers = new Headers();
object.writeHttpMetadata(headers);
headers.set("etag", object.httpEtag);
// When no body is present, preconditions have failed
return new Response("body" in object ? object.body : undefined, {
status: "body" in object ? 200 : 412,
headers,
});
}
case "DELETE": {
await env.MY_BUCKET.delete(key);
return new Response("Deleted!");
}
default:
return new Response("Method Not Allowed", {
status: 405,
headers: {
Allow: "PUT, GET, DELETE",
},
});
}
}
}
from workers import WorkerEntrypoint, Response
from urllib.parse import urlparse
class Default(WorkerEntrypoint):
async def fetch(self, request):
url = urlparse(request.url)
key = url.path[1:]
if request.method == "PUT":
await self.env.MY_BUCKET.put(
key,
request.body,
onlyIf=request.headers,
httpMetadata=request.headers,
)
return Response(f"Put {key} successfully!")
elif request.method == "GET":
obj = await self.env.MY_BUCKET.get(
key,
onlyIf=request.headers,
range=request.headers,
)
if obj is None:
return Response("Object Not Found", status=404)
# When no body is present, preconditions have failed
body = obj.body if hasattr(obj, "body") else None
status = 200 if hasattr(obj, "body") else 412
headers = {"etag": obj.httpEtag}
return Response(body, status=status, headers=headers)
elif request.method == "DELETE":
await self.env.MY_BUCKET.delete(key)
return Response("Deleted!")
else:
return Response(
"Method Not Allowed",
status=405,
headers={"Allow": "PUT, GET, DELETE"},
)
request.body에 접근할 때 생길 수 있는 오류 방지하기
Request ↗︎의 본문은 한 번만 접근할 수 있어요. 같은 요청에서 이전에 request.formData()를 사용했다면 request.body에 접근할 때 TypeError가 발생할 수 있어요.
오류를 피하려면 Request의 본문에 접근하려는 각 후속 시도마다 request.clone()으로 Request 객체의 복사본을 만들어요. Workers에는 Worker당 128MB 메모리 제한이 있고, 특히 큰 파일을 Worker 메모리에 여러 번 로드하면 이 제한에 도달할 수 있다는 점을 명심해요. 메모리 사용량이 이 제한에 도달하지 않도록 Streams 사용을 고려해 보세요.
5. 버킷 접근과 프라이버시
Worker에 위 코드를 추가하면 들어오는 모든 요청이 버킷과 상호작용할 수 있게 돼요. 즉 버킷이 공개적으로 노출되어 원치 않는 행위자가 콘텐츠에 접근하거나 수정할 수 있다는 뜻이에요.
이제 누가 버킷에 대해 어떤 작업을 할 수 있는지 결정하는 권한 부여(authorization) 로직을 정의해야 해요. 이 로직은 Worker의 코드 안에 있어요. 사용자 권한을 결정하는 것은 애플리케이션의 역할이기 때문이에요. 다음은 접근과 권한 부여 관행과 관련된 짧은 리소스 목록이에요:
- Basic Authentication: HTTP Basic 스키마로 접근을 제한하는 방법을 보여줘요.
- 커스텀 헤더 사용: 헤더의 알려진 사전 공유 키에 기반해 요청을 허용하거나 거부해요.
새로 만든 버킷과 Worker로 계속 진행하면서 모든 버킷 작업을 보호해야 해요.
PUT과 DELETE 요청에는 나중에 Wrangler secret으로 정의할 새 AUTH_KEY_SECRET 환경 변수를 사용할 거예요.
GET 요청에는 특정 파일만 요청할 수 있게 보장할 거예요. 이 모든 커스텀 로직은 authorizeRequest 함수 안에서, 커스텀 헤더 로직은 hasValidHeader 함수에서 처리돼요. 모든 검증을 통과하면 작업이 허용돼요.
const ALLOW_LIST = ["cat-pic.jpg"];
// Check requests for a pre-shared secret
const hasValidHeader = (request, env) => {
return request.headers.get("X-Custom-Auth-Key") === env.AUTH_KEY_SECRET;
};
function authorizeRequest(request, env, key) {
switch (request.method) {
case "PUT":
case "DELETE":
return hasValidHeader(request, env);
case "GET":
return ALLOW_LIST.includes(key);
default:
return false;
}
}
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
const key = url.pathname.slice(1);
if (!authorizeRequest(request, env, key)) {
return new Response("Forbidden", { status: 403 });
}
// ...
},
};
from workers import WorkerEntrypoint, Response
from urllib.parse import urlparse
ALLOW_LIST = ["cat-pic.jpg"]
# Check requests for a pre-shared secret
def has_valid_header(request, env):
return request.headers.get("X-Custom-Auth-Key") == env.AUTH_KEY_SECRET
def authorize_request(request, env, key):
if request.method in ["PUT", "DELETE"]:
return has_valid_header(request, env)
elif request.method == "GET":
return key in ALLOW_LIST
else:
return False
class Default(WorkerEntrypoint):
async def fetch(self, request):
url = urlparse(request.url)
key = url.path[1:]
if not authorize_request(request, self.env, key):
return Response("Forbidden", status=403)
# ...
이것이 동작하려면 Wrangler로 secret을 만들어야 해요:
npx wrangler secret put AUTH_KEY_SECRET
이 명령은 터미널에서 secret 입력을 요청할 거예요:
npx wrangler secret put AUTH_KEY_SECRET
Enter the secret text you'd like assigned to the variable AUTH_KEY_SECRET on the script named <YOUR_WORKER_NAME>:
*********
🌀 Creating the secret for script name <YOUR_WORKER_NAME>
✨ Success! Uploaded secret AUTH_KEY_SECRET.
이제 이 secret은 Worker의 env 파라미터에서 AUTH_KEY_SECRET으로 사용할 수 있어요.
6. Worker 배포
Worker와 버킷을 설정했으면 npx wrangler deploy 명령을 실행해 Cloudflare의 글로벌 네트워크에 배포해요:
npx wrangler deploy
배포된 Worker 엔드포인트로 다음 명령을 사용해 권한 부여 로직이 동작하는지 확인할 수 있어요:
주의
curl로 R2에 파일을 업로드할 때는 --data나 -d가 아니라 **--data-binary ↗︎**를 사용해야 해요. 그렇지 않으면 파일이 잘릴 수 있어요.
# Attempt to write an object without providing the "X-Custom-Auth-Key" header
curl https://your-worker.dev/cat-pic.jpg -X PUT --data-binary 'test'
#=> Forbidden
# Expected because header was missing
# Attempt to write an object with the wrong "X-Custom-Auth-Key" header value
curl https://your-worker.dev/cat-pic.jpg -X PUT --header "X-Custom-Auth-Key: hotdog" --data-binary 'test'
#=> Forbidden
# Expected because header value did not match the AUTH_KEY_SECRET value
# Attempt to write an object with the correct "X-Custom-Auth-Key" header value
# Note: Assume that "*********" is the value of your AUTH_KEY_SECRET Wrangler secret
curl https://your-worker.dev/cat-pic.jpg -X PUT --header "X-Custom-Auth-Key: *********" --data-binary 'test'
#=> Put cat-pic.jpg successfully!
# Attempt to read object called "foo"
curl https://your-worker.dev/foo
#=> Forbidden
# Expected because "foo" is not in the ALLOW_LIST
# Attempt to read an object called "cat-pic.jpg"
curl https://your-worker.dev/cat-pic.jpg
#=> test
# Note: This is the value that was successfully PUT above
이 가이드를 완료하면 Wrangler를 성공적으로 설치하고 R2 버킷을 Cloudflare에 배포한 거예요.
관련 리소스
더 알아보기 (Learn more)
- Workers API 참조 — 버킷 및 객체 작업 전체 참조
- Workers에서 멀티파트 업로드 — 대용량 파일 업로드