xin9le.net

Microsoft の製品/技術が大好きな Microsoft MVP な管理人の技術ブログです。

2020 年の振り返りと 2021 年の抱負

明けましておめでとうございます!今年もいい年にしよう ZE!

ということで年末年始。大事なひと区切りなので、忘れないように 2020 年の振り返りと 2021 年の抱負を書き残しておこうと思います。2020 年は本当に密な年でしたね。COVID-19 一色。とにかく密。

f:id:xin9le:20201231205530p:plain

お仕事

リモートワーク

世の中がコロナ禍でリモートワークでてんやわんやな中、僕は緊急事態宣言が出る 1 年前の 2019 年 4 月から地方在住型リモートワークを行っていたという十分な助走期間があったおかげで、本当に何の問題もなく過ごすことができました。ただこの 1 年唯一にして最大のデメリットだった点は、毎月欠かさず行っていた東京本社への出張ができなくなったことです。定期的に会社のメンバーと直接コミュニケーションを取ることは重要だと痛感します。それでも、誰から監視されるわけでもなく毎日当然のように仕事をし、一切の進捗遅れもなく安定して業務を遂行できたのは前年の慣れによるものが非常に大きかったと感じています。

それもこれも、弊社がコロナ禍以前からリモートワークを導入していて風土が出来上がっていたことが最大の成功理由ではないかと思います。福井の田舎にもこういう働き方をコロナ禍前からしている (多少なり珍しい) 人もいるんだよということを知ってもらえれば U ターンする人とか増えないかな?とかなんとか考えたりはします。

