#007: Windows Server VMを構築し、Bastion経由でアクセス

※本記事は、個人の意見および個人的活動を記したものであり、会社を代表するものではありません

# 1. BicepでWindows Server VMを構築
VNetの大まかな構築は完了したので、各サービスを構成するリソースを定義していく。
今回はVNetの内部から各リソースを管理・構成するための仮想マシン「AdminVm」のBicepを作成する。
VMを作成するために、OSディスク、NIC、NAT Gateway、IPなどいくつか関連するリソースも作成する。
ここで作成したVMはBastionからアクセスできるようにする。

## 1-1. 仮想マシンの定義
今回は以下のパラメータで構築する。
- VM名 = “AdminVm”
- OS コンピューター名 = “AdminHost”
- Adminユーザー名：satoadmin
- VM拡張機能の有効化
- OSの自動更新の有効化（Platformによる自動更新）
- OSのパッチ適用方式 = Platform (Azure) による自動適用 (後述)
- ハードウェアサイズ = Standard_D4as_v5 (4 vcpu, 16 ram)
- OSイメージ = WindowsServer 2022 Datacenter Azure Edition (後述)
- OSディスク = Premium LRS 128 GB
- Microsoft Entra認証の無効化 (後述)
- 休止状態の無効化 (後述)
- ディスクのブート診断の有効化
- トラステッド起動の有効化
    - セキュアブートの有効化
    - vTPMの有効化
- 仮想ネットワークの“AdminSubnet”に接続
- 23時（JST）に自動シャットダウン（拡張機能）

最終的なBicepテンプレートは以下Github リポジトリを参照。

https://github.com/masatoshisato/RagSystem/blob/v1.0.4/infra/compute/vm.bicep

以降、デプロイ時にエラーとなった設定と回避情報、やりたかったことができなかった時の代替方針について説明する。

## 1-2. hot patchイメージの場合の制約事項
まずはOSイメージである「Windows Server 2022 Datacenter Azure edition hot patch」の制約について。

https://learn.microsoft.com/ja-jp/azure/virtual-machines/automatic-vm-guest-patching

OSのパッチ適用方式として、「OSによる自動適用」を選択すると、以下のエラーが表示されて、デプロイに失敗。

```bash
Deployment Error Details:
InvalidParameter: 'PatchMode' must be set to 'AutomaticByPlatform' for resources created with Hotpatch-compatible images.
```

hot patchイメージを選択する場合、OSによる自動更新はサポートされておらず、Azureによる自動更新が必須。

また、自動更新の条件としてはVMが起動状態である事。VMが停止状態の場合、自動更新は適用されず、次回VMを起動した時に適用されるとの事。

プラットフォームによる自動更新というモードが存在することを知ったので、今回はOSの更新作業はAzureさんにお任せする。

## 1-3. 休止状態のサポートに関する制約事項

https://learn.microsoft.com/en-us/azure/templates/microsoft.compute/virtualmachines?pivots=deployment-language-bicep#additionalcapabilities

VMのBicep プロパティには、VMの休止状態を許可するためのプロパティ「hibernationEnabled」が存在する。休止状態にする事で、VMに対する課金が停止し、次回の起動時には休止したところから素早く開始できるため、利用できるならこれを利用したい。

だが、このプロパティは休止状態をサポートするOSでのみ利用可能であり、基本的にWindows Desktop (Win 10, 11) のイメージで利用可能との事。

今回はWindows Serverを利用しているため、このプロパティをtrueにすると、以下のエラーが表示されてデプロイが失敗する。

```bash
Deployment Error Details:
VMExtensionProvisioningError: VM has reported a failure when processing extension 'AzureHibernateExtension' (publisher 'Microsoft.CPlat.Core' and type 'WindowsHibernateExtension'). Error message: 'Enabling hibernate failed. Response from the powercfg command. Exit Code: 1. Error message:
Hibernation failed with the following error: The request is not supported.

The following items are preventing hibernation on this system.
        The current Device Guard configuration has disabled hibernation.
        An internal system component has disabled hibernation.
                Hypervisor'. More information on troubleshooting is available at https://aka.ms/vmextensionwindowstroubleshoot. 
```

休止状態を許可したかったのだが、しょうがない、「休止状態の許可」はオフにした。

## 1-4. Microsoft Entra IDを使用したVMへのログインの制約事項

https://learn.microsoft.com/en-us/answers/questions/959833/azure-bastion-host-using-azure-active-directory-cr

https://learn.microsoft.com/en-us/entra/identity/devices/howto-vm-sign-in-azure-ad-windows

https://learn.microsoft.com/en-us/azure/bastion/native-client

