mcpField
J
jenkins-agent-l3-deploy

jenkins-agent-l3-deploy

github.com/rheinmirCloud & DevOps10239 installsUpdated 2w ago
Repository

About jenkins-agent-l3-deploy

A Jenkins deployment skill for running a docker-compose deployment through an inbound agent on the target Linux host, without controller-initiated SSH. It stages runtime secrets in tmpfs and verifies that they are mounted and absent from `docker inspect`.

Key features

  1. 01Deploys docker-compose on the target agent
  2. 02Stages secrets in tmpfs
  3. 03Mounts secrets as Docker secrets
  4. 04Supports selective service deployment
  5. 05Fails verification on leaks or missing mounts

Use cases

  1. 01Deploying a compose stack without controller SSH
  2. 02Keeping runtime secrets out of `.env` and `docker inspect`
  3. 03Redeploying services after a target reboot clears tmpfs

Install

Copy the command and run it in a terminal.

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

After installing, type/jenkins-agent-l3-deployin Claude Code to confirm it loaded.

Details

From the project README

Jenkins-agent L3 deploy (no SSH)

Deploy a docker-compose stack where a Jenkins inbound agent on the target host runs deploy.py + docker compose locally. The controller never SSHes in. Runtime secrets are written to tmpfs /run/payroll-sec (file 0444, dir 0700), mounted as Docker secrets at /run/secrets/<name>, and a tiny entrypoint shim exports them to env at container start — so they are absent from docker inspect and from .env.

WHAT

Purpose và context
  • Purpose: dựng/chạy pipeline Jenkins deploy một stack docker-compose QUA inbound agent trên chính target (controller không SSH vào), với secret L3: chỉ nằm trên tmpfs, mount làm Docker secret, không có trong docker inspect lẫn .env.
  • Trigger (when to use vs the SSH variant):
    • Agent (this skill, preferred): an inbound agent runs on the target → sh steps execute locally; only a Secret file credential (the cred bundle) is needed. Controller opens no SSH outbound.
    • SSH variant: controller ssh/scp/rsync into the target (needs an SSH Username with private key credential). Use only when you cannot run an agent on the target.
    • User nói "deploy via jenkins agent", "no-ssh deploy", "agent thay vì ssh", "docker secrets L3", "ẩn secret khỏi docker inspect", "secret không nằm trên đĩa", hoặc đang nối pipeline Jenkins deploy docker-compose lên host Linux.
  • Non-goals: không dựng biến thể SSH (chỉ khi không chạy được agent); không sửa code ứng dụng để đọc secret (shim entrypoint lo); không cài Jenkins controller.
Mental model

Architecture (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 uses the official POSTGRES_PASSWORD_FILE; app containers use the entrypoint shim so no application code changes (avoids the Edge-runtime fs trap if a framework imports the secret module in middleware).

Input và output contract
FieldRequired?Ý nghĩa
InJenkins controller reachable :8080 + :50000 từ targetcóagent phải kết nối được
Intarget Linux: Java 21, user thuộc docker group + sudo NOPASSWDcóxem Gotchas 1, 4
InSecret file credential (cred bundle)cókhông cần SSH credential
Inrepo app có docker-compose.yml, deploy.py(.example), Dockerfile + secrets-entrypoint.shcóqua SCM checkout hoặc workspace có sẵn
Intick chọn service (WEB/ETL/DB/NGINX/ALL)khôngselective deploy; mặc định full
Outstack chạy trên targetcóbuild console Running on <target>
Outtín hiệu verifycóCLEAN <c> + MOUNT <c> mọi container, smoke 200, cred bundle đã shred
Rules và capabilities
  • RULE-01 (MUST): Don't pass secrets via compose environment: (visible in docker inspect, written to .env).
  • RULE-02 (MUST): Don't store the secret files 0600 (non-root container can't read) — use 0444 + dir 0700.
  • RULE-03 (MUST): Don't rely on the SSH variant if you can run an agent — agent keeps SSH closed and runs locally.
  • RULE-04 (MUST): Don't point a file:// SCM URL at a path the controller can't see — use a network git URL.
  • RULE-05 (MUST): Gotchas 1–5 (mỗi cái đã làm hỏng một lần chạy thật) phải được bake vào setup/pipeline.
  • Capabilities: cấu hình node/credential/job trên Jenkins; chạy shell + docker + sudo trên target; đọc git repo (SCM). Không mở SSH outbound từ controller.
Failure boundaries
  • Không chạy được agent trên target → ngoài phạm vi, dùng biến thể SSH (blocked cho skill này).
  • Stage Verify in LEAK <c> hoặc NOMOUNT <c> → pipeline failed (exit 1), không coi là deploy xong.
  • Agent chết UnsupportedClassVersionError / permission denied docker-sudo / secret 0600 → failed, sửa theo bảng Gotchas rồi chạy lại.
  • Repo private chưa có deploy key / known_hosts → blocked ở checkout.

HOW

Main workflow
StepTypeInputsActionOutputs/exitFailure/next
W01judgmenthạ tầng targetChọn agent (ưu tiên) hay SSH variantquyết địnhkhông chạy được agent → dừng, SSH variant
W02effectJenkins + targetSetup inbound agent (once): node, Java 21, agent.jar, user docker+sudo, Secret file credentialagent onlinegotcha 1/4 → sửa, lặp
W03effectrepo appBake secrets-entrypoint.sh vào mỗi image (ENTRYPOINT trước CMD); provisioner ghi secret 0444/0700image + provisioner—
W04effectjobPipeline template (Sync → Deploy → Verify), code từ SCMjob chạycheckout lỗi → B02
W05deterministicbuildVerify success (real signals)Running on <target>, CLEAN, MOUNT, smoke 200, cred shreddedLEAK/NOMOUNT → failed

