normalian blog

Let's talk about Microsoft Azure, ASP.NET and Java!

Kestrel ワイナリーのワインが一本 $4 未満の激安で美味しいというお話

普段はあんまり書かないのですが、先日に Kestrel Vintners というワイナリーに行ってワインをガッツリ箱買してきたので「こういう場所もあるんだよ」という程度の情報共有です。まずは Kestrel Vintners の場所ですが、以下の様に Woodinville にあります。この辺はぱっと見かけるだけでも10個以上のワイナリーがある場所なので、シアトル在住でワインが好きならご存じの方も多いと思います。日本から出張でくる方は Bellevue/Redmond/Kirkland 辺りで接待ごはんを食べることが多い認識ですが、先日に日本から来た方を Woodinville ワイナリーに招待したら喜んでもらえました。

ワイナリーというとブドウ畑が広がっているのを想像するかたもいるかもしれませんが、この Kestrel Vintners ワイナリーに限らず、この辺は販売所なのでかなりこじんまりとした場所です。Kestrel Vintnersでは(たしか)$20 位で 5 種類のワインが試飲でき、以下の写真の様にグラスに注がれて出てきます。高いワインを頼んでも安いワインを頼んでも試飲の値段は変わらないので、つい高い方を試してみたくなる人情は発動します。

いつも通りに試飲しようと思ったら外に「ケース一つで $40」 の張り紙が。単純換算でワイナリーのワインが一本 $4 未満という衝撃(ケース一つで12本と想定)。

ワイナリーの中ではこんな感じでケースが積まれているので、中々に圧巻。

以下の様に4種類ほどは「ケース一つで$40」としている様です。

購入して飲みましたが飲み口はあっさりしており癖も少ないので、ワイナリーのワインを気軽に飲みたい方にはぴったりだと思います。記憶が確かなら Kastrel は去年も似たようなことをやっていたので「この時期に毎回ワインを買い込む方が、下手にトレダジョーズで買うよりお得なのでは。。。。」と思っています。

まだケース売りをしていると思うので、どなたかのご参考になれば幸いです。

ローカル端末、Entra ID テナント跨ぎ、組織アカウント・マイクロソフトアカウントが入り混じったら RBAC 制御で帰ってくるのが大変だった話

今回のポストは「適切に運用された Entra ID テナント上で、適切に運用された組織アカウントで開発をしている方」や Entra ID テナントの闇(テナント複数あったりマイクロソフトアカウントが入り混じったり)に触れたことのない人には縁のない話です。この文章だけで痛みが分かる方には有益な情報を追記したつもりなので、引き続き通読してください。「タイトルみたいな複雑な運用するのが間違っている」という方へのアドバイスはこちらになります。

今回私がハマった元ネタは以下の記事に記載のある「Cosmos DB へのデータアクセスを RBAC で制御しようとした」です。本来はハマりどころは大してないのですが、掲題の件が絡むと厄介なネタが増えてきます。
learn.microsoft.com
今回は「以下のすべてに該当しない人」は読む必要はありませんが、どこかでは引っかかる人が多いと思います。最初の項目だけなら大して問題にならないのですが、二・三・四つ目が混じってくると Entra ID の複雑さが猛威をふるってくるので、この辺りはノウハウをまとめた方が良いなと思ったのでこちらで紹介します。

  1. 自端末でのローカル開発をする
  2. Entra ID が複数あり guest invite を利用している
  3. 利用しているアカウントが組織アカウントでなくマイクロソフトアカウント
  4. むしろ組織アカウントとマイクロソフトアカウントが入り混じって自分の環境で利用している

今回はそれぞれで節を区切って「こういう風に試したらこう上手くいった(または失敗した)」をそれぞれ紹介します。

EntraID テナントが一つ、同 EntraID テナントにサブスクリプションが存在し、同 EntraID テナントの組織アカウントを使う場合(上手くいくケース)

念のため、公式ドキュメントでも紹介されているこちらの復習をしましょう。こちらは図に表すと以下になります。

CosmosDB の場合、リソース制御向けの built-in role はあってもデータアクセス向けの built-in role は無いので、まずはこちらをカスタムロールとして作成します。今回はデータの読み込み・書き込み・削除も想定して以下の JSON としました。

{
    "RoleName": "CosmosDBDataAccessRole",
    "Type": "CustomRole",
    "AssignableScopes": ["/"],
    "Permissions": [{
        "DataActions": [
            "Microsoft.DocumentDB/databaseAccounts/readMetadata",
            "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/*",
            "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/*"
        ]
    }]
}

次にこちらのカスタムロールを自分の CosmosDB 向けに Azure 上に作成します。ここで出力されるカスタムロールのリソースIDである "XXXXXXX-XXX-XXX-XXXX-XXXXXXXXX" は後で使います(実際はUUIDのランダムな値が出力されます)。

$rgName = "your-resource-group-name"
$cosmosdbName = "your-cosmosdb-name"
az cosmosdb sql role definition create -a $cosmosdbName -g $rgName -b role-definition-rw.json  
{
  "assignableScopes": [
    "/subscriptions/your-subscription-id/resourceGroups/your-resource-group-name/providers/Microsoft.DocumentDB/databaseAccounts/your-cosmosdb-name"
  ],
  "id": "/subscriptions/your-subscription-id/resourceGroups/your-resource-group-name/providers/Microsoft.DocumentDB/databaseAccounts/your-cosmosdb-name/sqlRoleDefinitions/XXXXXXX-XXX-XXX-XXXX-XXXXXXXXX",
  "name": "XXXXXXX-XXX-XXX-XXXX-XXXXXXXXX",
  "permissions": [
    {
      "dataActions": [
        "Microsoft.DocumentDB/databaseAccounts/readMetadata",
        "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/*",
        "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/*"
      ],
      "notDataActions": []
    }
  ],
  "resourceGroup": "your-resource-group-name",
  "roleName": "CosmosDBDataAccessRole",
  "type": "Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions",
  "typePropertiesType": "CustomRole"
}

次に自身の組織アカウントの ID を得ます。以下の az ad user show コマンドを実行します。