やりたかったことは、Bastion経由でVMにアクセスする時、OSのローカルアカウントではなくEntraユーザーでログインしたかった。OSローカルアカウントとEntraのアカウントの制約が違うのだが、セキュリティ要件を満たすために２つのアカウントを別々に管理するのが面倒だったし、セキュリティ観点でEntraアカウントのみ利用できれば非常に都合が良いと考えていた。

あっちこっちのドキュメントを見ないとハッキリとした現状の仕様が見えてこないのだが、結論、「Azure AD認証」という機能は存在するものの、Bastion経由で利用するための事前設定、実際に接続する時の手間が多すぎて今回は利用を諦めた。

条件や必要な設定項目について調査した結果を簡単にまとめると、

- Entra 認証に対応しているOSイメージ
- システム割り当てのマネージドIDの有効化
- VMへのAzure AD login が可能となる拡張機能「AADLoginForWindows」のインストール
- Entraの「ゲスト」アカウントではなくEntraの「メンバー」アカウントが必須
- メンバーアカウントに「Virtual Machine Administrator Login」もしくは「Virtual Machine User Loing」のどちらかのRBACを付与
- 接続元のPCでAzure CLIを実行し、Bastion経由でRDPセッションを開始
- 接続元のPCでWindowsネイティブクライアントを起動して、Azure CLIのセッション情報を使ってAzure AD認証でログイン

ちなみに、OSイメージがWindows Serverの場合、MacだとAzure AD認証は使えないらしい。

現時点ではこれ以上調べる気力がなくなったので、現時点では大人しくVMローカルアカウントを使用する。
Bastionを利用せずにAAD認証を用いてもう少しセキュアなやり方を後で検討してみるか。

## 1-5. Azure Editionのイメージに関する制約と回避方法

「Windows Server 2022 Datacenter Azue edition hot patch」のイメージでは、OS自動更新の制約があり、一旦「Windows Server 2022 Datacenter Azure Edition」に変更しようとした。

だが、このイメージも、実は１つ制約事項があった。

VM作成後にログインした後で、十数秒待っていると警告メッセージがポップアップされる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466853/ce63fd70-ae20-711c-43d2-7ed208594929.png)


Azure上で実行しているにも関わらず、Azure もしくは Azure Stack上で実行していないと判断され、ライセンスがアクティベートされないらしい。あとから何か問題が出てくるのは勘弁なので、何か解決策がないかと検索。

https://learn.microsoft.com/en-us/answers/questions/1659610/windows-server-2022-datacenter-azure-edition-vm-de

この記事から察するに、IMDS（Azure Instance Metadata Service）との通信時に、なぜかOSイメージに正常な証明書がインストールされていないため正常に通信できないことに起因しているようだ。証明書を正しくインストールする事でアクティベーションが正常に実行される。（後述）
ただし、このサイトでの回答として、このエラーは特になんの影響もないのでそのまま利用してよく、今後MSにてこのIssueの対応を行うことが記されていた。

回避策となる方法も記されているので今回はそれを実施してみた。
以下の手順は、全てVMのWindows Server上で実行する。

::: note warn
上記に記したリンクには、そもそもMSエンジニアが「影響なし」かつ「今後対応する」と表明している。また、この回避策はVM内で操作が必要なものであり、証明書のダウンロードは自動化するのが困難。よって、スクラップ＆ビルドを前提とした構築作業には向かない。どうしても実施するのであれば、スクラップ＆ビルドのサイクルが終わった後、実施するのが良いだろう。
:::

### 1-5-1. IMDSとの接続を確認。

以下コマンドを実行した結果が以下のように表示されていれば接続はOK。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466853/4327216b-3289-a59f-5eb6-cc7c50ac07e6.png)


### 1-5-2. IMDSの証明書を確認。

以下コマンドを実行した結果がTrueなら問題ないが、正しい証明書がインストールされていないのでFalseが出力されるはず。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466853/0c68f6fd-dd37-264f-b1f9-dc043ad92999.png)

### 1-5-3. IMDSの証明書をダウンロード。

Cドライブ直下にIMDS.cerというファイル名で証明書をダウンロード。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466853/771695b2-dc73-42b4-18f3-e3af15167f7e.png)

ダウンロードしたファイルを確認。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466853/5b8c85e4-da51-9438-1dbb-52674afdff3e.png)

### 1-5-4. IMDSの証明書のCNを確認

IMDS.cerファイルをダブルクリックして、IsserのCNを確認。

IssuerのCN = “Microsoft Azure RSA TLS Issuing CA 03”

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466853/37a81b7f-a328-cf88-e585-3d96cbc1dec2.png)

### 1-5-5. IMDSの証明書をダウンロードしてインストール

以下のサイトから、CNと同じ名前のリンクから証明書をダウンロード。

https://learn.microsoft.com/en-us/azure/security/fundamentals/azure-ca-details?tabs=certificate-authority-chains

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466853/b0c27643-e555-912e-b372-bbb43101dcbc.png)

