Azure BatchのCI/CDを考えてみた

# はじめに

本記事は、[Azure DevOps Advent Calendar 2019](https://qiita.com/advent-calendar/2019/azuredevops) 10日目の記事です。

最近、Azure BatchのCI/CDを構築する機会があったので、
本記事でAzureBatchの作成からAzureDevOpsでCI/CDの構築手順を紹介したいと思います。
また、他に良い方法があればご指摘をお願い致します。

# Azure Batchとは

大規模な並列コンピューティングやハイパフォーマンス コンピューティング (HPC) のバッチ ジョブを実行が可能なサービス。 

AzureBatchのワークフローとしては、コンピューティング ノード (仮想マシン) のプールを作成を行い、
実行するアプリケーションをAzureStorageにアップロードしてノードにインストールをして、
ノードで実行するジョブ定義してタスクを実行する流れになります。

![Azure Batch とは](https://docs.microsoft.com/ja-jp/azure/batch/media/batch-technical-overview/tech_overview_03.png)


# 前準備

まずは、CI/CDパイプラインを構築する前にAzureBatchの環境と
5分毎に稼働しデプロイされている環境をログに出力するアプリを作成します。

## アプリの構築手順

### AzureBatchで稼働するアプリケーションの作成

コンソールアプリ(.NETFramework)でバージョン`.NETFramework,Version=v4.7.2`で作成を行います。

![visualstudio.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144897/ec529851-9add-db22-1192-3a01de240e56.png)

対象のファイルを以下のように変更してください。

```C#:Program.cs
using System;
using System.Configuration;

namespace AzureBatchDemoApp
{
	class Program
	{
		static void Main(string[] args)
		{
			Console.WriteLine(ConfigurationManager.AppSettings["ApplicationName"]);
			Console.WriteLine(ConfigurationManager.AppSettings["ApplicationVersion"]);
			Console.WriteLine(ConfigurationManager.AppSettings["Environment"]);
		}
	}
}

```

```xml:App.config

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <startup> 
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
    </startup>
    <appSettings>
      <add key="ApplicationName" value="AzureBatchDemoApp" />
      <add key="ApplicationVersion" value="1.0" />
      <add key="Environment" value="LOCAL" />
    </appSettings>  
</configuration>
```
ローカルの実行結果がコンソールに以下のように表示されれば完了です。

![2019-12-07_23h58_25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144897/d9056544-6242-e3af-8018-6066be2ebdc3.png)

### 成果物の作成

ビルドを行いAzureBatchにアップロードする成果物を作成します。
以下のファイルを `LOCAL` ⇒ `PRODUCTION`に書き換え、`bin\Release` 内の成果物をZip化します。

```xml:AzureBatchDemoApp.exe.config

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <startup> 
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
    </startup>
    <appSettings>
      <add key="ApplicationName" value="AzureBatchDemoApp" />
      <add key="ApplicationVersion" value="1.0" />
      <add key="Environment" value="PRODUCTION" />
    </appSettings>  
</configuration>
```

## AzureBatchの構築

### Batchアカウント・ストレージアカウントの作成

Cloud Shellに下記コマンドを張り付けて、Batchアカウントとストレージアカウントを作成します。

```bash:AzureCLI

###################################################
# 変数
###################################################

# リソースグループ名
ResourceGroup="yantzn-azurebatch-je-demo"
# リージョン
Location="japaneast"
# 稼働させるアプリケーション格納用ストレージアカウント
StorageAccount="yantzndemostorage"
# Batchアカウント
BatchAccount="yantzndemobatchaccount"
# アプリケーションの名称
AppName="demoapp"

###################################################
# Batchアカウント・ストレージアカウントの作成
###################################################

# リソースグループの作成
az group create \
    --name $ResourceGroup \
    --location $Location

# リソースグループの作成を待つ
az group wait --created \
    --resource-group $ResourceGroup

# AzureBatch用のストレージアカウントを作成
az storage account create \
    --resource-group $ResourceGroup \
    --name $StorageAccount \
    --location $Location \
    --sku Standard_LRS

# Batchアカウントを作成
az batch account create \
    --name $BatchAccount \
    --storage-account $StorageAccount \
    --resource-group $ResourceGroup \
    --location $Location

# アプリケーションを作成
az batch application create \
   --application-name $AppName \
   --name $BatchAccount \
   --resource-group $ResourceGroup

```

### アプリケーションのアップロード

下記手順に従い、AzureBatchで稼働させるアプリケーションをアップロードします。

![batchaccunt.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144897/b5e16d53-3ced-4632-095d-3fd8cff5d04f.png)


![2019-12-08_23h00_05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144897/7a152070-c567-73df-8e69-f8afcfe75adc.png)


![2019-12-07_23h18_53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144897/f10cf9c2-19cb-2e92-b199-317db51be696.png)


![2019-12-07_20h07_34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144897/4e92a2c7-fdfc-e077-e3c1-0f6a42755467.png)


### プールの作成

Cloud Shellに下記コマンドを張り付けて、プールとスケジュールされたジョブを作成します。

```bash:AzureCLI


###################################################
# 変数
###################################################

# リソースグループ名
ResourceGroup="yantzn-azurebatch-je-demo"
# Batchアカウント
BatchAccount="yantzndemobatchaccount"
# アプリケーションの名称
AppName="demoapp"
# プールの名称
PoolName="demopool"

###################################################
# プールとスケジュールされたジョブの作成
###################################################

# プールとジョブを作成のためにログイン
az batch account login \
    --name $BatchAccount \
    --resource-group $ResourceGroup \
    --shared-key-auth

# プール作成
az batch pool create \
    --id $PoolName \
    --vm-size standard_a1 \
    --target-dedicated-nodes 1 \
    --image microsoftwindowsserver:windowsserver:2016-datacenter:latest \
    --node-agent-sku-id "batch.node.windows amd64" \
    --application-package-references $AppName


# スケジュールされたジョブの作成
az batch job-schedule create \
    --id demoschedule \
    --job-manager-task-id demotask \
    --job-manager-task-command-line "cmd /c  %AZ_BATCH_APP_PACKAGE_demoapp%/AzureBatchDemoApp.exe" \
    --pool-id $PoolName \
    --recurrence-interval PT5M \
    --start-window PT1M

```

### ここまでの構築結果

![2019-12-07_23h46_15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144897/a4701bb4-5925-1fdb-8800-cf49f46479e2.png)


![2019-12-07_23h46_54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144897/3d80634e-7b8f-4ebd-37ba-0071910437e0.png)


![2019-12-07_23h44_44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144897/32352bb6-0994-07f7-8c1e-931f5cb79ab3.png)

![2019-12-07_23h51_55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144897/625a79e7-1a58-1367-e96c-7367b38f08bf.png)


# Build Pipelines構築

## 実現すること

前準備では、AzureBatchにリリースする前に`bin/Release`内の`AzureBatchDemoApp.exe.config`を書き替えましたが、
やはり手動で本番情報に書き換えることや本番環境の設定ファイルで上書きするのは、ヒューマンエラーの原因となります。
それに、ビルドした後に書き換えるのは意味がありません。

そのため、デプロイしたい環境の設定ファイルでビルドを行い、成果物をZip化するまでを自動化したいと思います。

## 完成形

Build Pipelinesの完成形は下記となります。

![2019-12-08_16h50_33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144897/129dadbe-6b78-3885-4b45-2cd4529fd8e6.png)

```yml:BuildPipelines

pool:
  name: Azure Pipelines
  demands:
  - msbuild
  - visualstudio

steps:
- task: NuGetToolInstaller@1
  displayName: 'Use NuGet 4.4.1'
  inputs:
    versionSpec: 4.4.1

- task: NuGetCommand@2
  displayName: 'NuGet restore'
  inputs:
    restoreSolution: '$(Parameters.solution)'

- task: DownloadSecureFile@1
  displayName: 'Download Production AppConfig'
  inputs:
    secureFile: 'a721f735-aa88-4e9c-9f33-5359a997294b'

- powershell: |
   # ダウンロードした設定ファイルを一時ソースディレクトリに配置
   Move-Item  $(appconfig.secureFilePath) $(Build.SourcesDirectory)/App.config -Force
   Get-ChildItem $(Build.SourcesDirectory)/
   
  displayName: 'OverWrite AppConfig PowerShell Script'

- task: VSBuild@1
  displayName: 'Build solution **\*.sln'
  inputs:
    solution: '$(Parameters.solution)'
    vsVersion: 16.0
    msbuildArgs: '/p:DeployOnBuild=true /p:WebPublishMethod=Package /p:PackageAsSingleFile=true /p:SkipInvalidConfigurations=true /p:PackageLocation="$(Build.SourcesDirectory)\\"'
    platform: '$(BuildPlatform)'
    configuration: '$(BuildConfiguration)'
    msbuildArchitecture: x64

- task: ArchiveFiles@2
  displayName: 'Archive bin zip'
  inputs:
    rootFolderOrFile: '$(Build.SourcesDirectory)/bin/Release/*'

- task: PublishBuildArtifacts@1
  displayName: 'Publish Artifact: drop'
  inputs:
    PathtoPublish: '$(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip'
  condition: succeededOrFailed()


```
## 解説
### Download Production AppConfig

本番環境の接続情報などのセキュアな情報をGitに含めたくないが、
Build Pipelinesで利用したいというケースの場合、Secure Filesを使います。
また、Secure FilesではRBACの設定が可能となっています。

例えば、本番用アプリケーション設定ファイルとして下記内容のものを用意します。

```xml:App_production.config

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <startup> 
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
    </startup>
    <appSettings>
      <add key="ApplicationName" value="AzureBatchDemoApp" />
      <add key="ApplicationVersion" value="2.0" />
      <add key="Environment" value="PRODUCTION" />
    </appSettings>  
</configuration>

```

作成したファイルをアップロードします。

![2019-12-08_16h44_39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144897/9e304025-bd0b-1966-0458-f32fac0108aa.png)

以下のようにアップロードしたファイルを指定することで簡単にパイプラインで利用することができます。

![2019-12-08_21h04_22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144897/ea25d310-cd2c-23bf-7435-213dfa0d6cdf.png)

## 実行結果

このような実行結果になれば完了です。

![2019-12-08_22h58_43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144897/da305533-090d-4007-8720-175a6c3f6212.png)


# Release Pipelines構築

## 実現すること

Build Pipelinesで作成した成果物をアプリケーションパッケージにアップロードして適用するまでを自動化します。

## 完成形

![2019-12-08_19h49_54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144897/8f8e28b8-6ff2-380b-b091-678522b82c50.png)




```yml:ReleasePipelines

steps:
    - task: AzurePowerShell@4
      displayName: 'Azure PowerShell script: login to Service Principal '
      inputs:
        azureSubscription: '<your azureSubscription>'
        ScriptType: InlineScript
        Inline: |
         # AzureDevOps用のユーザでAzureにログイン
         az login --service-principal -u $(Name) -p $(Password) --tenant $(Tenant)
        FailOnStandardError: true
        azurePowerShellVersion: LatestVersion

variables:
  ScheduleId: 'demoschedule'
  AccountEndpoint: 'https://yantzndemobatchaccount.japaneast.batch.azure.com'
  AccountName: 'yantzndemobatchaccount'

steps:
- task: AzurePowerShell@4
  displayName: 'Azure PowerShell script: Stop Batch Job Schedule'
  inputs:
    azureSubscription: '<your azureSubscription>'
    ScriptType: InlineScript
    Inline: |
     try {
       # スケジュールを無効化
       az batch job-schedule disable --job-schedule-id $(ScheduleId) --account-endpoint $(AccountEndpoint) --account-name $(AccountName)
     
       # 現在の状態を確認
       $schedule = az batch job-schedule show --job-schedule-id $(ScheduleId) --account-endpoint $(AccountEndpoint) --account-name $(AccountName) | ConvertFrom-Json | Select-Object state
     }catch{
       Write-Error($_.Exception)
     }
     
     # 無効化出来ていない場合は異常終了する
     If($schedule.state -ne "disabled"){
       exit 1
     }
    FailOnStandardError: true
    azurePowerShellVersion: LatestVersion

variables:
  ApplicationName: 'demoapp'
  AccountName: 'yantzndemobatchaccount'
  ResourceGroup: 'yantzn-azurebatch-je-demo'

steps:
- task: AzurePowerShell@4
  displayName: 'Azure PowerShell script: Application Deploy'
  inputs:
    azureSubscription: '<your azureSubscription>'
    ScriptType: InlineScript
    Inline: |
     try {
       # 現在の既定のバージョンを取得
       $app = az batch application show --application-name $(ApplicationName) --name $(AccountName) --resource-group $(ResourceGroup) | ConvertFrom-Json | Select-Object defaultVersion
     
       $oldversion = [int]$app.defaultVersion
       $newversion = ($oldversion + 1).ToString("0.0")
     
       # モジュールの適用
       az batch application package create --application-name $(ApplicationName) --name $(AccountName) --resource-group $(ResourceGroup) --version-name $newversion --package-file $(System.DefaultWorkingDirectory)/$(Build.DefinitionName)/drop/$(Build.BuildId).zip
     
       # 既定バージョンの変更
       az batch application set --application-name $(ApplicationName) --name $(AccountName) --resource-group $(ResourceGroup) --default-version $newversion
     
     }catch{
       Write-Error($_.Exception)
     }
     
    FailOnStandardError: true
    azurePowerShellVersion: LatestVersion

variables:
  NodeId: 'tvmps_df69768de9443ead39cceb5fa9a04bf6eacdad7e99115c2e7895ef9f70e07d46_d'
  PoolId: 'demopool'
  AccountEndpoint: 'https://yantzndemobatchaccount.japaneast.batch.azure.com'
  AccountName: 'yantzndemobatchaccount'

steps:
- task: AzurePowerShell@4
  displayName: 'Azure PowerShell script: Batch Node Reboot'
  inputs:
    azureSubscription: '<your azureSubscription>'
    ScriptType: InlineScript
    Inline: |
     try {
       # ノードの再起動を行う
       az batch node reboot --node-id $(NodeId) --pool-id $(PoolId) --account-endpoint $(AccountEndpoint) --account-name $(AccountName) --node-reboot-option taskcompletion
     
       # アイドル状態になるまでポーリングを行う
       $i = 0
       while ($i -eq 0) {
         $node = az batch node show --node-id $(NodeId) --pool-id $(PoolId) --account-endpoint $(AccountEndpoint) --account-name $(AccountName) | ConvertFrom-Json | Select-Object state
      
         write-host $node.state
         if($node.state -eq "idle"){
           write-host "再起動完了"
           exit 0
         }elseif($node.state -eq "creating" -or $node.state -eq "rebooting" -or $node.state -eq "reimaging" `
         -or $node.state -eq "running" -or $node.state -eq "starting" -or $node.state -eq "waitingForStartTask"){
           Start-Sleep -m 10000
         }else{
           Write-Error "再起動失敗"
           exit 1
         }
       }
     }catch{
       Write-Error($_.Exception)
     }
     
    FailOnStandardError: true
    azurePowerShellVersion: LatestVersion

