tech.guitarrapc.cóm

Technical updates

.NET Core Tools と DotNetCliToolReference

.NET Core 3.1 がリリースされました、LTS リリースなので netcoreapp3.0 にしていたものは悉く netcoreapp3.1 に移行ですね。

さて、C# 的には .NET Core 3.0 から変化がほぼないのですが、dotnet cli や ビルド的には.NET Core 2.2から破壊的変更が行われました。 .NET Core Local Tools です。

今回は、これまでもあった .NET Core Global Tools と、.NET Core 2.2 までの DotNetCliToolReference、.NET Core 3.1からの .NET Core Local Toolsについてです。

これまでも書こうと思ってたのですが、ちょっとながしてたやつ。

目次

TL;DR

  • ツールに対して一意のバージョンでCLIを使うなら .NET Core Global Tool でok
  • プロジェクトごとにバージョン使い分けたりプロジェクトに紐づけるなら、DotNetCliToolReference or .NET Core Local Tools を使う
  • DotNetCliToolReference は.NET Core 2.2 and below に限定されてオワコン
  • .NET Core 3.1 以上では、.NET Core Local Tools に移行必須
  • .NET Core Local Tools では dotnet tool restore を忘れないで
  • Manifest の二重管理の世界へようこそ

Summary

.NET Core 2.2 まで

  • マシンで一意に使う CLI : .NET Core Global Tools
  • プロジェクト固有に利用する CLI (csproj単位) : DotNetCliToolReference

.NET Core 3.0 以降

  • マシンで一意に使うCLI : .NET Core Global Tools
  • プロジェクト固有に利用する CLI (sln/csproj単位) : .NET Core Local Tools

.NET Core Global Tools (> .NETCore 2.1)

.NET Core で大きく変わった体験の1つが.NET Core Global Tool です。 コンセプトとして NPM global tools を意識しているだけあり、npm に相当するnuget にアップロードしたコンソールアプリケーションを dotnet CLI から取得して、パスを意識することなく利用できます。

Announcing .NET Core 2.1 Preview 1 | .NET Blog

# npm
$ npm install -g TOOL

# .NET Global Tools
$ dotnet install tool -g TOOL

これを使うことで、.NET Core製のCLIの配布が dotnet cli で行えるようになり、.NET Core SDK が入っている環境でのツール配布が一元化、簡略化され、利用者も.NET Core SDK が入っていればok になりました。 日常的な開発体験もそうですが、ちょっとしたツールの展開、CI/CDでのツール利用まで大きく簡略化されています。

何よりも、.NET Core をターゲットに作ることでマルチプラットフォームで利用できるツールをサクッと作って配布できるようになったのが .NET Core Tools の嬉しいことです。

.NET Core Global Tools以前のCLI配布

各人がGitHub Release や 直バイナリなどで配布、利用者はダウンロードしてパスを通して利用。

.NET Core Global Tools以降のCLI配布

NuGet にpackしてpush すれば配布はNuGet任せ。利用者はパスが通った箇所にツールがインストールされるのでCLIをインストール後は実行するだけ。

.NET Core Global Tools で満たされるケース

大概のケースではうまく機能するはずです。 唯一の前提は1つです。(.NET Core SDK は大前提なので含みません)

  • 通常のCLI利用として、ツールごとに一意のバージョンでよい

.NET Core Global Tools で困るケース

一方で、NPM global tools がそうであるように、.NET Core Global Toolsではツールごとに一意のバージョン参照になるという制約は当然あります。

.NET Core Global Tools を各自がインストールすればいいように見えますね。ダメなんでしょうか?

.NET Core Global Tools を使うと、複数のプロジェクトで個別のバージョンでツールを使いたいときに不自由します。 特にそういうケースは、マルチプラットフォームに csproj のビルド後イベントで何かを実行したいときに生じます。

