Kibotos

用容器运行

ctrl、E2B 网关与 Exposure 网关每个版本都另外发布容器镜像,可以用 Kubernetes 或 Docker 运行。node 仍然装在宿主机上,见安装 node。运维容器平台时可以用这些镜像。

本页只写与宿主安装不同的部分。配置项的含义见 ctrl 配置、安装 ctrl,以及安装 E2B 网关与安装 Exposure 网关里的变量表,升级的判据见重装与升级。示例清单在 deploy/kubernetes/ 与 deploy/compose/。

镜像

镜像默认命令运行账户(uid:gid)端口持久卷挂载点
ghcr.io/felinics/kibotos-ctrlkibotos-ctrl serve10001:1000118092(API)、18101(node API)、18192(指标)/var/lib/kibotos,状态目录是其中的 ctrl/
ghcr.io/felinics/kibotos-e2bkibotos-e2b10002:1000218095(API)、18195(指标)/var/lib/kibotos,身份目录默认 e2b/identity
ghcr.io/felinics/kibotos-gwkibotos-gw10003:1000318097(tunnel 入口)、18197(指标)/var/lib/kibotos,身份目录默认 gw/identity
  • 指标端口只有配置了指标监听时才打开,见集群与服务指标。
  • E2B 网关还在容器内的 127.0.0.1:18096 上打开一个只服务 envd 协议的监听。示例不发布它:18095 端口已经服务 envd 请求。
  • tag 与发布版本相同,例如 v1.2.3、v1.2.3-rc.1。没有 latest 这类会移动的 tag。
  • 镜像里的二进制取自同版本的 deb,两者逐字节相同。
  • 每个版本的附件 kibotos-images_<版本>.txt 按 digest 列出这三个镜像,并被 SHA256SUMS 覆盖。部署时按 digest 引用,与下载的发布包互相印证。

用下载发布包得到的目录查看这个版本的镜像,并核对镜像报告的版本:

cat kibotos-releases/v1.2.3/kibotos-images_1.2.3.txt
docker run --rm ghcr.io/felinics/kibotos-ctrl:v1.2.3 version --json

源码仓库尚未公开,拉取镜像同样要有仓库读取权限:用一个带 read:packages 权限的 GitHub token 登录 ghcr.io,Kubernetes 里把它做成 imagePullSecret。

与宿主安装的不同

状态放在持久卷上

  • ctrl 的 PKI 与网关的身份都在持久卷里。 卷挂在 /var/lib/kibotos,服务自己在其中建 0700 的子目录。
  • ctrl 的卷丢失,就丢失了这个部署的身份:全部 node 与 Operator 都要重新注册。要像备份数据库一样备份它。
  • 不能把 Secret 或 ConfigMap 目录挂成状态目录或身份目录。 它们由符号链接组成、带组权限,服务会拒绝加载。
  • Kubernetes 设 fsGroup 与 fsGroupChangePolicy: OnRootMismatch。 fsGroup 让服务能在卷根下建自己的目录。改变策略的默认值 Always 会让 kubelet 每次挂载都给卷里的文件加上组读写,ctrl 下次启动就会因为私钥权限过宽而拒绝启动。
  • Docker 命名卷第一次挂载时复制镜像里的目录,属主已经正确。改用宿主目录时,先用 install -d -o 10001 -g 10001 -m 0700 这类命令建好服务的子目录。

配置

  • ctrl 的配置可以整目录挂载。 Kubernetes 把 Secret 或 ConfigMap 挂到 /etc/kibotos/ctrl,Docker 绑定挂载一个目录。config.toml、dsn_file 与指标签名私钥文件可以是符号链接,最终指向的必须是普通文件,并且服务账户能读取。
  • 网关的配置只来自环境变量,变量名与安装包的 config.env 相同,完整列表见 e2b.example.env 与 gw.example.env。监听地址默认是回环地址,容器里改成 0.0.0.0。
  • 缺少必填项或取值非法时,网关一次列出全部有问题的变量后退出,不输出取值。
  • 指标监听默认关闭。 ctrl 在 config.toml 里写 [telemetry],两个网关设 KIBOTOS_E2B_TELEMETRY_LISTEN 与 KIBOTOS_GW_TELEMETRY_LISTEN,容器里同样绑 0.0.0.0。示例清单已经打开两个网关的监听;采集方式见集群与服务指标。

