ARMテンプレートでVirtual Machine Scale Setをデプロイしてみる

この記事は[エムティーアイ Advent Calender 2016](http://qiita.com/advent-calendar/2016/mti "エムティーアイ Advent Calender 2016")の22日目の記事です。

Advent Calenderも残すところ4日となった本日は、今流行りのAzure Resource Managerテンプレート(以降Armテンプレート)について書いていきます。  
**これからArmテンプレートを勉強したい、とりあえず動く環境をテンプレートで作りたい**  
そんな方におすすめです。


# やりたいこと  
1. ArmテンプレートでLinux Virtual Machineをデプロイします。  
2. テンプレートデプロイ時にjava・Tomcatを自動でインストールします。  
今回作成するテンプレートを使用すると、同一環境のVMを一度に複数台デプロイ可能です。  

ArmテンプレートはJsonで書かれています。  
お好きなエディタで編集できますが、VisualStudioを使うとインテリセンスが使えたり、プロジェクトテンプレートを自由に生成してくれたりと何かと便利です。  


# 実行環境
* Windows10
* VisualStudio 2015 Community  
* Azure SDK 2.9  
* PowerShell 5.0
※VisualStudioでテンプレートを作成するには、AzureSDK2.6以上が必要です。  

# VisualStudioでテンプレートを作成する  
では早速、Visual Studioで新規プロジェクトを作成しましょう。  
「新規作成」＞「プロジェクト」からAzureリソースマネージャを選択します。
![新規プロジェクト作成.PNG](https://qiita-image-store.s3.amazonaws.com/0/150361/2904113e-36de-a59d-abc2-6e738c95c7a7.png)
  

様々なリソースが選択できますが、今回は「Linux Virtual Machine Scale Set」を選択します。  
[仮想マシンスケールセットの概要](https://docs.microsoft.com/ja-jp/azure/virtual-machine-scale-sets/virtual-machine-scale-sets-overview)  
 ![テンプレート選択.PNG](https://qiita-image-store.s3.amazonaws.com/0/150361/4e76dcd7-0f63-baf3-9092-1fa73db459d3.png)

作成したプロジェクトは以下のような構成になっています。  
デプロイ用のPowershellスクリプトも自動で生成してくれています。  
![ソリューションエクスプローラ.PNG](https://qiita-image-store.s3.amazonaws.com/0/150361/8f8d077f-cb2e-58b1-085a-385c17a66707.png)


簡単に各ファイルの説明をします。
* Deploy-AzureResourceGroup.ps1  
デプロイ用のPowerShellスクリプト。  
PowerShell 5.0 で動作します。  
Powersshell 5.0 Windows10からは標準で搭載されていますが、Windows 7・8の場合はアップデートが必要です。  

* LinuxVirtualMachineScaleSet.json  
こちらが所謂Armテンプレートです。リソースの情報を記載しています。  
テンプレートは以下のような構成になっています。  
  * parameters  
  デプロイ時にユーザが指定する値を定義しています。  
  * variables  
  テンプレート内で使用する変数を定義しています。  
  乱数を自動生成することもできます。
  * resourrces  
  Virtual MachineやNIC等のデプロイされるリソースを定義しています。  


* LinuxVirtualMachineScaleSet.parameters.json  
Powershellからデプロイする時に使用します。  
ArmテンプレートのParametersで定義した項目の値を記述しておくことで、手入力の手間を省きます。
　　

自動生成されたテンプレート「LinuxVirtualMachineScaleSet.json」はそのままデプロイすることができます。
今回はデフォルトでUbuntuになっているOSをCentOSに変更してからデプロイします。

## OSをCentOSに変更する。  
CentOS 7.2 に変更します。  

ParametersのubuntuOsVersionを以下に差し替えます。

```json:LinuxVirtualMachineScaleSet.json
"centOsVersion": {
  "type": "string",
  "defaultValue": "7.2",
  "metadata": {
    "description": "The Ubuntu version for the VM. This will pick a fully patched image of this given Ubuntu version. Allowed values are: 15.10, 14.04.4-LTS."
  }
}
```  

variablesのosTypeを以下に差し替えます。  

```json:LinuxVirtualMachineScaleSet.json
"osType": {
  "publisher": "OpenLogic",
  "offer": "CentOS",
  "sku": "[parameters('centOSVersion')]",
  "version": "latest"
}
```
変更点は以上です。  

## パラメータを用意する  
自動生成されたLinuxVirtualMachineScaleSet.parameters.jsonのValueに値を記述します。  
adminUserPasswordはSecureStringで指定されているため、  
パラメータファイルには記述せず、Powershell実行時に直接入力します。  

```json:LinuxVirtualMachineScaleSet.parameters.json
{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "vmssName": {
            "value": "testVmss"
        },
        "instanceCount": {
            "value": 2
        },
      "adminUsername": {
        "value": "testUser"
      }
    }
}
```

# デプロイしてみる  
それではテンプレートをデプロイしてみましょう。  
今回はPowerShellを使用したデプロイ方法を紹介します。  

まずデプロイ先のサブスクリプションのアカウントでログインします。  
![Powershell_ログイン.PNG](https://qiita-image-store.s3.amazonaws.com/0/150361/a61189ec-7777-1df3-56de-09a60d1106d7.png)


ログインしたアカウントに複数のサブスクリプションが紐づいている場合は、今回デプロイするサブスクリプションを選択する必要があります。 
![Powershell_選択_Ink_LI.jpg](https://qiita-image-store.s3.amazonaws.com/0/150361/0240c52c-4ff2-14aa-9782-357d817ed517.jpeg)
 

ログインできたら、VisualStudioが生成してくれたDeploy-AzureResourceGroup.ps1を実行すればOKです。  


このスクリプトは作成するリソースグループのリージョンを聞いてくるので、適当なリージョン名を入力します。
![Powershell_リージョン.PNG](https://qiita-image-store.s3.amazonaws.com/0/150361/a0857d0d-3112-66e5-e227-0f34b099d43a.png)
  
西日本→japanwest
東日本→japaneast  
といった要領です。特別な理由がない限り、日本のどちらかで問題ありません。　　

先述の通りパスワードはSecureStringで定義しているので、ここで入力する必要があります。
ダイアログボックスが表示されるので、適当なパスワードを指定してください。
**※VMのパスワードは12文字以上でなければいけないので注意してください。**

デプロイが完了したらポータルで確認してみましょう。
作成したリソースが完成していますね。
![デプロイ成功.PNG](https://qiita-image-store.s3.amazonaws.com/0/150361/66948c25-5fdb-6aed-0915-32b1d6a40028.png)


# CustomScriptで、デプロイ時にJava・Tomcatをインストールする  
さて、実際にVM Scale setのデプロイができましたが、  
デプロイした後に、手動でマシンのセットアップを行うの大変ですよね。  
VMに用意されている拡張機能のカスタムスクリプトを使用すると、デプロイ時に指定したコマンドを実行することができます。  
次はこの拡張機能を使ってTomcatとJavaをインストールできるようにしてみます。  

ストレージに保存してあるシェルスクリプトをダウンロードし実行します。  
VirtualMachineScaleSetのProfilesに以下のようなExtensionProfileを付け足してください。  

```json:LinuxVirtualMachineScaleSet.json
        "extensionProfile": {
            "extensions": [
              {
                "name": "CustomScript",
                "properties": {
                  "publisher": "Microsoft.Azure.Extensions",
                  "type": "CustomScript",
                  "typeHandlerVersion": "2.0",
                  "settings": {
                    "fileUris": [ "[variables('scriptUri')]" ]
                  },
                  "protectedSettings": {
                    "commandToExecute": "bash testscript.sh",
                    "storageAccountName": "[variables('scriptsStorageAccount')]",
                    "storageAccountKey": "[listkeys(resourceId('Microsoft.Storage/storageAccounts', variables('scriptsStorageAccount')), '2015-06-15').key1]"
                  }
                }
              }
            ]
          }
```

今回用意したシェルスクリプトは以下のようになっています。  
カスタムスクリプトでは**Root権限でコマンドが実行される**ため、sudoする必要はありません。

```shell-session:testscript.sh
#!/bin/sh

yum install java-1.7.0-openjdk
useradd -s /sbin/nologin tomcat
cd ~
wget http://ftp.jaist.ac.jp/pub/apache/tomcat/tomcat-7/v7.0.53/bin/apache-tomcat-7.0.53.tar.gz
tar -xzvf ~/apache-tomcat-7.0.53.tar.gz
mkdir /opt/apache-tomcat
mv ~/apache-tomcat-7.0.53 /opt/apache-tomcat
chown -R tomcat:tomcat /opt/apache-tomcat
```

テンプレート全体は以下のようになっています。  

```json:LinuxVirtualMachineScaleSet.json
{
  "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "vmSize": {
      "type": "string",
      "defaultValue": "Standard_A1",
      "minLength": 1
    },
    "centOsVersion": {
      "type": "string",
      "defaultValue": "7.2",
      "metadata": {
        "description": "The Ubuntu version for the VM. This will pick a fully patched image of this given Ubuntu version. Allowed values are: 15.10, 14.04.4-LTS."
      }
    },
    "vmssName": {
      "type": "string",
      "metadata": {
        "description": "DNS name used for public IP addresses and as base for naming other resources. Must be globally unique and 3 to 61 characters long."
      },
      "minLength": 3,
      "maxLength": 61
    },
    "instanceCount": {
      "type": "int",
      "metadata": {
        "description": "Number of VM instances (100 or less)"
      },
      "minValue": 1,
      "maxValue": 100
    },
    "adminUsername": {
      "type": "string",
      "minLength": 1,
      "metadata": {
        "description": "Administrator username on all VMs"
      }
    },
    "adminPassword": {
      "type": "securestring",
      "metadata": {
        "description": "Administrator password on all VMs"
      }
    }
  },
  "variables": {
    "location": "[resourceGroup().location]",
    "storageAccountType": "Standard_LRS",
    "namingInfix": "[toLower(substring(concat(parameters('vmssName'), uniqueString(resourceGroup().id)), 0, 9))]",
    "longNamingInfix": "[toLower(parameters('vmssName'))]",
    "newStorageAccountSuffix": "[concat(variables('namingInfix'), 'sa')]",
    "uniqueStringArray": [
      "[concat(uniqueString(concat(resourceGroup().id, variables('newStorageAccountSuffix'), '0')), variables('newStorageAccountSuffix'))]",
      "[concat(uniqueString(concat(resourceGroup().id, variables('newStorageAccountSuffix'), '1')), variables('newStorageAccountSuffix'))]",
      "[concat(uniqueString(concat(resourceGroup().id, variables('newStorageAccountSuffix'), '2')), variables('newStorageAccountSuffix'))]",
      "[concat(uniqueString(concat(resourceGroup().id, variables('newStorageAccountSuffix'), '3')), variables('newStorageAccountSuffix'))]",
      "[concat(uniqueString(concat(resourceGroup().id, variables('newStorageAccountSuffix'), '4')), variables('newStorageAccountSuffix'))]"
    ],
    "vhdContainerName": "[concat(variables('namingInfix'), 'vhd')]",
    "osDiskName": "[concat(variables('namingInfix'), 'osdisk')]",
    "addressPrefix": "10.0.0.0/16",
    "subnetPrefix": "10.0.0.0/24",
    "virtualNetworkName": "[concat(variables('namingInfix'), 'vnet')]",
    "publicIPAddressName": "[concat(variables('namingInfix'), 'pip')]",
    "subnetName": "[concat(variables('namingInfix'), 'subnet')]",
    "loadBalancerName": "[concat(variables('namingInfix'), 'lb')]",
    "publicIPAddressID": "[resourceId('Microsoft.Network/publicIPAddresses', variables('publicIPAddressName'))]",
    "lbID": "[resourceId('Microsoft.Network/loadBalancers', variables('loadBalancerName'))]",
    "natPoolName": "[concat(variables('namingInfix'), 'natpool')]",
    "bePoolName": "[concat(variables('namingInfix'), 'bepool')]",
    "nicName": "[concat(variables('namingInfix'), 'nic')]",
    "ipConfigName": "[concat(variables('namingInfix'), 'ipconfig')]",
    "frontEndIPConfigID": "[concat(variables('lbID'), '/frontendIPConfigurations/loadBalancerFrontEnd')]",
    "osType": {
      "publisher": "OpenLogic",
      "offer": "CentOS",
      "sku": "[parameters('centOSVersion')]",
      "version": "latest"
    },
    "imageReference": "[variables('osType')]",
    "scriptsStorageAccount": "vmssjavasampleaccount",
    "scriptUri": "スクリプトを保存したURL"
  },
  "resources": [
    {
      "type": "Microsoft.Network/virtualNetworks",
      "name": "[variables('virtualNetworkName')]",
      "location": "[variables('location')]",
      "apiVersion": "2015-06-15",
      "tags": {
        "displayName": "VirtualNetwork"
      },
      "properties": {
        "addressSpace": {
          "addressPrefixes": [
            "[variables('addressPrefix')]"
          ]
        },
        "subnets": [
          {
            "name": "[variables('subnetName')]",
            "properties": {
              "addressPrefix": "[variables('subnetPrefix')]"
            }
          }
        ]
      }
    },
    {
      "type": "Microsoft.Storage/storageAccounts",
      "name": "[variables('uniqueStringArray')[copyIndex()]]",
      "location": "[variables('location')]",
      "apiVersion": "2015-06-15",
      "tags": {
        "displayName": "StorageAccounts"
      },
      "copy": {
        "name": "storageLoop",
        "count": "[length(variables('uniqueStringArray'))]"
      },
      "properties": {
        "accountType": "[variables('storageAccountType')]"
      }
    },
    {
      "type": "Microsoft.Network/publicIPAddresses",
      "name": "[variables('publicIPAddressName')]",
      "location": "[variables('location')]",
      "apiVersion": "2015-06-15",
      "tags": {
        "displayName": "PublicIP"
      },
      "properties": {
        "publicIPAllocationMethod": "Dynamic",
        "dnsSettings": {
          "domainNameLabel": "[variables('longNamingInfix')]"
        }
      }
    },
    {
      "type": "Microsoft.Network/loadBalancers",
      "name": "[variables('loadBalancerName')]",
      "location": "[variables('location')]",
      "apiVersion": "2015-06-15",
      "tags": {
        "displayName": "LoadBalancer"
      },
      "dependsOn": [
        "[resourceId('Microsoft.Network/publicIPAddresses', variables('publicIPAddressName'))]"
      ],
      "properties": {
        "frontendIPConfigurations": [
          {
            "name": "LoadBalancerFrontEnd",
            "properties": {
              "publicIPAddress": {
                "id": "[variables('publicIPAddressID')]"
              }
            }
          }
        ],
        "backendAddressPools": [
          {
            "name": "[variables('bePoolName')]"
          }
        ],
        "inboundNatPools": [
          {
            "name": "[variables('natPoolName')]",
            "properties": {
              "frontendIPConfiguration": {
                "id": "[variables('frontEndIPConfigID')]"
              },
              "protocol": "tcp",
              "frontendPortRangeStart": 50000,
              "frontendPortRangeEnd": 50099,
              "backendPort": 22
            }
          }
        ]
      }
    },
    {
      "type": "Microsoft.Compute/virtualMachineScaleSets",
      "name": "[variables('namingInfix')]",
      "location": "[variables('location')]",
      "apiVersion": "2016-03-30",
      "tags": {
        "displayName": "VMScaleSet"
      },
      "dependsOn": [
        "storageLoop",
        "[resourceId('Microsoft.Network/loadBalancers', variables('loadBalancerName'))]",
        "[resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]"
      ],
      "sku": {
        "name": "[parameters('vmSize')]",
        "tier": "Standard",
        "capacity": "[parameters('instanceCount')]"
      },
      "properties": {
        "overprovision": "true",
        "upgradePolicy": {
          "mode": "Manual"
        },
        "virtualMachineProfile": {
          "storageProfile": {
            "osDisk": {
              "vhdContainers": [
                "[concat('https://', variables('uniqueStringArray')[0], '.blob.core.windows.net/', variables('vhdContainerName'))]",
                "[concat('https://', variables('uniqueStringArray')[1], '.blob.core.windows.net/', variables('vhdContainerName'))]",
                "[concat('https://', variables('uniqueStringArray')[2], '.blob.core.windows.net/', variables('vhdContainerName'))]",
                "[concat('https://', variables('uniqueStringArray')[3], '.blob.core.windows.net/', variables('vhdContainerName'))]",
                "[concat('https://', variables('uniqueStringArray')[4], '.blob.core.windows.net/', variables('vhdContainerName'))]"
              ],
              "name": "[variables('osDiskName')]",
              "caching": "ReadOnly",
              "createOption": "FromImage"
            },
            "imageReference": "[variables('imageReference')]"
          },
          "osProfile": {
            "computerNamePrefix": "[variables('namingInfix')]",
            "adminUsername": "[parameters('adminUsername')]",
            "adminPassword": "[parameters('adminPassword')]"
          },
          "networkProfile": {
            "networkInterfaceConfigurations": [
              {
                "name": "[variables('nicName')]",
                "properties": {
                  "primary": true,
                  "ipConfigurations": [
                    {
                      "name": "[variables('ipConfigName')]",
                      "properties": {
                        "subnet": {
                          "id": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', resourceGroup().name, '/providers/Microsoft.Network/virtualNetworks/', variables('virtualNetworkName'), '/subnets/', variables('subnetName'))]"
                        },
                        "loadBalancerBackendAddressPools": [
                          {
                            "id": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', resourceGroup().name, '/providers/Microsoft.Network/loadBalancers/', variables('loadBalancerName'), '/backendAddressPools/', variables('bePoolName'))]"
                          }
                        ],
                        "loadBalancerInboundNatPools": [
                          {
                            "id": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', resourceGroup().name, '/providers/Microsoft.Network/loadBalancers/', variables('loadBalancerName'), '/inboundNatPools/', variables('natPoolName'))]"
                          }
                        ]
                      }
                    }
                  ]
                }
              }
            ]
          },
          "extensionProfile": {
            "extensions": [
              {
                "name": "CustomScript",
                "properties": {
                  "publisher": "Microsoft.Azure.Extensions",
                  "type": "CustomScript",
                  "typeHandlerVersion": "2.0",
                  "settings": {
                    "fileUris": [ "[variables('scriptUri')]" ]
                  },
                  "protectedSettings": {
                    "commandToExecute": "bash testscript.sh",
                    "storageAccountName": "[variables('scriptsStorageAccount')]",
                    "storageAccountKey": "[listkeys(resourceId('Microsoft.Storage/storageAccounts', variables('scriptsStorageAccount')), '2015-06-15').key1]"
                  }
                }
              }
            ]
          }
        }
      }
    }
  ]
}

```

先ほどと同様にデプロイしてください。

できあがったScaleSetにsshで接続してみると、/opt/apache-tomcatができあがっています。
![tomcat.PNG](https://qiita-image-store.s3.amazonaws.com/0/150361/abe949b4-55b7-756a-5538-a22f74cd54f7.png)


※ScaleSetへの接続は、作成したLoadBalancerの「フロントエンドIPプール」のIPとポートを使用します。 
![ポート.PNG](https://qiita-image-store.s3.amazonaws.com/0/150361/8805dc97-3fec-b24d-5956-097db527f628.png)
 


# まとめ
VisualStudioでテンプレートプロジェクトを作成し、PowerShellでデプロイするまで一連の流れを説明しました。  
テンプレートの細かいところまで解説する余裕はありませんでしたが、
リソース単位でよく見ると単純な構成になっていて理解しやすいです。  
MicrosoftがGitHubにArmテンプレートのサンプルを載せていたり、
Azureポータルからテンプレートのパラメータ一覧を確認できたりと
ソースも多く転がっていて取り組みやすいのではないでしょうか。  

Infrastracture as code の端っこにほんのちょっとでも触れる手助けとなれば幸いです。
ボリューミーになりすぎてしまい申し訳ありませんでした