こういうビルド後処理はシンプルなことが多くコマンド1つで行けたりするので、シェルを適当にたたきたいところですが、マルチプラットフォームが前提だと案外困ります。

  • Bash だとWindows で困る
  • BatはWindowsでない環境で困る
  • PowerShell は非Windowsでインストールされてないと困る

どれもそのプラットフォームに閉じればいいのですが、各人の開発環境やCI環境など実行個所を選ばない前提に立つと微妙に選択肢に困ります。

こういったときに.NET Core Global Tools は、.NET Core SDK が入っていればok なので .NET Core 開発環境、ビルド環境で利用するのに支障がないように見えます。 マルチプラットフォームに動作が保証されて導入も楽なのでいい感じに思えるのですが、前述の通りプロジェクトごとに個別のバージョンを利用したいときに困ります。 いちいちプロジェクトごとに、ツールのバージョンを変えるということはしたくないのです。

プロジェクト固有のCLI参照

.NET Core 2.2 までと、.NET Core 3.1 以降で方法が変わります。

DotNetCliToolReference (< .NETCore 2.2)

プロジェクトに固有で.NET CoreなCLIを利用したい、それを満たすために用意されていたのが DotNetCliToolReference です。

# Consuming per-project tools

Consuming these tools requires you to add a <DotNetCliToolReference> element to your project file for each tool you want to use. Inside the <DotNetCliToolReference> element, you reference the package in which the tool resides and specify the version you need. After running dotnet restore, the tool and its dependencies are restored.

.NET Core CLI extensibility model - .NET Core CLI | Microsoft Docs

DotNetCliToolReference を利用するには、SDK Styleの .csproj でNuGet に放流したパッケージを<DotNetCliToolReference> で参照すると (.NET Core Tools とは違う通常のNuGet パッケージ)、プロジェクトのビルド処理で利用できるものです。 dotnet restore でnuget パッケージと同様に復元されるので、.csprojに定義を追加してコミットすれば意識するなく利用できます。

PackageReference と同様に属性を定義ができるので、バージョンも固定もできるしプロジェクト固有のCLI処理だけ考えると概ね満たされます。

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp2.1</TargetFramework>
  </PropertyGroup>

  <!-- The tools reference -->
  <ItemGroup>
    <DotNetCliToolReference Include="dotnet-api-search" Version="1.0.0" />
  </ItemGroup>
</Project>

.NET Core なプロジェクトでなくとも、SDK Style の csproj であれば .NET Framework 4.6 でも利用できるので案外使いどころはあります。

DotNetCliToolReference に対応した NuGetパッケージの作成、配布

難点として、配布側が少しだけ面倒です。 DotNetCliToolReference は、.NET Core Tools ではなく NuGet パッケージであるため、配布側は .NET Core Tools とは別に NuGetパッケージを作成する必要があります。CLIの処理的には何も変わらないので、同じコードのまま .csproj を変えて参照するだけです。

csproj としては2点だけ注意が要ります。

  • PackageType を DotNetCliTool にすること
  • PackageId と AssemblyName を合わせて、dotnet- の prefix で始めるように命名する

ほぼほぼ .NET Core Global Tools と違わないので、むむっという感じでしょう。

サンプルに実際にDotNetCliToolReference として配布している NuGetパッケージ dotnet-kustomizationconfigmapgenerator の .csproj を示します。

guitarrapc/KustomizeConfigMapGenerator: Kustomize ConfigMapGenerator Generator CLI

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp2.1</TargetFramework>
    <Version>0.1.0</Version>
    <AssemblyVersion>$(Version)</AssemblyVersion>
  </PropertyGroup>

  <!-- nuget -->
  <PropertyGroup>
    <PackageType>DotNetCliTool</PackageType>
    <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
    <PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
    <RootNamespace>KustomizeConfigMapGenerator.ProjectTool</RootNamespace>
    <AssemblyName>dotnet-kustomizationconfigmapgenerator</AssemblyName>
    <PackageId>dotnet-kustomizationconfigmapgenerator-project-tool</PackageId>
    <Description>
      <![CDATA[Project-installable Kustomize configMapGenerator commandline tool.
This package can be installed into a project using `DotNetCliToolReference`.
* To install as a dotnet global or local tool, use `dotnet-kustomizeconfigmapgenerator` instead.]]>
    </Description>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="MicroBatchFramework" Version="1.6.0" />
  </ItemGroup>

  <ItemGroup>
    <Compile Include="..\KustomizeConfigMapGenerator\**\*.cs" Exclude="..\KustomizeConfigMapGenerator\obj\**\*.cs"/>
  </ItemGroup>

  <ItemGroup>
    <None Include="..\..\LICENSE.md">
      <Pack>True</Pack>
      <PackagePath></PackagePath>
    </None>
  </ItemGroup>

