C# で Azure 仮想マシンのデータディスクを入れ替える

Azure.ResourceManager.Compute を使って、Azure 仮想マシンのデータディスクもアタッチやデタッチができる。

www.nuget.org

OSディスクのときと違って、データディスクは ID の変更ができないので、データディスクのアタッチとでタッチを行うことで、データディスクを入れ替えた状態にしてみた。

using Azure;
using Azure.Identity;
using Azure.ResourceManager;
using Azure.ResourceManager.Compute;
using Azure.ResourceManager.Compute.Models;

const string ClientId = "クライアントID";
const string ClientSecret = "クライアントシークレット";
const string TenantId = "テナントID";
const string SubscriptionId = "サブスクリプションID";
const string ResourceGroupName = "リソースグループ名";
const string VmName = "仮想マシン名";
const string DiskName = "マネージドディスク名";

var credential = new ClientSecretCredential(
    tenantId: TenantId,
    clientId: ClientId,
    clientSecret: ClientSecret);
var client = new ArmClient(credential);

var vmId = VirtualMachineResource.CreateResourceIdentifier(
    subscriptionId: SubscriptionId,
    resourceGroupName: ResourceGroupName,
    vmName: VmName);
var vm = client.GetVirtualMachineResource(vmId);
vm = await vm.GetAsync();

// 仮想マシンを停止
await vm.DeallocateAsync(WaitUntil.Completed);

// データディスクをデタッチ
var lun = vm.Data.StorageProfile.DataDisks.Select(x => x.Lun).FirstOrDefault();
var detachPatch = new VirtualMachinePatch
{
    StorageProfile = vm.Data.StorageProfile,
};
detachPatch.StorageProfile.DataDisks.Clear();
var detachOperation = await vm.UpdateAsync(WaitUntil.Completed, detachPatch);
vm = await detachOperation.WaitForCompletionAsync();

// データディスクをアタッチ
var diskId = ManagedDiskResource.CreateResourceIdentifier(
    subscriptionId: SubscriptionId,
    resourceGroupName: ResourceGroupName,
    diskName: DiskName);
var attachPatch = new VirtualMachinePatch
{
    StorageProfile = vm.Data.StorageProfile,
};
attachPatch.StorageProfile.DataDisks.Add(new VirtualMachineDataDisk(lun, DiskCreateOptionType.Attach)
{
    Caching = CachingType.ReadWrite,
    ManagedDisk = new VirtualMachineManagedDisk
    {
        Id = diskId,
    },
});
var attachOperation = await vm.UpdateAsync(WaitUntil.Completed, attachPatch);
vm = await attachOperation.WaitForCompletionAsync();

// 仮想マシンを開始
await vm.PowerOnAsync(WaitUntil.Completed);

そば処まさや

薬院にある「そば処まさや」に行ってきました。薬院大通駅の近く。福岡中央病院の斜め前。福岡に来て最初に住んでいたマンションの目の前。懐かしい。まぁ、1年くらいで引越したけど。住んでいた当時は有名な店と知らなくて、行ったことなかったな。旅サラダっていう番組で知った。

この店はそば処とあるけど、ラーメンが人気。今回のお目当ては、もちろんラーメン。鶏ガラスープを使った和風スープにストレート細麺が、シンプルであっさりしていて、滋味溢れて美味。

なんだか、子どもの頃住んでいた団地に木曜日来ていた、トラック屋台のラーメン屋を思い出した。そば処の店内の雰囲気も相まって、懐かしさすら感じる一杯だった。

うちの子どもは店で食べるラーメンが好みじゃないみたいで、なかなか家族でラーメンを食べに行きづらい。この店なら蕎麦もあるので、子どもは蕎麦を食べ、親はラーメンを食べる、という作戦がとれるな。

あと、後で知ったんだけど、ラーメンと小ライスと唐揚げのセットがあったらしい。そっちにしたかった。メニュー見落とした…。家族で動植物園に行った帰りにでもリベンジしたい。

