Deno sandbox Persistent Volumes を使いたいソフトウェアを使う

@deno/sandboxで使える機能に、Persistent Volumes が増えていました。
こちら、SandBox内での永続ボリューム/ストレージを提供する機能です。

こちらを使い、git 使ったメモツール/サイトを作ってみました。

参考

Persistent Volumes とは

Persistent Volumes は、Deno SandBox で立ち上げられるマイクロインスタンスにリージョンブロックストレージをアタッチする機能。
これを使うと都度立ち上がるマイクロインスタンスに、セットアップされたストレージを提供できる。

公式のドキュメントでは、以下のように紹介されています。

Persistent volumes let you attach regional block storage to a sandbox so data survives process restarts and new connections. They are ideal for package caches, build artifacts, SQLite databases, or any workflow that needs a small amount of durable storage without promoting code to a full Deno Deploy app.

パッケージキャッシュ、ビルドアーティファクト、SQLiteデータベースなどを利用例として挙げられています。

2025年12月30年日時点では、プライベートベータ版使うためには、サポートへの連絡が必要です。

とてもミニマムな利用例としては、以下のようになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import { Client, Sandbox } from "@deno/sandbox";

const client = new Client();
const volume = await client.volumes.create({
slug: "storage",
region: "ord",
capacity: "300MiB",
});

console.log("=== ファイル書き込み処理サンドボックス ===");

{
await using sandbox = await Sandbox.create({
volumes: {
"/data/storage": volume!.id,
},
region: "ord",
});

await sandbox.writeTextFile("./volatile-memo.md", "volatile storage test memo");
console.log(`> ls .\n${await sandbox.sh`ls .`.text()}`);

await sandbox.writeTextFile("/data/storage/persistent-memo.md", "Persistent storage test memo");
await sandbox.sh`sync`.sudo();
console.log(`> ls /data/storage\n${await sandbox.sh`ls /data/storage`.text()}`);

}

console.log("=== ファイル読み込み処理サンドボックス ===");

{
await using sandbox = await Sandbox.create({
volumes: {
"/data/storage": volume!.id,
},
region: "ord",
});

console.log(`> ls .\n${await sandbox.sh`ls .`.text()}`);
console.log(`> ls /data/storage\n${await sandbox.sh`ls /data/storage`.text()}`);
}

await client.volumes.delete(volume!.id);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
> deno run -EN --env sample.ts
deno run -EN --env .\test2-sudo-volume-minimum-sample.ts
=== ファイル書き込み処理サンドボックス ===
> ls .
volatile-memo.md

> ls /data/storage
persistent-memo.md

=== ファイル読み込み処理サンドボックス ===
> ls .

> ls /data/storage
persistent-memo.md

このように、. 置いたファイルは2回目に起動したサンドボックスでは消えていますが、/data/storage 配下のファイルは残っています。
公式ドキュメントには無いですが、sync コマンドを実行しておかない場合、ファイルが保存されない場合があるようです。

git を Persistent Volumes において使う。

特定パッケージをPersistent Volumesに置いて使う例として、git を置いてみます。

github - Octo8080X/deno-sandbox-memo に置いたものがあります。
全体を見たい場合、こちらをご覧ください。

sandboxGit.ts(抜粋)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
import { Client, Sandbox, SandboxOptions, Volume } from "@deno/sandbox";

const GIT_STORAGE_VOLUME_NAME = "git-storage-volume";
const SERVER_APP_STORAGE_VOLUME_NAME = "git-server-app-storage-volume";
const DATA_STORAGE_VOLUME_NAME = `${Deno.env.get("APP_ENV")}-git-data-storage-volume`

async function getStorageVolume(slug: string) {
const client = new Client();

let volume: Volume | null = null;

const volumes = await client.volumes.list();
if (volumes.items.filter((v) => v.slug === slug).length > 0) {
volume = await client.volumes.get(slug)!;
} else {
volume = await client.volumes.create({
slug,
region: "ord",
capacity: "300MiB",
});
}
return volume;
}

const createSandbox = async (options?: SandboxOptions) => {
const sandbox = await Sandbox.create({
memoryMb: 4096,
region: "ord",
...options,
});
return sandbox;
};

const withSandbox = async <T>(
fn: (sandbox: Sandbox) => Promise<T>,
options?: SandboxOptions,
): Promise<T> => {
await using sandbox = await createSandbox(options);
return await fn(sandbox);
};

export const SERVER_APP_ENTRYPOINT = "/data/server_app/server.ts";
export const SERVER_APP_SANDBOX_OPTIONS: SandboxOptions = {
volumes: {
"/data/server_app": SERVER_APP_STORAGE_VOLUME_NAME,
"/data/git": GIT_STORAGE_VOLUME_NAME,
"/data/storage": DATA_STORAGE_VOLUME_NAME,
},
env: {
GIT_CONFIG_GLOBAL: "/data/git/.gitconfig",
},
region: "ord",
memoryMb: 4096,
timeout: "10m",
};