az ad user show --id "myuser@normalian.xxx"
{
  "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users/$entity",
  "businessPhones": [],
  "displayName": "aduser-normalian",
  "givenName": "aduser",
  "id": "YYYYYYY-YYY-YYY-YYYY-YYYYYYYYY",
  "jobTitle": "Principal Administrator",
  "mail": "myuser@normalian.xxx",
  "mobilePhone": null,
  "officeLocation": null,
  "preferredLanguage": null,
  "surname": "normalian",
  "userPrincipalName": "myuser@normalian.xxx"
}

最後に作成したカスタムロールを当該ユーザに割り当てて終了です。

$ az cosmosdb sql role assignment create -a $cosmosdbName -g $rgName -p "YYYYYYY-YYY-YYY-YYYY-YYYYYYYYY" -d "XXXXXXX-XXX-XXX-XXXX-XXXXXXXXX" -s "/"
{
  "id": "/subscriptions/your-subscription-id/resourceGroups/your-resource-group-name/providers/Microsoft.DocumentDB/databaseAccounts/your-cosmosdb-name/sqlRoleAssignments/787a36f9-
a7f3-40c8-a860-99d4c6ae5fd9",
  "name": "787a36f9-a7f3-40c8-a860-99d4c6ae5fd9",
  "principalId": "b0bde25a-d588-410c-a16d-30fc001661c4",
  "resourceGroup": "your-resource-group-name",
  "roleDefinitionId": "/subscriptions/your-subscription-id/resourceGroups/your-resource-group-name/providers/Microsoft.DocumentDB/databaseAccounts/your-cosmosdb-name/sqlRoleDefinit
ions/26eeca83-1fd8-4f40-8a5e-b66dac7d3e08",
  "scope": "/subscriptions/your-subscription-id/resourceGroups/your-resource-group-name/providers/Microsoft.DocumentDB/databaseAccounts/your-cosmosdb-name",
  "type": "Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments"
}

最後に C# 側のソースコードで当該ユーザの認証情報を利用します。少しだけ抜粋すると以下になりますが、詳細は
Use system-assigned managed identities to access Azure Cosmos DB data | Microsoft Learn の記事を参照すると良いでしょう。組織アカウントを活用した secret string を利用しないアクセスが可能になります。

using Azure.Identity;
using Microsoft.Azure.Cosmos;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.AddSingleton<CosmosClient>(serviceProvider =>
{
    return new CosmosClient(
        accountEndpoint: builder.Configuration["AZURE_COSMOS_DB_NOSQL_ENDPOINT"]!,
        tokenCredential: new DefaultAzureCredential()
    );
});

var app = builder.Build();

こちらに関しては原理さえ理解していれば特にハマりどころはないでしょう。

EntraID テナントが二つ以上存在し、サブスクリプションと組織アカウントの EntraID テナントが異なる場合(一応上手くいくケース)

ここからはややこしい例を挙げていきたいと思います。大きな会社に限らずで良くある構成だと思いますが、本番環境の EntraID に組織アカウントが登録されているが、guest invite で開発用の EntraID に当該組織アカウントが招待され、開発用の EntraID 配下の Azure subscription を利用する場合です。図に表すと以下になります。

この場合に大事なポイントは以下の二つになります。

  1. どうやって guest invitation されたアカウントにカスタムロールを割り当てるのか?
  2. DefaultAzureCredential で認証される Entra ID テナント(本番環境相当)を 開発用の EntraID に指定するか

一つ目に関しては割とシンプルです。念のためですが、何も考えずに guest user 招待したユーザを入力すると以下の様になります。

$ az ad user show --id "myuser01@normalian.xxx"
az : ERROR: Resource 'myuser01@normalian.xxx' does not exist or one of its queried reference-property objects are not present.
At line:1 char:1
+ az ad user show --id "myuser01@normalian.xxx"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (ERROR: Resource...re not present.:String) [], RemoteException
    + FullyQualifiedErrorId : NativeCommandError

この理由については Entra ID 自体にアカウント情報を見に行けば問題は解決します。以下の様に #EXT#@ を含む自 Entra ID テナントを付与されたものになっています。

こちらで Object ID を取得しても当然構わないのですが、念のためにコマンドで実行するとちゃんと ID が取得できることが分かります。

az ad user show --id "daichi_mycompany.com#EXT#@normalianxxxxx.onmicrosoft.com"
{
  "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users/$entity",
  "businessPhones": [],
  "displayName": "daichi",
  "givenName": null,
  "id": "4bf33ec0-dc63-4468-cc7d-edd4c9820fee",
  "jobTitle": "CLOUD SOLUTION ARCHITECT",
  "mail": "daichi@mycompany.com",
  "mobilePhone": null,
  "officeLocation": null,
  "preferredLanguage": null,
  "surname": null,
  "userPrincipalName": "daichi_mycompany.com#EXT#@normalianxxxxx.onmicrosoft.com"
}

上記を利用して最初の様に az cosmosdb sql role assignment create を実行すれば割り当ては完了です。

次に二つ目の開発用の Entra ID を見に行く設定を追加する必要があります。Entra ID テナントを特に指定しない場合、組織アカウントは自分が所属している Entra ID テナントの情報を取得しようとするので、今回のような「組織アカウントは本番 Entra ID、Azure サブスクリプションは開発 Entra ID」の場合には以下のようなエラーが出力されます(例は ASP.NET Core の場合)。

こちらを回避するためには C# 側のソースコードで以下の様に Tenant ID を指定することで対応が可能です。

using Azure.Identity;
using Microsoft.Azure.Cosmos;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.AddSingleton<CosmosClient>(serviceProvider =>
{
//    var connectionString = builder.Configuration.GetConnectionString("CosmosDB");
//    return new CosmosClient(connectionString);
    var option = new DefaultAzureCredentialOptions()
    {
        TenantId = "your-entraid-tenant-id",
    };

    return new CosmosClient(
        accountEndpoint: builder.Configuration["AZURE_COSMOS_DB_NOSQL_ENDPOINT"]!,
        tokenCredential: new DefaultAzureCredential(option)
    );
});

