0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

NanoKVM + ipmitoolでProxmoxインストールとセットアップを完全リモート自動化する【Proxmox自動インストール Part 2】

0
Last updated at Posted at 2026-03-28

この記事は「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ケーブルを内部で接続する構造です。


全体構成

image.png

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サーバーから配信することです。

image.png

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に埋め込むURLhttp://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マシンがある(ipmitoolansible が動けば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ポート → スイッチ/ルーター

image.png

image.png

取り付け後、PCの電源を入れるとNanoKVMのOLEDにIPアドレスが表示されます。
※正直全然見えない。。

image.png

1.2 WebUIアクセスとパスワード変更

ブラウザで http://<NANOKVM_IP> を開きます。デフォルトのユーザー名・パスワードはともに admin です。ログイン後、必ずパスワードを変更してください。

image.png

1.3 システムイメージのバージョン確認

①NanoKVMのIPMI機能はシステムイメージ v1.4.2 以降でのみ利用可能です。

WebUIのUpdate画面に表示されるアプリバージョン(2.3.x)とは別に、SDカードのシステムイメージのバージョンが重要です。古いイメージでは /etc/ipmi/ ディレクトリ自体が存在しません。

②NanoKVMはデフォルトではSSHの機能はOFFになってます

WebUIで下記のようにSSHのトグルがあるので手動でONにしておく必要があります。
image.png

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

image.png

NanoKVMのWebUIにもデスクトップの画面が映ります。

image.png


2. BIOSのブート順序設定(初回のみ・手動)

この手順は初回の1回だけ必要です。

NanoKVM-PCIeの仮想USBをブートデバイスとして認識させるため、対象PCのBIOSでUSBをSSDより優先するように設定します。NanoKVMのWebUIからBIOS操作画面にアクセスできます。

Proxmoxインストール完了後はBIOSのブート順序がSSD優先に自動的に変更されるため、2回目以降は設定不要です。仮想USBがマウントされていない場合は自動的にSSDから起動します。
image.png

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を選択してマウントします。

image.png


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 をクリックします。

image.png

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

image.png

なぜポート開放が不要か:
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

image.png

image.png

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_passwordroot-passworddisk_listfilter.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以前で使う場合は trixiebookworm に変更し、.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

image.png

変数名 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)

image.png

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

image.png

8.2 パイプラインを実行

GitLab → プロジェクト → Build → Pipelines → New pipeline(右上のボタン)

image.png

Variables に入力します。

Key:   TARGET_HOST
Value: pve01

image.png

New pipeline をクリックするとパイプラインが実行されます。

image.png

Stage 2(boot)の実行中、NanoKVMのWebUIでインストール画面が確認できます。

image.png

image.png

image.png

image.png

全ステージが完了したら https://<IP>:8006 にアクセスします。

image.png

image.png

image.png

image.png


ここまでのまとめ

この記事(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_simchassis bootdev をサポートしていないため、初回のBIOS設定は手動(iLO/iDRAC環境では chassis bootdev cdrom で代替可能)
  • sleep 240 はSSDの速度を前提にしているため、HDD環境では延長が必要

補足

Cephクラスタ構成の場合:
OS用SSDが故障した場合でも、OSD用SSDのデータはそのまま残っています。OSのみ再インストール後に ceph-volume lvm activate --all で復旧できます。hosts.ymlceph_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}")

参考リンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?