variables:
  ScheduleId: 'demoschedule'
  AccountEndpoint: 'https://yantzndemobatchaccount.japaneast.batch.azure.com'
  AccountName: 'yantzndemobatchaccount'

steps:
- task: AzurePowerShell@4
  displayName: 'Azure PowerShell script: Start Batch Job Schedule'
  inputs:
    azureSubscription: '<your azureSubscription>'
    ScriptType: InlineScript
    Inline: |
     try {
       # スケジュールを有効化
       az batch job-schedule enable --job-schedule-id $(ScheduleId) --account-endpoint $(AccountEndpoint) --account-name $(AccountName)
     
       # 現在の状態を確認
       $schedule = az batch job-schedule show --job-schedule-id $(ScheduleId) --account-endpoint $(AccountEndpoint) --account-name $(AccountName) | ConvertFrom-Json | Select-Object state
     
     }catch{
       Write-Error($_.Exception)
     }
     
     # 有効化出来ていない場合は異常終了する
     If($schedule.state -ne "active"){
       exit 1
     }
    FailOnStandardError: true
    azurePowerShellVersion: LatestVersion


```

## 解説

### login to Service Principal

AzureBatchは、`az login`を行ってから操作を行う必要となります。
そのため、今回はサービスプリンシバルを使ってログインを行いました。

参考：[Azure CLI 2.0 でサービスプリンシパルが簡単に作れるようになっていた](https://blog.shibayan.jp/entry/20170123/1485154436)

### Stop Batch Job Schedule

デプロイを行う前に、5分毎に稼働しているスケジュールを停止します。

### Application Deploy

既定のバージョンに+1.0を足した値を新バージョンとして、Build Pipelinesで作成した成果物をアップロードします。

![2019-12-08_22h15_33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144897/2f59c657-d3d2-71cf-78d3-286e9e165ff6.png)

![2019-12-08_22h13_51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144897/63038151-cfca-f387-d70b-3182638a84d7.png)

### Batch Node Reboot

アップロードしたアプリをノードに適用するには、再起動が必要になります。

>プールのアプリケーション パッケージを " 既存 " のプールに追加した場合は、そのコンピューティング ノードを再起動して、アプリケーション パッケージをノードにデプロイする必要があります。

引用：[アプリケーション パッケージ](https://docs.microsoft.com/ja-jp/azure/batch/batch-api-basics#application-packages)

しかし、処理中に再起動をされてしまっては困るケースがあると思いますので、
そういう場合は、`az batch node reboot`のオプションに`-node-reboot-option taskcompletion`を設定することで、実行中のタスクが完了したタイミングでノードの再起動を行うことができます。

参考：[az batch node reboot](https://docs.microsoft.com/en-us/cli/azure/batch/node?view=azure-cli-latest#az-batch-node-reboot) 

スクリプトは、ノードがアイドル状態になるまでポーリングして実行され、以下のように現在のノードの状態を表示します。
![2019-12-08_22h47_36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144897/81ab96db-0ece-fc91-d33e-dc4da4001b04.png)

### Start Batch Job Schedule)

ノードがアイドル状態になったら停止していたスケジュールを起動します。

## 実行結果

このような実行結果になれば完了です。

![2019-12-08_23h05_04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144897/fa379e72-bc50-d009-ba4c-7e95a3dfea1c.png)

![2019-12-08_23h57_33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144897/2491ad95-db2e-2875-dcb7-a697a1822bcf.png)


# 参考資料

・ [Batch を使って大規模な並列コンピューティング ソリューションを開発する](https://docs.microsoft.com/ja-jp/azure/batch/batch-api-basics)

・ [Azure Pipelines を使用する HPC ソリューションの構築とデプロイ](https://docs.microsoft.com/ja-jp/azure/batch/batch-ci-cd)