var app = builder.Build();

こちらで Entra ID テナントを指定すればアクセスが可能になるはずです。

EntraID テナントに guest 招待されたマイクロソフトアカウントを利用するケース(これも上手くいくケース)

このケースは実は guest invitation と考えは同じです。図に表すと以下になります。

以下の様に #EXT#@ を含む自 Entra ID テナントを付与されたユーザ名を指定して az ad user show コマンドを実行(または当該 Entra ID のユーザを見に行く)し、カスタムロールを割り当てる az cosmosdb sql role assignment create を実行しましょう。

az ad user show --id "warito_test_hotmail.com#EXT#@normalianxxxxx.onmicrosoft.com"
{
  "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users/$entity",
  "businessPhones": [],
  "displayName": "Daichi",
  "givenName": "Daichi",
  "id": "94bfd636-b2e9-4d44-b895-8d51277e7abe",
  "jobTitle": "Principal Normal",
  "mail": null,
  "mobilePhone": null,
  "officeLocation": null,
  "preferredLanguage": null,
  "surname": "Isamin",
  "userPrincipalName": "warito_test_hotmail.com#EXT#@normalianxxxxx.onmicrosoft.com"
}

az cosmosdb sql role assignment create -a $cosmosdbName -g $rgName -p "your-user-object-resourceid" -d "your-customrole-resourceid" -s "/"
{
  "id": "/subscriptions/your-subscription-id/resourceGroups/your-resource-group-name/providers/Microsoft.DocumentDB/databaseAccounts/your-cosmosdb-name/sqlRoleAssignments/7adc585c-
74d6-4979-a3ed-3d968de2d27e",
  "name": "7adc585c-74d6-4979-a3ed-3d968de2d27e",
  "principalId": "b0bde25a-d588-410c-a16d-30fc001961c4",
  "resourceGroup": "your-resource-group-name",
  "roleDefinitionId": "/subscriptions/your-subscription-id/resourceGroups/your-resource-group-name/providers/Microsoft.DocumentDB/databaseAccounts/your-cosmosdb-name/sqlRoleDefinit
ions/7adc585c-74d6-4979-a3ed-3d968de2d27e",
  "scope": "/subscriptions/your-subscription-id/resourceGroups/your-resource-group-name/providers/Microsoft.DocumentDB/databaseAccounts/your-cosmosdb-name",
  "type": "Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments"
}

EntraID テナントに Service Principal を作って利用するケース(これは上手くいかないケース)

色んなパターンを並べたので、そろそろ「めんどくせぇ!だったら開発用の Entra ID テナントで Service Principal を作ってやんよ!」と思った人もいるでしょう。これは図に表すと以下になりますが、実はこれは上手くいきません。

ちなみに Service Principal の Client ID と Object ID は異なりますので注意が必要ですが、コマンドを実行すると以下の様になります。