</Project>

DotNetCliToolReference で困るケース

利用する分には概ねありません。

プロジェクトごとの参照になるため、ソリューション単位で必要な場合には各プロジェクトで埋め込みが必要になるでしょう。

何より DotNetCliToolReference は .NET Core 2.2 までのサポートです。.NET Core 3.1 以降では、次に述べる .NET Core Lobal Tools を使う必要があります。

Limit DotNetCliToolReference Tools to .NET Core 2.2 and Below · Issue #107 · dotnet/announcements

.NET Core Tools (> .NETCore 2.1)

.NET Core Global Tools では、 dotnet tool install -g Tool-g を付けていました。-g を外して、パスを指定することで、グローバルではなく指定したパスにツールをインストールして利用できます。

# インストール
$ dotnet tool install --tool-path .tools Cake.Tool

# 利用
$ ./.tools/dotnet-cake --help

これなら、プロジェクトごとにツールを利用できそうです。

パスを指定した .NET Core Tools のインストールで困ること

何もツールとしてはサポートがないので、あると便利な機能がありません。

  • インストールパスが決まっていないので、何をインストールしたのか一覧をとるコマンドサポートがなく利用する際に困る
  • 相対パスでインストールする前提だと、ほかのプロジェクトでも同じツールを使いたい場合でも、コピーを作ることになる。この例なら ./tools
  • マルチプラットフォームで利用を想定するとツールの起動パスは ./ で開始するでしょう。すると pwsh や bash (そのほか) に利用が制限されます。cmd ではこのパスしてはエラーなのはわかっていても、使う側からするとちょっと悩ましい

どれも些細ですが、微妙に使い勝手も困ります。

もう少し、ツールをバージョンごとに指定して利用できる仕組みがないでしょうか。

.NET Core Local Tools (> .NETCore 3.1)

.NET Core 3.1 で、Global Tools のようですが所定のパスにツールをインストールし解決する仕組みが追加されました。 これを .NET Core Local Tools といいます。

https://docs.microsoft.com/en-us/dotnet/core/whats-new/dotnet-core-3-0#local-tools

使うプロジェクトやソリューションで、dotnet new tool-manifest を実行します。

dotnet new tool-manifest

すると、.config/dotnet-tools.json が生成されます。

ls .config

.NET Core Tools で配布している中から、使いたいツールを追加します。

dotnet tool install KustomizeConfigMapGenerator

ツールが追加されたことが json でわかります。

{
  "version": 1,
  "isRoot": true,
  "tools": {
    "kustomizeconfigmapgenerator": {
      "version": "0.2.1",
      "commands": [
        "dotnet-kustomizeconfigmapgenerator"
      ]
    }
  }
}

コマンドでも取得できます。

$ dotnet tool list

dotnet tool list
パッケージ ID                         バージョン      コマンド                                    マニフェスト          
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------
kustomizeconfigmapgenerator      0.2.1      dotnet-kustomizeconfigmapgenerator      C:\git\ConsoleApplication100\.config\dotnet-tools.json

ビルドイベントに仕掛けてみましょう。

dotnet tool restore
dotnet tool run dotnet-kustomizeconfigmapgenerator

実行前に dotnet tool restore でリストアを忘れないようにします。リストア時に、実行できるコマンドが表示されるので親切です。