r.gnavi.co.jp

C# で Azure 仮想マシンの OS ディスクをスワップする

Azure.ResourceManager.Compute を使って、Azure 仮想マシンの OS ディスクをスワップできた。

www.nuget.org

using Azure;
using Azure.Identity;
using Azure.ResourceManager;
using Azure.ResourceManager.Compute;
using Azure.ResourceManager.Compute.Models;

const string ClientId = "クライアントID";
const string ClientSecret = "クライアントシークレット";
const string TenantId = "テナントID";
const string SubscriptionId = "サブスクリプションID";
const string ResourceGroupName = "リソースグループ名";
const string VmName = "仮想マシン名";
const string DiskName = "マネージドディスク名";

var credential = new ClientSecretCredential(
    tenantId: TenantId,
    clientId: ClientId,
    clientSecret: ClientSecret);
var client = new ArmClient(credential);

var vmId = VirtualMachineResource.CreateResourceIdentifier(
    subscriptionId: SubscriptionId,
    resourceGroupName: ResourceGroupName,
    vmName: VmName);
var vm = client.GetVirtualMachineResource(vmId);
vm = await vm.GetAsync();

// 仮想マシンを停止
await vm.DeallocateAsync(WaitUntil.Completed);

// OS ディスクをスワップ
var diskId = ManagedDiskResource.CreateResourceIdentifier(
    subscriptionId: SubscriptionId,
    resourceGroupName: ResourceGroupName,
    diskName: DiskName);
var update = new VirtualMachinePatch
{
    StorageProfile = new()
    {
        // Attach ではなく FromImage にしないと 409 になる
        OSDisk = new VirtualMachineOSDisk(DiskCreateOptionType.FromImage)
        {
            OSType = SupportedOperatingSystemType.Windows,
            Caching = CachingType.ReadWrite,
            ManagedDisk = new VirtualMachineManagedDisk
            {
                Id = diskId,
            },
        },
    }
};
var operation = await vm.UpdateAsync(WaitUntil.Completed, update);
vm = await operation.WaitForCompletionAsync();

// 仮想マシンを開始
await vm.PowerOnAsync(WaitUntil.Completed);

VirtualMachineResource.UpdateAsync に渡した VirtualMachinePatch の内容がこれで良いのか自信がない。一応期待通り動いたっぽいけど。

C# で Azure VM の停止・開始・再起動を行う

Azure.ResourceManager.Compute を使えば、C#仮想マシンの停止・開始・再起動ができる。

www.nuget.org

認証には Azure.Identity を使う。

www.nuget.org

アプリに組み込んで使うことを想定しているので、Azure AD でアプリを登録しておく。

  1. Microsoft Azure ポータルで、リソースグループと仮想マシンはあらかじめ用意
  2. Azure Active Directory のアプリ登録でアプリを登録
  3. リソースグループのアクセス制御(IAM)で、ロールの割り当てを追加
    • 登録したアプリに仮想マシン共同作成者をひとまず割り当てておく

C# .NET 6 コンソールアプリを書いてみた。

using Azure;
using Azure.Identity;
using Azure.ResourceManager;
using Azure.ResourceManager.Compute;

const string ClientId = "クライアント ID";
const string ClientSecret = "クライアントシークレット";
const string TenantId = "テナント ID";
const string SubscriptionId = "サブスクリプション ID";
const string ResourceGroupName = "リソースグループ名";
const string VmName = "仮想マシン名";

var credential = new ClientSecretCredential(
    tenantId: TenantId,
    clientId: ClientId,
    clientSecret: ClientSecret);
var client = new ArmClient(credential);

var id = VirtualMachineResource.CreateResourceIdentifier(
    subscriptionId: SubscriptionId,
    resourceGroupName: ResourceGroupName,
    vmName: VmName);
var vm = client.GetVirtualMachineResource(id);