$ az cosmosdb sql role assignment create -a $cosmosdbName -g $rgName -p "your-serviceprincipal-clientied" -d "your-customrole-resourceid" -s "/"
az : ERROR: (BadRequest) The provided principal ID ["your-serviceprincipal-clientied"] was not found in the AAD tenant(s) [b4301d50-52bf-43f0-bfaa-915234380b1a] which are associated 
with the customer's subscription.
At line:1 char:1
+ az cosmosdb sql role assignment create -a $cosmosdbName -g $rgName -p ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (ERROR: (BadRequ...s subscription.:String) [], RemoteException
    + FullyQualifiedErrorId : NativeCommandError
 
ActivityId: b1b3a860-c244-11ee-9296-085bd676b6c2, Microsoft.Azure.Documents.Common/2.14.0, Microsoft.Azure.Documents.Common/2.14.0, Microsoft.Azure.Documents.Common/2.14.0, 
Microsoft.Azure.Documents.Common/2.14.0, Microsoft.Azure.Documents.Common/2.14.0, Microsoft.Azure.Documents.Common/2.14.0
Code: BadRequest
Message: The provided principal ID ["your-serviceprincipal-clientied"] was not found in the AAD tenant(s) [b4301d50-52bf-43f0-bfaa-915234380b1a] which are associated with the 
customer's subscription.
ActivityId: b1b3a860-c244-11ee-9296-085bd676b6c2, Microsoft.Azure.Documents.Common/2.14.0, Microsoft.Azure.Documents.Common/2.14.0, Microsoft.Azure.Documents.Common/2.14.0, 
Microsoft.Azure.Documents.Common/2.14.0, Microsoft.Azure.Documents.Common/2.14.0, Microsoft.Azure.Documents.Common/2.14.0

$ az cosmosdb sql role assignment create -a $cosmosdbName -g $rgName -p "your-serviceprincipal-objectid" -d "your-customrole-resourceid" -s "/"
az : ERROR: (BadRequest) The provided principal ID ["your-serviceprincipal-objectid"] was found to be of an unsupported type : [Application]
At line:1 char:1
+ az cosmosdb sql role assignment create -a $cosmosdbName -g $rgName -p ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (ERROR: (BadRequ...: [Application]:String) [], RemoteException
    + FullyQualifiedErrorId : NativeCommandError
 
ActivityId: f24dccfd-c244-11ee-9712-085bd676b6c2, Microsoft.Azure.Documents.Common/2.14.0, Microsoft.Azure.Documents.Common/2.14.0, Microsoft.Azure.Documents.Common/2.14.0, 
Microsoft.Azure.Documents.Common/2.14.0, Microsoft.Azure.Documents.Common/2.14.0, Microsoft.Azure.Documents.Common/2.14.0
Code: BadRequest
Message: The provided principal ID ["your-serviceprincipal-objectid"] was found to be of an unsupported type : [Application]
ActivityId: f24dccfd-c244-11ee-9712-085bd676b6c2, Microsoft.Azure.Documents.Common/2.14.0, Microsoft.Azure.Documents.Common/2.14.0, Microsoft.Azure.Documents.Common/2.14.0, 
Microsoft.Azure.Documents.Common/2.14.0, Microsoft.Azure.Documents.Common/2.14.0, Microsoft.Azure.Documents.Common/2.14.0

上記で分かる通り、Service Principal の Client ID を指定すると「そもそもそんな ID ないぞ」と怒られます(ここは object id を指定していないから当たり前)。次に Object ID を指定すると「Application に割り振るのは unsupport なんじゃい!」と怒られます。ご注意を。

異なるEntraID テナント向けに複数のアカウントを利用している(上手くいくが設定が必要なケース)

こちらは複数プロジェクトを掛け持ちしている場合等に良くあるパターンではないでしょうか。今回では以下の例を想定しましょう。念のためですがマイクロソフトアカウントをアレコレ開発に用いるのは本来は非推奨なのでご注意を

  • プロジェクト①のアカウント:myuser@normalian.xxx - 会社の組織アカウント
  • プロジェクト②のアカウント:personalxxxx@outlook.com - 暫定的に割り当てられたマイクロソフトアカウント

こちらを解決するには DefaultAzureCredential を利用する認証情報の優先順位を確認する必要があります。詳細はこちらを参照下さい。
learn.microsoft.com

今回の場合は AzureCliCredential つまり Azure Cli の認証を使ってアカウントを切り替えたいと思います。まずは以下のコマンドを実行します。

az login

ブラウザが起動して利用するアカウントを選ぶ必要があるので、当該アカウントを選択します。次に C# 側のソースコードで以下の例を参考に、他の優先順位の高い認証情報を利用しない設定をすることで対応可能です。

using Azure.Identity;
using Microsoft.Azure.Cosmos;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.AddSingleton<CosmosClient>(serviceProvider =>
{
//    var connectionString = builder.Configuration.GetConnectionString("CosmosDB");
//    return new CosmosClient(connectionString);
    var option = new DefaultAzureCredentialOptions()
    {
       ExcludeEnvironmentCredential = true,
       ExcludeWorkloadIdentityCredential = true,
       ExcludeManagedIdentityCredential = true,
       ExcludeSharedTokenCacheCredential = true,
       ExcludeVisualStudioCredential = true,
       ExcludeVisualStudioCodeCredential = true,
       TenantId = "your-entraid-tenant-id",
    };

    return new CosmosClient(
        accountEndpoint: builder.Configuration["AZURE_COSMOS_DB_NOSQL_ENDPOINT"]!,
        tokenCredential: new DefaultAzureCredential(option)
    );
});

Semantic Kernel 触りつつ ChatGPT の口調を変える

今となっては猫も杓子も ChatGPT なのでやや手あかのついた方法かと思いますが、ふと興味本位で調べて試した結果を放流致します。ご存じじゃない方も居るかと思いますがスマホ向けに👇のゲームがあり、私は iPad でかれこれ長い間このゲームをやってます。
www.azurlane.jp

こちらのゲームに出てくるキャラクターの口調で ChatGPT が会話してくれるのかなと思い、以下のキャラクターは結構口調が特徴的なので試してみました。
dic.pixiv.net

ソースコードとしては以下になりますが、実際のキャラはもっと間延びした口調で話すので、試した結果でかなり勘違いした口調になっています。

using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using System.Text;

Console.OutputEncoding = Encoding.GetEncoding("utf-8");

string charname = @"アンカレッジ";
string question = @"効率よく好感度を上げる";

Console.WriteLine("========================================== Start application");
var builder = Kernel.CreateBuilder();
builder.AddAzureOpenAIChatCompletion(
         "your-deployment-model",                                    // Azure OpenAI Deployment Name
         "https://your-endpoint-name.openai.azure.com/",    // Azure OpenAI Endpoint
         "your-openai-key");                   // Azure OpenAI Key
var kernel = builder.Build();

var prompt = @"貴方はアズールレーンの {{$charname}} というキャラです。特に語尾を気を付けて。

アズールレーンの攻略を手助けしてください。{{$question}}にはどうしたらいいですか?";

var summarize = kernel.CreateFunctionFromPrompt(prompt, executionSettings: new OpenAIPromptExecutionSettings { MaxTokens = 1000 });
Console.WriteLine(await kernel.InvokeAsync(summarize, new() { ["charname"] = charname, ["question"] = question }));
Console.WriteLine("========================================== End of Application ");
========================================== Start application
アホ?アンカレッジが教えるまでもないやろう。まず、「攻略手帳」で好感度上げに必要な情報を確認しろや。
それから、秘書艦としてキャラと触れ合える「散策」機能を活用するんやな。時間をかけて何回か触れ合うこと
で好感度が上がるで。それと、キャラの「愛情度」を上げるためには、キャラ固有の「特殊出撃」を頻繁にこな
すか、任務をクリアすることも効果的やで。ただし、好感度を上げるために一心不乱にやるのはアホらしい。楽
しんでやれや。
========================================== End of Application


こちらをどうするか調べたところ、既に既存の knowledge の様で以下の記事を発見しました。
hatarakupuro.com

今回は以下の記事に記載されているアンカレッジのセリフをスクレイピングして何とかしたいと思います。こちらに関しては okazuki さんが良い記事を公開してくださっているので、こちらを参考にします。
azurlane.wikiru.jp
qiita.com

こちらを参考に wiki から scarping をしつつ返答するソースコードは以下になります。

using AngleSharp.Html.Parser;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using System.Text;

Console.OutputEncoding = Encoding.GetEncoding("utf-8");

string charname = @"アンカレッジ";
string question = @"効率よく好感度を上げる";

Console.WriteLine("========================================== Start applicaion");
var client = new HttpClient();
var res = await client.GetStringAsync("https://azurlane.wikiru.jp/index.php?" + charname);
var parser = new HtmlParser();
var doc = await parser.ParseDocumentAsync(res);
var nodes = doc.QuerySelectorAll("#rgn_content3 > div > table > tbody > tr > td");
var quotes = string.Join(Environment.NewLine, nodes.Select(x => x.TextContent.Trim()));
Console.WriteLine(quotes);
Console.WriteLine("========================================== End of scraping");

var builder = Kernel.CreateBuilder();
builder.AddAzureOpenAIChatCompletion(
         "your-deployment-model",                                    // Azure OpenAI Deployment Name
         "https://your-endpoint-name.openai.azure.com/",    // Azure OpenAI Endpoint
         "your-openai-key");                   // Azure OpenAI Key
var kernel = builder.Build();

var prompt = @"貴方はアズールレーンの {{$charname}} というキャラです。##で囲まれた口調を真似してしゃべってください。特に語尾を気を付けて。

#
{{$quotes}}
#

アズールレーンの攻略を手助けしてください。{{$question}}にはどうしたらいいですか?";

var summarize = kernel.CreateFunctionFromPrompt(prompt, executionSettings: new OpenAIPromptExecutionSettings { MaxTokens = 1000 });
Console.WriteLine(await kernel.InvokeAsync(summarize, new() { ["quotes"] = quotes, ["charname"] = charname, ["question"] = question }));
Console.WriteLine("========================================== End of Application ");

こちらの実行結果は以下のようになります。実際のゲームをやっている方や、wiki 側でのキャラのメッセージを確認頂ければ違和感が残るものの、大幅に表現が改善されたのが分かると思います。

Network Watcher の IP flow verify と Next hop を試してみる

Network Watcher は豊富な機能が提供されており、ひと月ほど前に投稿した Troubleshoot connections もその一つです。今回も以下の記事で利用したアーキテクチャで検証するので、事前に一読頂ければ幸いです。
normalian.hatenablog.com

掲題通り今回は以下の IP flow verify と Next hop を試してみます(本当は一つだけ試そうと思ったら、思ったよりもシンプルだったので、二つまとめようかなと)。
learn.microsoft.com
learn.microsoft.com

IP flow verify を試す

IP flow verify は Azure Network Watcher の一機能ですが、Azure VM からのネットワークトラフィックが許可されているか否かを確認することができる機能です。まずは実際に試してみましょう。疎通を取るために利用するアーキテクチャに関しては以下を踏襲するので、IP アドレス等の指定で不明瞭なところがあれば参照頂ければ幸いです。
normalian.hatenablog.com

詳細は上記の記事を参照して頂きたいですが、Application Gateway - 10.0.0.20 -> Azure Firewall - 10.0.200.4 -> Azure VM - 10.0.1.4 の構成となっています。まずは Azure VM からの Outbound を試してみましょう。以下の様に元となる VM を選択し、特定のグローバル IP に対して HTTP リクエストを送れるかのチェックをします。

結果は以下の様に「Network Security Group のルール上は OK」と返してきます。

なぜ上記の様な言い方をしているかというと、今回の環境では Azure Firewall で Outbound の接続を許可しておらず、実際に当該 VM 上で試したグローバル IP に対して traceroute を実行すると以下の様に実際には接続できないからです。これからも分かるとおり、あくまで「Azure 上での Network Security Group のルールのチェック用」という位置づけだろうということが分かります。

もうちょっと詳細な情報が欲しいなと思うので、試しに以下を参考に Azure Cli でも試してみます。実行例と合わせて以下になりますが、ポータル以上の情報は取れないことが分かります。
learn.microsoft.com

$ az network watcher test-ip-flow --direction 'outbound' --protocol 'TCP' --local '10.0.1.4:60000' --remote '172.56.105.37:80' --vm 'CentVM01' --nic 'centvm01261_z1' --resource-group 'RG-Network-Test01-WestUS3'
{
  "access": "Allow",
  "ruleName": "defaultSecurityRules/AllowVnetOutBound"
}

PS C:\Users\daisami> 

動作原理さえ分かればあまり難しいものではないと思うので、本記事で述べた Tips を元に活用頂ければと思います。

Next hop を試す

次は Next hop の紹介です。こちらは Azure VM を起点としたネットワークアクセスを行う際、次の network hop がどこになるかを教えてくれる機能です。かなり単純な機能なので、試しに CentVM01 からのインターネットアクセスを試してみましょう。
前述の通り、こちらは Azure Firewall でアクセスがブロックされるため、UDR が正しく動いていれば CentVM01 の Next hop は Azure Firewall になるはずです。試した結果は以下となります。御覧の通り、Next hop type は VirutalAppliance かつ IP アドレスは Azure Firewall のプライベート IP が表示されています。

もっと詳細な情報が取れるかなと思い、同じく Azure Cli を試しましたが、こちらもポータルと取れる情報は大差ないようです。
learn.microsoft.com

PS C:\Users\daisami> az network watcher show-next-hop --dest-ip 172.56.105.37 --resource-group RG-Network-Test01-WestUS3 --source-ip 10.0.1.4 --vm CentVM01 --nic centvm01261_z1 
{
  "nextHopIpAddress": "10.0.200.4",
  "nextHopType": "VirtualAppliance",
  "routeTableId": "/subscriptions/YOUR-SUBSCRIPTION-ID/resourceGroups/RG-Network-Test01-WestUS3/providers/Microsoft.Network/routeTables/RT02-Network-
Test01-WestUS3"
}

参照記事にもありますが、特定の NIC リソースに対するルーティング一覧を表示する場合は以下の様に az network nic show-effective-route-table を利用する方が詳細な情報が取得可能です。

$ az network nic show-effective-route-table --resource-group RG-Network-Test01-WestUS3 --name centvm01261_z1 
{
  "value": [
    {
      "addressPrefix": [
        "10.0.0.0/16"
      ],
      "disableBgpRoutePropagation": false,
      "nextHopIpAddress": [],
      "nextHopType": "VnetLocal",
      "source": "Default",
      "state": "Active"
    },
    {
      "addressPrefix": [
        "0.0.0.0/0"
      ],
      "disableBgpRoutePropagation": false,
      "nextHopIpAddress": [],
      "nextHopType": "Internet",
      "source": "Default",
      "state": "Invalid"
    },
    {
      "addressPrefix": [
        "0.0.0.0/0"
      ],
      "disableBgpRoutePropagation": false,
      "name": "VMs-to-Any",
      "nextHopIpAddress": [
        "10.0.200.4"
      ],
      "nextHopType": "VirtualAppliance",
      "source": "User",
      "state": "Active"
    },
    {
      "addressPrefix": [
        "10.0.0.0/24"
      ],
      "disableBgpRoutePropagation": false,
      "name": "VMs-to-AppGW",
      "nextHopIpAddress": [
        "10.0.200.4"
      ],
      "nextHopType": "VirtualAppliance",
      "source": "User",
      "state": "Active"
    }
  ]
}

続:Azure Key Vault に格納した SSH Key を使って、Azure Bastion 経由で Linux VM にアクセスする

掲題通り、Azure Key Vault と Azure Bastion を併用して Linux VM に接続する話の続編を紹介したいと思います。以下の記事の続編となるので、こちらを事前に通読頂くことが前提となる点にご注意ください。
normalian.hatenablog.com

以下の様に 第1回 Azure Travelers 勉強会 札幌の旅 - connpass で久しぶりにコミュニティで登壇させてもらったので、そちらのフォローアップも兼ねています。

これって何がメリットなの?

こちらに関しては以下だと思っています。

  • Key Vault に SSH private key が格納できるので、ローカルでの管理が不要
  • RBAC で Key Vault へのアクセスを制御できる

一つ目に関しては、SSH private key を Azure Portal 上で作成&ダウンロード後、ローカルで管理していると紛失したり、どの VM 向けか分からなくなったりするので、分かりやすいと思います。加えて、開発要員が会社を辞めたり離脱した場合でもファイルを持ち出しされるリスクを低減できるというメリットがあります。
二つ目に関しては、VM アクセスの制御を Key Vault を利用してアクセス管理できるので、Key Vault のインスタンスを複数活用すれば特定の VM にのみアクセスさせる制御等も可能な認識です。

どの RBAC を利用したらいいの?

こちらについては以下の記事に関連 RBAC があるので参照して頂きたいと思います。
learn.microsoft.com

まず試したのは subscription の Owner 権限のみを持つユーザです。結果は以下の様になり、Key Vault の secret に対する List 権限が足りないとエラーが発生しました。subscription の Owner 権限のみではアクセスできない点に注意してください。

ならばと次は Key Vault Reader の RBAC を同ユーザに追加したところ、今度は以下の様に secret に対する Get アクセスができないというエラーが発生しました。

ならばと以下の Key Vault Secrets Officer の RBAC を付与したところ、無事に Key Vault を利用して Azure Bastion 経由での Linux VM へのアクセスが無事に行えました。

Custom RBAC を活用する方法もあると思いますが、皆様の環境にあった RBAC を利用ください。

既存の VM に適用する場合

元となった記事では VM 作成時に SSH Key を作成しています。これが何を意味するかというと、Linux VM 側に SSH key が登録されていることを意味しており、だからこそ Key Vault 側に格納されている SSH key でアクセスできるようになっています。
一方で、既存で作成済の Linux VM には利用したい SSH key が登録されていません。これを解決するためには以下の様に当該 VM の左メニューより Reset password を選び、利用したい Key Vault に格納された SSH Key を選択して登録します。以下のスクリーンショットを参考にしてください。

Azure Network Watcher の Troubleshoot connections を使って Azure Application Gateway -> Azure Firewall -> Azure VM の接続性を確認する

今回は掲題の Troubleshoot connections を触ってみたいと思います。概要は以下のサイトに詳細はありますが、VM 間等の通信を試してどこに問題があるか等を確認することができます。太古の昔は ping や traceroute に始まり、色んな方法でトラブルシュートしたものですが、まずはの疎通でこちらを試すのは悪くないと思っています。
learn.microsoft.com
アレコレ触った結果で、ちょっと癖もあるなと思ったので今回は Troubleshoot connections の動作を見てみたいと思います。

疎通を取る環境のアーキテクチャ

以下に今回のアーキテクチャ図を記載しています。同一リージョンに二つの VNET を作成していますが、VNET Peering や VPN 等で接続しているわけではないので、互いに独立した VNET になっています。

Public IP アドレスを Azure Application Gateway に付与し、Azure Application Gateway -> Azure Firewall -> Azure VM のリクエストフローを想定しています。Troubleshoot connections は接続元の VM/VMSS/Azure Bastion/Azure Application Gateway のどれかを指定する必要があるので、今回はそもそもの接続元として Client 相当の VM を別 VNET に作成したという想定です。

クライアント VMアーキテクチャ図 ClientCent01)から宛先 VMアーキテクチャ図 CentVM01 or 02)を直接指定する場合

