JavaScriprt(Node.js)なAzureSDKを使ってAzureのVMを起動/停止(割り当て解除)する

# はじめに

JavaScriptのAzureSDKを使用してAzureにアクセスを行う手順です。
Microsoftのドキュメントを参照すれば分かることですが、「ちょっと動かしてみる」ができる記事があればと思い投稿しました。

既にAzureを使用してVMを使ったりしている方を対象としています。

# 本記事の動作確認環境
本記事は以下の環境で動作確認を行っています。

* Windows10 Professional 20H2 
* WSL2.0
    * Node.js v14.15.4
    * azure-cli 2.0.81
        * Python (Linux) 3.8.2 (default, Jul 16 2020, 14:00:26)

# 必要なもの
* Azureアカウント
    * 持っていなければ作りましょう。
        * https://azure.microsoft.com/ja-jp/free/
    * 本記事ではVMの起動を例にしているので、VMが作られていると良いかもしれません。
* Azure CLI 2.0
    * お使いのOSで動くものをインストールしてください。
        * https://docs.microsoft.com/ja-jp/cli/azure/install-azure-cli
* Node.js
    * お使いのOSで動くNode.jsをインストールしてください。
        * https://nodejs.org/ja/

# 準備
VMの起動や停止(割り当て解除)で使用する情報やパラメータを集めます。
## 資格情報を取得する

SDKでAzureに接続するには資格情報が要ります。
資格情報の種類はいくつかありますが、今回は認証ファイルを使用する方法にします。
`az ad sp create-for-rbac` コマンドで資格情報を作成し、`credential.json`というファイル名で保存します。

```
$ az login
$ az ad sp create-for-rbac -n sp-sawada-sample --sdk-auth > credential.json
Changing "sp-sawada-sample" to a valid URI of "http://sp-sawada-sample", which is the required format used for service principal names
Found an existing application instance of "***************************". We will patch it
Creating a role assignment under the scope of "/subscriptions/*****************************"
```

資格情報名を `-n {資格情報名}` で指定し、`--sdk-auth` オプションを付加することでSDKで使用する資格情報を作成します。
※`--sdk-auth`のオプションに気づかず、すこしハマりかけました。[^mslink]
`--role` オプションで作成する資格情報のロールを設定することができ、省略すると `Contributor(共同作成者)`のロールが付与されます。
本記事のサンプルではContributorを使用しますが、必要なロールのみを設定したほうがよいでしょう。

※注意 `--role` オプションの省略は今後のリリースで不可となるようです。
https://docs.microsoft.com/en-us/cli/azure/ad/sp?view=azure-cli-latest#az_ad_sp_create_for_rbac
> WARNING: In a future release, this command will NOT create a 'Contributor' role assignment by default. > If needed, use the --role argument to explicitly create a role assignment.


成功すると以下のような資格情報が記載されたjsonが出力されます。

```json:credential.json
{
  "clientId": "*****************************",
  "clientSecret": "*****************************",
  "subscriptionId": "*****************************",
  "tenantId": "*****************************",
  "activeDirectoryEndpointUrl": "https://login.microsoftonline.com",
  "resourceManagerEndpointUrl": "https://management.azure.com/",
  "activeDirectoryGraphResourceId": "https://graph.windows.net/",
  "sqlManagementEndpointUrl": "https://management.core.windows.net:8443/",
  "galleryEndpointUrl": "https://gallery.azure.com/",
  "managementEndpointUrl": "https://management.core.windows.net/"
}
```

Azure Portal のサブスクリプション -> アクセス制御(IAM) -> ロールの割り当て を見ると、作成した資格情報のユーザーが作成されていることが確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/518444/67267694-edf1-f6e7-e215-04e9360c8b4f.png)

## SDKで必要なパラメータを確認する

VMの起動と停止(割り当て解除)では以下のパラメータが必要になります。

* サブスクリプションID
* リソースグループ名
* VM名

Azure Portal や Azure CLI で確認します。
以下に、Azure CLI での確認例を記載します。

```:サブスクリプションID
$ az account list
[
  {
    "cloudName": "AzureCloud",
    "id": "<サブスクリプションID>",
    "isDefault": true,
    "name": "従量課金",
    "state": "Enabled",
    "tenantId": "*****************************",
    "user": {
      "name": "*****************************",
      "type": "user"
    }
  }
]
```

```:リソースグループ名とVM名
$ az vm list -o table
Name              ResourceGroup         Location    Zones
----------------  --------------------  ----------  -------
<VM名>            <リソースグループ名>    japaneast
sawada-sample-vm  SAMPLE-RG             japaneast
```

## 必要なパッケージをインストールする

```bash
npm install "@ms-rest-nodeauth"
npm install "@azure/arm-compute"
```

# スクリプトを作る

VMの起動と停止(割り当て解除)をするスクリプトを作ります。

