Azure Instance Metadata Service(IMDS)を使ってみる

# はじめに
[AWSのEC2でメタデータを取得する](https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/ec2-instance-metadata.html)のは結構メジャーな話題ですが、同様の機能はAzure VMにもあります。
それが[Azure Instance Metadata Service](https://docs.microsoft.com/ja-jp/azure/virtual-machines/linux/instance-metadata-service)(IMDS)です。

# 検証環境
今回、以下のような構成をAzure東日本リージョン内に作成し動作を確認しました。
![20220217_imds (2).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274613/ddf09f1a-8096-22c9-4245-32694ea723da.jpeg)
Load Balancer配下に[VMSS](https://azure.microsoft.com/ja-jp/services/virtual-machine-scale-sets/)でLinux(Ubuntu)を1台作成しています。
Blobストレージには、VMのシステム割り当てマネージドIDに対し読み書きの許可（ストレージ BLOB データ共同作成者ロール）を付与しています。

# メタデータのエンドポイント
メタデータはREST APIの形で公開されています。そのルートエンドポイントは、
http://169.254.169.254/metadata
です。
169.254.0.0/16のセグメントは[リンクローカルアドレス](https://ja.wikipedia.org/wiki/%E3%83%AA%E3%83%B3%E3%82%AF%E3%83%AD%E3%83%BC%E3%82%AB%E3%83%AB%E3%82%A2%E3%83%89%E3%83%AC%E3%82%B9)であり、外部のネットワークからはアクセスできません。Azure上のVMからアクセスすることで、そのVMに関する情報を取得できるようになっています。

# メタデータのバージョン
メタデータ機能は随時バージョンアップされていますが、後方互換性を保つためアクセス時には必ずバージョンの指定が必要となります。
現在公開されているバージョンを確認するためには、以下のようにエンドポイントにアクセスします。

```bash
$ curl -H Metadata:true http://169.254.169.254/metadata/versions
```
メタデータのエンドポイントにアクセスする際は必ず「Metadata:true」のヘッダが必要となることに注意しましょう。

この記事を書いている2022/02/19現在では以下のバージョンが存在しています。(取得結果をjqコマンドで見やすく整形しました)

```json
{
  "apiVersions": [
    "2017-03-01",
    "2017-04-02",
    "2017-08-01",
    "2017-10-01",
    "2017-12-01",
    "2018-02-01",
    "2018-04-02",
    "2018-10-01",
    "2019-02-01",
    "2019-03-11",
    "2019-04-30",
    "2019-06-01",
    "2019-06-04",
    "2019-08-01",
    "2019-08-15",
    "2019-11-01",
    "2020-06-01",
    "2020-07-15",
    "2020-09-01",
    "2020-10-01",
    "2020-12-01",
    "2021-01-01",
    "2021-02-01",
    "2021-03-01",
    "2021-05-01"
  ]
}
```
以降の項では最も新しいバージョンである"2021-05-01"を利用して例示します。

# IMDSで何ができる？
以下の5つのカテゴリの情報が取得できます。

- インスタンス メタデータ
- 構成証明済みデータ
- Load Balancer メタデータ
- マネージド ID
- スケジュールされたイベント

それぞれ順に内容を見ていきましょう。

## インスタンス メタデータ
「インスタンス メタデータ」ではVMの構成情報が提供されます。
以下のように情報を取得します。

```bash
$ curl -H Metadata:true http://169.254.169.254/metadata/instance?api-version=2021-05-01
```
今回の環境では以下の情報が取得できました。

```json
{
  "compute": {
    "azEnvironment": "AzurePublicCloud",
    "customData": "",
    "evictionPolicy": "Delete",
    "extendedLocation": {
      "name": "",
      "type": ""
    },
    "isHostCompatibilityLayerVm": "false",
    "licenseType": "",
    "location": "japaneast",
    "name": "imdstest_0",
    "offer": "0001-com-ubuntu-server-focal",
    "osProfile": {
      "adminUsername": "azureuser",
      "computerName": "imdstestk000000",
      "disablePasswordAuthentication": "false"
    },
    "osType": "Linux",
    "placementGroupId": "8475ae57-1ef2-42e6-a377-d84cfc4faf28",
    "plan": {
      "name": "",
      "product": "",
      "publisher": ""
    },
    "platformFaultDomain": "0",
    "platformUpdateDomain": "0",
    "priority": "Spot",
    "provider": "Microsoft.Compute",
    "publicKeys": [],
    "publisher": "canonical",
    "resourceGroupName": "IMDStest",
    "resourceId": "/subscriptions/xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx/resourceGroups/IMDStest/providers/Microsoft.Compute/virtualMachineScaleSets/imdstest/virtualMachines/0",
    "securityProfile": {
      "secureBootEnabled": "false",
      "virtualTpmEnabled": "false"
    },
    "sku": "20_04-lts-gen2",
    "storageProfile": {
      "dataDisks": [],
      "imageReference": {
        "id": "",
        "offer": "0001-com-ubuntu-server-focal",
        "publisher": "canonical",
        "sku": "20_04-lts-gen2",
        "version": "20.04.202201310"
      },
      "osDisk": {
        "caching": "ReadWrite",
        "createOption": "FromImage",
        "diffDiskSettings": {
          "option": ""
        },
        "diskSizeGB": "30",
        "encryptionSettings": {
          "enabled": "false"
        },
        "image": {
          "uri": ""
        },
        "managedDisk": {
          "id": "/subscriptions/xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx/resourceGroups/IMDStest/providers/Microsoft.Compute/disks/imdstest_imdstest_0_OsDisk_1_fd9dc877831343b8ab62b9985017ead0",
          "storageAccountType": "Standard_LRS"
        },
        "name": "imdstest_imdstest_0_OsDisk_1_fd9dc877831343b8ab62b9985017ead0",
        "osType": "Linux",
        "vhd": {
          "uri": ""
        },
        "writeAcceleratorEnabled": "false"
      },
      "resourceDisk": {
        "size": "32768"
      }
    },
    "subscriptionId": "xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx",
    "tags": "",
    "tagsList": [],
    "userData": "",
    "version": "20.04.202201310",
    "virtualMachineScaleSet": {
      "id": ""
    },
    "vmId": "1b04791f-53df-428b-93b6-d0beb1cf98dd",
    "vmScaleSetName": "imdstest",
    "vmSize": "Standard_D4s_v3",
    "zone": "1"
  },
  "network": {
    "interface": [
      {
        "ipv4": {
          "ipAddress": [
            {
              "privateIpAddress": "10.0.0.5",
              "publicIpAddress": ""
            }
          ],
          "subnet": [
            {
              "address": "10.0.0.0",
              "prefix": "24"
            }
          ]
        },
        "ipv6": {
          "ipAddress": []
        },
        "macAddress": "00224867DF4E"
      }
    ]
  }
}
```
構成情報をCMDBに登録するなどの用途に有用かと思います。

## 構成証明済みデータ
「構成証明済みデータ」は、Microsoftにより署名されたデータです。
以下のように取得します。

```bash
$ curl -H Metadata:true http://169.254.169.254/metadata/attested/document?api-version=2021-05-01
```

```json
{
  "encoding": "pkcs7",
  "signature": "MIILOgY～中略～kIll51i0Q=="
}
```
signatureの値は、base64でエンコードされた[PKCS#7](https://ja.wikipedia.org/wiki/PKCS)署名済みメッセージです。
[公式ドキュメントを参考にして](https://docs.microsoft.com/ja-jp/azure/virtual-machines/linux/instance-metadata-service?tabs=linux#sample-1-validate-that-the-vm-is-running-in-azure)デコードしてみましょう。

```bash
$ curl -H Metadata:true http://169.254.169.254/metadata/attested/document?api-version=2021-05-01 | jq -r '.["signature"]' > signature
# base64のデコード
$ base64 -d signature > decodedsignature
# PKCS#7のメッセージの表示
$ openssl smime -verify -in decodedsignature -inform DER -noverify | jq
Verification successful
{
  "licenseType": "",
  "nonce": "20220219-153819",
  "plan": {
    "name": "",
    "product": "",
    "publisher": ""
  },
  "sku": "20_04-lts-gen2",
  "subscriptionId": "xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx",
  "timeStamp": {
    "createdOn": "02/19/22 09:38:19 -0000",
    "expiresOn": "02/19/22 15:38:19 -0000"
  },
  "vmId": "1b04791f-53df-428b-93b6-d0beb1cf98dd"
}
```
このメッセージはMicrosoftの証明書によって署名されているため、AzureのIMDSから取得されたものであることを証明することができます。
利用用途の一例として[公式ドキュメント](https://docs.microsoft.com/ja-jp/azure/virtual-machines/linux/instance-metadata-service?tabs=linux#sample-1-validate-that-the-vm-is-running-in-azure)では、
「Azure Marketplace のベンダーが、自分たちのソフトウェアがAzure でのみ実行されるようにライセンスされていることを確認する」際に利用する、
というものが挙げられています。
一般のAzureユーザーとしてはあまり利用する必要はないかもしれません。
（当方はまったく使ったことがなく…。利用されている方がいらっしゃったらぜひ利用用途などコメントいただきたいです。）

## Load Balancer メタデータ
「Load Balancer メタデータ」は、その名のとおりロードバランサーの情報です。
取得方法は以下のとおり。

```bash
$ curl -H Metadata:true http://169.254.169.254/metadata/loadbalancer?api-version=2021-05-01
```

```json
{
  "loadbalancer": {
    "publicIpAddresses": [],
    "inboundRules": [
      {
        "frontendIpAddress": "20.89.89.137",
        "protocol": "Tcp",
        "frontendPort": 22,
        "backendPort": 22,
        "privateIpAddress": "10.0.0.5"
      }
    ],
    "outboundRules": [
      {
        "frontendIpAddress": "20.89.89.137",
        "privateIpAddress": "10.0.0.5"
      }
    ]
  }
}
```
ロードバランサーに登録されている「負荷分散規則」「送信規則」が取得できます。


## マネージド ID
マネージドIDの認証用アクセストークンを取得できます。
「マネージドIDってなに？」という方は以下の記事が参考になると思います。（Microsoftの中の人による解説記事です）
[はじめての Azure リソース マネージド ID (概要編)](https://qiita.com/Shinya-Yamaguchi/items/edd75f7ee47471a98670)

マネージドIDを利用してBlobストレージにデータを書き込んでみます。
よくあるパターンでは各プログラム言語用に提供されているSDKを利用して行いますが、今回はIMDSの解説も兼ねREST APIで行ってみます。
下記のサンプルのストレージアカウント名は「imdstest」、Blobコンテナ名も同じく「imdstest」です。

```bash
# Blobに書き込むテストデータを作成
$ echo "Test data" > testdata

# IMDSから認証用アクセストークンの取得
$ bearer_token=$(curl -H Metadata:true "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2021-05-01&resource=https://imdstest.blob.core.windows.net/" | jq -r '.["access_token"]')

# トークンを利用しBlobストレージへテストデータをアップロード
$ curl -X PUT "https://imdstest.blob.core.windows.net/imdstest/testdata" \
> -H "Authorization: Bearer $bearer_token" \
> -H "x-ms-date: $(TZ=GMT date '+%a, %d %h %Y %H:%M:%S %Z')" \
> -H "x-ms-version: 2021-04-10" \
> -H "Content-Length: $(stat -c %s testdata)" \
> -H "x-ms-blob-type: BlockBlob" \
> -T testdata

# アップロードしたBlobの内容を取得
$ curl "https://imdstest.blob.core.windows.net/imdstest/testdata" \
> -H "Authorization: Bearer $bearer_token" \
> -H "x-ms-date: $(TZ=GMT date '+%a, %d %h %Y %H:%M:%S %Z')" \
> -H "x-ms-version: 2021-04-10"
Test data
```
SDKを利用していると意識することは殆どないですが、上記のようにマネージドIDの認証にはIMDSが利用されています。

## スケジュールされたイベント
VMに対して発生する予定のイベント情報を取得できます。
このエンドポイントのみ[冒頭に挙げたメタデータバージョン](#メタデータのバージョン)とは異なる独自のバージョン管理がされているようです。
[公式ドキュメント](https://docs.microsoft.com/ja-jp/azure/virtual-machines/linux/scheduled-events)に記載されている最新バージョンの「2020-07-01」を使用することとします。

```bash
$ curl -H Metadata:true http://169.254.169.254/metadata/scheduledevents?api-version=2020-07-01
```
特にイベントがなければ以下のようにEventsが空の応答が返ってきます。

```json
{
  "DocumentIncarnation": 0,
  "Events": []
}
```

さて「イベントとはなんぞや？」ですが、[公式ドキュメント](https://docs.microsoft.com/ja-jp/azure/virtual-machines/linux/scheduled-events)によると以下の5種が存在します。

・Freeze
・Reboot
・Redeploy
・Preempt
・Terminate

これらのイベントが発生することを事前に察知し、サービス影響が発生しないように手当をしたり管理者へ通知を行うという事に利用できます。

それぞれのイベントがどういったものか順に確認していきましょう。

### Freeze
Azureのプラットフォーム側メンテナンスによって発生するVMの一時停止のことです。 [この辺り](https://docs.microsoft.com/ja-jp/azure/virtual-machines/maintenance-and-updates#maintenance-that-doesnt-require-a-reboot)で述べられています。
Azure側の都合で発生するものなのでテストができないですが、[Azureサポートの人の記事によると](https://jpaztech1.z11.web.core.windows.net/%E8%87%AA%E5%88%86%E3%81%AEVM%E3%81%AB%E4%BD%95%E3%81%8C%E8%B5%B7%E3%81%8D%E3%82%8B%E3%81%8B%E6%8A%8A%E6%8F%A1%E3%81%99%E3%82%8BScheduledEvents(InstanceMetadataService)%E3%81%AE%E3%81%94%E7%B4%B9%E4%BB%8B.html)以下のような応答が得られるようです。

```json
{
  "DocumentIncarnation": 10,
  "Events": [
    {
      "EventId": "9C7442D3-9206-45D8-8DA8-26A94E577C51",
      "EventStatus": "Scheduled",
      "EventType": "Freeze",
      "ResourceType": "VirtualMachine",
      "Resources": [
        "_tidv2promo"
      ],
      "NotBefore": "Thu, 12 Oct 2017 14:59:54 GMT"
    }
  ]
}
```
"EventType"が「Freeze」となっていますね。
"NotBefore"にメンテナンス開始の予定時刻が表示されています。おおむね開始予定の15分ほど前から通知を得られるようです。

### Reboot
VMの再起動予定を示します。
Azureポータルから該当のVMの再起動ボタンをクリックしたのち、IMDSにアクセスすると以下のような応答が得られます。

```json
{
  "DocumentIncarnation": 1,
  "Events": [
    {
      "EventId": "11DA3DE7-AE41-41EE-9369-21520E37F5EB",
      "EventStatus": "Scheduled",
      "EventType": "Reboot",
      "ResourceType": "VirtualMachine",
      "Resources": [
        "imdstest_0"
      ],
      "NotBefore": "Sat, 19 Feb 2022 10:16:14 GMT",
      "Description": "Virtual machine is going to be restarted as requested by authorized user.",
      "EventSource": "User",
      "DurationInSeconds": -1
    }
  ]
}
```
"EventType"が「Reboot」となっています。
"NotBefore"に表示されている予定時刻は再起動を実行してから15分後となっており、この時刻になるとVMが再起動されます。

### Redeploy
VMの別ノードへの再デプロイを示します。再デプロイはVMのOSの再起動を伴います。
Azure CLIがインストールされている端末から以下のコマンドを実行すると再デプロイが可能です。
(VMSSは再デプロイができないので、これのみ別でVMを建てて検証しました)

```bash
$ az vm redeploy --resource-group <リソースグループ名> --name <VM名>
```
コマンド実行後VMからIMDSにアクセスすると、以下のような応答を得られます。

```json
{
  "DocumentIncarnation": 1,
  "Events": [
    {
      "EventId": "25872E87-7EE9-415C-9B9F-92D8B084DAD0",
      "EventStatus": "Scheduled",
      "EventType": "Redeploy",
      "ResourceType": "VirtualMachine",
      "Resources": [
        "IMDStest2"
      ],
      "NotBefore": "Sat, 19 Feb 2022 10:33:47 GMT",
      "Description": "",
      "EventSource": "User",
      "DurationInSeconds": -1
    }
  ]
}
```
"EventType"が「Redeploy」、"NotBefore"に表示されている予定時刻はコマンド実行から10分後となっており、この時刻になるとVMが再デプロイされます。

### Preempt
[スポットインスタンス](https://azure.microsoft.com/ja-jp/services/virtual-machines/spot/)の終了を指します。
スポットインスタンスは作成時に指定した価格をAzure提供価格が上回った場合にVMが終了してしまうので、IMDSを利用することで終了の予定を確認することができます。

Azure CLIには、この動作をシミュレートするコマンドがあります。
※シミュレートと表現してますが、実際にVMが削除されます。実行する際は要注意。

```bash
# VMの場合
az vm simulate-eviction --resource-group <リソースグループ名> --name <VM名>
# VMSSの場合
az vmss simulate-eviction --resource-group <リソースグループ名> --name <VMSS名> --instance-id <インスタンスID>
```
上記コマンドを実行したのちIMDSにアクセスすると、以下のような応答を得られます。

```json
{
  "DocumentIncarnation": 4,
  "Events": [
    {
      "EventId": "D1556AEB-3E61-4A61-8D2D-286251AB049E",
      "EventStatus": "Scheduled",
      "EventType": "Preempt",
      "ResourceType": "VirtualMachine",
      "Resources": [
        "imdstest_0"
      ],
      "NotBefore": "Sat, 19 Feb 2022 10:38:34 GMT",
      "Description": "",
      "EventSource": "Platform",
      "DurationInSeconds": -1
    }
  ]
}
```
"EventType"が「Preempt」、"NotBefore"の予定時刻はコマンド実行から約30秒後です。この時刻になるとVMが終了(削除)されます。

### Terminate
VMSSのスケールイン時の削除を示します。
この通知を得るためには、VMSSの設定で終了通知を有効にしておく必要があります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274613/b3709593-6b5b-958d-12a1-0778af5b8e49.png)

手動でスケールアウト・スケールインを実行し、削除予定のノードでIMDSにアクセスすると以下の応答を得られます。

```json
{
  "DocumentIncarnation": 1,
  "Events": [
    {
      "EventId": "42A6758D-AEDE-4FB6-B64B-1222EC434D49",
      "EventStatus": "Scheduled",
      "EventType": "Terminate",
      "ResourceType": "VirtualMachine",
      "Resources": [
        "imdstest_5"
      ],
      "NotBefore": "Sat, 19 Feb 2022 12:18:05 GMT",
      "Description": "",
      "EventSource": "User",
      "DurationInSeconds": -1
    }
  ]
}
```
"EventType"が「Terminate」、"NotBefore"の予定時刻はスケールイン実行からインスタンスの終了の遅延で指定した時間後（今回は5分後）となっています。この時刻になるとVMが削除されスケールインが完了します。

# 補足: 「スケジュールされたイベント」の手動実行
「スケジュールされたイベント」は通常ではNotBeforeで通知された時間になるまで実行されません。しかし、この時間を待たずに早期に実行してほしいシーンもあると思います。
その場合、取得した"EventId"の値を以下のようにPOSTすることで、イベントの実行を承認できます。

```bash
curl -H Metadata:true -X POST -d '{"StartRequests": [{"EventId": "<取得したEventId>"}]}' http://169.254.169.254/metadata/scheduledevents?api-version=2020-07-01
```

# 補足2: 「スケジュールされたイベント」の挙動について
「スケジュールされたイベント」には有効化・無効化の概念があります。
[公式ドキュメント](https://docs.microsoft.com/ja-jp/azure/virtual-machines/linux/scheduled-events#enabling-and-disabling-scheduled-events)に以下のように記載されています。

> スケジュールされたイベントは、ユーザーが初めてイベントを要求したときに、サービスに対して有効になります。 最初の呼び出しでは、最大 2 分の応答遅延が発生すると予想されます。
> スケジュールされたイベントは、サービスが 24 時間要求を行わないと、サービスに対して無効になります。

この有効化によってどういった影響があるかというと、

- 有効化していない（一度もエンドポイントにアクセスしていない）状態でAzureポータルからVMを再起動 … 即時に再起動が実行される。
- 有効化した（エンドポイントにアクセスした）状態でAzureポータルからVMを再起動 … 15分後に再起動がスケジュールされる。

という挙動になります。
（ドキュメントを見てもいまいち読み取れないのですが、実際試した限りではこのような挙動になりました…）

# おわりに
Azure上のVMを運用をしていく中で、Azure Instance Metadata Serviceから取得できる情報は非常に有用です。
（と、私は思ってるのですが、なぜか日本語で解説しているものが公式ドキュメント以外にほとんどない現状…。あまり使われてない？）
うまく活用して、運用の効率化に繋げていきましょう