C3(create-cloudflare-cli)は、Workers と Pages のアプリケーションを Cloudflare へすばやくセットアップしてデプロイするためのコマンドラインツールです。
まず、ターミナルを開き、次を実行します。
npm create cloudflare@latest -- r2-workeryarn create cloudflare r2-workerpnpm 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次のコマンドでバケットを作成します。
npx wrangler r2 bucket create <YOUR_BUCKET_NAME>バケットが作成されたことを確認するには、次を実行します。
npx wrangler r2 bucket listlist コマンドを実行すると、作成したバケットを含む、すべてのバケット名が表示されます。
バケットを Worker にバインドする必要があります。
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 の設定の詳細(たとえば 管轄(jurisdictions) を使う場合)は、Wrangler の設定ドキュメント を参照してください。
Worker のコード内では、バケットが MY_BUCKET 変数として使えるようになり、操作を始められます。
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"},
)上記のコードを Worker に追加すると、届くすべてのリクエストがバケットを操作できます。つまりバケットは公開され、意図しない第三者に内容のアクセスや変更を許す可能性があります。
誰がバケットに対してどの操作をできるかを決める認可ロジックを定義する必要があります。このロジックは Worker のコード内に置きます。ユーザー権限の判定はアプリケーションの役割です。アクセスと認可の実装に関する資料をいくつか挙げます。
- Basic Authentication: HTTP Basic スキーマでアクセスを制限する方法です。
- Using Custom Headers: ヘッダー内の既知の事前共有鍵に基づいて、リクエストを許可または拒否します。
作成したばかりのバケットと Worker では、すべてのバケット操作を保護する必要があります。
PUT と DELETE リクエストでは、あとで Wrangler のシークレットとして定義する新しい環境変数 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 でシークレットを作成する必要があります。
npx wrangler secret put AUTH_KEY_SECRETこのコマンドは、ターミナルでシークレットの入力を求めます。
npx wrangler secret put AUTH_KEY_SECRETEnter 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.このシークレットは、Worker の env パラメーター上で AUTH_KEY_SECRET として使えます。
Worker とバケットの準備ができたら、npx wrangler deploy コマンド を実行して、Cloudflare のグローバルネットワークへデプロイします。
npx wrangler deployデプロイした Worker のエンドポイントを使い、次のコマンドで認可ロジックが動いていることを確認できます。
# 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 へデプロイできています。