tech.guitarrapc.cóm

Technical updates

AWS Secret Manager を使ってASP.NET Core のシークレット情報を扱う

.NET Core で AWS において機微情報を扱うときに、AWS Secret Manager や System Manager の Parameter Store が候補に上がります。

ここでは、Secret Manager を使った ASP.NET Core での組み込みについて書いておきます。

目次

TL;DR

コード、並びにappsettings.json などgit commit から機微情報を排除し、CIで差し込むのではなく、実行環境に応じて安全に取得するように組むことでより安全なアプリケーションからのアクセスを実現できます。 AWS Secretを使うことで、許可された環境からでないと守る必要があるデータへのアクセスができないように制御します。

コードのサンプルは、GitHub にあげておきます。

github.com

AWS Secret の選択

AWS で機微情報を扱う方法としては、AWS System Manager の Parameter Store と、AWS Secret Manager の選択があります。

AWS Secret Managerは 料金以外の心配が少なく、スケーラブルな環境で一斉にデプロイしても問題が起こりにくいメリットがあります。 一方で、シークレット一件当たりの料金が$0.4/monthであるため、ユーザー一人一人の情報を扱う/毎度一意なテンポラリの情報を扱うといったシークレットの件数の増加で恐ろしいほどコストがかかります。

AWS Secret はシークレットの件数が増えると料金が跳ね上がる

aws.amazon.com

Secret Manager ではアプリごとに単一になる情報を取り扱うほうがコスト的にはいいでしょう。

アプリケーションの設定などはコストが抑えられて使いやすい

System Manager の Parameter Store は、同時アクセスの規定が明確でなく Rate Limit に到達する可能性はありますが、無料でできるためアクセス頻度の少ないパラメーターを保持するのにはとても優秀です。

今回は、アプリケーションの起動時に1回読み込まれるRDSの接続情報をAWS Secret Manager に保持させて読み込んでみましょう。

AWS SecretManager の用意

AWS Secret Manager を使うため、Secret Manager とそこにいれるデータを用意しておきます。 SecretManager の名前を test として、JSON で取得することを想定します。

{
  "ConnectionStrings": {
    "DATABASE": "YOUR_AWESOME_CONNECTION_STRINGS"
  }
}

AWS Secret Manager にデータをいれる

AWS Secret Manager とデータをterraform やAWS Console でも aws cli でサクッと作ります。

gist.github.com

自動更新や自動Expire も可能ですが、DBの接続先としてはあまりないので今回は静的に組み立てておきます。

これでtest が登録されます。

Secret Store での登録例

呼び出し元がSecret Managerにアクセスできるようにする

作ったAWS SecretManager を呼び出すときの権限を委譲するため、IAM RoleにこのAWS SecretManager の 読み取り権限をつけておきましょう。

ここでは、AWS が提供している "arn:aws:iam::aws:policy/SecretsManagerReadWrite" で代用します。

gist.github.com

あとはIAM Role につければAWS 側の環境は準備は完了です。(アプリケーションはSecret ARN を知る必要がありません。)

.NET Core で AWS Secret Manager の呼び出しを行う

AWS SecretManager をコンフィグの置き場としてみなすため、nuget パッケージで公開されているASSDK.SecretsManager を用います。 .NET Coreでも同じSDKでokです。

NuGet Gallery | AWSSDK.SecretsManager 3.3.100.13

生で使うときの各種言語のコードは、Secret Manager にシークレットを作成したときに下に出ています。

gist.github.com

適切なIAM Roleがある状態で実行するとSecretManager に格納した情報が secret に格納されたことがわかります。

しかし、このコードはIAM Role前提で認証が渡されることを想定されているため、ローカルでプロファイルを使って実行しようとするとうまく動きません。 ローカルでプロファイルを使って動かすようにしてみましょう。

gist.github.com

違いは単純です。 実行時に Profile から認証を取得するように CredentialProfileStoreChain を使って、AmazonSecretsManagerClient にこの認証を渡しているだけです。 なお、Profile を使って認証する場合は AWSSDK.SecurityToken nuget パッケージを追加してください、このパッケージがないと認証トークンのハンドルができません。

これでAWS Secret Manager を .NET Core から取得する方法は把握できました。

ASP.NET Core や Generic Host で AWS Secret を取り扱う

動かすだけなら動きましたが、このままのコードでは生すぎて使いにくさがあります。 実際にアプリに組み込む場合は、ASP.NET Core や Generic Host へ追加することになるので、HostBuilder からのチェーンでIConfiguration に突っ込みたいところです。

ASP.NET Core なら WebHostBuilder からのチェーンだとうれしいです。

