mcpField
J
jenkins-agent-l3-deploy

jenkins-agent-l3-deploy

github.com/rheinmirクラウド・DevOps10239 回インストール2 週間前に更新
リポジトリ

jenkins-agent-l3-deploy について

ターゲットの Linux ホスト上で稼働する Jenkins のインバウンドエージェント経由で、コントローラーから SSH 接続せずに docker-compose をデプロイするスキルです。実行時シークレットを tmpfs に置き、マウント済みで `docker inspect` に現れないことを検証します。

主な機能

  1. 01ターゲットエージェント上での docker-compose デプロイ
  2. 02tmpfs へのシークレット配置
  3. 03Docker 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 エージェントによる L3 デプロイ(SSH なし)

ターゲットホスト上の Jenkins インバウンドエージェントが deploy.py と docker compose をローカルで実行する docker-compose スタックをデプロイします。コントローラーから SSH 接続することはありません。 実行時のシークレットは **tmpfs /run/payroll-sec(ファイル 0444、ディレクトリ 0700)**に書き込まれ、Docker シークレットとして /run/secrets/<name> にマウントされます。小さなエントリーポイントシムがコンテナー起動時にこれらを環境変数としてエクスポートするため、docker inspect と .env には表示されません。

WHAT

目的と背景
  • 目的: Jenkins パイプラインを構築/実行し、ターゲット上のインバウンドエージェント経由で docker-compose スタックをデプロイします(コントローラーから SSH 接続しません)。L3 シークレットは tmpfs 上にのみ置き、Docker シークレットとしてマウントするため、docker inspect にも .env にも表示されません。
  • トリガー(SSH 版との使い分け):
    • エージェント(このスキル、推奨): ターゲット上でインバウンドエージェントが実行されます → sh ステップはローカルで実行され、必要なのは Secret file クレデンシャル(クレデンシャルバンドル)のみです。コントローラーから外向きの SSH 接続はありません。
    • SSH 版: コントローラーから ssh/scp/rsync でターゲットに接続します(SSH Username with private key クレデンシャルが必要です)。ターゲット上でエージェントを実行できない場合にのみ使用してください。
    • ユーザーが「jenkins agent 経由でデプロイ」「SSH なしのデプロイ」「SSH の代わりにエージェント」「docker secrets L3」「docker inspect から secret を隠す」「secret をディスクに置かない」と言っている場合、または Linux ホストに docker-compose をデプロイする Jenkins パイプラインを構築している場合。
  • 対象外: SSH 版は構築しません(エージェントを実行できない場合に限ります)。シークレットを読み込むようアプリケーションコードを変更しません(エントリーポイントシムが処理します)。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 を使用し、アプリコンテナーはエントリーポイントシムを使用するため、アプリケーションコードの変更は不要です(ミドルウェアでフレームワークがシークレットモジュールをインポートする際の Edge ランタイムの fs トラップを回避します)。

入力と出力の契約
FieldRequired?意味
InJenkins コントローラーから :8080 + :50000 へターゲットが接続可能はいエージェントが接続できる必要があります
Inターゲット Linux:Java 21、docker グループに属するユーザー + sudo NOPASSWDはいGotchas 1、4 を参照
InSecret file クレデンシャル(クレデンシャルバンドル)はいSSH クレデンシャルは不要です
Inアプリのリポジトリに docker-compose.yml、deploy.py(.example)、Dockerfile + secrets-entrypoint.sh があるはいSCM checkout または既存のワークスペース経由
Inサービス選択チェック(WEB/ETL/DB/NGINX/ALL)いいえ選択的デプロイ。デフォルトは全体
Outターゲット上でスタックが実行中はいビルドコンソールに Running on <target>
Out検証シグナルはいすべてのコンテナーで CLEAN <c> + MOUNT <c>、スモークテスト 200、クレデンシャルバンドルは shred 済み
ルールと機能
  • RULE-01(必須):environment: の compose 経由でシークレットを渡してはいけません(docker inspect に表示され、.env に書き込まれます)。
  • RULE-02(必須):シークレットファイルを 0600 で保存してはいけません(root 以外のコンテナーから読み取れません)。0444 + ディレクトリ 0700 を使用してください。
  • RULE-03(必須):エージェントを実行できる場合、SSH 版に頼ってはいけません。エージェントなら SSH を閉じたままローカルで実行できます。
  • RULE-04(必須):コントローラーから参照できないパスを file:// SCM URL に指定してはいけません。ネットワーク上の git URL を使用してください。
  • RULE-05(必須):Gotchas 1~5(それぞれ実際の実行を一度は失敗させています)をセットアップ/パイプラインに組み込む必要があります。
  • 機能:Jenkins のノード/クレデンシャル/ジョブの設定、ターゲット上でのシェル + docker + sudo の実行、git リポジトリ(SCM)の読み取り。コントローラーから外向きの SSH 接続は開きません。
