Skip to content

キー別インスタンス

キー(ユーザー、セッション、テナント)ごとに 1 つのアプリのインスタンス。キーが初めて現れたときに作られ、同じキーを持つ以降のすべてのリクエストのために、メモリとディスクごと保持されます。

更新

アプリの通常のインスタンスは交換可能です。どのインスタンスもどのリクエストにも応答し、プラットフォームはリクエストの増減に合わせてインスタンスを作成、削除します。キー別インスタンスは 1 つのキーに属します。キーが初めて使われたときに作られ、それ以降そのキーを持つリクエストはすべて、メモリとディスクの中身ごとそのインスタンスに届きます。エージェントのセッション、ユーザーのワークスペース、テナントのデータベースなど、自分の状態を再び見つける必要があるものに使います。

キー別インスタンスは、アプリの現在のバージョンのテンプレートから作られた、プロジェクトのサンドボックスです。コマンド、ファイル、ターミナル、スナップショット、フォークなど、サンドボックスにあるものはすべて /v1/sandboxes/{id}/… で使えます。

キーの取得元

keys.from は、アプリのアドレスに届くリクエストのどこにキーがあるかを指定します。すぐに反映され、バージョンは作られません。Python では次のようにします(コマンドラインにはまだオプションがありません)。

from glac import Glac

glac = Glac()
app = glac.cube.deploy("ghcr.io/acme/agent:3", name="agent", port=8080,
                          keys={"from": {"header": "X-Agent-Key"}, "delete_after_idle_ms": 86_400_000})
  • {"header": "X-Agent-Key"}:そのリクエストヘッダーの値がキーになります。
  • {"path_segment": 1}:パスのセグメント(1 が最初、最大 32)をデコードしたものがキーになります。/agent%2042/x のキーは agent 42 です。

キーのないリクエストは、アプリの通常のインスタンスに届きます。キーのあるリクエストは、キーを署名付きで運びます(次のセクション)。キーは制御文字を含まない 1〜128 文字で、それ以外には 400 invalid_key が返ります。

署名付きキー

アプリのアドレスにアクセスできる人なら誰でも任意のキーを作れてしまい、新しいキーのたびにアカウントにインスタンスが作られます。そのため、エッジを通るリクエストは、バックエンドがアプリのシークレットで署名したキーを運び、エッジは署名が正しい場合だけキーを通します。キーの最初のリクエストだけでなく、すべてのリクエストでそうです。API で作るキー(POST /v1/apps/{app}/instances)には署名は不要で、API キーが証明になります。

シークレットを作成します(表示されるのは一度だけです。アプリが持てるのは最大 2 つで、リクエストを拒否せずに新しいシークレットへ移行できます)。

glac cube key-secrets create agent        # prints GLAC_KEY_SECRET=ks_…

バックエンドは、渡すキーにそれぞれ署名します。署名付きキーは、キーがあった場所にそのまま入れます。ヘッダーの値か、パスのセグメントです。

import glac

signed = glac.sign_key(secret, app.id, "agent-42", expires_at=datetime.now(timezone.utc) + timedelta(hours=1))
requests.get(f"{app.url}/chat", headers={"X-Agent-Key": signed})                   # keys.from: header
link = f"{app.url}/{glac.sign_key(secret, app.id, 'agent 42', path=True)}/"       # keys.from: path_segment 1

署名付きの形式は <key>.<expiry>.<signature> です。有効期限は Unix 秒(なしなら空:agent-42..<signature>)、署名は、アプリの ID、キー、有効期限を 1 行ずつ並べたものに対する、シークレットを鍵とした HMAC-SHA256 で、パディングなしの base64url で表します。どの SDK にもヘルパー(sign_key、signKey、SignKey、KeySecret.signKey)があり、コマンドラインにもあります。

GLAC_KEY_SECRET=ks_… glac cube keys sign agent agent-42 --expires 12h
レスポンス 条件
403 key_signature_invalid 署名がない、別のキーやアプリ用の署名、アプリがもう持っていないシークレットでの署名、またはアプリにまだシークレットがない(メッセージにそう示される)
403 key_signature_expired 署名は正しいが、有効期限が過ぎている

拒否されたリクエストはどのインスタンスにも届きません。何も作成も復帰もされず、リクエストとして課金されることもありません(アプリのログの使用量では edge_refused として数えられます)。