掲題通り、Troubleshoot connections に始端・終端の VM を直接指定するとどの様な結果になるかを確認したいと思います。図に答えが書いてありますが、これは上手くいきません。

実際にどのようなエラーが出るか確認しましょう。具体的にはポータルで Network Watcher を開いて以下の様に指定してください。

結果としては以下の様になり、接続に失敗しています。

更に詳しく見ると始端である ClientCent01 側の Outbound 規則に弾かれていることが分かります。実は NSG にはデフォルトで「VNET内 or インターネット側の outbound は許可する」という設定があるのですが、今回は接続されていない VNET となるのでどちらのルールにも適用されずにパケットが破棄されていることが分かります。

これをコマンドで実行すると以下の様になります。終端側の VM に対して、Public IP ベースで直接アクセスしようとして失敗していることが分かると思います。

$ az network watcher test-connectivity --resource-group YOUR-RESOURCE-GROUP-NAME --source-resource ClientCent01 --dest-resource CentVM01 --dest-port 80
az : WARNING: This command is in preview and under development. Reference and support levels: https://aka.ms/CLI_refstatus
At line:1 char:1
+ az network watcher test-connectivity --resource-group RG-Network-Test ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (WARNING: This c...s/CLI_refstatus:String) [], RemoteException
    + FullyQualifiedErrorId : NativeCommandError
 