失敗の境界
  • ターゲット上でエージェントを実行できない → 対象外です。SSH 版を使用してください(このスキルでは ブロック)。
  • Verify ステージで LEAK <c> または NOMOUNT <c> → パイプラインは 失敗(exit 1)となり、デプロイ完了とはみなしません。
  • エージェントが UnsupportedClassVersionError / docker-sudo の permission denied / シークレット 0600 で停止 → 失敗です。Gotchas の表に従って修正し、再実行してください。
  • private リポジトリに deploy key / known_hosts が未設定 → checkout で ブロックされます。

HOW

メインワークフロー
StepTypeInputsActionOutputs/exitFailure/next
W01判定target のインフラエージェント(優先)と SSH バリアントのどちらを使うか選択決定エージェントを実行できない → 停止し、SSH バリアントを使用
W02実行Jenkins + targetインバウンドエージェントを一度だけセットアップ:ノード、Java 21、agent.jar、docker+sudo ユーザー、Secret file 認証情報エージェントオンライン落とし穴 1/4 → 修正して再試行
W03実行アプリのリポジトリ各イメージに secrets-entrypoint.sh を組み込む(ENTRYPOINT は CMD より前);プロビジョナーが secret を 0444/0700 で書き込むイメージ + プロビジョナー—
W04実行ジョブパイプラインテンプレート(Sync → Deploy → Verify)、SCM からコードを取得ジョブ実行checkout エラー → B02
W05決定的ビルド成功を検証(実際のシグナル)Running on <target>、CLEAN、MOUNT、smoke 200、認証情報を完全消去LEAK/NOMOUNT → 失敗

各ステップの詳細(W01~W05 の信頼できる情報源):

