运行时环境(RTE)
RTE 是什么、它如何运行 playbook,以及它可以接触哪些内容
面向管理员:在运行 RTE、确定其规格或将其迁移到另一个网络之前,先了解 RTE。
| 页面 | 内容 |
|---|---|
| 镜像 | 三种 RTE 镜像,以及构建包含您的 playbook 所用集合的自定义镜像 |
| 模式 | 出站、入站或数据库模式:谁打开连接、spool、作业的文件 |
| 运行 RTE | 每种模式的设置,以及每种模式的 docker run 示例 |
| 多个 RTE | 容量、同一地址后的副本,以及 RTE 停止或宕机时会发生什么 |
| 另一个数据中心 | 操作手册:在第二个数据中心添加 RTE,从一台裸主机到运行第一个作业 |
RTE 是什么
AnsibleForms 7 不在应用容器中运行任何 playbook。ansible 表单的作业会交给类型为 rte 的 运行器,即运行时环境:以 AF_ROLE=rte 启动的 AnsibleForms 服务器,搭配 python 和 ansible,没有 Web 界面。它通过一个 WebSocket 与应用通信,不持有任何数据库凭据 (RTE_MODE 为 outbound 或 inbound,参见 模式):应用将作业发给它, RTE 运行 ansible-playbook,应用保存它发回的输出和最终状态,浏览器再从应用读取这些内容。
应用解析作业的凭据,并以仅供该 RTE 和该作业使用的密封形式交给 RTE。RTE 从不读取 凭据表或密钥库,playbook 也不会继承 AnsibleForms 自身的环境变量 (ENCRYPTION_SECRET、DB_*、RTE_TOKEN 等):请将这类值作为 extravar 或凭据传入。7.0 到 7.2 的 RTE 会自己读写数据库,它在 7.x 中仍以已弃用的数据库模式运行(参见 数据库模式)。
RTE 如何运行
RTE 是您自己运行的容器,可以位于应用旁边,也可以位于另一个网络中:AnsibleForms 从不创建 RTE。下面依次介绍 它在哪里运行、与应用共享什么,以及从表单启动到 playbook 进程结束的整个过程中, 其内部发生了什么:
无论哪一方打开连接,图中每个 RTE 都只有一个套接字:应用主动连接入站 RTE,出站 RTE 主动连接应用(参见 模式)。连接打开后,相同的消息在两个方向上传递,并且只有应用读写数据库。
谁来启动它。 RTE 是一个长期运行的容器,由运行您其他容器的系统启动。它等待作业, 在作业之间保持运行:
| AnsibleForms 的运行方式 | 谁启动 RTE |
|---|---|
docker run 或 podman run | 您自己,使用类似 运行 RTE 中的命令,在任何能访问应用或应用能访问的主机上运行 |
| Docker Compose | docker-compose.yml 中的 rte 服务,与 app 和 mysqldb 并列。其配置种子将它声明为默认运行器 rte,入站模式,地址为 http://rte:8000 |
| Helm chart | Kubernetes,作为应用 pod 中名为 rte 的第二个容器(containers.rte)。应用在 127.0.0.1 上以入站模式连接它,chart 的种子将它声明为默认运行器 |
| 安装程序 | 如上所述的 Docker Compose 或 Helm chart,或 不使用容器 的 systemd 服务;它自己的运行器会请求加入,并等待管理员批准 |
容器引擎由您负责:主机上的 Docker 或 Podman,或 Kubernetes 集群的容器运行时,由您安装并保持 更新。AnsibleForms 从不与它通信。应用和 RTE 都不会调用 Docker 或 Podman、挂载它们的套接字或启动 容器:应用只通过套接字与 RTE 通信。
里面有什么。 一个容器,只包含运行 playbook 所需的内容:
- 服务器:与应用相同的 Node.js 服务器,在
tini下以AF_ROLE=rte(在镜像中设置)启动 - 省略的部分:Web 界面、登录、调度器、worker 以及所有数据库模块,此角色不会加载它们
- 监听的内容:其
PORT(8000):用于探针的/live和/ready、用于 Prometheus 的/metrics,入站模式下还有应用 连接的套接字/rte/v1/socket。出站 RTE 不接受来自应用的连接 - Python 和 ansible:由您选择的镜像层提供(参见 RTE 镜像)
- 用户:服务器和每个 playbook 都以
node(uid 1000)运行,从不以 root 运行
与应用共享什么。 它的令牌;使用 RTE_FILES=local 时还有应用的文件。使用默认的 RTE_FILES=transfer 时, playbook 和清单随每个作业一起传送,RTE 不需要共享磁盘:
| 与应用共享 | RTE 用它做什么 |
|---|---|
RTE_TOKEN | 在连接应用时出示它(出站),或在应用连接它时检查它(入站)。应用在运行器的 RTE 凭据中保存相同的值,作业的密钥也用它密封 |
持久化卷 /app/dist/persistent,仅在 RTE_FILES=local 时 | playbook 文件夹(ANSIBLE_PATH),或 worker 克隆到 REPO_PATH 下的 playbook 仓库、清单(INVENTORY_PATH 和清单仓库)以及上传文件。RTE 会将作业的 extravars 文件写入 playbook 文件夹,因此请以读写方式挂载 |
ssh 文件夹 /home/node/.ssh | playbook 的 ssh 连接所用的私钥和 known_hosts:挂载应用的(在其设置页面上管理),或为 RTE 提供它自己的 |
RTE 不克隆任何内容:仓库通过作业的文件到达它,或位于它与应用共享的卷上。集合、角色和 ansible.cfg 来自镜像,或者来自您挂载到 /etc/ansible/collections、/etc/ansible/roles 和 /etc/ansible/ansible.cfg 的文件夹(Docker Compose 项目就是这样做的),或者来自 playbook 旁边的 ansible.cfg。
作业的生命周期。 下面的编号与图中的编号对应,适用于从浏览器或 API 启动的 playbook 表单:
- 启动:应用检查表单及其值,构建 extravars,并以状态
running保存作业。带有 审批 的表单会在这里停住,直到审批人接受,在此之前不会到达任何 RTE - 选择运行器:应用使用表单以
runner: <name>指定的 RTE,否则使用类型为 RTE 的默认运行器,并将其 名称保存在作业上。它从数据库或 密钥库 解析作业的凭据、ansible 登录信息和 vault 密码,并用 AES-256-GCM 以从该运行器令牌和作业 ID 派生的密钥将其密封 - 交接:持有该 RTE 套接字的应用节点将作业发给它:extravars、表单的选项、密封的密钥以及 作业文件的哈希。任何应用节点都可以启动作业;持有套接字的节点会在两秒内发送。RTE 已满 或未连接时,作业会等待,最长为
RTE_QUEUE_MINUTES - 认领:RTE 回复
claimed;当它已满、正在停止、无法打开密钥,或作业带有与其自身不同的 协议约定 时,回复refused。使用RTE_FILES=transfer时,它按哈希从缓存中获取 playbook 和清单, 向应用请求它没有的哈希,并将它们解压到作业专属的文件夹中 - 开始:在 playbook 文件夹(加上表单的
playbookSubPath,该路径必须存在)中写入两个文件:包含表单值和凭据的extravars_<id>.json,以及一个包含 ansible 登录信息的隐藏文件。然后它以自己的子进程启动ansible-playbook, 不经过 shell,位于独立的进程组中,使用-e @指向这些文件,每个清单一个-i,并带上表单的 tags、limit、check、 diff 和详细级别。vault 密码通过 stdin 传入,从不写入文件或命令行,进程以node身份在经过过滤的环境中运行(见下文) - 运行:playbook 从 RTE 容器连接受管主机,通过 ssh 使用
/home/node/.ssh中的密钥和known_hosts, 密码登录时使用sshpass,或者通过其模块调用的 API。需要能访问这些主机的是 RTE,而不是应用 - 输出与结束:RTE 每半秒或每 64 KB 将 stdout 和 stderr 打包一次,掩码凭据,并为每批 编号。它在发送之前会把每一项写入其磁盘上作业的 spool:输出、事件、
.joblogs/job_log_<id>.log中的作业日志(每 2 秒)、开始以及带有最终状态(success、failed或aborted)的结束。应用 每一项只保存一次并确认收到,结束状态保存后,RTE 会丢弃该 spool。某个流超过PROCESS_MAX_BUFFER字节(默认 50 MB)后,它不再发送更多内容, playbook 继续运行。然后应用发送表单的 通知 - 跟踪:浏览器从不与 RTE 通信:表单页面每 2 秒通过应用重新读取作业,作业页面 则在应用保存每一项时实时跟踪
套接字断开。 playbook 继续运行。RTE 重新连接(出站),或某个应用节点再次连接它(入站),并重新发送 应用尚未确认的内容:应用会跳过已经保存的内容,因此不会丢失任何一行,也不会重复保存。前提是套接字 在两分钟内恢复:应用只在套接字打开时写入 RTE 的心跳,一旦该心跳已过去两分钟, worker 就会将该 RTE 正在运行的作业作为已放弃作业结束,即使其 playbook 仍在运行。请将 spool (RTE_SPOOL_PATH)放在卷上,这样重启后的 RTE 仍能发送它保留的内容。 RTE 重启时正在运行的 playbook 无法继续:当 RTE 回来时没有它,应用会将其作业作为已放弃作业结束。
中止。 中止会设置作业的中止标志,持有该 RTE 套接字的应用节点会在两秒内发送 cancel。RTE 向 playbook 的进程组发送 SIGTERM,因此 ansible-playbook 及其启动的每个分支进程会一起停止,作业以 aborted 结束。早于作业到达的取消请求会保留一分钟,作业随后以中止状态开始。
清理。 进程一旦退出,无论结果如何,RTE 都会删除两个 extravars 文件(设置了 keepExtravars 的表单 会保留可见的那个,但从不保留隐藏的那个)、/tmp 中的工件文件、作业日志文件以及作业专属的文件文件夹。 结束状态只保存一次:第二个写入者(例如应用让 RTE 已经结束的作业失败)不会改变任何内容。
每个作业一个进程,而不是每个作业一个容器。 AWX 和 AAP 会为每个作业从执行环境镜像启动一个新的 pod 或容器。 AnsibleForms 不启动任何东西:RTE 容器已经在运行,每个作业都是其中的一个子进程。作业启动时 无需拉取镜像或调度 pod,AnsibleForms 也不需要您的容器引擎或集群的任何权限。相应地, 在同一个 RTE 上运行的 playbook 共享其 CPU、内存、/tmp、主目录和 python:请为容器分配足够的资源,以运行 RTE_MAX_JOBS 个 playbook 及其分支进程。要改变 playbook 可以使用的内容,请修改镜像,而不是作业。
安全边界
RTE 运行的是表单作者编写的代码,因此它被设计为持有尽可能少的东西:
- 只有它的令牌:RTE 不持有数据库凭据,也没有
ENCRYPTION_SECRET。除了应用通过套接字使用其令牌发起的调用外, 它不接受任何调用,没有用户、没有登录,也没有网页。通过套接字,它只能运行应用发给它的作业、报告这些作业并 请求它们的文件:它不能创建作业、读取其他作业或分发凭据 - 密封的密钥:运行作业时,RTE 从不读取凭据表或密钥库。它获得的是自己运行的作业的密钥, 并且仅为其令牌和该作业 ID 密封,因此发给另一个 RTE 或另一个作业的数据包无法打开,被改动过一个字节的数据 也会被拒绝
- 经过校验的连接:出站时,RTE 校验应用的证书。通过 HTTPS 入站时,应用在 RTE 加入时固定其证书, 之后只有在 RTE 出示该证书时才会连接它(参见 模式)。通过纯 HTTP 的入站 RTE 应位于 私有网络中,例如 Docker Compose 项目的网络
- 不继承环境:
ansible-playbook获得的 RTE 环境中去掉了 AnsibleForms 自己读取的所有变量 (ENCRYPTION_SECRET、DB_PASSWORD、RTE_TOKEN、VAULT_TOKEN、ACCESS_TOKEN_SECRET、MYSQL_*等),因此lookup('env', ...)无法获取它们。您为 playbook 设置的变量(PATH、代理、ANSIBLE_*)会原样传递 - 不使用来自用户的模板:包含
{{、{%或{#的表单值会以 unsafe 字符串的形式到达 playbook, ansible 从不对其进行模板渲染,除非表单设置了allowJinjaInExtravars,并且命令行从不经过 shell 运行 - 不是 root:RTE 及其 playbook 以
node(uid 1000)运行,因此 playbook 能做的就是该用户在容器中能做的事,例如 读取其文件和 ssh 密钥,仅此而已
请像对待应用一样谨慎对待它的容器、镜像和环境。
数据库模式(已弃用)
7.0 到 7.2 的 RTE(没有 RTE_MODE)在整个 7.x 中都可以运行,并将在 8.0.0 中移除。它与上述步骤有四点不同:
- 它持有数据库:其
DB_*设置,以及在自行注册时的ENCRYPTION_SECRET(RTE_REGISTER) - 交接是一次 HTTP 调用:应用携带令牌和密封的密钥调用
POST <address>/rte/v1/jobs,RTE 通过 在数据库中把自己的名称写到作业上来认领该作业(第 3 和第 4 步) - 它自己写入输出:分批写入数据库,没有 spool(第 7 步)。中止通过作业的中止标志到达它, 它每 2 秒读取一次该标志,另外还有
/cancel调用 - 它从应用的卷读取文件:它需要与应用相同的磁盘,与
RTE_FILES=local相同