この記事は「Proxmox自動インストール&CI/CD構築」シリーズの Part 2 です。
Part 1はこちら → OCI / AWS 無料枠にGitLab + Runnerを立てる【Part 1】
はじめに
Part 1では、OCI / AWSの無料枠にGitLab CEと動作確認用のRunnerをOCI VM上に立てました。
Part 2(この記事)では、実際にGitLab CI/CDパイプラインを組んで、「New pipeline」を押すだけでProxmox VEが自動インストールされる環境を作ります。
パイプラインの実行には、自宅LAN内に新たにローカルRunnerを追加します。 OCI上のRunnerは自宅LANに直接アクセスできないため、自宅LANに常時接続しているマシンを使います。私の場合は勉強用に購入した Raspberry Pi 4B を使っていますが、自宅LANに接続されたLinuxマシンであれば何でも構いません。
リモートからの電源制御には NanoKVM-PCIe を使います。NanoKVMはIPMIプロトコルに対応しているため、ipmitool コマンドで操作できます。これはHPE iLOやDell iDRACと同じコマンド体系です。
NanoKVM-PCIeとは:
Sipeed社が開発した約1万円のIP-KVMデバイスです。PCIeスロットに挿してケース内に収まり、HDMI画面キャプチャ・USB HIDエミュレーション・ATX電源制御・仮想USBストレージをネットワーク経由で操作できます。PCIeスロットは電源供給のみで、データ通信はHDMI・USB・LANケーブルを内部で接続する構造です。
全体構成
git push(VSCode)
→ OCI GitLab CE(リポジトリ / CI/CD変数 / WebUI)
↓ ポーリング(HTTPS、ポート開放不要)
Raspberry Pi(GitLab Runner)
├─ Stage 1: answer.toml生成 → NanoKVMにSCP → HTTP server起動
├─ Stage 2: ipmitool電源ON → デスクトップがインストール
├─ Stage 3: HTTP server停止 → SSH疎通待ち
└─ Stage 4: Ansible ポストインストール設定
NanoKVMのネットワーク構成と answer.toml 配信
今回最も独特な点が、answer.tomlをNanoKVM上のHTTPサーバーから配信することです。
NanoKVM-PCIeはデスクトップPCにUSBで接続されると、10.27.94.x のプライベートネットワークを提供します。インストール中のデスクトップPCはこのUSB NICから 10.27.94.100 を取得します。
インストール開始直後はUSB NICしかアクティブでないため、インストーラーが answer.toml を取りに行けるのは 10.27.94.1(NanoKVM自身)だけです。Raspberry Pi(192.168.100.101)には届きません。
そのため:
-
ISOに埋め込むURL →
http://10.27.94.1:8080/answer.toml(固定) - HTTPサーバー → NanoKVM上で動かす(PiからSSHで起動)
- 通常のLAN → インストール完了後の Ansible SSH に使う
iLO / iDRAC環境の場合:
iLO / iDRAC ではこのUSB NICの問題はありません。インストーラーはサーバー本来のNICでネットワークを取得するため、RunnerマシンのIPをISOに埋め込んでそのままRunner上でHTTPサーバーを動かせます。NanoKVM特有の対応です。
リポジトリ構成
この記事で作成するファイルはすべて GitLabリポジトリのルート に置きます。
homelab-iac/ ← GitLabリポジトリのルート
├── .gitlab-ci.yml ← パイプライン定義
├── hosts.yml ← ホスト定義ファイル
├── answer.toml.template ← Proxmoxインストール設定テンプレート
├── serve.py ← NanoKVM上で動かすHTTPサーバー
├── playbooks/
│ └── post-install.yml ← Ansibleポストインストールplaybook
└── tools/
└── ilo-disk-survey.py ← iLO Redfish APIディスク調査スクリプト(任意)
前提条件
- Part 1の手順でGitLab CE + OCI上のRunnerがセットアップ済み
- NanoKVM-PCIeがデスクトップPCに取り付け済み(ATX電源ケーブル・HDMI・USB-HID接続済み)
- 自宅LAN内にRunner用のLinuxマシンがある(
ipmitoolとansibleが動けばOK)
iLO / iDRAC搭載サーバーをお持ちの方へ:
セクション1(NanoKVMのセットアップ)はスキップし、CI/CD変数の KVM_PASS をiLO/iDRACのIPMIパスワードに置き換えてください。ipmitool コマンドは共通です。また、セクション2でISOのURLはRunner自身のIPにできます(10.27.94.x の問題がないため)。
さらに、iLO環境では ipmitool chassis bootdev cdrom でブートデバイスをリモートから制御できる場合があります。対応している場合はパイプラインの boot ステージに追加することで、BIOSの手動設定が不要になります。
1. NanoKVMのセットアップ
1.1 取り付けと配線
NanoKVM-PCIeの概要を動画で確認したい方へ:
セットアップの参考にした動画です。ハードウェアの概要や取り付けのイメージをつかむのに役立ちます。
デスクトップPCの電源を切り、以下を接続します。
| 接続 | 内容 |
|---|---|
| PCIeスロット | NanoKVM本体を挿入(電源供給のみ、データ通信なし) |
| HDMI | PCのHDMI出力 → NanoKVMのHDMI入力 |
| USB-HID | PCの内部USB 2.0ヘッダー or 背面USB → NanoKVMのUSB-HIDポート |
| ATX電源 | マザーボードの9ピンパワーヘッダー → NanoKVMのリボンケーブル |
| Ethernet | NanoKVMのLANポート → スイッチ/ルーター |
取り付け後、PCの電源を入れるとNanoKVMのOLEDにIPアドレスが表示されます。
※正直全然見えない。。
1.2 WebUIアクセスとパスワード変更
ブラウザで http://<NANOKVM_IP> を開きます。デフォルトのユーザー名・パスワードはともに admin です。ログイン後、必ずパスワードを変更してください。
1.3 システムイメージのバージョン確認
NanoKVMにSSHでログインしてバージョンを確認します。
# WSL上で実行
ssh root@<NANOKVM_IP>
# デフォルトSSHパスワード: root
cat /boot/ver
# 例:2025-04-17-... ← 古い(v1.4.2より前)→ 焼き直しが必要
# 例:2026-01-23-... ← OK(v1.4.2以降)
古い場合はWSL上でイメージをダウンロードし、Windows側の balenaEtcher でSDカードに書き込みます。
# WSL上で実行
wget https://github.com/sipeed/NanoKVM/releases/download/v1.4.2/20260123_NanoKVM_Rev1_4_2.img.xz
xz -d 20260123_NanoKVM_Rev1_4_2.img.xz
# → \\wsl$\Ubuntu\home\<ユーザー名>\ にあるimgファイルをbalenaEtcherで書き込む
書き込み後はWebUIのパスワード変更など初期設定をやり直してください。
1.4 IPMIサービスの有効化
# NanoKVM上で実行(SSH接続中)
/etc/ipmi/ipmi-sim.sh enable
/etc/ipmi/ipmi-sim.sh start
/etc/ipmi/ipmi-sim.sh status
# → [OK] ipmi_sim is running (PID: XXXX)
# Listening on: UDP port 623
1.5 IPMIパスワードの変更
パスワードは英数字のみにしてください。 記号(@ など)が含まれるとGitLabのMasked変数として登録できない場合があります。
# NanoKVM上で実行
vi /etc/ipmi/lan.conf
ファイル末尾のユーザー設定を変更します。
user 2 true "admin" "<英数字のみのパスワード>" admin 10 md5
/etc/ipmi/ipmi-sim.sh restart
1.6 RunnerマシンからNanoKVMへのSSH鍵設定
パイプラインがNanoKVMにSSHするために、RunnerマシンのSSH鍵をNanoKVMに登録します。
# Raspberry Pi上で実行
sudo -u gitlab-runner ssh-keygen -t ed25519 \
-f /home/gitlab-runner/.ssh/nanokvm_key -N ""
# 公開鍵をNanoKVMに登録
sudo cat /home/gitlab-runner/.ssh/nanokvm_key.pub | \
ssh -o StrictHostKeyChecking=no root@<NANOKVM_IP> \
"mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"
# 動作確認
sudo -u gitlab-runner ssh \
-i /home/gitlab-runner/.ssh/nanokvm_key \
-o StrictHostKeyChecking=no root@<NANOKVM_IP> "echo ok"
# → ok
1.7 動作確認(ipmitool)
# WSL上で実行(またはRaspberry Pi上)
ipmitool -H <NANOKVM_IP> -U admin -P <パスワード> -I lanplus -C 3 power status
# → Chassis Power is on (または off)
ipmitool -H <NANOKVM_IP> -U admin -P <パスワード> -I lanplus -C 3 power on
# → Chassis Power Control: Up/On
NanoKVMのWebUIにもデスクトップの画面が映ります。
2. BIOSのブート順序設定(初回のみ・手動)
iLO / iDRAC環境の場合:
ipmitool chassis bootdev cdrom コマンドが機能する場合、この手動BIOS操作は不要です。パイプラインのbootステージに以下を追加するだけでリモートからブートデバイスを指定できます。
ipmitool -H "$TARGET_KVM_HOST" -U admin -P "$KVM_PASS" -I lanplus chassis bootdev cdrom
NanoKVMではこのコマンドがサポートされていないため、初回のみ手動設定が必要です。
3. 汎用ISOの事前ビルドとNanoKVMへの転送(WSL上で1回だけ)
proxmox-auto-install-assistant はx86_64専用バイナリです。WSL上で1回だけ実行します(Raspberry PiはARM64のため実行できません)。
3.1 ツールのインストール(WSL上)
# WSL上で実行
echo "deb http://download.proxmox.com/debian/pve bookworm pve-no-subscription" \
| sudo tee /etc/apt/sources.list.d/pve-install-repo.list
sudo wget https://enterprise.proxmox.com/debian/proxmox-release-bookworm.gpg \
-O /etc/apt/trusted.gpg.d/proxmox-release-bookworm.gpg
sudo apt update
sudo apt install -y proxmox-auto-install-assistant
3.2 ISOのダウンロードとビルド(WSL上)
# WSL上で実行
wget https://enterprise.proxmox.com/iso/proxmox-ve_9.1-1.iso
# URLは固定:NanoKVMのUSBネットワーク上のIP(10.27.94.1)
proxmox-auto-install-assistant prepare-iso proxmox-ve_9.1-1.iso \
--fetch-from http \
--url http://10.27.94.1:8080/answer.toml \
--output proxmox-ve-9.1-autoinstall.iso
このISOは「起動したら 10.27.94.1:8080/answer.toml を取得する」という動作だけが入っています。answer.tomlの内容(IP・ホスト名・ディスク等)を変えてもISOの再ビルドは不要です。
3.3 NanoKVMへの転送とマウント(WSL上)
# WSL上で実行
# NanoKVMはISOを /data ディレクトリで管理する
scp proxmox-ve-9.1-autoinstall.iso root@<NANOKVM_IP>:/data/
NanoKVMのWebUIから仮想ディスクメニューを開き、アップロードしたISOを選択してマウントします。
4. ローカルRunnerのセットアップ(Raspberry Pi上)
Part 1で作ったRunnerとの関係:
Part 1ではOCI VM上にRunnerを立てました。このRunnerは自宅LAN内のNanoKVMやProxmoxに直接アクセスできません。
Part 2では自宅LAN内のRaspberry Piに新たにRunnerを追加します。タグ local を付けてパイプラインがこちらのRunnerで実行されるようにします。
4.1 必要パッケージのインストール(Raspberry Pi上)
# Raspberry Pi上で実行
sudo apt update
sudo apt install -y curl ansible ipmitool python3-pip python3-yaml
4.2 GitLab Runnerのインストールと登録(Raspberry Pi上)
# Raspberry Pi上で実行
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | sudo bash
sudo apt install -y gitlab-runner
GitLabプロジェクト → Settings → CI/CD → Runners → New project runner
Tagsフィールドに local と入力し、Create runner をクリックします。
sudo を付けて登録してください。 sudo なしだとユーザーモードになり、セッションが切れるとRunnerが停止します。
# Raspberry Pi上で実行(sudo必須)
sudo gitlab-runner register \
--url http://<GitLab_IP> \
--token <コピーしたトークン>
# 対話式入力
# Enter a name for the runner: local-runner(任意)
# Enter an executor: shell
# 登録確認・起動
sudo gitlab-runner list
sudo gitlab-runner start
なぜポート開放が不要か:
GitLab RunnerはGitLabに対してHTTPSでポーリング(pull型)する仕組みです。Pi側からのアウトバウンド通信のみで動作するため、自宅ルーターのポート開放は不要です。
5. リポジトリファイルの作成
以下のファイルをVSCodeで自分のリポジトリのルートディレクトリに作成します。
5.1 hosts.yml
hosts:
pve01:
ip: "192.168.100.50" # 自分の環境に合わせて変更
gateway: "192.168.100.1" # 自分の環境に合わせて変更
hostname: "pve01"
os_disk: "ata-ADATA_SU650_4N3523F2TF8P" # 自分のディスクのシリアル(後述)
net_mac: "enx7085c22d814d" # 自分のNICのMAC(後述)
kvm_host: "192.168.1.9" # NanoKVM or iLO/iDRACのIP
kvm_type: "nanokvm" # "nanokvm" または "ilo"
# ceph_osds:
# - "ata-Samsung_SSD_870_EVO_500GB_AAA"
os_disk の確認方法(デスクトップPC上またはインストーラーのシェルから):
# デスクトップPC上で実行
ls -l /dev/disk/by-id/ | grep -v part
lrwxrwxrwx 1 root root 9 ata-ADATA_SU650_4N3523F2TF8P -> ../../sdc
lrwxrwxrwx 1 root root 9 ata-HGST_HUS722T2TALA604_WCC6N1XR2312 -> ../../sdb
lrwxrwxrwx 1 root root 9 ata-HGST_HUS722T2TALA604_WMC6N0R544C1 -> ../../sda
lrwxrwxrwx 1 root root 9 ata-TOSHIBA_DT01ACA100_178RP4YFS -> ../../sdd
lrwxrwxrwx 1 root root 9 usb-NanoKVM_USB_Mass_Storage_0123456789ABCDEF-0:0 -> ../../sde
なぜ /dev/sda ではなく filter.ID_SERIAL を使うのか
/dev/sda はカーネルの検出順で動的に変わります。複数ディスク環境で誤ったディスクにインストールされるとデータが全消しです。シリアル番号は固有値なので、物理的に同じディスクを確実に特定できます。
net_mac の確認方法(インストーラーのシェルから):
# インストーラーのシェル上で実行
udevadm info /sys/class/net/enp0s31f6 | grep ID_NET_NAME_MAC
# 例: E: ID_NET_NAME_MAC=enx7085c22d814d
NICの名前(enp0s31f6 等)やMACアドレスはマザーボードによって異なります。 確認した ID_NET_NAME_MAC の値をそのまま net_mac に使ってください。
管理台数が増えてきたら:
今回は手動で確認して hosts.yml に書きましたが、管理するホスト数が増えたり、定期的に複数台を再インストールするような運用になってくると、このあたりを見直す余地があると思います。
例えば現在稼働中の各ホストで ls -l /dev/disk/by-id/ の出力を定期的にスクリプトで収集しておけば、ディスク交換前後のシリアル番号の突合が楽になります。iLO環境なら Redfish API でリモートから取得できるので、パイプライン自体に「インベントリ更新ステージ」を組み込んで、インストール前に最新のディスク情報を自動で hosts.yml に反映する構成も考えられます。
あるいは hosts.yml を直接管理するのではなく、inventory/ ディレクトリにホストごとのYAMLを分けて置いておき、スクリプトでマージして生成する形にすれば、複数人でのホスト管理もやりやすくなります。この記事の構成は1台から始める最小構成なので、台数や運用スタイルに合わせて自分なりのやり方を探ってみてください。
5.2 answer.toml.template
Proxmox VE 9.0以降はkebab-caseが必須です。
root_password → root-password、disk_list → filter.ID_SERIAL のように変更されています。旧形式(snake_case)は9.0以降で非推奨となり、将来エラーになります。
[global]
keyboard = "jp"
country = "jp"
fqdn = "${TARGET_HOSTNAME}.local"
mailto = "root@localhost"
timezone = "Asia/Tokyo"
root-password = "${PROXMOX_ROOT_PW}"
root-ssh-keys = ["${SSH_PUBLIC_KEY}"]
[network]
source = "from-answer"
cidr = "${TARGET_IP}/24"
dns = "1.1.1.1"
gateway = "${TARGET_GATEWAY}"
filter.ID_NET_NAME_MAC = "${TARGET_NET_MAC}"
[disk-setup]
filesystem = "ext4"
filter.ID_SERIAL = "${TARGET_DISK_SERIAL}"
5.3 serve.py
Proxmoxのインストーラーはanswer.tomlをHTTP POSTリクエストで取得します。Pythonの標準HTTPサーバーはGETのみ対応のため、専用スクリプトが必要です。
from http.server import HTTPServer, BaseHTTPRequestHandler
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
self._serve()
def do_POST(self):
self._serve()
def _serve(self):
if self.path == '/answer.toml':
with open('/tmp/answer.toml', 'rb') as f:
data = f.read()
self.send_response(200)
self.send_header('Content-Type', 'text/plain')
self.end_headers()
self.wfile.write(data)
else:
self.send_response(404)
self.end_headers()
def log_message(self, fmt, *args):
print(f"{self.address_string()} - {fmt % args}")
HTTPServer(('0.0.0.0', 8080), Handler).serve_forever()
5.4 playbooks/post-install.yml
---
- name: Proxmox VE post-install configuration
hosts: all
become: true
gather_facts: true
vars:
ansible_user: root
ansible_ssh_common_args: '-o StrictHostKeyChecking=no'
tasks:
# -----------------------------------------------
# リポジトリ設定(PVE 9 / trixie / DEB822形式)
# -----------------------------------------------
- name: Disable pve-enterprise repository
lineinfile:
path: /etc/apt/sources.list.d/pve-enterprise.sources
insertafter: 'Components:'
line: 'Enabled: no'
state: present
ignore_errors: true
- name: Disable ceph enterprise repository
lineinfile:
path: /etc/apt/sources.list.d/ceph.sources
insertafter: 'Components:'
line: 'Enabled: no'
state: present
ignore_errors: true
- name: Add pve-no-subscription repository (trixie)
copy:
content: |
deb http://download.proxmox.com/debian/pve trixie pve-no-subscription
dest: /etc/apt/sources.list.d/pve-no-subscription.list
# -----------------------------------------------
# パッケージ更新
# -----------------------------------------------
- name: Update apt cache
apt:
update_cache: true
- name: Full system upgrade
apt:
upgrade: dist
autoremove: true
# -----------------------------------------------
# 追加パッケージ(任意)
# -----------------------------------------------
- name: Install monitoring and logging packages
apt:
name:
- zabbix-agent2
- rsyslog
state: present
- name: Add Tailscale GPG key
shell: |
curl -fsSL https://pkgs.tailscale.com/stable/debian/trixie.noarmor.gpg \
| tee /usr/share/keyrings/tailscale-archive-keyring.gpg > /dev/null
args:
creates: /usr/share/keyrings/tailscale-archive-keyring.gpg
- name: Add Tailscale repository
copy:
content: |
deb [signed-by=/usr/share/keyrings/tailscale-archive-keyring.gpg] https://pkgs.tailscale.com/stable/debian trixie main
dest: /etc/apt/sources.list.d/tailscale.list
- name: Update apt cache for Tailscale
apt:
update_cache: true
- name: Install Tailscale
apt:
name: tailscale
state: present
# -----------------------------------------------
# 動作確認
# -----------------------------------------------
- name: Verify Proxmox VE version
command: pveversion
register: pve_version
- name: Display PVE version
debug:
msg: "{{ pve_version.stdout }}"
- name: Check Proxmox WebUI is accessible
uri:
url: "https://{{ ansible_host }}:8006"
validate_certs: false
status_code: 200
retries: 3
delay: 10
- name: Display result
debug:
msg: "Proxmox VE installed successfully. WebUI: https://{{ ansible_host }}:8006"
Ansibleタスクの拡張について:
post-install.yml は必要なものだけ入れるのが基本です。今回は zabbix-agent2・rsyslog・Tailscale を追加しています。用途に応じてSSH設定の強化、クラスタへの再参加(pvecm add)、ファイアウォール設定なども追加できます。
PVE 8.x以前で使う場合は trixie → bookworm に変更し、.sources ファイルではなく .list ファイルが使われているためリポジトリ無効化の方法が異なります。
6. CI/CD変数の設定
6.1 SSH鍵の生成(Raspberry Pi上)
# Raspberry Pi上で実行
ssh-keygen -t ed25519 -C "gitlab-runner" -f ~/.ssh/id_ed25519 -N ""
# 秘密鍵をbase64エンコード(改行なし・これをコピーする)
cat ~/.ssh/id_ed25519 | base64 -w0
# 公開鍵(これもコピーする)
cat ~/.ssh/id_ed25519.pub
6.2 GitLabへの変数登録
GitLab → プロジェクト → Settings → CI/CD → Variables → Add variable
| 変数名 | 値 | Visibility |
|---|---|---|
PROXMOX_ROOT_PW |
Proxmoxのrootパスワード(英数字のみ) | Masked |
SSH_PRIVATE_KEY |
base64 -w0 の出力(改行なしの文字列) |
Masked |
SSH_PUBLIC_KEY |
id_ed25519.pub の内容 |
Visible |
KVM_PASS |
NanoKVMのIPMIパスワード(英数字のみ) | Masked |
Maskedにできない場合:
GitLabのMasked変数は特定の記号が含まれると登録できません。登録できない場合は Masked and hidden を選択してください。パスワード類は英数字のみにすることを強く推奨します。
TARGET_HOST はパイプライン実行時に毎回入力します。 事前登録は不要です。
7. パイプライン(.gitlab-ci.yml)
workflow:
rules:
- if: $CI_PIPELINE_SOURCE == "web" # pushでの自動実行を無効化
stages:
- prepare
- boot
- wait
- configure
# -----------------------------------------------
# 共通処理:hosts.ymlから対象ホストの情報を読み込む
# -----------------------------------------------
.load_host: &load_host
- |
if [ -z "$TARGET_HOST" ]; then
echo "ERROR: TARGET_HOST is not set. Specify it in New pipeline."
exit 1
fi
eval $(python3 -c "
import yaml, sys
with open('hosts.yml') as f:
hosts = yaml.safe_load(f)['hosts']
if '${TARGET_HOST}' not in hosts:
print('echo \"ERROR: ${TARGET_HOST} not found in hosts.yml\"; exit 1')
sys.exit(1)
h = hosts['${TARGET_HOST}']
disk_serial = h['os_disk'].replace('ata-', '', 1)
print(f'export TARGET_IP={h[\"ip\"]}')
print(f'export TARGET_GATEWAY={h[\"gateway\"]}')
print(f'export TARGET_HOSTNAME={h[\"hostname\"]}')
print(f'export TARGET_NET_MAC={h[\"net_mac\"]}')
print(f'export TARGET_DISK_SERIAL={disk_serial}')
print(f'export TARGET_KVM_HOST={h[\"kvm_host\"]}')
print(f'export KVM_TYPE={h[\"kvm_type\"]}')
osds = h.get('ceph_osds', [])
print(f'export CEPH_OSDS=\"{\" \".join(osds)}\"')
")
echo "Target: $TARGET_HOSTNAME ($TARGET_IP) disk=$TARGET_DISK_SERIAL kvm=$TARGET_KVM_HOST type=$KVM_TYPE"
# -----------------------------------------------
# Stage 1: answer.toml生成 + HTTPサーバー起動
# NanoKVM: NanoKVM上にSCPしてHTTPサーバーを起動
# iLO: Runner上でHTTPサーバーを起動
# -----------------------------------------------
prepare:
stage: prepare
tags:
- local
script:
- *load_host
# Ceph OSDとの衝突チェック
- |
for osd in $CEPH_OSDS; do
if [ "$TARGET_DISK_SERIAL" = "$osd" ]; then
echo "ERROR: $TARGET_DISK_SERIAL is a Ceph OSD. Aborting."
exit 1
fi
done
# answer.tomlにCI/CD変数を埋め込む
- envsubst < answer.toml.template > /tmp/answer.toml
- echo "=== Generated answer.toml ===" && cat /tmp/answer.toml
- |
if [ "$KVM_TYPE" = "nanokvm" ]; then
NANOKVM_KEY=/home/gitlab-runner/.ssh/nanokvm_key
# NanoKVMにanswer.tomlとserve.pyを転送
scp -i $NANOKVM_KEY -o StrictHostKeyChecking=no \
/tmp/answer.toml root@${TARGET_KVM_HOST}:/tmp/answer.toml
scp -i $NANOKVM_KEY -o StrictHostKeyChecking=no \
serve.py root@${TARGET_KVM_HOST}:/tmp/serve.py
# NanoKVM上でHTTPサーバーをバックグラウンド起動
ssh -i $NANOKVM_KEY -o StrictHostKeyChecking=no root@${TARGET_KVM_HOST} \
"nohup python3 /tmp/serve.py > /tmp/serve.log 2>&1 & echo \$! > /tmp/serve.pid && echo 'HTTP server started'"
else
# iLO環境: Runner上でHTTPサーバーを起動
sudo fuser -k 8080/tcp 2>/dev/null || true
cd /tmp && python3 -m http.server 8080 &
echo $! > /tmp/http_server.pid
echo "HTTP server started on Runner (:8080)"
fi
# -----------------------------------------------
# Stage 2: リモートブート(ipmitool)
# -----------------------------------------------
boot:
stage: boot
tags:
- local
script:
- *load_host
# 電源OFF(-N 5 -R 1 でタイムアウト短縮してIPMIセッションを早く解放)
- ipmitool -H "$TARGET_KVM_HOST" -U admin -P "$KVM_PASS" -I lanplus -C 3 -N 5 -R 1 power off || true
# セッションが完全に解放されるまで待機(重要)
- sleep 60
# 電源ON
- ipmitool -H "$TARGET_KVM_HOST" -U admin -P "$KVM_PASS" -I lanplus -C 3 power on
- echo "Power ON sent. Waiting for installation..."
# Proxmoxの無人インストール完了を待つ
# SSDで4分程度だが、HDD環境や低スペックでは長くかかる場合がある
- sleep 240
# -----------------------------------------------
# Stage 3: HTTPサーバー停止 + SSH接続待ち
# -----------------------------------------------
wait:
stage: wait
tags:
- local
before_script:
- mkdir -p ~/.ssh
- echo "$SSH_PRIVATE_KEY" | base64 -d > ~/.ssh/id_ed25519
- chmod 600 ~/.ssh/id_ed25519
script:
- *load_host
# HTTPサーバーを停止(インストール後の再起動でUSBからブートしないよう)
- |
if [ "$KVM_TYPE" = "nanokvm" ]; then
NANOKVM_KEY=/home/gitlab-runner/.ssh/nanokvm_key
ssh -i $NANOKVM_KEY -o StrictHostKeyChecking=no root@${TARGET_KVM_HOST} \
"if [ -f /tmp/serve.pid ]; then kill \$(cat /tmp/serve.pid) 2>/dev/null || true; rm /tmp/serve.pid; echo 'HTTP server stopped'; fi"
else
if [ -f /tmp/http_server.pid ]; then
kill $(cat /tmp/http_server.pid) 2>/dev/null || true; rm /tmp/http_server.pid
fi
fi
# ProxmoxへのSSH接続待ち(最大30分)
- |
echo "Waiting for SSH on ${TARGET_IP}..."
for i in $(seq 1 60); do
if ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 \
-i ~/.ssh/id_ed25519 root@${TARGET_IP} 'echo ok' 2>/dev/null; then
echo "SSH is ready!"
exit 0
fi
echo "Attempt $i/60 - waiting 30s..."
sleep 30
done
echo "ERROR: Timeout waiting for SSH"
exit 1
# -----------------------------------------------
# Stage 4: ポストインストール設定(Ansible)
# -----------------------------------------------
configure:
stage: configure
tags:
- local
before_script:
- mkdir -p ~/.ssh
- echo "$SSH_PRIVATE_KEY" | base64 -d > ~/.ssh/id_ed25519
- chmod 600 ~/.ssh/id_ed25519
script:
- *load_host
- >
ansible-playbook
-i "${TARGET_IP},"
--private-key ~/.ssh/id_ed25519
-e "target_hostname=${TARGET_HOSTNAME}"
playbooks/post-install.yml
- echo "Done. https://${TARGET_IP}:8006"
workflow: rules について:
この設定により、git push した際にパイプラインが自動で走らなくなります。GitLabの Build → Pipelines → New pipeline ボタンからのみ実行できます。うっかり push のたびに電源ON→インストールが走るのを防ぐためです。
8. GitLabにpushして動作確認
8.1 リポジトリにpush(VSCode上)
git add .
git commit -m "Add Proxmox auto-install pipeline"
git push
8.2 パイプラインを実行
GitLab → プロジェクト → Build → Pipelines → New pipeline(右上のボタン)
Variables に入力します。
Key: TARGET_HOST
Value: pve01
New pipeline をクリックするとパイプラインが実行されます。
Stage 2(boot)の実行中、NanoKVMのWebUIでインストール画面が確認できます。
全ステージが完了したら https://<IP>:8006 にアクセスします。
ここまでのまとめ
この記事(Part 2)では以下を構築しました。
- NanoKVM-PCIeのシステムイメージ更新・IPMI設定・SSH鍵認証の設定
-
--fetch-from http方式(URL:10.27.94.1:8080)による汎用ISOの事前ビルド - Raspberry PiへのGitLab Runner追加登録(ポート開放不要)
-
hosts.ymlによる複数ホスト対応のホスト定義管理 -
filter.ID_SERIAL/filter.ID_NET_NAME_MACを使った安全なディスク・NIC指定 - GitLab CI/CDパイプライン(4ステージ、NanoKVM/iLO分岐対応)
- PVE 9対応のAnsibleポストインストールplaybook
1台の自宅デスクトップPCから始めていますが、hosts.yml にエントリを追加するだけで複数ホストに対応できます。kvm_type: "ilo" にするだけでiLO/iDRAC環境でもほぼ同じパイプラインが動作します。
未解決・今後の課題
- NanoKVM APIを使ったISO仮想マウントの自動化(現在はWebUIから手動マウント)
- NanoKVMの
ipmi_simがchassis bootdevをサポートしていないため、初回のBIOS設定は手動(iLO/iDRAC環境ではchassis bootdev cdromで代替可能) -
sleep 240はSSDの速度を前提にしているため、HDD環境では延長が必要
補足
Cephクラスタ構成の場合:
OS用SSDが故障した場合でも、OSD用SSDのデータはそのまま残っています。OSのみ再インストール後に ceph-volume lvm activate --all で復旧できます。hosts.yml の ceph_osds フィールドにOSD用ディスクのシリアルを列挙しておくと、パイプラインがインストール前に衝突チェックを行います。
iLO環境でのディスク情報取得(参考):
iLO 5/6のRedfish APIを使うと、ホストに直接ログインしなくてもディスクのシリアル番号をリモートで取得できます。セクション5.1で述べたような「インベントリ自動更新」の仕組みを作る際の出発点として。
#!/usr/bin/env python3
"""iLO Redfish APIからディスク一覧を取得するサンプル / pip install requests"""
import json, ssl, base64, getpass
try:
from urllib.request import Request, urlopen
except ImportError:
from urllib2 import Request, urlopen
HOST = "10.0.0.201" # iLOのIP
USER = "Administrator"
PW = getpass.getpass("iLO password: ")
ctx = ssl._create_unverified_context()
AUTH = "Basic " + base64.b64encode(f"{USER}:{PW}".encode()).decode()
def get(path):
req = Request(f"https://{HOST}{path}")
req.add_header("Authorization", AUTH)
req.add_header("Accept", "application/json")
return json.loads(urlopen(req, context=ctx).read().decode())
def dig(obj, *keys):
for k in keys:
if not isinstance(obj, dict) or k not in obj: return None
obj = obj[k]
return obj
storage = get("/redfish/v1/Systems/1/Storage/DA000006/")
print(f"{'SLOT':<12} {'MODEL':<35} {'SERIAL':<25}")
print("-" * 75)
for d in storage.get("Drives", []):
dpath = d.get("@odata.id")
if not dpath: continue
drv = get(dpath)
slot = dig(drv, "PhysicalLocation", "PartLocation", "ServiceLabel") or drv.get("Id") or "N/A"
print(f"{slot:<12} {(drv.get('Model') or 'N/A').strip():<35} {(drv.get('SerialNumber') or 'N/A').strip():<25}")




