$ dotnet tool restore

Tool 'kustomizeconfigmapgenerator' (version '0.2.1') was restored. Available commands: dotnet-kustomizeconfigmapgenerator

CLI から実行するときも、dotnet tool run dotnet-kustomizeconfigmapgenerator で実行できます。

dotnet-tools.json マニフェストの解決順は次の通りです。

  1. 現在のパスの .config フォルダー ./.config/dotnet-tools.json
  2. 現在のフォルダー./dotnet-tools.json
  3. 親フォルダー ../dotnet-tools.json
  4. ルートにたどり着くまでの親フォルダを順次

REF

What's new in .NET Core 3.0 | Microsoft Docs

.NET Core ツールの使用に関する問題のトラブルシューティング - .NET Core CLI | Microsoft Docs

Local Tools Early Preview Documentation · Issue #10288 · dotnet/cli

Limit DotNetCliToolReference Tools to .NET Core 2.2 and Below · Issue #3115 · dotnet/sdk

.NET ローカル ツールの使い方 - Qiita

pulumi up で変化するリソースの内容を確認する

この記事は、Pulumi dotnet Advent Calendar 2019 の20日目です。

qiita.com

些末なコマンドメモです。 Terraformと違って細かい差分でないのですが、Continuous Delivery していると details を忘れるアレ。

目次

TL;DR

pulumi up 後の選択肢で、details を選べばリソースの詳細が表示される。

Summary

pulumi up をした状態では、どのような変化があるかわからない。

$ pulumi up

Previewing update (dev):
     Type                 Name        Plan     Info
     pulumi:pulumi:Stack  pulumi-dev           'dotnet build -nologo .' completed successfully
������
     pulumi:pulumi:Stack                   pulumi-dev                             2 messages
���������                        Name                       Plan        Info
     └─ pkg:component:ekscluster           sandbox
        └─ pkg:component:autoscaling       sandbox-asg
 +-        ├─ aws:ec2:LaunchConfiguration  sandbox-asg-autoscale-lc   replace     [diff: ~imageId]
 ~         └─ aws:autoscaling:Group        sandbox-asg-autoscale-asg  update      [diff: ~launchConfiguration]

Diagnostics:
  pulumi:pulumi:Stack (pulumi-dev):

Resources:
    ~ 1 to update
    +-1 to replace
    2 changes. 55 unchanged

Do you want to perform this update?
  yes
> no
  details

このリソースの変化を見るには、details を選ぶといい。

Do you want to perform this update? details
  pulumi:pulumi:Stack: (same)
    [urn=urn:pulumi:dev::pulumi::pulumi:pulumi:Stack::pulumi-dev]
            ++aws:ec2/launchConfiguration:LaunchConfiguration: (create-replacement)
                [id=sandbox-plumi-cluster20191119190921211300000001]
                [urn=urn:pulumi:dev::pulumi::pkg:component:ekscluster$pkg:component:autoscaling$aws:ec2/launchConfiguration:LaunchConfiguration::sandbox-asg-autoscale-lc]
                [provider=urn:pulumi:dev::pulumi::pulumi:providers:aws::default_1_9_0_alpha_1573920297_g8292aa92::36586a29-3f5b-435f-a618-2e3ec6c62be8]
              ~ imageId: "ami-02e124a380df41614" => "ami-0b60cbd90564dfe00"
            +-aws:ec2/launchConfiguration:LaunchConfiguration: (replace)
                [id=sandbox-plumi-cluster20191119190921211300000001]
                [urn=urn:pulumi:dev::pulumi::pkg:component:ekscluster$pkg:component:autoscaling$aws:ec2/launchConfiguration:LaunchConfiguration::sandbox-asg-autoscale-lc]
                [provider=urn:pulumi:dev::pulumi::pulumi:providers:aws::default_1_9_0_alpha_1573920297_g8292aa92::36586a29-3f5b-435f-a618-2e3ec6c62be8]
              ~ imageId: "ami-02e124a380df41614" => "ami-0b60cbd90564dfe00"
            ~ aws:autoscaling/group:Group: (update)
                [id=sandbox-asg-autoscale-asg-cb81c3d]
                [urn=urn:pulumi:dev::pulumi::pkg:component:ekscluster$pkg:component:autoscaling$aws:autoscaling/group:Group::sandbox-asg-autoscale-asg]
                [provider=urn:pulumi:dev::pulumi::pulumi:providers:aws::default_1_9_0_alpha_1573920297_g8292aa92::36586a29-3f5b-435f-a618-2e3ec6c62be8]
              ~ launchConfiguration: "sandbox-plumi-cluster20191119190921211300000001" => output<string>
            --aws:ec2/launchConfiguration:LaunchConfiguration: (delete-replaced)
                [id=sandbox-plumi-cluster20191119190921211300000001]
                [urn=urn:pulumi:dev::pulumi::pkg:component:ekscluster$pkg:component:autoscaling$aws:ec2/launchConfiguration:LaunchConfiguration::sandbox-asg-autoscale-lc]
                [provider=urn:pulumi:dev::pulumi::pulumi:providers:aws::default_1_9_0_alpha_1573920297_g8292aa92::36586a29-3f5b-435f-a618-2e3ec6c62be8]