ちなみに生活リズムはこんな感じだったみたいです。データって本当に正しいから困るなぁ...(遠い目

担当 EC サイトの .NET Core への移植プロジェクト

2020 年最大の頑張りポイントでした。そもそもなんでこんなことを始めたのかですが、計画自体は 2019 年の年末 (コロナ禍以前) からしていました。それを「やるぞ、絶対やり遂げるぞ!」と完全にギアが入ったのが緊急事態宣言の発動でした。おうち時間需要で担当 EC (美容系) の受注が突然約 10 倍になったことで、バックエンドで動かしていたバッチ等がタイムアウトエラーなどを毎日毎晩吐くようになってしまいました。そのときのビジネススケールの伸びと言ったら構築当初と比べると数十倍みたいな規模じゃないかと思うのですが、そこまで耐えるようにはできていなかったわけです。ビジネス規模に合わせて徐々に改善していければよかったものが本当の本当に突然やってきた。流石にどうしようもない。

いつ明けるかも分からない緊急事態宣言の中、GW がエラー対応でガチのマジですべて潰れ、このまま毎日エラー対応をし続けるのは無理があり過ぎると思って改善活動に当たる決心をしました。のちに緊急事態宣言は解除されて多少マシになるのですが、以降も緊急事態宣言前と比べるとずっと受注数が多く、同じ状況に陥るのは時間の問題だったでしょう。緊急事態宣言解除後のちょっと落ち着いている間に全力で改善を始めなければ、自分が日々不毛なエラー対応による忙しさで殺されてしまう。やるしかない。このシステムが構築されたのは僕の入社前で .NET Framework でできています。2020 年の前半は C# 8.0 + .NET Core 3.x 世代。改善活動と合わせて開発環境を最新にすることで寿命を大きく伸ばすのが使命。

単純な移植ではダメなので自分なりに目標を立てます。あるべき姿を追求し、ビジネスインパクトを限りなくなくすようシームレスな移行を目指しました。

  • 開発環境は妥協なく最新に
  • 実行結果は同じだが、設計 / アーキテクチャはすべてゼロベースで再実装
    • Azure Functions の Consumption Plan を上手に活用し、課金額を抑えつつも今後のビジネススケールに耐える
  • Azure のリソース整理なども同時に
    • 新規開発当初に多人数で開発に当たっていたためか統一性がなく、統廃合すべきリソースが多く見受けられた
  • 事故のないデプロイを可能にする
    • CI が準備されておらず、デプロイ頻度が低いとは言え毎回全集中を要求されてしんどさの極みだった
    • 手動デプロイ ダメ、ゼッタイ!
  • 移行は機能単位で段階的に行い、サイトは一切停止しない (= ゼロ・ダウンタイム)

まず .NET Core でライブラリ群が使えるように .NET Standard ベースのライブラリ構築をいくつも行いました。特に苦労したのが決済プロバイダの API wrapper の実装です。DI ベースの作りになるように大幅に書き換え (というか完全な再実装) をして回りました。このあたりはだいぶ詳しくなったと思います。

  • メルペイ
  • atone
  • クロネコ WebCollect
  • クロネコ後払い
  • Amazon Pay

途中 Amazon Pay (v1) から Amazon Pay (v2) への移行も同時に行うことになって輪をかけて難易度が上がりましたが、それでもゼロ・ダウンタイムは死守しました。途中公式 SDK のバグを見つけて Issue / Pull-Request を出すなど、地味に貢献したりしてました。

結果としてエラーを吐きまくっていたバックエンドはすべて改善に成功し、今では相当なこと (= TimerTrigger が発火しないとか : マジである) が起こらない限り手がかからない子になりました。クリティカルな箇所はやっつけたのですが、それでも .NET Core への移植の進捗は 50% 程度です。当然このミッションだけに注力していたわけではないですが、ここまでで過ぎ去った時間は 9 か月ほど。移植作業ってこんなに大変なんだなと痛感しています。とは言えまだ .NET Framework な部分を駆逐し切ったわけではないので、今年もそのミッションの達成に向けて頑張ります。

コミュニティ活動

ほぼ、なくなりました。定期的に社外の方々に会ってモチベを維持するという大切な場が消えたことは大変残念です。

YouTube 配信

そのポッカリと空いた穴を埋めるために YouTube 配信を始めました。当初は YouTube 配信をするつもりはなかったのですが、岩永さん (@ufcpp) が配信を始めた理由をアップしているのを見て「素晴らしい」「その通りだ」と感動して即真似しましたw

とにかく自分のモチベの火を消さないためにも勢いが大事だと思って、まずは自分のチャンネルを作り、ノウハウなんて全くないけどとりあえず配信ボタンを押すところまでやってみました。ネタは何でもいいやと思って、なぜか LINQPad で Win32 API を叩いてますw

最初はひとりだったのですが、部屋でひとりで喋るのはメチャクチャしんどくて心が荒むということに改めて気付きました。このままでは自分が継続できない。すぐにそう思ったのでゲストとして師匠の小島さん (@Fujiwo) をお呼びして配信してみたりしました。

これが自分の中ではとても良い体験でした。配信をするならこのスタイルじゃないとダメだと思って、Grani 時代の同僚の南さん (@_y_minami) と一緒にチャンネルを運用をはじめて今に至ります。

最近は「南さんが (本気で) 仕事が忙し過ぎる」のと、ひょんなことから始めることになった「岩永さんのお手伝い」として配信へのゲスト参加を理由に自チャンネルの更新が止まってますが、またタイミングを見て配信するんじゃないかと思います。結果として夏から数か月、ほぼ毎週のように何等かの形で YouTube 配信を継続していますが、とても楽しめています。半隔離生活をしている自分の精神衛生を保つ意味で非常にポジティブに機能しました。

C# Japan Discord

先の YouTube 配信の中で「作ってみる?」という話になったので、これも完全にノリと勢いだけで鯖立てしました。執筆現在でなんと 597 名もの方にご参加いただけました

雑談から記事 / OSS のシェア、疑問/質問の解決などなど、日本中の C#er のみなさんの会話を見ているだけでも勉強になります。ご興味のある方は上記招待リンクからご参加くださいませませ。

ふくもく会

福井でやっている所謂「もくもく会」です。顔見知りの地元エンジニア / デザイナーが集まって空間をシェアしながら個々人の作業をするだけなんですが、今年はそういう時間が改めて大事に思えました。秋頃からオフライン活動を再開したので、タイミングが合う限り参加してます。オフライン会の大切さが身に染みる。

プライベート

漫画

正直なところ、仕事に費やした時間がとにかく狂っていて。世の中が「巣ごもり需要」「おうち時間」と言ってやることがなくなって暇っぽい雰囲気とは裏腹に、先の理由で仕事ばっかりの日々でした。そんな中でも漫画はかなり大人買いしていて、例えば以下は新たに全巻揃えました。

特に「約束のネバーランド」の前半部分 (アニメ 1st シーズン部分) が自分の中で過去最大級の大ヒット!鬼ごっこを昇華するとこんなにハラハラするなんて。あと「この音とまれ!」も満足度 100% の大ヒット!相変わらず青春モノに弱い。

ピアノ (聞き専)

僕はピアノを習ったことがないので数曲しか弾けないんですが、ピアノ歴 8 年くらいになる娘がアレコレと弾いてくれたので幸せ気分を味わえました。今年娘が弾けるようになった曲は例年より格段に多く、ピアノ教室の課題になっているクラシックピアノだけでなく、有名なゲーム曲や最新のボカロ曲をアレコレ弾いてくれたのが嬉しかった。レパートリーが増えると家が華やかになりますね。

あと、娘に和音を聞き分ける程度の絶対音感がついている事実を知って驚いてます。鳶が鷹を産んだ模様。

Minecraft

岩永さんの配信で #みんクラufcpp *1 をやりはじめまして、ズブの素人から Minecraft を覚えました。教わったというのが正しいけど!

娘が Minecraft に長年ハマっているというのもあり、共通の話題を持てたことはメチャクチャよかったです。3D 酔いさえしなければ最高w

プログラミング以外の学習

コロナ禍、時事問題に目を向ける機会が多くなりました。そんな中各国の歴史だとか経済、転じて資産運用まで幅広く興味が出ました。あとは英語の発音練習。特に以下の 3 つは本当によく見ました。

プログラミング以外の学習に時間を使うのが楽し過ぎて、IT 関連のニュースをあまり追っていない説まである。

2021 年の目標

まず仕事の面では担当プロジェクトの .NET Core / .NET 5 移植を完遂すること。これは古い技術での塩漬け運用をしない強い意思表示と、実際移植に際して必要となるノウハウの蓄積が目的なので、絶対やり遂げたい。

もうひとつは社内のリソース (少なくとも自分というリソース) を担当箇所の運用だけに使い切らないよう、最大限運用コストを圧縮して余力を残した状態で過ごせるようにすること。個人で作ってみたいサービスもあるんですが、新しいことを始めるためにも自分に余力のある状態を作ることが大事かなと。若干言い訳がましいんですが、現在の会社内での振る舞いとしてもこれが最善だと思っているので引き続き地道にやっていきます。

*1:ワイワイと Minecraft をやる会

Azure Functions SDK の更新に伴う FunctionsStartup の書き方の変更

2020/9/16 に Microsoft.Azure.Functions.Extensions パッケージが v1.1.0 がリリースされました。これに伴い、Azure Functions で DI を利用するときに書くことになる FunctionsStartup にアプリケーションの構成情報ソースをカスタマイズするためのオーバーライド (ConfigureAppConfiguration) が追加されました。

実装サンプル

最近は Azure App Configuration が便利過ぎるのでよく利用するのですが、そういうのを読み込み元として追加するときに利用できるでしょう。実装イメージはこんな感じ。

using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;

[assembly: FunctionsStartup(typeof(AzureFunctionsSample.Startup))]

namespace AzureFunctionsSample
{
    internal class Startup : FunctionsStartup
    {
        // 新たに追加されたオーバーライド
        public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder)
        {
            // 既定の構成を取得
            base.ConfigureAppConfiguration(builder);
            var defaultConfig = builder.ConfigurationBuilder.Build();

            // App Configuration を読み込む
            builder.ConfigurationBuilder.AddAzureAppConfiguration(o =>
            {
                var connectionString = defaultConfig["AppConfig:ConnectionString"];
                o.Connect(connectionString);
                o.Select("*");
            });
        }

        // 前からあったオーバーライド
        public override void Configure(IFunctionsHostBuilder builder)
        {
            var services = builder.Services;
            var config = builder.GetContext().Configuration;
            // DI 登録とかとか
        }
    }
}

