mcpField
J
jenkins-agent-l3-deploy

jenkins-agent-l3-deploy

github.com/rheinmir云与 DevOps10239 次安装2 周前更新
代码仓库

关于 jenkins-agent-l3-deploy

用于通过目标 Linux 主机上的 Jenkins 入站 Agent 执行 docker-compose 部署的技能,无需控制器发起 SSH。它将运行时密钥暂存于 tmpfs,并验证密钥已挂载且未出现在 `docker inspect` 中。

主要功能

  1. 01在目标 Agent 上部署 docker-compose
  2. 02将密钥暂存于 tmpfs
  3. 03以 Docker secrets 挂载密钥
  4. 04支持选择性部署服务
  5. 05密钥泄露或挂载缺失时验证失败

适用场景

  1. 01不使用控制器 SSH 部署 compose 堆栈
  2. 02避免运行时密钥进入 `.env` 和 `docker inspect`
  3. 03目标主机重启清除 tmpfs 后重新部署服务

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/rheinmir/setup --skill jenkins-agent-l3-deploy

安装后在 Claude Code 中输入/jenkins-agent-l3-deploy确认已加载。

详细介绍

Jenkins-agent L3 部署(无 SSH)

部署一个 docker-compose 栈,其中目标主机上的 Jenkins 入站 Agent 在本地运行 deploy.py + docker compose。控制器不会通过 SSH 登录。 运行时密钥会写入 tmpfs /run/payroll-sec(文件权限 0444,目录权限 0700), 并作为 Docker 密钥挂载到 /run/secrets/<name>;启动容器时,一个精简的入口点 shim 会将它们导出为环境变量,因此它们不会出现在 docker inspect 或 .env 中。

内容

目的与背景
  • 目的: 构建/运行 Jenkins 流水线,通过目标主机上的入站 Agent 部署一个 docker-compose 栈(控制器不通过 SSH 登录),并使用 L3 密钥:仅存放在 tmpfs 上,作为 Docker 密钥挂载,不会出现在 docker inspect 或 .env 中。
  • 触发条件(何时使用,与 SSH 变体的区别):
    • Agent(本技能,首选): 目标主机上运行入站 Agent → sh 步骤在本地执行;只需要一个 Secret file 凭据(凭据包)。控制器不会发起 SSH 出站连接。
    • SSH 变体: 控制器通过 ssh/scp/rsync 登录目标主机(需要 SSH Username with private key 凭据)。仅在无法在目标主机上运行 Agent 时使用。
    • 用户提到“通过 Jenkins Agent 部署”“无 SSH 部署”“用 Agent 而不是 SSH”“Docker secrets L3”“从 docker inspect 中隐藏密钥”“密钥不落盘”,或正在将 Jenkins 部署流水线连接到 Linux 主机上的 docker-compose。
  • 非目标: 不构建 SSH 变体(仅在无法运行 Agent 时使用);不修改应用代码来读取密钥(由 shim 入口点处理);不安装 Jenkins 控制器。
概念模型

架构(L3):

