tech.guitarrapc.cóm

Technical updates

.NET Core で Generic Host を利用する

ASP.NET Core 2.1 で追加された Generic Host (汎用ホスト) は、non-Web App アプリの作成をASP.NET Core と似た書き心地で提供します。

今後のスタンダードとなる見込みですが、どのようにして Generic Hostを利用するのか見てみましょう。

※ 社内向けブログの転載なのでシリーズ化します。

目次

TL;DR;

ここでは、.NET Core のコンソールアプリをGeneric Hostとして実装する方法を見てみましょう。

Generic Host とは

.NET Core 2.1 で追加された Generic Host (汎用ホスト)は、これからの.NET Coreにおける中心となる実装で、現在のASP.NET Core MVC で用いられている WebHostを参考に作られています。今はWebHost は Web専用、それ以外はGeneric Hostで実装ですが、今後はGeneric Hostに集約されていくロードマップです。

https://docs.microsoft.com/ja-jp/aspnet/core/fundamentals/host/generic-host?view=aspnetcore-2.2

HostBuilder を起点にConfiguration、Logger、DI、各種機能をチェーンで追加、初期化できるのが特徴で、ホスト(アプリ) の起動と有効期間をうまく制御しようという意図が垣間見えます。

WebHost (ASP.NET Core) との違い

IHostBuilderをどのように構成するかはほぼ違いがありません。ASP.NET Core MVC では StartupクラスでConfigure したりAddするのが慣例ですが、中身はそこまで大きく違いません。

違いがでるのは、ASP.NET Core MVC がやっていた暗黙的なインフラ部分です。

ASP.NET Core MVC では、ASPNETCORE_ENVIRONMENT環境変数があると自動的に値を見てDevelopment なら Developmentに切り替え、なかったりするとProduction として扱ってくれました。

しかし、現状の Generic Hostにこの仕組みはなく、.NET Core 3.0 で入る予定のようです。

https://github.com/aspnet/AspNetCore/issues/4150

それまでは次のような処理が必要なので入ると嬉しいですね。

            .ConfigureAppConfiguration((hostContext, configApp) =>
            {
                hostContext.HostingEnvironment.EnvironmentName = System.Environment.GetEnvironmentVariable("NETCORE_ENVIRONMENT") ?? "production";
            })

Generic Host を使う目的

ASP.NET Core MVC と似たものになるように感じますが、Stack Overflow やGitHub Issueを見てるとまだこれからすそ野を広げていく感じとも感じます。特にこのあたりは共通しています。

  • Web UI と Web API を構築するプロセスの統一
  • テストの容易性を考慮したアーキテクチャ
  • 最新のクライアント側フレームワークと開発ワークフローの統合
  • 組み込まれている依存性の注入

Configure 処理の追加

Generic Host で処理を追加する場合は、Microsoft.Extensions.Xxxxx なパッケージを追加することになります。 例えば先ほどの構成なら、次のパッケージになります。

    <PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="2.2.0" />
    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.2.0" />
    <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="2.2.0" />
    <PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="2.2.0" />

書き心地

WebHost の体験をもとにしているので、書き心地はASP.NET Core (OWIN) とほぼ同じで、定番の実装を組み込むまで一本の流れが出来上がります。

Generic Host を使わずに.NET Core ConsoleでDIする場合は次のように書いていました。*1 個人的にはかなり書き心地が悪くつらい思いをしていました。

gist.github.com

一方で、Generic Host では次のように書けます。

gist.github.com

比べてみると違いは明らかで、Generic Hostの書き心地は処理の追加方法に統一感が出たので好ましいと感じます。

先ほどの Generic Host で組んでみる例では、必要な処理を ConfigureXxxx で追加しました。つまり、そのConfigureXxxx を外せばその処理は追加されません。

特にDIベースでロガーが入るのは確かに便利で、ILoggerで隠蔽されているので、.NET あるなるだったロガーの差し替え面倒だなぁと感じるケースはかなり軽減するように感じています。逆に言うと、こういうのがいらないシンプルな実装では使う強い理由はなく、今まで通り書けばいいとも感じます。

サービス処理をDIする

Generic Host では、実際に呼び出したい処理を次のようにDIできます。

            .ConfigureServices(services =>
            {
               // サービス処理のDI
                services.AddSingleton<IBarService, BarService>();
            })

よくあるDIですが、これを書いておけば実際に処理をしたいクラス(FunctionA とします) で次のように書くと BarService がコンストラクタインジェクションされます。

    public class FunctionA
    {
        private readonly IBarService service;
        public FunctionA(IBarService service)
        {
            this.service = service;
        }

        public async Task HogeAsync()
        {
            // this.service を使って何か処理
        }
    }

まとめ

DI とかテンプレート的に構成していくことを含めて、なんとなくMicrosoft はこの書き方をオシススメテキソウな気配がします。 さくっと動かす系じゃない場合は、こっちの書き方は広まりそうかもかも?

Tips

IHostBuilder.RunConsoleAsync, IHostBuilder.Start, Host.StartAsync, IHost.RunAsync の違い

ref Stack Overflow

https://stackoverflow.com/questions/52413002/whats-the-difference-between-these-ways-to-start-run-a-generic-host-in-asp-net/52413414

ASP.NET Core 3.0ではGenericHostから構成になるかも?

ASP.NET Team のTweetによると、ASP.NET Core 3.0 では、Generic HostにConfigureWebHostDefaults で構成できるようです。

想定通りの書き方ですね。

Ref

.NET での汎用ホスト

アプリケーション停止処理の実装例

*1:まったく同じ処理ではありません

パスワード管理をTeamsId から Bitwarden に移行した

