ROMANCE DAWN for the new world

Microsoft Azure を中心とした技術情報を書いています。

ASP.NET Core アプリケーションで Azure Key Vault の使い方を改めて考えてみた

11 月に .NET 6 がリリースされることもあり、現時点での ASP.NET Core アプリケーションにおける Azure Key Vault の使い方を改めて考えてみました。
データベースの接続文字列や API Key などのセンシティブな情報は、アプリケーションのリポジトリ内では管理したくありません。
アプリケーションとセンシティブな情報を分離させる構成を実現する方法にはいつくかありますが、現時点では次の方針がシンプルでいいのではと考えています。

  • Azure の本番環境では、Azure Key Vault を使って Managed Service Identity(MSI)で認証する
  • ローカルの開発環境では、ASP.NET Core の Secret Manger を使う
  • 単体テストでは、センシティブな情報のモッククラスを作る

上記の方針に従って、実際に構成を作って試してみます。

Azure Key Vault にデータベースの接続文字列を登録する

ASP.NET Core アプリケーションからアクセスする SQL Database の接続文字列を登録するシナリオを例に説明していきます。
Key Vault の Secret に WebApp20--SqlConnection という名前で、SQL Connection for Key Vault. という Value を登録します。

f:id:TonyTonyKun:20210912095856p:plain

名前に WebApp20 のプレフィックスを付けた理由は、1つの Key Vault でフロントエンドやバックエンドなど複数のアプリケーションの構成を管理する想定だからです。
JSON の階層構造で管理したいので -- で区切っています。Key Vault の名前には : を使うことができないで注意してください。

ASP.NET Core アプリケーションで Azure Key Vault を参照する

.NET 5 の Web API アプリケーションを作成し、必要な NuGet パッケージをインストールします。

Install-Package Azure.Identity -Version 1.4.1
Install-Package Azure.Extensions.AspNetCore.Configuration.Secrets -Version 1.2.1

開発環境以外では MSI を使って Key Vault に接続するように構成します。

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.ConfigureAppConfiguration((hostingContext, config) =>
            {
                var settings = config.Build();

                if (!hostingContext.HostingEnvironment.IsDevelopment())
                {
                    var keyVaultUrl = settings["KeyVaultUrl"];
                    config.AddAzureKeyVault(new Uri(keyVaultUrl), new DefaultAzureCredential());
                }
            });

            webBuilder.UseStartup<Startup>();
        });

接続する Key Vault の URL は、appsettings.json で定義しておきます。

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "KeyVaultUrl": "https://xxx.vault.azure.net/"
}

アプリケーションで SQL Database の接続文字列を扱いやすくするため、POCO クラスにマッピングします。

public class MySettings
{
    public string SqlConnection { get; set; }
}

Key Vault の Secret から自分のアプリケーション情報だけを取得したいので、WebApp20 のプレフィックスを指定しています。

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<MySettings>(Configuration.GetSection("WebApp20"));

    services.AddControllers();
    services.AddSwaggerGen(c =>
    {
        c.SwaggerDoc("v1", new OpenApiInfo { Title = "WebApplication20", Version = "v1" });
    });
}

動作確認のため、SQL Database の接続文字列を返す API を作ります。

[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
    private readonly MySettings _settings;

    public ValuesController(IOptions<MySettings> optionsAccessor)
    {
        _settings = optionsAccessor.Value;
    }

    [HttpGet]
    public ActionResult<string> Get()
    {
        return Ok(_settings.SqlConnection);
    }
}

Azure 本番環境

ASP.NET Core アプリケーションをデプロイする Azure Web Apps の MSI を有効化し、Key Vault の Access policies を設定します。

$ az webapp identity assign --name "<Web Apps Name>" --resource-group "<Resource Group Name>"
{
  "principalId": "xxx",
  "tenantId": "yyy",
  "type": "SystemAssigned",
  "userAssignedIdentities": null
}

$ az keyvault set-policy --name "<Key Vault Name>" --object-id "xxx" --secret-permissions get list

これで Web Apps 上の ASP.NET Core アプリケーションは、MSI で Key Vault に認証して SQL Database の接続文字列を取得できます。

f:id:TonyTonyKun:20210912114101p:plain

Azure ポータルから設定する場合は、こちらの記事を参照してください。
gooner.hateblo.jp

ローカル開発環境

Visual Studio の機能でローカル開発環境から Key Vault に接続することはできますが、環境によって上手く認証できなかったり、デバッグ実行が遅くなったりするので、今回は使いません。
ASP.NET Core の Secret Manger を使って、プログラマーが開発用の構成情報を管理してコードを書いていくようにします。
docs.microsoft.com

secrets.json ファイルには、このように SQL Database の接続文字列を定義します。

{
  "WebApp20": {
    "SqlConnection": "SQL Connection for Secret Manager."
  }
}

Visual Studio のプロジェクトを右クリックしたユーザーシークレットの管理から定義してもいいですが、アプリケーションのリポジトリとは別で管理する JSON ファイルを dotnet user-secrets コマンドで読み込ませるほうが便利です。

$ type .\webapp20-secrets.json | dotnet user-secrets set --project "C:\Users\thara\source\repos\WebApplication20\WebApplication20"

これでローカル環境では、secrets.json から取得した SQL Database の接続文字列を使って開発できます。

単体テスト環境

単体テストを実行する際には、テストプロジェクト内で MySettings クラスの モックを作り、SQL Database の接続文字列を渡すようにします。