これまでは Configure メソッドだけですべてやる必要があったのですが、タイミングを切り離すことができるようになった感じでしょうか。

呼び出し順序

試してみたところ、以下の順で呼び出されました。

  1. ConfigureAppConfiguration
  2. Configure

注意事項

Consumption Plan か Premium Plan の場合、スケールコントローラーの関係でアプリケーション構成の値が変更は許可されていない旨の記述がありました。実行時エラーになりそうなので、気を付ける必要がありそうです。

Microsoft 365 (旧 Office 365) のアプリを個別インストールする

f:id:xin9le:20200831010352p:plain

昔 Office のセットアップをしようとしたらアプリごとにインストールするかどうかを選択できました。しかし Microsoft 365 (= Office 365 からリブランドされた) のセットアップを叩いても、インストールしたいアプリを選択できません。

つまりどうなるかと言うと、こんなにたくさんのアプリが一気にドカッとインストールされます。問答無用。

  • Excel
  • Word
  • PowerPoint
  • Outlook
  • Access
  • Groove
  • Lync (= Skype for Business)
  • OneDrive
  • OneNote
  • Publisher

正直全くいらん。使うのは Excel / Word / PowerPoint だけです。少なくとも僕は。ということで、個別にアプリを選択してインストールしたい需要が非常に高いです。

