Azure Stack Hub でおれおれ Web App を作る

## はじめに

2020年10月現在、Azure Stack Hub 上の App Service は、ASP.NET (.NET Framework / Core), PHP, Java, クラシックASP に対応しています。

参考：[App Service on Azure Stack Hub 2020 Q3 のリリース ノート](https://docs.microsoft.com/ja-jp/azure-stack/operator/app-service-release-notes-2020-q3?view=azs-2008)

App Service 自体は Microsoft が Azure Stack Hub のリソースプロバイダとして提供する「ソフトウェア」です。App Service を動かす IaaS レイヤーの管理は、Azure Stack Hub Operator の役割です。折角そこを管理している（管理する必要がある）のだから、好きなプラットフォームを追加して、おれおれ App Service を作りたくなるのが人情というものです。


## 環境

この記事は以下の環境でのものです。

- Azure Stack Hub 1910
- App Service Update 7 (84.0.2.10)

これと異なる環境では同じようにならない可能性が、大いにあります。


## App Service の IaaS レイヤー

App Service の裏側は以下のようなリソースで構成されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64309/fd0fa952-aa6e-c821-ace2-0401ba6175a8.png)
https://blog.aimless.jp/archives/2019-03-05-install-appservice-resource-provider-to-azurestack より引用（Azure Stack Hub 上の App Service 構築については、こちらが大変参考になります）

このうち、Worker Tier が実際に Web アプリケーションをホストする役割を担っており、仮想マシンスケールセット (VMSS) で構成されています。


## 通常の Worker Tier 作成

Worker Tier の作成自体は、管理者用の Web ポータルから普通に出来ます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64309/535e08da-133a-a95f-a446-e3ef062939f7.png)

ただし、使用するイメージは「2016-Datacenter」以外には選択出来ません。

この時点で真っ当なやり方では難しそうです。
しかしながら、Worker Tier の実体が VMSS である事が判っています。VMSS は任意の仮想マシンイメージから作成する事が出来る事から、この VMSS を弄ってみます。


## 手順

一旦「2016-Datacenter」で Worker Tier を作成してから、VMSS のイメージを変更します。

手順としては以下のようになります。

1. カスタムイメージの作成
2. 「2016-Datacenter」で Worker Tier を作成
3. WorkerTierVmss のイメージを変更
4. VMSS インスタンスの追加と PricingTier の作成


## カスタムイメージの作成

Azure Stack Hub 管理者として、2016-Datacenter で仮想マシンを作成します。
通常のカスタムイメージ作成手順と変わりありません。イメージを登録する際に OS ディスクの Blob URI が必要なので、マネージドディスクを使わない方が楽だと思います。

参考：[Azure Stack Hub に対してカスタム VM イメージを追加または削除する](https://docs.microsoft.com/ja-jp/azure-stack/operator/azure-stack-add-vm-image?view=azs-2008&tabs=image-add-portal%2Cimage-rem-portal)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64309/5887bbba-cfc7-4b7d-a31d-175080c9b49f.png)


イメージを登録する際は以下を指定します。

| 項目 | 値 |
|:--|:--|
| Publisher | MicrosoftWindowsServer |
| Offer  | WindowsServer |
| Sku  | 任意 |
| Version | 任意 |
| Blob URI  | カスタムイメージ用 OS ディスクのURL |

注意すべきは Publisher と Offer です。
VMSS を更新する際にはこれらを変更する事は出来ないので、「2016-Datacenter」と同じものにする必要があります。

また、仮想ディスクが格納されている Blob コンテナを読取アクセス可能にしておく事も忘れないでください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64309/3c2599c7-3776-3053-3b13-ef60c5de1624.png)

ステータスが「Succeeded」になればイメージは完成です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64309/5663bccb-bece-2752-afa7-c6f9c7ed5145.png)


プラットフォームやミドルウェアには、今回は以下をインストールしておきます。この辺のチョイスはただの趣味です。

- ASP.NET 5
- Go
- Oracle Data Provider for .NET 11.2.0.3 


## Worker Tier の作成

管理者ポータルから「2016-Datacenter」の Worker Tier を作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64309/463ba8e1-95ec-787d-0bf0-2985cef03847.png)

当たり前に作られます。
![2020-12-14_19h14_20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64309/0d8bb776-3c9a-b855-8d31-39460c6fa2e5.png)

App Service のリソースグループ内には VMSS が作られている事も確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64309/42a9ed11-b6a8-5014-0a40-4273149e89b6.png)



## VMSS の変更

管理者用の Powershell モジュールを利用して、VMSS を変更します。

```powershell
$vmss = Get-AzureRmVmss -ResourceGroupName appservice.local -VMScaleSetName CustomWorkerTierScaleSet
$vmss.VirtualMachineProfile.StorageProfile.ImageReference.Sku = "2016-Custom"
$vmss.VirtualMachineProfile.StorageProfile.ImageReference.Version = "0.0.1"
Update-AzureRmVmss -ResourceGroupName appservice.local -VMScaleSetName CustomWorkerTierScaleSet -VirtualMachineScaleSet $vmss
```

OS イメージの Sku に上で作成したカスタムイメージ（必要に応じて Version も）を指定して、`Update-AzureRmVmss` で更新します。
更新が終了すると、App Service の Worker Tier ブレードでも image が変更されている事が確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64309/b8f2d023-05c9-f250-7c8a-425bd2463e41.png)



