Azure Web App から Managed Identity で SQL Database にアクセスする

# プロジェクトを作成して起動

```posh:PowerShell
$app = "ManagedIdentityTestApp"
mkdir $app
cd $app
dotnet new sln --name $app
dotnet new webapi --output $app --framework net5.0
dotnet sln add $app\$app.csproj
start .\$app.sln
```

# NuGet パッケージインストール

```posh:パッケージマネージャーコンソール
Install-Package Microsoft.EntityFrameworkCore.SqlServer
Install-Package Microsoft.EntityFrameworkCore.Tools
```

# コーディング

```csharp:Entities/Book.cs
namespace ManagedIdentityTestApp.Entities
{
    public class Book
    {
        public int BookId { get; set; }
        public string Title { get; set; }
        public string Author { get; set; }
    }
}

```

```csharp:Data/AppDbContext.cs
using ManagedIdentityTestApp.Entities;
using Microsoft.EntityFrameworkCore;

namespace ManagedIdentityTestApp.Data
{
    public class AppDbContext : DbContext
    {
        public AppDbContext(DbContextOptions options) : base(options)
        {
        }

        public DbSet<Book> Books { get; set; }
    }
}

```

```Controllers/BookController.cs
using ManagedIdentityTestApp.Data;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;

namespace ManagedIdentityTestApp.Controllers
{
    [Route("api/books")]
    [ApiController]
    public class BookController : ControllerBase
    {
        private readonly AppDbContext context;

        public BookController(AppDbContext context)
        {
            this.context = context;
        }

        [HttpGet]
        public async Task<IActionResult> ListAsync()
        {
            var books = await context.Books.ToListAsync();
            return Ok(books);
        }
    }
}

```

```csharp:Startup.cs
using ManagedIdentityTestApp.Data;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.OpenApi.Models;

namespace ManagedIdentityTestApp
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {

            services.AddControllers();
            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new OpenApiInfo { Title = "ManagedIdentityTestApp", Version = "v1" });
            });
            services.AddDbContext<AppDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); //追加
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseSwagger();
                app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "ManagedIdentityTestApp v1"));
            }

            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }
}

```

```json:appsettings.json
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=ManagedIdentityTestAppDb;Trusted_Connection=True;"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*"
}

```

# データ準備

```posh:パッケージマネージャーコンソール
Add-Migration init
Update-Database
```

SQL Server でレコード追加クエリ実行。

```sql
INSERT INTO Books (Title, Author) VALUES (N'たったひとつの冴えたやりかた',N'ジェイムズ・ティプトリー・ジュニア');
INSERT INTO Books (Title, Author) VALUES (N'アンドロイドは電気羊の夢を見るか?',N'フィリップ・K・ディック');
INSERT INTO Books (Title, Author) VALUES (N'夏への扉',N'ロバート・A. ハインライン');
INSERT INTO Books (Title, Author) VALUES (N'幼年期の終り',N'アーサー C クラーク');
INSERT INTO Books (Title, Author) VALUES (N'われはロボット',N'アイザック・アシモフ');
```

# 動作確認

ローカル実行して `api/books` にアクセスしてデータが取得できることを確認。

```json
[
	{
		"bookId": 1,
		"title": "たったひとつの冴えたやりかた",
		"author": "ジェイムズ・ティプトリー・ジュニア"
	},
	{
		"bookId": 2,
		"title": "アンドロイドは電気羊の夢を見るか?",
		"author": "フィリップ・K・ディック"
	},
	{
		"bookId": 3,
		"title": "夏への扉",
		"author": "ロバート・A. ハインライン"
	},
	{
		"bookId": 4,
		"title": "幼年期の終り",
		"author": "アーサー C クラーク"
	},
	{
		"bookId": 5,
		"title": "われはロボット",
		"author": "アイザック・アシモフ"
	}
]
```

# Azure リソース作成

```posh:PowerShell
# Azure にログイン
az login

# リソースグループ作成
az group create --location japaneast --name ManagedIdentityTestRG

# App Service Plan 作成
az appservice plan create --name ManagedIdentityTestAppServicePlan --resource-group ManagedIdentityTestRG --location japaneast --sku FREE

# Web App 作成
$random = Get-Random
az webapp create --name web-app-$random --plan ManagedIdentityTestAppServicePlan --resource-group ManagedIdentityTestRG --runtime "DOTNET:5.0"

# SQL Server 作成
az sql server create --name sql-server-$random --resource-group ManagedIdentityTestRG --enable-ad-only-auth --external-admin-name SqlServerAdministrators --external-admin-principal-type Group --external-admin-sid 3329c7ca-75c6-4823-a9b0-8932a900fd37 --minimal-tls-version 1.2

# SQL Database 作成
az sql db create --name ManagedIdentityTestDb --resource-group ManagedIdentityTestRG --server sql-server-$random --collation Japanese_CI_AS --edition Free

# Azure サービスからのアクセスを許可
az sql server firewall-rule create --resource-group ManagedIdentityTestRG --server sql-server-$random --name allowAzureService --start-ip-address 0.0.0.0 --end-ip-address 0.0.0.0

# システム割り当てマネージド ID 有効化
az webapp identity assign --resource-group ManagedIdentityTestRG --name web-app-$random

# マネージド ID を管理者グループに追加
az ad group member add --group SqlServerAdministrators --member-id 24d820ca-8e3a-4b4d-987c-36fdd10e021b

# 接続文字列追加
az webapp config connection-string set --connection-string-type SQLServer --name web-app-$random --resource-group ManagedIdentityTestRG --settings DefaultConnection="Server=tcp:sql-server-$random.database.windows.net;Authentication=Active Directory Managed Identity; Database=ManagedIdentityTestDb;"
```