const buildGitSandboxOptions = (volumes: Record<string, string>): SandboxOptions => {
return {
volumes,
env: {
GIT_CONFIG_GLOBAL: "/data/git/.gitconfig",
},
region: "ord",
memoryMb: 4096,
};
};

export async function initSandBoxStorage() {
const gitStorageVolume = await getStorageVolume(GIT_STORAGE_VOLUME_NAME);
const storageVolume = await getStorageVolume(DATA_STORAGE_VOLUME_NAME);

const gitSandboxOptions = buildGitSandboxOptions({
"/data/git": gitStorageVolume!.id,
"/data/storage": storageVolume!.id,
});

await withSandbox(async (sandbox) => {
console.log("initialize volume on sandbox");

console.log("apt update");
await sandbox.sh`apt-get update > /dev/null 2>&1`.sudo();

console.log("install git");
await sandbox.sh`apt-get install -y git`.sudo();
console.log(await sandbox.sh`git --version`.text());

console.log("stage git binaries into /data/git volume");
await sandbox.sh`mkdir -p /data/git/app /data/git/lib`.sudo();
await sandbox.sh`sh -c 'cd /tmp && apt-get download git git-man'`.sudo();
await sandbox.sh`dpkg -x /tmp/git_* /data/git/app`.sudo();
await sandbox.sh`dpkg -x /tmp/git-man_* /data/git/app`.sudo();
await sandbox
.sh`sh -c 'set -e; ldd /data/git/app/usr/bin/git | awk "/=>/ {print $3}" | grep -E "^/" | while read -r path; do echo "lib: $path"; cp -n "$path" /data/git/lib/; done'`
.sudo();
await sandbox.sh`sh -c 'cat <<"EOF" > /data/git/git
#!/bin/sh
export LD_LIBRARY_PATH=/data/git/lib:\${LD_LIBRARY_PATH}
export GIT_EXEC_PATH=/data/git/app/usr/libexec/git-core
export PATH=/data/git/app/usr/bin:/data/git/app/usr/libexec/git-core:\${PATH}
exec /data/git/app/usr/bin/git "$@"
EOF'`.sudo();
await sandbox.sh`chmod +x /data/git/git`.sudo();
await sandbox.sh`/data/git/git --version`.sudo();
console.log(await sandbox.sh`ls -la /data/git | head`.text());
await sandbox.sh`sync`.sudo();

console.log("configure git identity (volume git)");
await sandbox.sh`/data/git/git config --global user.name "sandbox"`;
await sandbox
.sh`/data/git/git config --global user.email "sandbox@example.com"`;

console.log("init repo into storage volume using volume git");
await sandbox
.sh`cd /data/storage && /data/git/git init && /data/git/git branch -m main && /data/git/git add -A && /data/git/git commit --allow-empty -m "initial commit"`;

console.log("list git log from volume");
await sandbox
.sh`cd /data/storage && /data/git/git log`;

// sandbox を落とす前にsyncしておかないと反映されないケースがある。
await sandbox.sh`sync`.sudo();
}, gitSandboxOptions);

// server app 用の volume初期化
const serverAppStorageVolume = await getStorageVolume(SERVER_APP_STORAGE_VOLUME_NAME);
const serverAppSandboxOptions: SandboxOptions = {
volumes: {
"/data/server_app": serverAppStorageVolume!.id,
},
region: "ord",
};

await withSandbox(async (sandbox) => {
await sandbox.fs.writeTextFile("/data/server_app/server.ts", Deno.readTextFileSync("./sandboxServerApp/server.ts"));
await sandbox.fs.writeTextFile("/data/server_app/deno.json", Deno.readTextFileSync("./sandboxServerApp/deno.json"));
await sandbox.sh`cd /data/server_app && deno install`;
await sandbox.sh`ls -la /data/server_app`.sudo();

// sandbox を落とす前にsyncしておかないと反映されないケースがある。
await sandbox.sh`sync`.sudo();
}, serverAppSandboxOptions);
}

if (import.meta.main) {
await initSandBoxStorage();
}

apt-get install により取得された tmp/git/data/git 配下に展開。
依存ファイル群も、/data/git/lib 以下にコピーしています。
そして、/data/git/git に依存ライブラリのパスなど記述し、本体gitを実行するシェルスクリプトを作成しています。

1
2
3
4
5
#!/bin/sh
export LD_LIBRARY_PATH=/data/git/lib:\${LD_LIBRARY_PATH}
export GIT_EXEC_PATH=/data/git/app/usr/libexec/git-core
export PATH=/data/git/app/usr/bin:/data/git/app/usr/libexec/git-core:\${PATH}
exec /data/git/app/usr/bin/git "$@"

最初これを扱うとき、起動の都度sandboxを起動することをしていました。
しかし、同一のPersistent Volumesをマウントすることは仕様的にできないようです。
そのため、gitを操作するsandboxをある程度の時間起動をキープし、サーバアプリを介して操作するようにしました。
Webアプリのバックエンドとして扱うならば、レスポンスの観点でこの対応をすることが望ましいと考えます。


Deno sandobox Persistent Volumes の上で、gitを使う例を紹介しました。
これを使ったアプリ も稼働状態で公開しています。

では。