## VMSS インスタンスの追加と PricingTier の作成

あとは普通に VMSS のインスタンスと App Service の Sku を作成して、ユーザーテナントで利用出来るようにします。

VMSS インスタンスを作成。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64309/ea9a8791-61e1-b80d-f119-2afd517e40d9.png)

カスタム Worker Tier を指定して Sku を作成。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64309/5f4b8f29-00ec-56de-b9eb-ed1228941f3f.png)

Sku を作成すると Pricing Tier が作られます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64309/8b6208c9-3b76-df45-3fbc-5810bf987546.png)

これで準備が整いました。


## Web App の作成

ユーザーテナントで Web App を作成します。
App Service Plan を作成する際にカスタムワーカーの Pricing Tier を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64309/9f6c3c01-8d1f-e5c0-242d-6cd6be680037.png)

カスタムワーカーの App Service Plan と Web App が作成されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64309/c1095cdd-fedc-5849-f569-7d66100f4402.png)



## 動作確認

### ASP.NET 5

ASP.NET MVC アプリをデプロイして実行します。
`Environment.Version` と `Environment.MachineName` の値を表示してみます。

```index.cshtml
@{
    ViewData["Title"] = "Home Page";
}

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
    <div>
        .NET Version: @Environment.Version
    </div>
    <div>
        Machine Name: @Environment.MachineName
    </div>
</div>
```

```AspNet5Mvc.csproj
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>
</Project>
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64309/bdc1fe9c-3490-8ff4-2bfa-988372e05c20.png)

.NET 5 のランタイムで動作している事が分かります。

ちなみに、通常の Web App にデプロイするとちゃんとこけます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64309/05ec08b7-47d8-1618-88b0-99e9d826667a.png)



### Go

以下の記事で紹介されているコードをそのままお借りして動かします。
[Azure Web サイトで Go 言語を httpPlatformHandler を使って動かしてみた - しばやん雑記](https://blog.shibayan.jp/entry/20140929/1411916723) 

```server.go
package main

import (
  "fmt"
  "net/http"
  "os"
  "runtime"
)

func viewHandler(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintf(w, "Hello, World, " + runtime.Version())
}

func main() {
  http.HandleFunc("/", viewHandler)
  http.ListenAndServe(":" + os.Getenv("HTTP_PLATFORM_PORT"), nil)
}
```

```Web.config
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <handlers>
      <add name="httpPlatformHandlerMain" modules="httpPlatformHandler" path="*" verb="*" resourceType="Unspecified" />
    </handlers>
    <httpPlatform processPath="C:\Go\bin\go.exe" arguments="run C:\home\site\wwwroot\server.go" startupTimeLimit="60" startupRetryCount="5" />
  </system.webServer>
</configuration>
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64309/17815618-c1fd-a4f1-67a0-a72a47c045f8.png)


動きました！

ちなみに、HttpPlatformHandler は Worker Tier 構築時にインストールされます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64309/503112a2-7493-5cff-66f2-188c9080dd33.png)


### .NET Framework 2.0 + ODP.NET アンマネージドドライバー

つまりレガシーなアプリケーションです。

```WebForm1.aspx
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="WebForm1.aspx.cs" Inherits="dotnetfw2ora.WebForm1" %>

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
        <div>
            Runtime Version: <asp:Label runat="server" ID="lblRuntimeVersion" />
        </div>
        <div>
            Oracle Client: <asp:Label runat="server" ID="lblOracleVersion" />
        </div>
        <div>
            <asp:GridView runat="server" ID="grdData" />
        </div>
    </form>
</body>
</html>
```

```WebForm1.aspx.cs
protected void Page_Load(object sender, EventArgs e)
{
    lblRuntimeVersion.Text = Assembly.GetExecutingAssembly().ImageRuntimeVersion;

    var connStr = ConfigurationManager.AppSettings["ConnectionString"];
    var query = "select * from all_tables where owner='SYS'";
    var conn = new OracleConnection(connStr);
    var adapter = new OracleDataAdapter(query, conn);
    var ds = new DataSet();
    adapter.Fill(ds);

    lblOracleVersion.Text = Assembly.GetAssembly(conn.GetType()).FullName;
    grdData.DataSource = ds;
    grdData.DataMember = ds.Tables[0].TableName;

    this.DataBind();
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64309/721a2751-820c-6f6b-58ae-e851954627fb.png)

ODP.NET のランタイムが読み込まれ、Oracle データベースでのクエリの実行にも成功しました。



## カスタム Worker Tier 削除時の注意

カスタム Worker Tier を削除する場合は、VMSS イメージを「2016-Datacenter」に戻してから削除します。こうしないと VMSS だけが削除され、Worker Tier は残ったままになります。

詳しくは[こちら](https://qiita.com/tkeydll/items/5c814f319b053c92321e)を。


## おわりに

かなり強引ではありますが、独自のプラットフォームに対応させた Web App を作る事に成功しました。

最新のプラットフォームでの開発環境などには使えるのではないでしょうか。
また、PaaS 化など望めないようなレガシーな Web アプリケーションであっても、アプリ自体に手を入れる事なく、スケーリングやデプロイスロットといった Web App の恩恵を受ける事が出来るのは大きなメリットだと思います。

まともな方法でサポートされる事を期待したいです。