**インスタンスから見えるもの。**元の場所にあるキーだけです。ヘッダーには agent-42 が入り(リクエストが何度送っても 1 回だけ)、パスのセグメントにはリクエストが書いたとおりのキーが入ります(/agent%2042/chat)。同じホストのページからの Referer からも署名は取り除かれます。署名もシークレットもインスタンスには届かないため、アプリがキーから組み立てるリンクには署名が含まれません。署名付きのパスの下で配信するページでは、相対リンクを使ってください。

**ローテーション。**2 つ目のシークレットを作成し、バックエンドをそれに切り替え、最初のシークレットを削除します。両方がある間は、どちらで署名しても有効です。シークレットの削除は、接続中のエッジがそのシークレットを受け付けなくなった時点で応答します(1 秒以内)。コントロールプレーンから切り離されたエッジは、再接続するまで古い表を使い続けます。

呼び出し 内容
POST /v1/apps/{app}/key-secrets シークレットを作成(201)。secret はこのレスポンスにだけ含まれる。3 つ目は 403 key_secret_limit_reached
GET /v1/apps/{app}/key-secrets アプリのシークレットの一覧(新しい順):id、prefix(先頭 11 文字)、created_at。値は含まない
GET /v1/apps/{app}/key-secrets/{id} ID で指定した 1 つのシークレット。値は含まない(glac cube key-secrets get agent ksec-…)
DELETE /v1/apps/{app}/key-secrets/{id} 1 つを削除

作成と取得

呼び出し 内容
POST /v1/apps/{app}/instances {key, region?} キーのインスタンスを作成する(201)。すでにあれば復帰させて返す(200)。同じキーで 2 回呼んでも、作られるインスタンスは 1 つ
GET /v1/apps/{app}/instances アプリのキー別インスタンスの一覧(新しい順。1 つのキーなら ?key=)
GET /v1/apps/{app}/instances/{key} 1 つのキーのインスタンスの、マシンから見た現在の状態(キーにインスタンスがなければ 404 key_not_found)
DELETE /v1/apps/{app}/instances/{key} インスタンスを削除してキーを解放する。そのキーを持つ次のリクエストで、新しいインスタンスが作られる

パスの中では、キーを 1 つのセグメントとして書き、英字、数字、-._~ 以外のバイトはすべて %XX で表します(agent 42 は agent%2042)。SDK はこれを自動で行います。

inst = app.instance("user-42")          # made, or woken
print(inst.id, inst.key, inst.region)
print(inst.run("ls /").stdout)          # it is a sandbox
app.delete_instance("user-42")

同じキーの最初のリクエストが同時に 2 つ届いても、作られるインスタンスは 1 つで、2 つ目は 1 つ目のインスタンスを受け取ります。インスタンスは、アプリが動くリージョンのうち、リクエストが届いたエッジのリージョン(または呼び出しで指定した region)に作られます。

スリープと削除のタイミング

キー別インスタンスは、サンドボックスと同じようにスリープします。リクエストだけでなく、CPU、ネットワーク、接続、タイマーも含めて、マシン自体が sleep_after_ms(最小 1 秒)の間静かだとスリープします。必要とするものがあれば、何であれ復帰させます。

指定しない限り、アイドルを理由に削除されることはありません。

  • アプリの keys.delete_after_idle_ms(最小 1,000):その時間スリープしたキー別インスタンスは削除され、キーが解放されます。
  • インスタンス自体の delete_after_idle_ms(PATCH /v1/sandboxes/{id})。アプリの設定より優先されます。
  • DELETE /v1/apps/{app}/instances/{key}、サンドボックスとしての削除、またはアプリの削除。

利用上限額、プラン、未払いによってスリープさせられたインスタンスが、アイドルを理由に削除されることはありません。

キーごとのドライブ

ドライブを使うと、キーごとのインスタンスに専用のドライブを持たせられます。ドライブは最初のインスタンスとともに作られ、キーとともに削除されます。バージョンで名前、パス、サイズを指定します。

glac deploy ghcr.io/acme/agent:3 --name agent --port 8080 --key-drive home:/home/app:10GiB

このドライブは、アプリの ID とキーとともに GET /v1/drives に表示されます。キーが持っている間は、削除することも、ほかでマウントすることもできません(409 key_drive)。サイズを指定しなければ、プランのドライブの最大サイズ(最大 64 GiB)になります。

1 つのインスタンスを新しいバージョンに