// 仮想マシンの停止
await vm.DeallocateAsync(WaitUntil.Completed);

// 仮想マシンの開始
await vm.PowerOnAsync(WaitUntil.Completed);

// 再起動
await vm.RestartAsync(WaitUntil.Completed);

Microsoft Authentication Library for .NET でリフレッシュトークンを永続化する

Microsoft Authentication Library for .NET(MSAL.NET) は、リフレッシュトークンを内部でキャッシュしていて、それを使ってアクセストークンを再取得できる。

ただ、キャッシュはメモリ上にしかないので、そのままだとプロセス終了したら失われてしまう。次回起動時もアクセストークンをサイレントに取得したいなら、永続化しないと。

Microsoft.Identity.Client.Extensions.Msal というパッケージを導入すれば、キャッシュを永続化できそう。

www.nuget.org

Microsoft Graph を呼び出すサンプルに組み込んでみた。

using System.Net.Http.Headers;
using Microsoft.Graph;
using Microsoft.Identity.Client;
using Microsoft.Identity.Client.Extensions.Msal;

// Microsoft Todo のタスクとリストを CRUD するには
// Tasks.ReadWrite の許可が必要。
// リフレッシュトークンを取得するためには
// offline_access も必要。
var scopes = new[]
{
    "offline_access",
    "Tasks.ReadWrite",
};

// 一般ユーザーの Microsoft Todo にアクセスするときは
// common を指定すれば良いみたい。
var tenantId = "common";

// Azure AD で登録したアプリケーションの ID (Client ID)
var clientId = "YOUR_CLIENT_ID";

// Web ブラウザでログインするときのメールアドレス。
// ちゃんと対応するときは、この情報も永続化しておく必要がある。
var loginHint = "your-address@outlook.jp";

var pca = PublicClientApplicationBuilder.Create(clientId)
    .WithTenantId(tenantId)
    .WithRedirectUri("http://localhost")
    .Build();

// キャッシュをシリアライズ・デシリアライズできるようにする。
var storageProperties = new StorageCreationPropertiesBuilder(
    "UserTokenCache",
    AppContext.BaseDirectory)
    .Build();
var cacheHelper = await MsalCacheHelper.CreateAsync(storageProperties);
cacheHelper.RegisterCache(pca.UserTokenCache);

AuthenticationResult? authResult = null;
try
{
    // キャッシュされているリフレッシュトークンを使って、
    // 新しいアクセストークンを取得。
    authResult = await pca.AcquireTokenSilent(scopes, loginHint)
        .WithForceRefresh(true)
        .ExecuteAsync();
    Console.WriteLine($"ExpiresOn: {authResult.ExpiresOn}");
}
catch (MsalClientException ex)
{
    // アクセストークンを取得できなかったら、
    // Web ブラウザを表示して、
    // ユーザーに Microsoft アカウントにログインし、
    // アクセストークンを取得。
    authResult = await pca.AcquireTokenInteractive(scopes).ExecuteAsync();
    Console.WriteLine($"ExpiresOn: {authResult.ExpiresOn}");
}

// DelegateAuthenticationProvider を使って、
// 取得しておいたアクセストークンを Authorization ヘッダーにセットして
// Microsoft Graph を呼び出せるようにする。
var authProvider = new DelegateAuthenticationProvider(request =>
{
    request.Headers.Authorization = new AuthenticationHeaderValue(
        "Bearer",
        authResult.AccessToken);
    return Task.CompletedTask;
});
var graphClient = new GraphServiceClient(authProvider);

// リスト一覧表示
var todoLists = await graphClient.Me
    .Todo
    .Lists
    .Request()
    .GetAsync();
foreach (var todoList in todoLists)
{
    Console.WriteLine($"既存のリスト: {todoList.DisplayName}");
}

Console.ReadLine();

Microsoft Authentication Library for .NET でアクセストークンを再取得して Microsoft Graph を呼び出す

