つばろぐ

主に C#, .NET, Azure の備忘録です。たまに日記。

Project Tye でデプロイするアプリケーションで Azure SQL Database を使う

これまで Project Tye の記事で扱っていたサンプルコードでは、 RDB を使っておらず Redis だけがデータストアでした。
実際にアプリケーションを書く場合は RDB を使わないというケースはあまりないので、今回は SQL Server や Azure SQL Database を使ってオーケストレーションしてみます。

ASP.NET Core Razor Page のチュートリアルのアプリケーションを使って Project Tye の構成を作っていきます。

AspNetCore.Docs/aspnetcore/tutorials/razor-pages/razor-pages-start/sample/RazorPagesMovie30 at master · dotnet/AspNetCore.Docs · GitHub

なお、今回のローカル開発環境は SQL Server Express Local DB を使うため、 Windows 限定の内容になっています。
ちゃんとクロスプラットフォームな構成にしたかったので SQL Server Linux を Docker を使っていたのですが、どうにもエラーが解消しきれない状態なので一旦 Windows 向けの手順になります。
このあたりはまた別の記事で書きたいと思います。

データベースのオートマイグレーションを有効にする

Project Tye でオーケストレーションを行うにあたり、データベースのオートマイグレーションが有効になるようコードを書き換えます。
RazorPagesMovie30 では Entity Framework Core を使ったマイグレーションファイルが用意されています。
ただしこのマイグレーションファイルを適用するためにはコマンドラインdotnet ef database update を行う必要があり、 Tye でのオーケストレーションには不向きです。

そこで Program.cs を以下のように変更します。変更箇所はコメントを入れています。

using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using RazorPagesMovie.Models;
using System;

// 追加 ここから
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Data;
// 追加 ここまで

namespace RazorPagesMovie
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var host = CreateHostBuilder(args).Build();

            using (var scope = host.Services.CreateScope())
            {
                var services = scope.ServiceProvider;

                try
                {
                    // 追加 ここから
                    var context = services.GetRequiredService<RazorPagesMovieContext>();
                    context.Database.Migrate();
                    // 追加 ここまで

                    SeedData.Initialize(services);
                }
                catch (Exception ex)
                {
                    var logger = services.GetRequiredService<ILogger<Program>>();
                    logger.LogError(ex, "An error occurred seeding the DB.");
                }
            }

            host.Run();

        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    }
}

context.Database.Migrate() でアプリケーション実行時にマイグレーションが行われるようになります。

tye.yaml を作成して実行

RazorPagesMovie30 を Project Tye で実行するには、まず tye.yaml を作成します。

tye init
Created 'D:\src\yuta\netcore\project-tye\RazorPagesMovie30\tye.yaml'.
Time Elapsed: 00:00:00:07

生成された tye.yaml はこのような内容になっています。

name: razorpagesmovie
services:
- name: razorpagesmovie
  project: RazorPagesMovie.csproj

では tye を実行します。

> tye run
Loading Application Details...
Launching Tye Host...

[16:16:17 INF] Executing application from D:\src\yuta\netcore\project-tye\RazorPagesMovie30\tye.yaml
[16:16:17 INF] Dashboard running on http://127.0.0.1:8000
[16:16:17 INF] Building projects
[16:16:18 INF] Launching service razorpagesmovie_c59ea1fd-7: D:\src\yuta\netcore\project-tye\RazorPagesMovie30\bin\Debug\netcoreapp3.1\RazorPagesMovie.exe
[16:16:18 INF] razorpagesmovie_c59ea1fd-7 running on process id 35244 bound to http://localhost:56350, https://localhost:56351
[16:16:18 INF] Replica razorpagesmovie_c59ea1fd-7 is moving to a ready state
[16:16:19 INF] Selected process 35244.
[16:16:19 INF] Listening for event pipe events for razorpagesmovie_c59ea1fd-7 on process id 35244

起動しました。ただ、画面が崩れている理由はよくわかりません。
ダッシュボードから RazorPagesMovie30 にアクセスして、きちんとデータが表示されることを確認しました。
ローカルDBもマイグレーションとデータシードが実行されたことも確認できました。

f:id:tech-tsubaki:20201108170011p:plain

f:id:tech-tsubaki:20201108170024p:plain

Azure SQL Database を使用する状態で AKS にデプロイする

ではアプリケーションに変更を加えずに AKS にデプロイします。このときデータベースがローカルDBのままでは正しく動かないので、Azure SQL Databaseを使うように設定を上書きします。

あらかじめ Azure SQL Database は作成しておきます。

docs.microsoft.com