secret source (cred bundle)
   → deploy provisioner writes 7 files to tmpfs /run/payroll-sec  (file 0444, dir 0700 root)
   → docker-compose `secrets:` mounts each at /run/secrets/<name> (tmpfs, ro)
   → secrets-entrypoint.sh in each image: for f in /run/secrets/*; export $(UPPER basename)=$(cat f); exec "$@"
   ⇒ secret NOT in `docker inspect` (set at runtime), NOT on disk (RAM only)

postgres 使用官方的 POSTGRES_PASSWORD_FILE;应用容器使用 shim 入口点,因此无需修改任何应用代码(可避免框架在中间件中导入密钥模块时触发 Edge 运行时的 fs 陷阱)。

输入与输出约定
字段必需?含义
输入Jenkins 控制器可从目标主机访问 :8080 + :50000是Agent 必须能够连接
输入目标 Linux:Java 21,用户属于 docker 组且配置 sudo NOPASSWD是参见 Gotchas 1、4
输入Secret file 凭据(凭据包)是不需要 SSH 凭据
输入应用仓库包含 docker-compose.yml、deploy.py(.example)、Dockerfile + secrets-entrypoint.sh是通过 SCM 检出,或使用已有工作区
输入勾选要部署的服务(WEB/ETL/DB/NGINX/ALL)否选择性部署;默认全部部署
输出栈在目标主机上运行是构建控制台显示 Running on <target>
输出验证信号是所有容器均显示 CLEAN <c> + MOUNT <c>,smoke 测试返回 200,凭据包已 shred
规则与能力
  • RULE-01(必须):不要通过 compose environment: 传递密钥(会显示在 docker inspect 中,也会写入 .env)。
  • RULE-02(必须):不要将密钥文件存储为 0600(非 root 容器无法读取)——使用 0444 + 目录权限 0700。
  • RULE-03(必须):如果可以运行 Agent,不要依赖 SSH 变体——Agent 可保持 SSH 关闭并在本地运行。
  • RULE-04(必须):不要将 file:// SCM URL 指向控制器无法访问的路径——使用网络 git URL。
  • RULE-05(必须):Gotchas 1–5(每条都曾导致一次真实运行失败)必须纳入配置/流水线。
  • 能力:配置 Jenkins 节点/凭据/作业;在目标主机上运行 shell、docker 和 sudo;读取 git 仓库(SCM)。不从控制器发起 SSH 出站连接。
失败边界
  • 无法在目标主机上运行 Agent → 超出范围,使用 SSH 变体(本技能不支持)。
  • Verify 阶段出现 LEAK <c> 或 NOMOUNT <c> → 流水线失败(退出码 1),不可视为部署完成。
  • Agent 因 UnsupportedClassVersionError / docker-sudo 权限被拒 / 密钥权限为 0600 而崩溃 → 失败,按照 Gotchas 表修正后重新运行。
  • 私有仓库尚未配置 deploy key / known_hosts → 检出阶段受阻。

操作

主要工作流
步骤类型输入操作输出/退出失败/下一步
W01判断目标基础设施选择 Agent(优先)或 SSH 变体决策Agent 无法运行 → 停止,使用 SSH 变体
W02操作Jenkins + 目标设置入站 Agent(一次性):节点、Java 21、agent.jar、docker+sudo 用户、Secret file 凭据Agent 在线遇到问题 1/4 → 修复并重试
W03操作应用仓库将 secrets-entrypoint.sh 烘焙到每个镜像中(ENTRYPOINT 在 CMD 之前);配置器以 0444/0700 写入 secret镜像 + 配置器—
W04操作作业Pipeline 模板(Sync → Deploy → Verify),代码来自 SCM作业运行checkout 出错 → B02
W05确定性构建验证成功(实际信号)Running on <target>、CLEAN、MOUNT、smoke 200、凭据已清除LEAK/NOMOUNT → 失败

各步骤详情(W01–W05 的事实来源):

设置入站 Agent(一次性)
  1. Jenkins → Manage Jenkins → Nodes → New Node:Permanent Agent,label = <target>, Remote root = /home/<user>/jenkins-agent,Launch = inbound (JNLP);复制 secret。
  2. 在目标机器上(Agent 必须能够访问控制器的 :8080 + :50000):
    sudo apt-get install -y openjdk-21-jre-headless          # Java 21 — NOT 17 (see gotcha 1)
    J21=$(ls /usr/lib/jvm/java-21-openjdk-*/bin/java | head -1)
    mkdir -p ~/jenkins-agent && cd ~/jenkins-agent
    curl -sO http://<CONTROLLER>:8080/jnlpJars/agent.jar
    nohup "$J21" -jar agent.jar -url http://<CONTROLLER>:8080/ \
      -secret <AGENT_SECRET> -name <target> -workDir ~/jenkins-agent > agent.log 2>&1 &
    
    使用同时属于 docker 组且拥有 sudo NOPASSWD 权限的用户运行 Agent。 在生产环境中,将 nohup 替换为 systemd unit(Restart=always)。
  3. 凭据:一个 Secret file 凭据(凭据包)。不使用 SSH 凭据。