WebHost.CreateDefaultBuilder(args)
    .AddAwsSecrets()
    .UseStartup<Startup>();

Generic Host なら HostBuilder からのチェーンでしょう。 わかりやすい例としてのコード例ならMicroBatchFramework で次のように.AddAwsSecrets() が出来れば嬉しいと感じます。

BatchHost.CreateDefaultBuilder()
    .AddAwsSecrets()
    .RunBatchEngineAsync<CredentialSample>(args);

ではこのようなコードを書けるように組んでみましょう。

ASP.NET Core で Secret Store から値を取得する

ASP.NET Core でサンプルプロジェクトを開始します。 わかりやすいようにView に取得結果を表示するので、MVCで行きましょう。

初期状態は次のように Program.cs が書かれています。

gist.github.com

ここに AWS Secret をConfigとして読み込むのですが、自分で書かずともKralizek.Extensions.Configuration.AWSSecretsManager パッケージがある程度いい感じになっているのでこれを使います。

www.nuget.org

これで、ConfigureAppConfigurationを使って次のようにシークレットを呼び出せるようになります。

gist.github.com

もしプロファイルを使いたければ、先程の例のように Profile を AWSCredential に使えばいいでしょう。 もちろんその場合は、AWSSDK.SecurityToken パッケージを追加します。

gist.github.com

既存のIndex ページに仮表示しましょう。 新規にIndexViewModel を用意して、既存のView となるIndex.cshtml に埋め込み、IndexController から IConfiguration経由でSecretManager から取得したデータをViewModel に埋めます。

gist.github.com

SecretManager の値はSecretStoreの名前:JSONキー で指定する必要があるので、Controller でIConfigurationから GetValue するときに注意がいります。

これでデバッグ実行すると、意図したとおりに取得して表示されたことがわかります。

SecretManager からデータを取得した結果

使いやすく修正する

さて、一見良いようですが実際に利用するときにはあまり使い勝手がありません。

  • このままだとAWS SecretManager に登録してあるすべての値を読んでしまいます
  • SecretManager のキー名をアプリが知る必要がある

そこで、コンフィグに指定した特定キーのSecret Storeのみ読み込むことと、Secret Storeの名前をConfigのキーで指定せずに済むように修正をいれます。

appsettings.json や appsettings.Development.json でシークレット名を指定できるようにマッピングクラスを用意し、これに対応したappsettings.json のセクションを作ります。*1

gist.github.com

あとは、このフィルタを効かせつつSecretManagerを読み込むようにAddSecretsManagerに軽くラップをかけた AwsSecretsConfigurationBuilderExtensions を用意します。

gist.github.com

これで、Program.cs では次のようにかけるようになりました。

gist.github.com

ローカル開発でProfileを使いたい場合は、次のようにかけます。

gist.github.com

HomeController でも、Secret Store の名前を知ることなく、JSONキーでほしいデータが取れるようになっています。

gist.github.com

実行してみると思ったとおりのデータが取れました、バッチリですね。

修正後、指定したキーのSecretStoreが取得できている

Secret Store から必要なシークレットのみ取得するフィルタ

実装を見てみましょう。

public static IWebHostBuilder AddAwsSecrets(this IWebHostBuilder hostBuilder, string prefix, string region, string profile) のシグネチャはプロファイル経由での読み込みようなので無視してok です。

実際にアプリから利用するのは、public static IWebHostBuilder AddAwsSecrets(this IWebHostBuilder hostBuilder, string region) シグネチャです。 AWS Secret Manager はregion 依存なので、適当にリージョンを合わせてください。 引数やAWS_REGION などの環境変数から取得するようにするのもいいでしょう。

今回、appsettings.json で必要なキーを指定しているので、Secret Manager に問い合わせる前にフィルタしている方をマッピングしています。

            // build partially
            var partialConfig = configurationBuilder.Build();
            var settings = new AwsSecretsManagerSettings();
            partialConfig.GetSection(nameof(AwsSecretsManagerSettings)).Bind(settings);

            // Filter which secret to load
            var allowedPrefixes = settings.SecretGroups
                .Select(x => $"{prefix}{x}")
                .ToArray();

あとは、Func である SecretFilter で対象のSecret Store があるか検査して読み込むだけです。

            configurationBuilder.AddSecretsManager(region: endpoint, credentials: credentials, configurator: opts =>
            {
                opts.SecretFilter = entry => HasPrefix(allowedPrefixes, entry);
                opts.KeyGenerator = (entry, key) => GenerateKey(allowedPrefixes, key);
            });

// 省略

        private static bool HasPrefix(IEnumerable<string> allowedPrefixes, SecretListEntry entry)
            => allowedPrefixes.Any(prefix => entry.Name.StartsWith(prefix));

