每个应用都是其项目中的一个服务。除了应用具备的一切,服务还可以:
- 一起部署时等待其他服务:
depends_on,每项为started、ready或completed(--depends-on db:ready); - 在项目网络上以其他名称被找到(
network.aliases、--alias),这些名称解析为服务的地址,或解析为每个实例的地址(network.endpoint_mode:vip或dnsrr); - 不暴露到互联网 (
public: false)。
私有网络
每个项目都有自己的网络,位于 10.64.0.0/10 内,按项目的需要以每段 64 个地址分配。每个服务都会得到一个不变的地址,即使重命名也不变,网络上的每个沙箱也会得到一个地址。地址在每台机器上都相同:无论项目的沙箱和实例在哪里运行,这个网络都能通过平台机器之间自己的网络到达它们。项目中的沙箱或服务通过 <service>.internal 或地址访问一个服务。连接网络上休眠的实例就会唤醒它。平台变量会告诉服务它自己的信息:SBX_SERVICE、SBX_PRIVATE_DOMAIN、SBX_PRIVATE_ADDRESS、SBX_PROJECT。
到服务地址的新连接,由实例所在的机器分配给它的各个实例。项目之间无法访问彼此的网络。
共享变量与引用
共享变量是项目中的服务都可以使用的一对名称和值(glac variables put REGION eu、/v1/variables):每个项目最多 1,000 个,每个值最大 32 KiB。与机密不同,它的值可以读回。
服务的环境变量可以引用共享变量 ${{ shared.REGION }},或另一个服务的变量 ${{ db.POSTGRES_USER }}。创建版本时,引用会被替换成值:版本保存这个值,应用保存引用。对另一个服务的机密的引用必须是整个值 ("${{ db.POSTGRES_PASSWORD }}"):机密按名称传递,从不写进版本。
修改共享变量会重新部署版本会因此改变的服务(项目部署,cause 为 variables):PUT /v1/variables/{name} 返回这次部署的 id (deploy),没有版本改变时返回 null。如果这次部署无法进行(比如它的服务会互相循环等待),修改会被拒绝,返回部署自己的错误码,值也不会写入:错误信息会说明变量没有被修改,列出引用它的服务,并在其中相应的位置说明它们的部署为何无法进行 (services.api.env.DATABASE_URL)。
部署项目
glac deploys create (POST /v1/deploys) 把项目中的多个服务一起部署。每个被指定的服务都会得到单独部署它时会得到的东西,替换它现有的(api=IMAGE:新镜像;只写 web:按原样),环境变量引用了它们的服务也会一起部署:
glac deploys create api=ghcr.io/acme/api:2 web
glac deploys ls-f FILE 用 JSON 给出服务,每个服务带有版本的字段 ({"api": {"image": "…", "env": {…}}})。部署按各服务所等待的对象排序(它的 depends_on,以及它的环境变量引用的服务):
- 有状态服务最先:替换各自唯一的实例,见下文。
- 任务 (
kind: job) 其次:每个任务在项目私有网络上的一个沙箱中运行一次,直到结束。退出码 0 表示completed;其他退出码都会让部署失败(job_failed,附带任务的最后几行输出)。沙箱创建 30 分钟后仍未结束的任务会让部署失败 (job_timed_out),它的沙箱会被删除;在看到任务结束之前沙箱就没了,也会让部署失败 (job_lost)。一次部署中任务从不运行两次。 - 其余服务:它们的版本并行构建,每个都在它等待的对象完成后开始,然后一起承接流量。
失败的部署停在失败的地方:之前已切换的保持切换,其他所有服务保留各自的版本,部署会说明哪个服务失败以及原因(project.deploy_failed 事件)。一个项目同一时间只运行一次部署:在此期间到来的部署会等待 (queued),更晚到来的部署会取代正在等待的那个 (replaced),把对方的服务和自己的一起部署。如果控制平面重启,部署会从上次停下的地方继续:已结束的任务不会再运行,它的 30 分钟仍从开始时算起。
glac deploys create 会等待部署,在 stderr 上报告每个服务的进展,失败时以 1 退出;--no-wait 立即返回,之后可以用 glac deploys get ID --wait 等待。部署 (GET /v1/deploys/{id}) 包含它的 status(queued、deploying、deployed、failed 或 replaced)、cause(api;variables,共享变量被修改;compose,导入)、每个服务的状态、版本和它等待的对象,以及每一步用了多久 (steps)。SDK 有同样的调用(Python 和 TypeScript 中为 glac.deploys),MCP 服务器有 project_deploy 和 project_deploys。
保存数据的服务
有自己云盘的服务是有状态服务。它只有一个实例,一直保留到你删除它为止,运行在持有云盘的机器上,使用服务的地址:
glac drives create --name pgdata --size-bytes 5GiB --region eu
openssl rand -hex 16 | glac secrets put PG_PASSWORD -
glac deploy postgres:16 --name db --port 5432 --region eu --secret POSTGRES_PASSWORD=PG_PASSWORD \
-e PGDATA=/var/lib/postgresql/data/pgdata --drive pgdata:/var/lib/postgresql/data --public false(PGDATA 把数据库放在云盘的一个目录中:新文件系统的顶层有 lost+found,PostgreSQL 不会在这样的目录中启动。)
- 只有一个写入方:这样的服务只有一个实例。
--max-instances不为 1,或使用键,都会被拒绝 (409drive_single_writer)。 - 只有一个区域:云盘只能在它自己的区域中挂载,而实例创建在应用的第一个区域中。所以要像上面那样,把云盘的区域设为服务的区域:第一个区域是其他区域的版本会失败 (409
not_on_one_machine)。 - 只能读写:版本目前还不能以只读方式挂载云盘的快照(
mode: "snapshot"会被拒绝,400invalid_field)。只读挂载只供沙箱使用。 - 新版本会先为服务的云盘创建一个备份 (
reason: before_version),然后停止旧实例,用同一个云盘、地址和名称启动新实例。在此期间到达的连接会等待,最多 10 秒,而不会被拒绝。如果新版本没能起来,就在同一个云盘上重新启动之前的版本。 - 何时算就绪。按镜像自己的
HEALTHCHECK判断;没有时,对于平台认识的数据库镜像,用该数据库自己的命令判断(PostgreSQL 用pg_isready,MySQL 和 MariaDB 用mysqladmin ping,Redis、Valkey 和 KeyDB 用redis-cli ping,MongoDB 用mongoshping);否则在它的端口接受连接时算就绪。每种方式都先等服务器在环回以外的地址上监听,这样数据库在初始化期间运行的临时服务器不算数。因此第一个查询不会碰上仍在启动的数据库(PostgreSQL 的57P03)。 - 从云盘重新启动时(重启,或它的机器恢复),它会把发给它的连接暂存到就绪为止,最多 150 秒,而不是拒绝它们;120 秒后检查仍未通过的,照样放行,它的日志会说明这一点。
- 如果它的服务器自行停止(崩溃,或被内核的 OOM killer 或
kill结束),在上一次运行的一切都清理完之后,它会在同一个实例中重新启动,云盘保持原样:0.1 秒后启动;每次在启动后 10 秒内再次停止,等待时间就加倍,最多一分钟(与 Docker 的restart: always数值相同)。每次启动都是一个instance.restarted事件,附带上一次运行是如何结束的(exit_status、signal);实例的start_command(GET /v1/sandboxes/{id}) 说明它的服务器如何运行(running,或restarting及时间)、重新启动了多少次、上一次是如何结束的。平台要求的停止(删除、重启、新版本)不属于这些情况。普通(无状态)服务的实例在服务器停止时,会被另一个实例替换。 - 它和沙箱一样,根据内部的活动决定是否休眠,但它自己的定时器(数据库的检查点)不会让它保持运行。连接它就会唤醒它。
- 它不在应用的日志中:用
glac sandboxes logs <instance> exe-start读取它的输出。
数据库就是这样一个服务及其云盘,只能通过 /v1/databases 创建和修改:它的应用会在 managed_by 中说明这一点 ({"object": "database", "id": "db-…"}),在这里对它更新、部署、回滚或删除都会被拒绝 (409 managed_by_database)。它的日志和指标与任何应用一样读取。
导入 Compose 文件
glac compose up # compose.yaml in this directory, or the one above文件中的每个服务都成为项目中的一个同名应用。文件中的每一项都会报告为 done、adapted(以另一种方式完成,并说明原因)、ignored 或 refused(并说明如何修改);只要有一项被拒绝,就什么都不写入。文件中的机密会先放进项目;存储卷会被创建。然后服务作为一次项目部署一起部署(cause 为 compose),glac compose up 会等它完成。再次导入同一个文件不会改变任何东西。
- 查找文件的方式与 Docker Compose 相同:
-f(-f -从标准输入读取),否则用环境变量中的COMPOSE_FILE,否则用.env文件中的(--env-file,或--project-directory中或当前目录下的.env;用COMPOSE_PATH_SEPARATOR分隔),否则用当前目录或上级目录中的compose.yaml(或docker-compose.yml等)。 --dry-run只打印报告;--strict拒绝会被忽略的内容。- 运行时设置。
ulimits、sysctls、tmpfs、dns和shm_size会成为服务的运行时设置,应用到它的每个实例中;dns会报告为adapted,因为在项目网络上,由平台代替这些服务器回答它的查询。 - 被忽略的内容会说明。平台不应用的设置(
cap_add、security_opt、logging、pull_policy等)会报告为ignored,并各自说明原因,glac compose up会在最后一行再把它们全部列出来。network_mode: none为adapted:服务的出站规则拒绝一切出站流量,它仍留在项目网络上。 - 如果这台机器上没有 Docker Compose,
glac会自己读取这些文件,读法与 Docker Compose v5.6.1 相同(--normalise local要求使用这种读法)。 - 镜像声明的卷。镜像声明为卷的路径(
VOLUME,比如 WordPress 的/var/www/html或 Redis 的/data),如果服务没有在那里挂载存储卷或云盘,它就是每个实例自己磁盘上的一个目录,新版本不会保留它。版本的resolved.unmounted_volumes列出这些路径,glac compose up和glac deploy会对每一个发出警告。 - 项目中有而文件中已经没有的服务会被保留,并在报告中列出;
--remove-orphans会在部署完成后,删除同一个 Compose 项目之前的导入为这类服务创建的应用。 glac compose ps列出项目的服务及各自的状态,glac compose logs把它们的日志一起打印出来,每行带有所属的服务(-f持续跟随),glac compose restart把每个服务按原样重新部署,让它的实例重新启动(有状态服务会先备份它的云盘,再在云盘上重新启动)。--build用这台机器上的 Docker Compose 构建带有build:的服务,把每个服务推送到其image:指定的镜像仓库,然后导入:由此产生的版本按摘要 (digest) 引用镜像。如果服务的image:指定的标签和之前相同,它就不会改变,也不会得到新版本:请给每次构建一个独立的标签。glac compose down删除导入创建的应用。存储卷和机密会保留。- 不能这样导入的文件(有构建但没有镜像、宿主机路径、
network_mode: service:…)可以用 Docker 在项目的一个沙箱中整体运行:glac compose up --one-sandbox。这时它的服务作为一个沙箱,一起唤醒、休眠和计费。