今のパスワード管理に小さな不満があるので長年次のパスワード管理をさがしていたのですが、Bitwardenが今ある全ての望みをかなえてくれました。

bitwarden.com

今回、TeamsId から Bitwarden に全面移行したのでその移行についてメモをしておきます。

目次

今まで使ってきたパスワード管理

個人のパスワード管理は、2015年3月まではMeldiumを使ってましたが Discontinueが発表されてからはTeamsId を使っていました。

www.teamsid.com

TeamsId を見返す

TeamsId (運営はSplashData) の良いと感じてる点は次の通りで多くの面で満足していました価格は$3user/month です。

  • Web上で管理される
  • 少人数でのパスワード共有も管理が容易
  • Chrome拡張があり入力が容易
  • iPhoneアプリもある
  • Googleログインで統制できる
  • G Suite の9 squareに表示できる

一方で、3年つかっていると細かい不満がたまってきました。

  • Web管理画面で描画が遅い
  • 反映までの3-5secの間に他のレコードを触ると意図と異なったレコードを編集することがある
  • 利用者の声が他に全然おらずサービス終わることありそうという気配
  • 日本語サービス名にすると全レコードが表示されなくなる
  • サポートの対応が技術的に掘り下げ弱い

特に日本語サービス名にした時に表示されなくなるのはかなり焦るものがあり、半年前に3度目が発生し改善の見込みが見えないので乗り換えを考えていました。

機能数よりは、少人数での共有のしやすさ、使い勝手の良さを評価しています。

www.itqlick.com

検討したサービス

いくつか検討し、仕事でも使ったりすることで探ってきました。

1Password、LastPass、KeePass、Zohoいずれも使い勝手と価格と少人数での共有の面からあまり満足できるものはなく悩みが多かったです。ちょうど1Password Teams が発表されたタイミングもあり触っていたのですが結局TeamsId でいいやとなっていました。

1password.com

www.lastpass.com

ここ1年は、Dashlane がワンチャンかと思っていましたが、決定的な理由がないのでペンディングしていました。 お高めですし。

無料パスワードマネージャー | Dashlane

やりたいこととのバランス、使い勝手はそこまで変わらないなら乗り換えないというのは自分の行動を見ていてもそういうものかなぁと思います。個人的には、クラウド管理でもいいと思っており、ローカル/自前クラウド管理は避けたいところです。(必要ならやりますが必然性は感じていない)

Bitwarden

Bitwardenはたびたび見ていましたが、あまり興味がでなかったものの2018年末に改めて良いという話を聞いて試しました。

bitwarden.com

オープンソースなのは結構良いと思っているのですが、妙な処理があっても全コードは見てられないので気付けるかなぁと思いつつ。しかし、最悪コード読めるのはいいと感じています。 それでも不安なら kubernetes クラスタ組めばいいと思います。

2FA して2人で共有できればいいので、PersonalもBusinessも無料で十分満たせそうです。 $5 + $2(user) でも十分安いので、課金しています。

YubiKeyに移行も進めているので、Personal だけ Premium もあり得ます。

ブラウザ拡張、スマホアプリ共に十分使い勝手は良く感じます。

Bitwardenの使い方

これは多くの記事があるのであえて書くことはないと感じるのでそちらをどうぞ。

tips4life.me

excesssecurity.com

TeamsIdからの移行

ぱっと使ってみた感じはよさそうなので、今のTeamsId のレコードを移行します。残念ながら TeamsId はありません、同一会社のSpashIdがあるのに。

試しにTeamsIdのエキスポートcsvをそのまま取り込んでみると、すさまじいことになったので推奨できません。Bitwarden のフォルダがまともに消せなくていやになりそうになったのは内緒です。

仕方ないので、Bitwarden の Generic Importフォーマットで取り込みを試みます。ドキュメントではcsv とあります。

しかし、Fields は入れ子のレコードでExportしてみると大変なことになるのが分かります。

folder,favorite,type,name,notes,fields,login_uri,login_username,login_password,login_totp
Social,1,login,Twitter,,,twitter.com,me@example.com,password123,
,,login,My Bank,Bank PIN is 1234,"PIN: 1234
Question 1: Blue",https://www.wellsfargo.com/home.jhtml,john.smith,password123456,
,,login,EVGA,,,https://www.evga.com/support/login.asp,hello@bitwarden.com,fakepassword,TOTPSEED123
,,note,My Note,"This is a secure note.

Notes can span multiple lines.",,,,,

Csvは行を跨ぐ、入れ子データの取り扱いが面倒なので避けたいところです。 ドキュメントにはありませんが、jsonでインポートできます。

{
  "folders": [],
  "items": [
    {
      "id": "2fb7acc2-41dc-4659-9dc3-a9d301283339",
      "organizationId": null,
      "folderId": null,
      "type": 3,
      "name": "card sample",
      "notes": "note line 1",
      "favorite": false,
      "card": {
        "cardholderName": "Jane Doe",
        "brand": "Visa",
        "number": "123456778",
        "expMonth": "1",
        "expYear": "2021",
        "code": "1234"
      },
      "collectionIds": null
    },
    {
      "id": "5a9eb019-e092-43ae-ac30-a9d301279b2a",
      "organizationId": null,
      "folderId": null,
      "type": 1,
      "name": "LoginSample",
      "notes": "note line 1\nnote line 2",
      "favorite": false,
      "login": {
        "uris": [
          {
            "match": null,
            "uri": "https://example.com"
          }
        ],
        "username": "username",
        "password": "password",
        "totp": "authenticator key"
      },
      "collectionIds": null
    },
    {
      "id": "254a958e-dc04-4330-aa05-a9d3012801c0",
      "organizationId": null,
      "folderId": null,
      "type": 1,
      "name": "LoginSample2",
      "notes": null,
      "favorite": false,
      "fields": [
        {
          "name": "custom",
          "value": "value",
          "type": 0
        },
        {
          "name": "custom hiden",
          "value": "value",
          "type": 1
        },
        {
          "name": "custom boolean",
          "value": "true",
          "type": 2
        }
      ],
      "login": {
        "uris": [
          {
            "match": null,
            "uri": "https://example.com"
          }
        ],
        "username": "username",
        "password": "password",
        "totp": "authenticaticator key"
      },
      "collectionIds": null
    }
  ]
}