SecretStore名を除く

これは単純ですね。

            configurationBuilder.AddSecretsManager(region: endpoint, credentials: credentials, configurator: opts =>
            {
                opts.SecretFilter = entry => HasPrefix(allowedPrefixes, entry);
                opts.KeyGenerator = (entry, key) => GenerateKey(allowedPrefixes, key);
            });

// 省略

        private static string GenerateKey(IEnumerable<string> prefixes, string secretValue)
        {
            // don't use '/' in your environment or secretgroup name.
            var prefix = prefixes.First(secretValue.StartsWith);

            // Strip the prefix
            var s = secretValue.Substring(prefix.Length + 1);
            return s;
        }

留意点

AWS Secret Store は、JSON や Environment Variables、引数のIConfiguration 処理後に読んでいるため、もし同じキーのコンフィグをAWS Secret Store から読んだ場合上書きされます。

まとめ

あくまで薄いラッパーなのでご自身の使いやすいように調整できるはずです。

例えばローカル開発向けに、「既存のConnectionStrings がもし定義されていたらSecret Store はみない」、とかも簡単ですね。

コードからシークレットを抜く、かと言って環境変数にいれるのではなく いわゆるSecret Store / KeyValt から取得するのは、やっておいて損はないのでさくっとどうぞ。

Generic Host も IWebHostBuilder が IHostBuilder になるだけでほぼ一緒です。

Ref

docs.aws.amazon.com

他言語

www.sambaiz.net

AWSSecretsManagerConfigurationExtensions のコード例

github.com

参考

Secure secrets storage for ASP.NET Core with AWS Secrets Manager (Part 1)

Secure secrets storage for ASP.NET Core with AWS Secrets Manager (Part 2)

*1:private class なのは処理の中で外に公開する必要がないからです

非ASP.NET Coreなプロジェクトで UserSecretsを使うためのVisual Studio 拡張を作りました

非ASP.NET Core、特にGeneric Host で開発するなら必要になるVS拡張を公開していたのを忘れていました。*1

今回は、GenericHost など非ASP.NET Coreなプロジェクトでも、ASP.NET Core なプロジェクトにある Manage UserSecrets (ユーザーシークレットの管理) と同様の手助けをするVisual Studio 拡張 Open UserSecrets を作った話です。

marketplace.visualstudio.com

目次

TL;DR

Open UserSecrets拡張をいれることで、UserSecrets を開くためのメニューが 非ASP.NET Core プロジェクトでも表示されます。

Open UserSecrets

appsettings.Development.json にはCD先の開発環境を設定しておいて、ローカル開発環境はUserSecrets で実行時にコンフィグ上書きするのが楽になるのでぜひどうぞ。

marketplace.visualstudio.com

身近そうな例として、MicroBatchFramework の Config を利用した例は、この Open UserSecrets を使うとローカルテストが圧倒的に楽になります。

github.com

むしろ、これなしでUserSecrets 使うのつらすぎませんか?

UserSecrets とは

ASP.NET Core を開発していて、開発環境とローカル開発でコンフィグを分けたい時に使うのが UserSecrets です。

docs.microsoft.com

UserSecrets を使うことで、appsettings.jsonappsettings.Development.json 以外に secrets.json%APPDATA%\Microsoft\UserSecrets\<user_secrets_id>\secrets.json に保持できるようになります。

ASP.NET Core なプロジェクトではUserSecrets が空気のように利用できる

ASP.NET Core ではこれを管理するためのVisual Studio のメニュー拡張が自動的に追加され、Visual Studio 2017/2019 で プロジェクト名を右クリックすると Manage UserSecrets (ユーザーシークレットの管理) がコンテキストメニューに表示されます。

ASP.NET Core なプロジェクトを右クリックすると UserSecrets を管理するためのメニューが追加されている

これを選択するだけで、自動的に secrets.json が開かれます。

secrets.json をVSで編集できるようになる

非ASP.NET Core なプロジェクトでUserSecretsを利用する難しさ

UserSecretsは何も ASP.NET Core なプロジェクト専用の機能ではありません。

.NET Framework でも .NET Coreでも Microsoft.Extensions.Configuration.UserSecrets nuget をプロジェクトに追加することで利用できるようになります。

www.nuget.org

ただしNuGetをいれても素のVisual Studio のままでは、次の方法で管理することになります。

Enable secret storage

dotnet user-secrets を使った管理

  1. Enable secret storageに従い、csproj のPropertyGroup の中に、UserSecretsId 要素でGUID を指定して保存
  2. .csproj のあるパスで dotnet user-secrets set KEY VALUE コマンドで %APPDATA%\Microsoft\UserSecrets\<user_secrets_id>\secrets.json を作成
  3. 以降もdotnet user-secrets で管理