ctrl 只能运行一个

  • 同一时刻只运行一个 ctrl。 Kubernetes 用 replicas: 1 与 strategy: Recreate,不要用 RollingUpdate,也不要调高副本数。Docker 只起一个容器。
  • 第二个 ctrl 连上同一个数据库时只回答读取,不会和第一个同时写入,见执行权。但两个进程不能共用同一份 storage.root。
  • ReadWriteOnce 卷让调度到其他宿主的第二个 Pod 挂不上卷,但它不能代替 Recreate。
  • 不要在每次启动时自动执行 init,例如把它放进 initContainer。卷挂错或丢失时,serve 应当加载失败;init 却会生成一套新的部署身份。
  • 承载 ctrl 的宿主失联时,旧 Pod 停在 Terminating,卷不释放,新 Pod 起不来,这段时间没有可用的 ctrl。只有确认旧宿主已经停机后,才能强制删除旧 Pod。

入口与主机名

  • ctrl 的两个端口前面只能是 L4 透传。 两者都是端到端 mTLS,Kubernetes 用 LoadBalancer 或 NodePort Service,不能用 Ingress。
  • [identity].host 解析到这个入口。 node 的 [ctrl].endpoint、kibo 与网关都用这个名字连 ctrl,因为 ctrl 的证书只签这一个名字。
    • 网关在集群里不能用 Service 的集群内名字连 ctrl。集群 DNS 解析不到 [identity].host 时,Kubernetes 用 hostAliases 把它指向 ctrl Service 的地址;Compose 示例给 ctrl 加了同名的网络别名。
  • artifact store 的 endpoint 写 node 与 Operator 工作机都能直连的地址。 ctrl 签发的预签名 URL 用的是同一个 endpoint,写集群内地址会让 node 下载失败。
  • 网关要能解析并连到 node 的数据面主机名,即 <node_id>.<host_suffix>。
  • 承载 ctrl 的宿主要开启时间同步。 容器用宿主的时钟,要求见时间同步。

网关:单实例,首次启动自行登记

两个网关都是无状态的,但每个实例需要自己的 Operator 身份。几个实例共用一份身份目录时,它们各自续期;ctrl 只接受一个身份当前与上一张证书,第三个实例会被拒绝。示例清单每个网关只提供一份身份,所以每个网关只运行一个实例。更多实例与宿主安装一样,每个都需要自己的身份,见身份的使用规则。

  • 网关的身份目录放在持久卷上。卷是空的时,网关用 KIBOTOS_E2B_ENROLLMENT_KEY_FILE 或 KIBOTOS_GW_ENROLLMENT_KEY_FILE 指向的一次性注册密钥登记,然后才开始服务。
  • 已经登记过的网关不再读取这个文件,所以用过的密钥可以留在 Secret 里。
  • 密钥默认 1 小时内有效,最长 24 小时(--ttl),签发后在有效期内启动网关。
  • Kubernetes 挂载的 Secret 文件带组读权限,网关登记时会记一条「权限过宽」的警告,可以忽略。
  • 网关的卷丢失后,它启动时登记失败、不开始服务。为同一个 principal 签发新密钥,更新 Secret 后重启网关。
  • Kubernetes 示例用 strategy: Recreate:旧 Pod 停下之后新 Pod 才启动,同一份身份不会被两个进程同时使用。

Kubernetes

示例清单假设命名空间是 kibotos,用到下面几个文件。照实际情况改镜像版本、主机名、域名与存储大小后再应用。

文件内容
ctrl-state.yamlctrl 的持久卷
ctrl-command.yaml在 ctrl 卷上执行一次性命令的 Job,COMMAND 替换成命令名
ctrl.yamlctrl 的 Deployment、LoadBalancer Service 与 NetworkPolicy
e2b.yamlE2B 网关的持久卷、单副本 Deployment、Service、NetworkPolicy 与 Ingress
gw.yamlExposure 网关的持久卷、单副本 Deployment、Service 与 NetworkPolicy,以及 cloudflared

三个清单的 NetworkPolicy 只允许指标采集器连 telemetry 端口。采集器的清单是 deploy/metrics/kubernetes-vmagent.yaml,见集群与服务指标。

vmagent 写入的存储可以装在同一个集群里,用 Helm chart deploy/helm/kibotos-metrics/,见集群里的 VictoriaMetrics。

实例指标存储可以装在同一个集群里,用 Helm chart deploy/helm/kibotos-instance-metrics/,见Kubernetes 上的参考部署。

1. 准备命名空间、拉取凭证与 ctrl 配置