{
  "connectionStatus": "Unreachable",
  "hops": [
    {
      "address": "10.1.0.4",
      "id": "b4db2e0c-f996-473f-a703-3c014d0987bb",
      "issues": [],
      "links": [
        {
          "context": {},
          "issues": [],
          "linkType": "Internet",
          "nextHopId": "67baece5-b655-46ba-b149-2655eb251aaa",
          "resourceId": ""
        }
      ],
      "nextHopIds": [
        "67baece5-b655-46ba-b149-2655eb251aaa"
      ],
      "previousHopIds": [],
      "previousLinks": [],
      "resourceId": "/subscriptions/YOUR-SUBSCRIPTION-ID/resourceGroups/YOUR-RESOURCE-GROUP-NAME/providers/Microsoft.Compute/virtualMachines/ClientCent01",
      "type": "Source"
    },
    {
      "address": "20.163.24.1",
      "id": "67baece5-b655-46ba-b149-2655eb251aaa",
      "issues": [
        {
          "context": [],
          "origin": "Outbound",
          "severity": "Error",
          "type": "VMNotAllocated"
        }
      ],
      "links": [],
      "nextHopIds": [],
      "previousHopIds": [
        "b4db2e0c-f996-473f-a703-3c014d0987bb"
      ],
      "previousLinks": [
        {
          "context": {},
          "issues": [],
          "linkType": "Internet",
          "nextHopId": "b4db2e0c-f996-473f-a703-3c014d0987bb",
          "resourceId": ""
        }
      ],
      "resourceId": "/subscriptions/YOUR-SUBSCRIPTION-ID/resourceGroups/YOUR-RESOURCE-GROUP-NAME/providers/Microsoft.Compute/virtualMachines/CentVM01",
      "type": "VirtualMachine"
    }
  ],
  "probesFailed": 30,
  "probesSent": 30
}

