つばろぐ

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

Project Tye 0.5.0-alpha.20555.1 で .NET 5 環境で実行できるようになりました

.NET 5 が GA しましたね。どんどんインストールしていきましょう。

dotnet.microsoft.com

さて Project Tye はこれまで .NET 5 では動きませんでした。
バージョンでいうと 0.4.0-alpha.20371.1 ですね。

$ tye --version
0.4.0-alpha.20371.1+d7623120d66b85bfeda8ab69eb5ff792df6b7243

.NET 5 の GA のタイミングと合わせたかどうかはわかりませんが、11月5日にバージョン 0.5.0-alpha.20555.1 がリリースされて .NET 5 でも動作するようになりました。

www.nuget.org

ただし公式には特にアナウンスが出ていないため、何かしら不具合があるかもしれません。そういうときはフィードバックしましょう。

github.com

.NET 5 + 0.4.0-alpha.20371.1

まず動作しなかったときのログを貼っときます。

$ tye run -v Debug
Loading Application Details...
Locating .NET SDK...
Found .NET SDK at: C:\Program Files\dotnet\sdk\5.0.100\
Registered .NET SDK.Loading project 'D:\src\yuta\netcore\project-tye\project-tye-sample\backend\backend.csproj'.
Drats! 'run' failed:
        Failed to load project: 'D:\src\yuta\netcore\project-tye\project-tye-sample\backend\backend.csproj'.

Microsoft.Build.Exceptions.InvalidProjectFileException: SDK 競合回避モジュールの型 "WorkloadSdkResolver" を読み込めま
せんでした。Could not load file or assembly 'System.Runtime, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'. 指定されたファイルが見つかりません。  D:\src\yuta\netcore\project-tye\project-tye-sample\backend\backend.csproj
 ---> System.IO.FileNotFoundException: Could not load file or assembly 'System.Runtime, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'. 指定されたファイルが見つかりません。