config.toml 按安装与初始化 ctrl填写,[storage].root 保持默认的 /var/lib/kibotos/ctrl。它含数据库与存储的凭证,放进 Secret;Secret 整个挂到 /etc/kibotos/ctrl,配置里用到的 dsn_file、指标签名私钥与 CA 文件也放进同一个 Secret,路径写成 /etc/kibotos/ctrl/<文件名>。要导出 ctrl 自己的指标时,配置里加上 [telemetry] 段,listen 写 0.0.0.0:18192。启用实例指标时多加一个 --from-file=metrics-signing-key.pem=...,生成方法见写入凭证:

set -eu
kubectl create namespace kibotos
kubectl -n kibotos create secret docker-registry ghcr \
  --docker-server=ghcr.io --docker-username=deploy-bot --docker-password="$GHCR_TOKEN"
kubectl -n kibotos patch serviceaccount default -p '{"imagePullSecrets":[{"name":"ghcr"}]}'
kubectl -n kibotos create secret generic kibotos-ctrl-config --from-file=config.toml=./config.toml

bucket 与权限的准备与宿主安装相同,见准备 bucket 与权限。

2. 初始化 ctrl

先建卷,再在卷上依次执行 init 与 migrate。每个 Job 完成后看一眼日志再删除:

set -eu
kubectl apply -f deploy/kubernetes/ctrl-state.yaml
for command in init migrate; do
  sed "s/COMMAND/$command/g" deploy/kubernetes/ctrl-command.yaml | kubectl apply -f -
  kubectl -n kibotos wait --for=condition=complete --timeout=10m "job/kibotos-ctrl-$command"
  kubectl -n kibotos logs "job/kibotos-ctrl-$command"
  kubectl -n kibotos delete job "kibotos-ctrl-$command"
done

init 已经初始化过时输出 unchanged,不改任何文件。

3. 启动 ctrl 并签发第一个 Operator 密钥

set -eu
kubectl apply -f deploy/kubernetes/ctrl.yaml
kubectl -n kibotos rollout status deploy/kibotos-ctrl --timeout=5m
kubectl -n kibotos get service kibotos-ctrl

把 [identity].host 的 DNS 记录指向 Service 的外部地址。然后在 ctrl 容器里签发密钥,取回本机后删除容器里的那份:

set -eu
umask 077
kubectl -n kibotos exec deploy/kibotos-ctrl -- \
  kibotos-ctrl enrollment create --operator operator-a --output /var/lib/kibotos/ctrl/operator-a.key
kubectl -n kibotos exec deploy/kibotos-ctrl -- cat /var/lib/kibotos/ctrl/operator-a.key > operator-a.key
kubectl -n kibotos exec deploy/kibotos-ctrl -- rm /var/lib/kibotos/ctrl/operator-a.key

之后按身份与注册用这把密钥注册 kibo,再按安装 node接入 node。

4. 启动 E2B 网关

先为网关签发一把注册密钥。不敏感的变量写在 e2b.yaml 的 ConfigMap 里;API key、token 密钥与命名空间放进 Secret,e2b-secret.env 里只写这三个变量:

set -eu
umask 077
kibo enrollment create --operator kibotos-e2b --ttl 24h --output kibotos-e2b.key
kubectl -n kibotos create secret generic kibotos-e2b-enrollment --from-file=enrollment.key=kibotos-e2b.key
kubectl -n kibotos create secret generic kibotos-e2b --from-env-file=e2b-secret.env
kubectl -n kibotos create secret tls kibotos-e2b-tls --cert=sandbox.crt --key=sandbox.key
kubectl apply -f deploy/kubernetes/e2b.yaml
kubectl -n kibotos rollout status deploy/kibotos-e2b --timeout=10m
rm kibotos-e2b.key

Ingress 的要求与宿主上的反向代理相同,见配置反向代理:证书覆盖 api.<domain> 与 *.<domain>,保留原始 Host,允许 WebSocket,关闭请求与响应缓冲。用 ingress-nginx 时,加上 nginx.ingress.kubernetes.io/proxy-buffering: "off" 与 nginx.ingress.kubernetes.io/proxy-request-buffering: "off" 两个注解。

5. 启动 Exposure 网关

ctrl 的 config.toml 要先填好 [exposure] zone。tunnel 的凭证文件由 provider 创建,放进 Secret:

