Azureで4GiBのマネージドディスクにUbuntuを乗せてVMのストレージ代を節約する

ケチ担当です。

背景
--------

Azure VMといえばスペックに目が行きがちですが、マネージドディスクも案外高いです。むしろマネージドディスクは存在するだけでかかるので1日1時間だけ使うVMだからといっても24時間分まるまる掛かります。

しかし、メジャーなLinuxイメージは30GiB以上からとなります。OS用のマネージドディスクはプランに基づいてストレージ容量が決まっており、30GiBの場合のマネージドディスクの価格は32GiB相当のプラン(P4,E4等)からとなります。

[マネージドディスクの価格](https://azure.microsoft.com/ja-jp/pricing/details/managed-disks/)をみてみましょう。

- Premium SSD P4(32GiB): `$6.08`
- Standard SSD E4(32GiB): `$2.40`
- Standard HDD S4(32GiB): `$1.54`

SLAから外れますがStandard SSDにするだけでも安くなります。

しかし、Bastion(踏み台)といったストレージ容量やIOPSがあまり必要ではないワークロードでは32GiBでも過剰です。

もし4GiBになれば以下のプランとコストでマネージドディスクが運用できます。

- Premium SSD P1(4GiB): `$0.87` (P4と比べ`-$5.21`)
- Standard SSD E1(4GiB): `$0.30` (E4と比べ `-$2.10`)

マネージドディスク1台あたり月額約`$2.1～$5.2`の節約となります。

ちりも積もれば！ととらえるか、正直Standard SSDにするだけでもいいかな…ととらえるかはあなた次第…

というわけで、4GiBのマネージドディスクでUbuntu VMを動かして節約したいなぁ、というのが趣旨です。


手順
-------

### 前提

- Windows 11のマシンで管理者権限を持っている
- Hyper-V関連のコマンドを利用できる（VHDの操作コマンドが必要）
- Azure Portalをある程度操作できる（StorageAccountの作成、Blobのアップロード、Managed Diskの作成などの操作・権限があること）

### VHDファイルを用意する

https://learn.microsoft.com/en-us/azure/virtual-machines/linux/create-upload-ubuntu

Ubuntuでは[Cloud用のイメージも配布されており](https://cloud-images.ubuntu.com/)、すぐ使えるAzure向けVHDファイルも用意されています。

今回はこのAzure向けVHDファイルを使います。使わない場合は、自前でHyper-VにVMを作成し、Ubuntuをインストールし、[Azureのドキュメント](https://learn.microsoft.com/en-us/azure/virtual-machines/linux/create-upload-ubuntu)のManual stepsを実施する必要がありそうです。


早速ですが、[配布されているVHDが含まれている`noble-server-cloudimg-amd64-azure.vhd.tar.gz`ファイル](https://cloud-images.ubuntu.com/noble/current/)をダウンロードして展開した図です。0.5GBが30GBにもなります。この後さらに使うので、ディスク容量は充分に空けてください。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/52527/0066d213-085a-8f84-6cc2-2137546ad45e.png)

次に、PowerShellを**管理者として**実行し、以下のコマンドを実行します（Hyper-Vマネージャなどが使えるようにしていないとコマンドがないかもしれません）

```powershell
# 変数用意
$VHD_PATH='livecd.ubuntu-cpc.azure.vhd'
$WORK_VHDX_PATH="$($VHD_PATH).dyn.vhdx"
$DEST_VHD_PATH="$($VHD_PATH).small.vhd"

# 作業ディレクトリをVHDがある場所へ移動
cd path\to\dir

# 動的なディスク(Dynamic)のVHDXファイルに変換する
Convert-VHD -Path $VHD_PATH -DestinationPath "$WORK_VHDX_PATH" -VHDType Dynamic

# 作成したVHDXファイルを確認。まだSizeが30GBぐらいになっている
Get-VHD -Path $WORK_VHDX_PATH

# VHDXファイルを縮小（MiniumumSizeを見ながら調整）
# ここをマネージドディスクのサイズに合うように調整（空きがもう少し欲しいなら8GBなど）
Resize-VHD -Path $WORK_VHDX_PATH -SizeBytes 4GB

# Sizeが縮小できたことを確認
Get-VHD -Path $WORK_VHDX_PATH

# 固定ディスクのVHDファイルに変換する
Convert-VHD -Path $WORK_VHDX_PATH -DestinationPath $DEST_VHD_PATH -VHDType Fixed 
```

一通りの処理が終わると`*.small.vhd`ができあがります。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/52527/b9dddab4-ba87-859b-c029-c70198f28e4d.png)

他のファイルは不要なので、消しても構いません。

### VHDファイルをBlobストレージへアップロードする

やり方は色々ありそうです以下の手順で行います

- Locationを決める（Storage Account, マネージドディスク）
- Storage Account,コンテナを作ってアップロード
- マネージドディスク作成時にBlobを参照


マネージドディスクを作成する際にStorage AccountとLocationを合わせないとエラーになります。

その点以外は普通にStorageAccountを作って、コンテナを作ってBlobにPage Blobとしてアップロードするだけです。ポータル上からアップロードしてください。

![Pasted image 20250121235148.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/52527/cdedf359-ef5a-e803-8456-10997e6794ad.png)

アップロード後、 BlobのURLを控えておいてください。マネージドディスク作成時に使います。StorageAccountがAzureサービスからアクセスできる状態であれば、SASも必要ないです。



#### マネージドディスクの作成を行う

Azureのリソース、`Disk`から行えます。（通常VMのついでで作るので、Disk単品を作ることはあまりないと思いますが）

Disk作成時のポイント以下です。

- Disk nameはVM作成時に参照するため良い名前をつける
- Source BlobにStorage BlobでアップロードしたVHDファイルのBlobのURLを指定
- SizeもStandard SSDの4GiBにします。 SLAがほしければPremium SSDにしましょう
- 他はデフォルトでよいです（必要に応じて設定してください）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/52527/458df957-3ee2-f928-0886-cb0798f82c22.png)

作成後はBlobを削除しても構いません。もちろん、他のVMでも同じことをしたい場合は残しても良いでしょう。

### 作ったマネージドディスクを使ってVMの作成

デプロイできたら、Diskのリソースにアクセスしてみてください。4GiBのマネージドディスクになっていますね。

`Create VM` からVMを作ってみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/52527/3548355a-5f8d-138a-d519-6f2d15986bf7.png)


イメージ選択ができない、ユーザ作成ができない、VM Generationは第2世代にする点以外は普通のVM作成と一緒です。

ユーザの作成はこの後で行います。

VM作成時・作成後はまず、NSGの設定などを行ってください。配布されているUbuntuのディスクイメージはSSHのパスワードログイン不可能な状態なので最悪インターネットにSSHポートを晒しても大丈夫なんですが、世の中何があるかわからないので自宅・会社のIPを許可するなどいい感じにやってください。



### VM内にSSH接続用のユーザ作成

Azure VM作成時にユーザ作成がなかったのでログインできません。（Ubuntuイメージにはデフォルトのユーザもありません）

なので、Azure Portalからコマンドを実行します。

Operations→Run Commandがあり、RunShellScriptというモノが用意されています。ここからRoot権限でShellScriptが発行できます（悪用厳禁）

![Pasted image 20250122011124.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/52527/755f6e9b-1fbc-1f9e-994a-d4371c4204de.png)

ユーザを作ってSSH公開鍵を仕込むスクリプトの例です。

公開鍵は適当に用意して、3つの変数は良い感じに埋めてください。パスワードは後で換える前提で一時的なものにしましょう。

```sh
USERNAME='azureuser'
PUBLIC_KEY='ssh-ed25519 AAAA....'
PASSWORD=passw0rd

# 作成されるディレクトリ・ファイルのPermissionをOwnerのみ許可するようマスクする
umask 0077

# ユーザ作成
useradd -m --shell "/usr/bin/bash" "$USERNAME"

# sudoできるグループへユーザを追加
usermod -G sudo "$USERNAME"
# sudo時のパスワードをとりあえず設定
echo "$USERNAME:$PASSWORD" | chpasswd

# (Option)sudo時のパスワードが面倒な場合、大いなる責任を感じながら NOPASSWD: をつける
# echo "$USERNAME ALL=(ALL) NOPASSWD: ALL" > "/etc/sudoers.d/90_${USERNAME}"

# SSH公開鍵を登録
mkdir /home/$USERNAME/.ssh
echo "$PUBLIC_KEY" >> /home/$USERNAME/.ssh/authorized_keys
# 所有者をログインユーザに変更
chown -R $USERNAME: /home/$USERNAME/.ssh

# 作成確認
ls -al /home/$USERNAME/.ssh
```

作業後、公開鍵方式でSSHログインできるはずです。`df`などでストレージ容量を確認してみてください。

あとはパスワードを正規のものに変更してください。
なお、このポータルから実行したスクリプトは`/var/lib/waagent/run-command/download/*/script.sh`にroot権限で参照できる形で残ります。root権限無いと見られないとはいえ、パスワードも記録されてしまうことになるので変えておきましょう。

作業は以上です。あとはDisk fullに注意しながら運用しましょう。

おまけ
----

### パーティションやファイルシステムのリサイズは大丈夫？

VHDのリサイズしかしていないのだからディスクイメージ内のファイルシステムは古いままではないか？と思っていました。

結果としてはUbuntuで配布されているイメージを使っている場合、初回起動時にcloud-initでリサイズがかかるため大丈夫です。


以下はSSHログイン直後に確認した様子です。

```
azureuser@ubuntu:~$ lsblk
NAME    MAJ:MIN RM  SIZE RO TYPE MOUNTPOINTS
sda       8:0    0    4G  0 disk
├─sda1    8:1    0    3G  0 part /
├─sda14   8:14   0    4M  0 part
├─sda15   8:15   0  106M  0 part /boot/efi
└─sda16 259:0    0  913M  0 part /boot

$ sudo fdisk -l
Disk /dev/sda: 4 GiB, 4294967296 bytes, 8388608 sectors
Disk model: Virtual Disk
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 4096 bytes
I/O size (minimum/optimal): 4096 bytes / 4096 bytes
Disklabel type: gpt
Disk identifier: 604309DA-4FE5-4F90-BBFC-6DBB76EBD0CA

Device       Start     End Sectors  Size Type
/dev/sda1  2099200 8388574 6289375    3G Linux filesystem
/dev/sda14    2048   10239    8192    4M BIOS boot
/dev/sda15   10240  227327  217088  106M EFI System
/dev/sda16  227328 2097152 1869825  913M Linux extended boot

Partition table entries are not in disk order.
```

cloud-initのログをみたところ、パーティション操作(growpart)とファイルシステムのリサイズ(resize2fs)を行っていました。

```
2025-01-21 15:28:05,956 - subp.py[DEBUG]: Running command ['growpart', '/dev/sda', '1'] with allowed return codes [0] (shell=False, capture=True)
2025-01-21 15:28:06,649 - performance.py[DEBUG]: Running ['growpart', '/dev/sda', '1'] took 0.693 seconds
2025-01-21 15:28:06,650 - performance.py[DEBUG]: Resizing devices took 0.900 seconds
2025-01-21 15:28:06,650 - cc_growpart.py[INFO]: '/' resized: changed (/dev/disk/by-partuuid/1b4cd629-def0-4dac-89eb-bb9119fdc5d4) from 2683289088 to 3220160000
2025-01-21 15:28:06,650 - handlers.py[DEBUG]: finish: init-network/config-growpart: SUCCESS: config-growpart ran successfully and took 0.920 seconds
2025-01-21 15:28:06,650 - modules.py[DEBUG]: Running module resizefs (<module 'cloudinit.config.cc_resizefs' from '/usr/lib/python3/dist-packages/cloudinit/config/cc_resizefs.py'>) with frequency always
2025-01-21 15:28:06,650 - handlers.py[DEBUG]: start: init-network/config-resizefs: running config-resizefs with frequency always
2025-01-21 15:28:06,650 - helpers.py[DEBUG]: Running config-resizefs using lock (<cloudinit.helpers.DummyLock object at 0x77b8aaaeec60>)
2025-01-21 15:28:06,650 - util.py[DEBUG]: Reading from /proc/659/mountinfo (quiet=False)
2025-01-21 15:28:06,651 - util.py[DEBUG]: Reading 2310 bytes from /proc/659/mountinfo
2025-01-21 15:28:06,651 - cc_resizefs.py[DEBUG]: resize_info: dev=/dev/root mnt_point=/ path=/
2025-01-21 15:28:06,651 - cc_resizefs.py[DEBUG]: Resizing / (ext4) using resize2fs /dev/root
2025-01-21 15:28:06,651 - subp.py[DEBUG]: Running command ('resize2fs', '/dev/root') with allowed return codes [0] (shell=False, capture=True)
2025-01-21 15:28:06,709 - performance.py[DEBUG]: Running ('resize2fs', '/dev/root') took 0.059 seconds
2025-01-21 15:28:06,710 - cc_resizefs.py[DEBUG]: Resized root filesystem (type=ext4, val=True)
2025-01-21 15:28:06,710 - handlers.py[DEBUG]: finish: init-network/config-resizefs: SUCCESS: config-resizefs ran successfully and took 0.059 seconds
```

おまけにdfも。2024.04のイメージでも1GBぐらいは空いてます。

```
$ df
Filesystem     1K-blocks    Used Available Use% Mounted on
/dev/root        2974484 1858468   1099632  63% /
tmpfs            4063764       0   4063764   0% /dev/shm
tmpfs            1625508     980   1624528   1% /run
tmpfs               5120       0      5120   0% /run/lock
efivarfs          131072      26    131042   1% /sys/firmware/efi/efivars
/dev/sda16        901520   59924    778468   8% /boot
/dev/sda15        106832    6246    100586   6% /boot/efi
tmpfs             812752      12    812740   1% /run/user/1000
```