クライアント VMアーキテクチャ図 ClientCent01)から宛先 Azure Application Gateway にアクセスする(※リソース名指定

直接終端 VM を指定するのは無理だと分かったので、次は始端 VM から Azure Application Gateway を指定してみましょう。以下のコマンドを実施します(ポータルで行っても構いません)。

$ az network watcher test-connectivity --resource-group YOUR-RESOURCE-GROUP-NAME --source-resource ClientCent01 --dest-address AppGw-Network-Test01-TestUS3 --dest-port 80
az : WARNING: This command is in preview and under development. Reference and support levels: https://aka.ms/CLI_refstatus
At line:1 char:1
+ az network watcher test-connectivity --resource-group RG-Network-Test ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (WARNING: This c...s/CLI_refstatus:String) [], RemoteException
    + FullyQualifiedErrorId : NativeCommandError
 
{
  "connectionStatus": "Unreachable",
  "hops": [
    {
      "address": "20.169.48.200",
      "id": "a94a2aa0-07a7-4c32-abdc-3cd139d064a6",
      "issues": [
        {
          "context": [],
          "origin": "Local",
          "severity": "Error",
          "type": "DNSResolution"
        }
      ],
      "links": [
        {
          "context": {},
          "issues": [],
          "nextHopId": "d996b6e0-dee4-49a8-ba70-f0b7a1c55e03",
          "resourceId": ""
        }
      ],
      "nextHopIds": [
        "d996b6e0-dee4-49a8-ba70-f0b7a1c55e03"
      ],
      "previousHopIds": [],
      "previousLinks": [],
      "resourceId": "/subscriptions/YOUR-SUBSCRIPTION-ID/resourceGroups/YOUR-RESOURCE-GROUP-NAME/providers/Microsoft.Compute/virtualMachines/ClientCent01",
      "type": "Source"
    },
    {
      "address": "AppGw-Network-Test01-TestUS3",
      "id": "d996b6e0-dee4-49a8-ba70-f0b7a1c55e03",
      "issues": [],
      "links": [],
      "nextHopIds": [],
      "previousHopIds": [
        "a94a2aa0-07a7-4c32-abdc-3cd139d064a6"
      ],
      "previousLinks": [
        {
          "context": {},
          "issues": [],
          "nextHopId": "a94a2aa0-07a7-4c32-abdc-3cd139d064a6",
          "resourceId": ""
        }
      ],
      "type": "Destination"
    }
  ],
  "probesFailed": 0,
  "probesSent": 0
}

上記は失敗していますが 20.169.48.200 は ClientCent01 の Public IP であり、始端 VM から Azure Application Gateway でアクセスしようとした結果で、名前解決で失敗(DNSResolution)していることが分かります。リソースタイプ側でコマンド実行すればよいのか、念のためコマンド自体を確認してみると、以下の様に現時点でコマンドラインでは VM しかサポートされていないことが分かりました。

$ az network watcher test-connectivity -h

...

Arguments
    --source-resource [Required] : Name or ID of the resource from which to originate traffic.
                                   Currently only Virtual Machines are supported.
    --no-wait                    : Do not wait for the long-running operation to finish.  Allowed
                                   values: 0, 1, f, false, n, no, t, true, y, yes.
    --protocol                   : Protocol to test on.  Allowed values: Http, Https, Icmp, Tcp.
    --resource-group -g          : Name of the resource group the target resource is in.
    --source-port                : Port number from which to originate traffic.

Destination Arguments
    --dest-address               : IP address or URI at which to receive traffic.
    --dest-port                  : Port number on which to receive traffic.
    --dest-resource              : Name or ID of the resource to receive traffic. Currently only
                                   Virtual Machines are supported.

クライアント VMアーキテクチャ図 ClientCent01)から宛先 Azure Application Gateway にアクセスする(※ IP アドレス指定

Azure Application Gateway 側できちんと名前解決できるように設定してあげれば問題ないと思いますが、検証環境ではそんな設定をしたくないということが多いでしょう。その場合は直接 Azure Application Gateway のグローバル IP を指定すればアクセス可能です。本節では以下の部分の疎通を取ります。

ポータルで以下の様に Destination type を Specify manually を選択し、Azure Application Gateway のグローバル IP を入力してください。

正常に設定されていれば以下の様にアクセスが可能なはずです。

詳細を確認すると 10.0.0.4 や 10.0.0.6 といった IP がありますが、こちらは終端側の VM のプライベート IP でなく、Azure Application Gateway 側のサブネットの範囲なので、Azure Application Gatewayインスタンス(という表現が適切か分かりませんが)までしか届かないことが分かります。

これをコマンドで実行すると以下の様になります。

$ az network watcher test-connectivity --resource-group YOUR-RESOURCE-GROUP-NAME --source-resource ClientCent01 --dest-address 20.150.136.xxx --dest-port 80
az : WARNING: This command is in preview and under development. Reference and support levels: https://aka.ms/CLI_refstatus
At line:1 char:1
+ az network watcher test-connectivity --resource-group RG-Network-Test ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (WARNING: This c...s/CLI_refstatus:String) [], RemoteException
    + FullyQualifiedErrorId : NativeCommandError
 
{
  "avgLatencyInMs": 1,
  "connectionStatus": "Reachable",
  "hops": [
    {
      "address": "10.1.0.4",
      "id": "ea04f6fb-fb7e-4aad-9cc8-6c19106dd861",
      "issues": [],
      "links": [
        {
          "context": {},
          "issues": [],
          "linkType": "Internet",
          "nextHopId": "72256fb6-35ce-4442-80ba-0453f63f3b0f",
          "resourceId": ""
        }
      ],
      "nextHopIds": [
        "72256fb6-35ce-4442-80ba-0453f63f3b0f"
      ],
      "previousHopIds": [],
      "previousLinks": [],
      "resourceId": "/subscriptions/YOUR-SUBSCRIPTION-ID/resourceGroups/YOUR-RESOURCE-GROUP-NAME/providers/Microsoft.Compute/virtualMachines/ClientCent01",
      "type": "Source"
    },
    {
      "address": "20.150.136.xxx",
      "id": "72256fb6-35ce-4442-80ba-0453f63f3b0f",
      "issues": [],
      "links": [
        {
          "context": {},
          "issues": [],
          "linkType": "VirtualNetwork",
          "nextHopId": "f3308930-64a5-4657-a70c-ba5d7aa719c0",
          "resourceId": ""
        },
        {
          "context": {},
          "issues": [],
          "linkType": "VirtualNetwork",
          "nextHopId": "8f8d04fa-5c91-4b0d-9e5c-b1d96fb912a8",
          "resourceId": ""
        }
      ],
      "nextHopIds": [
        "f3308930-64a5-4657-a70c-ba5d7aa719c0",
        "8f8d04fa-5c91-4b0d-9e5c-b1d96fb912a8"
      ],
      "previousHopIds": [
        "ea04f6fb-fb7e-4aad-9cc8-6c19106dd861"
      ],
      "previousLinks": [
        {
          "context": {},
          "issues": [],
          "linkType": "Internet",
          "nextHopId": "ea04f6fb-fb7e-4aad-9cc8-6c19106dd861",
          "resourceId": ""
        }
      ],
      "type": "PublicLoadBalancer"
    },
    {
      "address": "10.0.0.4",
      "id": "f3308930-64a5-4657-a70c-ba5d7aa719c0",
      "issues": [],
      "links": [],
      "nextHopIds": [],
      "previousHopIds": [
        "72256fb6-35ce-4442-80ba-0453f63f3b0f"
      ],
      "previousLinks": [
        {
          "context": {},
          "issues": [],
          "linkType": "VirtualNetwork",
          "nextHopId": "72256fb6-35ce-4442-80ba-0453f63f3b0f",
          "resourceId": ""
        }
      ],
      "type": "VirtualNetwork"
    },
    {
      "address": "10.0.0.6",
      "id": "8f8d04fa-5c91-4b0d-9e5c-b1d96fb912a8",
      "issues": [],
      "links": [],
      "nextHopIds": [],
      "previousHopIds": [
        "72256fb6-35ce-4442-80ba-0453f63f3b0f"
      ],
      "previousLinks": [
        {
          "context": {},
          "issues": [],
          "linkType": "VirtualNetwork",
          "nextHopId": "72256fb6-35ce-4442-80ba-0453f63f3b0f",
          "resourceId": ""
        }
      ],
      "type": "VirtualNetwork"
    }
  ],
  "maxLatencyInMs": 4,
  "minLatencyInMs": 1,
  "probesFailed": 0,
  "probesSent": 66
}

Azure Application Gateway から終端 VMアーキテクチャ図 CentVM01)にアクセスする(※リソース名指定

先ほどまでの検証だと始端 VM -> Azure Application Gateway までの疎通しか取れていないので、Azure Application Gateway -> Azure Firewall -> Azure VM 部分の疎通を通したいと思います。アーキテクチャ図でいうと以下の部分になります。

こちらの疎通を通すには以下の様にポータルで入力してください。Source type で Application Gateway を選択し、当該リソースを選択してください。

設定に問題が無ければ以下の様に疎通が通るはずです。

詳細を確認すると Azure Firewall (10.0.200.5)のアドレスを経由しており、ネットワークが適切に経由されていることが分かります。

こちらをコマンドで実行してみたかったのですが、コマンドの場合は --source-resource の指定が必須であり、VM 以外は受け付けていません。コマンド実行は REST API を直接叩く等の処理が必要になると思います。

コマンドで Azure Application Gateway を止めたり開始したりして課金を抑える

皆さんは Azure Application Gateway が起動や停止の制御ができるのはご存じでしょうか?使っていない仮想マシンを止めるのは良く行う節約術だと思いますが、Azure Application Gateway は管理ポータルだと開始・停止の制御ができないのでご存じではない方もいるのではないかと思っています。
実際に止めた場合は Properties の画面で状態を確認でき、以下の様に Operational State が Stopped になっています。この場合に Azure Application Gateway は課金されませんが当然リクエストは通しません(課金されない旨は参考リンクの GitHub 上でその旨記載があります)。

実際にどのようなコマンドを利用すればよいかというと、以下のコマンドで Azure Application Gateway の開始・停止が可能です。

az network application-gateway start --name YOUR-APPGW-NAME --resource-group YOUR-RESOURCE-GROUP-NAME
az network application-gateway stop --name YOUR-APPGW-NAME --resource-group YOUR-RESOURCE-GROUP-NAME

起動・停止のコマンド実行完了には体感では2~3分程度かかったので、ご参考までに。