set -eu
umask 077
kibo enrollment create --operator kibotos-gw --ttl 24h --output kibotos-gw.key
kubectl -n kibotos create secret generic kibotos-gw-enrollment --from-file=enrollment.key=kibotos-gw.key
kubectl -n kibotos create secret generic kibotos-cloudflared --from-file=credentials.json=./tunnel-credentials.json
kubectl apply -f deploy/kubernetes/gw.yaml
kubectl -n kibotos rollout status deploy/kibotos-gw --timeout=10m
rm kibotos-gw.key
  • cloudflared 的几个副本都运行同一条 named tunnel,把整条 zone 发给网关的 Service。tunnel 的配置见配置 tunnel。
  • 网关与 tunnel 之间是明文 HTTP。示例的 NetworkPolicy 只允许 cloudflared 的 Pod 连网关的服务端口,指标端口只允许指标采集器连接,集群要启用 NetworkPolicy。

Docker Compose

compose.yaml 在一台 Docker 宿主上运行 ctrl、E2B 网关,以及 Exposure 网关与 cloudflared。它读取同目录下的这些文件:

文件内容属主
ctrl/config.tomlctrl 配置。目录整个挂到 /etc/kibotos/ctrl,配置里用到的其他文件也放在这里目录与文件都是 10001:10001,目录 0700、文件 0600
e2b/e2b.envE2B 网关的环境变量,从 e2b.example.env 复制后填写只给 Docker 读取,0600
e2b/enrollment.keyE2B 网关的注册密钥10002:10002,0600
gw/gw.envExposure 网关的环境变量,从 gw.example.env 复制后填写只给 Docker 读取,0600
gw/enrollment.keyExposure 网关的注册密钥10003:10003,0600
gw/cloudflared.yaml、gw/cloudflared-credentials.jsontunnel 配置与凭证65532:65532,0600
  • 两个环境文件里的 CTRL_ENDPOINT 都写 https://<[identity].host>:18092:Compose 网络里 ctrl 带着这个名字的别名,示例里是 ctrl.kibotos.example,改成实际的名字。
  • gw/cloudflared.yaml 的 ingress 指向 http://gw:18097,其余与配置 tunnel相同;credentials-file 写 /etc/cloudflared/credentials.json。
  • 不提供 Exposure 时,删掉 gw 与 cloudflared 两个服务。
  • 两个网关的指标监听已经打开,只在 Compose 网络里可达;ctrl 的在 ctrl/config.toml 里写 [telemetry]。采集方式见集群与服务指标。

以 ctrl 的配置为例,把目录与文件交给服务账户:

set -eu
sudo install -d -o 10001 -g 10001 -m 0700 deploy/compose/ctrl
sudo install -o 10001 -g 10001 -m 0600 config.toml deploy/compose/ctrl/config.toml

初始化 ctrl、启动并签发第一个 Operator 密钥:

set -eu
cd deploy/compose
docker compose run --rm ctrl init
docker compose run --rm ctrl migrate
docker compose up -d ctrl
docker compose exec ctrl kibotos-ctrl enrollment create --operator operator-a --output /var/lib/kibotos/ctrl/operator-a.key
(umask 077; docker compose exec ctrl cat /var/lib/kibotos/ctrl/operator-a.key > operator-a.key)
docker compose exec ctrl rm /var/lib/kibotos/ctrl/operator-a.key

用这把密钥注册 kibo 后,为网关签发 key、放进上表的位置,再启动其余服务:

set -eu
cd deploy/compose
docker compose up -d
docker compose ps
docker compose logs e2b gw

E2B 网关发布在 127.0.0.1:18095,宿主上的 HTTPS 反向代理转发到这里。

一次性命令

宿主安装里以服务账户执行的 kibotos-ctrl 命令,在容器部署里这样执行:

命令什么时候KubernetesDocker Compose
init首次部署,只执行一次ctrl-command.yaml 的 Jobdocker compose run --rm ctrl init
migratectrl 停止后,用目标版本的镜像同上docker compose run --rm ctrl migrate
check-upgrade停机前预检,以及停止后的正式检查同上,用目标版本的镜像docker compose run --rm ctrl check-upgrade
enrollment createctrl 运行时kubectl exec 进 ctrl 的 Poddocker compose exec ctrl kibotos-ctrl enrollment create
catalog applyctrl 运行时同上同上
  • 一次性命令都以镜像里的服务账户运行,不要加 --user 0 或改用 root:root 建出的文件服务账户读不了。
  • ctrl 运行时的 check-upgrade Job 要和 ctrl 落在同一台宿主上才能挂载卷,ctrl-command.yaml 为此设了 Pod 亲和;调度到别处时 Job 停在 ContainerCreating。