Office 展開ツールを取得

なんとかして消しましょう。消すには Office Deployment Tool というものが必要です。ODT って言う略記が存在するほど高頻度で使われるものとは思えないけど、業界では ODT って言うらしいです。とにかくダウンロードして自己解凍 exe を実行し、setup.exe を取り出しましょう。

f:id:xin9le:20200831004755p:plain

構成オプションファイルを作成

次に、除外するアプリを選択するために構成ファイルを準備します。僕の場合は Excel / Word / PowerPoint 以外不要なので以下のような感じで、除外リストを記述します。

<Configuration>
    <Add OfficeClientEdition="64">
        <Product ID="O365ProPlusRetail" Channel="Current">
            <Language ID="ja-jp"/>
            <ExcludeApp ID="Access"/>
            <ExcludeApp ID="Groove"/>
            <ExcludeApp ID="Lync"/>
            <ExcludeApp ID="OneDrive"/>
            <ExcludeApp ID="OneNote"/>
            <ExcludeApp ID="Outlook"/>
            <ExcludeApp ID="Publisher"/>
        </Product>
    </Add>
</Configuration>

構成オプションはアレコレ記述できるので、公式ドキュメントを参考するとよいかと思います。

セットアップを実行

上で準備した構成オプションファイルを Office Deployment Tool に食わせて実行しましょう。コマンドは以下のような感じで引数を与えて実行するだけです。

.\setup.exe /configure MyConfig.xml

これで不要なアプリたちとはオサラバ!平和になれましたね!

Azure Functions の TimerTrigger の処理時間を可視化する

最近、業務でバッチ処理の最適化を行っています。弊社のバッチ処理は Azure Functions の TimerTrigger で書くことが多いです。主な理由は、仮想マシンなどを利用することなくマネージドな環境でタイマーを発動できて丁度よいという点ですが、Consumption Plan (= 従量課金プラン) を利用していると非常に安価に済むというのもあります。

なのですが、Consumption Plan だと実行時間の上限が 10 分と決まっています。これが結構曲者で、タイムアウトを避けるためには処理時間を短くする努力する必要が出てきます。もしそれでも超過してしまう場合は Premium Plan か App Service Plan を選択することになりますが、できる限り 10 分以内に抑える努力をして Consumption Plan を利用したいものです。