こちらは素直なフォーマットで型変換も容易です。

ということで、TeamsId Csv -> Bitwarden Json への変換を書きましょう。

下準備

TeamsId は、Export結果にタグは含みません。あくまでもフィールドとしてすべて出てくるので、Bitwardenに取り込みたい項目はFieldに割り当てておきます。

私の場合は、Bitwarden Personalに取り込む場合は、TeamsId でGroupフィールドを作って割り当てたいグループ名をいれました。

実装

パーサーライブラリとして公開しました。*1

github.com

TeamsId のPersonalデータ -> Bitwarden の Personal データ変換、TeamsId の Organizationデータ -> Bitwarden の Organizationデータの両方に対応しています。

Dockerfile、.NET Coreも対応済みです。使い方はSampleを用意したのでどうぞ。

github.com

これに限らずですが、作業はLinqPad でやっていたので C# です。こういうパーサーを書くとき、LinqPadは非常に便利です。

変換結果を見ながら書き換えできます。*2

実装のポイントだけメモしておきます。

TeamsId Csv の解析

C# で Csv パーサーを探していくつか試しましたが、結果 CsvHealper が最も使いやすかったです。AutoMapperもあり、改行レコードがあっても素直に処理してくれます。.NETStandard も対応しています。

github.com

joshclose.github.io

TeamsId はカスタムフィールドがあるとどんどんExport時のカラムが増えるので、40フィールドのマッピングをするのはつらいのでAutoMapperはほしいところです。さて、利用に際してはStreamで読み込みが必要なので軽いラッパーだけ用意しました。

public class CsvParser
{
    private readonly string path;

    public CsvParser(string path)
    {
        this.path = path;
    }

    public T[] Parse<T>() where T : class
    {
        using (var reader = new StreamReader(path))
        using (var csv = new CsvReader(reader))
        {
            csv.Configuration.MissingFieldFound = null;
            var records = csv.GetRecords<T>();
            return records.ToArray();
        }
    }
}

TeamsId のフィールド解析、値取得

TeamsId のExport Csv は、次のフォーマットになります。

description,note,Field0,Type0,Value0,Field1,Type1,Value1,Field2,Type2,Value2,Field3,Type3,Value3,Field4,Type4,Value4,Field5,Type5,Value5,Field6,Type6,Value6,Field7,Type7,Value7,Field8,Type8,Value8,Field9,Type9,Value9,Field10,Type10,Value10,Field11,Type11,Value11,Field12,Type12,Value12,Field13,Type13,Value13,Field14,Type14,Value14,Field15,Type15,Value15

description、note以外は、レコード1つあたり FieldN, TypeN, ValueN の連続です。そのため、今回はリフレクションしてフィールドの値から型にマッピングします。