```javascript:index.js
const msRestNodeAuth = require('@azure/ms-rest-nodeauth');
const { ComputeManagementClient } = require('@azure/arm-compute');

const SUBSCRIPTION_ID = '<SUBSCRIPTION_ID>'
const RESOURCE_GROUP_NAME = '<RESOURCE_GROUP_NAME>';
const VM_NAME = '<VM_NAME>';

function createRestNode() {
    return new Promise((resolve, reject) => {
        msRestNodeAuth.loginWithAuthFile({
            filePath: 'credential.json'
        }, async (err, credentials) => {
                if (err) {
                    reject(err)
                } else {
                    resolve({credentials: credentials});
                }
            }
        );
    });
}

function vmStart(restNode) {
    return new Promise((resolve, reject) => {
        var client = new ComputeManagementClient(
            restNode.credentials,
            SUBSCRIPTION_ID
        );
        client.virtualMachines.start(
            RESOURCE_GROUP_NAME,
            VM_NAME
        ).then((result) => {
            resolve(result);
        });
    });
}

function vmStop(restNode) {
    return new Promise((resolve, reject) => {
        var client = new ComputeManagementClient(
            restNode.credentials,
            SUBSCRIPTION_ID
        );
        client.virtualMachines.deallocate(
            RESOURCE_GROUP_NAME,
            VM_NAME
        ).then((result) => {
            resolve(result);
        });
    });
}

async function main () {
    let node = await createRestNode();
    let result = await vmStart(node);
    // let result = await vmStop(node);
    console.log("The result is:", result);
}

main().then(() => {
    process.exit(0);
}).catch(error => {
    console.log('ERROR', error);
    process.exit(1);
});
```

## VMを起動してみる

`let result = await vmStart(node);` のコメントアウトを外した状態にして ``` node ./index.js``` で実行します。
以下のレスポンスが返ります。

```json
The result is: {
  startTime: '2021-02-05T19:14:14.4602947+00:00',
  endTime: '2021-02-05T19:14:17.4291149+00:00',
  status: 'Succeeded',
  name: '************************************'
}
```

VMが起動できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/518444/8b72f292-ccf4-3981-d356-0b28b9f956ed.png)

## VMを停止(割り当て解除)してみる

`let result = await vmStop(node);` のコメントアウトを外した状態にして ``` node ./index.js``` で実行します。
以下のレスポンスが返ります。

```json
The result is: {
  startTime: '2021-02-05T19:22:24.7855197+00:00',
  endTime: '2021-02-05T19:22:27.7856109+00:00',
  status: 'Succeeded',
  name: '************************************'
}
```

VMを停止(割り当て解除)できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/518444/67140730-fda7-3261-6d54-c937260c4407.png)

# スクリプトの解説
「資格情報を取得する」で記載したとおり、資格情報には種類があり、認証のメソッドもそれぞれが用意されています。これらのメソッドでAzureに対して認証を行い、資格情報オブジェクトを取得することができます。
今回は認証ファイル`credential.json` を使用してAzureに接続するので、`loginWithAuthFile` メソッドを使用します。

```javascript:資格情報オブジェクトを取得する
function createRestNode() {
    return new Promise((resolve, reject) => {
        msRestNodeAuth.loginWithAuthFile({
            filePath: 'credential.json'
        }, async (err, credentials) => {
                if (err) {
                    reject(err)
                } else {
                    resolve({credentials: credentials});
                }
            }
        );
    });
}
```

資格情報オブジェクトとサブスクリプションIDで`ComputeManagementClient`でクライアントオブジェクトを作成します。

```javascript:クライアントオブジェクトの作成
var client = new ComputeManagementClient(
   restNode.credentials,
   <サブスクリプションID>
);
``` 

リソースグループ名とVM名を使用して、クライアントオブジェクトの`virtualMachines.start`でVMの起動を行います。

```javascript:VMを起動する
client.virtualMachines.start(
    RESOURCE_GROUP_NAME,
    VM_NAME
).then((result) => {
    resolve(result);
});
```

リソースグループ名とVM名を使用して、クライアントオブジェクトの`virtualMachines.deallocate`でVMの停止(割り当て解除)を行います。

```javascript:VMを停止(割り当て解除)する
client.virtualMachines.deallocate(
    RESOURCE_GROUP_NAME,
    VM_NAME
).then((result) => {
    resolve(result);
});
```

# おまけ
本記事のスクリプトをGitHubに載せています。
https://github.com/Piecemeal-Technology-Inc/azure-vm-startup-by-nodejs

# 参考URL

**Microsoftのオフィシャルページ**
https://docs.microsoft.com/ja-jp/azure/developer/javascript/core/what-is-azure-for-javascript-development
(本記事と同様にAzureに接続するまでの手順が記載されています[^mslink]。)

**各サービスに対応するnpmパッケージ一覧**
https://docs.microsoft.com/ja-jp/javascript/api/overview/azure/

**AzureSDK(JavaScript)のドキュメント**
https://azure.github.io/azure-sdk-for-js/

[^mslink]: プリンシパル(資格情報)の作り方は↓を参照と書いてあるのですが、<br/>https://docs.microsoft.com/ja-jp/cli/azure/create-an-azure-service-principal-azure-cli<br/>このページではSDK用のプリンシパルの作り方には触れておらず `--sdk-auth` の記載が無いので、<br/>書いてあるとおりに進めるとハマります。