インバウンドエージェントのセットアップ(一度だけ)
  1. Jenkins → Manage Jenkins → Nodes → New Node:Permanent Agent、label = <target>、 Remote root = /home/<user>/jenkins-agent、Launch = inbound (JNLP);secret をコピーします。
  2. target 上で(エージェントがコントローラーの :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 を持つユーザーで実行します。 本番環境では、nohup を systemd ユニット(Restart=always)に置き換えてください。
  3. 認証情報:Secret file 認証情報を 1 つ(認証情報バンドル)。SSH 認証情報は不要です。
パイプラインテンプレート(宣言型)
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 でワークスペースのコードを用意するか、エージェントのワークスペースにあらかじめ配置します。

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 を実行します。

落とし穴(それぞれ実際の実行を失敗させる要因です。あらかじめ対処してください)
#症状修正
1エージェントが UnsupportedClassVersionError: class 65.0 … up to 61.0 で終了するJenkins 2.5xx の agent.jar には 17 ではなく Java 21 が必要です。
2アプリコンテナーが Permission denied /run/secrets/* で再起動を繰り返すCompose の非 swarm モードでは、ソースファイルのパーミッションがバインドマウントに引き継がれます。非 root のコンテナーユーザーが読み取れるよう、secret ファイルを 0444(ディレクトリは 0700)で書き込みます。
3shred /dev/shm/.../cred: Permission deniedJenkins の file 認証情報は **0400(読み取り専用)**で渡されます。cp してから chmod 600 を実行し、`shred -uf …
4パイプライン内で docker/sudo が「permission denied」になるエージェントプロセスのユーザーは docker グループに所属し、sudo NOPASSWD を持つ必要があります。
5再起動後にコンテナーが secret のマウントに失敗する/run は tmpfs のため、再起動時に secret は消去されます。compose up の前に secret を再生成するため、デプロイを再実行する(または起動時フックを使う)必要があります。
成功を検証(実際のシグナル)
  • ビルドコンソールに Running on <target> と表示されます(コントローラーではなくエージェント上で実行されたことを証明します)。
  • すべてのコンテナーについて CLEAN <c>(secret が docker inspect に存在しない)。
  • MOUNT <c>(/run/secrets に secret が存在する)。
  • アプリのスモークテスト(例:ログイン)が 200 を返します。
  • 認証情報バンドルが /dev/shm からシュレッドされている(ディスク上に残っていない)。
分岐
IDKindGuardHành viSkip / failureRejoin
B01ユーザー任意すべてではなく、いくつかのサービスだけをデプロイする(WEB/ETL/DB/NGINX にチェックし、ALL は選ばない)選択的デプロイ:booleanParam → deploy.py --services "$SVC" + --no-deps;NGINX の場合は専用プロキシを docker restartALL → 全体を実行;何もチェックしない → none = secret のみ、起動なしW05
B02条件付き必須コードを git から取得する(本番環境:Pipeline script from SCM)SCM を設定;プライベートリポジトリの場合 → コントローラーとエージェントの両方に SSH デプロイキー + known_hosts を設定ラボに git サーバーがない場合 → エージェントで git clone file:///…/repo.git を実行W04
B03復旧target が再起動(/run tmpfs から secret が消去される)compose up の前に secret を再生成するため、デプロイを再実行する(または起動時フックを使う)—W05
参考 — 分岐の詳細
選択的なデプロイ(一部のサービスのみをデプロイし、すべてはデプロイしない)

booleanParam のチェックボックス(WEB/ETL/DB/NGINX/ALL)を表示し、選択した Compose サービスをプロビジョナーに渡します。--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 はエージェントのワークスペースに checkout scm を自動実行し、その後パイプラインが実行されます。git URL はコントローラー(Jenkinsfile の取得)とエージェント(ワークスペースのチェックアウト)の両方から到達可能である必要があります。file:// は、そのファイルを所有する側でのみ機能します。git サーバーのないラボ環境では、エージェント上で git clone file:///…/repo.git を実行してください。

プライベートリポジトリ(GitHub でテスト済み。ビルド SUCCESS):

  • プライベートリポジトリへの HTTPS 匿名クローンは失敗します。認証が必要です。
  • リポジトリで読み取り専用の SSH デプロイキーを使います。秘密鍵を Jenkins の SSH Username with private key 認証情報(ユーザー名 git)に登録し、その credentialsId を指定して SCM URL を [email protected]:owner/repo.git にします。
  • ホストキーに関する注意点: コントローラーとエージェントの両方で github.com を known_hosts に追加する(ssh-keyscan github.com >> ~/.ssh/known_hosts)か、git のホストキー戦略を「Accept first connection」に設定してください。そうしないと、SSH クローンはホストキーの検証に失敗します。
  • GitHub に登録したデプロイキーの公開鍵は、Jenkins の秘密鍵と一致している必要があります(ssh-keygen -lf key.pub のフィンガープリントと GitHub に表示されるものを比較してください)。
検証と停止

Verify ステージは確定的なゲートです。LEAK/NOMOUNT がある場合は exit 1 になります。人が確認する項目:コンソールの Running on <target>、スモークログインの 200、/dev/shm に認証情報が残っていないこと。Verify success の 5 つのシグナルがそろったら停止します。エラーが Gotchas の表に該当する場合は、該当する Fix の行を修正してビルドを再実行してください。表にない原因を推測しないでください。

例
  • 成功例: 給与計算アプリ(web/etl/postgres)— エージェントラベル <target>、Java 21、docker グループに属し sudo NOPASSWD が設定されたユーザー、ビルドで ALL にチェック → コンソールに Running on <target>、すべてのコンテナで CLEAN/MOUNT、ログイン 200、認証情報バンドルが /dev/shm から完全に shred されていること。
  • 境界条件/失敗例: シークレットを 0600 で書き込む → web コンテナが Permission denied /run/secrets/* でクラッシュループ(gotcha 2)→ プロビジョナーが 0444 に変更し、ディレクトリを 0700 に設定して再実行。初回デプロイ時に WEB のみチェック → --no-deps により web だけが起動し、etl/postgres がない → 連鎖全体 web → etl → postgres(または ALL)にチェック。

コメント