# データ登録

DDL を取得する。

```posh:パッケージマネージャーコンソール
Script-Migration
```

こんなのが出力される。

```sql
IF OBJECT_ID(N'[__EFMigrationsHistory]') IS NULL
BEGIN
    CREATE TABLE [__EFMigrationsHistory] (
        [MigrationId] nvarchar(150) NOT NULL,
        [ProductVersion] nvarchar(32) NOT NULL,
        CONSTRAINT [PK___EFMigrationsHistory] PRIMARY KEY ([MigrationId])
    );
END;
GO

BEGIN TRANSACTION;
GO

CREATE TABLE [Books] (
    [BookId] int NOT NULL IDENTITY,
    [Title] nvarchar(max) NULL,
    [Author] nvarchar(max) NULL,
    CONSTRAINT [PK_Books] PRIMARY KEY ([BookId])
);
GO

INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES (N'20211031085850_init', N'5.0.11');
GO

COMMIT;
GO

```

出力された DDL を SQL Database に流し込む。Azure Portal のクエリエディタを使用。

レコードも追加する。

```sql
INSERT INTO Books (Title, Author) VALUES (N'たったひとつの冴えたやりかた',N'ジェイムズ・ティプトリー・ジュニア');
INSERT INTO Books (Title, Author) VALUES (N'アンドロイドは電気羊の夢を見るか?',N'フィリップ・K・ディック');
INSERT INTO Books (Title, Author) VALUES (N'夏への扉',N'ロバート・A. ハインライン');
INSERT INTO Books (Title, Author) VALUES (N'幼年期の終り',N'アーサー C クラーク');
INSERT INTO Books (Title, Author) VALUES (N'われはロボット',N'アイザック・アシモフ');
```

NuGet パッケージを追加しておく。

```posh:パッケージマネージャーコンソール
Install-Package Microsoft.Data.SqlClient
```

Visual Studio から Web App に発行する。

`api/books` にアクセスしてデータが取得できることを確認。

```json
[
	{
		"bookId": 1,
		"title": "たったひとつの冴えたやりかた",
		"author": "ジェイムズ・ティプトリー・ジュニア"
	},
	{
		"bookId": 2,
		"title": "アンドロイドは電気羊の夢を見るか?",
		"author": "フィリップ・K・ディック"
	},
	{
		"bookId": 3,
		"title": "夏への扉",
		"author": "ロバート・A. ハインライン"
	},
	{
		"bookId": 4,
		"title": "幼年期の終り",
		"author": "アーサー C クラーク"
	},
	{
		"bookId": 5,
		"title": "われはロボット",
		"author": "アイザック・アシモフ"
	}
]
```

# VNet 統合

```posh:PowerShell
# 仮想ネットワーク作成
az network vnet create --name virtualnetwork --resource-group ManagedIdentityTestRG

# サブネット作成
az network vnet subnet create --address-prefixes 10.0.0.0/24 --name subnet1 --resource-group ManagedIdentityTestRG --vnet-name virtualnetwork

# サービスエンドポイント追加
az network vnet subnet update --name subnet1 --resource-group ManagedIdentityTestRG --vnet-name virtualnetwork --service-endpoints Microsoft.Sql

# 仮想ネットワークルール追加
az sql server vnet-rule create --server sql-server-$random --name vnetrule --resource-group ManagedIdentityTestRG --subnet subnet1 --vnet-name virtualnetwork

# App Service Plan スケールアップ
az appservice plan update --name ManagedIdentityTestAppServicePlan --resource-group ManagedIdentityTestRG --sku S1

# VNet 統合
az webapp vnet-integration add --name web-app-$random --resource-group ManagedIdentityTestRG --subnet subnet1 --vnet virtualnetwork

# Azure サービスからのアクセスを拒否
az sql server firewall-rule delete --name allowAzureService --resource-group ManagedIdentityTestRG --server sql-server-$random
```