Marp 使う時に見るもの
Marp を使ってスライドを作ってみる
三宅さんに教えてもらったものを使ってみました。
— k-miyake (@kazuyukimiyake) July 21, 2019
Marp というものですが、調べてて気を付けないといけないなと思ったのは最近作り直されたみたいなので、何か調べるときは新しいほうを当たるようにしたほうがよさそうです。 各種機能はこちらから。
ちょっと見た目カスタマイズしたい!
テーマ作ることも出来るみたいなのですが、まだ VSCode の拡張機能のテーマ対応は絶賛作り中みたいです。なので、今回は style で指定する方法でやってみました。
スライド作ってると、表紙・セクションタイトル・普通のスライド・背表紙とか何個かあると思います。私が普段仕事で使ってるスライドは、以下のような見た目です。
こんな感じのスライドを markdown で書けたら捗りそう!!ということでやってみました。
style
カスタマイズは簡単です。theme を一番癖が無さそうな base にして、 style で CSS 当てていきます。
section と h1 とか footer とかにスタイルを当てるだけでそれっぽくなります。
--- marp: true theme: base style: | h1, h2, h3, h4, h5, header, footer { color: black; } section { background-color: white; font-family: 'Yu Gothic UI'; color: black; } ---#表紙!!  2019/xx/xx Kazuki Ota ---#セクション見出し ---#ほんぶん -ディオォォオオーーッ -君が -泣くまで -殴るのをやめないッ! ---  <!-- footer: © Copyright Micorsoft Corporation All rights reserved. -->
これで、こんな感じになります。背景画像はパワーポイントのスライドマスターから取り出したい画像を選択した状態で画像として保存で png にして取り出しました。
さて、あとは一部のページだけ背景を青にしたい。そしてフォントは白にしたいという感じです。そんなときは該当ページのところに
<!-- _class: blue -->
みたいなものを書くと、そのページの section タグの class に blue がつくみたいです。最初のアンダーバーがあるのが、そのページだけという印です。 ということで、スタイルの部分に以下のようなものを付け足します。
section.blue{background-color: #0078D7; color: white; }section.blue>h1,h2,h3,h4,h5,header,footer{color: white; }
そして、青くしたいページにコメントを差し込みます。markdown ファイルの全体はこんな感じになりました。
--- marp: true theme: base style: | h1, h2, h3, h4, h5, header, footer { color: black; } section { background-color: white; font-family: 'Yu Gothic UI'; color: black; } section.blue { background-color: #0078D7; color: white; } section.blue > h1, h2, h3, h4, h5, header, footer{ color: white; } ---<!-- _class: blue -->#表紙!!  2019/xx/xx Kazuki Ota ---<!-- _class: blue -->#セクション見出し ---#ほんぶん -ディオォォオオーーッ -君が -泣くまで -殴るのをやめないッ! ---  <!-- _class: bluefooter: © Copyright Micorsoft Corporation All rights reserved. -->
見た目はこんな感じ。
許容範囲の見た目かな。
ローカルファイルの画像込みで Export
デフォルトではセキュリティ上の理由でローカルファイルの画像をエクスポート時に見てくれないみたい。VSCode の拡張機能的にはローカルファイルを入れるようにするオプションが見当たらなかった。 なので、以下のコマンドで marp cli をインストールして
npm install -g @marp-team/marp-cli
そして、こんなコマンドで出力します。
# PDF の場合 marp --pdf --allow-local-files test.md # PPTX の場合 marp --pptx --allow-local-files test.md
試しに PPTX にしてみました。
完璧!!
WPF の TreeView で任意の項目が表示されるようにスクロールする
というネタを見つけたのでやってみます。久しぶりの WPF ネタ!因みにせっかくなので .NET Core 3.0 Preview 7 で VS2019 Preview 使ってやってみます。
表示用データはこれ!
using System; using System.Collections; using System.Collections.Generic; using System.Text; namespace TreeViewScrollSample { publicclass Person { publicstring Name { get; set; } public IEnumerable<Person> Children { get; set; } } }
画面が側はこんな感じでいきましょう。
<Windowx:Class="TreeViewScrollSample.MainWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:d="http://schemas.microsoft.com/expression/blend/2008"xmlns:local="clr-namespace:TreeViewScrollSample"xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"Title="MainWindow"Width="800"Height="450"mc:Ignorable="d"><Grid><Grid.RowDefinitions><RowDefinition Height="Auto" /><RowDefinition /></Grid.RowDefinitions><Button Click="ScrollButton_Click"Content="Scroll" /><TreeView x:Name="treeView"Grid.Row="1"><TreeView.ItemTemplate><HierarchicalDataTemplate ItemsSource="{Binding Children}"><TextBlock Text="{Binding Name}" /></HierarchicalDataTemplate></TreeView.ItemTemplate></TreeView></Grid></Window>
これで、こんな感じでコードを書けばスクロールします。
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; namespace TreeViewScrollSample { /// <summary>/// Interaction logic for MainWindow.xaml/// </summary>publicpartialclass MainWindow : Window { private IEnumerable<Person> People { get; } = Enumerable.Range(1, 100) .Select(x => new Person { Name = $"Tanaka {x}", Children = Enumerable.Range(1, 10) .Select(y => new Person { Name = $"Kimura {x}-{y}", }) .ToArray(), }) .ToArray(); public MainWindow() { InitializeComponent(); treeView.ItemsSource = People; } private async void ScrollButton_Click(object sender, RoutedEventArgs e) { // ContainersGenerated になるまで待つ Task waitUntilContainersGenerated(TreeViewItem container) { var tcs = new TaskCompletionSource<object>(container); void statusChanged(object _, EventArgs __) { if (container.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated) { tcs.SetResult(null); container.ItemContainerGenerator.StatusChanged -= statusChanged; return; } } container.ItemContainerGenerator.StatusChanged += statusChanged; return tcs.Task; } // 一番最後の最後の要素にスクロールする予定 var parent = People.Last(); var target = parent.Children.Last(); // ツリービュー直下の最後の要素のTreeViewItemを取得 var container = (TreeViewItem)treeView.ItemContainerGenerator.ContainerFromItem(parent); // 開く container.IsExpanded = true; // 子の ItemContainerGenerator のステータスが Generated になるまで待つif (container.ItemContainerGenerator.Status != GeneratorStatus.ContainersGenerated) { await waitUntilContainersGenerated(container); } // スクロール先を取得してスクロール var targetContainer = (TreeViewItem)container.ItemContainerGenerator.ContainerFromItem(target); targetContainer.BringIntoView(); } } }
実行すると…
動いた!!
ソースコードは GitHub に上げておきました。
ASP.NET WebForm に Vue.js を入れてみよう
Vue.js は Progressive なので出来るでしょう。
プロジェクトの作成
ASP.NET WebForm のプロジェクトを作ります。凄く久しぶりなのですが、普通に ASP.NET Web Application のプロジェクトテンプレートの中の選択肢にありました。
Scripts フォルダーに vue.js をコピーして下準備完了です。
Default.aspx の変更
このページは Vue.js で作る!という意気込みで行くなら簡単です。Scripts フォルダーに default.js を追加して、以下のような感じで Default.aspx を編集します。
<%@ Page Title="Home Page"Language="C#" MasterPageFile="~/Site.Master" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="VueWebForm._Default" %><asp:ContentID="BodyContent" ContentPlaceHolderID="MainContent" runat="server"><divid="app"><h1>Vue.js app</h1><inputtype="text" v-model="input" /><p>{{ output }}</p></div><scripttype="text/javascript"src="Scripts/vue.js"></script><scripttype="text/javascript"src="Scripts/default.js"></script></asp:Content>
そして default.js に好きなように Vue.js 書いていきます。
const vm = new Vue({ el: '#app', data: function () {return{ input: ''}; }, computed: { output: function () {returnthis.input.toUpperCase(); }}});
動いた。
注意点
ASP.NET WebForm はページ自身に POST メソッドを投げるというポストバックを通じてページの状態の差分などからイベントを発火したり色々します。 ASP.NET WebForm のコントロールを使ってると、このときに TextBox の入力値とかは保持されるのですが Vue.js の世界からすると単純な再表示できれいさっぱりリセットされます。
なので以下のように WebForm のコントロールのあるページの一部が Vue.js だとすると…
<%@ Page Title="Home Page"Language="C#" MasterPageFile="~/Site.Master" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="VueWebForm._Default" %><asp:ContentID="BodyContent" ContentPlaceHolderID="MainContent" runat="server"><asp:TextBox runat="server"ID="Input" /><asp:Button runat="server"ID="Button"Text="Click"OnClick="Button_Click" /><divid="app"><h1>Vue.js app</h1><inputtype="text" v-model="input" /><p>{{ output }}</p></div><scripttype="text/javascript"src="Scripts/vue.js"></script><scripttype="text/javascript"src="Scripts/default.js"></script></asp:Content>
実行して適当に入力して…
ASP.NET WebForms のボタンコントロールのボタンを押すと…
こんな感じで消えてしまいます。注意すべきなのは TextBox のテキスト変更イベントとかを使ってたら、それでもポストバックが走って Vue.js の画面がリフレッシュされてしまうということです。
ただ、input タグに name を付けてれば、ASP.NET WebForm のポストバックイベントの中で値を参照することは出来ます。 例えば以下のように vueinput という名前を input タグにつけておいて
<%@ Page Title="Home Page"Language="C#" MasterPageFile="~/Site.Master" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="VueWebForm._Default" %><asp:ContentID="BodyContent" ContentPlaceHolderID="MainContent" runat="server"><asp:TextBox runat="server"ID="Input" /><asp:Button runat="server"ID="Button"Text="Click"OnClick="Button_Click" /><divid="app"><h1>Vue.js app</h1><inputtype="text" v-model="input" /><p>{{ output }}</p><inputname="vueinput"type="hidden" :value="output" /></div><scripttype="text/javascript"src="Scripts/vue.js"></script><scripttype="text/javascript"src="Scripts/default.js"></script></asp:Content>
ボタンのクリックイベントで値をとることができます。
protectedvoid Button_Click(object sender, EventArgs e) { var input = Request.Form["vueinput"]; Input.Text = $"Hello from {input}"; }
実行して適当に入力して…
ボタンを押すと Vue.js で大文字変換した結果がちゃんと ASP.NET WebForm のほうの TextBox に出てることが確認できます。
まとめ
ASP.NET WebForms アプリでも単一ページ全体を Vue.js 化することは割と簡単に出来そう。 でも、ほかのページとステートを共有してとかなんだかんだしてるとめんどくさそうな気がします。
1 ページ内に Vue.js と WebForm が混ざってるとかなりめんどくさそうなので混ぜないのが吉な気がする…。
ということで、WebForms に完全新規ページで、割と独立性の高い機能を追加するのであれば Vue.js を入れることは出来なくはないと思いますが、継続的同じチームで見ていくようなシステムでみんなの合意が取れてる状態じゃないと、メンテナンスで引き取った人がかわいそうな雰囲気を感じました。
ReactiveProperty v6.0.2 をリリースしました
しました。
Release v6.0.2 · runceel/ReactiveProperty · GitHub
メジャーバージョンが上がってます
ということで、1 つ破壊的変更があります。
これまで WPF では EventToReactiveCommand
と EventToReactiveProperty
を使うのに Blend SDK のアセンブリの Behavior を使用していました。
この Blend SDK は Visual Studio 2019 から同梱されなくなっていて、公式の NuGet パッケージもない状態になりました。そのため、それを置き換える OSS の Behavior のライブラリーである XAML Behaviors for WPF を使用するようにしました。
そのため、クラス名は同じですが参照元アセンブリや、名前空間が変わっているため、この機能を使用している場合にはコードの変更が必要になります。
更新手順
以下の手順で更新可能です。
- ReactiveProperty を v6 以上に更新する
- Blend SDK の参照を消す(
System.Windows.Interactivity
とMicrosoft.Expression.Intaractions
) - XAML 内の
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
をxmlns:i="http://schemas.microsoft.com/xaml/behaviors"
に変更する xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
がある場合もxmlns:i="http://schemas.microsoft.com/xaml/behaviors"
に置き換える
小さな新機能
INotifyPropertyChanged
インターフェースを実装したクラスの変更通知機能を持ったクラスのプロパティから ReactiveProperty
を生成して同期をとるための ToReactivePropertyAsSynchronized
のソースからターゲット、ターゲットからソースへの変換ロジックを Rx を使って書けるオーバーロードが追加されています。
例えば ReactivePropertySlim<int>
と ReactiveProperty<string>
で値を変換しつつ同期したいケースで、ReactiveProperty<string>
が空文字のときは 0 にして、それ以外のケースでは数字としてパース出来るなら ReactivePropertySlim<int>
に書き戻したいとします。こんな感じになります。
publicclass MainWindowViewModel { public ReactivePropertySlim<int> Source { get; } = new ReactivePropertySlim<int>(); public ReactiveProperty<string> Target { get; } public MainWindowViewModel() { Target = Source.ToReactivePropertyAsSynchronized(x => x.Value, convert: ox => ox.Select(x => x.ToString()), // int -> string convertBack: ox => Observable.Merge( ox.Where(x => string.IsNullOrEmpty(x)).Select(_ => 0), // 空文字は 0 ox.Where(x => int.TryParse(x, out _)).Select(x => int.Parse(x)))); // それ以外で数字に変換可能だったら数字にする } }
画面と適当にバインドすると以下のように動きます。Target プロパティを TextBox にバインドして、Source プロパティを TextBlock にバインドしています。
これまでは ReactiveProperty
にバリデーションエラーがある場合のみソースへの変更の反映をスキップするという形しかできませんでしたが、Rx を間に差し込めるようになったので Where などで自由にフィルタリングが出来ます。用途に応じてお使いください。
WPF on .NET Core 3.0 対応
これまでも WPF on .NET Core 3.0 に普通に NuGet から導入可能でしたが、EventToReactiveProperty
や EventToReactiveCommand
は利用できませんでした。
このバージョンから .NET Core 3.0 でも、これらの機能が使えるようにしました。
ReactiveProperty v6 以降と .NET Core 3.0 の WPF のプロジェクトに追加して、Microsoft.Xaml.Behaviors.Wpf パッケージを追加することで使えるようになります。 注意点として、Microsoft.Xaml.Behaviors.Wpf パッケージは、まだ .NET Core 対応のパッケージが出ていないため警告がでます。各自の判断で NU1701 の警告を抑止してお使いください。
Marp で上詰めにする方法
Marp でスライド作るのを色々試してるのですが、全体的にコンテンツを上下方向で中央寄せになってるのが気になりました。
なんとなく上からコンテンツを詰めていくようにしてほしい。ということで section
タグに以下のようなスタイルを当てることでパワポのように上からコンテンツを配置するようになりました。
section{ justify-content: start; }
元のように中央寄せにしたい場合は justify-content: center;
にすれば OK です。
Durable Functions の Entity のクラススタイルプログラミングモデルを試してみよう
Durable Functions の Entity のプログラミング体験が割と辛いのですがクラススタイルのプログラミングモデルを使うと既存のプログラミングと同じような感じでいけて素敵です!
クラススタイルのプログラミングモデルは、2019/08/05 時点では Durable Functions v2.0.0-beta1 で試すことが出来ます。
試してみよう!
早速 Azure Functions のプロジェクトを作ります。そして Microsoft.Azure.WebJobs.Extensions.DurableTask
の 2.0.0-beta1 を追加します。
クラススタイルのエンテティは、普通のクラス!違う点があるとすると、クラス名の FunctionName
属性のついた EntityTrigger
の関数があるところですね。
using Microsoft.Azure.WebJobs; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using System; using System.Threading.Tasks; namespace FunctionApp1 { publicinterface IPerson { void SetName(string name); Task CurseAsync(); } publicclass Person : IPerson { [JsonProperty("name")] publicstring Name { get; set; } [JsonProperty("age")] publicint Age { get; set; } publicvoid SetName(string name) => Name = name; // 適当な時間経過後に加齢する呪いpublic async Task CurseAsync() { var r = new Random(); await Task.Delay(TimeSpan.FromSeconds(r.Next(10))); Age += r.Next(10); } [FunctionName(nameof(Person))] publicstatic Task Run([EntityTrigger] IDurableEntityContext ctx) => ctx.DispatchAsync<Person>(); } }
この EntityTrigger
のついた関数の DispatchAsync
が凄くいい仕事します。Person クラスの関数をいい感じに呼び出してくれます。
HttpTrigger などから呼び出してみよう
HttpTrigger などから Entity を使うには、IDurableOrchestrationClient#SignalEntityAsync
でメソッドを呼び出して、IDurableOrchestrationClient#ReadEntityStateAsync
で Entity のステータスを取得します。ステータスは、先ほど定義した Person クラスのプロパティに値が入ってる感じのものが返ってきます。
例えば…適当に SetName を呼んでステータスを読み取ってみましょう。
[FunctionName("CreatePerson")] publicstatic async Task<IActionResult> CreatePersonAsync( [HttpTrigger(AuthorizationLevel.Function, "get", "post")]HttpRequest req, [OrchestrationClient] IDurableOrchestrationClient client) { var id = CreateEntityIdFrom(req); if (!id.HasValue) { returnnew BadRequestResult(); } await client.SignalEntityAsync<IPerson>(id.Value, proxy => proxy.SetName(id.Value.EntityKey)); returnnew OkObjectResult(await client.ReadEntityStateAsync<Person>(id.Value)); } privatestatic EntityId? CreateEntityIdFrom(HttpRequest req) { var name = req.Query["name"]; if (string.IsNullOrEmpty(name)) { returnnull; } returnnew EntityId(nameof(Person), name); }
EntityId
の最初の引数が Person クラスに定義した関数の名前です。そして、EntityId
の第二引数に Entity の識別子みたいな感じですね。今回は HttpTrigger の URL のクエリーパラメータの name でを使う感じにしました。
POST http://localhost:7071/api/CreatePerson?name=kazuakix HTTP/1.1
こんな感じのURL を叩くと下のような結果が返ってきます。
HTTP/1.1 200 OK Connection: close Date: Mon, 05 Aug 2019 11:30:47 GMT Content-Type: application/json; charset=utf-8 Server: Kestrel Content-Length: 41 { "entityExists": false, "entityState": null }
entityState
が null なのが気になりますが、これは SignalEntityAsync
は処理の完了を待たずに、処理の要求がキューに入ったら戻ってくる感じです。ReadEntityStateAsync
も別に SignalEntityAsync
で何か処理が行われるか気にせず結果を返すので、こんな結果になります。
なので、もう一度同じ HttpTrigger の関数を叩くと以下のような結果になります。
HTTP/1.1 200 OK Connection: close Date: Mon, 05 Aug 2019 11:34:36 GMT Content-Type: application/json; charset=utf-8 Server: Kestrel Content-Length: 63 { "entityExists": true, "entityState": { "name": "kazuakix", "age": 0 } }
他に、Person#CurseAsync
を呼ぶ場合は以下のようなコードになります。
[FunctionName("Curse")] publicstatic async Task<IActionResult> CurseAsync( [HttpTrigger(AuthorizationLevel.Function, "get", "post")]HttpRequest req, [OrchestrationClient] IDurableOrchestrationClient client) { var id = CreateEntityIdFrom(req); if (!id.HasValue) { returnnew BadRequestResult(); } await client.SignalEntityAsync<IPerson>(id.Value, proxy => proxy.CurseAsync()); returnnew OkObjectResult(await client.ReadEntityStateAsync<Person>(id.Value)); }
この例でも CurseAsync
を処理するという要求をするだけなので、最後の結果の ReadEntityStateAsync
の結果は加齢前のものになります。試しに呼んでみると…
HTTP/1.1 200 OK Connection: close Date: Mon, 05 Aug 2019 11:36:45 GMT Content-Type: application/json; charset=utf-8 Server: Kestrel Content-Length: 63 { "entityExists": true, "entityState": { "name": "kazuakix", "age": 0 } }
年齢は変わりません。ちょっと何もせずステータスを返す以下のような関数を作って…
[FunctionName("GetPerson")] publicstatic async Task<IActionResult> GetPersonAsync( [HttpTrigger(AuthorizationLevel.Function, "get", "post")]HttpRequest req, [OrchestrationClient] IDurableOrchestrationClient client) { var id = CreateEntityIdFrom(req); if (!id.HasValue) { returnnew BadRequestResult(); } returnnew OkObjectResult(await client.ReadEntityStateAsync<Person>(id.Value)); }
適当な時間経過後に呼んでみましょう。
HTTP/1.1 200 OK Connection: close Date: Mon, 05 Aug 2019 11:37:46 GMT Content-Type: application/json; charset=utf-8 Server: Kestrel Content-Length: 63 { "entityExists": true, "entityState": { "name": "kazuakix", "age": 4 } }
年齢増えてますね。
処理結果を待ちつつ結果を取りたい
そういうときは OrchestrationTrigger
の中で Entity を使うと良さそう。
[FunctionName("CurseAndWait")] publicstatic async Task<IActionResult> CurseAndWaitAsync( [HttpTrigger(AuthorizationLevel.Function, "get", "post")]HttpRequest req, [OrchestrationClient] IDurableOrchestrationClient client) { var id = CreateEntityIdFrom(req); if (!id.HasValue) { returnnew BadRequestResult(); } var instanceId = await client.StartNewAsync("CurseAndWaitOrchestrator", id.Value); while((await client.GetStatusAsync(instanceId)).RuntimeStatus != OrchestrationRuntimeStatus.Completed) { await Task.Delay(5000); } returnnew OkObjectResult(await client.ReadEntityStateAsync<Person>(id.Value)); } [FunctionName("CurseAndWaitOrchestrator")] publicstatic async Task CurseAndWaitOrchestratorAsync( [OrchestrationTrigger]IDurableOrchestrationContext context) { var id = context.GetInput<EntityId>(); var proxy = context.CreateEntityProxy<IPerson>(id); await proxy.CurseAsync(); }
オーケストレーター関数の終了待つのに、WaitForCompletionOrCreateCheckStatusResponseAsync
使えばいいやって思ったけど、この関数は、あくまでレスポンス受け取った先の人のためのメソッドであって、HttpTrigger の中でこの関数を await しても必ずしも完了まで待ってくれないんですね。勉強になった。
ということでステータスが完了になるまで自分で待ってます。
そうすると、関数を呼ぶと加齢が完了して、結果に加齢後のオブジェクトが入ってます。 呼び出し前の状態を確認するために GetPerson 関数を呼んで結果を見てます。
HTTP/1.1 200 OK Connection: close Date: Mon, 05 Aug 2019 12:02:23 GMT Content-Type: application/json; charset=utf-8 Server: Kestrel Content-Length: 64 { "entityExists": true, "entityState": { "name": "kazuakix", "age": 47 } }
47 歳ですね。では先ほど作った CurseAndWait
関数をこんな感じ( GET http://localhost:7071/api/CurseAndWait?name=kazuakix HTTP/1.1
)で呼んでみます。
HTTP/1.1 200 OK Connection: close Date: Mon, 05 Aug 2019 12:02:50 GMT Content-Type: application/json; charset=utf-8 Server: Kestrel Content-Length: 64 { "entityExists": true, "entityState": { "name": "kazuakix", "age": 53 } }
53 歳になりましたね。あってるのかな。
まとめ
Durable Functions の Entity が本当に Entity 書いてるみたいになるのでとてもいい。Durable Functions v2 のリリースが楽しみになりますね。
Azure App Service の Easy Tables と Easy API が消える…!?
2019/11/09 に消えちゃうみたいです。
GUI が無くなるだけで、まぁ結局は node.js のコードとか設定ファイル書けば動くよ~ということではあるのですが、Mobile Apps 関連機能はメンテナンスモードにもなってるので、Visual Studio App Center の方への移行を検討するか、自分たちで作るかということになるのかなぁという感じです。
Azure Boards(タスク管理ツール)が Slack 対応しました
へ~、まじで。試してみよう! ということで Slack でアプリ追加をしてみます。アプリ一覧で Azure で検索するとありました。。
Pipelines (自動ビルド)もあるんですね。まぁ今回は Azure Boards をインストールします。 インストール時の設定項目はこんな感じ。
インストールするとチャンネルに以下のようなメッセージが出てきます。
では、/azboards signin
をしてみます。
すると、以下のようなメッセージが出てきます。
Sign in
ボタンを押すとサインインして以下のような承認画面が出てきます。
承認をすると数字が表示されるので、それを Slack の Enter Code
となっている場所に打ち込みます。
そうすると以下のように言われます。
言われた通り紐づけしたい Azure DevOps のプロジェクトの URL をコマンドで設定します。
Add subscription...
ボタンを押して適当に購読する変更を設定します。
では、/azboards create
をしてみます。
作るワークアイテムのタイプを選んで、どのエリアか選択するとタイトルと詳細を書く画面が出てきました。
作り終わると、ワークアイテムが作られた時にメッセージを出すように設定してたのでちゃんとメッセージが出てきました。
メッセージ内のリンクをクリックすると Azure Boards の該当のワークアイテムがブラウザーで開くので適当に自分をアサインしてステータスを Doing に変えてみました。そうすると、Slack のほうにちゃんと結果が出ました!
まとめ
これいいね。
Azure Kubernetes Service のハンズオンしてきた!その復習
お盆で帰省してたタイミングで丁度下のイベントが行われていたので参加者として参加してきました!
今まで Azure だと Web App とかで割となんとかなっていたので使うことはなかったのですが興味はあったので丁度いいと思ったのがきっかけ。
復習もかねて実際に使った以下のリポジトリーの内容を見ながら自分でもやってメモっておこうと思います。
Azure Kubernetes Service (AKS)
名前のとおり Azure の Kubernetes のサービス。 ノード数とノードのスペックくらいを設定しておけば、あとは割とよしなにやってくれるみたい。急激にスパイクしたときは Azure Container Instance のほうに展開するとかいうこともできるみたい。ノード立ち上げたりすると時間がかかるしね。
作ってみよう
さくっと Kubernetes Service を作ってみた
モブプログラミング形式でみんなでやったハンズオンでは、確か下の HTTP の機能を有効にしたけど…
本番では、HTTPS の構成をしたほうがよさそうということがドキュメントに書いてあった。
こんな感じで。
とりあえず ON にして始めてみよう。
Azure Container Registry の作成
プライベートな DockerHub みたいな ACR も作っておきます。 これは特筆事項もとくにないくらい作るだけです。
作業用マシンを作ろう
私は Azure 上に Linux VM を一台立ててるのでそれを使います。 az コマンドのインストールと
docker を入れておきます。
ACR へのログインを docker コマンドでしておきます。ACR のアクセスキーのところで見れます。管理者ユーザーっていうのを有効にするみたい。
docker login -u ユーザー名 -p "パスワード" ACRの名前.azurecr.io
実際に値を当てはめるとこんなかんじ。
sudo docker login -u kazukicr -p "sugo-kunagai-portal-kara-toreru-pasuwa-do-dayo!" kazukicr.azurecr.io
適当に ACR に push
ということでついに docker push の機運が高まってきました。Azure Functions でやってみます。
以下のように --docker
オプションをつけておくと docker ファイルもできます。便利。
func init --docker
そして、何もないと寂しいので func new
で HttpTrigger の関数でも追加しておきます。そして以下のコマンドをたたいてビルド
sudo docker build .
ビルドが通ったので、とりあえずタグ付けとか docker push
するスクリプトも書いておきます。これも寺田さん作のもの
#!/bin/bashset-eif ["$1"=""]thenecho"./build-create.sh [version-number]"exit1fiexport VERSION=$1DOCKER_IMAGE=okazukirc/dockerfunc DOCKER_REPOSITORY=kazukicr.azurecr.io docker build -t$DOCKER_IMAGE:$VERSION . -f ./Dockerfile docker tag $DOCKER_IMAGE:$VERSION$DOCKER_REPOSITORY/$DOCKER_IMAGE:$VERSION docker push $DOCKER_REPOSITORY/$DOCKER_IMAGE:$VERSION docker rm$(docker ps -aq) docker images | awk '/<none/{print $3}' | xargs docker rmi
bash のスクリプト慣れないなぁ。chmod して sudo ./docker-build.sh 1.0
してビルド成功!!
ACR の画面を見るとちゃんと push されてました。めでたい。
ちょっとローカルで動かしてみましょう。以下のコマンドをたたいて…
$ sudo docker run -p 8080:80 kazukicr.azurecr.io/okazukirc/dockerfunc:1.0
curl でたたくと…
$ curl http://local/Hello?name=aaaaaa Hello, aaaaaa
動きましたね。
ついに Kubernetes
kubectl
を入れます。コマンドでさくっと
$ az aks install-cli
コマンドが入ったので AKS への認証情報をゲットしましょう。
$ az aks get-credentials --resource-group AKS-rg --name kazukiaks Merged "kazukiaks" as current context in /home/okazuki/.kube/config
ノードも取れました。
$ kubectl get node NAME STATUS ROLES AGE VERSION aks-agentpool-51334753-0 Ready agent 3d4h v1.12.8 aks-agentpool-51334753-1 Ready agent 3d4h v1.12.8 aks-agentpool-51334753-2 Ready agent 3d4h v1.12.8 virtual-node-aci-linux Ready agent 3d4h v1.13.1-vk-v0.9.0-1-g7b92d1ee-dev
AKSからACRへのアクセスの認証情報を追加します。
$ kubectl create secret docker-registry \ > docker-reg-credential \ > --docker-server=kazukicr.azurecr.io \ > --docker-username=kazukicr \ > --docker-password=your-password \ > --docker-email=k_ota28@hotmail.com
ここら辺から、デプロイメントやサービスなどの定義の YAML を適用するのですが、ハンズオン時には出来上がった YAML の一部を編集して使ってたのでよくわからんって感じになりますね。
まず、Deployment については下を見ました。
今回の場合はこんな感じだろうか??
apiVersion: apps/v1 kind: Deployment metadata:name: dockerfunc-deployment labels:app: dockerfunc spec:replicas:3selector:matchLabels:app: dockerfunc template:metadata:labels:app: dockerfunc spec:securityContext:runAsUser:1000imagePullSecrets:- name: docker-reg-credential containers:- name: dockerfunc image: kazukicr.azurecr.io/okazukirc/dockerfunc:1.0 ports:- containerPort:80
imagePullSecrets
が先ほど kubectl
で作った secret の名前です。runAsUser
は、ルートで動かさないという感じ。1000 にしてるってことは最初に作られたユーザーアカウントってことなんだろう。
kubectl apply -f ファイル名
で適用できるので、上記ファイルを deploy.yaml
という名前で保存している場合は以下のようなコマンドで適用できます。
$ kubectl apply -f deploy.yaml deployment.apps/dockerfunc-deployment created
deployment がちゃんとできてるかコマンドを打って確認します。
$ kubectl get deploy NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE dockerfunc-deployment 3 3 3 0 74s
出来てるっぽい。Pod は…?
$ kubectl get po NAME READY STATUS RESTARTS AGE dockerfunc-deployment-76bb667bd-6cl68 0/1 CrashLoopBackOff 4 2m43s dockerfunc-deployment-76bb667bd-9jnsh 0/1 CrashLoopBackOff 4 2m43s dockerfunc-deployment-76bb667bd-vtcg8 0/1 CrashLoopBackOff 4 2m43s
CrashLoopBackOff なので激しく失敗してますね…。エラーのときは kubectl description
コマンドでログ見てみるといいといわれたので見てみます。
$ kubectl describe pods dockerfunc-deployment-76bb667bd-6cl68
結果はこれ…よくわからない...
Name: dockerfunc-deployment-68984c94bd-4vbr2 Namespace: default Priority: 0 Node: aks-agentpool-51334753-1/10.240.0.66 Start Time: Thu, 15 Aug 2019 05:18:22 +0000 Labels: app=dockerfunc pod-template-hash=68984c94bd Annotations: <none> Status: Running IP: 10.240.0.68 Controlled By: ReplicaSet/dockerfunc-deployment-68984c94bd Containers: dockerfunc: Container ID: docker://c62fc1f1d2a3818651d0b703140b3119ad3755ffb8cb99efc49ea563c15c2ddc Image: kazukicr.azurecr.io/okazukirc/dockerfunc:1.0 Image ID: docker-pullable://kazukicr.azurecr.io/okazukirc/dockerfunc@sha256:2ef5515278a6f17053dfc14289fdf5f77ea7d127819d75f6951b1beac00a212e Port: 80/TCP Host Port: 0/TCP State: Terminated Reason: Error Exit Code: 1 Started: Thu, 15 Aug 2019 05:19:05 +0000 Finished: Thu, 15 Aug 2019 05:19:05 +0000 Last State: Terminated Reason: Error Exit Code: 1 Started: Thu, 15 Aug 2019 05:18:39 +0000 Finished: Thu, 15 Aug 2019 05:18:39 +0000 Ready: False Restart Count: 3 Limits: cpu: 100m Requests: cpu: 100m Environment: <none> Mounts: /var/run/secrets/kubernetes.io/serviceaccount from default-token-psnwx (ro) Conditions: Type Status Initialized True Ready False ContainersReady False PodScheduled True Volumes: default-token-psnwx: Type: Secret (a volume populated by a Secret) SecretName: default-token-psnwx Optional: false QoS Class: Burstable Node-Selectors: <none> Tolerations: node.kubernetes.io/not-ready:NoExecute for 300s node.kubernetes.io/unreachable:NoExecute for 300s Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Scheduled 50s default-scheduler Successfully assigned default/dockerfunc-deployment-68984c94bd-4vbr2 to aks-agentpool-51334753-1 Normal Pulled 8s (x4 over 49s) kubelet, aks-agentpool-51334753-1 Container image "kazukicr.azurecr.io/okazukirc/dockerfunc:1.0" already present on machine Normal Created 8s (x4 over 49s) kubelet, aks-agentpool-51334753-1 Created container Normal Started 7s (x4 over 48s) kubelet, aks-agentpool-51334753-1 Started container Warning BackOff 7s (x5 over 47s) kubelet, aks-agentpool-51334753-1 Back-off restarting failed container
とりあえず RunAsUser を外してみたら動いた…う~ん。
apiVersion: apps/v1 kind: Deployment metadata:name: dockerfunc-deployment labels:app: dockerfunc version: v1 spec:replicas:3selector:matchLabels:app: dockerfunc template:metadata:labels:app: dockerfunc spec:# securityContext:# runAsUser: 1000imagePullSecrets:- name: docker-reg-credential containers:- name: dockerfunc image: kazukicr.azurecr.io/okazukirc/dockerfunc:1.0 ports:- containerPort:80resources:requests:cpu: 100m limits:cpu: 100m
apply したら動いた
$ kubectl get po NAME READY STATUS RESTARTS AGE dockerfunc-deployment-674d45656d-jgb9b 1/1 Running 0 119s dockerfunc-deployment-674d45656d-lsbf4 1/1 Running 0 115s dockerfunc-deployment-674d45656d-mnb8v 1/1 Running 0 2m1s
ポートフォワーディングして動いてるか確認しましょう。
$ kubectl port-forward dockerfunc-deployment-674d45656d-jgb9b 8080:80 Forwarding from 127.0.0.1:8080 -> 80 Forwarding from [::1]:8080 -> 80
curl でたたいてみると…
$ curl http://localhost:8080/api/Hello?name=okazuki Hello, okazuki
動いた!!
サービス
pod をまとめるサービスかな。それの定義を作りましょう。
apiVersion: v1 kind: Service metadata:labels:app: dockerfunc-service name: dockerfunc-service spec:ports:- port:80name: http targetPort:80selector:app: dockerfunc sessionAffinity: None type: LoadBalancer
dockerfunc の v1 で選択したやつをさくっとまとめておきます。最後の type: LoadBalancer
は本番では使わないで!って言われたけど、とりあえず動きを確認するために設定。これをしてると簡単に外から叩ける。
適用します。
$ kubectl apply -f service.yaml service/dockerfunct-service created
しばらく待つと IP が割り当てられます。(今回は 13.71.149.89)
$ kubectl get service NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE dockerfunc-service LoadBalancer 10.0.52.83 13.71.149.86 80:31505/TCP 7m4s kubernetes ClusterIP 10.0.0.1 <none> 443/TCP 3d5h
curl でたたくと…
$ curl http://13.71.149.86/api/Hello?name=kubernates Hello, kubernates
動いた!!
では、サービスを LoadBalancer で公開するのをやめます。ClusterIP にしておくのがよさそう。ClusterIP にしたら一度サービス削除して再度アプライしました。
$ kubectl delete service dockerfunc-service $ kubectl apply -f service.yaml
Ingress を作成
Ingress がサービスに処理を割り振ってくれるような動きをしてくれるものみたいです。これを作って、ここにサービスの割り振りを設定することでちゃんと外からアクセスできるようになります。
Ingress を作りにあたってドメインをゲットしましょう。ポータルの AKS を開くと HTTP アプリケーションのルーティング ドメイン
という項目があります。これを控えておきます。
そして、dockerfunc-service に対してアクセスできるようにしましょう。Ingress を定義する YAML を定義して…
apiVersion: extensions/v1beta1 kind: Ingress metadata:name: dockerfunc annotations:kubernetes.io/ingress.class: addon-http-application-routing spec:rules:- host: dockerfunc.ada93e44ae4b4d9ea00a.japaneast.aksapp.io http:paths:- backend:serviceName: dockerfunc-service servicePort:80path: /
apply しましょう。
$ kubectl apply -f ingress.yaml ingress.extensions/dockerfunc created
kubectl get ing
で Ingress の状態が見れます。
$ kubectl get ing NAME HOSTS ADDRESS PORTS AGE dockerfunc dockerfunc.ada93e44ae4b4d9ea00a.japaneast.aksapp.io 104.41.172.168 80 52s
では、curl でアクセスしてみましょう。
$ curl http://dockerfunc.ada93e44ae4b4d9ea00a.japaneast.aksapp.io/api/Hello?name=Ingress Hello, Ingress
動いた!!
バージョンアップしてみよう
ちょっと Azure Functions のコードを変えていきます。
using System; using System.IO; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Azure.WebJobs; using Microsoft.Azure.WebJobs.Extensions.Http; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Newtonsoft.Json; namespace DockerFunc { publicstaticclass Hello { [FunctionName("Hello")] publicstatic async Task<IActionResult> Run( [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req, ILogger log) { log.LogInformation("C# HTTP trigger function processed a request."); string name = req.Query["name"]; string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); dynamic data = JsonConvert.DeserializeObject(requestBody); name = name ?? data?.name; return name != null ? (ActionResult)new OkObjectResult($"こんにちは, {name}") : new BadRequestObjectResult("Please pass a name on the query string or in the request body"); } } }
ビルドスクリプトを実行して version 2.0 を作ります。
$ sudo ./docker-build.sh 2.0
できました。
deployment を新しくして…
apiVersion: apps/v1 kind: Deployment metadata: name: dockerfunc-deployment labels: app: dockerfunc version: v1 spec: replicas: 3 selector: matchLabels: app: dockerfunc template: metadata: labels: app: dockerfunc version: v2 spec: # securityContext: # runAsUser: 1000 imagePullSecrets: - name: docker-reg-credential containers: - name: dockerfunc image: kazukicr.azurecr.io/okazukirc/dockerfunc:2.0 ports: - containerPort: 80 resources: requests: cpu: 100m limits: cpu: 100m
apply してみましょう。
$ kubectl apply -f deploy.yaml deployment.apps/dockerfunc-deployment configured
curl でたたいてみると…
$ curl http://dockerfunc.ada93e44ae4b4d9ea00a.japaneast.aksapp.io/api/Hello?name=Ingress こんにちは, Ingress
ふむ…
deployment のラベルを完全に別名にして、サービスを新たに作って Ingress で新しいサービスを指し示すようにしたりすると古いサービスに戻すとかもできそう。
まとめ
まだまだ、いろいろな機能があるんだろうね…。 でも、とっかかりにとてもいいイベントでした!!
ちなみに実際には、このほかにも Azure DevOps を使って AKS に自動ビルドして自動デプロイするようなものもやりました。 一日なのに盛沢山。
ASP.NET Core 3.0 で gRPC してみよう
.NET Core になると WCF のサーバーサイドが消えて移行先として gRPC があげられてるのを何処かで見た気がします。OSS の WCF もあった気がするけど、そっちはよく見てない。
ということで、ASP.NET Core 3.0 Preview で gRPC 試してみようと思います。
プロジェクトの作成
今日は出先のカフェでコーヒー飲みながら Surface Go で書いてます。なので Visual Studio 2019 は入ってない(Surface Go には重すぎた)ので、Visual Studio Code でいきます。
適当なフォルダーで空の Web アプリを作ります。
$ dotnet new web -o GrpcServer
ソリューションも作って追加しておきましょう。
$ dotnet new sln $ dotnet sln add GrpcServer/GrpcServer.csproj
Visual Studio Code でソリューションのあるフォルダーを開いて task.json とかを生成して Ctrl + Shift + B
でビルドしたり F5
でデバッグできるようにしました。便利。
gRPC のためのパッケージを追加します。
$ dotnet add .\GrpcServer\GrpcServer.csproj package Grpc.AspNetCore -v 0.1.22-pre3
そして、.proto
ファイルを生成しましょう。これも dotnet new
で生成できます。後で作成するクライアントでも使う予定なので、ソリューションのあるフォルダーに Proto/Proto.proto
とかみたいな感じで作りました。
$ dotnet new proto -o Proto
サービスの定義を追加します。
syntax = "proto3"; option csharp_namespace = "GrpcSample"; service Greeter { rpc Greet (GreetRequest) returns (GreetReply); } message GreetRequest { string name = 1; } message GreetReply { string message = 1; }
サーバープロジェクトに追加するために .csproj
を編集しましょう。
<Project Sdk="Microsoft.NET.Sdk.Web"><PropertyGroup><TargetFramework>netcoreapp3.0</TargetFramework></PropertyGroup><ItemGroup><PackageReference Include="Grpc.AspNetCore"Version="0.1.22-pre3" /></ItemGroup><ItemGroup><!-- これを追加 --><Protobuf Include="../Proto/Proto.proto"LinkBase="Proto/Proto.proto"GrpcServices="Server" /></ItemGroup></Project>
これでビルドをすると GrpcServer/obj/Debug
に Proto.cs
と ProtoGrpc.cs
が生成されます。これを継承してサービスを実装します。
GrpcServer
プロジェクトに Services
フォルダーを作って、そこに GreeterService.cs
を作って以下のように Greeter.GreeterBase
を継承する形で処理を作ります。
using System.Threading.Tasks; using Grpc.Core; using GrpcSample; namespace GrpcService.Services { publicclass GreeterService : Greeter.GreeterBase { publicoverride Task<GreetReply> Greet(GreetRequest request, ServerCallContext context) { return Task.FromResult(new GreetReply { Message = $"Hello {request.Name}", }); } } }
.proto
に定義したクラスとサービスのメソッドのひな型は基本クラスで定義されているので、やることはメソッドをオーバーライドして実装するだけです。簡単。
Startup.cs
で gRPC 機能の有効化と上で作成したサービスを登録する処理を追加します。
using GrpcService.Services; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; namespace GrpcServer { publicclass Startup { publicvoid ConfigureServices(IServiceCollection services) { services.AddGrpc(); // これと } publicvoid Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapGrpcService<GreeterService>(); // これを追加 endpoints.MapGet("/", async context => { await context.Response.WriteAsync("Hello World!"); }); }); } } }
これでサーバーは完成しましたといいたいところですが、もうちょっとだけ設定を… gRPC は HTTP/2 を使うので、その設定を追加します。appsettings.json
に以下のような設定を追加します。
{"Logging": {"LogLevel": {"Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" }}, "AllowedHosts": "*", "Kestrel": {"EndpointDefaults": {"Protocols": "Http2" }}}
Kestrel の部分が追加したものになります。
クライアントの作成
サーバーだけ作っても、誰も呼んでくれないと何もできないのでクライアントも作ります。WPF でいきましょう。さくっとプロジェクトを作ってソリューションに追加します。
$ dotnet new wpf -o GrpcClient $ dotnet sln add .\GrpcClient\GrpcClient.csproj
そして gRPC のクライアント側に必要なパッケージを入れます。
dotnet add .\GrpcClient\GrpcClient.csproj package Google.Protobuf -v 3.9.1 dotnet add .\GrpcClient\GrpcClient.csproj package Grpc.Net.Client -v 0.1.22-pre3 dotnet add .\GrpcClient\GrpcClient.csproj package Grpc.Tools -v 2.23.0
そして、GrpcClient.csproj
に Protobuf タグを追加します。今回は生成してもらうのはクライアントなので GrpcServices 属性には Client を設定してます。余談ですがクライアントとサーバーの両方を生成してほしいときは Both とかくみたいです。
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop"><PropertyGroup><OutputType>WinExe</OutputType><TargetFramework>netcoreapp3.0</TargetFramework><UseWPF>true</UseWPF></PropertyGroup><ItemGroup><PackageReference Include="Google.Protobuf"Version="3.9.1" /><PackageReference Include="Grpc.Net.Client"Version="0.1.22-pre3" /><PackageReference Include="Grpc.Tools"Version="2.23.0"><IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets><PrivateAssets>all</PrivateAssets></PackageReference></ItemGroup><ItemGroup><Protobuf Include="../Proto/Proto.proto"LinkBase="Proto/Proto.proto"GrpcServices="Client" /></ItemGroup></Project>
そして、名前を入力するための TextBox とサービスを呼ぶための Button を置いた画面を MainWindow.xaml
に定義して…
<Window x:Class="GrpcClient.MainWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:d="http://schemas.microsoft.com/expression/blend/2008"xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"xmlns:local="clr-namespace:GrpcClient"mc:Ignorable="d"Title="MainWindow"Height="450"Width="800"><StackPanel><TextBox x:Name="textBoxName" /><Button Content="Call gRPC service"Click="CallGrpcServiceButton_Click" /></StackPanel></Window>
コードビハインドにサービスを呼び出すコードを書きましょう。
using System; using System.Net.Http; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; using GrpcSample; namespace GrpcClient { /// <summary>/// Interaction logic for MainWindow.xaml/// </summary>publicpartialclass MainWindow : Window { public MainWindow() { InitializeComponent(); } private async void CallGrpcServiceButton_Click(object sender, RoutedEventArgs e) { using (var client = new HttpClient { BaseAddress = new Uri("https://localhost:5001") }) { var greetServices = Grpc.Net.Client.GrpcClient.Create<Greeter.GreeterClient>(client); var response = await greetServices.GreetAsync(new GreetRequest { Name = textBoxName.Text, }); MessageBox.Show(response.Message); } } } }
そうするとコンパイルエラー!!
MainWindow.xaml.cs(16,7): error CS0246: The type or namespace name 'GrpcSample' could not be found (are you missing a using directive or an assembly reference?) [c:\Users\k_ota\source\repos\GrpcLab\GrpcClient\GrpcClient_teh220ey_wpftmp.csproj]
起きてるエラーとしては以下の Issue と似てるけど、こっちはクラシックツールチェーン…
試しに WPF じゃなくてコンソールアプリで同じ手順を踏んで呼び出す処理を書いたらコンパイルエラーにならないので WPF on .NET Core 用のツールまわりのバグかな?とりあえずの回避方法はクライアントコードの生成をクラスライブラリにうつすことです。
プロジェクトを作成して、必要な参照を追加したりします。
$ dotnet new classlib -o GrpcClientLib $ dotnet sln add .\GrpcClientLib\GrpcClientLib.csproj
生成されるのは .NET Standard 2.0 のプロジェクトなのですが、Grpc.Net.Client 0.1.22-pre3 は .NET Standard 2.1 (ターゲットにしてるライブラリ始めてみた!)なので GrpcClientLib.csproj の netstandard2.0 を netstandard2.1 に書き換えてから下記コマンドでライブラリを追加します。
$ dotnet add .\GrpcClientLib\GrpcClientLib.csproj package Google.Protobuf -v 3.9.1 $ dotnet add .\GrpcClientLib\GrpcClientLib.csproj package Grpc.Net.Client -v 0.1.22-pre3 $ dotnet add .\GrpcClientLib\GrpcClientLib.csproj package Grpc.Tools -v 2.23.0
そして GrpcClientLib.csproj
に、Protobuf タグの定義を追加します。
<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><TargetFramework>netstandard2.0</TargetFramework></PropertyGroup><ItemGroup><PackageReference Include="Google.Protobuf"Version="3.9.1" /><PackageReference Include="Grpc.Tools"Version="2.23.0"><IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets><PrivateAssets>all</PrivateAssets></PackageReference></ItemGroup><ItemGroup><Protobuf Include="../Proto/Proto.proto"LinkBase="Proto/Proto.proto"GrpcServices="Client" /></ItemGroup></Project>
WPF 側からは Protobuf のタグを削除しておきましょう。
そして、GrpcClient に GrpcCLientLib への参照を追加します。
$ dotnet add .\GrpcClient\GrpcClient.csproj reference .\GrpcClientLib\GrpcClientLib.csproj
これでコンパイルエラーなしでビルドが通るようになります。
まだ一度も入れたことがない人は .NET Core の開発用の証明書をインストールして
$ dotnet dev-certs https --trust
dotnet run
でサーバーとクライアントを起動して試してみましょう。
まずは、サーバー
$ dotnet run --project .\GrpcServer\GrpcServer.csproj
そして、クライアント
$ dotnet run --project .\GrpcClient\GrpcClient.csproj
適当に TextBox に何か入れてボタンを押すと無事動きました
Azure にデプロイ!!
Azure の App Service に gRPC のサービスをデプロイして動かすことはできないみたいです。残念。
AKS 使えば出来そうですが、ここに書くにはちょっとヘビーなので、また今度トライしてみて書きます。
まとめ
ASP.NET Core の gRPC 割とサクッと作れていい感じです。 LTS 版の .NET Core 3.1 が出たら使ってみたいなぁ。でも App Service の対応は早くしてほしいところ。
ソースコードは、GitHub にあげておきました。
ReactiveProperty v6.1 をリリースしました
プルリクエストをマージしてリリースするだけの簡単なお仕事。 詳細は GitHub のリリースページから!
Release v6.1.2 · runceel/ReactiveProperty · GitHub
プルリクエストや Issue への投稿いつもありがとうございます。多謝!
ASP.NET Core 3.0 Preview 8 で gRPC に Azure AD 認証つけてみよう
さて、前回は簡単に呼び出す奴を作ってみました。
今回は Azure AD 認証を付けたいと思います。
Azure AD にアプリの登録
では、Azure AD にアプリを登録します。サーバー側とクライアント側の 2 つを登録しましょう。
サーバーアプリの登録
とりあえずシングルテナント(自分のテナントのユーザーだけ)でさくっとサーバー側を作ります。
サーバー側のアプリが作成されたら、スコープを追加します。「APIの公開」で 「Scope の追加」を選択して適当な名前で作ります。 まず、アプリケーション ID URL を作るように言われるので「保存してから続ける」を選択します。
続けてスコープの追加です。適当に名前を入れて、同意できる人を管理者とユーザーにしてその他の項目も適当に埋めていきます。
スコープの追加ボタンを押すとスコープが追加されます。スコープの api://アプリID/スコープ名
は後で使うのでコピーしておきましょう。
クライアントアプリの登録
続けてクライアントのアプリを登録します。今回のクライアントアプリは .NET Core で作った WPF アプリです。 残念ながら各種ライブラリーが、まだ .NET Core には組み込みブラウザーなんてないと思って実装されてるので、そんな感じで登録します。
具体的にいうと、アプリ作成時(作成後でも編集できます)のリダイレクト URI で パブリッククライアント(モバイルとデスクトップ)を選択して http://localhost
を設定します。
アプリが出来たら「APIのアクセス許可」に行って「アクセス許可の追加」を選択します。API の選択画面になるので「自分の API」タブを選択して、先ほど作ったサーバー用アプリに作ったスコープを選択して「アクセス許可の追加」をします。
後で使うので、クライアントアプリの「概要」ページにいって「アプリケーション(クライアント)ID」をコピーしておきます。
テナント ID の取得
先ほどと同じアプリの「概要」ページから「ディレクトリ(テナント)ID」をコピーしておきます。
サーバーに認証機能を追加
では、サーバー側に認証機能を追加しましょう。 まず、以下の NuGet パッケージを入れます。
- Microsoft.AspNetCore.Authentication.JwtBearer 3.0.0-preview8.xxxxx
そして、Startup.cs
に JWT トークンによる認証の設定を追加します。
using GrpcService.Services; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; namespace GrpcServer { publicclass Startup { publicvoid ConfigureServices(IServiceCollection services) { // これと services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.Authority = "https://login.microsoftonline.com/ここにテナントID/"; options.Audience = "api://サーバー側アプリのクライアントID"; }); services.AddAuthorization(); services.AddGrpc(); } publicvoid Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseRouting(); // これを追加 app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapGrpcService<GreeterService>(); endpoints.MapGet("/", async context => { await context.Response.WriteAsync("Hello World!"); }); }); } } }
ここの ConfigureServices で先ほどメモした値を使います。Authority はテナントIDを埋め込んで、Audience にサーバー側アプリのスコープを作成したときに作られた api://アプリID
の値を設定します。先ほど控えたスコープの値から最後の /Call.API
をとった値になります。
そして gRPC のサービスに Authorize 属性をつけます。ここら辺は REST API と同じ感じですね。
using System.Threading.Tasks; using Grpc.Core; using GrpcSample; using Microsoft.AspNetCore.Authorization; namespace GrpcService.Services { [Authorize] publicclass GreeterService : Greeter.GreeterBase { publicoverride Task<GreetReply> Greet(GreetRequest request, ServerCallContext context) { return Task.FromResult(new GreetReply { Message = $"Hello {request.Name}", }); } } }
この状態でアプリを起動して API を呼ぼうとすると、認証エラーになります。
因みに前回は Visual Studio Code で dotnet run
で起動したので別によかったのですが Visual Studio だとデフォルトで IIS Express で起動しようとするので、これをやめてやる必要があります。以下のような感じで設定変更できます。
クライアントにログイン機能を追加
では、クライアントにログイン機能を追加しましょう。MSAL.NET を追加します。パッケージ名は、Microsoft.Identity.Client
になります。
現時点での最新の 4.3.1 を入れました。
今回は簡単に実装するため全部 MainWindow.xaml.cs
に書いていきます。書き忘れたけどサーバー側も設定をハードコーディングしてるけど、ちゃんと設定から読むようにしてね。
using System; using System.Net.Http; using System.Linq; using System.Threading.Tasks; using System.Windows; using GrpcSample; using Microsoft.Identity.Client; namespace GrpcClient { /// <summary>/// Interaction logic for MainWindow.xaml/// </summary>publicpartialclass MainWindow : Window { privatestring[] Scopes { get; } = new[] { "api://サーバーアプリのID/Call.API" }; // スコープ作った時にコピーしておいたやつprivatereadonly IPublicClientApplication _app; public MainWindow() { InitializeComponent(); // 本当は設定(appsettings.json とか)から読む var options = new PublicClientApplicationOptions { ClientId = "クライアントアプリのID", RedirectUri = "http://localhost", TenantId = "テナントのID", }; _app = PublicClientApplicationBuilder.CreateWithApplicationOptions(options).Build(); } private async void CallGrpcServiceButton_Click(object sender, RoutedEventArgs e) { var accessToken = await GetAccessTokenAsync(); using (var client = new HttpClient { BaseAddress = new Uri("https://localhost:5001") }) { var greetServices = Grpc.Net.Client.GrpcClient.Create<Greeter.GreeterClient>(client); var response = await greetServices.GreetAsync(new GreetRequest { Name = textBoxName.Text, }, new Grpc.Core.Metadata { { "Authorization", $"Bearer {accessToken}" }, }); MessageBox.Show(response.Message); } } private async Task<string> GetAccessTokenAsync() { AuthenticationResult r; try { var account = (await _app.GetAccountsAsync())?.FirstOrDefault(); r = await _app.AcquireTokenSilent(Scopes, account).ExecuteAsync(); } catch (MsalUiRequiredException) { r = await _app.AcquireTokenInteractive(Scopes) .WithSystemWebViewOptions(new SystemWebViewOptions { OpenBrowserAsync = SystemWebViewOptions.OpenWithChromeEdgeBrowserAsync, }) .ExecuteAsync(); } return r.AccessToken; } } }
IPublicClientApplication
の作成をしているコンストラクターの処理と、アクセストークンの取得をしている GetAccessTokenAsync
あたりが注目かな。
そして、最後に gRPC の API を呼び出すメソッドで Grpc.Core.Metadata
でお馴染みの Bearer でトークン渡してやる感じです。
では、実行してみましょう。
WPF のアプリでボタンを押すとブラウザーでログイン画面が開きます。アプリを作った Azure AD のテナントのユーザーでサインインしてください。 同意が求められるので逆らわずにいきましょう。
ログインに成功すると、ブラウザーは閉じていいよというメッセージが出ます。なので閉じて(ほっといてもいいけど)WPFアプリ側に戻ると gRPC の API が呼べてますね!やったね!
認証情報にアクセスしたい
サーバー側で認証情報触りたい場合はメソッドの引数の ServerCallContext
で GetHttpContext
を呼ぶと HttpContext が取れるので、それ経由でアクセスできます。例えば以下のような感じで
using System.Threading.Tasks; using Grpc.Core; using GrpcSample; using Microsoft.AspNetCore.Authorization; namespace GrpcService.Services { [Authorize] publicclass GreeterService : Greeter.GreeterBase { publicoverride Task<GreetReply> Greet(GreetRequest request, ServerCallContext context) { var identity = context.GetHttpContext().User.Identity; return Task.FromResult(new GreetReply { Message = $"Hello {request.Name}, IsAuthorized: {identity.IsAuthenticated}, Your account is {identity.Name}", }); } } }
実行すると、以下のような感じになります。
まとめ
Azure App Service でホスト出来るようになるのを首を長くして待ってます。
ソースコードは前回のリポジトリに addauth というブランチを切ってあるので見てください。
ASP.NET Core 3.0 + gRPC + WPF on .NET Core 3.0 で Azure AD を使って認証・認可
過去記事
本文
前回、認証だけはやりました。今回はユーザーの権限とかを見て何かしたいとか、この API は呼べる、呼べないを構成していきたいと思います。
Azure AD のグループ機能で認可してみよう
Azure AD のグループでユーザーをグループに所属させることが出来ます。私の環境ではグループ作る時にセキュリティと Office 365 が選択出来ますが今回はセキュリティの方で適当にグループを作ります。
グループを作って適当にユーザーを所属させます。そしてグループを識別するためのオブジェクト IDを控えておきます。以下のようにグループの一覧にもちらっとオブジェクト IDが表示されますが
グループを選択するとオブジェクト IDがコピーできます。
グループが出来たので、次に Azure AD に登録したアプリをいじっていきます。アプリにわたっていくクレームにユーザーのセキュリティグループがわたるようにします。Azure AD の「アプリの登録」からサーバー側のアプリを開きます。そして「マニフェスト」を選択します。すると JSON が表示されます。
その中に ""groupMembershipClaims": null,
という行があるので "groupMembershipClaims": "SecurityGroup",
に変更して保存します。
この状態でアプリを実行して適当なユーザーでログインして gRPC のサービスを呼び出してみましょう。gRPC のサービスのメソッドの中でブレークポイントで止めて HttpContext の中の User.Identity.Claims を覗いてみると groups
をキーにしてグループのオブジェクト ID がわたってきてることが確認できます。
これの有無で API が呼べる・呼べないを制御出来れば良さそうです。試しに admins グループじゃないと呼べない処理を 1 つ追加してみます。
.proto
ファイルに GreetForAdmin
という名前のメソッドをはやします。
syntax = "proto3"; option csharp_namespace = "GrpcSample"; service Greeter { rpc Greet (GreetRequest) returns (GreetReply); rpc GreetForAdmin (GreetRequest) returns (GreetReply); } message GreetRequest { string name = 1; } message GreetReply { stringmessage = 1; }
サーバー側のサービスの実装は以下のようにしてみました。管理者なので Hello と気軽に挨拶するのではなく Dear にしてみました。
publicoverride Task<GreetReply> GreetForAdmin(GreetRequest request, ServerCallContext context) { var identity = context.GetHttpContext().User.Identity; return Task.FromResult(new GreetReply { Message = $"Dear {request.Name}, IsAuthorized: {identity.IsAuthenticated}, Your account is {identity.Name}", }); }
権限の高い人には媚を売っていくスタイル。では、これを admins グループの人のみが呼べるようにします。(今のままだと誰でも呼べる)
Startup.cs
の ConfigureServices
メソッドにある services.AddAuthorization();
を以下のように変えます。
services.AddAuthorization(options => { options.AddPolicy("Admins", policy => policy.RequireClaim("groups", "admins グループのオブジェクト ID")); });
Admins というポリシーを追加しています。ポリシーの中身はクレームの中に groups に admins のオブジェクト ID があるということです。
ポリシーの適用は、Authorize
属性で行います。
[Authorize("Admins")] publicoverride Task<GreetReply> GreetForAdmin(GreetRequest request, ServerCallContext context) { var identity = context.GetHttpContext().User.Identity; return Task.FromResult(new GreetReply { Message = $"Dear {request.Name}, IsAuthorized: {identity.IsAuthenticated}, Your account is {identity.Name}", }); }
では、クライアント側にボタンを追加して GreetForAdmin を呼び出す処理を追加しましょう。 MainPage.xaml を編集してボタンを追加します。
<Windowx:Class="GrpcClient.MainWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:d="http://schemas.microsoft.com/expression/blend/2008"xmlns:local="clr-namespace:GrpcClient"xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"Title="MainWindow"Width="800"Height="450"mc:Ignorable="d"><StackPanel><TextBox x:Name="textBoxName" /><Button Click="CallGrpcServiceButton_Click"Content="Call gRPC service" /><Button Click="CallGrpcServiceForAdminButton_Click"Content="Call gRPC service for Admin" /></StackPanel></Window>
クリックイベントハンドラーは以下のように GreetForAdminAsync メソッドを呼ぶようにします。
private async void CallGrpcServiceForAdminButton_Click(object sender, RoutedEventArgs e) { var accessToken = await GetAccessTokenAsync(); using (var client = new HttpClient { BaseAddress = new Uri("https://localhost:5001") }) { var greetServices = Grpc.Net.Client.GrpcClient.Create<Greeter.GreeterClient>(client); var response = await greetServices.GreetForAdminAsync(new GreetRequest { Name = textBoxName.Text, }, new Grpc.Core.Metadata { { "Authorization", $"Bearer {accessToken}" }, }); MessageBox.Show(response.Message); } }
実行してみます。admins グループに所属してないユーザーでログインして Admin のほうのボタンを押すと例外が出ます。
admins グループに所属しているユーザーでログインして Admin の方のボタンを押すとちゃんと処理が呼べます。
クライアント側でログインユーザーのグループの判別
さて、実際のプログラムではログインユーザーの所属するグループに応じてボタンとかメニューの表示・非表示を制御することになると思います。 JWT トークンをパースすれば、その中にクレームが入っているのでそれでやります。
自前でパースしてもいいのですが System.IdentityModel.Tokens.Jwt
というパッケージが公開されているので、それを使いましょう。
JwtSecuretyToken
クラスにトークンを渡してやればいい感じに扱えます。Claims プロパティから目的のクレームがあるか探してチェックしたりすればいい感じですね。
以下のようにすれば呼び出し前に admins に所属しているかどうか確認できます。
private async void CallGrpcServiceForAdminButton_Click(object sender, RoutedEventArgs e) { var accessToken = await GetAccessTokenAsync(); var jwt = new JwtSecurityToken(accessToken); var groupId = jwt.Claims.FirstOrDefault(x => x.Type == "groups")?.Value; if (groupId != "admins グループのオブジェクト ID") { MessageBox.Show("You are not a member of admins group, right? Please do not click this button."); return; } using (var client = new HttpClient { BaseAddress = new Uri("https://localhost:5001") }) { var greetServices = Grpc.Net.Client.GrpcClient.Create<Greeter.GreeterClient>(client); var response = await greetServices.GreetForAdminAsync(new GreetRequest { Name = textBoxName.Text, }, new Grpc.Core.Metadata { { "Authorization", $"Bearer {accessToken}" }, }); MessageBox.Show(response.Message); } }
実行して admins グループにいないユーザーでサインインすると以下のように、ちゃんと呼び出し前にチェック出来てることがわかります。
一般的な注意点ですが、クライアントサイドでの権限チェックは処理を呼び出す呼び出さないといったことや、要素の表示・非表示くらいにしようねってのがあります。本当に権限が必要な処理はサーバーサイドでも、きちんとチェックしましょう。
アプリでロールを管理しよう
グループはグループでいいんですが、これは Azure AD 全体に影響があるものになります。 部門単位でちょっと開発するアプリで、そこの設定変更は影響が大きすぎるので、アプリ内で適当にロールみたいなのを割り当てたいといったこともあります。
その場合は、以下のドキュメントにあるように、Azure AD でアプリ単位でロールを定義出来ます。
Azure AD の「アプリの登録」でサーバー側のアプリを開きます。そして、マニフェストを開きます。そして、appRoles
に JSON 手書きでロールを定義していきます。硬派ですね…。
例えば Users と Admins みたいなロールを追加するなら以下のような感じです。id には GUID を指定しますが、これは PowerShell Core で New-Guid
と打って生成するのが楽でした。
"appRoles": [{"allowedMemberTypes": ["User" ], "description": "Administrators.", "displayName": "Admins", "id": "fc998089-b40c-40c1-af3c-161382ac6422", "isEnabled": true, "lang": null, "origin": "Application", "value": "Admins" }, {"allowedMemberTypes": ["User" ], "description": "Users.", "displayName": "Users", "id": "237d8777-e380-4b5d-bb4a-192c126640f5", "isEnabled": true, "lang": null, "origin": "Application", "value": "Users" }],
次に、ユーザーにこのロールを割り当てます。これは GUI でやれます。安心。やりかたは Azure AD の「エンタープライズ アプリケーション」を選択してサーバー側のアプリを開きます。
そして「ユーザーとグループ」を選択して「ユーザーの追加」を選択します。
そうするとユーザー(複数人選択可能)とロール(先ほど JSON で手書き追加したやつ)が選択できるのでお好みな感じで割り当てます。
このように構成しておくと、http://schemas.microsoft.com/ws/2008/06/identity/claims/role
という ClaimType で Role に指定した Value が入ってきます。なので、今回は Admins じゃないとダメにしたいので、Startup.cs の ConfigureServices の AddAuthorization メソッドを以下のように書き換えます。
services.AddAuthorization(options => { options.AddPolicy("Admins", policy => //policy.RequireClaim("groups", "admins グループのオブジェクト ID ") policy.RequireClaim("http://schemas.microsoft.com/ws/2008/06/identity/claims/role", "Admins") // 今度はこっちね ); });
クライアント側で JwtToken を解析した結果の Claim には Type が roles に入ってるので以下のように書き換える感じになります。
var jwt = new JwtSecurityToken(accessToken); //var groupId = jwt.Claims.FirstOrDefault(x => x.Type == "groups")?.Value;//if (groupId != "admins グループのオブジェクト ID")//{// MessageBox.Show("You are not a member of admins group, right? Please do not click this button.");// return;//} var roleName = jwt.Claims.FirstOrDefault(x => x.Type == "roles")?.Value; if (roleName != "Admins") { MessageBox.Show("You are not a member of admins group, right? Please do not click this button."); return; }
これで、Azure AD のセキュリティグループで設定したときと同じように動きます。
まとめ
Azure AD が導入されているなら、これでログイン出来る社内アプリを作らない手はないので使ってみましょう。
ソースコードは、これまでの GitHub のリポジトリーに moreauth というブランチを作ってそこに上げてます。
Surface Pro ユーザーが Dell の New XPS 13 2-in-1 を買ってみた感想
買ってみたのは以下の New XPS 13 2-in-1 です。
Dell Cinema & HDRディスプレイ搭載のXPS 13インチ7390 2-in-1ノートパソコン | Dell 日本
構成は以下のような感じ
- メモリ 32 GB
- CPU Core i7
- SSD 1TB
- キーボード配列 US
- 色は黒
買い替え前は Surface Pro 2017 年モデルです。
- メモリ 16 GB
- CPU Core i7
- SSD 512 GB
- キーボード配列 US
- キーボードの色は赤
買い替え理由
最近 Docker や Visual Studio や Visual Studio Code や Web ブラウザー(意外とメモリ食いますよねブラウザー…)を立ち上げてるとメモリーを 13 GB 以上軽く食うようになったので 32 GB メモリーで、そんなに重くないマシンを探してました。 でも、32 GB メモリーがあるノートは一目見てゲーミング PC と思うようなごつい見た目と重さを兼ね備えてたので持ち運び用との自分にはちょっとハードでした…。
あと、ディスクも 512GB だと残り 100GB を切るようになってきたので少し心もとない気持ちになってました。
そんな中、1.33 kg で 32 GB メモリの 13 インチのマシンが出る!というのを黒澤さんに教えてもらいました。
今月XPS 13 2in1が出るはずです。
— 黑澤㌠ (@kurosawa0626) August 13, 2019
1 TB モデルもあるということで出たら購入すると意思決定して購入。ちなみに Surface Pro は、新しい里親が見つかりました。
使ってみた感想ハード面
ベゼル狭い!!
写真が非常に見づらいのですが…
画面サイズの割に本体が小さいのですが、その理由はこのベゼルの狭さ。 最初に開封したときに思ったより本体小さいな?って思ったのですが、本当にベゼル狭いので本体の小ささに対して画面が広いです。
キーボードにちょっと違和感
今まで 5 年くらい Surface Pro / Book を触ってきてたのですが、それに比べるとキーボードが広いです。 普通のアルファベットの部分だけでも、ちょっと思ったより広くて違和感を感じますが、これはまぁいい感じです。
一番の違和感は Home, End, Page up, Page down キーが微妙な位置にあると感じるところ。
こんな感じに一番右上に電源兼指紋センサーがある pic.twitter.com/iecajJuPA2
— かずき(Kazuki Ota) (@okazuki) September 9, 2019
まず、矢印キーの左右の部分に pg up
と pg dn
があるので、左右キーの上側を押す癖がある人は左右に移動しようとしてページが飛ぶことがあります。
home と end は F10, F11 の部分にあって、私は F1 ~ F12 キーを割と使います。 なので Fn + F10, Fn + F11 で home, end になるように設定してるので若干エディターで行頭・行末に行くのがめんどくさいです。
Surface シリーズだと Fn + 左矢印、Fn + 右矢印でやってたので慣れの問題ですが慣れるまで時間がかかりそうです。
あと、上のツイートにあるとおり一番右上のボタンが電源ボタン兼指紋センサーなので、全体的にその列のキーが左に寄ってるような印象です。まだ直観で F7 を押してカタカナに変換が出来ないです。
まぁ、ここら辺は慣れの問題であって Surface シリーズとキー配置が違うね!というところですね。
指紋センサー
今まで Surface は Windows Hello の顔認証だったのですが、XPS 13 2-in-1 は指紋認証になります。 たまに、Surface のころの癖で PC を開いて PC の画面を見ながら「俺の顔はちゃんと見えてるか?」って思ってログインするのを待ってることがありますが、指紋認証なのでログインは当然されないということがあります。
まぁ、これも些細な慣れの問題。ボタンに指を置けば OK なので。
外部インターフェースが USB Type C が 2 個 + α
という感じなので、持ち歩くアダプターなどは当然それに合わせたものが必要になります。 私は Macbook 用に USB Type C からのディスプレイ出力(HDMI、VGA)・有線LAN・USBへの変換してくれるものを持ってたのでそれを流用してます。
家は、ドックをセットで買ったので家に帰ったらドックから伸びてるケーブル(USB Type C)をさせばディスプレイも電源もいい感じになるので快適です。
ということで、USB Type C 関連グッズがあれば問題にならない感じでした。
ソフト面
Windows 10 Pro 選んだので、まぁ Windows ですね。 自分のメインが GPU を酷使しないような開発系ソフトを使う感じなのでメモリ 32 GB と CPU がいいことが条件でした。
その面では超満足。
Visual Studio 2019、Visual Studio Code、Edge Preview(Chronium 版 Edge)、Microsoft Teams、メーラーなどの普段使いのアプリを立ち上げてる状態でこれなので、ここから更に追い Docker とかをしても余裕がありそうでとても素敵です。
各種ツールのインストールを Chocolatey でインストールしていく運用に変えてみたので XPS とは関係ないですが使い続けてみてどうなるのかが楽しみです。
ディスプレイは 4K なのですが、以下のように 175% で使ってます。
Full HD のディスプレイを繋いでみると解像度のでかさがわかる。
実際に画面全体のスクリーンショットをとってみるとこんな感じです。
上の、この記事書いてる画面のフルHDのモニター小さくてウケる。
まとめ
メモリ 32 GB で持ち運びが苦じゃない程度の重さなのが最高!
Promise 対応していないコールバック形式のライブラリーを Promise にしたい
Node.js v8.1 で util.promisify
っていう関数が追加されてたんですね。
古き良き伝統にしたがったコールバック形式の関数を Promise を返す形にしてくれる。つまり await 出来るようになる!
ということで、今時コールバック形式のライブラリなんて…と思ってたら azure-storage
がそれでした。
ということで使ってみよう。
使い方は簡単。
const f = util.promisify(hogehoge);
のようにコールバック形式の関数を渡してやると Promise を返す関数になるので…
await f();
とすれば OK。
じゃぁこんな感じに使えるかな。
import express from'express';import util from'util';import azure from'azure-storage';interface EntityType { PartitionKey: azure.TableService.EntityProperty<string>; RowKey: azure.TableService.EntityProperty<string>; Value: azure.TableService.EntityProperty<string>;}// Azure ストレージエミュレーターにつなぐconst client = azure.createTableService('AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;DefaultEndpointsProtocol=http;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;TableEndpoint=http://127.0.0.1:10002/devstoreaccount1;');asyncfunction getAll(){await util.promisify(client.createTableIfNotExists).bind(client)('test');const query =new azure.TableQuery().where('PartitionKey eq ?','a').select('Value');// 本当は util.promisify(client.queryEntities<EntityType>) みたいにしたかった。この場合どうするのが正解なんだろうreturnawait util.promisify(client.queryEntities).bind(client)('test', query,nullasany)as azure.TableService.QueryEntitiesResult<EntityType>;}const app = express(); app.get('/',async(req, res)=>{const result =await getAll(); res.json(result.entries.map(x => x.Value._));}); app.listen(3000);
注意点は、関数内部の this をちゃんと bind で指定してあげることくらいかな?
適当にデータつっこんだテーブルを用意して実行して localhost:3000 にアクセスすると…
返ってきた!!
実際使うとしたら都度都度 util.promisify するのではなく、都合のいい感じのラッパークラスを用意する感じかなぁ?
import express from'express';import util from'util';import azure from'azure-storage';interface EntityType { PartitionKey: azure.TableService.EntityProperty<string>; RowKey: azure.TableService.EntityProperty<string>; Value: azure.TableService.EntityProperty<string>;}// こういうラッパークラスにめんどくさい処理は押し込んでおいて…class TableService {constructor(private tableService: azure.TableService){}public createTableService = util.promisify(this.tableService.createTableIfNotExists).bind(this.tableService);private _queryEntities = util.promisify(this.tableService.queryEntities).bind(this);publicasync queryEntities<T>(tableName: string, query: azure.TableQuery, token: azure.TableService.TableContinuationToken){returnawaitthis._queryEntities(tableName, query, token)as azure.TableService.QueryEntitiesResult<T>;}}const innerClient = azure.createTableService('AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;DefaultEndpointsProtocol=http;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;TableEndpoint=http://127.0.0.1:10002/devstoreaccount1;');const client =new TableService(innerClient);asyncfunction getAll(){// 使う側は意識しなくてよくするawait client.createTableService('test');const query =new azure.TableQuery().where('PartitionKey eq ?','a').select('Value');returnawait client.queryEntities<EntityType>('text', query,nullasany);}const app = express(); app.get('/',async(req, res)=>{const result =await getAll(); res.json(result.entries.map(x => x.Value._));}); app.listen(3000);
まとめ
ライブラリー側で await 出来るように Promise 返してくれるようにしてほしさある。
Visual Studio Code で CLI から今開いている VS Code でフォルダーを開くコマンド
今まで
$ code .
してたのですが、これだと新しいウィンドウが開いてしまっていました。-r
オプションをつけると今あるものを再利用してくれるようです。
さらに --add
オプションはワークスペースにフォルダーを追加してくれる。
ということなので、例えば sample1
フォルダーと sample2
フォルダーを作って git init
でもした後に、VS Code で同じワークスペースとしてフォルダーを開くには以下のコマンドを打てばいい感じです。
$ mkdir sample1 $ cd sample1 $ git init $ code -r . # 一度画面がリフレッシュされるので VS Code のターミナルを開きなおして $ cd .. $ mkdir sample2 $ cd sample2 $ git init $ code --add .
という感じでいいです。やってみた感じだと以下のような風になります。
今までフォルダー開くダイアログ使ってたのがはかどることになりそう。
コマンドラインオプションのドキュメントはこちら。
Visual Studio Users Community #1 で「はじめよう Azure Functions」というタイトルで発表してきました
久しぶりにプライベートな活動として登壇してきました!!
www.slideshare.netAzure Functions の開発してましたが、Azure ポータルを一回も開かなかったセッションでした。 今回紹介したものの中では SignalR Service はローカル版がないので実際にクラウドに作ってますが、こちらもフリープランがあったりします。
開発してローカルで動かしてる限りは 0 円ではじめられるので是非是非セッションの目的であった Azure Functions いいなって思ってもらえたら試してみてください!
Visual Studio の場合
Visual Studio Installer で Azure にチェックを入れてインストール
Visual Studio Code の場合
以下のドキュメントあたりで環境の作り方があります。
プログラミング言語勉強用の環境を Visual Studio Code + Docker で手に入れてみる
Visual Studio Code を入れます。
Visual Studio のリモート開発の拡張機能を入れます。
そして docker を入れます。
Windows の人は入れたら設定からドライブ共有をオンにしておきましょう。
Python 3 の環境が欲しい
適当なフォルダーを Visual Studio Code で開きます。
F1
や Ctrl + Shift + P
あたりでコマンドパレットを出して Remote Add
あたりで検索すると Remote Containers: Add Development Container Configuration Files...
という項目が出てきます。
どんな開発環境が欲しいのかリストが出てくるので Python 3 を選びましょう。ファイルがいくつか追加されて以下のようなものが表示されるので Reopen in Container
を選択します。
初回は docker のビルドが走るのでしばらく待ってると…、Python の入ったコンテナーで先ほどのフォルダーが開かれます。
ハローワールドしてみましょう
F5 を押して Python File を選択すると実行されます。
ブレークポイントを置いてると…ちゃんと止まります!!
ホストには Python 入ってないというのを示そうとコマンドプロンプトで python って打ち込んだらストアが開いてびっくりした。
再度開くときは Remote Containers: Open Folder in Container
あたりから開けばいい感じになります。
フォルダーダイアログがだるい場合は…
$ cd 目的のフォルダー $ code -r .
あとはコンテナーで Reopen しますかと聞かれるので Reopen してもらうだけです。
まとめ
Docker があれば割と何でもできて便利。 VS Code でシームレスに使えるところが神。
docker 上に Vue.js + TypeScript の開発環境を整えて Visual Studio Code で開く
とりあえず新しいマシンにして docker が快調に動くようになったので少し色々試してみてます。昨日は Python の環境で今日は Vue.js で試してみました。
これのいいところは、誰かが環境整えたら他の人は VS Code で開くだけで同じ環境で開発できちゃうところですよね。
環境作り
とりあえず、適当な空のフォルダーを作って、そこにリモート開発の設定を追加します。何かベースにいいのがないかなと探してたら node.js LTS + TypeScript がありました。君に決めた。
生成された Dockerfile に Vue CLI を入れるコマンドを一行追加します。 tslint と typescript を入れている次の行くらいに入れておきました。
# Install tslint and typescript globally && npm install -g tslint typescript \ && npm install -g @vue/cli \
devcontainer.json
に npm run serve
したときに起動する 8080
ポートを追加します。
"appPort": [8080],
あと、Vetur 拡張機能も Visual Studio Code に入れておかないと話にならないので入れる設定をします。devcontainer.json
の extensions を以下のような感じにします。
"extensions": ["ms-vscode.vscode-typescript-tslint-plugin", "octref.vetur" ]
全体はこんな感じになりました。
// For format details, see https://aka.ms/vscode-remote/devcontainer.json or the definition README at// https://github.com/microsoft/vscode-dev-containers/tree/master/containers/typescript-node-lts{"name": "Node.js (latest LTS) & TypeScript", "dockerFile": "Dockerfile", "appPort": [8080], // Use 'settings' to set *default* container specific settings.json values on container create. // You can edit these settings after create using File > Preferences > Settings > Remote."settings": {"terminal.integrated.shell.linux": "/bin/bash" }, // Uncomment the next line if you want to publish any ports.// "appPort": [],// Uncomment the next line to run commands after the container is created.// "postCreateCommand": "yarn install",// Uncomment the next line to use a non-root user. On Linux, this will prevent// new files getting created as root, but you may need to update the USER_UID// and USER_GID in .devcontainer/Dockerfile to match your user if not 1000.// "runArgs": [ "-u", "node" ],// Add the IDs of extensions you want installed when the container is created in the array below."extensions": ["ms-vscode.vscode-typescript-tslint-plugin", "octref.vetur" ]}
出来たら Reopen in container のコマンドをコマンドパレットから実行すると docker build が走ります。
チーム開発とかで使う場合は、何処か適当なリポジトリーに @vue/cli とかまで入れたイメージ作っておいて、それをベースにするほうが各人が全部ビルドしなくていいし各種ツールのバージョンもしっかりそろっていい感じかなぁって思ってます。
ビルドが終わって開かれたので Terminal で vue と打ち込むとちゃんと入ってます!
とりあえずカレントのディレクトリーにプロジェクト作ってみる感じで以下のようにうってみましょう。
$ vue create .
お好みのオプションを選んでさくっとプロジェクトを作ると、ちゃんとできた!!(当然ですけど) Explorer 上でもモリモリファイル出来ていくのが面白い
そして npm run serve
うったら http://localhost:8080
に Windows 側からブラウザーで開きます。先ほど devcontainer.json
に 8080
ポートの設定を追加しておいたので 8080
ポートは自動的に docker コンテナ内に転送されます。
インテリセンスも効くのでいいですね!!
まとめ
普通に普段から node.js 使ったり vue.js やってるならローカルに入れるのが一番いいですがプロジェクトで全体的に環境整えたいとか、ちょっと興味があってやってみたいんだけどローカルに入れるのはなんかやだなぁっていうのには Visual Studio Code のリモート開発機能で Docker 上に開発環境作って開くのとてもよさそうですね。