Chi tiết từng bước (nguồn chân lý cho W01–W05):

Setup the inbound agent (once)
  1. Jenkins → Manage Jenkins → Nodes → New Node: Permanent Agent, label = <target>, Remote root = /home/<user>/jenkins-agent, Launch = inbound (JNLP); copy the secret.
  2. On the target (agent must reach controller :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 &
    
    Run the agent as a user that is in the docker group AND has sudo NOPASSWD. For production replace nohup with a systemd unit (Restart=always).
  3. Credential: one Secret file credential (the cred bundle). No SSH credential.
Pipeline template (declarative)
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
    ''' } }
  }
}

Provide the workspace code via SCM checkout, or pre-populate the agent workspace.

secrets-entrypoint.sh (baked into each app image; ENTRYPOINT before 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 "$@"

The provisioner (e.g. deploy.py) writes the secret files to a tmpfs dir mode 0444 (files) / 0700 (dir), then docker compose up -d.

Gotchas (each cost a failed real run — bake these in)
#SymptomFix
1Agent dies UnsupportedClassVersionError: class 65.0 … up to 61.0Jenkins 2.5xx agent.jar needs Java 21, not 17.
2App container crash-loops Permission denied /run/secrets/*Compose non-swarm bind-mounts the source file's perms; write secret files 0444 (dir 0700) so the non-root container user can read.
3shred /dev/shm/.../cred: Permission deniedJenkins file credential is delivered 0400 (read-only); cp then chmod 600, and use `shred -uf …
4docker/sudo "permission denied" inside the pipelineThe agent process user must be in the docker group and have sudo NOPASSWD.
5Reboot → containers fail to mount secret/run is tmpfs → secrets cleared on reboot; re-run the deploy (or a boot hook) to re-materialize before compose up.
Verify success (real signals)
  • Build console shows Running on <target> (proves it ran on the agent, not controller).
  • CLEAN <c> for every container (secret absent from docker inspect).
  • MOUNT <c> (secret present at /run/secrets).
  • App smoke (e.g. login) returns 200.
  • The cred bundle is shredded from /dev/shm (not left on disk).
Branches
IDKindGuardHành viSkip / failureRejoin
B01user_optionalchỉ deploy vài service (tick WEB/ETL/DB/NGINX thay ALL)Selective deploy: booleanParam → deploy.py --services "$SVC" + --no-deps; NGINX thì docker restart proxy riêngALL → full; không tick gì → none = secrets only, no upW05
B02conditional_requiredcode lấy từ git (production: Pipeline script from SCM)cấu hình SCM; repo private → SSH deploy key + known_hosts trên controller VÀ agentlab không git server → agent git clone file:///…/repo.gitW04
B03recoverytarget reboot (/run tmpfs xoá secret)chạy lại deploy (hoặc boot hook) để tái tạo secret trước compose up—W05
Reference — Branch details
Selective deploy (deploy only some services, not full)

Expose booleanParam checkboxes (WEB/ETL/DB/NGINX/ALL) and pass the chosen compose services to the provisioner. Use --no-deps so a tick rebuilds only that service (not its dependency chain):

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

Dependency graph (depends_on) — document it so operators know a fresh deploy needs the chain: web → etl → postgres (proxy/nginx independent). With --no-deps, tick web = ONLY web; for a clean first deploy tick the whole chain (or ALL). Verified: single ticks + combos all touch exactly the chosen services.

Pulling code from git

Production: configure the job as Pipeline script from SCM (git repo + scriptPath); Jenkins auto-checkout scm into the agent workspace, then the pipeline runs. The git URL must be reachable by BOTH controller (fetch Jenkinsfile) and agent (checkout workspace) — file:// only works on the side that owns it; in a lab without a git server, have the agent git clone file:///…/repo.git.

Private repo (tested with GitHub, build SUCCESS):

  • HTTPS anonymous clone fails for a private repo — needs auth.
  • Use an SSH deploy key (read-only) on the repo; put the private key in a Jenkins SSH Username with private key credential (username git); SCM URL [email protected]:owner/repo.git with that credentialsId.
  • Host key gotcha: add github.com to known_hosts on controller AND agent (ssh-keyscan github.com >> ~/.ssh/known_hosts) or set the git host-key strategy to "Accept first connection", else the SSH clone fails host-key verification.
  • The deploy key's public part on GitHub must match the private key in Jenkins (compare ssh-keygen -lf key.pub fingerprint vs what GitHub shows).
Validation và stopping

Stage Verify là cổng tất định: exit 1 khi có LEAK/NOMOUNT. Cần người xem: console Running on <target>, smoke login 200, /dev/shm không còn cred. Dừng khi đủ 5 tín hiệu ở "Verify success"; lỗi thuộc bảng Gotchas → sửa đúng dòng Fix rồi chạy lại build, không đoán ngoài bảng.

Examples
  • Positive: app payroll (web/etl/postgres) — agent label <target> Java 21, user trong docker group + sudo NOPASSWD, build tick ALL → console Running on <target>, CLEAN/MOUNT cho mọi container, login 200, cred bundle đã shred khỏi /dev/shm.
  • Boundary/failure: secret ghi 0600 → container web crash-loop Permission denied /run/secrets/* (gotcha 2) → provisioner đổi sang 0444 + dir 0700, chạy lại; tick chỉ WEB lần deploy đầu → web lên một mình vì --no-deps, thiếu etl/postgres → tick cả chuỗi web → etl → postgres (hoặc ALL).

Comments

Markdown supported