Azure.Identity でアクセストークンを再取得する方法を見つけることができなかったので、Azure.Identity が依存している Microsoft Authentication Library for .NET(MSAL.NET) を試してみた。

using System.Net.Http.Headers;
using Microsoft.Graph;
using Microsoft.Identity.Client;

// Microsoft Todo のタスクとリストを CRUD するには
// Tasks.ReadWrite の許可が必要。
// リフレッシュトークンを取得するためには
// offline_access も必要。
var scopes = new[]
{
    "offline_access",
    "Tasks.ReadWrite",
};

// 一般ユーザーの Microsoft Todo にアクセスするときは
// common を指定すれば良いみたい。
var tenantId = "common";

// Azure AD で登録したアプリケーションの ID (Client ID)
var clientId = "YOUR_CLIENT_ID";

// Web ブラウザを表示して、
// ユーザーに Microsoft アカウントにログインし、
// アクセストークンを取得。
var pca = PublicClientApplicationBuilder.Create(clientId)
    .WithTenantId(tenantId)
    .WithRedirectUri("http://localhost")
    .Build();
var authResult = await pca.AcquireTokenInteractive(scopes).ExecuteAsync();
Console.WriteLine($"ExpiresOn: {authResult.ExpiresOn}");

// 内部でキャッシュされているリフレッシュトークンを使って、
// 新しいアクセストークンを取得。
var refreshResult = await pca.AcquireTokenSilent(scopes, authResult.Account.Username)
    .WithForceRefresh(true)
    .ExecuteAsync();
Console.WriteLine($"ExpiresOn: {refreshResult.ExpiresOn}");

// DelegateAuthenticationProvider を使って、
// 取得しておいたアクセストークンを Authorization ヘッダーにセットして
// Microsoft Graph を呼び出せるようにする。
var authProvider = new DelegateAuthenticationProvider(request =>
{
    request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", refreshResult.AccessToken);
    return Task.CompletedTask;
});
var graphClient = new GraphServiceClient(authProvider);

// リスト一覧表示
var todoLists = await graphClient.Me
    .Todo
    .Lists
    .Request()
    .GetAsync();
foreach (var todoList in todoLists)
{
    Console.WriteLine($"既存のリスト: {todoList.DisplayName}");
}

Console.ReadLine();

MSAL.NET はリフレッシュトークンを内部でキャッシュしているけど、取り出す手段は提供されていない。AcquireTokenSilent を呼び出すと、内部でキャッシュしているリフレッシュトークンを使って、アクセストークンを再取得してくれる。

.NET 6 で実行。

アクセストークンを再取得して Microsoft Graph を呼び出し、Microsoft Todo の TodoList 取得に成功した。

リフレッシュトークンのキャッシュはインメモリなので、プログラムを終了したら失われてしまう。永続化する方法を調べないといけないな。

NumSharp で 2 層ニューラルネットワーク

「ゼロから作る Deep Learning」を読んで C# でゼロから作ってみる続き。これから先は C# で初挑戦になる。

NumSharp を使って 2 層ニューラルネットワークを実装してみた。

using NumSharp;

var net = new TwoLayerNet(input_size: 784, hidden_size: 100, output_size: 10);
Console.WriteLine(net.W1.Shape);
Console.WriteLine(net.b1.Shape);
Console.WriteLine(net.W2.Shape);
Console.WriteLine(net.b2.Shape);

{
    var x = np.random.rand(100, 784); // ダミーの入力データ(100枚分)
    var y = net.predict(x);
    Console.WriteLine(y.ToString());
}

{
    var x = np.random.rand(100, 784); // ダミーの入力データ(100枚分)
    var t = np.random.rand(100, 10); // ダミーの正解ラベル

    var grads = net.numerical_gradient(x, t); // 勾配を計算

    Console.WriteLine(grads.W1.Shape);
    Console.WriteLine(grads.b1.Shape);
    Console.WriteLine(grads.W2.Shape);
    Console.WriteLine(grads.b2.Shape);
}