デプロイしても、キー別インスタンスはそのままです。別のバージョンに移すには、次のように要求します。

呼び出し 内容
POST /v1/apps/{app}/instances/{key}/upgrade {version?, when?, keep_days?} キーのインスタンスを version(ID か番号。指定しなければアプリの現在のバージョン。準備ができた古いバージョンも可)に移す。レスポンスはアップグレードとともに 202。すでにそのバージョンなら、インスタンスとともに 200。when:now(デフォルト)または asleep。keep_days:リバートに必要なものを保持する日数(0〜365、デフォルト 7)。Idempotency-Key を指定可能
GET /v1/apps/{app}/instances/{key}/upgrades キーのアップグレードの一覧(新しい順)
GET /v1/apps/{app}/instances/{key}/upgrades/{id} 1 つのアップグレード。?wait_seconds=(最大 600)で終わるまで待つ
DELETE /v1/apps/{app}/instances/{key}/upgrades/{id} まだ waiting のものを取り消す。始まったものは 409 upgrade_started
POST /v1/apps/{app}/instances/{key}/upgrades/{id}/revert {keep_days?} インスタンスを、そのアップグレードの直前の状態に戻す。レスポンスはリバート(kind: revert のアップグレード)とともに 202。Idempotency-Key を指定可能で、レスポンスが失われた後に再度要求しても同じリバートになり、その後に書き込まれたものに対して 2 回目のリバートが行われることはない

インスタンスは、ID、ノード、キー、プロジェクト内のアドレス、トークン、専用のドライブとその中身を保ちます。ディスク、プロセス、起動コマンド、ラベルは新しいバージョンのものになり、メモリにあったものと開いていた接続は失われます。ノードが移行している間、エッジはリクエストを保留し、移動のときと同じように、新しいバージョンが立ち上がったらそちらへ送ります。サンドボックスへの呼び出しには 503 upgrading が Retry-After 付きで返ります。サンドボックスの読み取り(GET /v1/sandboxes/{id})は引き続き応答し、最後に確認されたときのサンドボックスと status upgrading を返します。古い起動コマンドは、古いバージョンの stop に従って停止されます。新しいバージョンが立ち上がらなければ、ドライブを以前の中身のまま、元の状態に戻されます。アップグレードは failed、error.code は upgrade_failed、put_back: true となり、インスタンスはそれまでどおり応答します。

専用のドライブは名前に従います。key_drives に、キーがまだ持っていないドライブの名前があるバージョンでは、その時点でキーにドライブが与えられます。インスタンスに手を付ける前に、インスタンスのマシン上でバージョンが指定するサイズでキーのために作られ、イメージのその場所にあるものを入れた状態でパスにマウントされます(バージョンが nocopy を指定した場合を除く)。新しいバージョンが指定しなくなったドライブは、アンマウントされて保持されます。引き続きキーのもので、GET /v1/drives に表示され、ドライブとして課金され、キーとともに削除され、後のバージョンが再び指定すれば元のとおりにマウントされます。作成できないドライブ(たとえばプランで許されるドライブのサイズより大きいもの)があると、アップグレードは failed、error.code は upgrade_not_here で終わり、理由は error.message に入り、インスタンスは元のままです。done で終わらなかったアップグレードは作成したドライブを削除し、それらは課金されません。マシン上で失敗した(バージョンが立ち上がらず、インスタンスが元に戻された)アップグレードは、ドライブが消える少し前に failed として読み取れるので、drive.deleted イベントを待ってください。アップグレード以降、インスタンスのログ行は、そのインスタンスが動くバージョンの下に記録されます。

アップグレードは preparing(インスタンスのノードがバージョンのテンプレートを取得)、switching と進み、done か failed で終わります。timings.held_ms は、リクエストが保留された時間です。1 つのキーで同時に行えるアップグレードは 1 つで(409 upgrade_in_progress がそのアップグレードを示します)、準備ができていないバージョンは 409 version_not_ready になります。instance.upgraded と instance.upgrade_failed が、それぞれの終わり方を伝えます。

スリープしたら実行。when: asleep を指定すると、アップグレードはインスタンスがスリープするまで waiting のままで、スリープした時点で始まります。待っている間、インスタンスは普段どおり応答し、リクエストは 1 つも保留されません。切り替えが始まる瞬間に届いたリクエストだけが、その完了を待ちます。スリープしないインスタンスでは、取り消すまで待ち続けます。