次に tye.yaml を変更します。
RazorPagesMovie30 ではデータベースの接続文字列は appsettings.json に記載されているので、同名の環境変数を tye.yaml で上書きします。

ちなみに通常 .NET Core ではネストした環境変数を指定するときには :(コロン)を使います。
なので ConnectionStrings:RazorPagesMovieContext と書くのが一般的ですが、AKSではコロンを含んだ名前を環境変数に使用することができません。
なので __ (アンダースコア2つ)を区切り文字に使用します。

Project Tye における環境変数の参照順序は以前の記事にまとめてますので、よかったらご覧ください。

tsubalog.hatenablog.com

name: razorpagesmovie
services:
- name: razorpagesmovie
  project: RazorPagesMovie.csproj
  env:
    - name: ConnectionStrings__RazorPagesMovieContext
      value: (接続文字列)

AKS へのデプロイ方法も詳しくは以前の記事をご覧ください。

tsubalog.hatenablog.com

$ tye deploy --interactive
Loading Application Details...
Verifying kubectl installation...
Verifying kubectl connection to cluster...
Enter the Container Registry (ex: 'example.azurecr.io' for Azure or 'example' for dockerhub): acrprojecttye.azurecr.io
Processing Service 'razorpagesmovie'...
    Applying container defaults...
    Compiling Services...
    Publishing Project...
    Building Docker Image...
            #2 [internal] load build definition from Dockerfile
            #2 transferring dockerfile: 159B done
            #2 DONE 0.0s

            #1 [internal] load .dockerignore
            #1 transferring context: 2B done
            #1 DONE 0.0s

            #3 [internal] load metadata for mcr.microsoft.com/dotnet/core/aspnet:3.1
            #3 DONE 0.2s

            #4 [1/3] FROM mcr.microsoft.com/dotnet/core/aspnet:3.1@sha256:4030ec40f9b5c...
            #4 DONE 0.0s

            #6 [internal] load build context
            #6 transferring context: 29.76MB 0.1s done
            #6 DONE 0.2s

            #5 [2/3] WORKDIR /app
            #5 CACHED

            #7 [3/3] COPY . /app
            #7 CACHED

            #8 exporting to image
            #8 exporting layers done
            #8 writing image sha256:77b158309f52d8bfac30696f04ace2d2195f377b3fdcd1c167f0dcdba8f17d0b done
            #8 naming to acrprojecttye.azurecr.io/razorpagesmovie:1.0.0 done
            #8 DONE 0.0s
        Created Docker Image: 'acrprojecttye.azurecr.io/razorpagesmovie:1.0.0'
    Pushing Docker Image...
        Pushed docker image: 'acrprojecttye.azurecr.io/razorpagesmovie:1.0.0'
    Validating Secrets...
    Generating Manifests...
Deploying Application Manifests...

        Verifying kubectl installation...
        Verifying kubectl connection to cluster...
        Writing output to '/tmp/tmp9DRZyP.tmp'.
        Deployed application 'razorpagesmovie'.
Time Elapsed: 00:00:13:68

$ kubectl port-forward svc/razorpagesmovie 5000:80
Forwarding from 127.0.0.1:5000 -> 80
Forwarding from [::1]:5000 -> 80

AKS にデプロイされたアプリケーションにアクセスすることができました。
このときデータベースは tye.yaml で指定した Azure SQL Database に接続しています。

f:id:tech-tsubaki:20201108170043p:plain

Tye で環境変数を設定すると appsettings.json より優先される

前回の記事で Tye で実行したサービスのなかで、環境変数にアクセスするための仕組みを紹介しました。

tsubalog.hatenablog.com

今回は Tye の仕組みのなかで環境変数を設定する方法を調べたので紹介します。

環境変数は tye.yaml に定義する

Tye を実行した際にサービスに対して環境変数を設定したい場合は tye.yaml に定義します。
リファレンスはこのあたりです。

https://github.com/dotnet/tye/blob/master/docs/reference/schema.md#environment-variables

services に定義されたサービスのなかで環境変数を設定したいサービスのブロックに env を書きます。

name: microservice
registry: tsubakimoto
services:
- name: backend
  project: backend\backend.csproj
- name: frontend
  project: frontend\frontend.csproj
  env:
  - name: YOUR_NAME
    value: "YUTA in YAML"
- name: redis
  image: redis
  bindings:
  - port: 6379
    connectionString: "${host}:${port}"
- name: redis-cli
  image: redis
  args: "redis-cli -h redis MONITOR"

試しにトップページに YOUR_NAME という環境変数の値を表示するようにしておきます。

(中略)
@inject Microsoft.Extensions.Configuration.IConfiguration configuration