public class ValuesControllerTest
{
    [Fact]
    public void Get_ReturnsOkResult()
    {
        var settings = new MySettings()
        {
            SqlConnection = "SQL Connection for Unit Test."
        };
        IOptions<MySettings> options = Options.Create(settings);
        var controller = new ValuesController(options);

        // Act
        var actionResult = controller.Get();

        // Assert
        var result = Assert.IsType<OkObjectResult>(actionResult.Result);
        Assert.Equal(settings.SqlConnection, (result.Value as string[])[0]);
    }
}

これでローカル開発でも CI/CD のパイプラインでも、実行環境の影響を受けずに単体テストを実行することができます。

まとめ

現時点での ASP.NET Core アプリケーションにおける Azure Key Vault の使い方を改めて考えてみました。
アプリケーションの要件やチームの文化などにもよりますが、ベースはこの方針がいいかなと思っています。
セキュリティという観点では、Key Vault 自体を閉域網構成で管理したい話もあるので、そちらは追加で検討が必要です。

Visual Studio 2022 for Mac Private Preview を試してみた

先月、Visual Studio 2022 for Mac の Private Preview が発表されました。ダウンロードするには申請が必要となり、リクエストが多いようで現在は新規申請を受け付けていない状況ですが、先日サインアップされた連絡が来たので試してみました。
devblogs.microsoft.com
まだ実際に試すことができる人は限られていると思いますので、ファーストインプレッションを共有することにしました。ちなみに、軽く触った結果のスクリーンショットをペタペタと貼っただけなので、あまり内容はありません。

インストール

早速インストールを行います。VS 本体は 908MB となっており、コンポーネントは .NET Core(291MB)のみ選択しました。

f:id:TonyTonyKun:20210822103528p:plain

コンポーネントのダウンロードとインストールが完了するまで待ちます。

f:id:TonyTonyKun:20210822103559p:plain

今回の環境には Xcode が入っていなかったので、インストールを要求されました。

f:id:TonyTonyKun:20210822103635p:plain

インストール完了後、VS を起動してみます。必須ではありませんが、Microsoft  アカウントの入力を要求されます。

f:id:TonyTonyKun:20210822103720p:plain

次に、キーボード ショートカットの種類を選択することができました。これは今まではなかった機能だと思います。デフォルトの Visual Studio for Mac を選択しました。

f:id:TonyTonyKun:20210822103758p:plain

ここまでが初回起動時に表示される画面で、次回以降はこのような画面から操作することになります。

f:id:TonyTonyKun:20210822103822p:plain

私がインストールしたバージョンは、17.0 Preview でした。

f:id:TonyTonyKun:20210822104110p:plain
docs.microsoft.com

ASP.NET Core

新しいプロジェクトを作成してみます。ASP.NET Core の API を選択しました。

f:id:TonyTonyKun:20210822103951p:plain

選択できる Target Framework は、.NET Core 3.1 と .NET 6.0 の2つでした。

f:id:TonyTonyKun:20210822104008p:plain

Version Control として、.gitignore を追加することができます。

f:id:TonyTonyKun:20210822111628p:plain

お馴染みの WeatherForecastController が作成され、コードを書いてデバッグ実行するなどのひと通りの操作ができました。

f:id:TonyTonyKun:20210822104028p:plain

Azure へのデプロイ

Windows 版と同様に、VS から Azure App Service にデプロイすることができます。

f:id:TonyTonyKun:20210822104333p:plain

新しくリソースを作成する以外にも、既存のリソースを選択することもできます。

f:id:TonyTonyKun:20210822104349p:plain

Azure Functions

今度は、Azure Functions の新しいプロジェクトを作成してみます。開発言語は C# しか選択できませんでした。
ひと通りのトリガーを選択できる感じですが、HTTP トリガーを作成してみます。

f:id:TonyTonyKun:20210822104425p:plain

Azure Functions を実行する際に使うストレージについては、ローカルの Azurite もしくは Azure の Storage Account を使います。今回は Azure に Storage Account を作成して動作確認しました。HTTP トリガーの以外に Queue トリガーも確認しましたが、問題なく動作しました。

f:id:TonyTonyKun:20210822105145p:plain

Azurite については、時間を見つけて確認してみたいと思います。
docs.microsoft.com

デバッグ実行した際に、Windows 版ではコマンドプロンプト画面が別ウインドウで起動しますが、Mac 版では VS 内部に統合されていました。

f:id:TonyTonyKun:20210822104456p:plain

まとめ

Mac 版の VS が初めてリリースされた5年前にも試してみましたが、当時と比較するとだいぶ使えるレベルに洗練されてきた感じです。
gooner.hateblo.jp

VS を Mac で使うシナリオとしては、ASP.NET Core アプリよりは Azure Functions の方が多いかなと思っています。現状では VS Code + Azure Functions Core Tools の組み合わせには劣ってしまうので、今後の進化に期待したいとフィードバックしておきました。

Azure Cognitive Search への接続で TLS1.2 エラーが発生していた件

6年くらい前に作った Azure Cognitive Search を使った Web アプリケーションで、「リモート パーティがトランスポート ストリームを終了したため、認証に失敗しました。」という接続エラーが発生していました。

f:id:TonyTonyKun:20210612200924p:plain

現在の Azure Cognitive Search は TLS1.2 接続を強制しているため、.NET Framework 4.5.2 の既定動作では接続できないことが原因でした。

対応としては、Web アプリケーションの起動時の Startup クラスで、SecurityProtocol に TLS1.2 の対応を追加してあげれば OK です。SecurityProtocolTypeを列挙せずとも、この書き方で追加できます。

[assembly: OwinStartup(typeof(GoroWebsitesV2.Startup))]
namespace GoroWebsitesV2
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            // TLS1.2 を追加
            ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12;

            app.UseWebApi(config);
        }
    }
}

OwinStartup とか、めっちゃ懐かしい・・・