Cloudflare R2をWranguler CLI/Web API/AWS CLIから操作してみる

# Cloudflare R2とは

- Cloudflareが提供する、S3互換のインタフェースを持つオブジェクトストレージ
- R2のRはSの１つ前のアルファベット、2は3の１つ前の数字ということでS3を意識していることが分かる
- RにはRapid(高速性)やReliable(信頼性)等の意味も含まれている

## 課金体系

- R2はS3のデータ転送にかかるコストの問題も解決している
    - CloudflareはS3のデータ転送にかかるコストが高いことを以前から指摘していた
    - 特にエグレス(アウトバウンド)において、動画等のサイズの大きいファイルを転送する際に高いコストがかかる
    - そのため、R2ではデータ転送にかかるコストを無料に設定している
- 課金体系としては、データ容量とClass A operationsとClass B operationsにかかる従量課金
    - ざっくりとClass A operationsがライト、Class B operationsがリード操作を指している
    - 1GBのデータ容量に対して月額0.015ドル、100万回のClass A operationsに対して4.5ドル、100万回のClass B operationsに対して0.36ドルかかる
    - ただし、10GB以下のデータ容量で月間100万回までのClass A operations、1,000万回までのClass B operationsは無料
    - 詳細: [Pricing](https://developers.cloudflare.com/r2/pricing/)

## 技術仕様

- バケットの作成時にリージョンを指定する必要がなく、作成者に一番近いリージョンが自動的に選定される
    - これはCloudflareの持つエッジネットワークによって実現されている
    - 将来的にはデータのアクセスパターンによってバケットの配置を最適化するらしい
- 信頼性はイレブンナイン（99.999999999％）を担保している
- Durable Objectsとの連携も可能

# Wrangler CLIの設定

- Wrangler CLIはCloudflare Workers向けのCLI
- Cloudflareの様々なサービスの設定や操作をコマンドラインで実現するためのツール
- 詳細: [Install/Update Wrangler](https://developers.cloudflare.com/workers/wrangler/install-and-update/)

```bash
# wranglerのインストール
$ npm i -g wrangler

# Cloudflareにログイン
$ wrangler login
```

# Wranguler CLIから操作

## バケットの作成

- バケット名はアカウント毎にユニークな名称であれば良いらしい
    - S3の場合はグローバルにユニークでなければならない

```bash
# バケットの作成
$ wrangler r2 bucket create my-first-bucket
 ⛅️ wrangler 2.12.3 
--------------------
Creating bucket my-first-bucket.
Created bucket my-first-bucket.

# バケットが作成されていることの確認
$ wrangler r2 bucket list
[
  {
    "name": "my-first-bucket",
    "creation_date": "2023-03-18T18:52:54.141Z"
  }
]
```

- ダッシュボードのR2からもバケットが作成されたことが確認できる

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78249/8ceb5091-ffdd-92d7-a798-366971914e1f.png)

## オブジェクトの操作

- オブジェクトの一覧を取得するコマンドは用意されていなさそう

```bash
# アップロード
$ wrangler r2 object put my-first-bucket/icon.jpg --file=icon.jpg
Delegating to locally-installed wrangler@2.12.3 over global wrangler@2.12.3...
Run `npx wrangler r2 object put my-first-bucket/icon.jpg --file=icon.jpg` to use the local version directly.

 ⛅️ wrangler 2.12.3 
--------------------
Creating object "icon.jpg" in bucket "my-first-bucket".
Upload complete.

# ダウンロード
$ wrangler r2 object get my-first-bucket/icon.jpg
Delegating to locally-installed wrangler@2.12.3 over global wrangler@2.12.3...
Run `npx wrangler r2 object get my-first-bucket/icon.jpg` to use the local version directly.

 ⛅️ wrangler 2.12.3 
--------------------
Downloading "icon.jpg" from "my-first-bucket".
Download complete.

# 削除
$ wrangler r2 object delete my-first-bucket/icon.jpg
Delegating to locally-installed wrangler@2.12.3 over global wrangler@2.12.3...
Run `npx wrangler r2 object delete my-first-bucket/icon.jpg` to use the local version directly.

 ⛅️ wrangler 2.12.3 
--------------------
Deleting object "icon.jpg" from bucket "my-first-bucket".
Delete complete.
```

- ちなみにダッシュボードのR2 > <バケット名> > オブジェクト からオブジェクトの各種情報の確認や操作も可能

# Workers + Web API(curl)から操作

## Workersの作成

- Workersを作成することで、R2のバケットがHTTPS経由でWeb APIとして操作できるようになる
- WorkersはJavaScriptのコードをエッジネットワークに配置し、実行できる仕組み

### プロジェクトの初期化

- 今回はTypeScriptを有効化し、Workerの方式はFetch Handlerとしている

```bash
$ wrangler init my-first-worker   
 ⛅️ wrangler 2.12.3 
--------------------
Using npm as package manager.
✨ Created my-first-worker/wrangler.toml
✔ Would you like to use git to manage this Worker? … yes
✨ Initialized git repository at my-first-worker
✔ No package.json found. Would you like to create one? … yes
✨ Created my-first-worker/package.json
✔ Would you like to use TypeScript? … yes
✨ Created my-first-worker/tsconfig.json
✔ Would you like to create a Worker at my-first-worker/src/index.ts? › Fetch handler
✨ Created my-first-worker/src/index.ts
✔ Would you like us to write your first test with Vitest? … no
npm WARN deprecated rollup-plugin-inject@3.0.2: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject.
npm WARN deprecated sourcemap-codec@1.4.8: Please use @jridgewell/sourcemap-codec instead

added 104 packages, and audited 105 packages in 24s

11 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
✨ Installed @cloudflare/workers-types and typescript into devDependencies

To start developing your Worker, run `cd my-first-worker && npm start`
To publish your Worker to the Internet, run `npm run deploy`
```

- 以下のようなディレクトリ構造でファイルが生成される

```
.
├── node_modules
│   └── ...
├── package-lock.json
├── package.json
├── src
│   └── index.ts
├── tsconfig.json
└── wrangler.toml
```

### アカウントIDの取得

- 次の手順で使用するため、アカウントIDを控えておく

```bash
$ wrangler whoami
Delegating to locally-installed wrangler@2.12.3 over global wrangler@2.12.3...
Run `npx wrangler whoami` to use the local version directly.

 ⛅️ wrangler 2.12.3 
--------------------
Getting User settings...
👋 You are logged in with an OAuth Token, associated with the email foo@example.com!
┌─────────────────────────────────┬──────────────────────────────────┐
│ Account Name                    │ Account ID                       │
├─────────────────────────────────┼──────────────────────────────────┤
│ Foo@example.com's Account       │ XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX │
└─────────────────────────────────┴──────────────────────────────────┘
🔓 Token Permissions: If scopes are missing, you may need to logout and re-login.
Scope (Access)
- account (read)
- user (read)
- workers (write)
- workers_kv (write)
- workers_routes (write)
- workers_scripts (write)
- workers_tail (read)
- d1 (write)
- pages (write)
- zone (read)
- ssl_certs (write)
- offline_access 
```

- アカウントIDはダッシュボードのWorkersからも確認することが可能

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78249/65cd8bf3-7033-33ea-cca0-6bbd692582db.png)