C# 7 から使えるswitch でのwhen 句を用いることで、強力に条件分岐ができるので、フィールドを特定のプロパティに割り当てたい時に便利でした。

    private FieldRecord ParseFieldRecord(PropertyInfo[] props, Type t, TeamsIdDefinition source)
    {
        var fieldRecord = new FieldRecord();
        // get FieldXX properties via reflection
        var fieldRecords = props
            .Where(x => Match(x.Name, @"Field\d+"))
            .Where(x => GetPropertyValue(source, t, x.Name) != "")
            .Select(x => (key: GetPropertyValue(source, t, x.Name), value: GetPropertyValue(source, t, x.Name.Replace("Field", "Value"))))
            .ToArray();

        // get field's value and categolize each via field name regex pattern
        var secureMemoList = new List<(string, string)>();
        var memoList = new List<(string, string)>();
        foreach (var record in fieldRecords)
        {
            switch (record.key)
            {
                case var _ when Match(record.key, "url") && fieldRecord.Url == null:
                    fieldRecord.Url = record.value;
                    break;
                case var _ when Match(record.key, "email|e-mail") && fieldRecord.Email == null:
                    fieldRecord.Email = record.value;
                    break;
                case var _ when Match(record.key, "username") && fieldRecord.UserName == null:
                    fieldRecord.UserName = record.value;
                    break;
                case var _ when Match(record.key, "password") && fieldRecord.Password == null:
                    fieldRecord.Password = record.value;
                    break;
                case var _ when Match(record.key, "pass|access|secret|pin|token"):
                    secureMemoList.Add((record.key, record.value));
                    break;
                case var _ when Match(record.key, "group"):
                    fieldRecord.Group = record.value;
                    break;
                default:
                    memoList.Add((record.key, record.value));
                    break;
            }
        }
        fieldRecord.Fields = memoList.ToArray();
        fieldRecord.SecureFields = secureMemoList.ToArray();
        return fieldRecord;
    }

    private bool Match(string text, string pattern)
    {
        return Regex.IsMatch(text, pattern, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
    }

    private string GetPropertyValue(TeamsIdDefinition record, Type t, string propertyField)
    {
        return (string)t.GetProperty(propertyField).GetValue(record);
    }

手元の300レコードは素直にインポートできたので、だいたいのケースではつかえそうです。

変換時の注意

Bitwarden Personal では FolderId が GUIDで定義、マッピングしておかないと死にます。(実装は対応済みで、Sampleに定義例があります。)

Bitwarden Organizationでは、OrganizationIdとCollectionIds を定義してマッピングしましょう。(実装は対応済みで、Sampleに定義例があります。)

インポート

変換した結果はJsonで出力されるのでBitwarden で取り込めば完了です!

まとめ

幸せな感じなので少し様子を見てみましょう。

*1:TeamsIdの利用者数からして需要はなさそう

*2:黒字で塗りつぶしたらさっぱりわからない

はてなブログを https 対応するためにmixed content を検知する MixedContentCheckerを作った

前回の記事でhttps 化の前段階として、はてなブログの全URLを取得しました。

tech.guitarrapc.com

https化を有効にすると、mixed content が出るようになるので有効にします。

あとは、https 化したページに httpコンテンツが混じっている時に起こる、mixed content 警告一覧を取得して直してみましょう。

なお、直すのは手作業です。

目次

TL;DR;

dockerでchrome ヘッドレスをSeleniumで動かして、logからmixed content があるか検知してログ出力する。

検知したページは、3パターンの修正のいずれかをちまちまかけていく。

環境

今回は、PowerShell と C# で実装しました。golang は実装中。

いずれの環境も、docker で ubuntu 18.04 環境で実行します。

  • PowerShell : mcr.microsoft.com/powershell:ubuntu-xenial
  • C# : microsoft/dotnet:2.2-runtime-bionic
  • golang : 未実装

処理の流れ

どの言語も変わらず、chrome のログから mixed content を取得します。

  • URLにchrome headlessで自動アクセスする
  • chrome driver を初期化
  • 指定したURLにアクセス
  • ログから mixed content をフィルター
  • 該当ログがあれば markdown テーブルフォーマットで出力

実装

リポジトリおいておきます。

github.com

実行方法は、READMEをみてください。

MixedContentChecker/README.md at master · guitarrapc/MixedContentChecker · GitHub

ローカル実行もできますが、Dockerで動かすことで、手元にSeleniumやChromeヘッドレスドライバーを用意したり、環境初期化で困ったりすることを割けます。

こういうのをローカルで動かす意味はあんまりないので、Dockerで動かすのがいいでしょう。

PowerShell

Dockerfileです。特に何も気にせず、粛々とchrome headless + chrome driver + selenium をいれます。

gist.github.com

前回のサイトマップから全URLを取得するスクリプトを呼び出しつつ、Chrome Driver + Selenium処理を書きます。

gist.github.com

ポイントは、chrome driverに渡した引数 "--no-sandbox" です。

C# では起こらないのですが、PowerShellからChrome ヘッドレスを実行するときは、--no-sandbox がないと実行できないようです。(はまった)

CSharp

Dockerfileです。先ほどと違い、C# のビルド時にselenium + chrome driverは入るので、chrome driverだけ入れます。

gist.github.com

先ほどのPowerShellと異なり、Parallel.Foreachdによる並列処理を用意しました。

gist.github.com

記事が480以上あるため、1つ1つにアクセスしていると終わりません。単純に1ページ5秒としても、2400sec (40min) かかります。実際は、OneDrive の写真埋め込みページなどで60secかかったりしていたのでもっとです。

おおよそCPUコア数で並列がいいのですが、Docker内部への割り当てしだいです。今回は、15並列で不安定になり10並列で安定したのでデフォルト値にしています。Docker実行時にパラーメータを変えたいので、環境変数から値を渡せるようにしています。*1

10並列で、10分ぐらいでおわるようになったのでだいたいこれぐらい感があります。

あとは、先ほど同様にSeleniumでchrome headlessを動かすだけです。

処理の全体はリポジトリをみてください。

MixedContentChecker/Program.cs at master · guitarrapc/MixedContentChecker · GitHub

Golang

[TBD]

修正

3つのパターンで修正します。

  • httpをhttps にする
  • 存在しないURLを消す
  • はてなフォトがembedded記法なら記事の保存しなおし
  • Google Web Master をhttpsで取り直し

http -httpsへの置き換え

心を無にしてやりました。 はてなブログにAPIで取得、保存しなおしはちょっと面倒感があります。

記事を全コピー、vscodeで置換、貼り付け直して保存です。サシミタンポポ

また、デザインページもhttp -> https が必要です。私の場合は、Google フォントと外部cssなどでしたが、ついでにptengine やzenback 当りが邪魔をしていたので外しました。zenbackははてなブログで関連記事機能があるので不要になってました。

github.com

存在しないURLを消す

特にOneDrive のimage埋め込みと2013ぐらいの古い記事でした。

OneDriveは、生imageがOneDriveにあったものははてなフォトにいったんおいています。OneDriveのimage埋め込みは、以前コメントでも指摘受けていたので、もう二度とつかわないでしょう。

存在しないURLは404で取得できるので消しました。

はてなフォト

はてなフォトは、生urlでなくはてな記法によるid埋め込みの場合、記事を保存しなおすことでhttps化されます。 粛々と記事を保存しなおします。サシミタンポポ

help.hatenablog.com

結果

1122 + 296 + 70 + 22 + 187 なので、1697件だったようです。

sample/logs においておきました。

MixedContentChecker/samples/logs at master · guitarrapc/MixedContentChecker · GitHub

手作業なら死んでました。修正は手作業なのでもうやりたくないです。

あと、セキュリティ警告でページ表示できない状況も直ったようです、すいませんでした。

まとめ

golang 書いてから上げようと思いましたが、やってすでに1週間立つのでとりあえず記事にしていくスタイルで。

*1:Docker実行だと、引数より環境変数の方が素直で扱いやすくて好みです。

はてなブログの全エントリーURLを取得する

このブログ、実はhttpのままです。 はてなブログをやめるか考えているのですが、いったんhttps対応を進めましょう。

困るのがmixed content なのですが、とっかかりとしてこのブログの全URLを取得します。

目次

sitemap の取得

全記事のURLを取得するときに考えるのが、Google Web Master でどうやってクローラーにヒントを出しているかです。ご存知の通り、こういう時に使うのがサイトマップです。

ということで、全記事のURL取得は安直にサイトマップから辿ればいいでしょう。

はてなブログのsitemap は、ブログURL + /sitemap.xml でpagenation付sitemapindexを取得できます。 このブログならこのような感じです。

<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <sitemap>
    <loc>http://tech.guitarrapc.com/sitemap.xml?page=1</loc>
    <lastmod>2019-01-06</lastmod>
  </sitemap>
  <sitemap>
    <loc>http://tech.guitarrapc.com/sitemap.xml?page=2</loc>
    <lastmod>2019-01-06</lastmod>
  </sitemap>
  <sitemap>
    <loc>http://tech.guitarrapc.com/sitemap.xml?page=3</loc>
    <lastmod>2019-01-06</lastmod>
  </sitemap>
  <sitemap>
    <loc>http://tech.guitarrapc.com/sitemap.xml?page=4</loc>
    <lastmod>2019-01-06</lastmod>
  </sitemap>
  <sitemap>
    <loc>http://tech.guitarrapc.com/sitemap.xml?page=5</loc>
    <lastmod>2019-01-06</lastmod>
  </sitemap>
</sitemapindex>

あとは、各sitemapごとにアクセスして、記事URLを拾うだけです。

<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>http://tech.guitarrapc.com/entry/2015/11/19/030028</loc>
    <lastmod>2015-11-19</lastmod>
  </url>
  <url>
    <loc>http://tech.guitarrapc.com/entry/2015/11/11/032544</loc>
    <lastmod>2016-09-24</lastmod>
  </url>
   <!-- ..... continue -->
</urlset>

結果がurl一覧で取れればいい感じに使えそうです。

http://tech.guitarrapc.com/ 
http://tech.guitarrapc.com/about 
http://tech.guitarrapc.com/entry/2019/01/05/060326 
http://tech.guitarrapc.com/entry/2019/01/05/044741 
http://tech.guitarrapc.com/entry/2018/12/22/235927 
http://tech.guitarrapc.com/entry/2018/09/29/165215 
http://tech.guitarrapc.com/entry/2018/09/29/154004 
http://tech.guitarrapc.com/entry/2018/09/29/151114 
..... continue

sitemap は所定のフォーマットに沿っているxmlなので適当に処理します。

今回はPowerShell、C#、Golang それぞれで書いてリポジトリにおいておきました。順にみていきます。

github.com

PowerShell

PowerShell 6.0以降 で動作します。(5.0で動かす場合は、Invoke-WebRequest-UseBasicParsing スイッチを追加するといいでしょう)

gist.github.com

特別に難しいことはないのですが、PowerShellの場合はXML型が担保できれば要素名をキーとして辿ることができます。 nullの取り扱いがゆるいこともあり、シンプルに書けます。

    [xml]$index = $res.Content
    $sitemaps = $index.sitemapindex.sitemap.loc

CSharp

C# 7.3 w/.NET Core 2.2 で動作します。*1

gist.github.com

C# でXMLだと、namespaceを毎度指定することになるのですこし面倒な感じがあります。

XNamespace ns = "http://www.sitemaps.org/schemas/sitemap/0.9";
var sitemaps = XElement.Parse(res).Descendants(ns + "loc").Select(x => x.Value).ToArray();

Golang

go-sitemapを使うとシンプルになります。

github.com

gist.github.com

練習なので、既存のパッケージを使わずstructを定義して、xmlのUnmarshal で割り当ててみます。

gist.github.com

安直に書いてみたのですが、こんな感じなのでしょうか? slice で要素数が事前にわからないので拡張に任せるのがいやなのと、要素確保をした場合に最初のappendで無駄になるのはどうするといいのかなぁ。

まとめ

とりあえず全ブログ記事URLが取れたので、次はmixed content の警告です。

*1:C# 8.0 / .NET Core 3.0 でも動作します。

2018年を振り返って

2019年になって4日経過しました。

2017年の振り返りに続いて、2018年の振り返りをしてみます。

tech.guitarrapc.com

目次

総合

2018年の初めにこう書きました。

流れは年末から変わらず。C# / Swift / TypeScript / Serverless / Container がメインです。やること多いので、いっこずつ片付けます。 やりこんでできないより、やりきるは何より大事です。できるを最低条件に過程をよくするのは力量です。自分でハードルを挙げておいて、工夫してよじ登るのが続くのでしょう。 インフラ、サーバーサイド、クライアントサイドの視点からどうやるといいのかを考えられるようになったのが2017年の最大の獲得であり、今後試されます。

2018年はクライアントとサーバーサイドの両方をやっており、アーキテクチャから考えて実装までやることが多かったです。クライアントとしてはHoloLensアプリ開発に携わっており、2017年に得た蓄積を2018年発展させることになりました。サーバーサイド/インフラは、Python/C#/Docker/Serverlessが主になり、一方でSwift/TypeScript を触る機会がかなり減りました。クロスプラットフォームで動かすことが多いため、Golang を触る機会が明らかに増えたのも目立ちました。転職、起業が入って想定よりも少し色々やることになりました。

総じて、自分の経験を活かしつつ足りない部分を学びつつ適用していくことの連続でした。今までの延長のようで、実際はOS、クラウドともに環境を選ばなくなっており一部に安住も固定もできなくなったという意味で、自分の未熟さと向上が求められる一年でした。

退職と転職と起業

グラニ社は2018年退職しました。ちょうど5年いたことになります。入社当初から「3年経ったら辞める or @neuecc がいなくなる時は私もやめる」と伝えていたのですが、2年延長するぐらいには好きな会社でした。延長を振り返ってみます。

1回目の延長は3年経った一昨年で、「黒騎士と白の魔王」のリリースを見届けたいこと + ゼロからVR部門の立ち上げに誘われたことからあまり機会ないのと面白そうと思って伸ばしました。

2回目の延長は昨年で、部門立ち上げてアプリもいくつかリリースしたころで、「黒騎士と白の魔王」のリリース間近だった + クローズドβで問題が確認できないことから社内のモニタリング基盤を一新することになりリリースを成功させるために延長しました。

このモニタリングの変更によって開発から運用まで根底から取り組み変えることになったため、安定するまで見届けたいという思いがありました。このあたりはグラニの開発者ブログで書きました。

engineering.grani.jp

2018年は延長する理由がないので1月にいくつかお会社に訪問して、面白そうなプロジェクトに参画しようと考えていましたが身内の緊急事態があり断念することになりました。*1と思いきや3月に@neuecc が退任することになったので予定通り退職しました。グラニは、neuecc に誘われたから入りneueccがいるからいたという意味で、人に惹きつけられて働くという試みでしたが、想定以上に楽しく自分を鍛える場となりました、ひとえに多くの同僚と周りの人に支えられた結果だったので心から感謝しています。やりたいことと人と仕事が一致していたという意味でとても面白かったです。

3月いっぱいでグラニを退職することにしたものの、どこに行くか決めておらずいくつか会社を訪問し、4月に某社に転職し2週間で退職しています。人生で一番早かったのですが、入社までにかかわった方には申し訳ない限りです。入社や退職エントリーを書いていないことからも詳しい理由は書きませんが、あくまでも自分の想定との乖離が激しかったためであって、その会社やメンバーに起因するものではないです。詳しく聞きたい方はご飯に行きましょう。

で、5月に起業しました。2018年4月時点では後2年先になる想定だったのですが、2年で得るもの、やることを考えた末でした。*2多くの人に支えられてまずはスタートを切っています。知らないことばかりで頭が回らない、日々いっぱいいっぱい感がありますが、本当に周りの人に助けられています。とはいえ年末の2週間は、手痛いドジを2回踏み、日常生活すらも余裕を失い、体力も限界に近かったので2019年はこれぐらいは余裕でクリアできるはず。*3

さて、起業したものの2018年は一人会社なので個人事業主とあまり変わりません。*42018年は、知人の紹介や打診で複数のお会社でお手伝いをしています。

プログラミング

2018年は、前半と後半でやっていることが全く変わっています。 前半は、ASP.NET MVC から ASP.NET Core 2.0/Terraformがメイン、後半はお手伝い先に合わせていろいろでした。

最も使ったのは、C# ですが Golangを読む機会が大きく増えたように思います。

git コミットは、まぁこんなものでしょうか。PowerShell本のコミットが混じっていてちょっとノイズがはげしいです。

CSharp

お仕事としては、ASP.NET MVC の ASP.NET Core MVC 化が大きなトピックでしたがやり残しになったのが残念でした。 一方で、HoloLens + Unity でのお仕事があり、CI/CDの構築からサーバーサイド連携、MVP4U アーキテクチャなどチャレンジができたのは良かったです。

特にMVP4Uは、Zenject による DIをアーキテクチャのために使う失敗に対するカウンターになったのが大きく、DIに対する自分の中の一喜一憂が大きく浮き沈みしました。ASP.NET CoreのDIは、フレームワークが組み込んでしまっているので、どうこう考えるより先に使えばいいでもその場は構わないと思いますが、Unity のDIは自分で意図をもって用いることになり、ここで失敗をしたのは良かったです。現状Unity でDIは、テストかScriptableObjectの解決以外は使いたくないし避けていくと思います。

C# はUnityを除き .NET Core に集約されました。.NET Framework ありがとう、さようなら、でいいと思います。ぐっばい。とはいえ、macOS、Linux 使っていますが、.NET Coreなアプリを動かすことないんですよね、たいがいコンテナ上で動かしてサービス組むので、.NET Core というより、コンテナで動かせる *5という見方です。コンテナとして考えて、開発はC# でできるのはCI/CD、オーケストレーションの全方位でメリットなのでようやく追いついてきた感じがします。

面白かったのがUWPアプリを書いていたことです。HoloLens のごにょごにょで必要になり書いたのですが、昔からXAMLは苦手な部類にあってようやく書ききりました。書きやすいは書きやすいのですが、これを今後も続けるのはどうにもと感じるのでなんともです。UWPも制約の高さが結構あからさまに出てるのが微妙で悩ましいです。今後のUWP全然光が見えないのですが、本当にこれやるんですかね?

Serverless に関しては、Azure Functions v2.0 がGAしたことで、いくつか書き換えが入り ASP.NET Core っぽくなりました。暗黙的なお約束が増えたのがDIに似た気持ち悪さがあるのですが、フレームワークの成熟ってこういう感じかなぁとも感じるので、シカタナイ気もします。

1つ感じるのは、.NET Standard はかなりきつく、.NET Core 最高感です。特に .NET Core 2.2 から楽になった感じが大きいので、今後は .NET Core 2.2 /3.0 がスタンダードでいきたいところです。

相変わらずC# に感じるビルドが苦しいという状況は光明が見えないです。dotnet (msbuild) でのラップはいいのですが、制御しにくさがなかなか大きいです。

シングルバイナリくるくる詐欺が本当にくるっぽいので、期待しています。シングルバイナリ何がうれしいって、CI/CD/コンテナ でめちゃめちゃうれしいのです。

と書いていたところで、反応もらいました。

運用面はどうとでもなるんだけど、それはugly であることを受け入れた結果なので、ベストではないと考えています。シングルバイナリならコピーと参照がシンプルになるのは明らかです。 ファイルを指定するか、フォルダを指定するかの違いは思ったよりも大きくて、基本的に対象が単独ファイルであることは簡単です。とくにフォルダになった場合、子フォルダも考慮する必要が出てくることがほとんどなので、シングルバイナリの楽さに比べて格段にめんどくさいと常々感じます。シングルバイナリでないだけで、余計な手間とコストがかかっているので「2019年にもなってそんなこといい加減なんとかしたい」というのが気持ちの根底にあります。ただ、dll差し替えで済むケースなど部分的な入れ替えとか考えだすとアカンコレなのでバランスなのでしょう。

Python

お手伝いの流れで、メイン言語で読み書きをしていたという意味で触る機会が増えました。C#以外は全てVSCode なのですが、コードレンズで追っかけるのが楽なので始まりのエディタとしては良さがありました。もともと時々読み書きすることはあったのですが、VSCodeで案外楽になっていたのは良かったです。

某SDKを使っていたのですが、エラーから原因を追いかける時にスタックとレース通りではあるもののエラーメッセージから状況が読めず、周りからも同じ見解であるものの解消まで手間取ったというのがありました。デプロイしないと状況が発生しないこともあり、リモートデバッグどうすればいいんだ、となりながら苦しかったので対処知りたい。

pyenv 苦しいので、全面的にpipenv 使えばいいと思います。あとDockerではpyenv とかせず、おとなしくpython をいれましょう。その方がパスと依存性がきっちり制御できて時間短縮できます。pyenv やばい、pyenv 使いたくなくなった。

そういえば、Azure / AWS / GCP全て Python 3.x で問題なくなっているので、Python 2.x は使わなくなりました。

Golang

Windows/macOS/Linux 全ての環境を触ると出てくるのが、クロスプラットフォームで上手く使えるツールなのですが、Golang の独壇場でした。それもあり、OSSツールを使うことがほとんどなので、Golang を読む機会がかなり増えました。

一方で、余り書く機会を作れずにいたので、Golang書く機会を強制的に設けるようにしはじめました。どうしても必須なので、しばらく維持して書く機会を強制的に増やす方向にしています。

PowerShell

これまでと違ったこととして、2017年末に出版社様から打診をいただいたPowerShell本の執筆が決まり書き上げました。

tech.guitarrapc.com

反省も書いたので、次があれば生かすということで。

tech.guitarrapc.com

PowerShell 6.0 がリリースされ、6.1 がリリースされたことでPowerShellも .NET Core に踏み出しました。*6

PowerShell Coreについては本でも書きましたが、参考になるモジュールを用意していなかったので書いたのがUtf8BomHeader です。

tech.guitarrapc.com

私自身のPowerShell を仕事で用いる頻度は、Windows 100% なのですが、個人では60% Docker (Linux) / 40% Windows なのでまぁこんなものかもしれません。そもそもWindows以外の仕事でPowerShell を用いる理由がないのですが、Windows、特にAzureだと用いているのが楽なケースが散見されるのでまぁそんなものかと思います。

いろんなお会社でいろんなやり方を見ている限り、PowerShell Core はWindowsがない環境で標準的に使われるのはないと感じますがもう少し様子見で。Azure は PowerShell でしかできない操作があるの本当に良くないと思います。

ShellScript

PowerShell よりも圧倒的に書くことが多かったのは当然Dockerで使うからです。Terraform と組み合わせることが多く、このやり方はつらいことへの一直線なので悲しいものがあります。

ある意味では素直なので、このままちょくちょく書くことが多い状況は続くのでしょう。

インフラ

お手伝いで求められ、発揮しやすいのがインフラとアプリをつなぐ部分とサービス全体の安定化です。ということで、インフラ的な部分は2018年もやってます。

特に、Docker、 Terraform + Ansibleが多く、Kubernetes に少し踏み込んだ感じでした。

Terraform

一年通して、Terraform でした。

Azure/AWS/GCP どのプラットフォームも書いていましたが、どこも癖が違って微妙にやりにくいのが解消せず厳しい..... とりあえず、AzureRm Providerに関しては、10以上Issueあげたりしたので、2018年末はだいぶん楽になりました。2018年5月はどうしようかと思いましたが、なんとかなるぐらいにはなってよかった。

AWS がやはり一番書きやすいというかリソースがそろっている感じはあります。書きやすいというと嘘で、Regionが露骨に出てくるので、どうstateを分割するのかは悩ましいところがありますが、Organization含めて、アカウント分離はしやすく安定して模範的な環境だと思います。2018年は、年始と年末でだいぶんTerraform への取り組みや構成、CI、Atomic担保が構成、説明できるようになったので次に行きましょうという感じです。

GCP でも問題なく書くだけのリソースはあります。マルチクラウドがふつうなので、Terrafornを使うのは妥当な判断だと思いますが、もう少しいい手がないかは常に悩むものがあります。2019年はそこかしら。

なお、カクカク詐欺な記事を書いているのでちょっと待ってください。(いつリリースなの)

Ansible

Ansbile + Terraform + Packer はまぁ安定なのですが、あんまり好みではないのでいやですね。ここはもう少しなんとかならないかなぁ感とお付き合いします。

コンテナ

コンテナに関しては、普通に開発フローに組むのが楽で当然あってしかるべきな状況が一段と浸透しています。むしろコンテナを前提にしないとCI/CDの難易度が高いという方が適切かもしれません。

そういう意味では、.NET Core + k8s は良い流れで、来年はこれが普通かなぁと感じます。ローカル開発バックエンドならDocker-Compose でいいのですが。

AWS

2018年は、嫌い/苦手だったサービスに真正面から取り組んだのでちょっとは楽になりました。

Organizations をベースに、マルチアカウントも楽になっているので引き続き楽になっていくでしょうし期待です。

sts の改善や2018年re:Invent でAWSはいい感じにゆるさと硬さのバランスが取れてきたのはあって、とてもバランスが良くなったように感じます。

Azure

2018年最も触ったクラウドはAzureになりました。

Portal を除き、Terraformのみで扱える範囲に関してはずいぶんと楽になりました。一方でリソースの生成、削除の時間のかかりよう、Atomic性が壊れる当りの挙動が一年通して安定しないのでそんなものかなぁと割り切っています。Preview だから、というのが出てきて一向にGAにならなかったりするのは苦しいものがあります。

MSI が使いやすいので、MSIを標準的に使いたいところです。

初期はいいけどサービススケール時以降は使いたくはない感じがありどうしようか悩ましいです。

AWSやGCP含めて使ってて感じるのは自分のサービスの最大の顧客が自分なんだなという感じに対して、「Azure の最大の顧客ってもしかしてMicrosoft じゃないのでは?」という感じがあり、Azure の今後に注目しています。

GCP

会社のメインクラウドはGCPです。理由はいくつかありますが、手間とスケールと安定のバランスから行くとちょうどいいというのがあります。

AWSだと手間が大きい、Azureは安定とバランスが好みではない、ちょうど自分がほしいのに近いのが現在はGCPです。

ヘビーに使うので、しばらくは注力が続くでしょう。

コミュニティ

2018年は、try! Swift や CEDEC に初めて参加しました。両方とても刺激的で面白かったです。会社のふるまいが見えるので、結構おススメです。

Yamagoya Meetup 2019やGoogle INSIDE、Serverless Conf 2018 も参加しましたが、最高でした。また参加したいぐらいには。

Unity 完全に理解したシリーズにも出てましたが、どれも学びがありました。「完全に理解した」は語句としては意味と内容がずれてるのでもう少しいい表現ないのかしら。

コミュニティに人が多いと私はすぐに逃げてしまうので、コミュニティ後の食事とかで見かけたらレアです。必要がない限り、今後も逃げると思います。

2018年は、書いているセッションノートを即公開することを試みてみました。Kibela にセッションノートを書いているのですが、これをそのままはてなブログに張り付ける方式です。ここ数年文章構造を作りながら書くようにしているので余り困らない感じがあります。

記事

21本、すくないです。PowerShellが多いのはいいですが、あんまりよくないです。お手伝い先で週2-4本書いているのでそこで体力使っている感じがあります。2019年は、公開前提で書けるといいかもなので調整してみます。

ライフスタイル

睡眠時間が年間平均3時間10分だったので、もう少し寝るようにします。また、ランチを抜くとかなり快適に一日を過ごせるのが分かっており、朝夜ご飯を徹底するのがよさそうです。昼食べる場合は、炭水化物を抜けばよさそうです。

PCの利用状況、睡眠リズム、徒歩数を計測することで、ある程度自分の傾向と反省を週一で促せるのでキープするとよさそうと感じます。また、ストレッチをしていると体が楽なので、これも継続がよさそうです。

さて、自分の欠点である「個人に依存することはエンジンがかかるのが遅い」を解消するために以下の対策をうちました。

  • 消費時間を計測する
  • Youtube をウェブサイトブロッカー (Beta) でブロックする
  • 期限を設ける
  • 一人でもセルフレビューをいれる

エンジンがかかるのが遅いのは他のことに時間を費やすことから見えるはずなので、何にどれだけ時間をかけているかを計測することを始めました。 私は、Windows / macOS両方を使うので、Rescue Time を使って一日のざっくりとした時間消費を測っています。

www.rescuetime.com

私は普段 Chrome を使っているのですが、自宅の環境でウェブサイトブロッカー (Beta) を使うことで、Youtube を完全にブロックしました。iframeは見れるので、何かの記事のYoutube閲覧には困りません。

期限に関しては、Google Calendar でこまめにスケジュールに組むことを徹底することで、上手くいっています。

セルフレビューは、Geekbot を用いて自己報告をしています。感じるのは、「年配の経験つまれた方に一日1回レビュー受けるサービス」受けたいなぁと。*7

2019年は?

会社としては、2019年は作りたいサービスを形にして何本かリリースするため働き方をシフトします。*8 個人は、会社のサービスリリースと収益化です。注力する言語的にはC# / Golang 、ツールはTerraform + Kubernetes + GCP/AWS/Azure + Unityです。

2019年は2018年より、さらに(自他に)求められるレベルが高くなっており、天井知らずを感じつつ楽しみたいと思います。

あとは自分の習慣の改善なので、ガンバリマス。

*1:予定になく先方にも申し訳なかったです

*2:予定では7年前に起業だったので予定より遅れた

*3:あるいは働き方を変える

*4:事業計画通り

*5:Windows Containerではない

*6:リリースして初めて踏み出せる

*7:なんていうんでしたっけ、すごいいいなぁと思ったけど忘れました

*8:事業計画通り