File name: 'System.Runtime, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'
   at Microsoft.NET.Sdk.WorkloadMSBuildSdkResolver.WorkloadSdkResolver..ctor()


   --- End of inner exception stack trace ---
   at Microsoft.Build.Shared.ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(Boolean condition, String errorSubCategoryResourceName, BuildEventFileInfo projectFile, Exception innerException, String resourceName, Object[] args)
   at Microsoft.Build.Shared.ProjectFileErrorUtilities.ThrowInvalidProjectFile(BuildEventFileInfo projectFile, Exception innerException, String resourceName, Object[] args)
   at Microsoft.Build.BackEnd.SdkResolution.SdkResolverLoader.LoadResolvers(String resolverPath, LoggingContext loggingContext, ElementLocation location, List`1 resolvers)
   at Microsoft.Build.BackEnd.SdkResolution.SdkResolverLoader.LoadResolvers(LoggingContext loggingContext, ElementLocation location)
   at Microsoft.Build.BackEnd.SdkResolution.SdkResolverService.Initialize(LoggingContext loggingContext, ElementLocation location)
   at Microsoft.Build.BackEnd.SdkResolution.SdkResolverService.ResolveSdk(Int32 submissionId, SdkReference sdk, LoggingContext loggingContext, ElementLocation sdkReferenceLocation, String solutionPath, String projectPath, Boolean interactive, Boolean isRunningInVisualStudio)
   at Microsoft.Build.BackEnd.SdkResolution.CachingSdkResolverService.<>n__0(Int32 submissionId, SdkReference sdk, LoggingContext loggingContext, ElementLocation sdkReferenceLocation, String solutionPath, String projectPath, Boolean interactive, Boolean isRunningInVisualStudio)
   at Microsoft.Build.BackEnd.SdkResolution.CachingSdkResolverService.<>c__DisplayClass3_0.<ResolveSdk>b__1()
   at System.Lazy`1.ViaFactory(LazyThreadSafetyMode mode)
   at System.Lazy`1.ExecutionAndPublication(LazyHelper executionAndPublication, Boolean useDefaultConstructor)
   at System.Lazy`1.CreateValue()
   at System.Lazy`1.get_Value()
   at Microsoft.Build.BackEnd.SdkResolution.CachingSdkResolverService.ResolveSdk(Int32 submissionId, SdkReference sdk, LoggingContext loggingContext, ElementLocation sdkReferenceLocation, String solutionPath, String projectPath, Boolean interactive, Boolean isRunningInVisualStudio)
   at Microsoft.Build.Evaluation.Evaluator`4.ExpandAndLoadImportsFromUnescapedImportExpressionConditioned(String directoryOfImportingFile, ProjectImportElement importElement, List`1& projects, SdkResult& sdkResult, Boolean throwOnFileNotExistsError)
   at Microsoft.Build.Evaluation.Evaluator`4.ExpandAndLoadImports(String directoryOfImportingFile, ProjectImportElement importElement, SdkResult& sdkResult)
   at Microsoft.Build.Evaluation.Evaluator`4.EvaluateImportElement(String directoryOfImportingFile, ProjectImportElement importElement)
   at Microsoft.Build.Evaluation.Evaluator`4.PerformDepthFirstPass(ProjectRootElement currentProjectOrImport)
   at Microsoft.Build.Evaluation.Evaluator`4.Evaluate()
   at Microsoft.Build.Evaluation.Evaluator`4.Evaluate(IEvaluatorData`4 data, ProjectRootElement root, ProjectLoadSettings loadSettings, Int32 maxNodeCount, PropertyDictionary`1 environmentProperties, ILoggingService loggingService, IItemFactory`2 itemFactory, IToolsetProvider toolsetProvider, ProjectRootElementCacheBase projectRootElementCache, BuildEventContext buildEventContext, ISdkResolverService sdkResolverService, Int32 submissionId, EvaluationContext evaluationContext, Boolean interactive)
   at Microsoft.Build.Evaluation.Project.ProjectImpl.Reevaluate(ILoggingService loggingServiceForEvaluation, ProjectLoadSettings loadSettings, EvaluationContext evaluationContext)
   at Microsoft.Build.Evaluation.Project.ProjectImpl.ReevaluateIfNecessary(ILoggingService loggingServiceForEvaluation, ProjectLoadSettings loadSettings, EvaluationContext evaluationContext)
   at Microsoft.Build.Evaluation.Project.ProjectImpl.ReevaluateIfNecessary(ILoggingService loggingServiceForEvaluation, EvaluationContext evaluationContext)
   at Microsoft.Build.Evaluation.Project.ProjectImpl.ReevaluateIfNecessary(EvaluationContext evaluationContext)
   at Microsoft.Build.Evaluation.Project.ProjectImpl.Initialize(IDictionary`2 globalProperties, String toolsVersion, String subToolsetVersion, ProjectLoadSettings loadSettings, EvaluationContext evaluationContext)
   at Microsoft.Build.Evaluation.Project..ctor(String projectFile, IDictionary`2 globalProperties, String toolsVersion, String subToolsetVersion, ProjectCollection projectCollection, ProjectLoadSettings loadSettings, EvaluationContext evaluationContext)
   at Microsoft.Build.Evaluation.Project.FromFile(String file, ProjectOptions options)
   at Microsoft.Tye.ProjectReader.EvaluateProject(OutputContext output, DotnetProjectServiceBuilder project) in /_/src/Microsoft.Tye.Core/ProjectReader.cs:line 166

Tye のアップデート

Tye は .NET Core グローバルツール であるため .NET CLI でアップデートを行います。

$ dotnet tool update -g Microsoft.Tye --version "0.5.0-alpha.20555.1"
ツール 'microsoft.tye' がバージョン '0.4.0-alpha.20371.1' からバージョン '0.5.0-alpha.20555.1' に正常に更新されました。

 $ tye --version
0.5.0-alpha.20555.1+fae47325b0c8d7dafcdec5d1248191b24b2adc23

.NET 5 で動くようになった

ツールをバージョンアップすればアプリケーションや tye.yaml を変更することなく動作しました。

$ cat tye.yaml
# tye application configuration file
# read all about it at https://github.com/dotnet/tye
#
# when you've given us a try, we'd love to know what you think:
#    https://aka.ms/AA7q20u
#
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"

$ tye run -v Debug
Loading Application Details...
Restoring and evaluating projects
Resolved metadata for service backend at D:\src\yuta\netcore\project-tye-sample\backend\obj\Debug\netcoreapp3.1\MicrosoftTye.ProjectMetadata.txt
Resolved metadata for service frontend at D:\src\yuta\netcore\project-tye-sample\frontend\obj\Debug\netcoreapp3.1\MicrosoftTye.ProjectMetadata.txt
Restore and project evaluation took: 4318.6486ms
Found application version: 1.0.0
RunCommand=D:\src\yuta\netcore\project-tye-sample\backend\bin\Debug\netcoreapp3.1\backend.exe
RunArguments=
TargetPath=D:\src\yuta\netcore\project-tye-sample\backend\bin\Debug\netcoreapp3.1\backend.dll
PublishDir=bin\Debug\netcoreapp3.1\publish\
AssemblyName=backend
IntermediateOutputPath=obj\Debug\netcoreapp3.1\
Found target framework: netcoreapp3.1
Parsed target framework name: netcoreapp
Parsed target framework version: 3.1
Found shared frameworks: Microsoft.NETCore.App, Microsoft.AspNetCore.App
IsAspNet=True
Evaluation Took: 7.4286ms
Found application version: 1.0.0
RunCommand=D:\src\yuta\netcore\project-tye-sample\frontend\bin\Debug\netcoreapp3.1\frontend.exe
RunArguments=
TargetPath=D:\src\yuta\netcore\project-tye-sample\frontend\bin\Debug\netcoreapp3.1\frontend.dll
PublishDir=bin\Debug\netcoreapp3.1\publish\
AssemblyName=frontend
IntermediateOutputPath=obj\Debug\netcoreapp3.1\
Found target framework: netcoreapp3.1
Parsed target framework name: netcoreapp
Parsed target framework version: 3.1
Found shared frameworks: Microsoft.NETCore.App, Microsoft.AspNetCore.App
IsAspNet=True
Evaluation Took: 4.5876ms
Launching Tye Host...

[09:23:22 INF] Executing application from D:\src\yuta\netcore\project-tye-sample\tye.yaml
[09:23:23 INF] Dashboard running on http://127.0.0.1:8000
[09:23:26 INF] Docker image redis already installed
[09:23:26 INF] Creating docker network tye_network_5b44c29a-7
[09:23:26 INF] Running docker command network create --driver bridge tye_network_5b44c29a-7
[09:23:27 INF] Running image redis for redis_2390830d-a
[09:23:27 INF] Running image redis for redis-cli_94877efc-3
[09:23:27 INF] Running image mcr.microsoft.com/dotnet/core/sdk:3.1 for backend-proxy_c7967b52-b
[09:23:27 INF] Running image mcr.microsoft.com/dotnet/core/sdk:3.1 for frontend-proxy_f67a9f1e-9
[09:23:27 INF] Building projects
[09:23:31 INF] Running container redis_2390830d-a with ID e1c1c6eae24b
[09:23:31 INF] Running container redis-cli_94877efc-3 with ID 5b5a0450106a
[09:23:31 INF] Running docker command network connect tye_network_5b44c29a-7 redis_2390830d-a --alias redis
[09:23:31 INF] Running docker command network connect tye_network_5b44c29a-7 redis-cli_94877efc-3 --alias redis-cli
[09:23:33 INF] Replica redis_2390830d-a is moving to a ready state
[09:23:33 INF] Collecting docker logs for redis_2390830d-a.
[09:23:33 INF] Replica redis-cli_94877efc-3 is moving to a ready state
[09:23:33 INF] Collecting docker logs for redis-cli_94877efc-3.
[09:23:34 INF] Running container backend-proxy_c7967b52-b with ID f436fa86c3e4
[09:23:34 INF] Running docker command network connect tye_network_5b44c29a-7 backend-proxy_c7967b52-b --alias backend
[09:23:34 INF] Running container frontend-proxy_f67a9f1e-9 with ID e34e0f29521d
[09:23:34 INF] Running docker command network connect tye_network_5b44c29a-7 frontend-proxy_f67a9f1e-9 --alias frontend
[09:23:35 INF] Collecting docker logs for frontend-proxy_f67a9f1e-9.
[09:23:35 INF] Collecting docker logs for backend-proxy_c7967b52-b.
[09:23:37 INF] Launching service backend_937c6cf4-7: D:\src\yuta\netcore\project-tye-sample\backend\bin\Debug\netcoreapp3.1\backend.exe
[09:23:37 INF] Launching service frontend_418c4bc0-4: D:\src\yuta\netcore\project-tye-sample\frontend\bin\Debug\netcoreapp3.1\frontend.exe
[09:23:37 INF] backend_937c6cf4-7 running on process id 7760 bound to http://localhost:59007, https://localhost:59008
[09:23:37 INF] frontend_418c4bc0-4 running on process id 36020 bound to http://localhost:59009, https://localhost:59010
[09:23:37 INF] Replica backend_937c6cf4-7 is moving to a ready state
[09:23:37 INF] Replica frontend_418c4bc0-4 is moving to a ready state
[09:23:38 INF] Selected process 7760.
[09:23:38 INF] Selected process 36020.
[09:23:38 INF] Listening for event pipe events for backend_937c6cf4-7 on process id 7760
[09:23:38 INF] Listening for event pipe events for frontend_418c4bc0-4 on process id 36020

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