### Workersの設定と実装

- 生成されたファイルをベースに設定と実装を追加していく

```toml:wrangler.toml
name = "my-first-worker"
main = "src/index.ts"
compatibility_date = "2023-03-19"

# 先程、控えておいたアカウントIDを設定
account_id = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
# Cloudflareから用意される、*.workers.devドメインにデプロイする場合に必要
# 詳細: https://developers.cloudflare.com/workers/platform/environments/#staging-environment-with-workersdev
workers_dev = true

[[r2_buckets]]
# src/index.tsから参照する際の環境変数名
binding = "MY_FIRST_BUCKET"
bucket_name = "my-first-bucket"
```

- オブジェクトの作成、取得、削除機能を実装してみた
- 実装は[Use R2 from Workers #5. Access your R2 bucket from your Worker](https://developers.cloudflare.com/r2/api/workers/workers-api-usage/#5-access-your-r2-bucket-from-your-worker)を参考にしている
- [Use R2 from Workers #6. Bucket access and privacy](https://developers.cloudflare.com/r2/api/workers/workers-api-usage/#6-bucket-access-and-privacy)のように、Web APIにアクセス制限を設けることも可能

```ts:src/index.ts
/**
 * Welcome to Cloudflare Workers! This is your first worker.
 *
 * - Run `wrangler dev src/index.ts` in your terminal to start a development server
 * - Open a browser tab at http://localhost:8787/ to see your worker in action
 * - Run `wrangler publish src/index.ts --name my-worker` to publish your worker
 *
 * Learn more at https://developers.cloudflare.com/workers/
 */

export interface Env {
	// Example binding to KV. Learn more at https://developers.cloudflare.com/workers/runtime-apis/kv/
	// MY_KV_NAMESPACE: KVNamespace;
	//
	// Example binding to Durable Object. Learn more at https://developers.cloudflare.com/workers/runtime-apis/durable-objects/
	// MY_DURABLE_OBJECT: DurableObjectNamespace;
	//
	// Example binding to R2. Learn more at https://developers.cloudflare.com/workers/runtime-apis/r2/
	MY_FIRST_BUCKET: R2Bucket;
	//
	// Example binding to a Service. Learn more at https://developers.cloudflare.com/workers/runtime-apis/service-bindings/
	// MY_SERVICE: Fetcher;
}

export default {
	async fetch(
		request: Request,
		env: Env,
		ctx: ExecutionContext
	): Promise<Response> {
		const url = new URL(request.url);
		const key = url.pathname.slice(1);

		switch (request.method) {
		case 'PUT':
			await env.MY_FIRST_BUCKET.put(key, request.body);
			return new Response(`Put ${key} successfully!`);
		case 'GET':
			const object = await env.MY_FIRST_BUCKET.get(key);

			if (object === null) {
				return new Response('Object Not Found', { status: 404 });
			}

			const headers = new Headers();
			object.writeHttpMetadata(headers);
			headers.set('etag', object.httpEtag);

			return new Response(object.body, {
				headers,
			});
		case 'DELETE':
			await env.MY_FIRST_BUCKET.delete(key);
			return new Response('Deleted!');

		default:
			return new Response('Method Not Allowed', {
				status: 405,
				headers: {
					Allow: 'PUT, GET, DELETE',
				},
			});
		}
	},
};
```

### Workersのデプロイ

```bash
$ wrangler publish
Delegating to locally-installed wrangler@2.12.3 over global wrangler@2.12.3...
Run `npx wrangler publish` to use the local version directly.

 ⛅️ wrangler 2.12.3 
--------------------
Your worker has access to the following bindings:
- R2 Buckets:
  - MY_FIRST_BUCKET: my-first-bucket
Total Upload: 1.02 KiB / gzip: 0.47 KiB
▲ [WARNING] You need to register a workers.dev subdomain before publishing to workers.dev


✔ Would you like to register a workers.dev subdomain now? … yes
✔ What would you like your workers.dev subdomain to be? It will be accessible at https://<subdomain>.workers.dev … foo
✔ Creating a workers.dev subdomain for your account at https://foo.workers.dev. Ok to proceed? … yes
Success! It may take a few minutes for DNS records to update.
Visit https://dash.cloudflare.com/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/workers/subdomain to edit your workers.dev subdomain
Uploaded my-first-worker (1.64 sec)
Published my-first-worker (33.42 sec)
  https://my-first-worker.foo.workers.dev
Current Deployment ID: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
```

- ダッシュボードのWorkersからWorkersが作成されたことが確認できる

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78249/1f3fd9cb-e12a-aec4-3e04-10513b3bc8af.png)

## オブジェクトの操作

- アップロード

```bash
# アップロード
$ curl https://my-first-worker.foo.workers.dev/icon.jpg -X PUT --data-binary "@icon.jpg"
Put icon.jpg successfully!

# ダウンロード
$ curl https://my-first-worker.foo.workers.dev/icon.jpg --output icon.jpg

# 削除
$ curl https://my-first-worker.foo.workers.dev/icon.jpg -X DELETE            
Deleted!
```

# AWS CLIから操作

## AWS CLIのインストール

- インストール方法: https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/getting-started-install.html
- GitHubリポジトリ: https://github.com/aws/aws-cli

```bash
# AWS CLIのバージョンの確認
$ aws --version
aws-cli/2.9.15 Python/3.9.11 Darwin/22.3.0 exe/x86_64 prompt/off
```

## API トークンの作成

- ダッシュボードのR2 > R2 API トークンの管理を開く
- 「API トークンを作成する」を押下

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78249/71d56653-cc6b-af2a-4153-d97b6b4c5809.png)

