しばやん雑記

Azure Serverless とメイドさんが大好きなフリーランスのプログラマーのブログ

Managed Identity を使った SQL Database の認証がとても簡単になった話

App Service のドキュメントには Managed Identity を使って SQL Database を利用するサンプルが載っていますが、ここのサンプルコードは結構いい加減で特に .NET Core 向けでは使う気がしないものでした。

サンプルコードがダメなだけで SQL Database の設定周りは特に問題ないので、Managed Identity を使って SQL Database を利用したい場合は、このドキュメントを参考に Azure AD のユーザー作成や管理者の追加などを行ってください。

具体的に何がダメかというと .NET Core の場合は以下のような実装を載せているところです。

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options)
        : base(options)
    {
        var connection = (SqlConnection)Database.GetDbConnection();

        // この例だと同期で取るしか方法がない
        connection.AccessToken = new AzureServiceTokenProvider().GetAccessTokenAsync("https://database.windows.net/").Result;
    }

    public DbSet<TodoItem> TodoItems { get; set; }
}

この方法だとローカル開発環境でも Managed Identity が強制されますし、そもそもコンストラクタ内で同期処理でトークン取得してを設定させるのは意味が分からないですね。サンプルとは言えとにかく酷いです。

しかし Microsoft.Data.SqlClient の v2.1.0 からはプロバイダに Managed Identity 対応が追加されたため、上のサンプルのような最悪なコードを書く必要が無くなりました。

最近追加されたのであまり知られていないようですが、ちゃんとドキュメントも用意されています。

同様に Microsoft.Data.SqlClient のリリースノートにも記載されています。

要約すると接続文字列に Authentication=Active Directory Managed Identity を追加するだけで、後は自動的に Managed Identity を使った認証を行ってくれるようになりました。最高ですね。

ちょっと前は App Service や Azure Functions などの System Assigned Managed Identity だと正常に動かなかったようですが、Microsoft.Data.SqlClient v2.1.1 で修正されたとリリースノートに書いてあります。

大きなメリットとして接続文字列を変更するだけで、本番では System Assigned Managed Identity を使いつつも、ローカル開発環境では ID / パスワードや Windows 統合認証でアクセス出来るところにあります。

サンプルコードを書いて試してみましたが、これまでのコードと全く同じで接続文字列のみ違うという状態です。接続文字列の修正だけで Managed Identity が使えるため、移行などの手間が発生しません。

public class Startup : FunctionsStartup
{
    public override void Configure(IFunctionsHostBuilder builder)
    {
        var connectionString = "Server=***.database.windows.net,1433;Database=***;Authentication=Active Directory Managed Identity;";

        builder.Services.AddDbContext<AppDbContext>(options => options.UseSqlServer(connectionString));
    }
}

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options)
        : base(options)
    {
    }

    public DbSet<TodoItem> TodoItems { get; set; }
}

public class TodoItem
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Body { get; set; }
}

NuGet パッケージに関しては Entity Framework Core が参照しているバージョンが古いため、明示的に新しいバージョンをインストールする必要はありますが、大した手間ではありません。

おまけ的に Function の実装も載せておきます。単純に DI で AppDbContext を受け取って、LINQ でレコードを取得しているだけなので説明するまでもないですが。

public class Function1
{
    public Function1(AppDbContext appDbContext)
    {
        _appDbContext = appDbContext;
    }

    private readonly AppDbContext _appDbContext;

    [FunctionName("Function1")]
    public async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Function, "get")] HttpRequest req,
        ILogger log)
    {
        var todoItems = await _appDbContext.TodoItems.AsNoTracking().Take(10).ToArrayAsync();

        return new OkObjectResult(todoItems);
    }
}

この Function を Managed Identity が有効になっている Azure Function にデプロイして、適当に実行してみると正しくレコードが取得できていることが確認できました。

f:id:shiba-yan:20210109161342p:plain

Application Insights で E2E ログを確認してみるとトークンを取得しているのが確認できます。

f:id:shiba-yan:20210109161906p:plain

SQL のログは残念ながら出ていませんが、これは Microsoft.Data.SqlClient で EventSource 名が変わったことに Application Insights 側が追従できていないことによります。

最新のプレビュー版 SDK だと修正されているようなので、ASP.NET Core アプリケーションで使う場合は試してみると良いでしょう。Azure Functions では Runtime がアップデートされるのを待つ必要があります。

既存のコードを変更することなく、パッケージのアップデートと接続文字列の変更だけで Managed Identity を使った認証が行えるようになったので、劇的に使い勝手が良くなりました。IaC との相性もかなり良い感じなので、今後は使っていきたいと思います。