手で管理

  1. Enable secret storageに従い、csproj のPropertyGroup の中に、UserSecretsId 要素でGUID を指定して保存
  2. %APPDATA%\Microsoft\UserSecrets\<user_secrets_id>\secrets.json を作成
  3. secrets.json を開き編集

ASP.NET Core とはずいぶんと異なるフローであることに気づきます。 ASP.NET Coreが提供する Manage UserSecrets は、上記フローを1つのコマンドで実施しVisual Studio で secrets.json も開いてくれます。

Visual Studio Code で開発しているなら、dotnet user-secrets でもいいのですが、Visual Studio でプロジェクトに関わるファイルを別のエディタをいちいち開くのは辛いでしょう。

Open UserSecrets の紹介

Open UserSecrets を作った動機はまさにこの難しさをASP.NET Core のプロジェクトと同等まで簡単にすることです。

Visual Studio 2019 (VS2017 も同様です)を Open UserSecrest を使う流れを見てみましょう。とはいってもふつうのVisual Studio 拡張と同じです。

Visual Studio > Extensions で Online から Open UserSecrets を検索してください。

Visual Studio拡張でOpen UserSecretsを検索

ダウンロードしたらVSを再起動します

拡張ダイアログがでるのでインストールします

インストール完了

では Console App で利用してみましょう。

サンプルプロジェクトをリポジトリに用意してあります。

github.com

適当な .NET Core Console プロジェクトに、Microsoft.Extensions.Configuration.UserSecrets Nuget を入れます。(これで UserSecrets が利用できるようになります)

あとはプロジェクトを右クリックして Open UserSecrets を選ぶだけです。

Open UserSecrets を選ぶ

Manage UserSecrets と同様に secrets.json が Visual Studio で開くので好きに編集してください。

自動的に secrets.json がVisual Studio で開く

まとめ

みなさんが幸せになりますように。