Pipeline 模板(声明式)
pipeline {
  agent { label '<target>' }                 // runs ON the target, no ssh
  environment { APP = '/home/<user>/<app-dir>' }
  stages {
    stage('Sync') { steps { sh '''
      set -e
      cp -f docker-compose.yml deploy.py.example "$APP/"
      cp -f web/Dockerfile web/secrets-entrypoint.sh "$APP/web/"
      cp -f etl/Dockerfile etl/secrets-entrypoint.sh "$APP/etl/"
      cd "$APP" && cp -f deploy.py.example deploy.py
    ''' } }
    stage('Deploy') { steps { withCredentials([file(credentialsId:'app-cred', variable:'CRED')]) { sh '''
      set -e
      mkdir -p /dev/shm/pl && chmod 700 /dev/shm/pl
      cp "$CRED" /dev/shm/pl/cred.txt && chmod 600 /dev/shm/pl/cred.txt   # gotcha 3
      cd "$APP" && ln -sf /dev/shm/pl/cred.txt cred.txt
      sudo python3 deploy.py --no-nginx --etl-url /etl
      rm -f cred.txt; shred -uf /dev/shm/pl/cred.txt 2>/dev/null || rm -f /dev/shm/pl/cred.txt
    ''' } } }
    stage('Verify') { steps { sh '''
      set -e
      for c in <containers>; do
        docker inspect "$c" --format "{{range .Config.Env}}{{println .}}{{end}}" \
          | grep -iqE "SESSION_SECRET=|AUTH_USERS=|PASSWORD=|DATABASE_URL=" && { echo "LEAK $c"; exit 1; } || echo "CLEAN $c"
        docker exec "$c" ls /run/secrets >/dev/null 2>&1 && echo "MOUNT $c" || { echo "NOMOUNT $c"; exit 1; }
      done
    ''' } }
  }
}

通过 SCM checkout 提供工作区代码,或预先填充 Agent 工作区。