**元に戻す。**完了したアップグレードは、リバートに必要なものを keep_days の間保持します。元の状態のインスタンスのスナップショット(プロジェクトのスナップショットで、名前は before-<upgrade id>、スナップショットとして課金)と、インスタンスが持っていた専用のドライブそれぞれのスナップショット(before-upgrade-<upgrade id>、ドライブのスナップショットとして課金)です。アップグレードの rollback がそれらと保持期限を示します。リバートで、ディスク、元の状態のドライブ、移行元のバージョンが戻ります(そのバージョンがマウントしていなかったドライブは、アンマウントしてそのまま保持)。ドライブのないインスタンスはプロセスも元のとおりに戻り、ドライブのあるインスタンスは、ドライブがマウントされてから起動コマンドを再び始めます。リバートも、自分自身を元に戻すために同じものを保持し、その中にはリバート前のドライブの中身も含まれます。期間が過ぎると両方のスナップショットは削除され、リバートは 409 rollback_gone になります。keep_days: 0 の場合や、スナップショットのどちらかが削除された場合も同じです。instance.reverted が、それぞれのリバートの完了を伝えます。

すべてのキーをまとめて。POST /v1/apps/{app}/upgrades {version?, from_versions?, keys?, when?, keep_days?, concurrency?, max_failures?} でバッチを作ります。インスタンスがそのバージョンでないすべてのキー(指定すれば、from_versions のもの、keys に含まれるもの)のアップグレードです。when が now でなければ各インスタンスのスリープを待ち、同時に進めるのは最大 concurrency(4)個、1 台のマシンでは 2 個までで、max_failures(5)個が失敗すると止まります。待機中のものは取り消され、完了したものは新しいバージョンのままです。GET /v1/apps/{app}/upgrades と …/upgrades/{id} はバッチと、それぞれの状態ごとの件数を表示し、POST …/upgrades/{id}/stop でバッチを止めます。アプリの keys.upgrade_on_deploy: true を設定すると、デプロイのたびにこのようなバッチが作られ、キーは、それぞれがスリープするたびに最新のバージョンに追従します。

専用の IPv6 アドレス

IPv6 アドレスのプールを持つマシン上で作られたキー別インスタンスは、そうしたサンドボックスと同じく、専用のパブリック IPv6 アドレスを持ちます。ポートを開くまでは、外から届くものすべてに対して閉じています(ネットワーク)。ポートには、インスタンスがどこにあっても、アプリとキーで指定してアクセスします。

呼び出し 内容
PUT /v1/apps/{app}/instances/{key}/tcp/{port} {from} from に挙げた送信元(IPv6 のプレフィックスかアドレス。全員なら ::/0)に対して TCP ポートを開く。または開いているポートにその送信元を設定する。UDP なら tcp の代わりに udp。インスタンスに専用のアドレスがなければ 409 no_ipv6
DELETE /v1/apps/{app}/instances/{key}/tcp/{port} ポートを閉じる。新しい接続は拒否され、既存の接続は続く
GET /v1/apps/{app}/instances/{key}/tcp 開いているポートと、それぞれの送信元

インスタンスはスリープや移動の後もポートを開いたままにしますが、同じキーの新しいインスタンスには引き継がれません。インスタンスのマシンに到達できない間、これらは 503 node_unavailable を返します。

glac cube instances ports open api user-42 5432 --from 2001:db8::/32
glac cube instances ports ls api user-42
glac cube instances ports close api user-42 5432

SDK にも、アプリに対してキーで指定する同じ 3 つがあります。Python では app.open_instance_port(key, port, from_=[…])、app.instance_ports(key)、app.close_instance_port(key, port) で(各 SDK のページにそれぞれの名前があります)、MCP サーバーには cube_instance_ports があります。

チェックポイント

マシンが失われたときに稼働中だったキー別インスタンスは、別の場所でディスクから再起動し、実行中だったものは失われます。チェックポイントがあれば、プロセスごと続きから再開します。アプリの checkpoint は、すべてのキー別インスタンスでチェックポイントを有効にします。すぐに反映され、新しいバージョンは作られません(どのサンドボックスでも使えます:サンドボックスのチェックポイント)。