ASP.NET Core チームでも .NET Core チームでもいいから早く Visual Studio 拡張かなにか用意してほしいです..... (Issue なのか?

github.com

*1:公開して3カ月がたっていました。スターで気づいた

CircleCI CLI を Windows でも利用する

circleci の cliを使うとconfigやorbs などローカルでの操作がはかどります。

この CircleCI Local CLI、以前は公式ページに macOS と Linux の記述しかありませんでしたがWindows の導入方法も示されるようになりました。

circleci.com

少し見てみましょう。

目次

TL;DR

  • Windows ローカルでも配置して利用できます
  • WSL 経由で利用できます

使いやすい方法どれでもいいでしょう。 workspace 使ってると circleci cli でローカル実行できないのでしょぼん。

circleci cli

CLI は、Golang でできています。この希望にあふれる感じを裏切らず、Windows でも利用ができます。Golang えらい。

circleci-cli の言語

Windows で CLI を配置する

公式ページには Chocolatey での導入が載っています。パスも更新も choco で管理されるので便利。

choco install circleci-cli -y

公式にはないものの Scoop でも導入できます。パスも更新も scoop で管理されるので便利。

scoop install circleci-cli

バイナリを直接持ってきたいなら、GitHub Release ページにバイナリが落ちています。バイナリならcircleci update による更新で管理もいいでしょう。

github.com

circleci cli を Windows で利用する

WSL

Linux インストールができるので、当然 Ubuntu 18.04 / 20.04 をWSLでいれれば利用できます。

VS Code などで開いておいて、ターミナルペインで bash から circleci でもいいですし、wsl circleci で呼び出してもいいでしょう。

VSCode で各種方法で circleci cli を起動する

.NET Core でgitなどSCM情報をCIで埋め込んでアプリケーションに表示する方法と選択

開発中、リリースのいずれにおいても「今どのバージョンなのか」という情報は重要な情報です。 とはいえ、実際に埋め込みたいのはバージョンというより「ソースコード」とくに「コミット」と連動する情報、加えて「ビルド」と紐づく情報もほしいでしょう。 どのようにすれば実現できるか、これを.NET Coreをベースに考えてみましょう。

真新しいことはなく、よく昔からあるやつを今ならどうやるかというメモです。 前回の知識があれば簡単です。

目次

TL;DR;

  • shallow-clone が不要なアプリケーションでは、GitVersioning がおすすめです。(お任せできる)
  • shallow-clone が必要なアプリケーションでは、GitInfo がおすすめです。(お任せできる)
  • git tag をベースにCIでバージョンを埋め込む方法もあります。(csproj の調整やビルドでの埋め込みが必要)
  • ビルド時にスクリプトを実行して実行時にjsonを読み込みDIすることももちろん可能です。(生成スクリプト、型セーフにあつかうためのクラス、DIが必要)
  • 独自にAssetmbly Attribute を埋めることもできます。(csprojとの調整が必要)

GitHub

この記事の内容は、GitHub で公開しています。 実コードのサンプルが見たい場合に利用してください。

github.com

なぜSCM情報を埋め込みたいのか

CI/CD を組むと、継続的にアプリケーションが展開されるようになります。 継続的にアプリケーションが展開されるとき、SCM / CI / CD / Application の4か所を通りますが、普段意識するのは末端となるSCMとApplicationの2か所でしょう。

結果、「Git commit をしたらアプリケーションが展開される。動いているアプリケーションをみたらGit Commit情報が知りたい。」となるでしょう。 これにより、今動いているアプリケーションが、どのコミットによるものなのか/PRによるものなのかを、「Git、Build、Deployの順にたどることなく、アプリケーションをみるだけでGitにとぶことが可能」になります。

途中のCI情報、CD情報も知りたいかもしれませんがここでは主題ではないので省略します。*1

.NET Framework におけるSCM情報の埋め込み方法

Golangを含め、各種言語にはその言語でやりやすいやり方があります。 .NET Core の場合を考える前に、過去、.NET Framework ではどうやっていたのかを振り返って、考え方の遷移をみてみます。

.NET Framework においては、 Properties/AssemblyInfo.cs が常に存在したためここへの直接的なアプローチが多く使われていました。

しかしこのようにAssemblyInfo.cs を手で触るのは避けたいでしょう。*2

gozuk16.hatenablog.com

そこで、Visual Studio ワークフローとも相性がいい .csproj に MSBuild task でフックする方法がよく利用されていたように思います。*3

nowfromhome.com

また、柔軟に埋め込み内容を調整できるためPreBuild でスクリプトを回して AssemblyInfo.cs に書き込む方法もよく使われていました。*4

blog.shibayan.jp

埋め込みはAssemblyInfo.cs へのテンプレーティングにすぎないので、T4という手もありますがあまり使われてないように思います。*5

qiita.com

もちろん、適当は public static なプロパティを用意しておいて sed をかませるのでもいいでしょう。

いずれにしても、「各種方法で特定の値をAssemblyInfo.cs に埋め込み、ビルドを通してAssembly の InformationalVersion に埋め込む、アプリの実行時はアセンブリ自身から情報を拾っていた」というのが大筋の流れです。

これらは .NET Core でも筋は同じですが、微妙に事情が変わります。

.NET Core でSCM情報をどこに仕込むと楽なのか

.NET Core で大きく変わったことは2つあります。

1つは.csproj が SDKベースのフォーマットになったことです。 前回の記事で見た通りProperties/AssemblyInfo.cs がビルド時に自動生成されるようになったため、バージョンを埋め込む時に今までとは違った考慮が必要になりました。 何も考えずに.NET Frameworkと同様にAssetblyInfo.cs を生成しようとすると属性の重複やあと勝ちにより意図しない挙動が生じます。

tech.guitarrapc.com

もう1つの転換が、WebHost/GenericHost と Configuration Provider と DI のフレームワーク化です。 .NET Coreでは、.NET Frameworkでつかわれていた app.config の仕組みから、xml/json/引数/環境変数 など各種読込先を個別に読み込む仕組みに変わりました。 また、WebHost/GenericHost により任意のファイルからタイプセーフにクラスにマッピングし、DIで各種処理で差し込むことも容易にできるようになっています。

以上の2点から、.NET Core でバージョンを仕込むには2つの方法がよく利用されています。

  • AssemblyInfo に差し込まれる仕組みをフックする
  • jsonなどを生成して、DIを経由してランタイムで読み込む

これを前提情報に、バージョンの埋め込みを見ていきましょう。

GitVersioningを使ったGit情報埋め込み

Git情報を埋め込んだり、アプリのバージョンを自動でやってほしいときは、Nerdbank.GitVersioning が使いやすいでしょう。

項目 情報
GitHub AArnott/Nerdbank.GitVersioning
アセンブリバージョンの生成方法 自動(Git Height) + CLI引数
SCM 情報の取得 可能
バージョンフォーマットの指定 version.json
shallow clone での利用 ×

このライブラリは、.NET 以外にも Node でも使え、VSIXでも埋め込みに利用できます。

このライブラリはとても使いやすいのですが、2つ注意がいります。 git height を利用しているため、実行時に全コミット履歴を辿ります。そのため、shallow clone と共存が不可能です。shallow clone を使っているプロジェクトでは利用できません。

github.com

バージョンのハンドルは、csproj のVersionではなく version.json による定義からの自動生成に任せましょう。

Nerdbank.GitVersioning/versionJson.md at master · AArnott/Nerdbank.GitVersioning

利用方法が微妙にわかりにくいため、使うにあたっての注意と.NET Core での利用方法をサンプルプロジェクトを使って説明します。

github.com

NuGet パッケージを使ってビルド時に自動的にバージョンを埋め込む

.NET Core で使う場合、nbgvという .NET Core Global Tool とNuGet Package の2つの方法があります。

ただ「毎ビルドで自動的にバージョンを埋め込みたいだけ」ならNuGet Packageで十分です。 CLIはもう少し複雑な操作を自動化するのに使います。

dotnet add package Nerdbank.GitVersioning

www.nuget.org

あとは一度ビルドすると、アプリケーションからThisAssembly というstatic class 経由でアセンブリに埋め込まれたGit Version情報を実行時に参照可能になります。 確認しましょう。

gist.github.com

実行結果です。

AssemblyConfiguration: Debug
AssemblyFileVersion: 0.0.10.14829
AssemblyInformationalVersion: 0.0.10+ed39ef6655
AssemblyName: NerdGitVersioningConsole
AssemblyTitle: NerdGitVersioningConsole
AssemblyVersion: 0.0.0.0
GitCommitId: ed39ef6655ebc044d8925f8c62aa09a4ceb0ea8c
RootNamespace: NerdGitVersioningConsole

バージョン書式は Version.json で調整できるので、リファレンスみつつ適当にやるかお任せするといいでしょう。 あんまり頑張ろうとするとつらくなります。

ほかにもいくつかのSaaS 型CIのビルド情報からバージョンを埋める機能もありますが、CircleCi はありません。

Nerdbank.GitVersioning/cloudbuild.md at master · AArnott/Nerdbank.GitVersioning

nbgv CLI を使ってバージョンを埋め込む

CLIを使うと、プロジェクトにNuGet Packageを適用することと、リリース用ブランチを切ってコミットさせることが簡単に自動化できます。 CIでGitVersioningを動的に導入してバージョンをはかせるときはCLIが使いやすいでしょう。 もしプロジェクトにNuGetパッケージを追加してコミットしてよく、リリースブランチ戦略も取ってないならCLIは不要です。

CLI は .NET Global Tool なので、dotnet sdk が入っていればコマンド1つで CLIを利用可能になります。

dotnet tool install -g nbgv

www.nuget.org

CLIを使って、GitVersioning をプロジェクトに導入するには install コマンドを利用します。

nbgv install

Directory.Build.props があるとき、ここにパッケージをadd として追加するので影響範囲が広がるため注意してください。 個別のプロジェクトフォルダでCLIを使ってinstallして影響範囲を狭めるといいでしょう。

あとは普通に dotnet build をすると、assemblyinfo.cs の生成をフックして、ビルドされたアセンブリにバージョンを埋め込んでくれます。 バージョンのフォーマットは、version.jsonの定義に従うので必要に応じてビルド前に生成しましょう。

CLIを使うと、リリースブランチ戦略が簡単に自動化できます。 よくある、reease/v1.x.xv1.0.0 のようなブランチを切ってリリースしていく場合、prepare-release を使うことで自動的にブランチを作りコミットしてくれます。

nbgv prepare-release

バージョン自動生成の裏側

裏側を説明します。NuGet Package を追加すると、Nerdbank.GitVersioning.Tasks というmsbuild task が追加されます。 このタスクによって、AssemblyInfo.cs ではなく ASSEMBLYNAME.AssemblyInfo.csASSEMBLYNAME.Version.cs をビルド前にobjフォルダに生成するようになります。

この中で重要なのが、ASSEMBLYNAME.Version.csです。中に ThisAssembly という静的クラスが書かれていることがわかります。

gist.github.com

GitInfoを使ったGit情報埋め込み

ただGit情報を埋め込みたいだけの場合、GitInfo はNerdbank.GitVersioningよりもシンプルにやりたいことをやってくれます。

項目 情報
GitHub kzu/GitInfo: Git and SemVer Info from MSBuild, C# and VB
アセンブリバージョンの生成方法 Assetmbly attribute で自分で指定可能
SCM 情報の取得 可能
バージョンフォーマットの指定 GitInfo.txt
shallow clone での利用

Gitの情報を拾ってきて埋めるだけなので、シンプルにできているのが最大のメリットです。

シンプルに利用する

ただ Git情報を参照するだけなら NuGet Package を導入するだけでできます。

dotnet add package Nerdbank.GitInfo

サンプルプロジェクトで見てみましょう。

github.com

NuGet パッケージの導入後、一度ビルドすると ThisAssembly経由でGitバージョン情報にアクセスできます。

gist.github.com

出力結果です。

Branch: master
BaseTag:
Commit: ed39ef6
Commits: 10
IsDirty: True
Sha: ed39ef6655ebc044d8925f8c62aa09a4ceb0ea8c
Tag:
Major: 0
Minor: 0
Patch: 0
DashLabel:
Label:
Major: 0
Minor: 0
Patch: 10
Source: Default

特徴的なのが、リポジトリのコミット総数を埋め込んでおり SemVer の Patchバージョンにこれを埋め込みます。 また、ファイルバージョンは何もしません。

バージョンの埋め込みも行う

.NET Core で AssemblyInfo.cs はビルド時に自動生成されるようになりました。 しかしこの自動生成自体を止めたり、特定の属性の出力を止めることはできることは前回の記事でみました。

tech.guitarrapc.com

これを利用して、GitInfo のThisAssembly を使ってアセンブリバージョンを埋め込んでみましょう。 AssemblyVersion、AssemblyFileVersion、AssemblyInformationalVersion の3つのバージョンがありますが、それぞれにバージョンを指定します。

gist.github.com

csproj で重複する属性を AssetmblyInfo に自動生成しないようにすることでビルドが通るようになります。

gist.github.com

また、GitInfo はこのままでは FileVersion は 0.0.0 なので、GitInfo.txt をプロジェクトの同一階層においてバージョンを指定します。

0.0.1

実行してみると意図したとおりにバージョンが書き込まれていることがわかります。

Branch: master
BaseTag:
Commit: ed39ef6
Commits: 1
IsDirty: True
Sha: ed39ef6655ebc044d8925f8c62aa09a4ceb0ea8c
Tag:
Major: 0
Minor: 0
Patch: 1
DashLabel:
Label:
Major: 0
Minor: 0
Patch: 2
Source: File
assemblyVersion: 0.0.1.0
fileVersion: 0.0.2
productVersion: 0.0.1+ed39ef6

Source が File になっており、GitInfo が読み込まれたことがわかります。

git tag をベースに埋め込む

この方法は、アセンブリやNuGet パッケージのバージョンを指定するのに最も簡素な方法の1つです。 SCMだとちょっと埋め込み時に工夫がいるので素朴すぎ感があります。

実際に使っているリポジトリを見てみましょう。

github.com

csproj に Versionプロパティ要素を用意しておくことで、dotnet build でビルドするときに値を差し込むことができます。

gist.github.com

あとは、ビルド時にプロパティを指定しましょう。

dotnet build -c Release -p:Version=${CIRCLE_TAG}

このようにすることで、Version や Git SCM の情報を任意のプロパティに埋めることができます。 CircleCI の場合は、CIRCLE_SHA1 環境変数でSHA1 を取り出せます。

Using Environment Variables - CircleCI

スクリプトでjsonを生成してDIする

csproj に頼らずバージョンを指定したいときには、.NET Core がjson など任意のファイルをコンフィグとして読み込み、DIで指せることが利用できます。 流れは単純です。

  • scm情報をCIでjsonに吐き出し
  • ビルド時にjsonを一緒に配置
  • アプリ実行時にDI
  • DI経由で呼び出し

scm情報をCIでjsonに吐き出し

プロジェクトに次のSet-GitAppVersion.ps1スクリプトをContentRoot直下に突っ込みます。 これは、CI でスクリプトを実行してSCMの情報をもったjsonを作るコマンドを並べただけです。 仮にPowerShell で書きましたがbashでもcsx でも pythonでもjson 作ればなんでもokです。

gist.github.com

これをビルド時に実行すればversion,json がプロジェクト直下に配置されます。

ビルド時にjsonを一緒に配置

csproj をいじって、version.jsonあったときは、ビルド時にコピーするようにします。

gist.github.com

普段は version.json はなくていいので、.gitignoreにしておくといいでしょう。

version.json

アプリ実行時にクラスにバインドする

型セーフに扱うため、マッピングするAppVersionクラスと、バインドを任せるAppVersionServiceを作ります。

gist.github.com

あとは、ConfigureServicesメソッドのラムダ内や Startup.cs でDIに登録することで View や各箇所で呼び出すことができます。

gist.github.com

DI経由で呼び出し

View に Razor で埋め込む場合は、@InjectでDIからとってくることができます。

gist.github.com

あとは、meta の html5で紹介された data-* を使ったり

    <meta name="application-name" content="Nextscape.Holojector.AssetGenerator.Web" data-version="@shortHash" data-deployment="@lastUpdate" />

json ブロックを吐き出してもいいでしょう。

    <script id="version" type="application/json">
        {
        "ShortHash": "@shortHash",
        "LastUpdate": "@lastUpdate"
        }

手間がかかるので幾分素朴すぎ感があります。 別にこんなことをしたくないという。

*1:この記事の内容を応用してすぐに組めるので

*2:手はやりたくない筆頭

*3:いわゆるド定番

*4:スクリプト使いたくないですね

*5:このためにT4したくないのは当然だと思います

.NET Core で AssemblyInfo.cs の生成を制御する

C# のアセンブリ情報は AssemblyInfo.cs によって制御されています。 .NET Core でいくぶん取り扱いが変わったものの基本は一緒です。

たびたび忘れるので、どのように取り扱いが変わったのか制御方法をメモしておきます。

目次

TL;DR

  • .NET Core で AssemblyInfo.cs はビルド時に自動生成されるようになりました
  • csprojにGenerateAssemblyInfoを指定することで自動生成自体を止めたり、GenerateAssemblyXxxxxAttributeを指定することで特定属性の出力を止めることができます
  • 出力を上書きたいときは、上書きたい属性だけ制御するようにして予期せぬ副作用は回避しましょう

Microsoft.NET.GenerateAssemblyInfo.targets

AssemblyInfo は、.NET Framework まではProperties/AssemblyInfo.cs として存在していましたが、.NET Core SDK ベースのcsproj ではビルド時に自動生成されるようになりました。

この自動生成を制御しているMSBuildのタスク Microsoft.NET.GenerateAssemblyInfo.targets を見つつどのように行うか見ていきましょう。

sdk/Microsoft.NET.GenerateAssemblyInfo.targets at master · dotnet/sdk

AssemblyInfo の自動生成をなぜ制御するのか

自前でAssemblyInfoを生成しようとしたときに、属性が重複してビルドできなくなるためです。

例えば、.NET Core の適当なプロジェクトを作って次のようにアセンブリのバージョンを任意の値で制御しようとするとエラーが起こります。

gist.github.com

エラーは次の通りです。

Error CS0579 Duplicate 'System.Reflection.AssemblyVersionAttribute' attribute

このような時に、AssemblyInfo の出力を制御したくなります。

AssemblyInfoの自動生成を止める

dotnet core のプロジェクトでビルドをすると、obj フォルダの中にASSEMBLYNAME.AssemblyInfo.cs が自動生成されます。 ASSEMBLYNAME は、デフォルトではプロジェクト名ですがご存知の通り指定もできます。(ここでは省きます)

先ほどの Microsoft.NET.GenerateAssemblyInfo.targets から、次の属性で制御されていることがわかります。

<PropertyGroup Condition="'$(GenerateAssemblyInfo)' == 'true'">

AssemblyInfo の自動生成を止めたいプロジェクトのcsproj に次の要素を書くことで、objに生成されなくなります。

<PropertyGroup>
  <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
</PropertyGroup>

例えば先ほどのConsole アプリのcsproj を編集してみましょう。(上:編集前 Before.csproj / 下:編集後 After.csproj)

gist.github.com

これでAssebly属性の重複によるビルドエラーが解消して、ビルドができるようになりました。 先ほどまであったConsoleApp2.AssemblyInfo.cs が obj フォルダに生成されなくなったことが確認できます。

obj の中を見るとConsoleApp2.AssemblyInfo.cs がないことが分かる

ただし、AssemblyInfo の自動生成を止めると副作用が大きいため推奨しません。

特に、ASP.NET Core や .NET Core で UserSecrets を使っている場合は、UserSecretsが機能しなくなります。 これは、UserSecrets が実行時にsecret のパスを参照するときにSecret Id はAssemblyInfo にビルド時に埋め込むようにしており、自動生成を止めるとUserSecrets の Id も参照できなくなるためです。

AssemblyInfo の特定の属性の生成を止める

先ほどのようにただバージョンを指定したものにするなら、AssemblyInfoは生成するけど、特定の属性をとめるほうが副作用がありません。 これは先ほどの Microsoft.NET.GenerateAssemblyInfo.targets から、次の属性で制御されていることがわかります。

gist.github.com

例えば、今回のようなAssemblyVersion 属性の重複なら、次の要素をcsprojに追加しましょう。

<PropertyGroup>
  <GenerateAssemblyVersionAttribute>false</GenerateAssemblyVersionAttribute>
</PropertyGroup>

これでAssemblyInfo の生成自体をとめることなくビルドできるようになります。 obj に自動生成された ConsoleApp2.AssemblyInfo.cs を見てみると、意図したとおり属性で指定したバージョンが埋め込まれていることがわかります。

gist.github.com