Azure の Azure Container Apps (Preview) ではカスタムドメインを設定して、外部からのアクセスを受け付けるようにできます。
詳しくはしばやんさんのブログを参照していただきたいのですが、現時点ではカスタムドメインの設定時に HTTPS 向けの証明書のアップロードが必要です。
つまりドメインに対する証明書を何らかの方法で作成/用意する必要があり、しばやんさんの記事では Acmebot を用意して Let’s Encrypt で証明書を作成、更新していく方法が紹介されています。
今回触っていた環境は個人で適当に立てているサイトなので動かすものを増やしたくないなと思っていたのですが、元々 Cloudflare を前に置いていたので Cloudflare が自動で発行、更新する証明書を使うようにすれば丸投げできそうな気がしてきました。が、しかし Container Apps は HTTPS 必須なのかカスタムドメインの登録時にどうにしても証明書が必要です。
Container Apps のカスタムドメイン設定に必要な証明書をどうするか、というところで Cloudflare にはオリジン証明書を作るという機能があるのでこれを使います。
オリジン証明書は Cloudflare とオリジン、この場合だと Container Apps の通信でのみ使用する目的の証明書で今回の用途にはぴったりです。デフォルトでは15年という長い期限で作成され、個人のサイトならそっちが先に朽ちるか Managed Certificates がくると思うのでよさそうです(今回はある程度放っておきたいのが目的)。
オリジン証明書は Cloudflare の管理画面の SSL/TLS → オリジンサーバー → オリジン証明書 で発行できます。発行すると証明書と秘密鍵がでてきますので PEM 形式で手元に保存します。特に秘密鍵はページを閉じると確認できなくなるのでしっかり保存しておきます。
次に発行した証明書と秘密鍵を Container Apps に登録します。ところが登録時にパスワードを求められたりしてそのままでは取り込めないので OpenSSL で pfx 形式 (PKCS#12) に変換します。
cat certificate.pem private.pem > origin.pemopenssl pkcs12 -export -in origin.pem -out origin.pfx
pfx 形式の証明書を作成したら Container Apps に登録します。Azure Portal の コンテナー アプリ環境 → (コンテナーアプリ) → 証明書 で “証明書の追加” で追加を行えます。
注意点として現時点では “証明書名” に大文字アルファベットを含む文字列を指定すると “この証明書名は既に使用されています。別の証明書名をお試しください。” という謎のエラーが出るので小文字で入力してください。
証明書が作成出来たら後はカスタムドメインの設定を行い、証明書として先ほど登録したものを選択すれば完了です。
Container Apps が Managed Certificates にはやく対応してほしいですね。
]]>追加されたものは次のようなあるといいよねといった API が増えている感じです。
GeneratorInitializationContext.RegisterForPostInitialization
メソッドISyntaxContextReceiver
インターフェースVisual Studio 2019 であれば 16.9 以降、NuGet パッケージであれば Microsoft.CodeAnalysis.CSharp 3.9.0 を参照すれば使用できます。
GeneratorInitializationContext.RegisterForPostInitialization
メソッドGeneratorInitializationContext.RegisterForPostInitialization
メソッドは Source Generator が読み込まれて初期化された後に呼び出されるコールバックを登録できます。
これは Source Generator で必要となる属性用のコードを追加したいパターンに役立ちます。例えば次のようなコードがサンプルにあります。
[Generator]public class AutoNotifyGenerator : ISourceGenerator{ private const string attributeText = @"using System;namespace AutoNotify{[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)][System.Diagnostics.Conditional(""AutoNotifyGenerator_DEBUG"")]sealed class AutoNotifyAttribute : Attribute{ public AutoNotifyAttribute() { } public string PropertyName { get; set; }}}"; public void Initialize(GeneratorInitializationContext context) { // Register the attribute source context.RegisterForPostInitialization((i) => i.AddSource("AutoNotifyAttribute", attributeText)); // Register a syntax receiver that will be created for each generation pass context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); }...}
今までソースで指定したい属性をどうするかという問題があった(一度生成させてその属性を使うのか、手で追加するのかとか)のがこれで解決できそうです。
ISyntaxContextReceiver
インターフェースコンパイラーがソースコードを処理する際に Source Generator を呼び出す ISyntaxReceiver
インターフェースがあります。
例えばクラス定義に対するコードジェネレートを行いたいときには自分でシンタックスツリーを走査して集めるのではなく、ツリーの走査はコンパイラーに任せて、 ClassDeclarationSyntax を集めておいて最後に処理するといった使い方です。
ISyntaxReceiver
インターフェースは OnVisitSyntaxNode
メソッドのみをもち引数として SyntaxNode
を受ける形でしたが、 ISyntaxContextReceiver
では GeneratorSyntaxContext
を受け取る形になります。
このコンテキストオブジェクトにはセマンティックモデルが含まれているので、Receiver で必要なものをかき集めるときに、コードの解析された情報にアクセスできるようになります。
例えば属性はセマンティックモデルを通してシンボルを取得して、GetAttributes で取得できるので、Source Generator 用の属性がついていることを Receiver が集める段階で確認できます。こちらもサンプルコードが例としてわかりやすいです。
16.9 で追加された API なので古い Visual Studio や .NET SDK では動かない可能性があります(試していない)。
Rider 2021.1.2 ではビルドは問題ないのですが Rider 側が対応していないようでエディター上では正しく動作しなかったりしたので、2021年5月頭時点では使うにはちょっと早いかもしれません。
]]>Blazor はお手軽に Single Page Application を作れるのですがコンポーネントのライフサイクル、ステート関連について知らないと若干不思議な仕組みで動いているように見えます。
例えばプロジェクトテンプレートの Counter は @onclick="IncrementCount"
でプライベート変数 currentCount
をインクリメントするとページのカウントがアップしますが、これは初見ではなかなか不思議な挙動です。Blazor は currentCount
の変更をどうやって知ったのか?としばらく不思議に思っていました。そういったこともライフサイクルを知ることで理解できます。
基本的には ASP.NET Core Blazor ライフサイクル というドキュメントに書いてあるのですがこれはそれを補完する目的のエントリーです。
Blazor コンポーネントはコンポーネントのステート(状態)が変更されるたびに再レンダリングを行うという流れが基本となります。これ自体は他のフレームワークと大きく変わるところではないと思います。
これらの多くのライフサイクルに関連するものは Microsoft.AspNetCore.Components.ComponentBase クラス に実装されています。
コンポーネントの初回のレンダリング、つまり初めてページが表示されたときの処理の流れは次のようになっています。
初めに SetParametersAsync が呼び出され、初期化とパラメータのセットを行います。この OnInitializedAsync (初期化), OnParameterSetAsync (パラメータセット) では非同期処理を行うことができますが、その場合は Task の完了を待たずにレンダリングが行われて完了次第再レンダリングが行われます。
つまり非同期初期化の場合、コンポーネントの HTML をレンダリングが行われるタイミングではまだ値がそろっていない場合がある点には注意が必要です。
private NanikaObject _nanika;protected override async Task OnInitializedAsync(){ await Task.Delay(1000); _nanika = new NanikaObject(); await base.OnInitializedAsync();}...@* OnInitializedAsync が終わるまで _nanika は null *@<h1>@_nanika.Description</h1>
BuildRenderTree メソッドは Blazor 上のドキュメントツリー構造(仮想DOM的なもの)を構築するものです。通常は Razor Component (.razor) ファイルから自動生成され、手でツリーを作るようなカスタムコンポーネントを作らない限りは触ることはありません。中身が気になる場合には obj ディレクトリの中を覗いてみると生成物を確認できます。
なお Server-side Blazor に関してはレンダリングモードが ServerPrerendered の場合、 SetParametersAsync + BuildRenderTree が2回呼び出されます。1回目はプリレンダリングのための実行です。またプリレンダリングはサーバーサイドで完成系を返す都合、SetParametersAsync の完了を待ってからのレンダリングとなります。
OnInitialized{Async} と OnParametersSet{Async} の違いは、OnInitialized がコンポーネントの初期化であるのに対して、OnParametersSet はプロパティの変更です。
つまり [Parameter]
として受けている値が変わったときに呼び出されるもので React で言うなら props の変更のような感じでしょうか。
例えば次のようなコンポーネントを用意して…
<p>@Count</p>@code { [Parameter] public int Count { get; set; } protected override Task OnParametersSetAsync() { Console.WriteLine($"Count={Count}"); return base.OnParametersSetAsync(); }}
Counter.razor にぶら下げてみます。
<Nantoka Count="currentCount" />
これでカウンターを更新すると OnParametersSetAsync が呼び出されることを確認できます。
そもそも Blazor におけるステートとは何かステートという特別な入れ物があるわけでもなく、あるのは何らかの何らかステートが変更されたということを通知する仕組みです。
データそのものはプライベート変数であったり、Parameter であったりするかもしれませんが Blazor としては「何か状態が変わったことを知る」ということが重要になります。
Blazor はステートの変更通知を受けると、コンポーネントの再レンダリングを行い、表示を更新します。
ステートの変更通知はどのようなタイミングで発生するのかですが、おおまかに下記の3パターンで発生します。
@bind
や @onclick
とか)3パターンと書きましたが2つ目と3つ目は暗黙的に StateHasChanged を呼び出しているというのが実際のところです。
StateHasChanged はステートが変更されたことを通知するメソッドです。このメソッドを呼び出すと再レンダリング候補としてマークされます。何が起こるのかは後ほど少し説明します。
多くのケースでは StateHasChanged を明示的に呼び出す必要はありませんが、ロジック側から表示を更新したいケースでは呼び出しを必要とします。
例えばサーバーからのデータ受信やタイマー処理といったユーザー操作によるイベントではないが表示を更新するようなパターンです。この場合は自らステートが変更されたので再レンダリングしてほしいという意思を伝えるために StateHasChanged を呼び出す必要があります。
冒頭でプロジェクトテンプレートの Counter でクリックしてプライベート変数を更新するだけでページの表示が変わって不思議、と書きましたがそのような挙動になる理由はここにあります。
Blazor の ComponentBase ではイベントハンドラーの実行時に StateHasChanged を呼び出しています。
https://github.com/dotnet/aspnetcore/blob/v5.0.0/src/Components/Components/src/ComponentBase.cs#L313
Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object? arg){ var task = callback.InvokeAsync(arg); var shouldAwaitTask = task.Status != TaskStatus.RanToCompletion && task.Status != TaskStatus.Canceled; // After each event, we synchronously re-render (unless !ShouldRender()) // This just saves the developer the trouble of putting "StateHasChanged();" // at the end of every event callback. StateHasChanged(); return shouldAwaitTask ? CallStateHasChangedOnAsyncCompletion(task) : Task.CompletedTask;}
これにより「クリックイベントが発生する」→「プライベート変数を更新する」→「StateHasChanged を呼び出す」→「再レンダリング」→「値が表示に反映される」という流れになっているというわけです。
変数を監視していたり、プロキシになっていたりするわけではなくイベントが発生したら StateHasChanged (=ステート変更通知)が呼び出されているだけという単純な仕組みです。
コンポーネントの Parameter に渡される値が変化した場合もステートの変更がありと認識されます。
これも ComponentBase.SetParametersAsync の中で StateHasChanged を呼びだしているので、値の変更があると SetParametersAsync が呼び出されることで結果的にステートの変更があった扱いになります。
https://github.com/dotnet/aspnetcore/blob/v5.0.0/src/Components/Components/src/ComponentBase.cs#L277
private Task CallOnParametersSetAsync(){ OnParametersSet(); var task = OnParametersSetAsync(); // If no async work is to be performed, i.e. the task has already ran to completion // or was canceled by the time we got to inspect it, avoid going async and re-invoking // StateHasChanged at the culmination of the async work. var shouldAwaitTask = task.Status != TaskStatus.RanToCompletion && task.Status != TaskStatus.Canceled; // We always call StateHasChanged here as we want to trigger a rerender after OnParametersSet and // the synchronous part of OnParametersSetAsync has run. StateHasChanged(); return shouldAwaitTask ? CallStateHasChangedOnAsyncCompletion(task) : Task.CompletedTask;}
StateHasChanged メソッドを呼ぶと次のような流れで処理が実行されます。
初回と似ていますが異なるのは初期化周りのメソッド(OnInitialized)が呼び出されないことと ShouldRender が呼ばれることです。
ShouldRender メソッドはレンダリングする必要があるかどうかを返すことができるものです。このメソッドのデフォルト実装は常に true
を返しますが、カスタムコンポーネントは再レンダリングを抑える目的でオーバーライドできます。
StateHasChanged の実装は ComponentBase クラスにあり、protected として公開されています。
https://github.com/dotnet/aspnetcore/blob/v5.0.0/src/Components/Components/src/ComponentBase.cs#L100
このメソッド自体は比較的シンプルでやっていることは次のようになっています。
先ほどの流れで ShouldRender の後に BuildRenderTree が呼び出されると書いていましたが、実際は一度レンダラーのキューに詰めてから BuildRenderTree が呼び出されます。
Blazor のライフサイクルやレンダリングでは StateHasChanged が重要な役割となります。
しかしイベントハンドラーのような暗黙的に呼び出されているケースもあり、Blazor の仕組みをあまり知らずに使っていても画面が更新されるので思わぬところでハマる可能性もあります。そういった罠を踏む前にある程度流れを抑えておくことをお勧めします。
]]>Visual Studio からの発行や dotnet publish -c Release
などを実行したとき、IL Linker 実行中に以下のような AssemblyResolutionException
がスローされます。
Fatal error in Mono IL LinkerC:\Users\Tomoyo\.nuget\packages\microsoft.aspnetcore.components.webassembly.build\3.2.1\targets\Blazor.MonoRuntime.targets(326,5): error : Unhandled exception. Mono.Cecil.AssemblyResolutionException: Failed to resolve assembly: 'SQLitePCLRaw.core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=1488e028ca7ab535' [C:\Users\Tomoyo\Source\Repos\BlazorApp6\BlazorApp6\BlazorApp6.csproj] ---> Mono.Cecil.AssemblyResolutionException: Failed to resolve assembly: 'SQLitePCLRaw.core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=1488e028ca7ab535' at Mono.Cecil.BaseAssemblyResolver.Resolve(AssemblyNameReference name, ReaderParameters parameters) at Mono.Linker.AssemblyResolver.Resolve(AssemblyNameReference name, ReaderParameters parameters) at Mono.Linker.LinkContext.Resolve(IMetadataScope scope) at Mono.Linker.LinkContext.Resolve(IMetadataScope scope) at Mono.Linker.LinkContext.ResolveReferences(AssemblyDefinition assembly) at Mono.Linker.Steps.LoadReferencesStep.ProcessReferences(AssemblyDefinition assembly) at Mono.Linker.Steps.LoadReferencesStep.ProcessAssembly(AssemblyDefinition assembly) at Mono.Linker.Steps.BaseStep.Process(LinkContext context) at Mono.Linker.Pipeline.ProcessStep(LinkContext context, IStep step) at Mono.Linker.Pipeline.Process(LinkContext context) at Mono.Linker.Driver.Run(ILogger customLogger) at Mono.Linker.Driver.Execute(String[] args, ILogger customLogger) at Mono.Linker.Driver.Main(String[] args)
IL Linker は依存しているアセンブリ参照をすべて検索して、不要な IL を削っていくというビルドステップです。
この例外(エラー)は依存先のアセンブリを探しているときにアセンブリが見つからなかった場合に発生するもので、通常 NuGet でパッケージ参照されているものに対しては発生しません。
しかし依存パッケージの作り次第では例外が発生する場合があります。例えば Microsoft.CodeAnalysis.Workspaces.Common
のようなパッケージを参照すると発生します。
これは Microsoft.CodeAnalysis.Workspaces.Common
が SQLitePCLRaw.bundle_green
パッケージを PrivateAssets="all"
として参照しているため、実際のパッケージにはパッケージ参照として SQLitePCLRaw.bundle_green
が含まれないことでアプリケーションから参照したときにパッケージが解決されないのでアセンブリが見つからないということが発生します。
つまりパッケージの依存としては扱わないもののアセンブリを参照はあるという状況で発生します。必須ではないパッケージなどの場合にはこういった構成になります。
解決方法としては次の2つの方法があります。
見つからないといわれているアセンブリが含まれているパッケージをプロジェクトから参照します。
SQLitePCLRaw.bundle_green
のようなものは参照してもエラーとなるのでそういった場合には IL Linker 自体を無効にします。
<PropertyGroup> <BlazorWebAssemblyEnableLinking>false</BlazorWebAssemblyEnableLinking></PropertyGroup>
]]>Visual Studio Code のリポジトリで ARM64 対応がないかなと眺めていたところ ARM64 ビルドをやっていく PR が作られているのを発見し、マージされるのを watch していたのですが先日ついにマージされました。
というわけで自分で VSCode リポジトリからビルドしてみたところ、手順が意外とわかりにくかったのでまとめておきます。
大体は vscode リポジトリの今トリビュートガイドのビルド手順に従って環境を用意します。
まず、クロスコンパイルする形になるので x64 の Windows 環境が必要になります。もしかするとコンパイラーなどは動くかもしれませんが死ぬほど遅いでしょうし、Node.js もメモリーの都合なのか x64 版を用意するように書かれているので素直に x64 環境でビルドするのがよいでしょう(AzureやAWSで適当にVM立ててビルドするとか)。
Python とコンパイラーは npm install -g windows-build-tools
でインストールできます。VSCode のガイドには --vs2015
を指定して Visual Studio 2015 で…のようなことが書いてありますがそのまま Visual Studio 2017 でもビルドできます。
コンパイラーは windows-build-tools
ではなく Visual Studio 2017 Build Tools を入れることでも大丈夫です。Visual Studio 2019 の場合でも 2017 のコンパイラーが入っていればビルドできそうな気がします。
さらに ARM64 版ビルドを作る場合には Visual Studio Installer から個別のコンポーネントとして Visual C++ compilers and libraries for ARM64
というものを入れておく必要があります。
Visual C++ 2010 Redistributable (x86) も必要です。これはビルド途中で使われるリソース書き換えツールの rcedit を動かすのに必要です。
ツールがそろったらコードを clone して、yarn でモジュールをインストールします。
git clone https://github.com/microsoft/vscode.git
と、yarn でモジュールをインストールする前にビルド対象のアーキテクチャを環境変数で設定しておきます。
cd vscodeset npm_config_arch=arm64set npm_config_target_arch=arm64
設定したら yarn を実行します。
yarn install
すると多分途中で %USERPROFILE%\AppData\Local\node-gyp\Cache\12.4.0\arm64\node.lib
がないというようなエラーになるかと思います。
node-gyp で使われる lib がないということなので次の場所からダウンロードしてきて放り込みます。
https://unofficial-builds.nodejs.org/download/release/v12.15.0/win-arm64/
本当はバージョンを合わせるべきな気がしますが、必ずしも同じバージョンのバイナリがあるわけではないのでそれっぽいバージョンを放り込みます。
ファイルを置いたらもう一度 yarn install
を実行すると最後まで通るはずです。
ここまできたらあとはビルドするだけですが、その前に少し VSCode のビルド設定を変更しておきます。
まず前提として VSCode リポジトリは Microsoft からリリースされている Visual Studio Code そのものではないのです。
VSCode リポジトリは Code - OSS
というオープンソースな Visual Studio Code 部分です。一方 Microsoft がバイナリでリリースしている Visual Studio Code は Code - OSS
にブランディングや Marketplace のエンドポイント設定、テレメトリーの有効化などのカスタマイズを行って、プロプラエタリなライセンスでリリースしているものとなっています。Chromium と Google Chrome とかも似たような感じかもしれません。
つまり Code - OSS
そのままだと Marketplace を使えないので、API エンドポイントを設定してあげます。そのあたりは VSCode リポジトリを元にコミュニティーバイナリビルドを作っている VSCodium を参考にして product.json を編集します。
diff --git a/product.json b/product.jsonindex 075a1d0ada..093f517644 100644--- a/product.json+++ b/product.json@@ -20,6 +20,9 @@ "licenseFileName": "LICENSE.txt", "reportIssueUrl": "https://github.com/Microsoft/vscode/issues/new", "urlProtocol": "code-oss",+ "quality": "stable",+ "extensionAllowedBadgeProviders": ["api.bintray.com", "api.travis-ci.com", "api.travis-ci.org", "app.fossa.io", "badge.fury.io", "badge.waffle.io", "badgen.net", "badges.frapsoft.com", "badges.gitter.im", "badges.greenkeeper.io", "cdn.travis-ci.com", "cdn.travis-ci.org", "ci.appveyor.com", "circleci.com", "cla.opensource.microsoft.com", "codacy.com", "codeclimate.com", "codecov.io", "coveralls.io", "david-dm.org", "deepscan.io", "dev.azure.com", "flat.badgen.net", "gemnasium.com", "githost.io", "gitlab.com", "godoc.org", "goreportcard.com", "img.shields.io", "isitmaintained.com", "marketplace.visualstudio.com", "nodesecurity.io", "opencollective.com", "snyk.io", "travis-ci.com", "travis-ci.org", "visualstudio.com", "vsmarketplacebadge.apphb.com", "www.bithound.io", "www.versioneye.com"],+ "extensionsGallery": {"serviceUrl": "https://marketplace.visualstudio.com/_apis/public/gallery", "cacheUrl": "https://vscode.blob.core.windows.net/gallery/index", "itemUrl": "https://marketplace.visualstudio.com/items"}, "extensionAllowedProposedApi": [ "ms-vscode.references-view" ],
準備ができたらビルドを実行してコンパイルして配布物一式を作成します。
set NODE_ENV=productionyarn gulp vscode-win32-arm64yarn gulp vscode-win32-arm64-archive
vscode-win32-arm64-archive
を実行すると .build\win32-arm64\archive\VSCode-win32-arm64.zip
という ZIP ファイルが出来上がります。
というわけでこの一式を ARM64 環境へもっていって展開すれば Visual Studio Code 的なものを使えます(インストーラーは2020年4月現在ビルドできません)。
ビルドの準備の途中でも書きましたが、手元でビルドしたものは Microsoft からリリースされるものとは異なります。
名前が違うこともあり設定は Visual Studio Code とは別の場所に保存されるものになります(つまり公式リリースが出た場合でも設定は別になります)。
ライセンスが違うのが実はちょっと罠なので注意が必要です。というのも Microsoft がリリースしている VSCode 拡張のライセンスは Microsoft 公式から配布されている Microsoft Visual Studio Code とともに使うことが許可されているものがあります(例えば Remote とか)ので注意してください。
]]>突然ですがスタックトレースから何らか自分の書いたメソッドを隠したいなと思ったことはないでしょうか?普段はそんなことを考える必要はないのですがアプリの下回りやミドルウェア的なものを作っているとたまにそういった場面に遭遇します。
例えば ASP.NET Core MVC の ActionFilter のようなフィルターチェーン的なものを作るといったパターンがあるとします。まず次のような感じのフィルター定義があって…
interface IFilter{ void Invoke(object context, Action<object> next);}class AFilter : IFilter{ public void Invoke(object context, Action<object> next) { Console.WriteLine("A Begin"); next(context); Console.WriteLine("A End"); }}class BFilter : IFilter{ public void Invoke(object context, Action<object> next) { Console.WriteLine("B Begin"); next(context); Console.WriteLine("B End"); }}
そのフィルターをつなげて呼び出すというような仕組みです。
// フィルターの一覧を作る (後ろに行くほうが内側 = 外側が AFilter, 内側が BFilter)var filters = new IFilter[] { new AFilter(), new BFilter() };// フィルターに渡す次のアクションの変数(その際最後になるアクションを入れておく)Action<object> next = (context) =>{ // 一番深いところでスタックトレースを取得する Console.WriteLine(Environment.StackTrace);};// フィルターチェーンを作る// 内側から外に向かってつないでいくforeach (var filter in filters.Reverse()){ // next をラムダにキャプチャーする必要がある var next_ = next; next = (context) => filter.Invoke(context, next_);}// 呼び出すnext(null);
このコードの実行結果はおおよそこんな感じになります。
A BeginB Begin at System.Environment.get_StackTrace() at Program.<>c.<Main>b__4_0(Object context) in C:\Program.cs:line 9 at Program.BFilter.Invoke(Object context, Action`1 next) in C:\Program.cs:line 44 at Program.<>c__DisplayClass4_1.<Main>b__1(Object context) in C:\Program.cs:line 17 at Program.AFilter.Invoke(Object context, Action`1 next) in C:\Program.cs:line 35 at Program.<>c__DisplayClass4_1.<Main>b__1(Object context) in C:\Program.cs:line 17 at Program.Main() in C:\Program.cs:line 21 (略)B EndA End
結果を見ていただくとわかるのですがスタックトレースに Program.<>c.<Main>b__4_0(Object context)
といった呼び出しが現れていますが、単にキャプチャーして渡すだけのメソッドなのでユーザーにはほぼ意味のないものです。とはいえコード上でラムダを挟んでいるので出てくるのは当然です。
そこで何とかしてこのような些末な呼び出しをスタックトレースから隠すためのハックを今回ご紹介します。
StackTraceHiddenAttribute
を使う.NET Core 2.0 には StackTraceHiddenAttribute
という属性が追加されていて、その属性のついたメソッドはスタックトレースから除外されるようになっています。
ということはこれを使えば解決しそうですが残念ながらこの属性は internal
です。効果を考えたらそんなカジュアルに使われても困るので内部向けに用意したというところでしょうか。
とはいえそれでも動的アセンブリ生成なら無理やり属性を引っ張り出してくっつけることができるはず…!イメージとしては次のようなヘルパークラスの InvokeNext
の部分を動的に作ってうまいことするような感じです。
/// <summary>/// 次のフィルター呼び出しと、フィルターのメソッドを保持するクラス。/// 以前のラムダのキャプチャーと同等。/// </summary>public class InvokeHelper{ /// <summary>フィルターのメソッド</summary> public Action<object, Action<object>> Invoke; /// <summary>次のフィルターの呼び出しとなるデリゲート</summary> public Action<object> Next; public InvokeHelper(Action<object, Action<object>> invoke, Action<object> next) { Invoke = invoke; Next = next; } [StackTraceHidden] public void InvokeNext(object context) { Invoke(context, Next); }}
ということでILをペコペコ書きます。
そしてフィルターチェーンを作るところをヘルパー経由に書き換え。
foreach (var filter in filters.Reverse()){ next = new InvokeHelper<object, Action<object>>(filter.Invoke, next).GetDelegate();}
そして実行すると…。
A BeginB Begin at System.Environment.get_StackTrace() at Program.<>c.<Main>b__4_0(Object context) in C:\Program.cs:line 9 at Program.BFilter.Invoke(Object context, Action`1 next) in C:\Program.cs:line 44 at Program.AFilter.Invoke(Object context, Action`1 next) in C:\Program.cs:line 35 at Program.Main() in C:\Program.cs:line 21 (略)B EndA End
ばっちり BFitler
と AFitler
の間にあった呼び出し行が消えました!めでたしめでたし。
案1でめでたしめでたしとなるかと思いきや、実は案1を試している間に気づいたのですが StackTraceHiddenAttribute
をつけなくても消えます。案1の下記の二行を削除してみても結果は変わりません。
var attrStacktraceHiddenAttribute = Type.GetType("System.Diagnostics.StackTraceHiddenAttribute");method.SetCustomAttribute(new CustomAttributeBuilder(attrStacktraceHiddenAttribute.GetConstructor(Array.Empty<Type>()), Array.Empty<object>()));
どうも CoreCLR の中も少し調べてみたのですがよくわからず、動的生成されたメソッドがスタックトレース的に何らかの特別扱いされることがあるようです。
というわけで動的にメソッドを定義できれば理屈は謎ですが消えますし、アセンブリを生成しなくとも DynamicMethod
で事足ります。
[MethodImpl(MethodImplOptions.AggressiveInlining)]
を使うStackTraceHiddenAttribute
のあたりのコードを見ていて気が付いたのですが .NET Core 3.0 以降では [MethodImpl(MethodImplOptions.AggressiveInlining)] がついているかどうかもチェックしています。
これは通常JITでインライン化されるとスタックトレースから消えてしまうけれども、Tiered Compilation の Tier 0 ではインライン化されないので表示されてしまったりして一貫性がないので属性がついているものは丸ごと非表示にしてしまおうということのようです。
ということで裏技っぽいですが [MethodImpl(MethodImplOptions.AggressiveInlining)]
をつければ非表示になります。内容的にもインライン化されたところで困るものでもないですしよさそうです。
ヘルパークラスのイメージとしてはこんな感じです。
public class InvokeHelper{ /// <summary>フィルターのメソッド</summary> public Action<object, Action<object>> Invoke; /// <summary>次のフィルターの呼び出しとなるデリゲート</summary> public Action<object> Next; public InvokeHelper(Action<object, Action<object>> invoke, Action<object> next) { Invoke = invoke; Next = next; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void InvokeNext(object context) { Invoke(context, Next); }}
ということでこれをまとめると .NET Core 3.0 以前では動的メソッド定義、それ以外では AggressiveInlining
をつけておくというのがよさそうです。シンプルですし、もし挙動が変わってスタックトレースが出るようになっても AggressiveInlining
がついているだけなのでプログラムの挙動には影響を与えないのもよいですね。
このハックは実際にMagicOnionのフィルター周りに入れていてスタックトレースの見やすさを向上させています。
スタックトレースは開発において重要な情報なのでノイズは少なければ少ないほうが望ましいですし、アプリケーションの基盤に近い部分ではそういった開発体験を考慮しておくのは重要だと思います。今回のようなハックも機会があればお役立てください。
なお、あくまでハックなのでいつまで効果があるかといった点は保証できませんのであらかじめご了承ください。
]]>4万年以上前の仔ウマが丸ごと出てくるのすごい。
音声ガイドを東山奈央さんがナビゲートされていたので折角なので借りてみました。ジュニアガイドとはいえTips的な話もあり、比較的ライトな雰囲気はこれはこれで良かったです。
]]>HttpClient
の StringContent
は自動で Content-Type
に charset=<encoding>
を付けるStringContent.Headers.ContentType
を手で設定しなおす必要があるHttpClient
でとある API (POST) を呼び出すとなぜか動かない… curl
で同じ内容を投げると動くのに…という相談を受けて、そんな不思議なことが?と思って調べてみたのが始まりでした。
curl
で動いているなら .NET の HttpClient が投げるものが何か違うのではと思って、リクエストやリクエストを作っているところを確認してみました。
// Initialize the 'Content-Type' header with information provided by parameters.MediaTypeHeaderValue headerValue = new MediaTypeHeaderValue((mediaType == null) ? DefaultMediaType : mediaType);headerValue.CharSet = (encoding == null) ? HttpContent.DefaultStringEncoding.WebName : encoding.WebName;Headers.ContentType = headerValue;
StringContent
クラスのコンストラクターのコードではメディアタイプとともに CharSet
プロパティを設定したものをヘッダーの ContentType
プロパティに設定しています。この CharSet
プロパティは Content-Type の charset 指定になります。
つまり StringContent
クラスを使って投げるリクエストを作るとデフォルトで application/json; charset=utf-8
のような Content-Type で送られるということになります。
ContentType
を再設定するcharset
パラメータが必ずついてしまうということが分かったので、付けずにリクエストを送信したいという場合にはこれを変える必要があります。
といってもやることは簡単で後から手動で ContentType
プロパティを再設定してあげれば大丈夫です。
content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
]]>Lifebear のサーバーアプリケーション (.NET Core) を Amazon EKS / Kubernetes 環境を構築してリリースするまでにメモしていたポイントまとめた形です。
「EKS/AKS/GKE で Kubernetes 立てて、.NET Core アプリをデプロイしてみた(使ってみた)」より一歩進んだ話が欲しいと思っていたのでそれがテーマでした。
.NET Core のアプリケーションを Kubernetes の上で動かしたという国内の実例というのは少なくて、自分で構築する際にも不安になったので、今後 Kubernetes でシステムを構築しようという人の参考になってほしいなと。…と思って書いたらちょっと盛りすぎてしまったので、Kuberentes 寄りの話と .NET Core 寄りの話を分ける方がよかったかなというのが反省点ですね。
ネタ帳にはあったものの時間の関係上話せなかったスライドに書いた以外のこともこの際なので書いておきます(主に Kubernetes)。
BuildKit でビルドすると高速かつキャッシュできるので有効にするのがおすすめです。特に NuGet パッケージのリストアに効果があります。
環境変数で DOCKER_BUILDKIT=1
を指定して Dockerfile でおまじないを追加するだけです。
# syntax = docker/dockerfile:experimental...RUN --mount=type=cache,target=/root/.nuget/ \ dotnet restore "WebApplication1.csproj" -c Release -o /app
Kubernetes のクラスターのバージョンは1マイナーバージョンアップごとに上げることになります。これはバージョンジャンプが大変なので日々追いかけておくつもりが必要ということです。
kubectl や kubelet のバージョンポリシーもあり、例えば kubelet は api-server に対して前2マイナーバージョン、kubectl はプラスマイナス1マイナーバージョンといった制約がありますのでどのみち大ジャンプが難しいということです。
通常ノードを落とす時には kubectl drain
するのが普通です。ある時 kubectl drain
しなくても kubectl cordon
してアプリを再デプロイして Pod を別なところに移せばそのままノードを落とせるのではと思い、やってみると CoreDNS 等のサービスがいなくなって死にました(もちろん対象のノードにいなければ無傷)。それはそう…。
基本カスタマイズは UserData で起動時に何とかします。AMI作っても絶対管理できないですし、ノードはコンテナーを動かすためだけの代物なので。
/etc/eks/bootstrap.sh
というものに kubelet のパラメータなどを渡せるのでそこでやりましょう。
Pod のシャットダウンと Service から外れるタイミングが異なるため、先に Pod がシャットダウンすると接続しようとしてしまうという話。preStop
フックで数秒待って外れてからシャットダウンへ進むようにします。
apiVersion: apps/v1kind: Deploymentmetadata: name: myappspec: template: spec: containers: - name: myapp lifecycle: # シャットダウン前に少し待たないと Service から削除されるより先に Pod が終了してしまうことがある # https://qiita.com/superbrothers/items/3ac78daba3560ea406b2 preStop: exec: command: ["sh", "-c", "sleep 5"]
Kubernetes API のエンドポイントをインターネット側からアクセスできないようにする設定が追加されたので特にプロダクション環境などでは有効にするのがおすすめです。
ただし、当然外部サービスから Kubernetes API を呼び出す難易度が上がるのでその点は考慮する必要があります(その制約で Azure Pipelines の Environments を使えなかった)。
Amazon EKS では Pod に対してプライベートIPを割り当てるのですが、普通の EC2 インスタンスの気持ちで小さめのサブネットにしていると Pod がぽこぽこ増えた時に困ったことになります。
アドレス空間はある程度余裕を持った形にしておくのがよいです。
Kubernetes 上で発生したエラーイベントを監視しておきましょう。Datadog では Kubernetes のイベントも監視できるのでひっかけて通知するようにしていると異変に気付きやすくなります。
CrashLoopBackOff は発生していても、以前の Pod にはアクセスできるので他のきっかけがないと気づきにくくなります。
Docker イメージを CI でビルドするのに CI のエージェントが Docker で動いているとそのままでは Docker 動かない問題にあたります。そこで俗にいう Docker in Docker (dind) です。
イメージをビルドするだけなので、ほかにも img とか Rootless Docker とか kaniko とか検討できるものはあるのですが無理しない方針で無難に Docker in Docker にしました。
privileged: true
にしないといけないのでそこは微妙なのですが、今ならもしかすると Rootless でうまいこと privileged: true
しなくてもいけるかもしれません。
Kubernetes の API にアクセスする場合 ServiceAccount の設定をミスっていたりするとうまくアクセスできなくなったりするのですが、これをアプリデプロイして試すのは大変なので kubectl exec podname -it /bin/bash
で入って curl
で叩くのがおすすめです。
通常、Pod をデプロイするときには Kubernetes がリソースの具合を見て適度にバランスしてスケジューリングします。一方でデプロイ後に再スケジュールはしてくれません。
例えば、2つあるうちの1つのノードが死んだ場合はそのノードで動いていた分の Pod が生きているノードで起動されなおします。そして、その後ノードが復活してきたとしても Pod たちは元のノードに帰っていったりはしません。偏ったままです。
これを解決する手っ取り早い方法としては再デプロイかレプリカ数を上げたり下げたり、要するに Pod の再作成です。
他には Kubernetes の拡張として Descheduler という再スケジュールしなおすものが開発されているのでこちらの利用を検討してもいいかもしれません(が、鋭意開発中っぽい雰囲気があります)。
振り返ってみると大変なのはアプリそのものよりは中小規模の Kubernetes クラスターを構築、運用するノウハウというところが大きい気がしました。
繰り返しにはなりますが、.NET Core を Kubernetes で実際に動かしてリリースまでもっていっているという事例の参考になって、事例がもっと出てきてほしいですね。
というわけでライフベアではほとんど好き勝手構築させていただいたので大変感謝しています。もし .NET Core (C#) + Kubernetes な環境ににご興味のある方はご一報いただくか、直接コンタクトしていただくと良いかと思います。
そして Cysharp では C# (サーバー、クライアント) に関するご相談を受け付けておりますのでお困りのことがあればぜひお問い合わせください。
]]>コンストラクターではまだ初期化しないのだけど、いずれフレームワークから初期化するので API を利用する側はほぼ null を扱わない、けど内部の初期状態は null なのを許してほしいというケースはまあよくあります。Kotlin の lateinit
や TypeScript の definite assignment assertion みたいなやつですね。
C# ではズバリそれっぽいものはないので default!
を初期値として代入しておけばよいです。
lateinit
とかもそうですが適当に使うと治安が最高に悪くなるので気を付けたほうがいいやつです。
class 制約のないジェネリクスの場合 ?
をつけて null 許容型であると宣言できません。そのため null
を扱うかもしれない、というケースを宣言するには属性を使用することになります。
逆を言えば where T: class
や where T: unmanaged
のような制約をつけておけば T?
と null 許容型を素直に書くことができるので書けるときは書いてしまうのが手です。
AllowNull
属性と DisallowNull
属性 (事前条件)例えば次のようなメソッドの引数で参照型のときは null 渡したいケースがあります。
そのままでは非許容なので null が来てもよいということをコンパイラーに伝える必要があるので、 AllowNull
属性を使用します。
逆に null が入ってきてほしくないというのを明示することもできます。例えば先ほどのコードをもう一度。
このような場合は DisallowNull
属性を使用すると null を許さないということを明示できます。
DisallowNull
属性は .NET Core のコードであれば IEqualityComparer<T>.GetHashCode
などに見られます。
MaybeNull
属性と NotNull
属性 (事後条件)次は値型ならデフォルト値、参照型なら null が返るようなメソッドを扱うケースです。GetValueOrDefault
や DefaultIfEmpty
のようなものでよくありますね。
このようなケースをカバーするには MaybeNull
属性を使うと戻り値が null になる可能性があることを表せます。なお、戻り値に対しての属性なので return:
です。
逆に絶対 null を返さないということがわかっている場合には NotNull
属性で宣言できます。例えば次のようなケースです。
AllowNull
属性と DisallowNull
属性 (事前条件)プロパティの setter で null を許可したかったり拒否したかったりというケースがあるかもしれません。例えば setter で null はセットできるけれども null は返ってこないような API です(というのはあまり想像しづらいですが…)。
ちなみにコンパイラーが中途半端に賢いので Value = null;
の後に Value
を使用しようとすると怒られます(が、実際は null じゃないはずなのでなんか変な気がします)。
逆は例によって DisallowNull
です。null が返るかもしれないけれど null をセットすることは許さないようなケースです。Microsoft のドキュメントのサンプルではこんな感じです。
初期化時は null が返るけど、ユーザーがセットすることは許さないみたいな感じですかね。
NotNullWhen
属性 と MaybeNullWhen
属性 (事後条件)TryGetValue
や TryParse
のような、成功時には out
引数で値を返すがそれ以外では null
を返し、成否は戻り値にするというケースがあります。例えば次のようなコードがあるとします。
この MyDictionary
を使う場合は次のようなコードになるわけですが、その際 null
の扱いも上手く処理されてほしいわけです。
そこで戻り値によって nullable かどうかを伝える NotNullWhen
という属性が用意されています。この属性を付けると「nullable な型が場合によってはその後 non-nullable 確定できるかも」という情報をコンパイラーに伝えられます。
他の使い道としては String.IsNullOrEmpty
みたいなもので使えます(使われています)。 IsNullOrEmpty
も後のフロー解析に影響を与えてほしい側面を持っているのでピッタリですね。
逆の意味を持つのが MaybeNullWhen
属性で non-nullable につけた MaybeNullWhen(false)
は NotNullWhen(true)
と同じになります。
これらの属性の使い分けですが
out
, ref
) が前提として null 非許容型 (T
) かもしれないなら MaybeNullWhen
属性out
, ref
) が前提として null 許容型 (T?
) かもしれないなら NotNullWhen
属性例えば TryGetValue
のようなもので MaybeNullWhen
属性を使った場合、前提として non-nullable になると out var
で宣言したローカル変数が non-nullable になり、if
のようなフロー解析から外れたときに non-nullable (ただし null が入っている可能性がある) という状況になります。
同様に IsNullOrEmpty
のようなもので MaybeNullWhen
属性を使った場合、引数は前提として non-nullable なので String?
な値を引数に渡すたびに !
を付けてあげないといけないことになります。
…と、ここまで読むと MaybeNullWhen
属性を使わなくてもほとんどのケースでは NotNullWhen
属性で事足りるのではと思うのですが、制約なしジェネリクスの型パラメータに ?
を付けて null 許容型とすることはできないのでその場合には MaybeNullWhen
を使う必要があります。
NotNullIfNotNull
属性 (事後条件)入力が null ではなかったら絶対に null ではないということがわかっていて、コンパイラーに伝えたいというケースに使えるのが NotNullIfNotNull
属性です。何を言っているのかみたいな名前…。
具体的なケースであれば例えばエスケープ処理のようなもので、文字列を受け取るけれども null が来たら null を返すが、それ以外は絶対値が返るようなメソッドです(その仕様がいいかどうかはさておき)。
属性を書き始めると書き味が悪くなるのと IntelliSense などでシグネチャヒントを見ただけではわからないという問題が起きやすくなるので、新規に書き起こすものは極力属性を書かなくて済むような API にするほうが望ましいように感じました。
例えば NotNullIfNotNull
属性のようなものなどはそもそも引数で null を受けない、TryGetValue
などは戻り値と out
を使わないで null 許容型をそのまま返すといった形にできます。
属性は既存のコードを壊さず null 許容型との相互運用のためにアノテーションをつけていくものというぐらいの認識がいいかもしれません。まあジェネリクスが厳しいのですが…。
]]>一方で逆のパターン、つまりGoogle (GSuite, Cloud Identity)をIdPとしてAzure ADやOffice 365にサインインしたり、ユーザー情報を同期するパターンについての説明は少ないので折角ですしメモもかねて残しておきます。
ちなみにAzure AD B2Bの機能としてGoogleをアプリとして登録することでAzure ADにゲストとして登録する機能というのもあるのですがそれとは別です。
公式ドキュメント(Office 365 クラウド アプリケーション - G Suite 管理者 ヘルプ)とushiyasanさんのブログエントリー(GoogleアカウントでOffice365(Azure AD)にSSOログイン)を参考にして大体設定しています。というわけで基本は公式のドキュメントの手順に沿って設定していきます。
公式ドキュメントの手順1で、ここはドキュメントそのままです。まずは Google の管理コンソールから設定のための情報を取得します。
特権管理者で管理コンソールにログインして セキュリティ
→ シングルサインオン (SSO) の設定
を開いて、各種情報のメモと証明書のダウンロードを…と思ったのですが実はSAMLアプリケーション (Office 365)の追加の手順の途中でも表示されるのでそっちからでもよいです。
特権管理者で アプリ
→SAML アプリ
で サービスやアプリをドメインに追加
をクリックして、SAMLアプリを追加する画面を表示します。SAML アプリケーションで SSO を有効にする
という画面が出るのでOffice 365
で検索します。Microsoft Office 365
というアプリが見つかるのでそれをクリックして進めます。
ステップ2 Google IdP 情報
という画面が表示されるとGoogleをIdPとして利用するために必要な情報が表示されますので下記の3つの情報を記録しておきます。
公式ドキュメントの手順 2で、ここからはAzure AD側の設定を行います。先のブログエントリーでも書かれているのですが、手順 2についての手順はPowerShellで設定してぐらいのざっくりとしたことしか書かれていません。
やることは次の二点です。
ImmutableId
を設定するManaged
から Federated
に変更して、SAML認証を行う設定をするAzure ADの設定にはPowerShellで接続できる必要があるのでその準備です。
まずはモジュールをインポートします。
Import-Module -Name MSOnline
次にサービスに接続します。管理権限のあるユーザーで接続してください。
PS> Connect-MsolServicePS> Get-MsolDomainName Status Authentication---- ------ --------------example.com Verified Managedexamplecom.onmicrosoft.com Verified Managed
ImmutableId
を設定するGoogleはAzure AD側のユーザーと突き合わせるためにAzure ADのユーザープロパティ ImmutableId
を使用します。
ImmutableId
に使用する値は特に理由がなければメールアドレス (例: `user@example.com`) にします。他でもできるはずですが、Google側が受け入れるのがメールアドレスか姓名ぐらいしかないようです。
Azure AD単体で使っている状態ではユーザーに対して設定されていないので既存のユーザーに関しては何らかの方法で設定してあげます。まあ何らかの方法というか Set-MsolUser
コマンドレットですね。
Set-MsolUser -UserPrincipalName alice@example.com -ImmutableId alice@example.com
Get-MsolUser | ?{ $_.UserPrincipalName.EndsWith("example.com") } | %{ Set-MsolUser -UserPrincipalName $_.UserPrincipalName -ImmutableId $_.UserPrincipalName }
後々ユーザープロビジョニングでGoogle側からやってきたユーザーに関しては自動で設定されます。
Federated
に変更し、SAML認証の設定をするAzure ADの認証モードを Federated
に変更し、SAML関連の設定をおこなうことで認証をAzure AD以外の場所で行うようにします。
設定には Set-MsolDomainAuthentication
コマンドレットを使用して、先ほどのIdP情報を指定します。
# "SSO の URL" と書かれていた項目$ssoUrl = "https://accounts.google.com/o/saml2/idp?idpid=<IdPId>"# "エンティティ ID" と書かれていた項目$entity = "https://accounts.google.com/o/saml2?idpid=<IdPId>"# 対象のドメイン名$domain = "example.com"# 証明書$cert = "ダウンロードした証明書の -----BEGIN CERTIFICATE----- から -----END CERTIFICATE----- までの「間」を改行をなしで一行で"Set-MsolDomainAuthentication -Authentication Federated -DomainName $domain -ActiveLogOnUri $ssoUrl -PassiveLogOnUri $ssoUrl -IssuerUri $entity -LogOffUri $ssoUrl -SigningCertificate $cert -PreferredAuthenticationProtocol SAMLP
PS> Get-MsolDomainName Status Authentication---- ------ --------------example.com Federated Managedexamplecom.onmicrosoft.com Verified Managed
下記のようなエラーが発生した場合には指定したドメインがAzure ADのプライマリドメインとなっていると思うので onmicrosoft.com
や他のドメインにプライマリを一度切り替える必要があります。
Set-MsolDomainAuthentication : You cannot remove this domain as the default domain without replacing it with anotherdefault domain. Use the the Set-MsolDomain cmdlet to set another domain as the default domain before you delete thisdomain.
再びGoogleの管理コンソールに戻ってステップを進めます。
Microsoft Office 365 の基本情報
はそのままで「次へ」で進めます。
サービス プロバイダの詳細
は「署名付き応答」にチェックを入れて「次へ」で進めます。
属性のマッピング
は「IDPEmail」を「基本情報」「メインのメールアドレス」を選択して「次へ」で進めます。
これでアプリの基本設定は完了ですがまだ有効になっていないので動きません。
Microsoft Office 365
アプリを有効化するSAMLアプリを追加しただけでは有効になっていないのでそれを有効化する必要があります。
アプリ
→SAML アプリ
でアプリの一覧から Microsoft Office 365
を選択して、右上の「サービスを編集」をクリックします。
「サービスのステータス」で「オン (すべてのユーザー)」を選択します。
ここまでの設定でAzure ADにGoogle経由でサインインできるようになっているはずでしょう。多分。
Cookieが残っていると挙動が怪しいのでプライベート ブラウジングやIn Private ウィンドウなどでテストするのをオススメします。
- That’s an error.
Error parsing the request, No SAML message present in request That’s all we know.
Azure ADから一度Googleにリダイレクト後、上記のようなエラーメッセージが出て進まない場合には Set-MsolDomainAuthentication
コマンドレットで設定時に -PreferredAuthenticationProtocol SAMLP
が指定されてなく、SAMLではなくWSで認証をかけている可能性があります。
無限サインインループになった場合はAzure AD側に ImmutableId
が設定されていない可能性や設定が間違っている可能性があるのでそちらを確認してください。
サインインできるようになった後はGoogle側からAzure AD側へユーザー情報を同期するためにユーザープロビジョニングの設定を行います。ユーザープロビジョニングによってGoogle側にユーザーを追加するとAzure ADにユーザーを自動で作成するといったことが可能になります。
ユーザープロビジョニングの設定は アプリ
→ SAML アプリ
でアプリの一覧から Microsoft Office 365
を選択し、ユーザー プロビジョニング
を開きます。
「ユーザー プロビジョニングを設定」をクリックするとダイアログが開くので「承認」をクリックします。
Azure AD側でアクセス許可の確認が表示されるので「組織の代理として同意する」にチェックして「承諾」でGoogleの管理コンソールに戻ります。
最後に属性のマッピング設定画面が表示されるので onPremisesImmutableId
を 基本情報 > ユーザー名
にマッピング設定して「次へ」で完了です。
後はしばらく待つとGoogle側のユーザー情報がAzure AD側へと反映され、ユーザーが作成されたりします。
というわけでGoogle (GSuite)をユーザー情報のソースとしてAzure ADを利用できるようになることでOffice 365やAzureに関連したサービス(AzureやAzure DevOps等)などのサインインを一元化できるのでよいのではないでしょうか。
すでにGSuiteを使用している環境ではAzure ADをIdPにするのが怖かったり抵抗がある、めんどくさいということもあるのでこの構成もオススメです。
]]>今まで .NET なアプリがあったりしたのでWindowsなApp Serviceでしたが、.NET Core化して全部Dockerに乗せた結果Windowsで動いているものがなくなってしまった…かなしい。
ちなみに最初はGKEを考えたのですが、Load Balancerが月2,000円ぐらいかかるのでAKSにしました。まあk8sに乗せてればどこかに行くのも簡単でしょうし(慢心)。
しかしYAMLこねこね結構めんどくさかったので、改めてApp Serviceのお手軽さはいいですねと感じたのでした。
]]>ところが設定しても再起動やユーザースイッチすると言語が英語に戻されてしまうという問題が発生しました。この現象自体は割とよくあるようです。
とはいえ不便なので試行錯誤したところ手元の環境ではMoreLocale 2で日本語に設定した後、第二言語設定にEnglishを追加したところ戻らなくなりました。もしこの現象で困っている人は試してみるともしかしたらうまくいくかもしれません。というメモです。
]]>このライブラリは何かというと IDictionary<TKey, TValue>
を操作すると、裏側ではネットワークの先にあるNoSQL的なもの、例えばAzure Storage TableやAmazon DynamoDBなどへ透過的に記録、読み取りを行うものです。
Azure FunctionsやAWS LambdaのようなServerlessアプリ、もしかしたら単なるCLIで小さいツールといったものを作るということはよくありますが、その際にちょっとしたデータを保存したいということも同時によくあります。
例えばFunctionsでWebhookを作ったとして、特定のパラメータに対して最後にアクセスされた時刻を記録する…などなど。
じゃあデータ保存しましょうとなると、ファイルは永続化できる書き込み先はないし、Redisやデータベース的なデータアクセスはサーバーを用意するのもオーバーキル。そうなるとお手軽なのはNoSQL系かとなるのですが、いざSDKを入れて使ってみるとテーブルの設計と呼び出しがめんどくさいものです。
欲しいのは雑に Dictionary<TKey, TValue>
が永続化されてくれるような程度でいいのですがーという気持ちからスタートしています。
IDictionary
ということはブロッキングだし筋があまりよくないのではという疑問はごもっともですが、クラウド内であればレイテンシーも小さいことやそもそも低負荷のアプリであることをこのライブラリは期待しています。
パフォーマンスを真面目に気にする段階になったときは、Asyncメソッドをご利用いただくか、そもそも普通にSDKでアクセスしてください。
使い方はとりあえず Azure Storage Table であれば簡単です。
まずポータルやCLIでストレージアカウントを作成しておきます。
次に KumoDictionary.AzureStorageTable
NuGetパッケージをインストールします。
そしてプログラムの頭で一度だけデフォルトのバックエンドの設定を行います。
using KumoDictionary;using KumoDictionary.Provider;// Set backend provider for Microsoft Azure Storage Tablevar tableName = "MyTestTable";var connectionString = "DefaultEndpointsProtocol=https;AccountName=...";KumoDictionaryAzureStorageTableProvider.UseAsDefault(connectionString, tableName);
あとは KumoDictionary<TValue>
クラスを使うだけです。ちなみにこれは KumoDictionary<string, TValue>
の別名です。
// KumoDictionary を作るときにディクショナリーの名前を付けてあげるvar dict = new KumoDictionary<MyClass>("dictionaryName1");// インデクサで値をセットdict["key1"] = new MyClass { ValueA = 1234 };dict["key2"] = new MyClass { ValueA = 5678 };// インデクサで値を取得Console.WriteLine(dict["key1"].ValueA); // => 1234
つまり最初のProviderの設定以外は普通のDictionaryっぽく扱えるのです(もちろん一部機能は実装されていなかったりはしますが)。
DynamoDBの場合にはテーブルの作成があらかじめ必要ですが、逆を言えばそれぐらいです。
裏側は単純にMessagePackでシリアライズして適当にストアに保存しているだけです。
ただ、値がもしデータストア側がネイティブで対応している型の場合にはそのままそれを使います。例えばAzure Storage TableやDynamoDBはStringを直接扱えるのでそのまま突っ込むことで管理画面や別なツールからもフレンドリーになるのです。
またキーの方も独自のクラスを使えますが、クラスの構造(プロパティやフィールドの名前、型、順番、数)が変更されると壊れるのであまりお勧めはできません。
雰囲気的にはRedis向けのCloudStructuresに近いですし、Redisに値を詰めたい or 詰めるのでよいのであればCloudStructuresを使うのをお勧めします。
これでちょっとしたものを作るときにデータや設定の保存の手助けとなれば幸いです。
]]>Application InsightsにはDependencyテレメトリーというリクエスト中に発行された外部リソースへのアクセスなどを記録する仕組みがあり、.NET向けの一式を入れておけばHTTPリクエストやSQL Serverへの問い合わせが自動で記録されます。
一方、標準で対応してないものをDependencyに出すには自前で何らかの方法で記録してあげる必要があります。MySQL Connector/Netもその例にもれず自動では記録されません。SQL Serverへの問い合わせが記録されるのはSqlClient (SQL Serverクライアント)のイベントを記録しているからであって、それ以外のデータベースドライバーでは記録されないのです。
というわけで、当然MySQLへの問い合わせでもDependencyに表示されてほしくなります。
まずはそもそもApplication Insights上でDependencyとして記録するにはどうすればいいのかというところからです。
ドキュメントを見るとDependencyはテレメトリーの一種で、DependencyTelemetry
というレコードを記録すればよいということになっています。
記録するには DependencyTelemetry
を生成する方法と、TrackDependencyメソッドで記録する方法があり、今回は DependencyTelemetry
を使った記録方法で実装してみます。後者は細かいことはできないもののメソッド呼び出し一発とお手軽です。
実際の手順としては次のようになります。
TelemetryClient
クラスのインスタンスを作るTelemetryClient.StartOperation<T>
メソッドを DependencyTelemetry
型を指定して呼び出すIOperationHolder
のTelemetryのプロパティを設定するTelemetryClient.StopOperation
メソッドを呼ぶ または IOperationHolder.Dispose
メソッドを呼ぶこれをコードにするとこうなります。
// TelemetryClientはスレッドセーフなので使いまわせるvar telemetryClient = new TelemetryClient();using (var operation = telemetryClient.StartOperation<DependencyTelemetry>("DependencyName")){ var telemetry = operation.Telemetry; telemetry.Type = "ResourceType"; // Dependencyの種類(Http, SQLなど) telemetry.Target = "リクエスト送信先"; // エンドポイントのホスト名とか telemetry.Data = "何か生データー"; // SQLとか // 何か時間のかかる処理... await Task.Delay(1000 * 3);}
難しいことはないですね。これでApplication Insightsに記録する方法はなんとなく理解できました。
Dependencyとして記録する方法がわかったので次はMySQLへの問い合わせを記録する方法です。
今回はMySQLドライバーはMySQL公式のConnector/Netを利用していて、単純にSQLのクエリを記録したいと考えていますが、それにはそのクエリのタイミングをつかむ必要があります。そこで少し調べてみると、Connector/NetにはInterceptorというExecute系メソッドに割り込んでSQLのロギングなどが行える仕組みがあったのでそれを利用します。
InterceptorにはあらかじめExecute系メソッドに割り込むためのベースクラスである CommandInterceptorBase
クラスがあるので、このクラスを継承して各種メソッドをオーバーライドします。そしてここではオーバーライドしたメソッドでDependencyの記録を行えば良さそうというわけです。
実際に実装した例はこんな感じになります。DependencyのTargetやNameといった値の設定はSQL Serverでの記録と同じような形にしました。
using System;using System.Collections.Generic;using System.Data;using System.Text;using Microsoft.ApplicationInsights;using Microsoft.ApplicationInsights.DataContracts;using MySql.Data.MySqlClient;namespace WebApplication2.Diagnostics{ public class ApplicationInsightsBaseCommandInterceptor : BaseCommandInterceptor { private TelemetryClient _telemetryClient = new TelemetryClient(); private string _name; public override bool ExecuteNonQuery(string sql, ref int returnValue) { using (var operation = _telemetryClient.StartOperation<DependencyTelemetry>(_name)) { var telemetry = operation.Telemetry; telemetry.Type = "SQL"; telemetry.Data = sql; telemetry.Target = _name; return base.ExecuteNonQuery(sql, ref returnValue); } } public override bool ExecuteReader(string sql, CommandBehavior behavior, ref MySqlDataReader returnValue) { using (var operation = _telemetryClient.StartOperation<DependencyTelemetry>(_name)) { var telemetry = operation.Telemetry; telemetry.Type = "SQL"; telemetry.Data = sql; telemetry.Target = _name; return base.ExecuteReader(sql, behavior, ref returnValue); } } public override bool ExecuteScalar(string sql, ref object returnValue) { using (var operation = _telemetryClient.StartOperation<DependencyTelemetry>(_name)) { var telemetry = operation.Telemetry; telemetry.Type = "SQL"; telemetry.Data = sql; telemetry.Target = _name; return base.ExecuteScalar(sql, ref returnValue); } } public override void Init(MySqlConnection connection) { _name = String.Format("{0} | {1}", connection.DataSource, connection.Database); base.Init(connection); } }}
Interceptorを実装したら最後にアプリケーションの設定でデータベース接続文字列に commandinterceptors
パラメータを追加して読み込ませます。パラメータの値は<CommandInterceptorClass>,<Assembly>
というフォーマットで、次のようなものになります。
commandinterceptors=WebApplication2.Diagnostics.ApplicationInsightsBaseCommandInterceptor,WebApplication2
そしてこれを有効にした状態でアプリケーションを実行するとApplication Insightsに記録されます。もちろんAzureに接続していない場合でもVisual Studioで確認できます。
この例では問い合わせが1ms以下なので全く面白くなくて残念ですが、ともあれこれで取れるようになったので無いよりは全然よさそうです。
]]>今回やりたいこととしては「認証機構は基本的に一つで、あるアクションの時だけ認証中に追加で特殊な処理を行いたい」というパターンです。例えばコントローラーに共通のAuthorizeを付けているが、一部特殊なアクションでは追加の処理をしたいというケースです。
そもそも認証ハンドラーはASP.NET Coreの一部であってMVCとは切り離されたものなので、操作可能なものは主にHttpContextとなります。つまりASP.NET Core MVCが認識しているアクションであるとかコントローラーであるとかを取り出すことは難しいため、アクションに付けた属性を引いてくるといったことはできないという悩みがあります。
そこで最初に考えたのは、二つの認証スキームとして登録してそれぞれで利用する認証スキームを変更するという方法です。
まず次のようなオプションを持つAuthenticationHandlerを用意します。
public class CustomAuthenticationSchemeOptions : AuthenticationSchemeOptions{ public bool EnableNanika { get; set; }}public class CustomAuthenticationHandler : AuthenticationHandler<CustomAuthenticationSchemeOptions>{ public CustomAuthenticationHandler(IOptionsMonitor<CustomAuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) { } protected override Task<AuthenticateResult> HandleAuthenticateAsync() { if (Options.EnableNanika) { // 一部で必要な特殊な処理... } return Task.FromResult(AuthenticateResult.Fail("Unauthorized")); } protected override Task HandleChallengeAsync(AuthenticationProperties properties) { Response.Headers["WWW-Authenticate"] = "Nanika"; return base.HandleChallengeAsync(properties); }}
このハンドラーをStartupで二つの認証スキームとして登録します。
services.AddAuthentication() .AddScheme<CustomAuthenticationSchemeOptions, CustomAuthenticationHandler>("Custom1", options => { options.EnableNanika = false; }) .AddScheme<CustomAuthenticationSchemeOptions, CustomAuthenticationHandler>("Custom2", options => { options.EnableNanika = true; });
コントローラーにAuthorize属性を付け、その際にコントローラーとアクションで認証スキームを別々に設定します。
[Route("api/[controller]")][Authorize(AuthenticationSchemes = "Custom1")]public class ValuesController : Controller{ // GET api/values [HttpGet] [Authorize(AuthenticationSchemes = "Custom2")] public IEnumerable<string> Get() { return new string[] { "value1", "value2" }; }}
これでどうかと思ったのですが、この指定で実際に動かすとコントローラーの指定の方が勝つことになります。当然ですがコントローラーにAuthorizeを付けずにアクションごと個別につけると期待通りに動作します。
認証はフィルターとしても実装できるので(Authorizeも実体はMVCのフィルターで、中で認証ハンドラーを呼び出している)、それをかけてみるのはどうかという風になります。
public class CustomAuthorize : Attribute, IAsyncAuthorizationFilter{ public bool EnableNanika { get; set; } public Task OnAuthorizationAsync(AuthorizationFilterContext context) { // ここで認証処理をあれこれやる return Task.CompletedTask; }}
この場合にはコントローラーとアクション両方に指定すると二つのフィルターが呼ばれることになる(=二回処理が走る)ので都合が悪い感じになります。それに加えてAuthorize属性と同じようなことを書いてあげる必要があり面倒です。
認証はフィルターとして実装できるという点を利用して、認証自体は行わずリクエストのデータにフラグを立てておき、認証ハンドラーで取り出して何とかする方法があります。その方法であれば 自前のフィルター(ASP.NET Core MVC) → 自前の認証ハンドラー (ASP.NET Core) という形にできます。
まずアクションにつけるフィルターを作ります。
public class UseCustomAuthenticationWithNanikaAttribute : Attribute, IAsyncAuthorizationFilter, IOrderedFilter{ public int Order => int.MinValue; public Task OnAuthorizationAsync(AuthorizationFilterContext context) { context.HttpContext.Features.Set<IUseCustomAuthenticationWithNanikaFeature>(new UseCustomAuthenticationWithNanikaFeature { Enable = true }); return Task.CompletedTask; }}public interface IUseCustomAuthenticationWithNanikaFeature{ bool Enable { get; set; }}public class UseCustomAuthenticationWithNanikaFeature : IUseCustomAuthenticationWithNanikaFeature{ public bool Enable { get; set; }}
このフィルターはOnAuthorizationAsyncでHttpContextのFeatures(リクエスト単位の機能を入れておけるもの)にフラグ情報を持つFeatureを保存します。IOrderedFilterを実装しているのはAuthorize属性より先に来てほしいためです。
次に認証ハンドラーを作ります。このハンドラーの中でFeaturesから先ほどのフィルターでつけていたFeatureを引っ張り出します。
public class CustomAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>{ public CustomAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) { } protected override Task<AuthenticateResult> HandleAuthenticateAsync() { var enableNanika = Context.Features.Get<IUseCustomAuthenticationWithNanikaFeature>()?.Enable ?? false; if (enableNanika) { // 一部で必要な特殊な処理... } return Task.FromResult(AuthenticateResult.Fail("Unauthorized")); } protected override Task HandleChallengeAsync(AuthenticationProperties properties) { Response.Headers["WWW-Authenticate"] = "Nanika"; return base.HandleChallengeAsync(properties); }}
Startupで認証ハンドラーを登録します。この際、AddAuthenticationでデフォルト認証スキームを登録しないように注意します。デフォルトの認証スキームとなるハンドラーは順番が特別なことになるので意図した挙動になりません。
services.AddAuthentication() .AddScheme<AuthenticationSchemeOptions, CustomAuthenticationHandler>("Custom", options => { });
そしてコントローラーにはAuthorize属性を、特別な処理をしたいアクションにはUseCustomAuthenticationWithNanika属性でフィルターを付けます。
[Route("api/[controller]")][Authorize(AuthenticationSchemes = "Custom")]public class ValuesController : Controller{ // GET api/values [HttpGet] [UseCustomAuthenticationWithNanika] public IEnumerable<string> Get() { return new string[] { "value1", "value2" }; } // GET api/values/5 [HttpGet("{id}")] public string Get(int id) { return "value"; }}
これでアクションに属性を指定するだけで認証時の挙動を少し変更するということが可能になります。
]]>製品仕様はキャンペーンサイトに書いてあり、Android 7.0であることが明記されています。
端末自体はHuaweiです。この手のセットトップボックスではHuawei製はよくあるようです(たしかauやdocomoのものもそうだったはず)。
さて、Androidに関してですがよく見ると公式サイトの製品仕様にはAndroid 7.0とはあるもののAndroid TVとは書いていないのでもしかしたら…と思っていたのですがセットアップし始めるとやはりAndroid TVではなく単なるAndroidです。
そもそもセットアップステップがAndroid TV風でなくU-NEXTのログインが必須であったり、オンスクリーンキーボードもAndroid TVのそれではない、ホーム画面は独自というあたりでお察しです。
製品仕様によればアプリも利用できることになっているのですがAndroidがベースになっているだけでGoogleサービスが入っていないのでGoogle Playは使えません。「U-NEXTサービス、HDMI-CEC対応、Player for YouTube、Androidなど」とあるので使えるのかと思いきや。
YouTubeを観れそうな雰囲気ですがGoogle Play入ってないのにどうやって?と思ったらHuaweiの独自アプリでした。oh…。
現状U-NEXT用のアプリストアがあるわけでもないのでプレインストールのアプリのみ利用できます。なお設定のアプリ管理からアプリをアンインストールできるのですが、それらはインストールする口がないので消したら最後です(これはひどい。
参考までにほぼ同型機のdocomoのドコモテレビターミナルは説明書を見てもわかるのですが正真正銘のAndroid TVでロゴも表記されています。
とはいえAndroidなのでUSBデバッグとかできればなんかいろいろできるかもということで探すとちゃんとUSBデバッグはあります。
「アプリ」→「端末設定」→「情報」→「開発者オプション」→「USBデバッグ」で有効になるので有効にして、USBケーブルをつなぐと許可するかどうかのダイアログが表示されます。ちなみにUSBケーブルはType-Aなので注意が必要です。
接続後はいつものようにadbでコマンドを実行できshellにも入れます。そこで早速apkをインストールしてみようとすると次のようなエラーが発生しました。
adb -s 0123456789 install "app-release.apk"adb: failed to install app-release.apk: Failure [INSTALL_FAILED_UPDATE_INCOMPATIBLE: Verify signature failed]
どうも署名チェックのようなものがあり、そこで引っかかっているようです。
提供元不明のアプリのインストールを許可する必要があるのか?と思ったので設定画面を開いてセキュリティから許可してみましたがダメです。Webを検索してもそれらしい情報はないので端末固有でロックをかけていそうです。うーん。
なお、設定画面は am コマンドを使えば開けます。
HWM380:/ $ am start -a android.intent.action.MAIN -n com.android.settings/.Settings
ちなみにバージョン情報はこんな感じです。
もう少し探ってみたところ /system/etc/security/apk_sign_white_list.xml
というファイルがあり、署名のホワイトリストのようなものだったのでユーザーが任意にインストールするのは無理そうですね(HuaweiやU-NEXTだけが含まれている)。
一応 pm list package
の結果、パッケージの一覧はこんな感じでした。
残念ながらAndroid端末としてはいじりがいがないのでそこを期待してはダメそうです。とはいえ特定サービス向けの装置なのでそこはしょうがないかなーという気もします。
U-NEXTのSTBとしてはきびきび動いたりするので普通に使う分には割とよいのではないかとは思いますが、実際お金を出す場合半額の4,980円でもどうかなー…って感じがしますね。リテラシがなくてつなげば使えるみたいなのを求めていない限り、正直縛りのないAndroid TVであるAirStick 4Kの方がよさそう。
そうそう、ログイン必須なので退会すると完全に使い物にならなくなります。
]]>SECRET
のようなApp Centerが許可しないキーワードを含んだ環境変数を定義している場合、そのままの名前で露出しないようになっているようです。PASSWORD
のような単語は通るので SECRET
のみの制限かもしれません(ドキュメントに特に書いてない)。
ユーザー定義の環境変数は USER-DEFINED_
というプレフィックスがついた環境変数も定義されるのでそちらから取得する方法もありますが、ハイフンを含んでいるため参照も定義も一筋縄ではいかないのでお勧めできません。
API_KEY
や NUGET_USERNAME
のような環境変数を定義してみたところ特に問題なく取得できたので、もしかして変数名に問題があるのでは?と思いいくつかのパターンを試してみました。すると以下のパターンで違いが出ることがわかりました。
この定義を設定した上でビルドし、pre-buildスクリプトで環境変数をすべて出力すると…
##[section]Starting: Pre Build Script==============================================================================Task : Shell ScriptDescription : Run a shell script using bashVersion : 2.1.3Author : Microsoft CorporationHelp : [More Information](https://go.microsoft.com/fwlink/?LinkID=613738)==============================================================================[command]/bin/bash /Users/vsts/agent/2.127.0/work/1/s/app/appcenter-pre-build.sh...snip...SECRE__HAUHAU=hauhau3...snip...USER-DEFINED_SEECRET_HAUHAU=hauhau2...snip...USER-DEFINED_SECRE__HAUHAU=hauhau3...snip...USER-DEFINED_SECRET_HAUHAU=hauhau1...snip...SEECRET_HAUHAU=hauhau2...snip...
…となり、SECRET_HAUHAU
という変数のみそのまま露出しないようになっています。この結果からApp Centerは SECRET
を含む環境変数をそのまま変数として露出しないようにしているようです。セキュリティのためでしょうか。
というわけで特別必要がなければ SECRET
という名前を含めないでおくというのがハマらないポイントです。
2018年2月時点では Build scripts | Microsoft Doc にある通り、以下のスクリプトを認識します(for UWPなら.ps1)。
ここまでは書いてある通りなのですが、実際リポジトリにビルドスクリプトを含めても一向に実行されないという問題が発生しました。設定画面を見ると Build scripts: ✔ Post-clone
と表示され認識はされているようでした。
新しいブランチを作ってビルド設定を追加して試すと問題なく動作したので悩んだのですが、実は Build scripts: ✔ Post-clone
となった後に Save
もしくは Save & Build
で設定を保存する必要があるようです。
逆を言うと、ビルドスクリプトを削除した後も保存しなおさないとビルドがエラーになります。
新しいブランチを作った時に動いたのは、すでにビルドスクリプトが含まれていて、ビルド定義を作る際に Build scripts: ✔ Post-clone
となった状態だったからということでした。
ふとTiarraでとったIRCのログをBigQueryに流し込むようにしたら手元にログを長期間残す必要もないし、検索もできるし便利そうだなと思ったので設定してみました。Twitterのログもあって若干流れますがBigQuery的には誤差の範囲レベル(1日1MB程度)なのでStreaming Insertして、たまーに検索しても無料枠で余裕で収まりそうです。
主な流れはTiarraで吐いたテキストログをfluentdのin_tailで読んで、fluent-plugin-bigqueryでBigQueryに流し込むという何の変哲もない形です。
ですでに手元のTiarraはDocker(docker-compose)で起動しているのでfluentdもDockerで起動しています。fluent-plugin-bigqueryはfluentdの公式イメージに追加する必要があるので適当に追加します。
FROM fluent/fluentdRUN apk --no-cache --update add ruby-bigdecimal ruby-dev build-base && \ fluent-gem install fluent-plugin-bigquery
ruby-bigdecimalが必要ということはREADMEにあるのですが、ruby-devとbuild-baseも必要で追加していないとfluent-plugin-bigqueryをインストール中にstrptimeのビルドで失敗します。
次にBigQueryのスキーマを用意します。今回は次のようなものになっています。
[ { "name": "Time", "type": "TIMESTAMP" }, { "name": "User", "type": "STRING" }, { "name": "Server", "type": "STRING" }, { "name": "Room", "type": "STRING" }, { "name": "Text", "type": "STRING" }]
テーブル自体はこのスキーマを使ってfluent-plugin-bigqueryに自動生成させるので、BigQuery側にはプロジェクトとデータセットだけ作っておきます。このスキーマファイルは次のfluent.confと同じところにおいてください(最終的にdockerでマウントされるところ)。
最後にfluentdの設定です。
source(入力)はssig33 - Tiarra のログを fluentd で流すを参考にin_tailを設定します。手元ではTIGのログが流れてくるのとサーバー名を分解するために少し変更しています。ログはローテートされるので %Y.%m.%d.txt で。
出力はfluent-plugin-bigqueryの設定を auto_create_table
で設定します。これ自体は何のことはない設定なのですが table を年で分けるようにしようとしたところでハマりました。
table にはプレースホルダ文字列を使えることになっているのですが、文字列置換が有効になる条件としてまず buffer time/timekey を設定しているというものがあります。そこで timekey を設定すると %Y
ではなく %d
精度のプレースホルダを持つ設定を要求します(参考: output.rb)。しかし今回は%Yでいいのに…困った…と考えた末に fetch_schema_table
という使っていない設定項目に log_%Y%m%d
入れて回避しました。
とはいえ真面目にBigQueryを使うレベルなら日別にすると思うので(扱いづらいし…)罠にははまらないとは思います。日付パーティションにしてもいいかもとか頭をよぎりますが量も大してないので最悪あとで何とかすればいいでしょう(適当。
<source> @type tail path /fluentd/log/tig/tig-twitter/%Y.%m.%d.txt,/fluentd/log/ircnet/*/%Y.%m.%d.txt,/fluentd/log/freenode/*/%Y.%m.%d.txt tag tiarra format /^(?<time>[^ ]+)\ (<(?<room>[^ ]*)@(?<server>[^ ]*):(?<user>[^ ]*)>|\((?<room>[^ ]*)@(?<server>[^ ]*):(?<user>[^ ]*)\)|>(?<room>[^ ]*)@(?<server>[^ ]*):(?<user>[^ ]*)<|-(?<user>[^ ]*)-|>(?<user>[^ ]*)@[^ ]*<)\ (?<text>.*?)(?: \u0003\d+\([^)]+\).*)?$/ time_format %H:%M:%S</source><match tiarra> @type bigquery method insert auth_method json_key json_key /fluentd/etc/serviceaccount_key.json project <YourProjectName> dataset <YourDataSetName> table Log_%Y auto_create_table true schema_path /fluentd/etc/schema.json # table でplaceholderを使うために必要 # buffer time/timekeyの設定が必要でそのためにplaceholderの%dをもつ設定が必要なので使わない項目をダミーとして使う… fetch_schema_table log_%Y%m%d <buffer time> timekey 1d </buffer> <inject> time_key time time_type string time_format %s </inject></match>
あとはこれでfluent/fluentd-docker-imageを参考にDockerでマウントしつつ起動すればログがBigQueryに良しなに送られます。便利。
]]>