- 適当なトークン名を設定し、アクセス許可を「編集」に変更し、適当なTTLを設定し、「API トークンを作成する」を押下

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78249/c2f67a1a-9b27-f5e9-097b-23d58c8799e0.png)

- 「アクセス キー ID」と「シークレット アクセス キー」を控えて終了する

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78249/f51dc125-81b2-0c07-8ea4-af10a5ee5069.png)

```bash
# 一時的にR2のアクセスキーを使用できるようにする
$ export AWS_ACCESS_KEY_ID=XXXXXXXXXX
$ export AWS_SECRET_ACCESS_KEY=XXXXXXXXXXXXXXXXXXXX

# 接続できることを確認
# エンドポイントに https://<アカウントID>.r2.cloudflarestorage.com を指定する
$ aws --endpoint-url https://XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.r2.cloudflarestorage.com s3 ls                 
2023-03-19 03:52:54 my-first-bucket
```

## バケットの作成

```bash
# バケットの作成
$ aws --endpoint-url https://XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.r2.cloudflarestorage.com s3 mb s3://my-second-bucket --region auto
make_bucket: my-second-bucket

# バケットが作成されたことを確認
$ aws --endpoint-url https://XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.r2.cloudflarestorage.com s3 ls                                    
2023-03-19 03:52:54 my-first-bucket
2023-03-20 07:33:56 my-second-bucket
```

## オブジェクトの操作

```bash
# アップロード
$ aws --endpoint-url https://XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.r2.cloudflarestorage.com s3 cp ./icon.jpg s3://my-second-bucket/         
upload: ./icon.jpg to s3://my-second-bucket/icon.jpg

# オブジェクトの一覧の取得
$ aws --endpoint-url https://XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.r2.cloudflarestorage.com s3 ls s3://my-second-bucket/
2023-03-20 07:38:34      15598 icon.jpg

# ダウンロード
$ aws --endpoint-url https://XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.r2.cloudflarestorage.com s3 cp s3://my-second-bucket/icon.jpg .
download: s3://my-second-bucket/icon.jpg to ./icon.jpg

# 削除
$ aws --endpoint-url https://XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.r2.cloudflarestorage.com s3 rm s3://my-second-bucket/icon.jpg
delete: s3://my-second-bucket/icon.jpg
```