Do you want to perform this update?
  yes
> no
  details

今回の例では、LaunchConfiguration のami id が変わっているため、差し替えになる。 また、LaunchConfiguration の変化に伴って、AutoScalingGroup も更新が必要になっていることがわかる。

pulumi up 時の文字化け対策

この記事は、Pulumi dotnet Advent Calendar 2019 の19日目です。

qiita.com

そろそろどうやるか見てみましょう。 と思ったのですが、日本語OS上では解決方法が見つからないのでゆるぼ。

目次

TL;DR

現状日本語 Windows では 打つ手なさそう

Summary

pulumi up をすると、dotnet コマンド処理が文字化ける。

cmd/powershell/pwsh のいずれでも発生。

.NET Core ���� Microsoft (R) Build Engine �o�[�W���� 16.3

出力というよりこっちの文字化けをどう対応するか

NOTE: だめな方法

CodePagesEncodingProvider

よく、出力文字列に関しては Encoding の違いということで次の対応がとられる。

Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

しかしこれでは、dotnet コマンドの文字化けは解消しない。

chcp

dotnet build がdotnet exec を介しているなら、ということでdotnet 出力が utf-8 であるという前提でコンソールの文字エンコーディングを utf-8にして合わせてみる。

ようはこれ。

.NET Core task outputs garbled characters in build log on non EN-US environments. · Issue #1344 · microsoft/azure-pipelines-agent

Visual Studio Codeタスクのdotnetコマンド出力を文字化けしないようにする - Qiita

この方法で取られるのが、コンソール文字列のエンコーディングを chcp 65001 で utf-8 にしてみる。

が、ダメ。

dotnet の言語を変えるようにしてみる

MSBuild should respect upstream tool language requests · Issue #1596 · microsoft/msbuild · GitHub

set DOTNET_CLI_UI_LANGUAGE=en
set VSLANG=1033

だめ。

推察

dotnet は単純に実行するとコンソールに合わせて文字化けしない。つまり shift-jis で出力している。

が、pulumi up 時に dotnet の出力を食ってはいているので、ここが utf-8 を前提にしている可能性がある。

pulumi/up.go at master · pulumi/pulumi

dotnet > Go > Console の間のエンコーディングっぽいので、現時点では英語OS でという身もふたもない結果になりそう。

REF

.NET Core task outputs garbled characters in build log on non EN-US environments. · Issue #1344 · microsoft/azure-pipelines-agent

Pulumi のプラグインを確認する

この記事は、Pulumi dotnet Advent Calendar 2019 の18日目です。

qiita.com

Pulumi の各プロバイダの実装は Plugin として提供されています。 このプラグインを確認する方法を見てみましょう。

目次

TL;DR

  • Issue 報告などで Pulumi Version や Plugin バージョンを添えると圧倒的に手早い
  • pulumi plugin ls でpluginが確認ができる
  • .NET (C#) では NuGet で管理されている

Pluginの確認

Plugin は、aws とか azure のように、どこの操作をするかのグループ。

pulumi plugin でプラグインの操作ができる。

$ pulumi plugin ls

NAME  KIND      VERSION  SIZE    INSTALLED   LAST USED
aws   resource  1.8.0    220 MB  6 days ago  6 days ago

TOTAL plugin cache size: 220 MB

GitHub

Plugin は、pulumi-xxxx という名前で GitHub 上でオープンに開発されている。

例えば、Awsプラグインなら、pulumi/pulumi-aws

.NET 実装では NuGet で参照される

Pulumi が生成する Infra.csproj を見ると Pulumi の Nuget が入っている。

  <ItemGroup>
    <PackageReference Include="Pulumi.Aws" Version="1.10.0-preview" />
    <PackageReference Include="Pulumi" Version="1.6.0-preview" />
  </ItemGroup>

多くの場合、Pulumi nuget は Pulumi.Aws など実際に使っているプロバイダの依存バージョンで暗黙的に利用されている。 しかし、Pulumi Nuget で修正されていて明示的に取得する必要があることがある。

Pulumi.Aws.Invokes.GetCallerIdentity throws System.NullReferenceException · Issue #801 · pulumi/pulumi-aws

この場合は、プロジェクトの NuGet 管理から PulumiPulumi.Aws NuGet を更新する。

Plugin の元ネタ

基本的に、Terraform をベースに書く言語実装が出力されている。

そのため、Plugin の挙動でもしよくわからないことがあったら、Terraform のリソースでそのパラメーターが受け付けられているかなどを確認すると動作したりもシエル。

バージョン履歴を見ると、現在どのバージョンの Terraform Provider をベースにしているかがわかる。

https://github.com/pulumi/pulumi-aws/blob/a113b17652528591ccee845ea27058de57265066/CHANGELOG.md#190-2019-11-16

Pulumi で他のstate 参照を行う

この記事は、Pulumi dotnet Advent Calendar 2019 の17日目です。

qiita.com

Pulumi のTIPS を見ていきましょう。

Terraform の Remote State に相当する処理を見てみます。 Pulumiで 他のStack で管理しているリソースは参照できるのでしょうか?

目次

TL;DR

  • 現状 Stack の参照は C# 実装では提供されていない
  • Provider を分けて持つことはできるので、Provider でリソースを参照して使うことで回避できる
  • TypeScript などGAされた言語では Stack Reference として提供されている

Stack Reference

TypeScript で次のように書いて、他の Stack の Output を参照できる。

import * as pulumi from "@pulumi/pulumi";
const other = new pulumi.StackReference("acmecorp/infra/other");
const otherOutput = other.getOutput("x")

これは dotnet (C#) ではまだサポートされいない。

そのため、もしほかの Stack を参照したい場合、Stack を直接みるのではなく、そのStack の Providerを別途用意して参照するしかない。

using Pulumi;
using Pulumi.Aws;

class Program
{
   async Task Main() =>
       Deployment.Run(() =>
       {
           // Create an AWS provider for the us-east-1 region.
           var useast1 = new Provider("useast1", new ProviderArgs { Region = "us-east-1" });

           // Create an ACM certificate in us-east-1.
           var cert = new Aws.Acm.Certificate("cert", new Aws.Acm.CertifiateArgs
           {
               DomainName = "foo.com",
               ValidationMethod = "EMAIL",
           }, new ResourceArgs { Provider = useast1 });
       });
}

イメージ的には、terraform の provider による管理に近い。

1つの terraform で複数 AWS Account をまとめて構築・管理する - エムスリーテックブログ

REF

Inter-Stack Dependencies | Organizing Projects and Stacks | Pulumi