Console.ReadLine();

public class TwoLayerNet
{
    public NDArray W1 { get; }

    public NDArray W2 { get; }

    public NDArray b1 { get; }

    public NDArray b2 { get; }

    public TwoLayerNet(
        int input_size,
        int hidden_size,
        int output_size,
        double weight_init_std = 0.01)
    {
        W1 = weight_init_std * np.random.randn(input_size, hidden_size);
        b1 = np.zeros(hidden_size);
        W2 = weight_init_std * np.random.randn(hidden_size, output_size);
        b2 = np.zeros(output_size);
    }

    public NDArray predict(NDArray x)
    {
        var a1 = np.dot(x, W1) + b1;
        var z1 = functions.sigmoid(a1);
        var a2 = np.dot(z1, W2) + b2;
        var y = functions.softmax(a2);
        return y;
    }

    public NDArray loss(NDArray x, NDArray t)
    {
        var y = predict(x);
        return functions.cross_entropy_error(y, t);
    }

    public NDArray accuracy(NDArray x, NDArray t)
    {
        var y = predict(x);
        y = np.argmax(y, axis: 1);
        t = np.argmax(t, axis: 1);
        var accuracy = np.sum(y == t) / (float)x.shape[0];
        return accuracy;
    }

    // x:入力データ, t:教師データ
    public (NDArray W1, NDArray b1, NDArray W2, NDArray b2) numerical_gradient(NDArray x, NDArray t)
    {
        NDArray loss_W(NDArray W) => loss(x, t);
        var W1 = gradient.numerical_gradient(loss_W, this.W1);
        var b1 = gradient.numerical_gradient(loss_W, this.b1);
        var W2 = gradient.numerical_gradient(loss_W, this.W2);
        var b2 = gradient.numerical_gradient(loss_W, this.b2);
        return (W1, b1, W2, b2);
    }
}

static class functions
{
    public static NDArray softmax(NDArray x)
    {
        var c = np.max(x);
        var exp_x = np.exp(x - c);
        var sum_exp_x = np.sum(exp_x, NPTypeCode.Double);
        var y = exp_x / sum_exp_x;
        return y;
    }

    public static NDArray sigmoid(NDArray x)
    {
        return 1 / (1 + np.exp(x * -1));
    }

    public static NDArray cross_entropy_error(NDArray y, NDArray t)
    {
        var delta = 1e-7;
        return (-1) * np.sum(t * np.log(y + delta), NPTypeCode.Double);
    }
}

static class gradient
{
    static NDArray numerical_gradient_1d(Func<NDArray, NDArray> f, NDArray x)
    {
        var h = 1e-4; // 0.0001
        var grad = np.zeros_like(x); // x と同じ形状の配列を生成

        foreach (var idx in Enumerable.Range(0, x.size))
        {
            var tmp_val = (double)x[idx];

            // f(x + h) の計算
            x[idx] = tmp_val + h;
            var fxh1 = f(x);

            // f(x - h) の計算
            x[idx] = tmp_val - h;
            var fxh2 = f(x);

            grad[idx] = (fxh1 - fxh2) / (2 * h);
            x[idx] = tmp_val; // 値を元に戻す
        }

        return grad;
    }

    public static NDArray numerical_gradient(Func<NDArray, NDArray> f, NDArray X)
    {
        if (X.ndim == 1)
        {
            return numerical_gradient_1d(f, X);
        }
        else
        {
            var grad = np.zeros_like(X);

            for (var idx = 0; idx < X.ndim; idx++)
            {
                var x = X[idx];
                grad[idx] = numerical_gradient_1d(f, x);
            }

            return grad;
        }
    }
}

.NET 6 で実行。

今回は準備みたいなもの。ダミーの入力データとダミーの正解ラベルを渡して、実行時エラーが発生しなかったので、ひとまずヨシ。