ダウンロードした証明書ファイルをインストール。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466853/8529efa7-ca6d-d9c6-0d3a-fac74fc30af2.png)

### 1-5-6. 「何言っているかちょっとよくわからない」コマンドを実行

以下コマンドを実行せよとのこと。これが何をしているのかは説明なし。調べる気力がないので後で気が向いたら調べてみる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466853/f75cd521-e11f-224c-988f-ae1b384a61b5.png)

### 1-5-7. ライセンスのアクティベーション状況を確認

最後にアクティベーションチェックだが、もしアクティベーションできていない場合はOSを再起動すればアクティベーションされていることを確認できる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466853/136259c1-84af-a44f-9392-52f5facaa976.png)

## 1-6. Adminユーザーのパスワード管理方針

今回はEntraユーザーでログインすることを諦めたが、OSローカルのAdminユーザーのパスワードはKeyVaultで管理することと、Bastionへのアクセスは自分のネットワークからに限定する事でBastionへの接続を制限しつつ、アカウントのパスワード管理をAzureに任せることにする。

::: note info
KeyVaultでのパスワード管理手順は今後検証する。
:::

## 1-7. 自動シャットダウン拡張機能の登録時の制約

VMはシャットダウンして割り当てを解除しないと課金され続けるリソースなので、自動シャットダウンも構成する。

「Microsoft.DevTestLab/schedules」というリソースタイプを指定する事で、VMの自動シャットダウンをBicepで構成することができるが１つ制約がある。

自動シャットダウンのnameプロパティの値はフォーマットが決まっており、

- 「shutdown-computevm-${vm name}」

としなければならない。Prefixの「shutdown-computevm-」は固定値であり、Postfixの「${vm name}」の部分は自動シャットダウンを設定するVMの名前でなければならない。

これを知らないでBicepでデプロイを実行した時に以下のエラーが表示された。

```bash
Deployment Error Details:
InvalidScheduleId: The schedule should be created in subscription 48a288e7-d57f-4e9a-b8f7-14042109d26f, resource group azurestrategicpartnership_masatoshi.sato_rg and with name shutdown-computevm-RagSystem-AdminVm-dev.
```

Github Copilotを使っているのだが、Copilotくんが別のフォーマットの名前を提案したので、それをそのまま使ったらエラーになった。。

「InvalidScheduleId」という意味不明なエラーだが、メッセージが丁寧だったので特段調査する必要もなく、どこをどのように修正すれば良いか判断できた。素晴らしい。

# 2. NAT Gatewayのインストールが先だった

VMの構築は一旦完了だが、ライセンスアクティベーションのIssue対応中に、IMDSの証明書ダウンロードとアクティベーション処理を実行するためにインターネットアクセスが必要だったので、先にNAT Gatwayをインストールしていたのを思い出した。

VMを配置したSubnetは、Private Subnetを有効化しており、かつVMにはPublic IPを割り当てていないため、この状態ではインターネットとの通信が遮断されている状態。

これではIMDSのIssueを解決できなかったため、急遽NAT Gateway をインストールすることにした。

本当は別記事で書きたかったのだが、これを定義しないとIMDS Issueの対応ができないので、ここに書くことにした。

## 2-1. NAT Gatewayの定義

今回は以下のパラメータで構築する。

- NAT Gateway名：RagSyatem-NatGw-dev
- 可用性ゾーン：指定なし（デフォルト）
- TCPアイドルタイムアウト：4分（デフォルト）
- Public IP名：RagSystem-NatGwIp-dev
- 関連付けする仮想ネットワーク：RagSystem-MainVnet-dev
- 関連付けするサブネット：AdminSubnetとAzureBastionSubnet

AdminSubnetにあるVMでは、Azureポータルにアクセスする必要があるためインターネットアクセスが必須なのでAdminSubnetに紐づけた。

今回構築しているサービスでは、主に管理用途で利用するため、特に高い可用性は求められない想定なので、可用性は指定しなかった。

最終的なBicep定義は以下GitHubリポジトリを参照。

https://github.com/masatoshisato/RagSystem/blob/v1.0.4/infra/network/natgw.bicep

## 2-2. NAT GatewayとSubnetの紐付け

NAT Gatewayを構築する時に、関連付けするサブネットを指定できるが、今回はNAT Gateway作成時には紐付けを指定せず、Subnet作成時にどのNAT Gatewayを紐づけるかを指定することにした。

その理由は、NAT Gatewayとサブネットの関係は、NAT Gatewayから見ると1:n、サブネットから見ると1:1となり、サブネット構築時にNAT Gatewayを紐づけた方がBicepテンプレートが簡単になるため。

また、1:1の紐付けを実現するために、今まではVNetのBicep定義にSubnetもネスト定義していたが、別のBicepファイルに切り出し、個別にmain.bicepで定義するようにした。