(中略)
<div class="text-center">
    <h1 class="display-4">Welcome @configuration["YOUR_NAME"]</h1>
    <p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>

上記の tye.yaml の状態で tye run を実行すると設定した値がきちんと表示されます。

f:id:tech-tsubaki:20201001234600p:plain

同一の環境変数は appsettings.json と tye.yaml のどちらが優先されるか?

ASP.NET Core で環境変数を定義する場合、通常は appsettings.json もしくは appsettings.{環境名}.json に書くと思います。
appsettings.jsontye.yaml はどちらが優先されるのかを調べてみました。

結論としては appsettings.json < appsettings.{環境名}.json < tye.yaml という優先順位になりました。
Azure App Service のアプリケーション設定と同じですね。

どういうときに使う?

前述の通り tye.yaml に記述した環境変数が優先されるため、アプリケーションソースコードで定義している環境変数をデプロイから上書きしたいときが考えられます。
たとえば一時的にログレベルを変更したいときなどの場面が想定できますね。

Tye で設定された環境変数にアクセスする仕組みを調べた

これまでの Tye の記事で使っているサンプルアプリケーションは、バックエンドアプリケーションにホストした API をフロントエンドアプリケーションから呼び出して、データを画面に表示するというものです。

f:id:tech-tsubaki:20200927223507p:plain

このとき、それぞれのアプリケーションでは2つの環境変数にアクセスしています。
今回の記事では Tye の仕組みの中で、どのように環境変数を取得しているかを調べてみました。

backend > Startup.cs

バックエンドアプリケーションでは Redis の接続文字列をスタートアップで取得しています。

services.AddStackExchangeRedisCache(o =>
{
    o.Configuration = Configuration.GetConnectionString("redis");
});

ただし IConfiguration.GetConnectionStringASP.NET Core 標準の機能で取得しているため、一般的なアクセスの仕方になっています。
では "redis" という接続文字列はどこで設定されているでしょうか。
通常は appsettings.json や OS の環境変数に書きますが、 Tye の場合は tye.yaml で定義することができます。

- name: redis
  image: redis
  bindings:
  - port: 6379
    connectionString: "${host}:${port}"

ローカルで Tye を動かす tye run コマンドのときに、実際にどのような環境変数が設定されているかを調べるために、環境変数を取得するエンドポイントを Startup.cs に書いてみました。

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    // 中略
    app.Map("/configs", HandleMapConfigs);
    // 中略
}

private void HandleMapConfigs(IApplicationBuilder app)
{
    app.Run(async context =>
    {
        var connectionStrings = Configuration.AsEnumerable();
        var content = new System.Text.StringBuilder();
        foreach (var config in connectionStrings)
        {
            content.AppendLine($"{config.Key} -> {config.Value}");
        }
        await context.Response.WriteAsync(content.ToString());
    });
}

動かしてみると接続文字列としての環境変数がセットされていました。なので IConfiguration.GetConnectionString("redis") で取得できるということですね。

ConnectionStrings -> 
ConnectionStrings:REDIS -> localhost:6379

frontend > Startup.cs

次はフロントエンドアプリケーションです。ここではスタートアップでバックエンドアプリケーションの URI を取得しています。
このとき使用しているメソッドは Microsoft.Extensions.Configuration.TyeConfigurationExtensions.GetServiceUri という Tye 独自の拡張メソッドです。

services.AddHttpClient<WeatherClient>(client =>
{
    client.BaseAddress = Configuration.GetServiceUri("backend");
});

https://github.com/dotnet/tye/blob/083a97f353069ef8a091651d4c9ca445c994b2e5/src/Microsoft.Tye.Extensions.Configuration/TyeConfigurationExtensions.cs#L11

このメソッドでは IConfiguration から以下のキーの値を取得しています。

  • service:backend:host
  • service:backend:port
  • service:backend:protocol

前述の環境変数を取得する処理をバックエンドアプリケーションにも追加したうえで、値一覧を見てみると service:backend から始まる設定がありました。

SERVICE:BACKEND -> 
SERVICE:BACKEND:PROTOCOL -> http
SERVICE:BACKEND:PORT -> 64699
SERVICE:BACKEND:HTTPS -> 
SERVICE:BACKEND:HTTPS:PROTOCOL -> https
SERVICE:BACKEND:HTTPS:PORT -> 64700
SERVICE:BACKEND:HTTPS:HOST -> localhost
SERVICE:BACKEND:HOST -> localhost

そのため Configuration.GetServiceUri("backend")http://localhost:64699/ という URI を取得することができ、この URI がバックエンドアプリケーションの URI ということになります。