jenkins-agent-l3-deploy について
ターゲットの Linux ホスト上で稼働する Jenkins のインバウンドエージェント経由で、コントローラーから SSH 接続せずに docker-compose をデプロイするスキルです。実行時シークレットを tmpfs に置き、マウント済みで `docker inspect` に現れないことを検証します。
主な機能
- 01ターゲットエージェント上での docker-compose デプロイ
- 02tmpfs へのシークレット配置
- 03Docker secrets としてのマウント
- 04サービスを選択したデプロイ
- 05漏えいやマウント欠落時の検証失敗
活用シーン
- 01コントローラーから SSH せずに compose スタックをデプロイ
- 02実行時シークレットの `.env` や `docker inspect` への露出防止
- 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 トラップを回避します)。
入力と出力の契約
| Field | Required? | 意味 | |
|---|---|---|---|
| In | Jenkins コントローラーから :8080 + :50000 へターゲットが接続可能 | はい | エージェントが接続できる必要があります |
| In | ターゲット Linux:Java 21、docker グループに属するユーザー + sudo NOPASSWD | はい | Gotchas 1、4 を参照 |
| In | Secret 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
メインワークフロー
| Step | Type | Inputs | Action | Outputs/exit | Failure/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 の信頼できる情報源):
インバウンドエージェントのセットアップ(一度だけ)
- Jenkins → Manage Jenkins → Nodes → New Node:Permanent Agent、label =
<target>、 Remote root =/home/<user>/jenkins-agent、Launch = inbound (JNLP);secret をコピーします。 - target 上で(エージェントがコントローラーの
:8080+:50000に到達できる必要があります):
エージェントは、docker グループに所属し、かつ sudo NOPASSWD を持つユーザーで実行します。 本番環境では、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 &nohupを systemd ユニット(Restart=always)に置き換えてください。 - 認証情報:
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)で書き込みます。 |
| 3 | shred /dev/shm/.../cred: Permission denied | Jenkins の 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からシュレッドされている(ディスク上に残っていない)。
分岐
| ID | Kind | Guard | Hành vi | Skip / failure | Rejoin |
|---|---|---|---|---|---|
| B01 | ユーザー任意 | すべてではなく、いくつかのサービスだけをデプロイする(WEB/ETL/DB/NGINX にチェックし、ALL は選ばない) | 選択的デプロイ:booleanParam → deploy.py --services "$SVC" + --no-deps;NGINX の場合は専用プロキシを docker restart | ALL → 全体を実行;何もチェックしない → 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)にチェック。