kkamegawa's weblog

Visual Studio,TFS,ALM,VSTS,DevOps関係のことについていろいろと書いていきます。Google Analyticsで解析を行っています

Hyper-VにUbuntuのVMを(ほぼ)自動で作る

ローカルのサーバー環境がよくなったので、ローカルでAzure Pipelines / GitHub Actionsのセルフホストランナーを作ろうとしています。最初はpackerでactions-runnerのレポジトリの内容を書き換えてローカルで動くようにしようと思っていたのですが、Ansibleでやったほうがよくないか?ということで(hclはほとんどCopilotに書き換えてもらった)、AnsibleのためにHyper-VのVMをほぼ自動でやるために環境を用意したのでそのメモです。大半はCopilotに書いてもらいましたがテストは自分でやりました。

必要なものは Windows Server 2025(2022でもたぶん大丈夫) と WSL2のみです。WSL2の入れ方はこちらを見てください。

learn.microsoft.com

cloud-initにはmeta-datanetwork-configuser-dataの3つのファイルが必要です。meta-dataはこのPowerShellスクリプト内で作るので不要です。

実行のためのPowerShellスクリプト

# 初期化作業
$seeddata = <cloud-initで使うファイルひな形を置いているところ>
$suffix = [system.datetime]::Today.Year.tostring()+[system.datetime]::Today.Month.ToString("00")+[system.datetime]::Today.Day.ToString("00")
$vmName   = "ubuntuagent"+$suffix
$workDir  = Join-Path "D:\HyperV\agent" -ChildPath $suffix
$vSwitch  = <Hyper-Vの仮想スイッチ名>
$cpu      = 4
$memoryGB = 16
$seediso = Join-Path $workDir -ChildPath "seed.iso"

#metadata作成
New-Item -ItemType Directory -Force -Path $workDir | Out-Null

$metadata = Join-Path $workDir -ChildPath "meta-data"
new-item $metadata -ItemType File -Force  | Out-Null
$utf8NoBom = [System.Text.UTF8Encoding]::new($false)
"instance-id: ubuntu-agent-"+$suffix, "`nlocal-hostname: agent-" + $suffix+"`n"  | Set-Content -Path $metadata -Encoding $utf8NoBom

# workfolderにコピー
$userdata = Join-Path $seeddata -ChildPath "user-data"
copy-item $userdata -Destination $workDir -Force
$networkconfig = Join-Path $seeddata -ChildPath "network-config"
Copy-Item $networkconfig -Destination $workDir -Force
Set-Location $workDir

wsl.exe -e bash -lc "sudo apt update && sudo apt install -y qemu-utils cloud-image-utils"

# ubuntu
$imgUrl = "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-amd64.img"
Invoke-WebRequest -Uri $imgUrl -OutFile "$workDir\ubuntu-24.04-amd64.img"

$disk = $vmName + "-amd64.vhdx"
wsl.exe -e bash -lc "qemu-img convert -p -f qcow2 -O vhdx ubuntu-24.04-amd64.img $disk"
$vhdx = Join-Path $workDir -ChildPath $disk
Resize-VHD -Path $vhdx -SizeBytes 64GB

# このフォルダに user-data  / network-config を保存済みとする
wsl.exe -e bash -lc "cloud-localds -v --network-config=network-config seed.iso user-data meta-data"

# VM 作成
New-VM -Name $vmName -MemoryStartupBytes ${memoryGB}GB -Generation 2 -SwitchName $vSwitch | Out-Null
Set-VM -Name $vmName -ProcessorCount $cpu

Get-VMHardDiskDrive -VMName $vmName | Remove-VMHardDiskDrive
Add-VMHardDiskDrive -VMName $vmName -Path $vhdx

# Secure Boot: Linux(Microsoft UEFI CA)
Set-VMFirmware -VMName $vmName -EnableSecureBoot On -SecureBootTemplate "MicrosoftUEFICertificateAuthority"

Add-VMDvdDrive -VMName $vmName -Path $seediso | Out-Null
$disk = Get-VMHardDiskDrive -VMName $vmName
Set-VMFirmware -VMName $vmName -FirstBootDevice $disk

# ゲストサービス(Guest Service Interface)を有効化
## 英語版OSはこちら
Enable-VMIntegrationService -VMName $vmName -Name "Guest Service Interface"
## 日本語版OSはこちら
Enable-VMIntegrationService -VMName $vmName -Name "ゲスト サービス インターフェイス"

network-config

version: 2
ethernets:
  any:
    # Hyper-V でインターフェイス名が変動しても拾えるようにワイルドカードでマッチして eth0 にリネーム
    match:
      name: "e*"
    set-name: eth0
    addresses: [IPアドレス]
    nameservers:
      addresses: [DNSサーバー]
    routes:
      - to: default
        via: <デフォルトゲートウェイ> 

IPアドレスはお好みで。最初使っていないやつ一覧から取得して…とか考えていましたがやめました。

#cloud-config
locale: en_US.UTF-8
ssh_pwauth: false

hostname: <ホスト名>

users:
  - name: agent
    gecos: Azure Pipeline Agent
    groups: [sudo]
    sudo: "ALL=(ALL) NOPASSWD:ALL"
    shell: /bin/bash
    ssh_authorized_keys:
      - 公開鍵

package_update: true
package_upgrade: true

apt:
  sources:
    ansible-ppa:
      source: "ppa:ansible/ansible"
    git-core-ppa:
      source: "ppa:git-core/ppa"

package_update: true
packages:
  - linux-virtual
  - linux-cloud-tools-virtual
  - linux-tools-virtual
  - curl
  - git
  - python3-venv
  - python3-pip
  - software-properties-common
  - ansible

# ルートパーティションとファイルシステムを自動拡張(64GB VHDX に合わせて)
growpart:
  mode: auto
resize_rootfs: true

runcmd:
  - systemctl enable --now hv-kvp-daemon.service || true
  - systemctl enable --now hv-fcopy-daemon.service || true
  - systemctl enable --now hv-vss-daemon.service || true
  - bash -lc "git config --system init.defaultBranch main"
  - bash -lc "mkdir -p /etc/ansible && printf '[defaults]\nhost_key_checking = False\n' > /etc/ansible/ansible.cfg"
  - bash -lc "curl -LsSf https://astral.sh/uv/install.sh | sh"
  - bash -lc "~/.local/bin/uv tool install ansible-core"
  - bash -lc "~/.local/bin/uv tool install ansible-dev-tools --with-executables-from ansible-core,ansible-lint,ansible-navigator"
  - systemctl restart ssh

ホスト名はスクリプトで直せば(書き換えれば)よかったですね。これはめんどくさがらずにやるべきだった…。

start-vm $vmnameでVMが起動するので、指定したIPアドレスでssh接続できるはずです。WSLでsudoするときにパスワード聞かれるので、まぁそこだけうまいことやってください。

これでVMが作成できたので、次はansibleでactions-runner相当のものを入れてみます。