secrets-entrypoint.sh(烘焙到每个应用镜像中;ENTRYPOINT 在 CMD 之前)
#!/bin/sh
set -e
if [ -d /run/secrets ]; then
  for f in /run/secrets/*; do [ -f "$f" ] || continue
    name=$(basename "$f" | tr '[:lower:]' '[:upper:]'); export "$name=$(cat "$f")"
  done
fi
exec "$@"

配置器(例如 deploy.py)将 secret 文件写入 tmpfs 目录, 权限模式为 0444(文件)/ 0700(目录),然后运行 docker compose up -d。

易踩问题(每项都会导致一次真实运行失败,应提前处理)
#症状修复方法
1Agent 因 UnsupportedClassVersionError: class 65.0 … up to 61.0 退出Jenkins 2.5xx 的 agent.jar 需要 Java 21,而非 17。
2应用容器因 Permission denied /run/secrets/* 不断崩溃重启Compose 的非 swarm bind-mount 会保留源文件权限;将 secret 文件写为 0444(目录为 0700),以便非 root 容器用户读取。
3shred /dev/shm/.../cred: Permission deniedJenkins file 凭据以 **0400(只读)**方式提供;先 cp,再 chmod 600,并使用 `shred -uf …
4Pipeline 中 docker/sudo 出现 "permission denied"Agent 进程用户必须属于 docker 组且拥有 sudo NOPASSWD 权限。
5重启后 → 容器无法挂载 secret/run 是 tmpfs → 重启后 secret 会被清除;重新运行部署(或使用启动钩子),在运行 compose up 前重新生成 secret。
验证成功(实际信号)
  • 构建控制台显示 Running on <target>(证明构建运行在 Agent 上,而非控制器上)。
  • 每个容器均显示 CLEAN <c>(secret 不存在于 docker inspect 中)。
  • 显示 MOUNT <c>(secret 存在于 /run/secrets)。
  • 应用 smoke 测试(例如登录)返回 200。
  • 凭据包已从 /dev/shm 中彻底清除(未留在磁盘上)。
分支
ID类型条件行为跳过 / 失败汇合
B01用户可选仅部署部分服务(勾选 WEB/ETL/DB/NGINX,而非 ALL)选择性部署:booleanParam → deploy.py --services "$SVC" + --no-deps;对于 NGINX,单独 docker restart 代理ALL → 完整部署;未勾选任何项 → none = 仅 secret,不运行 upW05
B02有条件必需从 git 获取代码(生产环境:Pipeline script from SCM)配置 SCM;私有仓库 → 在控制器和 Agent 上配置 SSH deploy key + known_hosts实验环境没有 git server → Agent 执行 git clone file:///…/repo.gitW04
B03恢复目标机器重启(/run tmpfs 清除了 secret)重新运行部署(或使用启动钩子),在运行 compose up 前重新生成 secret—W05
参考 — 分支详情
选择性部署(仅部署部分服务,而非全部)

暴露 booleanParam 复选框(WEB/ETL/DB/NGINX/ALL),并将所选 compose 服务传递给 provisioner。使用 --no-deps,这样勾选后只会重建该服务 (不会重建其依赖链):

if [ "$ALL" = true ]; then SVC=""; else
  S=""; [ "$WEB" = true ] && S="$S web"; [ "$ETL" = true ] && S="$S etl"; [ "$DB" = true ] && S="$S postgres"
  [ -z "$(echo $S|xargs)" ] && SVC=none || SVC=$(echo $S|xargs)
fi
deploy.py --services "$SVC"        # "" = full ; "web etl" = those ; "none" = secrets only, no up
# provisioner runs: docker compose up -d --build --no-deps $SVC
[ "$NGINX" = true ] && docker restart <proxy-container>   # proxy is a separate container, not compose

依赖关系图(depends_on)——请记录,让运维人员了解全新部署需要的依赖链: web → etl → postgres(proxy/nginx 相互独立)。使用 --no-deps 时,勾选 web = 仅部署 web;首次全新部署时勾选整个依赖链(或 ALL)。已验证:单项勾选和组合勾选都会且只会影响所选服务。

从 git 拉取代码

生产环境:将作业配置为 Pipeline script from SCM(git 仓库 + scriptPath);Jenkins 会自动在 Agent 工作区执行 checkout scm,然后运行流水线。git URL 必须能被控制器和 Agent 访问(控制器获取 Jenkinsfile,Agent 检出工作区)——file:// 仅在拥有该文件的一侧可用;在没有 git 服务的实验环境中,让 Agent 执行 git clone file:///…/repo.git。

私有仓库(已使用 GitHub 测试,构建 SUCCESS):

  • 对私有仓库进行 HTTPS 匿名克隆会失败——需要身份验证。
  • 在仓库上使用只读 SSH 部署密钥;将私钥放入 Jenkins 的 SSH Username with private key 凭据中(用户名为 git);SCM URL 使用 [email protected]:owner/repo.git,并配置对应的 credentialsId。
  • **主机密钥注意事项:**在控制器和 Agent 的 known_hosts 中添加 github.com(ssh-keyscan github.com >> ~/.ssh/known_hosts),或将 git 主机密钥策略设为 "Accept first connection",否则 SSH 克隆会因主机密钥验证失败。
  • GitHub 上部署密钥的公钥必须与 Jenkins 中的私钥匹配(比较 ssh-keygen -lf key.pub 的指纹与 GitHub 显示的指纹)。
验证与停止

Verify 阶段是确定性关卡:出现 LEAK/NOMOUNT 时退出码为 1。需要人工查看:控制台中的 Running on <target>、smoke 登录返回 200、/dev/shm 中不再有凭据。满足 "Verify success" 中的 5 个信号后停止;遇到属于 Gotchas 表的问题 → 按 Fix 列修复对应项后重新运行构建,不要猜测表外的原因。

示例
  • **正向:**payroll 应用(web/etl/postgres)——Agent 标签 <target>,Java 21,用户属于 docker group 且有 sudo NOPASSWD;构建时勾选 ALL → 控制台显示 Running on <target>,所有容器均为 CLEAN/MOUNT,登录返回 200,凭据包已从 /dev/shm shred 删除。
  • **边界/失败:**密钥以 0600 写入 → web 容器不断崩溃重启,报错 Permission denied /run/secrets/*(gotcha 2)→ provisioner 将权限改为 0444,目录改为 0700,然后重新运行;首次部署时只勾选 WEB → 由于 --no-deps,web 单独启动,缺少 etl/postgres → 勾选整个依赖链 web → etl → postgres(或 ALL)。

评论

支持 Markdown