チーム構成:複数サービス、AWS とオフィスのネットワーク、Sentry
製品が複数のサービスでできていて、dev 環境が AWS とオフィスのネットワークに分かれ、エラートラッキングに Sentry を使っているチーム向けの通しの例です。2 つのネットワークの接続、エージェントが変更したサービスを dev のほかのサービスが使う名前のままボックスで動かす方法、Sentry をそのまま使う方法、変更したサービスを別のボックスから使う方法、つながらないときに見るものを扱います。ここに書いたことはすべて ParallelSandbox の現在の動作です。できないことは、できないとこのページに書いてあります。
チームで 1 つのアカウント
ParallelSandbox のものはすべて 1 つのアカウントに属します。ボックス、環境とその接続、リンク、公開したバージョン、イメージのレジストリ、そしてクレジットです。チームは 1 つのアカウントで作業します:
- チームメイト(またはエージェント)ごとに、クイックスタートの 1 行で接続し、ブラウザでアカウントに一度サインインするだけです(OAuth)。事前に作るものも配るものもありません。OAuth に対応しないツールだけが
psbx_API キーを必要とします。アカウントのオーナーが、アプリにサインインしたセッションで REST からチームメイトやエージェントごとに 1 つ作ります(名前を付けてPOST /v1/keys。表示は一度だけ)。一覧はGET /v1/keys、失効はDELETE /v1/keys/{id}で、1 分以内に使えなくなります。API キーでキーを作ったり、一覧したり、失効させたりはできません。 - どのキーもアカウント全体に作用します。そのどれかのキーを持つエージェントは、アカウントのすべてのボックスを見て操作でき、その環境でボックスを起動し、そのどのボックスにもリンクし、そのどのバージョンも動かせます。リンクとバージョンはアカウントのすべてのキーの間で通用し、アカウントをまたぐことはありません。
- すべてのキーがアカウントのクレジットとプランを使います。
- アプリ(ボックス一覧、引き継ぎ、請求)を開けるのは、そのアカウント自身のログインだけです。チームメイトは自分の接続で作業し、変更を試す人にはボックスの URL だけで足ります。
1 つのアカウントに複数のチームメイトやエージェントがいるとき:
- どのキーも権限は同じです。どのキーでも、別のエージェントが起動したものを含め、アカウントのどのボックスでも停止、配線、引き継ぎができます。キーごとの権限はありません。チームメイトやエージェントごとにキーを分けるのは、1 つだけを失効できるようにするためです。
- クレジットは 1 つの残高です。どのエージェントのボックスもそこから消費し、
sandbox_status.creditsはどのエージェントにも同じ残高を示します。 - ボックス数の上限もアカウント単位です。同時に Free 3、Pro 5、Max 20 で、凍結中のボックスも数に入ります。上限に達すると、誰の
sandbox_startもplan <plan> allows <n> boxes at once and <m> are running or frozen; frozen boxes still hold a slot, so list them with sandbox_list (GET /v1/boxes) and sandbox_stop one you no longer need.で失敗し、続けてアイドルの規則が最初に止めるボックスを伝えます。sandbox_startにwaitForCapacitySecを付けると、呼び出しの中で枠が空くのを待てます。 - チームメイトのボックスは
sandbox_listで見つけます。誰が起動したかにかかわらずアカウントのすべてのボックスを、名前、目的、状態、URL、サービス(リンクのfromBox、バージョン)、リンクしているボックスと一緒に一覧にし、ボックスを復帰させることはありません。リンク先の id を探す、チームメイトが何を動かしているかを見る、忘れられたボックスを見つける、別の会話が残したボックスを引き継ぐ(ツールリファレンス)、といった用途に使います。 - ですから、各エージェントは使い終えたボックスを、
sandbox_reviewで人に渡すのでなければ、凍結のまま残さずsandbox_stopしてください。凍結中のボックスは課金されませんが、停止されるまで枠を使い続けます(idleTimeoutMinがなければ最後に使われてから 24 時間)。停止すれば枠はすぐに空きます。どちらもせずに残したボックスは、人のアプリで「AI は手を止めました」と表示され、どうするかを人が決めるまで待ちます。 - ボックスが記録するのは起動した MCP クライアント(
clientName)で、API キーではないので、誰のボックスかはボックスのnameでしかわからず、何のためのボックスかはgoalでわかります。エージェントはボックスに<人>: <作業>という名前を付けて残りはgoalに書き、止めるのは自分が起動したボックス(sandbox_startが返した id を控えておく)か名前で自分のものとわかるボックスだけにし、古い順にまとめて止めることは決してせず、チームメイトのボックスを止める前には確認してください。
誰が何を見られるか:
- アカウントにメンバーや役割はありません。「アカウント」で GitHub、Google、Apple のサインインをそれぞれ 1 つまでつなげられ、どれも同じアカウントをすべての権限で開くので、つなげるのは自分のものだけにしてください。
- アカウントにサインインしている人は、アプリでボックスのカード、「使う」ボタン、引き継ぎ、エージェントの
sandbox_sayのメッセージ(エージェントへのメモも残せます)を見られます。 - サインインしていないチームメイトは、自分のエージェント(
sandbox_start、sandbox_status、sandbox_list)からwebUrlと各services[].urlを受け取ります。これらの URL は持っている人なら誰でも使えます。アプリで「AI は手を止めました」と出るボックス(会話が終了した、または 1 時間使われておらず、人を待つカードもないもの)も同じくエージェントから探します。sandbox_listには各ボックスのagent.stateがあり、leftかidleのものが誰も作業していないボックスです。nameかidを伝えて自分のエージェントに引き継ぎを頼みます(別の会話が残したボックスを引き継ぐ)。 takeoverUrlはアプリのボックスのページを開くので、アカウントのサインインが必要です。sandbox_takeoverが作るリンク(…/box/<id>?t=<token>、30 分有効、初めて開いたときに開いてから 30 分に延長)は、サインインしなくてもその 1 回の引き継ぎを開けます。アカウントのモバイルアプリにプッシュ通知され、ツールの進捗メッセージにも含まれます。
サンプル構成
| 部品 | 動いている場所 | 呼び出し側が使うアドレス |
|---|---|---|
web、Next.js のフロントエンド、ポート 3000 |
ECS サービス web、クラスタ acme-dev |
https://dev.acme.example(公開ロードバランサー) |
api、ポート 8080 |
ECS サービス api |
api.acme.internal:8080(Cloud Map の DNS 名) |
billing、ポート 8080 |
ECS サービス billing |
billing.acme.internal:8080 |
admin、管理用の Web アプリ、ポート 8080 |
内部ロードバランサーの背後の ECS サービス admin |
internal-acme-dev-admin-1234567890.ap-northeast-1.elb.amazonaws.com:80 |
| Postgres | Aurora(RDS) | acme-dev.cluster-c1a2b3d4e5f6.ap-northeast-1.rds.amazonaws.com:5432 |
| Redis | ElastiCache | acme-dev.x1y2z3.ng.0001.apne1.cache.amazonaws.com:6379 |
ledger、ポート 9000 |
オフィスのサーバー | ledger.office.lan:9000 |
| ledger のデータベース | オフィス | 10.20.0.15:5432 |
| Sentry | sentry.io、またはオフィスのセルフホスト | DSN https://<key>@o123.ingest.sentry.io/456、または https://<key>@sentry.office.lan/7 |
エージェントは api を変更し、あとで billing も、さらに admin も変更しました。それ以外はすべて dev に残ります。
1. 1 つの環境に 2 つの接続
1 つの環境に接続はいくつでも持てます。ネットワークごとに 1 つ、ここでは aws と office を同じ環境 dev に作ります。
- 接続ごとに専用のトークン、専用のコネクタ(冗長化のために複数動かせる)、最大 100 個の
host:portのアドレス一覧があります。 - ボックス内のプログラムがアドレスに接続すると、その正確な
host:portを登録している接続を通ります。各アドレスは、そのネットワークから届く接続に登録してください。2 つの接続が同じアドレスを登録していると、名前の順で先の接続が使われます。 - 1 つのボックスが受け取れるアドレスは、すべての接続を合わせて最大 200 個です。アドレスはポート付きの正確なホスト名か IPv4 で、TCP のみです。
- 名前はコネクタが自分のネットワーク内で解決するので、Route 53 のプライベートゾーン、Cloud Map の名前、オフィスの DNS はサービスから使うときと同じように動きます。
エージェントが dev を作り(POST /v1/environments)、接続を 2 つ、aws と office を作ります(POST /v1/environments/dev/connections を 2 回)。トークンはそれぞれ、応答の runCommand に一度だけ出ます。
接続 aws:VPC 内の Fargate タスク
コネクタを、サービスと同じサブネットとセキュリティグループの ECS サービスとして動かします。トークンは SSM に置きます:
aws ssm put-parameter --name /parallelsandbox/connector-token --type SecureString --value 'psbx_conn_...'
aws logs create-log-group --log-group-name /ecs/parallelsandbox-connector
aws ecs register-task-definition --family parallelsandbox-connector \
--requires-compatibilities FARGATE --network-mode awsvpc --cpu 256 --memory 512 \
--execution-role-arn arn:aws:iam::<account>:role/ecsTaskExecutionRole \
--container-definitions '[{"name":"connector","image":"public.ecr.aws/b2n6a1j1/connector:latest","essential":true,
"secrets":[{"name":"PSBX_CONNECTOR_TOKEN","valueFrom":"arn:aws:ssm:ap-northeast-1:<account>:parameter/parallelsandbox/connector-token"}],
"logConfiguration":{"logDriver":"awslogs","options":{"awslogs-group":"/ecs/parallelsandbox-connector","awslogs-region":"ap-northeast-1","awslogs-stream-prefix":"connector"}}}]'
aws ecs create-service --cluster acme-dev --service-name parallelsandbox-connector \
--task-definition parallelsandbox-connector --desired-count 1 --launch-type FARGATE \
--network-configuration 'awsvpcConfiguration={subnets=[subnet-aaa,subnet-bbb],securityGroups=[sg-services],assignPublicIp=DISABLED}'
- 実行ロールには、そのパラメータへの
ssm:GetParametersを許可してください。SecureString がカスタマー管理の KMS キーで暗号化されているなら、そのキーへのkms:Decryptも必要です(既定のaws/ssmキーなら追加は不要)。 - サブネットにはインターネットへの出口が必要です(プライベートサブネットなら NAT ゲートウェイ)。タスクは
public.ecr.awsからイメージを pull し、api.parallelsandbox.com:443に接続します。内向きの接続はありません。 - コネクタが届くべき接続先はすべて、その接続先のポートでコネクタのセキュリティグループを受け入れる必要があります:RDS(5432)、ElastiCache(6379)、内部ロードバランサーのリスナーポート(80 か 443)、ECS タスク自体のコンテナポート(
api.acme.internalのような Cloud Map の名前はタスクの IP に解決されるので、接続はタスクに直接届きます)、そのほか登録したもの。コネクタをサービスのセキュリティグループで動かせば、そのグループをすでに受け入れている接続先はカバーされます。残りは個別に許可してください。 - サービスが DNS ではなく ECS Service Connect で互いを見つけている場合、その名前は同じ Service Connect 名前空間のタスクの中でしか解決できません。コネクタのサービスでも同じ名前空間で Service Connect を有効にしてください。
aws のアドレス。1 行に 1 つ、サービスの設定に書かれているとおりに:
acme-dev.cluster-c1a2b3d4e5f6.ap-northeast-1.rds.amazonaws.com:5432
acme-dev.x1y2z3.ng.0001.apne1.cache.amazonaws.com:6379
api.acme.internal:8080
billing.acme.internal:8080
internal-acme-dev-admin-1234567890.ap-northeast-1.elb.amazonaws.com:80
変更するかもしれないサービスの内部名も登録してください。ここでは api.acme.internal:8080 です。api を変更しないボックスはコネクタ経由で dev のものに届き、sandbox_start で api.acme.internal を宣言したボックスはその名前を引き取るので、そのボックスの reachable からは消えます。
ポート 80 の内部ロードバランサーも、ほかのアドレスと同じように登録するだけです。環境の各ホストはボックス内に専用のアドレスを持ち、boxd はアドレス自身のポートでは待ち受けないので、host:80 が boxd 自身のポート 80 とぶつかることはなく、自分のプロセスがこれらのアドレスと同じポート番号で待ち受けることもできます。
サービス設定を取り込むと(後述)、GET /v1/environments/dev/suggested-endpoints が設定内で見つかったアドレスを返します。対象は値の中の host:port、URL(ポートがなければスキームの既定:postgres 5432、mysql 3306、redis 6379、mongodb 27017、amqp 5672、http 80、https 443)、そして対応する *_PORT がある *_HOST 変数のうち、ホストがプライベート IPv4(10.0.0.0/8、172.16.0.0/12、192.168.0.0/16)か、.internal、.local、.lan、.corp、.svc、.cluster.local、.rds.amazonaws.com、.cache.amazonaws.com、.es.amazonaws.com、.mq.amazonaws.com、.docdb.amazonaws.com、.redshift.amazonaws.com で終わるものです。それ以外、たとえば内部ロードバランサーの *.elb.amazonaws.com という名前や自社ドメインのプライベートゾーンは自分で追加します。エージェントが PUT /v1/environments/dev/connections/<id>/endpoints でリスト全体を設定します。アドレスを調べるには:
aws rds describe-db-clusters --query 'DBClusters[].[Endpoint,ReaderEndpoint,Port]' --output text
aws rds describe-db-instances --query 'DBInstances[].[Endpoint.Address,Endpoint.Port]' --output text
aws elasticache describe-replication-groups --query 'ReplicationGroups[].[ReplicationGroupId,NodeGroups[0].PrimaryEndpoint.Address,NodeGroups[0].PrimaryEndpoint.Port]' --output text
aws elbv2 describe-load-balancers --query 'LoadBalancers[?Scheme==`internal`].[LoadBalancerName,DNSName,LoadBalancerArn]' --output text
aws elbv2 describe-listeners --load-balancer-arn <arn> --query 'Listeners[].[Port,Protocol]' --output text
実行時に追加のアドレスを知るクライアントは、サーバーが渡したアドレスに接続します。Redis Cluster のノードの IP、Kafka が通知するブローカー、MongoDB レプリカセットのメンバーなどです。それらの host:port も 1 つずつ登録しないと、ボックスからは接続できません。
接続 office:オフィス内のホスト
ledger、そのデータベース、Sentry に届くオフィス内の Linux マシンで:
docker run -d --name parallelsandbox-connector --restart=always \
-e PSBX_CONNECTOR_TOKEN=psbx_conn_... \
-e PSBX_ALLOW=ledger.office.lan:9000,10.20.0.15:5432,sentry.office.lan:443 \
public.ecr.aws/b2n6a1j1/connector:latest
office のアドレス:
ledger.office.lan:9000
10.20.0.15:5432
sentry.office.lan:443
PSBX_ALLOW は任意で、同じ一覧を自分の側でもう一度強制します。ホストでは *.office.lan が解決できるのにコンテナでは解決できない場合は、コネクタを --dns <オフィスの DNS サーバー> か --network host で起動してください。
サービス設定
- AWS から取り込む(
POST /v1/environments/dev/services/importに{"region":"ap-northeast-1","cluster":"acme-dev","service":"api"}、サービスごとに 1 回):api、billing、web、admin。AWS からサービス設定を取り込むの読み取り専用ロールが必要で、AWS アカウントを管理する人が 1 回作ります。通常のenvironmentの値はそのまま、SSM と Secrets Manager の参照は解決されます。S3 のenvironmentFilesは読めず、sourceRef.skippedに返ります。それぞれがボックスでは/work/.sbx/env/<サービス>.envと.shになります。 - 設定の一部を S3 の
environmentFilesのファイルに置いているサービスは、そのファイルの内容を別の名前(例:api-files)でアップロードし(PUT /v1/environments/dev/services/api-filesに{"dotenv": "…"})、両方のファイルを付けてサービスを起動します。ECS と同じくタスク定義自身の値が優先されるよう、そちらを先に置きます:docker run --env-file /work/.sbx/env/api-files.env --env-file /work/.sbx/env/api.env ...(または. api-files.sh && . api.sh)。取り込んだ名前でアップロードしないでください。アップロードはそのサービスの設定を置き換えます。 ledgerの設定も同じようにアップロードします(PUT /v1/environments/dev/services/ledger)。ボックスで動かすことがあるなら。externalBaseUrlは必要なければ空のままにします(公開された dev エンドポイントと externalBaseUrl を参照)。
ボックスに移したサービスの AWS ロール
ECS では api と billing の AWS の権限(S3、SQS など)はタスクロールから来ていて、設定には入っておらず、ボックスにはそのロールがありません。代わりに環境にロールを持たせます。手順はボックス内の AWS 権限のとおりで、エージェントが GET /v1/environments/dev で CloudFormation のリンク(awsBoxRoleQuickCreateUrl)を取り、AWS アカウントを管理する人がそれでロールを作って、移したサービスのタスクロールが持っていた権限(ボックスで dev のイメージを pull するなら ECR の pull も)を与え、エージェントが PUT /v1/environments/dev に {"awsRoleArn", "awsRegion"} を付けて設定します。そのあと起動したボックスは ECS のタスクと同じように一時的な認証情報を受け取ります。コンテナは --env-file /work/.sbx/env/<サービス>.env で受け取ります。
エージェントが結果を確認します:
sandbox_environments {}
dev の reachable に 8 つのアドレスすべてが並び、connections[] で aws と office がどちらも online: true、services に api、billing、web、admin、ledger とそれぞれの envFile があるはずです。
connectorOnline: true は、少なくとも 1 つの接続が接続中という意味で、両方とは限りません。接続ごとの状態は connections[] で見ます:online、sessions、lastSeenAt、connectorVersion、そして reachable(その接続を通るアドレス)です。止まっている接続があれば、sandbox_start の next が挙げます。たとえば These connections have no connector online right now, so their addresses will refuse connections until their connector runs again: office (ledger.office.lan:9000, 10.20.0.15:5432, sentry.office.lan:443).。最後の確認は、今でも動いているボックスからそのネットワークのアドレスの 1 つに本物のクライアント(curl、pg_isready、psql、redis-cli はボックスに入っています)で接続し、sandbox_status → health.privateEndpoints[] でそのアドレスを見ます。TCP 接続だけでは何もわかりません。boxd はコネクタに問い合わせる前に接続を受け付けるからです。office のコネクタが止まっていると、そのアドレスには failed が付き、lastError は HTTP 503: the connector "office" of this environment is offline; start it in your network (docker run … parallelsandbox connector) and retry になり、クライアントには接続のリセットとして見えます。
2. 変更したサービスを dev と同じ名前で動かす
ボックス内で名前がどう働くか:
servicesの名前は、ボックスが起動した時点からボックス内に専用のアドレスを持ち、ボックスの DNS はプロセスにもコンテナにもそのアドレスで名前を答えます。それが環境のアドレスでもある場合、そのボックスではreachableから外れるので、コネクタではなくボックス内のあなたのものを指します。宣言しなかったものは、すべてコネクタ経由で dev に届き続けます。- この変化が見えるのはボックス内のプログラムだけです。dev のサービス、そのキューや定期ジョブは dev の
apiを呼び続けます。あなたのネットワークからボックスに接続を開く手段はありません。apiを通る一連の処理をテストするなら、必要な呼び出し側もボックスで動かしてください(dev の変更していない呼び出し側をボックスで動かす)。 servicesのportは呼び出し側が接続するポートで、name:portはプロセスが待ち受けるtargetPort(省略時はport)に届きます。呼び出し側は dev で使っているアドレスのまま、プロセスは別のポートで待ち受けられます。dev でどちらも 8080 で待ち受ける変更サービスが 2 つあっても、api.acme.internal:8080とbilling.acme.internal:8080のまま、それぞれ別のtargetPortを持たせます(同じポートの 2 つ目の変更サービスを参照)。- プロセスは
0.0.0.0のtargetPortで待ち受けてください。127.0.0.1だけで待ち受けるプロセスには、自分の URL からは届きますが名前では届きません。 - ボックスのポート 80 と 9095 は boxd のものです(80 はシーンの入口、9095 はその API)。プロセスやコンテナはこれらで待ち受けられません。呼び出し側がポート 80 で接続するサービス(内部ロードバランサーの背後のサービスなど)は、ボックスでも名前とポート 80 をそのまま使います。自分のプロセスが待ち受けるポートを
targetPortとして宣言してください(内部ロードバランサーの背後の変更サービスを参照)。targetPortがないとsandbox_startは 400 で失敗します。それ以外のポートは 443 などの特権ポートも含めて自由に使えます。コマンドは root で動き、コンテナは通常どおりポートを公開できます。 - 環境のアドレスを引き取るには、その名前を
sandbox_startで宣言するか、sandbox_status→health.featuresにtake-overがあるボックスで、あとからsandbox_wireで宣言します。名前はアドレスをそのまま保ち、コネクタを通って開いていた接続は閉じられ、以後はボックスのコピーに届きます。古いボックスでは、あとから加えた名前が環境のアドレスならコネクタ経由のままです。 - 宣言も登録もしていないものには届きません。その現れ方はそれぞれ違います(ボックスで確認済み):
- 宣言した名前や環境のホストに別のポートで接続すると、ボックス内にとどまります。ボックスでそのポートを
0.0.0.0で待ち受けるものが応答し、何もなければ接続はすぐに拒否されます。あなたのネットワークや別のボックスには届きません。 - 宣言しておらず環境のアドレスでもないホスト名は、あなたのではなく公開 DNS で引かれます。プライベート DNS だけが知っている名前(Cloud Map、Route 53 のプライベートホストゾーン、オフィスの DNS)は解決されません(
Could not resolve host)。公開 DNS がプライベート IP に解決する名前(RDS のエンドポイントやinternal-…elb.amazonaws.com)は解決され、接続は登録していないプライベート IP と同じくタイムアウトします。届かせるには登録してください。公開された名前はインターネットに直接出ます。 - 登録していないプライベート IP アドレスは応答しません。接続はタイムアウトし、
private-endpoints.logにも何も書かれません。登録した IP アドレスに登録していないポートで接続した場合も同じです。たとえば10.20.0.15:22。 - ポート 80 は扱いが違います。ポート 80 で登録した環境のアドレス(内部ロードバランサーなど)と、ポート 80 で宣言したリンクは、ほかの名前と同じように使えます。どの名前も専用のアドレスを持ち、ボックス自身のポート 80 とぶつかりません。ポート 80 に
targetPortが要るのはボックス内のサービスだけです。80 で宣言も登録もしていない名前(別のポートで宣言したサービスやリンク、別のポートだけで登録した環境のホスト)に 80 で接続すると、boxd のシーンの入口に届き、プログラムからもコンテナからも 502nothing listens on <name>:80 in this box: that name is declared on another port. Dial the port it is declared on, or declare it on port 80 with targetPort, the port your process listens onが返ります。localhost:80は今もシーンの入口で、最初に宣言したサービスに届きます。2026-09-24 20:30 UTC より前に起動したボックスでは、そういう名前に 80 で接続すると最初に宣言したサービスに届きます。ポート 9095 は boxd の API で、404 page not foundを返します。
- 宣言した名前や環境のホストに別のポートで接続すると、ボックス内にとどまります。ボックスでそのポートを
ボックスを起動する
sandbox_start {
"name": "ana:api 返金の改修",
"goal": "会員センターの返金ボタンで返金が台帳に記録されるようにする。dev のデータでブラウザから返金が最後まで通れば完了",
"environment": "dev",
"size": 2,
"services": [
{ "name": "web", "port": 3000, "web": true },
{ "name": "api.acme.internal", "port": 8080 }
]
}
web: trueは、webに人が開ける URL を付けます。その URL が結果のwebUrl、つまり最初の web サービスの URL です(ここではwebをたまたま先頭に宣言したのでsceneUrlでもあります)。人は自分のスマホやパソコンでこれを開いて変更を試し、アプリの「使う」ボタンは、webが一覧の何番目にあっても同じ URL を開きます。apiにはwebがないので URL はなく、ボックスの外からは届きません。api.acme.internalは、webやほかのサービスがapiを呼ぶときの名前そのものです。そのためボックス内のwebは、設定を何も変えずにボックス内のapiに届きます。size: 2(4 vCPU、16 GB):このボックスは複数のサービスのイメージをビルドして動かし、あとでさらに増えます。サイズ 1 は Web フロントエンド 1 つか、Node、Python、Go のサービス 1〜2 個に向いています。nameの先頭にボックスを使う人を書いておくと、チームメイトのエージェントがsandbox_listで誰のものか見分けられます。goalにはボックスの目的と完了の条件を書きます。人はボックスのカードでそれを読み、チームメイトのエージェントや、この会話が閉じられたあとの新しい会話は、これまでの手順と一緒にsandbox_statusで読みます。- ボックス内のプログラムも、どの Docker ネットワーク(compose を含む)のコンテナも、同じように
api.acme.internalと環境のアドレスに届きます。 - 結果の
environment.reachableからapi.acme.internalは消え、RDS、Redis、billing.acme.internal、内部ロードバランサー、ledger.office.lan、10.20.0.15、sentry.office.lanは残ります。
作業ツリーを送り、両方のサービスを dev の設定で起動します:
sandbox_sync { "id": "<id>", "localPath": "/home/dev/acme/platform", "dest": "platform" }
sandbox_exec { "id": "<id>", "cmd": "docker build -t api ./api && docker run -d --name api --env-file /work/.sbx/env/api.env -e SENTRY_ENVIRONMENT=psbx -e SENTRY_RELEASE=a1b2c3d -e SBX_BOX_ID -p 8080:8080 api", "cwd": "platform", "timeoutSec": 900, "note": "変更した api を dev の設定でビルドして起動する" }
sandbox_exec { "id": "<id>", "cmd": "docker build -t web ./web && docker run -d --name web --env-file /work/.sbx/env/web.env -p 3000:3000 web", "cwd": "platform", "timeoutSec": 900, "note": "web フロントエンドを dev の設定でビルドして起動する" }
sandbox_exec { "id": "<id>", "cmd": "curl -fsS http://api.acme.internal:8080/health && curl -s -o /dev/null -w '%{http_code}\\n' http://localhost:3000/", "note": "api と web が応答するか確認する" }
--env-file の後ろの -e が優先されるので、ファイルを編集せずに 1 つの値だけ dev と変えられます。
どこからどこへ届くか:
| 呼び出し側 | 接続先 | 届く先 |
|---|---|---|
ボックス内の web |
api.acme.internal:8080 |
ボックス内の api |
ボックス内の api |
RDS、Redis、billing.acme.internal:8080、内部ロードバランサー |
AWS の dev(aws 経由) |
ボックス内の api |
ledger.office.lan:9000、10.20.0.15:5432、sentry.office.lan:443 |
オフィス(office 経由) |
ボックス内の api |
o123.ingest.sentry.io:443 などの公開アドレス |
インターネットに直接 |
| 人のブラウザ | webUrl |
ボックス内の web、ポート 3000 |
dev の web、billing |
api.acme.internal:8080 |
dev の api(変更前のまま) |
人が開くページ
webUrl やボックスのほかの URL で読み込んだページは、人のブラウザ、つまりボックスの外で動きます。ボックスに届くのはボックスの URL へのリクエストだけで、各 URL が届くのは 1 つのサービスです。sceneUrl は最初に宣言したサービス、services[].url はそれぞれのサービスです。フロントエンドのブラウザ側のコードが絶対 URL(NEXT_PUBLIC_API_URL=https://dev.acme.example/api)を呼ぶと、そのリクエストはボックスの api ではなく dev に行きます。ブラウザのコードがボックスの api を別オリジンとして呼べるのは、api に web があり、その URL をページに渡し(組み立てはできません)、CORS がページのオリジンを許可している場合だけです。より簡単なのは同一オリジンのプロキシです。ブラウザからはフロントエンド自身のオリジンのパスを呼ばせ、web のサーバーがボックス内でそれを http://api.acme.internal:8080 に転送します。パスは dev とまったく同じ形で転送してください。dev のロードバランサーが /api/... をそのまま api に渡しているなら(パスのルールはふつうそうです)、転送先でも接頭辞を残します。外すのは、dev で何かが外している場合だけです。
apiが/api/ordersを受け取る(接頭辞を残す):Next.js のrewrites()は{ source: '/api/:path*', destination: 'http://api.acme.internal:8080/api/:path*' }、Vite はserver: { proxy: { '/api': 'http://api.acme.internal:8080' } }。apiが/ordersを受け取る(接頭辞を外す):Next.js のrewrites()は{ source: '/api/:path*', destination: 'http://api.acme.internal:8080/:path*' }、Vite はserver: { proxy: { '/api': { target: 'http://api.acme.internal:8080', rewrite: (p) => p.replace(/^\/api/, '') } } }。
ボックスでその設定で web をビルドしてください。dev が /api を web の書き換えではなく、ロードバランサーのパスのルールで API に送っているなら、ボックスの web の前にそのロードバランサーはありません。ボックスのビルドに書き換えを加えてください。そうしないと、ページの /api/... の呼び出しは web 自身に届いて失敗します。ボックス内のブラウザ(Playwright、ディスプレイ上の Chromium)は宣言したすべての名前を解決できるので、これは不要です。
Next.js は next build の時点で 2 つのものを固定します。ブラウザのコードに書き込まれる NEXT_PUBLIC_* の値と、ビルドの出力に入る next.config.js の rewrites() です。そのため、ボックスで web をビルドする前に両方をそろえておきます。まず /api の書き換えを、ボックスのコピーの web/next.config.js に書きます(dev にこのファイルがあれば、ほかの設定と並べて):
module.exports = {
async rewrites() {
return [{ source: '/api/:path*', destination: 'http://api.acme.internal:8080/api/:path*' }];
},
};
次に設定を渡してビルドします。. /work/.sbx/env/web.sh はすべての設定をボックスのシェルに export し、docker build は --build-arg で名前を挙げた変数の値をそこから取ります:
cd /work/platform && . /work/.sbx/env/web.sh && export NEXT_PUBLIC_API_URL=/api
docker build $(sed -n 's/^\(NEXT_PUBLIC_[A-Za-z0-9_]*\)=.*/--build-arg \1/p' /work/.sbx/env/web.env) -t web ./web
Dockerfile ではそれぞれを宣言する必要があります(next build を実行するステージで ARG NEXT_PUBLIC_API_URL)。宣言のないビルド引数は、Docker が警告を出して無視します。export の行がボックス用に値を変える場所で、ここでは同一オリジンのパスにしています。web.env にない NEXT_PUBLIC_* の名前には、自分で --build-arg NAME を足します。どちらかをあとで変えたら、ビルドし直しです。コンテナを再起動しても反映されません。Docker を使わず、ボックス上で直接ビルドしてもかまいません:cd web && . /work/.sbx/env/web.sh && npm ci && npx next build。
ボックスの URL の前にログインはありません。URL の key が唯一の保護です。webUrl を持つ人は誰でもその人と同じように web を使え、ボックスを通じて届くものすべてにも届きます。ここではボックス内の api と、その先の dev のデータベースやサービスです。dev を使ってよい人にだけ共有してください。チャットアプリ(Slack、Teams、LINE など)は、貼られた URL を取りに行ってプレビューを作ります。その取得もほかのリクエストと同じで、プレビューのサービスはページを受け取り、リクエストはボックスの利用として数えられ、HTML を求める取得はページの読み込みと同じく凍結中のボックスを復帰させます。ボックスが止まると URL は使えなくなります。ボックスが人のブラウザに送るもの、ページもファイルも API の応答も、そのボックスの外向き通信として数えます(クレジット)。
その人がボックスを起こしておく必要はありません。その人のリクエストは、最後のツール呼び出しまたは復帰から 2 時間までボックスを起こしておきます。それを過ぎてもページを使い続けていると、途中でボックスが凍結することがあります。凍結していたら、たとえば昼食から戻ってきたときは、再読み込みするだけです。ページに「Waking this box…」と表示され、10 秒ほどで自動で再読み込みされてアプリになり、その復帰は使用として数えるので、また 2 時間続きます。復帰させるのはページの読み込みだけで、開いているページ自身のリクエストでは復帰しないので、長く間が空いて応答しなくなったフロントエンドは再読み込みが必要です。アカウントのクレジットが尽きていれば、ページはボックスが一時停止中だと表示します。停止したボックスは戻りません。新しいボックスには新しい URL が付きます。
ボックスのどの URL からのリクエストも Host: 127.0.0.1:<targetPort> でプロセスに届きます。公開ホストは X-Forwarded-Host(<id>-<key>.box.parallelsandbox.com または <id>-<key>-<targetPort>.box.parallelsandbox.com)、スキームは X-Forwarded-Proto(https)、クライアントのアドレスは X-Forwarded-For に入ります。既知のホストだけを受け付ける dev server はそのまま通し、相対パスのリダイレクトもそのまま動きます。Host から絶対 URL、リダイレクト、Cookie のドメインを組み立てるアプリは、代わりに転送ヘッダーを信頼するよう設定してください(Express なら app.set('trust proxy', true))。ボックス内で名前を使ってサービスを呼ぶプログラムは、api.acme.internal:8080 のような自分の Host を送ります。
ボックスの URL でのサインイン(SSO、OAuth)
web が ID プロバイダー経由でサインインさせる場合、プロバイダーはボックスの URL をリダイレクト URI として受け入れる必要がありますが、ボックスの URL はボックスごとにランダムなホストを持つので、前もって登録できません。使える方法は 2 つです:
- ボックスのコピーを、チームがローカルで使うログイン方法で動かします。あらかじめ用意したテストユーザーや dev 専用の認証モードを、サービスの設定か
--env-fileのあとの上書きで設定します。 - ボックスが動いている間だけ、その正確な URL を登録します。プロバイダーの管理 API で dev クライアントの許可リダイレクト URI を変えられるなら、エージェントは
sandbox_startのあとにwebUrlやservices[].urlの URL を加え、sandbox_stopの前に外します。例:Amazon Cognito のaws cognito-idp update-user-pool-client --callback-urls …(指定しなかった設定は既定値に戻るので、クライアントの現在の設定も一緒に渡します)、Auth0 Management API のPATCH /api/v2/clients/{id}とcallbacks、Keycloak 管理 API のPUT /admin/realms/{realm}/clients/{id}とredirectUris。
https://*.box.parallelsandbox.com/* のようなワイルドカードは決して登録しないでください。すべてのアカウントのボックスがこのドメインを共有しているので、ParallelSandbox の誰でも、あなたのユーザーの認可コードを自分のボックスに送らせることができてしまいます。サインインは URL の key の代わりにもなりません。ボックスの前にある保護は引き続きその key だけです。
同じポートの 2 つ目の変更サービス
billing も変更し、これも 8080 で待ち受けていますが、ボックスの 8080 はすでに api が使っています。名前とポートはそのまま、別の targetPort を与えます:
sandbox_start {
"name": "ana:api と billing 返金の改修",
"goal": "api と billing の返金をまとめて改修する。ブラウザで行った返金が変更した billing に記録されれば完了",
"environment": "dev",
"services": [
{ "name": "web", "port": 3000, "web": true },
{ "name": "api.acme.internal", "port": 8080 },
{ "name": "billing.acme.internal", "port": 8080, "targetPort": 8081 }
]
}
sandbox_sync { "id": "<id>", "localPath": "/home/dev/acme/platform", "dest": "platform" }
sandbox_exec { "id": "<id>", "cmd": "docker build -t billing ./billing && docker run -d --name billing --env-file /work/.sbx/env/billing.env -p 8081:8080 billing", "cwd": "platform", "timeoutSec": 900, "note": "変更した billing をビルドして 8081 で公開する" }
sandbox_exec { "id": "<id>", "cmd": "docker build -t api ./api && docker run -d --name api --env-file /work/.sbx/env/api.env -p 8080:8080 api", "cwd": "platform", "timeoutSec": 900, "note": "変更した api をビルドして起動する" }
sandbox_exec { "id": "<id>", "cmd": "curl -fsS http://billing.acme.internal:8080/health && curl -fsS http://api.acme.internal:8080/health", "note": "両方が dev の名前で応答するか確認する" }
名前ごとにボックス内のアドレスがあるので、billing.acme.internal:8080 は 8081 で公開した billing のコンテナに、api.acme.internal:8080 はそのまま api に届きます。呼び出し側の設定は何も変えません。api は dev と同じく http://billing.acme.internal:8080 を呼び続けます。billing.acme.internal は環境のアドレスなので、上書きするには sandbox_start で宣言する必要があります。
dev の変更していない呼び出し側をボックスで動かす
dev で billing を呼ぶのは api だけで、dev の api は dev の billing を呼び続けます。エージェントが billing だけを変更したなら、呼び出し側もボックスで動かさない限り、ボックス内のものは呼ばれません。変更していない api を、dev の名前のまま変更した billing と一緒にボックスで動かします:
sandbox_start {
"name": "ana:billing 返金ルール",
"goal": "billing の返金ルールを変更する。ボックスで動かす変更なしの dev の api が、変更した billing から新しい返金額を受け取れば完了",
"environment": "dev",
"services": [
{ "name": "api.acme.internal", "port": 8080 },
{ "name": "billing.acme.internal", "port": 8080, "targetPort": 8081 }
]
}
変更した billing は前の節と同じように起動します。dev の api を用意する方法は 3 つ:
dev が動かしているコミットのソースから、dev の設定でビルドする。ボックス内のコピーをそのコミットにしてから
docker build -t api ./api && docker run -d --name api --env-file /work/.sbx/env/api.env -p 8080:8080 api。環境の AWS ロールがそのリポジトリから pull できるなら、dev が動かしているイメージを自社の ECR から pull する(ボックス内の AWS 権限):
aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin <account>.dkr.ecr.ap-northeast-1.amazonaws.com docker run -d --name api --env-file /work/.sbx/env/api.env -p 8080:8080 <account>.dkr.ecr.ap-northeast-1.amazonaws.com/api:<dev が動かしているタグ>ボックスは x86_64(amd64)です。dev が
apiを Graviton で動かしていて(タスク定義のruntimePlatform.cpuArchitecture: ARM64)、イメージに amd64 版がなければ、docker runはexec format errorで失敗します。その場合は 1 つ目の方法でソースからビルドするか、dev のパイプラインにマルチアーキテクチャのイメージを push してもらいます(docker buildx build --platform linux/amd64,linux/arm64)。エミュレーションは入っていません(ツール)。ロールには
ecr:GetAuthorizationTokenと、そのリポジトリへのecr:BatchCheckLayerAvailability、ecr:GetDownloadUrlForLayer、ecr:BatchGetImageが必要です(AWS 管理ポリシーAmazonEC2ContainerRegistryReadOnlyに 4 つとも含まれます)。ないとログインはAccessDeniedException … is not authorized to perform: ecr:GetAuthorizationTokenで失敗します。dev が動かしているイメージは、サービスの現在のタスク定義にあります:aws ecs describe-services --cluster acme-dev --services api --query 'services[0].taskDefinition'のあとaws ecs describe-task-definition --task-definition <arn> --query 'taskDefinition.containerDefinitions[].image'(ロールが ECS を読めるなら)。読めなければ人に聞いてください。apiのビルドがすでにバージョンとして公開されていれば(sandbox_versions { "service": "api" })、それを宣言してボックスに動かさせます:{ "name": "api.acme.internal", "port": 8080, "version": "<ラベル>" }(公開したバージョンを動かす)。
そしてボックスから一連の処理を動かします。curl http://api.acme.internal:8080/... はボックス内の api に届き、それがボックス内の変更した billing を呼びます。呼び出し側がすでに自分の別のボックスで動いているなら、2 つ目のコピーは不要です。そのボックスからリンクで名前をこのボックスに向けます:sandbox_wire { "id": "<api を動かしているボックス>", "service": "billing.acme.internal", "port": 8080, "fromBox": "<このボックス>" }(別のボックスへのリンク)。
書き込みは dev のデータベースに行く
ボックス内の変更したサービスは dev の設定で動くので、dev の本物のデータベースにつながります。マイグレーションも書き込みもすべてそこに入り、dev の全員に影響します。変更にマイグレーションが含まれる場合や、テストがデータを書き込んだり消したりする場合は、ボックスに専用のデータベースを用意してください:
テストには、
psbx-testdb up <名前>がボックス内にまっさらな Postgres 16(--mysqlなら MySQL 8.0。データはメモリ上で、ボックスとともに消えます)をランダムなポートで起動し、接続できるまで待ってから/work/.sbx/testdb/<名前>.envをexportの行で書きます。DATABASE_URL、TEST_DATABASE_URL、TEST_POSTGRES_URI(Postgres)、PG*変数(psqlがそのまま使えます)、Laravel のDB_*で、MySQL ではPG*の代わりにMYSQL_*です。表示するのはデータベースごとの 1 行と、実行する. /work/.sbx/testdb/<名前>.envで、接続文字列は表示しません。psbx-testdb url <名前>がパスワードごと表示します。ボックスの127.0.0.1で待ち受けるので、ボックス上で動くプログラムからは届きますが、コンテナからは--network hostのときだけ届きます。ほかの使い方(psbx-testdb --help):- テストが別の変数名を読むと何も見つからず、まとめて SKIP されて通ったように見えます(全部
SKIP、または 0.0x 秒でok)。--as 名前(繰り返し可)でその名前にも接続文字列を書きます。例:psbx-testdb up --as BILLING_TEST_DSN。 - 名前を複数渡すと複数のデータベースを一度に起動します(
psbx-testdb up a b c)。すでに動いている名前はデータごとそのまま使います。psbx-testdb reset <名前>はそのデータベースを drop して作り直し、down <名前>は.envごと片付けます。 - データはメモリ(tmpfs)にあり、上限はボックスのメモリの 4 分の 1 で、2〜4 GB の範囲です。
--size 8gで上限を上げ、--diskならデータを/work/.sbx/testdb/<名前>/dataに置き、メモリを使わず上限もありません。データ領域がいっぱいになると Postgres が PANIC するかコンテナが終了します。そのときはurlとenvがそのdocker logsを表示するので、downしてからup --size 8gか--diskで起動し直してください。 --redisを付けると代わりに Redis 7 を起動します(名前を渡さなければredis)。メモリのみで保存はせず、上限はほかと同じく--sizeです(満杯の Redis は書き込みに OOM エラーを返します)。.envはREDIS_URL、TEST_REDIS_URL、REDIS_HOST、REDIS_PORT、resetはFLUSHALLで空にし、gotestには使えません。2026-10-06 以降にビルドしたイメージには入っていて、古いボックスでは最初のup --redisで取得します。- コンテナは Alpine イメージなので、
docker execで中で動かすコマンドは BusyBox 版です。df -Pkは使えますが、df --outputのような GNU のオプションは使えません。 psbx-testdb gotest ./...は、パッケージごとに新しいデータベースを用意して(go test -exec経由)go testを実行し、終わったら片付けます。共有のデータベースでパッケージ同士がテーブルを消し合うことがありません。--as、--size、--disk、--mysqlも使え、--keepなら終わったあともデータベースを残して調べられ、--より後ろはすべてgo testに渡ります。--s3は代わりに S3 互換サーバー(名前を渡さなければs3。rclone のserve s3で、MinIO のイメージとバイナリはもう入手できません)を起動し、名前と同じ bucket(アンダースコアはハイフンに)を作っておきます。.envはAWS_ENDPOINT_URL、AWS_ENDPOINT_URL_S3(http://127.0.0.1:<port>)、AWS_ACCESS_KEY_ID、AWS_SECRET_ACCESS_KEY、AWS_REGION、S3_BUCKETを export し、aws CLI、boto3、AWS SDK はそのまま読みます。エンドポイントが IP なので path-style でアクセスします。bucket を増やすにはaws s3 mb、resetで全部空にでき、--sizeと--diskはデータベースと同じです。psbx-testdb listはすべてを表示します。名前、エンジン、ポート、状態、上限に対する使用量、作成時刻、owner(--ownerで指定。既定はupを実行したディレクトリ)です。upは 6 時間以上動いたままのデータベースがあると知らせます。
- テストが別の変数名を読むと何も見つからず、まとめて SKIP されて通ったように見えます(全部
コンテナで動くサービスには、隣にデータベースのコンテナを起動し、
--env-fileの後ろで設定を上書きしてから、それに対してマイグレーションを実行します:docker network create acme docker run -d --name billing-db --network acme -e POSTGRES_PASSWORD=dev postgres:16-alpine docker run -d --name billing --network acme --env-file /work/.sbx/env/billing.env -e DATABASE_URL=postgres://postgres:dev@billing-db:5432/postgres -p 8081:8080 billing
dev の設定によるほかの副作用
dev の設定で動くコピーは、データベースへの書き込みだけでなく、dev のコピーがすることを何でもします。ボックスの billing は:
- dev のキューからメッセージを受け取り、dev の
billingの仕事を横取りするかもしれません; - 月次の請求書やリマインダーのメールといったスケジュールジョブを、もう一度実行するかもしれません;
- プロバイダーに webhook を登録したり、dev 宛ての webhook を受け取ったりするかもしれません;
- 設定にあるプロバイダーで本物のメールや SMS を送るかもしれません;
- 決済プロバイダーで課金や返金をするかもしれません。
ボックスのコピーでは:
- コンシューマーとスケジューラーをサービス自身のスイッチで止め、
--env-fileのあとに設定します。たとえば-e WORKERS_ENABLED=false -e SCHEDULER_ENABLED=false(billingが実際に読む名前で); - 決済、メール、SMS のプロバイダーにはテストモードのキーを使い、ParallelSandbox のシークレットとして保存して
-e STRIPE_SECRET_KEY=$STRIPE_TEST_SECRET_KEYのように渡します; - ボックスで
billingを公開したバージョンとして動かすなら、同じスイッチをそのenvに入れます:{ "name": "billing.acme.internal", "port": 8080, "version": "refund-a1b2c3d", "env": { "WORKERS_ENABLED": "false", "SCHEDULER_ENABLED": "false" } }(4 節); - ボックスのコピーに受け取らせたくない、あなたのネットワーク内のブローカー(RabbitMQ、Kafka、Redis のキュー)は、接続に登録しないでください。登録していないものにボックスは届きません。SQS は環境の AWS ロールでインターネット経由で使うので、サービス側でコンシューマーを止めるか、そのロールから
sqs:ReceiveMessageを外してください。
内部ロードバランサーの背後の変更サービス
続いてエージェントは admin を変更します。dev では呼び出し側(ここでは web のサーバー側)が内部ロードバランサー internal-acme-dev-admin-1234567890.ap-northeast-1.elb.amazonaws.com:80 経由で届き、そのコンテナは 8080 で待ち受けています。ボックスではロードバランサーの名前のままポート 80 で動かし、プロセスが実際に待ち受けるポートを targetPort にします:
sandbox_start {
"name": "ana:admin 監査ログの書き出し",
"goal": "admin に監査ログの書き出しを追加する。web の管理ページがロードバランサーの名前経由で書き出しをダウンロードできれば完了",
"environment": "dev",
"services": [
{ "name": "web", "port": 3000, "web": true },
{ "name": "internal-acme-dev-admin-1234567890.ap-northeast-1.elb.amazonaws.com", "port": 80, "targetPort": 8082, "web": true }
]
}
sandbox_sync { "id": "<id>", "localPath": "/home/dev/acme/platform", "dest": "platform" }
sandbox_exec { "id": "<id>", "cmd": "docker build -t admin ./admin && docker run -d --name admin --env-file /work/.sbx/env/admin.env -p 8082:8080 admin", "cwd": "platform", "timeoutSec": 900, "note": "変更した admin をビルドして 8082 で公開する" }
sandbox_exec { "id": "<id>", "cmd": "docker build -t web ./web && docker run -d --name web --env-file /work/.sbx/env/web.env -p 3000:3000 web", "cwd": "platform", "timeoutSec": 900, "note": "web フロントエンドを dev の設定でビルドして起動する" }
sandbox_exec { "id": "<id>", "cmd": "curl -fsS http://internal-acme-dev-admin-1234567890.ap-northeast-1.elb.amazonaws.com/health", "note": "admin がロードバランサーの名前のポート 80 で応答するか確認する" }
- ボックス内の呼び出し側は設定を変えずに
http://internal-acme-dev-admin-1234567890.ap-northeast-1.elb.amazonaws.com/(ポート 80)に接続し、8082 で公開したadminのコンテナに届きます。ポート 80 そのものは boxd のままです。targetPortがないとsandbox_startは 400 で失敗します(port 80 in the box is boxd's scene entry; add targetPort, the port your process listens on — callers keep dialing <name>:80)。 - この名前は環境のアドレス(
awsに登録)なので、sandbox_startで宣言するとボックスが引き取ります。environment.reachableから消え、ボックス内でこの名前への接続はすべてロードバランサーではなくボックスに届きます。ここのロードバランサーはadminだけを受け持っています。複数のサービスに振り分けるロードバランサーなら、その呼び出し側に必要なものもボックスで動かしてください。 - ボックスにはロードバランサーがありません。ボックスの呼び出し側はロードバランサーの名前で
adminのコンテナに直接届き、リクエストは送ったとおりに着きます。dev でロードバランサーがしていることはここでは起きません:認証アクション(ALB の OIDC や Cognito)、ホストやパスのルール、リダイレクト(HTTP から HTTPS へなど)、付け加えるヘッダー(X-Forwarded-For、X-Forwarded-Proto、X-Forwarded-Port、認証後のx-amzn-oidc-*)です。adminのボックス URL から来るリクエストには、ボックス自身が付けるX-Forwarded-For、X-Forwarded-Proto、X-Forwarded-Hostがあります(人が開くページ)が、X-Forwarded-Portとx-amzn-oidc-*はありません。ボックス内の小さなプロキシ(コンテナの nginx や Caddy。ロードバランサーの名前とポート 80 に専用のtargetPort付きで宣言し、8082 のadminに転送)なら、パスのルール、リダイレクト、X-Forwarded-*ヘッダーは再現できます。ログインは再現できません。x-amzn-oidc-dataは ALB が自分の鍵で署名するトークンで、AWS の指示どおり署名を検証するアプリは、プロキシが作ったものをすべて拒否します。adminが認証する ALB の後ろにあるなら、ボックスのコピーはアプリ自身の dev 用またはテスト用のログインモード(ローカルユーザー、テスト用の ID プロバイダー、設定にすでにある認証の切り替え)で動かしてください。--env-fileのあとの-eで、公開したバージョンならenvで設定します。 - 両方に
web: trueを付けると、それぞれが専用の URL を持ちます。webUrlは最初の web サービスであるwebのもの、services[]のadminのurlはhttps://<id>-<key>-8082.box.parallelsandbox.comの形で、8082 はそのtargetPortです。人はどちらも開けます。URL は返されたとおりに使ってください。
公開された dev エンドポイントと externalBaseUrl
ボックスはインターネットに直接つながります。https://dev.acme.example などの公開された dev エンドポイントは、プログラムからもコンテナからも設定なしで使えます。
externalBaseUrl は、宣言したがボックスでは動かさない名前のためのものです。name:port への各 HTTP リクエストが 1 つのベース URL に、パスはそのまま、Host を書き換えて転送されます。例:リポジトリの compose ファイルに gateway:8000 を呼ぶものがあり、それが dev では https://dev-gw.acme.example の場合、{ "name": "gateway", "port": 8000 } を宣言して "externalBaseUrl": "https://dev-gw.acme.example" を渡します。呼び出し側がポート 80 で接続する名前も同じように転送されますが、ポート 80 のサービスはどれも targetPort が必要です:{ "name": "gateway", "port": 80, "targetPort": 8000 }。externalBaseUrl はボックスに 1 つだけで、HTTP のみです。
externalBaseUrl がある場合(渡したもの、または環境に設定したもの)、宣言したサービスは web や api.acme.internal も含めてすべて external で始まります。ボックスで動かすものは、その targetPort でプロセスを起動し、名前を切り替えます。boxd はそのポートを占有しないので、順番は問いません:
sandbox_wire { "id": "<id>", "service": "api.acme.internal", "mode": "box" }
sandbox_wire { "id": "<id>", "service": "web", "mode": "box" }
切り替えで変わるのは転送だけです。開いている接続はそのまま残り、同じ targetPort を共有する名前は一緒に切り替わります。サービスが external の間は、ボックス上のプログラムが localhost:<port> に接続しても転送先に届きます。ただし boxd 自身の 80 と 9095 は除きます。
IP アドレスに直接つなぐ呼び出し側
名前を使わない呼び出し側もあります。billing が Cloud Map の API(DiscoverInstances)や Redis の一覧から api タスクの IP を得て、10.0.12.34:8080 に直接つなぐ場合です。そうした呼び出し側には、sandbox_start でプライベート範囲とポートを宣言します。範囲内のすべてのアドレスのそのポートが引き取られるので、できるだけ狭くしてください:
"services": [{ "name": "api.acme.internal", "port": 8080 }, { "name": "10.0.0.0/20", "port": 8080 }]
これでボックス内(コンテナも含む)から 10.0.0.0/20 のどのアドレスの 8080 への接続も、ボックス内のそのサービスの targetPort、ここでは 8080 に届きます(サービスが external の間は externalBaseUrl への転送)。範囲に入り同じポートの環境のアドレスは、コネクタを経由しなくなります。範囲は 10.0.0.0/8、172.16.0.0/12、192.168.0.0/16、100.64.0.0/10 の内側に限られ、あとから追加できません。Cloud Map の DNS 名を使う呼び出し側に必要なのは範囲ではなく名前です。
3. Sentry
チームの Sentry はそのまま使います。ParallelSandbox のログサービスは不要です。
DSN:取り込んだ設定には、たいてい
SENTRY_DSNが入っています(タスク定義の environment、またはそこから参照されるシークレット)。そのため/work/.sbx/env/api.envにあり、--env-fileでコンテナに届きます。設定にない場合は ParallelSandbox のシークレットとして保存して渡します:PUT /v1/secrets/SENTRY_DSN、起動時に"secrets": ["SENTRY_DSN"](あるいはあとからsandbox_secrets { "id": "<id>", "names": ["SENTRY_DSN"] })、そしてdocker run -e SENTRY_DSN ...。sentry.io:ボックスからインターネット経由で届きます。設定は不要です。
オフィスのセルフホスト Sentry:SDK は DSN のホストとポート、ここでは
sentry.office.lan:443にイベントを送ります。上のとおり、このhost:portをoffice接続に登録します。TLS はそのまま通るので、SDK は dev と同じようにサーバーの証明書を信頼している必要があります。自社の CA が発行した証明書なら、dev のコンテナと同じくコンテナにもその CA が必要です。オフィスにしかない Sentry にブラウザのエラーを送る:ブラウザの SDK はその人のスマホやノート PC から直接イベントを送るので、
sentry.office.lanには届きません。SDK のtunnelオプション(Sentry.initでtunnel: '/sentry-tunnel')を使うと、ページは各イベントをweb自身のオリジンのそのパスに送り、webのサーバーがボックスのoffice接続を通して転送します。転送側は Sentry のドキュメントのとおり、リクエスト本文の最初の行(エンベロープのヘッダー)を読んでdsnを取り出し、ホストがsentry.office.lanで、プロジェクトが自分たちのものか確かめてから、本文全体をhttps://sentry.office.lan/api/<プロジェクト id>/envelope/に POST します。@sentry/nextjsにはこれを自動で行うtunnelRouteオプションがありますが、Sentry のドキュメントではセルフホストの Sentry では動かないとされているので、このルートは自分で書きます。2 つ目のブラウザアプリadminも、自分のオリジンに自分用のルートを持って同じようにします。adminのページがその URL で届くのはadminのサーバーだけなので、adminが自分のイベントを転送します。ボックスから来たものに印を付ける。Sentry のサーバー SDK(Python、Node、Go、Java、.NET、PHP)は、コードが
Sentry.initに自分の値を渡していなければ、SENTRY_ENVIRONMENTとSENTRY_RELEASEを環境変数から読みます。そのため-e SENTRY_ENVIRONMENT=psbx -e SENTRY_RELEASE=<commit>(公開したバージョンなら、同じ名前をそのenvに)はコード変更なしで使えます。dev の環境で絞っているダッシュボードやアラートは汚れず、release でボックスがどのビルドを動かしたかがわかります。ボックスを区別するには-e SBX_BOX_IDを渡し、Sentry を初期化するコードでタグとして付けます。例:JavaScript ならinitialScope: { tags: { psbx_box: process.env.SBX_BOX_ID } }、Python ならsentry_sdk.set_tag("psbx_box", os.environ.get("SBX_BOX_ID"))。ブラウザのバンドルはenvironmentとreleaseをビルド時にSentry.initから受け取るので、ボックスでのビルド時に設定してください。エラーを読む:
logs_search、logs_errors、logs_tailが読むのは ParallelSandbox のログサービスだけで、Sentry は読みません。Sentry 自身の手段を、読み取り専用の認証トークンで使ってください。トークンはエージェントのマシンに置くか、エージェントがボックスから問い合わせるなら ParallelSandbox のシークレット(SENTRY_AUTH_TOKEN)にします。Sentry の API(例:curl -s -H "Authorization: Bearer $SENTRY_AUTH_TOKEN" "https://sentry.io/api/0/organizations/<org>/issues/?project=<project id>&environment=psbx&statsPeriod=24h&query=psbx_box:<box id>%20release:<commit>"。そのボックスがそのビルドで起こした issue が並びます。セルフホストなら自分の Sentry のホストで同じパス。ボックスからはoffice経由で届きます)、sentry-cli、エージェントにあれば Sentry の MCP サーバー。ボックスの URL で開いたブラウザのページでは、Sentry が記録するすべての URL(ページの URL、ブレッドクラム、スタックフレーム)にボックスの key が入ります。
@parallelsandbox/logと同じ方法でbeforeSendで取り除き、同じホストから取ったボックス id をタグにします:const m = /^([a-z0-9]{6,32})-([a-z0-9]{8,32})(?:-[0-9]{1,5})?\.box\.parallelsandbox\.com$/.exec(location.hostname); const scrub = (event) => (m ? JSON.parse(JSON.stringify(event).split(`${m[1]}-${m[2]}`).join(m[1])) : event); Sentry.init({ dsn: '<ブラウザ用の DSN>', environment: 'psbx', release: '<commit>', initialScope: m ? { tags: { psbx_box: m[1] } } : undefined, beforeSend: scrub, beforeSendTransaction: scrub, });<id>-<key>は英数字とハイフンだけなので、シリアライズしたイベントの中で置き換えても JSON は壊れません。webのhttps://<id>-<key>.box.parallelsandbox.com/cart(最初に宣言しているのでurlはsceneUrl)はhttps://<id>.box.parallelsandbox.com/cartとして、adminのhttps://<id>-<key>-8082.box.parallelsandbox.com/はhttps://<id>-8082.box.parallelsandbox.com/として記録されます。psbx_boxタグはサービス側が送るのと同じボックス id なので、1 回のクエリで両方が見つかります。ボックスでビルドしたブラウザ向けコードのソースマップ。Sentry がボックスのリリースで元のコードを表示できるようにします。これは Sentry の標準的な使い方で、ParallelSandbox 固有のものではありません。ブラウザ用のソースマップ付きで
webをボックス上でビルドし(Next.js ならproductionBrowserSourceMaps: true)、debug ID を注入し、Sentry.initと同じリリースでアップロードします。ソースマップをアップロードできる auth token をシークレットSENTRY_AUTH_TOKENとして保存し、起動時に渡すか("secrets": ["SENTRY_AUTH_TOKEN"])、あとからsandbox_secretsで入れます:cd /work/platform/web && . /work/.sbx/env/web.sh && npm ci && npx next build export SENTRY_ORG=<組織の slug> SENTRY_PROJECT=<ブラウザ用プロジェクトの slug> # セルフホストなら SENTRY_URL=https://sentry.office.lan/ も npx @sentry/cli sourcemaps inject .next/static npx @sentry/cli sourcemaps upload --release a1b2c3d .next/static注入はビルドしたファイルを配信する前に行い、そのあとこのビルドで
webを起動します。sentry.io へのアップロードはインターネット経由です。セルフホストの Sentry には、sentry.office.lan:443を登録したofficeを通して届きます。Sentry と並べて
@parallelsandbox/logを入れる価値があるのはブラウザのページです。エラーだけでなくコンソール出力もエージェントに読ませたい、logs_*でボックスごとに絞りたい、Sentry のトークンを渡したくない、という場合です。ブラウザ専用で、サービス側の Sentry の代わりにはなりません。ログ SDK を参照してください。
4. 変更した billing を別のボックスから使う
変更した billing はボックス 1(2 節)で動いています。別のボックス(ボックス 2。同じエージェントが起動したものでも、チームメイトのものでも)は、再ビルドせずに 2 つの方法でそれを使えます:
- おすすめ:ボックス 2 で billing の公開したバージョンを動かす。 ボックス 1 の
billingのイメージをビルドできたらすぐに公開し(下記)、ボックス 2 を最初からbilling.acme.internalにversionを付けて起動し、envで Sentry の印を付けてワーカーを止めます。ボックス 2 はボックス 1 が動いていてもいなくても自分のコピーを動かし、新しいビルドにはsandbox_wire … version1 回で切り替えられます。 - ボックス 1 へのリンク:ボックス 2 がボックス 1 の動いている
billing(それまでのデータや、つないだデバッガー)を必要とするときに使います。ボックス 1 が動いている間しか使えません。ボックス 1 がなくなる前なら、ボックス 2 はその名前を公開したバージョンにその場で切り替え、URL もそのままにできます(下記)。この機能が出た 2026-09-25 12:40 UTC より前に起動したボックス 2 では、新しいボックス 2 が必要で、URL も変わります。
ボックス 1 が動いている間:リンクする
sandbox_start {
"name": "ben:返金改修の billing を使う web",
"goal": "ボックス 1 にある ana の変更した billing を使って web の返金画面を作る。ブラウザで行った返金が新しい返金ルールに従えば完了",
"environment": "dev",
"services": [
{ "name": "web", "port": 3000, "web": true },
{ "name": "api.acme.internal", "port": 8080 },
{ "name": "billing.acme.internal", "port": 8080, "fromBox": "<ボックス 1>" }
]
}
<ボックス 1>はボックス 1 の id です。ボックス 1 を自分で起動していないエージェント(チームメイトのものなど)は、sandbox_listでボックスのname、goal、servicesから見つけます。- ボックス 2 の
billing.acme.internal:8080は、ボックス 1 で動くbillingに届きます。ボックス 2 のプログラムからもコンテナからも同じです。fromPortを省略したので、ボックス 1 のbilling.acme.internalのtargetPort、つまり 8081 になります。 billing.acme.internalは環境のアドレスでもあります。リンクが優先されるので、ボックス 2 は dev のものではなくボックス 1 のものに届き、この名前はボックス 2 のenvironment.reachableから外れます。- ボックス 2 は
webとapiを 2 節と同じように自分で動かし、それらはbilling.acme.internal:8080を呼び続けます。 - ボックス 2 が接続を開いている間、ボックス 1 は起きたままで、凍結していても次の接続で復帰します。ボックス 1 が止まると、ボックス 2 からの接続は 410(
… start a new box and re-point the link with sandbox_wire)で失敗し、ボックス 1 へのsandbox_stopはlinkedFromにボックス 2 を挙げます。名前を別のボックスに向けるには:sandbox_wire { "id": "<ボックス 2>", "service": "billing.acme.internal", "port": 8080, "fromBox": "<新しいボックス>" }。 - ボックス 1 がなくなる前に、ボックス 2 を billing の公開したバージョンにその場で切り替えます:
sandbox_wire { "id": "<ボックス 2>", "service": "billing.acme.internal", "port": 8080, "version": "refund-a1b2c3d", "env": { "SENTRY_ENVIRONMENT": "psbx", "WORKERS_ENABLED": "false" } }。sandbox_status→health.featuresにtake-overがあるボックス(2026-09-25 12:40 UTC 以降に起動したものはすべて)では、バージョンがその名前を引き取ります。アドレスはそのままで、ボックス 2 からボックス 1 への開いていた接続は閉じられてプログラムはバージョンに接続し直し、記録はリンクでなくなり、ボックス 1 のlinkedFromからボックス 2 が消え、ボックス 2 の URL は変わりません。古いボックス 2 では 409 になります(<name> is a link in this box, and box <id> runs boxd <version>, which cannot turn a link into a published version. Start a new box that declares the version, or wire the link again with fromBox or fromPort)。上のようにリンクを billing が動いているボックスに向け直すか、そのバージョンを宣言した新しいボックス 2 を起動してください(次の節)。 - ボックス 2 自身が凍結から復帰したときは、凍結前から開いていたボックス 1(と dev)への接続が閉じられます。そのプログラムは接続し直す必要があります。
- ボックス 2 が凍結または停止すると、ParallelSandbox はボックス 2 からボックス 1 への接続を 1 分ほどで閉じ、ボックス 1 はそのあと自分のアイドル時間で凍結します。ボックス 2 が起きていて接続を持っている間は、ボックス 1 も起きたままです。
- リンクの通信は ParallelSandbox を通り、送る側のボックスの外向き通信として数えます。ボックス 2 が送るリクエストはボックス 2 の、ボックス 1 が返す応答はボックス 1 の分です。
sandbox_statusにはボックス 2 のlinks[]とボックス 1 のlinkedFrom[]が出ます。詳しくは別のボックスへのリンクを参照してください。
ボックス 1 がなくなった後:公開したバージョンを動かす
リンクにはボックス 1 が動いている必要があります。ボックス 1 が止まった後もビルドを残すには、先にボックス 1 からイメージを公開します:
sandbox_status { "id": "<ボックス 1>" }
registry.imagePrefix が正確な接頭辞 <registry>/parallelsandbox/tenant-<アカウント>: です。アカウントのすべてのボックスはそのレジストリにログイン済みで、push も pull もでき、動いている間はログインが続きます。リポジトリはアカウントに 1 つなので、タグにサービス名を入れます:
sandbox_exec { "id": "<ボックス 1>", "cmd": "docker tag billing '<imagePrefix>billing-a1b2c3d' && docker push '<imagePrefix>billing-a1b2c3d'", "timeoutSec": 900, "note": "ほかのボックス用に billing のビルドを push する" }
sandbox_publish_version { "service": "billing", "label": "refund-a1b2c3d", "image": "<imagePrefix>billing-a1b2c3d", "gitSha": "a1b2c3d", "note": "返金ルール" }
image は imagePrefix で始まる必要があり(そうでなければ 400)、存在は確認されないので先に push してください。label はサービス内で一意です(重複すると 409)。push はボックスからの外向き通信なので課金されます。レジストリに有効期限のルールはなく、イメージはアカウントが削除されるまで残ります。同じタグをもう一度 push するとイメージが置き換わり、ボックスのレジストリログインでは push と pull はできても削除はできません。DELETE /v1/versions/{id} が消すのはバージョンの記録だけです。
ボックス 2 では、リンクの代わりに billing の dev の名前でこのバージョンを宣言します。pull と起動はボックスが行います:
sandbox_start {
"name": "ben:返金改修の billing を使う web",
"goal": "billing の公開された返金バージョンを使って web の返金画面を作る。ブラウザで行った返金が新しい返金ルールに従えば完了",
"environment": "dev",
"services": [
{ "name": "web", "port": 3000, "web": true },
{ "name": "api.acme.internal", "port": 8080 },
{ "name": "billing.acme.internal", "port": 8080, "version": "refund-a1b2c3d",
"env": { "SENTRY_ENVIRONMENT": "psbx", "SENTRY_RELEASE": "a1b2c3d", "WORKERS_ENABLED": "false", "SCHEDULER_ENABLED": "false" } }
]
}
sandbox_status { "id": "<ボックス 2>" }
sandbox_startはすぐに返ります。ボックス 2 の準備ができるとイメージを pull し、コンテナpsbx-svc-billing-acme-internalとして起動します。billing のservices[].run.stateがrunningになるまでsandbox_statusを呼んでください。failedにはエラーとログの最後が入ります。- ここのように
sandbox_startで宣言すれば、どのボックスでも使えます。billing.acme.internalは環境のアドレスです。動いているボックスでは、take-over機能があればversion付きのsandbox_wireもそれを引き取ります。古いボックスでは、バージョンは起動しますが、名前はコネクタに向いたままです。 - ボックス 2 を billing の新しいビルドに切り替えるには、新しいラベルで公開してから
sandbox_wire { "id": "<ボックス 2>", "service": "billing.acme.internal", "port": 8080, "version": "<新しいラベル>" }を呼びます。起動時にversion付きで宣言した名前は、環境のアドレスでもそうでなくても、こうして切り替えられます。コンテナは置き換わり、runはstartingからやり直し、呼び出し側はbilling.acme.internal:8080に接続し続けます。 - ボックス 2 の 8080 はすでに
apiが使っているので、コンテナは 20000(20000 から上で最初に空いているポート)で公開されます。呼び出し側はボックス 2 のプログラムからもコンテナからも、これまでどおりbilling.acme.internal:8080に接続します。コンテナの中では、イメージが EXPOSE しているポート、なければ 8080 で待ち受けます。 - このバージョンはサービス
billingとして公開されたので、コンテナは billing の dev の設定/work/.sbx/env/billing.env、そのあとにここで渡したenv(同じ名前ならenvの値)、そしてSBX_BOX_IDを受け取ります。その Sentry のイベントにはpsbxの印とリリースが付き(3 節)、ワーカーとスケジューラーは止まったままです(dev の設定によるほかの副作用)。envの値はsandbox_statusに出るので、秘密は入れないでください。 refund-a1b2c3dはラベルです。別のサービスにもrefund-a1b2c3dというラベルのバージョンがあれば、名前の最初のラベルbillingでこちらが選ばれます。それでも決まらなければ、sandbox_versionsのversionIdを渡してください。
ボックス 2 で billing.acme.internal を宣言しているから、その呼び出し側がこのイメージに届きます。REST では GET /v1/versions と DELETE /v1/versions/{id} でバージョンを管理します。規則は公開したバージョンを動かすにあります。
できないこと:
- 別のアカウントのボックスへのリンクや、アドレスや範囲によるリンク。リンクは同じアカウントのボックスを、ホスト名でつなぎます。
- 止まった後のボックス 1 に届くこと。リンクを動いているボックスに向け直すか、公開したバージョンを動かしてください。
- バージョンはイメージだけです。
/work、コンテナの状態、データベースの中身は引き継がれません。
5. トラブルシューティング
- ボックス 1 が凍結しない:別のボックスがリンクで接続しています(ボックス 1 の health に
servedLinksが出ます)。そのボックスが凍結または停止すると 1 分ほどでこれらの接続は閉じられます。ボックス 1 のsandbox_statusのlinkedFromに接続しているボックスが出ます。 sandbox_environments→connections[]:どの接続が止まっているか、いつから(lastSeenAt)か、コネクタのバージョン、その接続が運ぶアドレスがわかります。どれか 1 つの接続が接続中ならconnectorOnlineはtrueなので、それだけでは止まっているofficeはわかりません。最後の確認は、そのアドレスの 1 つに本物のクライアントで接続することです(前述の確かめ方)。sandbox_statusのhealth.privateEndpoints[]:アドレスごとのhost、port、localIpとlocalPort(boxd がボックス内でそのアドレス用に開く入口。アドレス自身のポートではない)、active、opened、failed、lastError。/work/.sbx/logs/private-endpoints.log:接続に失敗するたびに理由付きで 1 行。403:そのアドレスが環境のどの接続にも登録されていません。登録し直してください。ボックスの起動後に追加したアドレスは、そのボックスには知られていません。新しいボックスを起動してください。503:そのアドレスを登録している接続のコネクタがオフラインで、メッセージにその接続の名前が入ります。オフィスのホストではdocker logs parallelsandbox-connector、AWS ではロググループ/ecs/parallelsandbox-connectorを見てください。ボックス内のクライアントには、接続がいったん受け付けられてからリセットされたように見えます。502:コネクタは接続中ですが接続先に届きません。コネクタとそのアドレスの間の DNS、ルート、セキュリティグループを確認してください。- 宣言した名前がまだ dev に届く:環境のアドレスである名前を、起動後に
sandbox_wireで追加しています。sandbox_startで宣言してください。 - 接続がタイムアウトし、ログに何もない:どの接続にも登録されていないプライベート IP です。
Could not resolve host:宣言しておらず環境のアドレスでもない名前です。すぐに拒否される:宣言した名前か環境のホストに誰も宣言していないポートで接続し、ボックス内でもそのポートを待ち受けるものがありません。 - リンクが失敗する:
sandbox_status.links[].lastErrorとprivate-endpoints.logに理由があります。410:相手のボックスが止まっています。sandbox_wireでリンクを向け直してください。409:相手がまだ起動中か、リンクが出る前のイメージで動いています。502nothing listens on port …:相手のボックスのプロセスがfromPortで待ち受けていません。 aws ecr get-login-passwordがAccessDeniedExceptionで失敗する:環境の AWS ロールに ECR から pull する権限がありません(dev の変更していない呼び出し側をボックスで動かす)。- ボックスが復帰した直後に接続エラーが出る:boxd は、凍結前から開いていた環境のアドレスやリンクへの接続を、復帰後 15 秒ほどのうちに閉じます。プログラムは、いつまでも応答しない接続を握ったままにならず、エラーを受け取ります。データベースの接続プールはたいてい自分で接続し直します。長く接続を保つクライアント(メッセージのコンシューマー、WebSocket や gRPC のストリーム)は再試行が必要です。
- 502
nothing listens on <name>:80 in this box: that name is declared on another port. …:呼び出し側がその名前にポート 80 で接続していますが、名前は別のポートで宣言(または登録)されています。宣言したポートに接続するか、targetPortを付けて名前をポート 80 で宣言してください。2026-09-24 20:30 UTC より前に起動したボックスでは、同じリクエストが最初に宣言したサービスに届きます。 - ボックス内のサービスに名前で届かない:
0.0.0.0のtargetPortで待ち受け、コンテナならそのポートを公開してください(-p <targetPort>:<コンテナ内のポート>)。sandbox_status.wiring.services[]に各名前のport、targetPort、address、modeがあります。externalBaseUrlがある場合はboxになっているか確認してください。 sandbox_startがport 80 in the box is boxd's scene entry; add targetPort, the port your process listens on — callers keep dialing <name>:80で失敗する:そのサービスにtargetPortを付けてください。sandbox_wireがボックスの boxd はtargetportに対応していないと言う:そのボックスはtargetPortが出る前に起動したものです。新しいボックスを起動してください。- ボックスの URL が 404 になる:key が違うか、
-<port>の先にwebのサービスがありません。URL はsandbox_statusが返すものをそのまま使ってください。 - 人のブラウザにボックスではなく dev のデータが出る:フロントエンドが絶対 URL で API を呼んでいます(人が開くページ)。
この構成でできないこと
- あなたのネットワークからボックスに届くこと。dev のサービスはボックス内のものを呼べず、見えるのはボックス内のプログラムだけです。
- 別のアカウントのボックスへのリンクや、止まったボックスのサービスを使い続けること。リンクは同じアカウントの動いているボックスどうしをつなぎ、相手が止まると 410 で失敗します。
- 接続のアドレスをワイルドカードや範囲で指定すること。
host:portはすべて正確に登録し、1 つの接続に最大 100、1 つのボックスに最大 200 です。 - 宣言した名前ごとに別の外部 URL に送ること。
externalBaseUrlはボックスに 1 つだけで、転送するのは HTTP だけです。 - ボックスでポート 80 や 9095 で待ち受けること。boxd が使っています。それでも呼び出し側はポート 80 で名前に接続でき、プロセスの
targetPortに届きます。 webがなく、最初に宣言したサービスでもないサービスを、ボックスの外から開くこと。logs_*で Sentry を読むこと。これらのツールが読むのは ParallelSandbox のログサービスだけです。