glac cube update agent --checkpoint 10         # every 10 seconds while awake (on: 10)
glac cube update agent --reset checkpoint      # none
  • **取得されるもの。**インスタンスが稼働している間、every_seconds(1〜600 秒、指定しなければ 10 秒)ごとに、前回のチェックポイント以降にメモリとディスクで変わった部分が、圧縮されてオブジェクトストアに送られます。そのたびにマシンはインスタンスを数ミリ秒一時停止し、その間ネットワークも止めます。ドライブ(専用の key_drives を含む)も、同じ瞬間にディスクとともに固定されます。マシンが失われると、別のマシンが最新のチェックポイントからプロセスごとインスタンスを引き継ぎ、ドライブをその瞬間の状態に戻します。スリープするインスタンスはスリープから続きを再開し、何も失いません。ストアにすでにスリープの状態がある場合、復帰後の最初のチェックポイントで送られるのは、それ以降にインスタンスが書いたものだけです。
  • **引き継ぎで失われるもの。**インスタンスは最新のチェックポイントの瞬間に戻ります。それ以降に受け取って確認応答したものは失われ、相手側がもう一度送ることはありません。その時点の接続は次のパケットでリセットされるため、プログラムは接続し直します。それ以降に外の世界に対して行ったこと(別の場所にあるデータベースへの書き込み、送ったメッセージ)は、もう一度行われる可能性があります。外から見れば、少なくとも 1 回(at least once)です。
  • 書き込みの多いインスタンスは、回線を埋め尽くすのではなく遅れます。チェックポイントは、前のチェックポイントがストアに届いてから始まります。
  • 状況の確認:インスタンスの checkpoint(GET /v1/sandboxes/{id}、glac sandboxes get)には、every_seconds、last_at、behind_seconds(最新のチェックポイントが every_seconds よりどれだけ遅れているか)、bytes_per_second、stored_bytes、error(最後のチェックポイントが取得または送信されなかった理由)があります。
  • 取得されないのは、ステートフルなサービスのインスタンスと、ドライブ以外のボリューム(ストアにコピーのない通常のボリュームや共有ボリューム)をアタッチしたインスタンスです。checkpoint.error にそう示されます。
  • **料金。**インスタンスの最新のチェックポイントのうちストアが保持するものは、インスタンスの稼働中、スリープ中のインスタンスの状態と同じく課金されます(state、1 GB-月あたり $0.02)。送信に料金はかからず、送信トラフィックにもなりません。

SDK では、アプリのほかの設定とともに指定します(Python では checkpoint={"every_seconds": 10}、TypeScript では checkpoint: { everySeconds: 10 }、Go では Checkpoint(10)、Rust と Java では checkpoint(10))。MCP サーバーでは cube_deploy の checkpoint_seconds で指定します。

知っておくべきこと

  • サーバーが自ら停止した場合は、ステートフルなサービスと同じく(サービス)、同じインスタンスの中で、ディスクをそのままにして再起動されます(置き換えると、キーが保持するものが失われるため)。イベント instance.restarted がアプリとキーを示し、start_command が再起動の回数を示します。
  • **バージョン。**デプロイしても、既存のキー別インスタンスには影響しません。アプリが keys.upgrade_on_deploy を指定しない限り、それぞれはアップグレードされるまで(上記)、自分を作ったバージョンのまま、メモリとディスクを保ちます。新しいキーには、トラフィックを受けているバージョンが使われます。
  • **未対応:**コマンドラインと SDK では、リバート、スリープを待つアップグレード(when、keep_days)、バッチにはまだ対応していません。現在できるのはアップグレードの開始と追跡です(glac cube upgrade、SDK の upgrade)。MCP サーバーはアップグレードを開始しません(人が行うものです)。API はすべてに対応しています。アプリのログにはキー別インスタンスの行がありません。アプリのネットワークルール(egress)はキー別インスタンスには適用されません。
  • **署名にかかるコスト。**エッジは、リクエストがインスタンスに届く前に、1〜2 マイクロ秒で署名を確認してリクエストからキーを取り出します。
  • **課金。**キー別インスタンスは、ほかのサンドボックスと同じく計測されます。リクエストは key ラベルの下で数えられるため、GET /v1/usage?group_by=metadata.key でキーごとの量がわかります。
  • **制限。**キー別インスタンスはそれぞれ、アカウントの稼働中のサンドボックスとその vCPU、メモリ、そして 1 分間に作成するサンドボックスとスリープ中に保持するサンドボックスの上限に数えられます(制限)。
ナビゲーション

キーワードを入力して検索…

↑↓ 移動↵ 選択Esc 閉じる