升级

判据与步骤与宿主部署相同,见同一 MINOR 内逐台升级与跨版本升级。

ctrl

ctrl 只能先停后起,替换期间新的 Workspace 操作与新的数据面会话要等它恢复,已经建立的数据面连接不受影响。以 Kubernetes 为例,v1.2.3 升到 v1.2.4:

set -eu
target=v1.2.4
run_ctrl_command() {
  sed -e "s/COMMAND/$1/g" -e "s/kibotos-ctrl:v[^ ]*/kibotos-ctrl:$target/" deploy/kubernetes/ctrl-command.yaml | kubectl apply -f -
  kubectl -n kibotos wait --for=condition=complete --timeout=10m "job/kibotos-ctrl-$1" || true
  kubectl -n kibotos logs "job/kibotos-ctrl-$1"
  kubectl -n kibotos delete job "kibotos-ctrl-$1"
}
kubectl -n kibotos scale deploy/kibotos-ctrl --replicas=0
kubectl -n kibotos wait --for=delete pod -l app.kubernetes.io/name=kibotos-ctrl --timeout=2m
run_ctrl_command check-upgrade

报告没有阻塞项时,按报告执行迁移,再换镜像启动:

set -eu
target=v1.2.4
sed -e "s/COMMAND/migrate/g" -e "s/kibotos-ctrl:v[^ ]*/kibotos-ctrl:$target/" deploy/kubernetes/ctrl-command.yaml | kubectl apply -f -
kubectl -n kibotos wait --for=condition=complete --timeout=10m job/kibotos-ctrl-migrate
kubectl -n kibotos delete job kibotos-ctrl-migrate
kubectl -n kibotos set image deploy/kibotos-ctrl ctrl="ghcr.io/felinics/kibotos-ctrl:$target"
kubectl -n kibotos scale deploy/kibotos-ctrl --replicas=1
kubectl -n kibotos rollout status deploy/kibotos-ctrl --timeout=5m
  • 报告有阻塞项时不换镜像,把副本数调回 1,ctrl 以原版本恢复服务。
  • Docker Compose 的顺序相同:docker compose stop ctrl,用目标版本镜像 docker compose run --rm ctrl check-upgrade 与 migrate,改 compose.yaml 里的版本后 docker compose up -d ctrl。
  • 迁移执行过后,旧版本的 ctrl 会拒绝更高的 schema,只能往前修。

网关

同一发布线内,网关可以在 ctrl 升级之后的任意时刻替换:

  • Kubernetes:kubectl -n kibotos set image deploy/kibotos-e2b e2b=ghcr.io/felinics/kibotos-e2b:v1.2.4。
  • Docker Compose:改 compose.yaml 里的版本后 docker compose up -d e2b。

网关只有一个实例,替换期间它的入口短暂不可用。网关收到停止信号后立即停止接收新连接,进行中的请求有 5 秒完成;之后仍未结束的长连接(命令输出流、终端、Exposure 的 WebSocket)会断开,客户端重连即可。

跨版本升级时网关随停机流程一起替换,先升 ctrl 再升网关。

排查

现象原因处理
ctrl 日志 load managed PKI state: ... must not grant group or other accesskubelet 给卷里的文件加了组权限:fsGroupChangePolicy 不是 OnRootMismatch改成 OnRootMismatch,用一个以 10001 运行的一次性 Pod 对 /var/lib/kibotos/ctrl 执行 chmod -R go= 后重启 ctrl
ctrl 日志 config: open /etc/kibotos/ctrl/config.toml: permission denied配置文件对服务账户不可读Kubernetes 设 fsGroup 并让 Secret 的 defaultMode 带组读权限;Docker 把文件交给 10001
网关日志 holds no enrolled identity and no enrollment key file is configured卷是新的,又没有配置 key 文件签发 key 并配置 KIBOTOS_E2B_ENROLLMENT_KEY_FILE 或 KIBOTOS_GW_ENROLLMENT_KEY_FILE
网关日志 enroll: 后面是 permission_denied、failed_precondition 或 already_exists密钥已被使用、过期或被撤销为这个网关的 principal 签发新密钥,更新 Secret 后重启它
网关连不上 ctrl,或报证书不匹配用了 ctrl Service 的集群内名字,或 Pod 里解析不到 [identity].host用 [identity].host,必要时加 hostAliases
Pod 停在 ErrImagePull,原因是 401 或 403拉取镜像的凭证缺失或没有读取权限按镜像一节配置 imagePullSecret

本页内容