これにより、AdminSubnet構成時にはNAT Gatewayの紐付けを指定し、AzureBastionSubnet構成時にはNAT Gatewayは紐付けしないといった事が実現できる。

変更後のSubnetのBicepファイルの詳細はGithubリポジトリを参照されたし。

https://github.com/masatoshisato/RagSystem/blob/v1.0.4/infra/network/subnet.bicep

# 3. BicepでBastionを構築

## 3-1. Bastionの定義

今回は以下のパラメータで定義する。

- sku: Basic（スケールユニットは２固定）
- 関連づけるSubnet: “AzureBastionSubnet”　（サブネットの名称は固定）
- コピーペーストの許可: true

BastionにはパブリックIPが必要なので、同じBicepテンプレート内で別途IPのリソースも構築してBastion に関連づける。

## 3-2. [tips] InvalidGatewayTypeというエラーの原因

Bastion をBicepで定義してデプロイ実行時、不明なエラー「InvalidGatewayType」が発生し、エラーメッセージから原因特定できず困惑。

```bash
sato@[21:36:54]:~/proj/RagSystem% azd up

Packaging services (azd package)

Provisioning Azure resources (azd provision)
Provisioning Azure resources can take some time.

Subscription: Subscription-IBMJPOCKSPOC-MPN-EA-Dev-Test (48a288e7-d57f-4e9a-b8f7-14042109d26f)
Location: East US 2

  You can view detailed progress in the Azure Portal:
  https://portal.azure.com/#view/HubsExtension/DeploymentDetailsBlade/~/overview/id/%2Fsubscriptions%2F48a288e7-d57f-4e9a-b8f7-14042109d26f%2Fproviders%2FMicrosoft.Resources%2Fdeployments%2Fdev-1723293536

  |=======| Creating/Updating resources
ERROR: error executing step command 'provision': deployment failed: error deploying infrastructure: deploying to subscription:

Deployment Error Details:
InvalidGatewayType: The gateway type 'Basic' is invalid for this operation.

TraceID: 1947f4c3e56f4e95dd1852339b0515ed
```

Bicepには「gateway type」なんて指定しておらず、唯一のヒントは「Basic」というキーワード。

Bastionの仕様を見ると、「sku」に”Basic”を指定した場合、「enableFileCopy」に”true”は指定できないことが判明。両方を指定した場合に「InvalidGatewayType」というエラーメッセージが表示されるようだ。

たまたまBastionリソースの定義だけ「Basic」を指定しているプロパティがあったから「Bastionかな」と推測できたからよかったものの、複数のソースで「Basic」を指定していたら、このバグフィックスには多くの無駄な調査時間がかかっただろうな。

ファイルコピー機能は「sku」が”Standard”でのみ指定可能なプロパティだからエラーになったのなら、まさにそのことをメッセージに含めてほしい。。GatewayTypeなんて指定してないから、どこが原因なのか全く分からなかった。。

::: note
昔、新人が書いたプログラムから出力されたエラーメッセージによく似ている。。エラー処理は行なっているのだが、エラーの根本原因を特定するための情報がログに出力せず、「システムエラーが発生しました」とか「ファイルI/Oエラーが発生しました」的なメッセージしか出てないような状態。運用開始後にエラーが発生した場合、どこで発生していて、何が原因なのか全くわからず対応に多くの時間を費やすことになった。
:::

ちなみに、enableFileCopyプロパティはWindowsネイティブクライアントでのみ利用可能な機能とのこと、このプロパティにはfalseを指定し、おとなしくBasicで構築することにする。

## 3-3. Subnetとの紐付け

Bastionは「AzureBastionSubnet」という名前のサブネットにだけデプロイが可能。

サブネットを参照できるよう、VNetとAzureBastionSubnetというサブネット名をパラメータに指定できるように構成。

## 3-4. でもね、本当はアカウント管理を一元化したい。。

RDPを公開せずにBastionでアクセスするのもいいのだが、本当は、、

- アクセスできるEntraユーザーを明示的に限定したい（OSアカウントがわかれば誰でもログインできるというのは止めたい）
- アカウント情報が漏洩するリスクにも対応しておきたいのでログイン時は二要素認証にしたい
- そもそもOSローカルのアカウントとAADアカウントを二重で管理したくない
- 上記を、できるだけ簡単に実現したい

もう少しじっくり確認してみるか。

その前に、OSローカルのアカウントパスワード、今のままでは、パスワード付きExcelでパスワードを管理して、、、みたいなカッコ悪い管理方法になりそうだから、次回以降でパスワード管理をKeyVaultに任せる方法を確認する。

# 4. 作業結果のソースコード

ここまでの全容は以下を参照されたし。

https://github.com/masatoshisato/RagSystem/tree/v1.0.4