なぜこんなことを言っているかと言うと、ちょくちょくタイムアウトが発生していて困っていたからです...(だいぶ直した

可視化に利用する KQL

パフォーマンス改善を行うに際してはまず状況の把握が大事ということで、可視化を図ります。複数あるバッチ (TimerTrigger な Function) の中でも特にどれがインパクトを与えているのかを調査し、改善に繋げます。今回は Azure Functions のログを Application Insights に送り込むようにしている前提で、Application Insights のログを KQL を使って可視化してみました。

requests
| where customDimensions.TriggerReason startswith 'Timer fired at'
| project timestamp, name, duration
| render scatterchart

このクエリを使って実際に運用している環境のログを取り出してみると、以下のようになりました。Functions の開始時間と処理時間の関係がひと目で分かりますね。

f:id:xin9le:20200412232757p:plain

上図によると概ね 5 秒 ~ 15 秒程度で処理が完了していますが、ものによっては 250 秒近くかかっていると分かります。これらに改善の余地があるかを確認すると良いでしょう。他にも

  • 処理の実行を夜間にすることが望ましいか
  • 期待した頻度/回数で実行されているか (= 無駄に実行されていないか)

などの把握や判断にも繋がるでしょう。先の KQL はどの環境でもコピペで使えるはずなので、ぜひお試しください。

XSitemaps - SEO のためのサイトマップファイルを作る

業務で (主に) EC サイトの実装 / 運営をしているのですが、EC サイトと言えば SEO!お客様が来ないことには商売にならないので、検索エンジンを上手に操って流入を確保することは非常に重要です。ということで、ページのクローリングを制御するために Google Search Console などを使うわけですが、そこで必要になるのがサイトマップファイルです。サイトマップファイルにはいくつか種類があるのですが、主に XML 形式が使われるのだと思います。ファイル仕様は sitemaps.org をご覧ください。

f:id:xin9le:20200112015442j:plain

サイトマップファイルを生成するライブラリは C# / .NET の世界にもいくつか転がっているのですが、どれも微妙に気が利いてなく、社内でもプロジェクト毎に独自に実装を持っていたりして自分的に全く気に入らなかったので、せっかくなのでと思って車輪の再発明をしました。それが今回ご紹介する XSitemaps です。.NET Standard 1.1 以上をサポートしているので、.NET Framework 4.5 でも利用できます。ただ XML ファイルを作るだけとあって幅広い。

主な機能

以下は主にサポートしている機能の一覧です。見る人が見ると案外痒い所に手が届くようになっている...と思います(たぶん。というのも、サイトマップファイルには以下のような地味に面倒な仕様 / 制限があります。

  • 1 ファイルに含められる URL は最大 50,000 件
  • 50 MB 以下

もしこの制限に引っ掛かったときはサイトマップファイルを適切に分割し、サイトマップインデックスファイルを作ってあげなければなりません。これを簡単に調整できないと困ってしまうわけです。XSitemaps はこの辺りの制約に対して、

  • サイトマップファイルを指定の URL 件数単位で分割
  • インデントの有無の調整
  • GZIP 形式でのファイル圧縮 (= 仕様で認められている)

を自然な形でサポートしているので、そこが推しポイントというか便利なところです。

使い方

サイトマップファイル (Sitemap.xml) は以下のような感じで作ります。Sitemap.Serialize()Stream に書き込むこともできますし、byte[] で返すこともできます。

// URL から Sitemap を作る
var modifiedAt = DateTimeOffset.Now;
var urls = new[]
{
    new SitemapUrl("https://blog.xin9le.net", modifiedAt, ChangeFrequency.Daily, priority: 1.0),
    new SitemapUrl("https://blog.xin9le.net/entry/rx-intro"),
    new SitemapUrl("https://blog.xin9le.net/entry/async-method-intro", frequency: ChangeFrequency.Weekly),
};
var sitemaps = Sitemap.Create(urls, maxUrlCount: 2);  // 2 件ごとに分割

// ファイル出力
for (var i = 0; i < sitemaps.Length; i++)
{
    var desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
    var path = Path.Combine(desktop, $"Sitemap_{i}.xml");
    using (var stream = new FileStream(path, FileMode.CreateNew))
    {
        // 地味に気の利いたオプション
        var options = new SerializeOptions
        {
            EnableIndent = true,
            EnableGzipCompression = false,
        };
        sitemaps[i].Serialize(stream, options);
    }
}

// Sitemap_0.xml
/*
<?xml version="1.0" encoding="utf-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://blog.xin9le.net</loc>
    <lastmod>2020-01-12T00:07:12.2351485+09:00</lastmod>
    <changefreq>daily</changefreq>
    <priority>1</priority>
  </url>
  <url>
    <loc>https://blog.xin9le.net/entry/rx-intro</loc>
    <changefreq>never</changefreq>
    <priority>0.5</priority>
  </url>
</urlset>
*/

// Sitemap_1.xml
/*
<?xml version="1.0" encoding="utf-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://blog.xin9le.net/entry/async-method-intro</loc>
    <changefreq>weekly</changefreq>
    <priority>0.5</priority>
  </url>
</urlset>
*/

そして、サイトマップインデックスファイル (SitemapIndex.xml) は以下のような感じです。全然難しくない!

// 分割されたファイル情報から SitemapIndex を作る
var modifiedAt = DateTimeOffset.Now;
var info = new[]
{
    new SitemapInfo("https://example.com/Sitemap_0.xml", modifiedAt),
    new SitemapInfo("https://example.com/Sitemap_1.xml"),
};
var index = new SitemapIndex(info);

// ファイル出力
var desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
var path = Path.Combine(desktop, $"SitemapIndex.xml");
using (var stream = new FileStream(path, FileMode.CreateNew))
{
    var options = new SerializeOptions
    {
        EnableIndent = true,
        EnableGzipCompression = false,
    };
    index.Serialize(stream, options);
}

/*
<?xml version="1.0" encoding="utf-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <sitemap>
    <loc>https://example.com/Sitemap_0.xml</loc>
    <lastmod>2020-01-12T00:13:24.4802279+09:00</lastmod>
  </sitemap>
  <sitemap>
    <loc>https://example.com/Sitemap_1.xml</loc>
  </sitemap>
</sitemapindex>
*/

まとめ

SEO はエンジニアからすると比較的縁遠いものかもしれませんが、流入を気にする Web サービスの運営においては重要なことなので、もしサイトマップファイルを作ることがあれば使ってみてください。