<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Kipp Engineering Blog</title>
        <link>https://engineering.kipp-corp.com</link>
        <description>Kippのエンジニアが技術情報を発信するブログです。</description>
        <lastBuildDate>Mon, 26 Jan 2026 10:29:42 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>ja</language>
        <copyright>© Kipp Financial Technologies, Inc.</copyright>
        <atom:link href="https://engineering.kipp-corp.com/feed.xml" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[kipp輪読会のご紹介]]></title>
            <link>/blog/reading-club</link>
            <guid isPermaLink="false">/blog/reading-club</guid>
            <pubDate>Fri, 01 Sep 2023 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>はじめまして、浅野と申します。</p>
<p>kippではプレイングマネージャーとして、プロダクトマネジメントチームをリードしています。kippにジョインする前はGoogle社でGoogle Ads（当時はAdWordsという名前でした）やGoogle Analyticsのエキスパートとして勤務しておりました。FinTech未経験で転職してきたこともあり、業界知識やソフトスキル含め学び続ける毎日を過ごしています。</p>
<h2 id="輪読はじめました">輪読はじめました</h2>
<p>今回のブログ記事では、kippで行っている輪読の取り組みについてご紹介します。kippでは社員の自主学習を推奨しており、スキルや知識を身につけるための学習も業務時間中に行うことが許可されています。業務に関係する書籍は基本的に経費として申請できますし、有料の講座も一部または全額補助された実績があります。輪読はこのような自学推奨の取り組みの一環として、CTO冨田の旗振りで開始されました。参加者全員で1冊の本を読み、回毎の担当者が学習内容を解説するという、大学のゼミや研究室では馴染みのある学習方法ですね。</p>
<p>記念すべき第1回目の輪読本は、戸田山和久著<a href="https://www.amazon.co.jp/%E8%AB%96%E7%90%86%E5%AD%A6%E3%82%92%E3%81%A4%E3%81%8F%E3%82%8B-%E6%88%B8%E7%94%B0%E5%B1%B1-%E5%92%8C%E4%B9%85/dp/4815803900">『論理学をつくる』</a>です。フルリモートで業務しているkippでは、メンバー全員に論理的な思考とコミュニケーションを求めています。しかしそもそも「論理的」とはどのようなことか、という根本にアプローチしているのがこの本です。私の論理学の知識は高校の数学Aとエンタメ的な論理パズル程度しかなく、一から学ぶよい機会だと捉えて輪読に参加しました。</p>
<h3 id="輪読はよいものだ">輪読はよいものだ</h3>
<p>私が最後にまともな輪読をしたのは学部時代のゼミが最後ですが、全員の理解を促進する素晴らしい学習方法だと改めて感じました。「自分が担当するところだけ読んであとは他の人の発表を聞き流せばいい」などと思っていたわけではありませんが、実際全くそのようなことはなかったです。他人の発表中でも定理の証明や練習問題に回答するよう振られることがあるので、発表内容に集中し、理解できなければ都度進行を止めて質問する姿勢が必要です。少人数での実施のため何回も発表担当が回ってくるということもあり、皆アクティブな聞き手として積極的にセッションに参加していました。自分の担当回では内容にツッコミがこないか、タフな質問がこないかとヒヤヒヤしていましたが、そういうところも含めていい体験でした。</p>
<h2 id="論理学をつくるの感想">『論理学をつくる』の感想</h2>
<p>結論から言うとかなり難易度が高かったです。私がガチガチの文系なのもありますが、「論理学って逆・裏・対偶とかだよね」という程度の認識で読み始めると序盤から度肝を抜かれます。</p>
<p>『論理学をつくる』という書名のとおり、まずは最も原始的な論理言語を構築するところから始まります。その後、意味論的なアプローチと統語論的なアプローチの両方を取りながらより複雑な論理言語へと発展させていきます。数学でいうと、以下を厳密に定義しながら論をすすめるという感じです。</p>
<ul>
<li>「1」「2」「+」「=」という文字・記号の配置ルール（統語論、シンタックス）</li>
<li>それぞれの文字・記号の意味（意味論、セマンティクス）
本の中では当然上記の逆・裏・対偶の概念も出てきますが、本当に一瞬で通り過ぎてしまうので私の数学A知識が役に立つことはありませんでした。</li>
</ul>
<p>「数学は絶望的なまでに積み重ねの学問である」と絹田村子著の漫画<a href="https://www.amazon.co.jp/%E6%95%B0%E5%AD%97%E3%81%A7%E3%81%82%E3%81%9D%E3%81%BC%E3%80%82-1-%E3%83%95%E3%83%A9%E3%83%AF%E3%83%BC%E3%82%B3%E3%83%9F%E3%83%83%E3%82%AF%E3%82%B9%E3%82%A2%E3%83%AB%E3%83%95%E3%82%A1-%E7%B5%B9%E7%94%B0-%E6%9D%91%E5%AD%90/dp/4098702819/">『数字であそぼ。』</a>で言っていましたが（うろ覚え）、この本の内容がまさにそれです。過去に出てきた定義や定理、証明を理解できていないと永遠に論理言語の海を漂うことになります。個人的には続々と追加される記号とそれらの意味づけの理解（あと口に出すときの読み方も）に苦労しました。</p>
<h3 id="業務に役に立つか">業務に役に立つか</h3>
<p>本自体の難易度もあってか、三分の一程度進んだところから脱落者が出始めましたが、過半数は完走できました。とはいえkippでの業務に直接的に結びつけやすいのは序盤の内容なので、そこまで全員が到達できたのは良かったです。中盤以降についても、単純化された命題よりも複雑な、より現実世界で我々が取り扱う自然言語に近いところまで論理言語を拡張するという内容なので、日常的な会話を論理的に捉える上での解像度があがりました。</p>
<p><a href="https://www.amazon.co.jp/%E3%83%AC%E3%82%AA%E3%83%B3-%E5%AE%8C%E5%85%A8%E7%89%88-%E3%82%AA%E3%83%AA%E3%82%B8%E3%83%8A%E3%83%AB%E7%89%88-UHD-Blu-ray/dp/B09NN5FZQW">『レオン』</a>という映画を最近見ていたんですが、作品中の会話で以下のようなものがありました。</p>
<p><code>マチルダ：「豚は臭いから嫌」</code><br>
<code>レオン：「臭くない豚もいる」</code></p>
<p>私はこの会話を聞きながら、以下のようなことを考えていました。<br>
「∀xSx,∀xSx↔Hmxだというマチルダに対して∃x¬Sxだからその論理は成り立たないとレオンは主張しているんだなァ」<br>
ということで、輪読の内容は確かに身になっているのだと実感できました。今後業務でもそのように活用していけるとよいのですが。</p>
<h2 id="輪読は続く">輪読は続く</h2>
<p>去年の第四四半期から続いた「論理学をつくる」の輪読も2023年7月で一段落したので、また別の本で輪読を続けていきます。次に取り扱う本は『アンガーマネジメント11の方法』です。またしてもFinTechと直接関係ない内容です。しかし感情をうまくコントロールし、怒りを正しい方向に向かわせることは業務だけでなく人生にも通ずるテクニックですので、ぜひこの本からその方法論を吸収していきたいですね。</p>
<p>kippでは一緒に働いていただける方を募集しています。今入社すれば一緒にアンガーマネジメントを学ぶこともできます。ご興味ある方は、お気軽に<a href="mailto:people@kipp-corp.com">people@kipp-corp.com</a>までご連絡ください。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[WSL2で動作するUbuntuにMicrosoft Defender for Endpointをインストールする]]></title>
            <link>/blog/wsl2-mde</link>
            <guid isPermaLink="false">/blog/wsl2-mde</guid>
            <pubDate>Tue, 10 May 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>こんにちは。CTOの冨田です。
春になったかと思えば寒くなったり、不安定な天気が続きますね。
Windowsでシステム開発をする場合、Linux仮想マシンや<a href="https://docs.microsoft.com/ja-jp/windows/wsl/about">Windows Subsystem for Linux （以下WSL）</a>を利用することも多いでしょう。
WindowsのcmdやPowerShellはバージョンが上がるごとに使いやすくなっていますが、Linuxに慣れた開発者にとっては学習コストが高く感じられます。</p>
<p>kippでは安全なサービスを提供し、情報漏洩を防止するため、従業員の端末の管理やセキュリティ対策にも力をいれています。
現在は<a href="https://www.microsoft.com/ja-jp/security/business/threat-protection/endpoint-defender">Microsoft Defender for Endpoint （以下MDE）</a>の導入を進めています。
MDEはWindows OS標準添付のセキュリティ機能として知られるMicrosoft Defenderの集中管理バージョンです。
通常のMicrosoft DefenderはWindows固有の機能ですが、MDEはWindowsのほか<a href="https://docs.microsoft.com/ja-jp/microsoft-365/security/defender-endpoint/minimum-requirements?view=o365-worldwide#other-supported-operating-systems">Android、iOS、Linux、macOS</a>でもサポートされている点が優れています。
さらにLinux環境でも<a href="https://docs.microsoft.com/en-us/microsoft-365/security/defender-endpoint/tvm-software-inventory">ソフトウェアインベントリ</a>や<a href="https://docs.microsoft.com/en-us/microsoft-365/security/defender-endpoint/device-timeline-event-flag">操作履歴の一部</a>が取得できます。
MDEをすべての環境にインストールし、すべての環境のマルウェア対策を一元管理したいと考えました。</p>
<p>MDEは上述のようにLinuxとmacOSにもインストールできるため現在の従業員の環境をすべてカバーしていると想定していました。
しかし実際にはWSL2上のLinuxはサポートされていません。
WSL2がLinuxをゲストOSとしてのみ動作させることを想定しているため、必要な一部機能が省略されているからです。
今回はいくつかのハックを組み合わせて、通常省略されている機能を復活させ、MDEを動作させることに成功しました。</p>
<p>なお、Microsoft社の公式見解は見つけられませんでしたが、MDEはWSL2のサポートを想定して設計されていないように見受けます。
最悪の場合、ハックを導入したためにベンダー標準の状態で使うよりも脆弱な状態に陥るリスクさえあります。
MDEの導入と安定していてサポートされたWSL2環境のどちらを取るべきか、各自が吟味するべきです。</p>
<h2 id="どうしてmdeが動作しないのか">どうしてMDEが動作しないのか</h2>
<p>MDEを動作させるためには、まずサポートしないとされている原因を把握します。
本記事ではホスト環境としてWindows 10を、WSL2のゲストディストリビューションとしてUbuntu 20.04を使います。
サンプルもすべてUbuntu 20.04を利用しています。
Windows 11では一部動作しない恐れがあります。</p>
<p><a href="https://docs.microsoft.com/ja-jp/microsoft-365/security/defender-endpoint/linux-install-manually">インストールガイド</a>によれば、適切な準備をして<code>sudo apt-get install mdatp</code>を実行すればインストールが完了するとしています。
しかしWSL2の環境で実行するとmdatpのパッケージは展開されるものの、post install scriptsの実行に失敗し、中途半端な状態になってしまいます。
エラーメッセージをよく見ると、<code>systemd</code>がないためにエラーが起きているとわかります。</p>
<p>systemdは最近の多くのLinux distributionで利用されているinitシステムですが、標準のWSL2環境には存在しません。
スクリプトを動かしたりGCCなどLinuxの開発環境を使う程度であれば必要ないため、省略されているのでしょう。
しかしmdatpのみならずデーモンプログラムやサーバを動かそうとすると、systemdを要求されるケースがあります。</p>
<h2 id="systemdを導入する">systemdを導入する</h2>
<p>systemdはいくつかの方法で導入できますが、現在一番簡単で信頼性が高いのは<a href="https://github.com/nullpo-head/wsl-distrod">wsl-distrod</a>を使う方法でしょう。
wsl-distrodは既存のWSL2 distributionに影響を与えず、linuxcontainers.orgから新しいイメージを展開するオプションがあるので、非破壊的に導入できます。
導入方法自体もとても簡単です。
Windows上でexeファイルをひとつ実行し、CLIインタフェースでいくつかの質問に答えるだけです。
いくつかの導入方法がありますので、上のリンクから公式情報を確認してください。</p>
<p>導入に成功すると、<code>ps -aux</code>等で全プロセスを表示したときPID1で<code>/usr/lib/systemd/systemd</code>が動作していることを確認できます。</p>
<h2 id="auditを有効にする">auditを有効にする</h2>
<p>systemdが動いている状態で<code>mdatp</code>のインストールを再試行すると、<code>audit</code>がサポートされていないというエラーに変わりました。</p>
<pre><code>Error - audit support not in kernel
Cannot open netlink audit socket
</code></pre>
<p><code>auditd</code>を調べると、この機能はパッケージマネージャなどでインストールできるものではなく、カーネル設定で有効にしなければならないと分かります。
さらに調べると、<a href="https://gist.github.com/cerebrate/d40c89d3fa89594e1b1538b2ce9d2720">WSL2カーネルをビルドし上書きする方法</a>が見つかりました。
この記事のなかで<code>.config</code>ファイルを編集していますが、これはカーネルのビルドオプションを指定するファイルです。
このファイルで<code>CONFIG_AUDIT=y</code>を指定すればaudit機能が有効になりそうです。</p>
<p>実際に手順に沿ってビルドするとカーネルイメージ（bzImage）が出力されます。
Linuxカーネル全体をビルドする作業なので、マシンスペックによりますが30分程度かかります。
このbzImageを一旦Windowsのファイルシステムに待避し、<code>C:\Windows\System32\lxss\tools</code>にある<code>kernel</code>というファイルを置き換えます。
WSL2で起動するすべてのdistributionのカーネルが置きかわるようです。</p>
<p>実際にこの手順を行ったあと、mdatpを再インストールすると今度は成功しました。
<code>systemctl status auditd</code>で確認するとauditdも動作しています。</p>
<p>さらにMDEのインストール手順を最後まで進めると、無事テストウイルスファイルに対して検疫が発動することを確認できました。</p>
<h2 id="カーネル生成の自動化">カーネル生成の自動化</h2>
<p>この導入手順では通常のWSL2環境にくらべてsystemdの導入、カーネルのカスタムコンパイルという2つの余計なことをしています。
余計なことをした部分は通常よりインセキュアになっていると考えるべきです。</p>
<p>リスクを詳しく分析すると、systemdやdistrodに脆弱性がある場合と、カーネルに脆弱性がある場合に特に深刻な影響を受けるとわかります。
systemdとdistrodは配布されているものをそのまま利用しているのでそれぞれの脆弱性情報を確認しパッチを適用すればよいでしょう。</p>
<p>しかしカーネルについてはオリジナルのものを使っているため、通常配布されているバイナリを利用できません。
WSL2には<code>wsl --update</code>というカーネルファイルを更新する機能が実装されていますが、この更新を利用するとauditdが利用できなくなるでしょう。
かわりに自分達で常に最新のカーネルをビルドし、利用するプロセスを確立する必要があります。</p>
<p>今回はGitHub Actionsを使って定期的な自動ビルドを実装しました。
<a href="https://github.com/kipp-corp/wsl2-kernel-with-audit">kipp-corp/wsl2-kernel-with-audit</a>はシンプルなworkflowだけが定義されているリポジトリです。
<a href="https://github.com/kipp-corp/wsl2-kernel-with-audit/blob/main/.github/workflows/github-actions-build.yml">ビルドスクリプト</a>が上のビルド方法をGitHub Actions上で実行する定義になります。
実行結果は<a href="https://github.com/kipp-corp/wsl2-kernel-with-audit/actions">Actionsの履歴</a>から確認でき、ビルド成果物のカーネルファイルもダウンロードできます。</p>
<p>LinuxカーネルやWSL2カーネルの脆弱性情報を確認し、更新が必要なときここからダウンロードして利用できます。
新しいメンバーが増える時でも秘伝のタレを伝授するかわりに公開された信頼できるソースを提示できます。</p>
<h2 id="最後に">最後に</h2>
<p>今回は実験的な試みとして、WSL2上のUbuntuでMDEを動かしてみた過程をまとめました。
また長期的に運用する上で課題となりうる部分を検討し、自動化まで達成しました。</p>
<p>kippはアプリケーション開発に留まらず、コンピュータサイエンスやセキュリティの幅広い知識をお持ちの開発者と一緒に働きたいと考えています。
情報管理やセキュリティの取り組みに関心をお持ちの方は<code>people@kipp-corp.com</code>までぜひお気軽にご連絡ください。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[「リモートワークスタイルチェック」に回答してみた]]></title>
            <link>/blog/remote-work-style-check</link>
            <guid isPermaLink="false">/blog/remote-work-style-check</guid>
            <pubDate>Tue, 05 Apr 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>こんにちは。CTOの冨田です。
すっかり春になりました。
私の住んでいる札幌でもほとんどの雪が溶け、自転車に乗れることを喜んでいます。
今回は<a href="https://gist.github.com/mugi-uno/a794bc199ce7d663f01a434a6e72504c">mugi-unoさんが公開されたリモートワークスタイルチェック</a>の意図に賛同し、チェック項目に回答してみました。
mugi-unoさんはこのようなチェックリストを作った背景を<a href="https://mugi1.hateblo.jp/entry/2022/03/24/002534">「"リモートワーク"の認識ズレ解消のためにリモートワークスタイルチェックを作ってみた」</a>で解説されています。
また本ブログでも過去に<a href="/blog/workflow/">ワークスタイルの紹介</a>や<a href="/blog/team/">チームビルディング</a>についてお話しています。
これらの記事もあわせてご覧いただくと、多面的にリモートワークの様子が想像できるでしょう。</p>
<p>本記事はKipp社の投稿時点の内容となります。
現時点で変更を予定している部分はありませんが、長い時間が過ぎれば社会状況の変化や会社の体制変化に伴ってルールが変わることも予期されます。
また主に開発チームの状況を紹介しており、他のチームでは事情が異なる部分もあります。
ご了承ください。</p>
<h2 id="リモートワーク比重度">リモートワーク比重度</h2>
<p>原文に沿って、各項目を順に見ていきます。</p>
<h3 id="リモートワークの導入期間">リモートワークの導入期間</h3>
<p>「4: 継続してリモートワークを3年以上導入している」になります。
創業時点よりリモートワークを継続して導入しています。
Kipp社は創業後4年目にはいったところですので、年数の句切りは3年以上になります。</p>
<h3 id="リモートワークの導入ポリシー">リモートワークの導入ポリシー</h3>
<p>「5: 恒久的な導入であり、根本的な制度変更が行われることはない」になります。
私を始め多くのメンバーがリモートワークに強い意志を持っていますので、容易に変更されることはありません。
しかし企業体制が根本的に変わるようなイベントがあれば制度変更が避けられなくなる恐れも生じます。</p>
<h3 id="リモートワークのマジョリティ度合い">リモートワークのマジョリティ度合い</h3>
<p>「4: 全員が常にリモートワークの可能性がある（出社している可能性もある）」が該当します。
ほとんどのメンバー、とくに開発チームやプロダクトマネジメントチームのメンバーは全員がリモートです。
しかしkippは物理オフィスを所有しているため、オペレーション担当者が定期的に出社して郵便物や宅配物の対応にあたっています。</p>
<h3 id="申請許可の必要性">申請・許可の必要性</h3>
<p>「5: 申請や許可は必要ない」です。
原則がリモートワークであるため、手続きはありません。
逆にオフィスの入室は登録制で、オフィスに入れない正社員メンバーもいます。</p>
<h3 id="リモートワークの適用範囲">リモートワークの適用範囲</h3>
<p>「5: 業務に関わる全メンバー（正社員以外も含む）がリモートワークを利用できる」
業務委託も含め、すべてのメンバーがリモートワークをしています。</p>
<h3 id="出社義務">出社義務</h3>
<p>「5: 出社義務は無い」
出社義務はありません。書類のやり取りが必要な場合、郵送での対応が基本です。
ラップトップPCなど貸与物も宅配便で送付、返却しています。</p>
<h3 id="リモートワークで必要なサービスアプリケーションの取り扱い">リモートワークで必要なサービス・アプリケーションの取り扱い</h3>
<p>「5: 業務で利用するサービス・アプリケーションはすべてリモートワークを前提としており、リモートワーク・非リモートワークによる差異はない」です。
コミュニケーションの大半をSlackとGoogle Drive、Backlogに集約しています。
特定の拠点でないとアクセスできないツールや紙の書式はありません。</p>
<h3 id="情報のアクセス範囲">情報のアクセス範囲</h3>
<p>「5: リモートワーク・非リモートワークでアクセスできる情報に差はない」です。
上述のとおり情報はすべてクラウドサービスで管理されており、アクセス制限も行っていません。</p>
<h3 id="その他の制度や特徴">その他の制度や特徴</h3>
<p>最大の特徴はCEOとCTOがともにオフィスのある東京に居住せず、リモートワークをしている点でしょう。
冒頭でご挨拶したとおり私は札幌に住んでいます。
稀にオフィスや東京のメンバーに用事がある場合は飛行機で出張しています。
私は暑いのが苦手なので、夏が短い札幌の気候は大変気に入っています。</p>
<p>ワーケーション（保養地や旅行先で羽を伸ばしながら就業するスタイル）を時折実施しているメンバーもいます。
昨今では旅行じたいしにくくなっていますが、2019年の夏には1ヶ月弱ヨーロッパを旅行しながら就業していました。</p>
<p>住宅手当という形で、月に5万円を給与に加算しています。
住宅手当といっても出社しやすい居住することを推奨するのではなく、労働に適した住環境を整えてもらうための手当としています。
もっぱら業務に利用する機器は経費で購入できますが、長期に個人で使いたい椅子や机、ディスプレイにはこの手当を活用できます。
申請や清算もないので、単にその分給与が上乗せされているというのが実態です。</p>
<h2 id="リモートワークの文化">リモートワークの文化</h2>
<h3 id="同期非同期コミュニーションの割合">同期・非同期コミュニーションの割合</h3>
<p>テキストの非同期コミュニケーションが中心です。</p>
<p>同期的なコミュニケーションは「A: 同期的なものを使うこともあるが全て非同期の方法で代替することができる」と「B: 一部で同期的なコミュニケーションが必要」の両方のケースがあります。</p>
<p>開発チームの業務委託のメンバーなど関与の範囲が限定的な場合、すべてのコミュニケーションを非同期で行っています。
深夜や休日に作業するメンバーも多いため、管理者の負担を下げることにもつながります。</p>
<p>一方、複数のサービスや会社全体の状況を理解すべき立場では、プル型の情報取得で把握するのが難しいこともあります。
またテキストのメッセージ発信はタスクに基づいたものが中心となり、ワークスタイルなど目前のタスクと直接関係ない相談がしにくい傾向にあります。
必要と感じた場合は躊躇せずビデオチャットを行います。
参加メンバーが必要に応じて定例のミーティングも行っています。</p>
<p>ビデオや音声を繋ぎっぱなしにするようなことは行っていません。</p>
<h3 id="テキストコミュニケーションの割合">テキストコミュニケーションの割合</h3>
<p>「B: テキストが主で、補助的にビデオ通話・音声通話を用いる」に該当します。
非同期のコミュニケーションはテキストのみ、同期のコミュニケーションはビデオ通話が中心です。</p>
<p>すこし論点がかわりますが、状況に応じてビデオ、音声の送信をオフにしてもかまいません。
アバターで参加しているメンバーもいます。</p>
<h3 id="コミュニケーション頻度">コミュニケーション頻度</h3>
<p>概ね「E: 成果物のみでやり取りをする」になります。
timesチャンネル（分報）や趣味のチャンネルでの業務に直接関連づかないコミュニケーションがありますが、話が長く続くことはないので雑談とはすこし違います。</p>
<h3 id="働く時間">働く時間</h3>
<p>「A: 全員働く時間は定まっておらず、同期的なやりとりには調整が必要」です。
実際に深夜、休日に作業しているメンバーもいます。
とくに開発チームはそういった時間に作業している比率が高いです。
予期せぬ時間にミーティングを入れると寝坊してしまうこともあるので、全員が出席できる自信のある時間に調整しています。</p>
<h2 id="最後に">最後に</h2>
<p>今回はチェックリストに照らして私達のリモートワークの様子を確認してみました。
まさにmugi-unoさんの指摘するとおり、リモートワークと一口に言っても様々な観点と様相があります。
いままでのカジュアル面談では聞かれるままに取り止めなく語ることも多かったですが、このように論点が整理されていると説明しやすくなります。
就職や転職を考える方も、基準に沿って列挙されていると就職先の比較検討が容易になりそうです。
ずっとリモートで勤務したい方は是非kippもご検討いただき、興味を持たれましたらお気軽に<code>people@kipp-corp.com</code>までご連絡ください。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[PCI DSSの審査を完了しました]]></title>
            <link>/blog/pcidss2022</link>
            <guid isPermaLink="false">/blog/pcidss2022</guid>
            <pubDate>Mon, 14 Mar 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>こんにちは。CTOの冨田です。
先日、Kipp Financial TechnologiesはPCI DSSの監査を完了しました。</p>
<p><a href="https://www.jcdsc.org/pci_dss.php">PCI DSS</a>はカード会員データ（主にカード番号）を取り扱う事業体が準拠すべきセキュリティ基準です。
PCI DSSの監査は毎年受け、準拠状況を更新することが求められます。
監査は毎年<a href="https://www.icms.co.jp/pcidssresult.html">国際マネジメントシステム認証機構</a>に依頼しています。
Kipp社は2020年に初回の監査を受け、2022年で3回目になります。
初年度は準備も監査中もかなりの時間と労力を要しましたが、ノウハウを積み重ねることで年々監査の負担が減少しています。
今回はPCI DSS準拠の負担を軽減しつつ、セキュアにサービス構築するためのノウハウをお伝えします。</p>
<p>この記事で参照するPCI DSSはバージョン3.2.1です。
近くバージョン4への移行が予定されていますが、執筆時点では正式公開されていません。
AWSクラウドサービス上で構築することを前提に、全体でAWSの用語を使っています。
同様の考え方は他のクラウドサービスにも適用できますが、非クラウド形式のインフラサービスや自社が所有する機器では事情が大きく異なりますのでご注意ください。
本記事の内容はすべてkippの経験に基づくものであり、PCI SSCや国際マネジメントシステム認証機構の見解を示すものではありません。</p>
<h2 id="対象領域のコントロール">対象領域のコントロール</h2>
<p>PCI DSS準拠の負担を軽減する最も効果的な方法は、監査対象領域を縮小することです。
これがこの記事で説明するすべてです。
当たり前のように聞こえますが、実際に対象領域を縮小するアプローチは興味深いものがあります。
この記事ではkippが実際に行った対象領域をギリギリまで減らす努力を具体的に紹介します。</p>
<p>企業規模にかかわらず、事業体は複数の業務を行うの一般的です。
kippのようにプリペイドカードを発行する事業体でも、会員管理やプリペイドカードへの入金など、カード会員データと直接関係ない機能があります。
またkippが提供するサービスには<a href="https://www.mirai-barai.co.jp/">ミライバライ</a>向け機能のようにカード情報の取扱が一切生じないサービスもあります。
対象領域のコントロールをしないと、無関係にみえるサービス、実装もすべてPCI DSS監査対象になってしまいます。</p>
<p>これは直感的におかしいです。
カード会員データを扱わない部分はPCI DSSを適用する必要はないはずです。
PCI DSSより条件の緩い、別のセキュアなサービスを構築するプラクティスを適用すれば十分でしょう。</p>
<p>PCI DSSの監査対象になるのはカード会員データ（カード番号など）と機密認証データ（CVVや暗証番号など）の「保存、処理、伝送」に関わるシステムコンポーネントと事業体です。
決済サービスを行っていてもクレジットカードを取り扱わない事業体はPCI DSSに準拠する必要がありません。
カード発行機能を持つサービスの中でもカード会員データの「保存、処理、伝送」に関係ない部分は監査対象から外せます。</p>
<p>「関係ない」と言いきれるのはネットワークの構造やデータの流れを調査して、「保存、処理、伝送」のいずれにも関与しない場合です。
この3つの要素をよく見ると、保存するには処理する必要があり、処理するにはどこかからカード会員データを受け取る（伝送される）必要があります。
ですからカード会員データに関係するコンポーネントを図に整理したとき、伝送していない部分は対象外にできます。</p>
<p>データの流れはどのように調査するのでしょうか。
PCI DSS要件1.1はコンポーネントとその関係をネットワーク図やデータフロー図といった図にまとめることを要求しています。
図は人が手で書くものですので、実際のシステム構成とずれてしまうリスクがあります。
ずれが生じないよう定期的に見直す要件（1.1.2.b）もありますが、見直しも人手で行う点は同じです。
監査もこれら文書や図を元に行うので、図に抜け漏れがあると不正に準拠状態になってしまう危険があります。
人が作成した文書に基づいて人が監査するということの不確実性を肝に命じ、普段の業務でも監査時も嘘偽りなく対応することが健全な準拠の前提条件になります。</p>
<p>伝送していない部分は対象外になりますから、ネットワークが完全に分離しており、通信の手段がひとつもなければ間違いなく対象外です。
AWSではVPCが主要なコントロール手段になります。
カード会員データを取り扱うVPCが完全に独立してVPC PeeringやInternet Gatewayを持たなければ、そのVPCのみが監査対象になります。</p>
<p>kippではカード会員データを取り扱う専用のAWSアカウントを作成し、AWS Organizationsの機能で管理しています。
他のサービスもそれぞれ独立のAWSアカウントで動作しており、サービス間のデータの混入や障害の相互影響のリスクを低減しています。
その中でインターネットにも接続していないカード会員データ環境VPCを用意し、監査対象のネットワークをこのVPCひとつに限定しています。</p>
<h2 id="アウトソーシング">アウトソーシング</h2>
<p>通常、カード決済業務は外部サービスとの通信が必要です。
カード発行者（イシュア）であればブランドのネットワークと接続し、決済リクエスト（オーソリゼーション）や売上情報を受信します。
決済代行やアクワイアラであれば消費者からカード番号を受け取り、上位のアクワイアラやブランドに伝送します。
そう考えるとインターネットにも接続していないVPCで業務が成立するのは変ですね。</p>
<p>kippはブランド接続の機能をほとんどアウトソーシングしています。
<a href="https://service.paycierge.com/solution/branded-prepaid-processing-service/">TIS社のPAYCIERGE ブランドプリペイドプロセッシングサービス</a>(以下、PAYCIERGE)はワンストップでプリペイドカードのイシュイング機能を実装できるサービスです。
ブランドネットワークと接続するための開発はとても分量が多く、慎重な実装が求められます。
PAYCIERGEを利用すればブランドネットワークとの通信はすべて提供されており、カスタマイズしたい部分だけを拡張できます。
kippではカード発行前に発行データを調整する部分と、決済リクエストに対して不正対策や複雑な残高処理を実現する部分で拡張しています。</p>
<p>PCI DSSはインターネットからのアクセスが特に重大な脅威源となることを踏まえ、インターネットからのリクエストを受け付けるネットワークとアプリケーションに重厚な要件を課しています。
しかしPAYCIERGEからの決済リクエストのカード番号はすべてPAYCIERGE内のトークンに置き換えられているため、決済リクエスト処理ではカード会員データを受け取りません。
同様にクレジットカードから入金を受ける場合も、決済代行のトークン化APIを利用することで自社のサーバでカード会員データを受け取るのを避けられます。
PAYCIERGEのようなトークン化の機能を持ったサービスを活用することで、機能性を損ねることなく公開されたコンポーネントからカード会員データを排除でき、監査対象を大幅に削減できます。</p>
<p>ただし私達はカード会員データの管理の一部をPAYCIERGEに委託していることになりますから、PAYCIERGEがカード会員データを適切に扱っているか確認しなければいけません。
PCI DSS要件12.8では委託先の適格性を各社が調査するほか、PCI DSS AOC（準拠証明書）を確認することでこの責務を果たすよう義務づけています。</p>
<h2 id="マネージドサービスの利用">マネージドサービスの利用</h2>
<p>アウトソーシングによりkippがカード会員データを扱うのはPAYCIERGEからカード発行データファイルを受領し、変換し、印刷会社に受け渡す部分のみになりました。
この変換機能はAWSにあるので、PAYCIERGEおよび印刷会社との接続はインターネット経由になります。
するとPAYCIERGEからカード発行データファイルを受け取る部分、印刷会社に受け渡す部分も上述の要件の対象になりそうです。</p>
<p>ここでもアウトソーシングの考えを活用し自社の責任範囲から外すことができました。
AWSは<a href="https://aws.amazon.com/jp/compliance/pci-dss-level-1-faqs/">非常に多くの機能に対してPCI DSSの準拠証明を受けており</a>、S3や<a href="https://aws.amazon.com/jp/aws-transfer-family/">Transfer for SFTP</a>もそのなかに含まれます。
PAYCIERGEからはS3 APIを利用してアップロードしてもらい、印刷会社にはTransfer for SFTPで提供したSFTPサーバからS3内のデータをダウンロードしてもらっています。
インターネットからのリクエストを受ける部分はすべてAWSマネージドサービスになりますから、自社でASVスキャンを受けずに済みます。
かわりにAWSのAOCを参照し、該当機能についてAWSが責任を持っていることを確認します。</p>
<p>ここまでの工夫により自社で管理する部分はS3バケットからデータを取り出し、別のS3バケットにデータを書き込む部分のみとなりました。
実にサービスの99%程度が監査対象から外せたと言えます。
この変換処理の中でも監査対象を減らす工夫があります。
AWS VPC内からS3にアクセスするのにインターネットを経由せず、VPC内に<a href="https://docs.aws.amazon.com/ja_jp/vpc/latest/privatelink/vpc-endpoints-s3.html">S3エンドポイント</a>を建てています。
さらにEC2などのサーバを管理するとサーバのミドルウェアの更新（要件2.2）やウィルススキャンの要件（要件5.1）が生じます。
AWS Fargateで動作するように設定したECSタスクを利用すれば、これらも緩和できます。</p>
<p>結果的に、インフラストラクチャで監査対象になるのは以下に絞れます。</p>
<ul>
<li>ECSでタスクが動作するVPC</li>
<li>そのルーティングテーブル</li>
<li>そのサブネット</li>
<li>そのセキュリティグループ</li>
<li>タスク上で動くアプリケーションの開発、管理手順</li>
<li>アプリケーションのイメージのビルド手順</li>
<li>ECS実行環境の設定とIAM権限</li>
<li>それらのCloud Watch Logsログ</li>
</ul>
<p>12章もある要件のすべてが1時間程度で見られる項目に絞れるのは非常に価値があります。</p>
<p>なお、PCI DSSバージョン3.2.1はコンテナ技術やマネージドサービスについての言及が限定的であり、監査会社や監査人によって解釈が異なる場合もあります。</p>
<p>最後にAWSマネージドサービスを使い、かつペネトレーションテストを行う場合、<a href="https://aws.amazon.com/jp/security/penetration-testing/">AWSのテストポリシー</a>に従う必要があることに注意します。
たとえばAmazon ECSやAWS Fargateはこの許可リストの中に含まれていないため、Fargate上で動作するコンテナにペネトレーションテストを行う場合は個別に判断を仰ぐ必要があります。
S3のパブリックエンドポイントのようにペネトレーションテストが許可されないだろうリソースについてはAWSのPCI DSS AOCを以て不要と整理します。</p>
<h2 id="物理環境の管理">物理環境の管理</h2>
<p>クラウド環境のみでなくオフィスやデータセンターなど物理環境を利用する場合、それらもPCI DSSの監査対象になります。
サーバを設置せず、オペレーション業務用の端末を利用するだけでも厳しいコントロールが必要になります。
kippも当初はカード番号を取り扱うオペレーション用のオフィスを用意していましたが、賃料から担当者によるメンテナンスまで費用が非常に高く付くので、現在は閉鎖しました。</p>
<p>カード会員データを取り扱えるオフィスを管理すると管理リソースが増え、結果的にカード情報漏洩の危険が増します。
カード会員データを扱うためには入退室管理、監視カメラの設置、私有デバイスの持ち込み禁止など通常のオフィスで求められる以上のコントロールが必要です。
すでに一般業務用のオフィスを持っていても、そこでカード会員データを扱えるように変更するのは手間です。
感染症の流行下にあり、また働き方が多様化する現在にあってはオフィスを開設しない選択肢を積極的に検討するべきでしょう。</p>
<p>しかしほとんどのカード会員データを取り扱う事業者はオフィスを持っており、また他の事業者もカード会員データを取り扱える場所を持つことを前提としています。
オフィスを置かない場合、オフィスがない、カード会員データを人が取り扱えないことを取引先に理解し、正しく対応してもらう必要があります。
カード業務を行う会社間ではチャージバックやカードデータ漏洩時の対応など、カード番号を伝達する場面があります。
kippの従業員はカード会員データを直接取り扱わない定めをしているため、そういった場合トランケートして授受するよう依頼しています。
特別にカード会員データを扱える場合を定めるより、端から扱わないよう業務を構築したほうがセキュアかつ簡単になります。</p>
<h2 id="オフィスに関連した例外規定">オフィスに関連した例外規定</h2>
<p>カード会員データを扱える環境を廃止するにあたり、懸案になった箇所が2つあります。
ひとつは物理プリペイドカードを発送したもののカードが受け取られず、郵便が戻ってきた場合です。
郵便の返戻先はカード発行会社と自社オフィスを選べましたが、費用の問題もあり自社オフィスになっていました。
この返戻された封筒にはカード会員データが入っているため、そのまま取り扱うと監査対象になります。</p>
<p>PCI SSCは<a href="https://pcissc.secure.force.com/faq/articles/Frequently_Asked_Question/Does-PCI-DSS-apply-to-hot-cards-expired-cancelled-or-invalid-card-account-numbers">無効であると証明できるPANに適用されない</a>という公式解釈があります。
そこでカード発行のライフサイクルを工夫し、利用者が自身でカードを受け取り、アプリから有効化しない限り利用できないように設計しました。
さらに同一カード番号での再発行を禁止し、すでに利用可能なカード番号や将来利用可能になりうるカード番号が返戻されてこないようにしました。
結果オフィスに返戻されるカードは返戻された時点で無効であり、将来にわたっても無効でありつづけるため、返戻カードをカード会員データ環境外で扱えます。</p>
<p>もう一点は管理アクセスの拠点の問題です。
実際にカード会員データを閲覧する業務は発生しないものの、カード会員データを保存するAWS環境を設定・運用するためのリモートアクセスが必要です。
kippは現在ほとんどすべての業務をリモートで行っているため、管理アクセスもリモートで行うべく検討しました。</p>
<p>こちらも<a href="https://blog.pcisecuritystandards.org/guidance-how-pci-dss-requirements-apply-to-wfh-environments">公式のガイダンス</a>が出ています。
簡単にまとめると、従業員の自宅からのアクセス経路は信頼できないネットワークと識別しなければならないが、その経路は事業体の管理が及ばないので監査対象にはしない。
そのかわり自宅からのリモートアクセスの手順と仕組み、接続をセキュアにして全体のセキュリティを担保すべきとなります。</p>
<p>AWS環境の例であればログインに使う端末、ログイン時の認証手段、そしてAWSとの通信経路が適切に保護されていることを確認します。
kippでは次のようにこの保護を達成しています。</p>
<ul>
<li>カード会員データ環境の管理に使えるラップトップの貸与</li>
<li>ウィルススキャンの導入を含めたラップトップの管理方法の策定</li>
<li>Oktaを導入し、きめ細かく認証とセッションライフサイクルを管理</li>
<li>Okta、AWSとの接続がTLS v1.2以降で保護されていることの確認</li>
<li>Okta、AWSのAOCの確認</li>
</ul>
<h2 id="管理対象を削減し一石二鳥">管理対象を削減し一石二鳥</h2>
<p>今回の記事では3回のPCI DSS監査を受けた経験をもとに、PCI DSS監査の負担を軽減する業務設計のノウハウを紹介しました。
監査負担の軽減が監査対象を減らすことによって達成されること、同時に漏洩リスクを抱える箇所が減ってセキュリティも向上することを説明しました。</p>
<p>私自身、当初は「そんなに削っちゃって大丈夫か」という不安を抱いていました。
最初はAWS Fargateを使わず、わざわざEC2インスタンスを起動してECSを運用していたほどです。
監査員の方々に丁寧に説明をしていただき、まったくの杞憂であること、むしろ管理する対象を減らすのが正しいことを理解しました。
業務効率化によってPCI DSS準拠の負担は年々減り、今年の監査は検出事項ゼロを達成したのみならず、予定より1時間以上早く終了しました。</p>
<p>本記事の内容はすべてkippの経験に基づくものであり、PCI SSCや監査会社である国際マネジメントシステム認証機構の見解を示すものではありません。
要件の具体的な取り扱いは監査会社や監査人によって異なる場合があります。
すべての監査において今回紹介したロジックが適用されるわけではありません。
あくまで一例、体験談として参考にしてもらえれば幸いです。</p>
<p>kippはプリペイドカードの発行サービスを維持、拡大し、ますます多くのお客様に安心して使っていただくため、今後もPCI DSSへの準拠とシステム全体のセキュリティ向上を続けます。
セキュリティの知見を生かしたい、セキュアなソフトウェア構築の現場で働いてみたいという方はお気軽に<code>people@kipp-corp.com</code>までご連絡ください。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[kippのワークスタイル紹介]]></title>
            <link>/blog/workflow</link>
            <guid isPermaLink="false">/blog/workflow</guid>
            <pubDate>Thu, 10 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>こんにちは、CTOの冨田です。
<a href="https://engineering.kipp-corp.com/blog/team/">以前の記事</a>ではkippのチームについて理念をベースに方針や取り組みを説明しました。
新しいメンバーを探すなかで、この記事に魅力を感じてくださった方が多数いらっしゃいました。
一方で抽象的、長期的な内容にとどまっていて、実際の働き方がイメージしにくいという感触も得ました。</p>
<p>この記事では今まさに実践しているワークフローを紹介します。
主に開発チームメンバーの視点でお話ししますが、他のチームに興味がある方にも参考になるでしょう。
<a href="https://engineering.kipp-corp.com/blog/team/">先述の記事</a>とあわせて読むと、理念と実践がペアになり理解しやすいです。
複数のチームがどのようにコラボレートするのか、開発チームのメンバーがどう作業を進めているか伝われば幸いです。</p>
<h2 id="チームの構成">チームの構成</h2>
<p>kippには開発に携わるチームが開発チームの他に2つあります。
この他に運用担当、コーポレート業務のチームがあります。</p>
<ul>
<li>開発チーム：サービスの開発、運用を担当します。正社員の開発チームメンバーは輪番でオンコールも担当します。</li>
<li>事業開発チーム：社外のパートナーと共に新プロダクトや改善の企画をします。</li>
<li>プロダクトマネジメントチーム：全サービスの妥当性・整合性に対して責任を持ちます。仕様の調整から受け入れテストまで開発の全ライフサイクルを管理します。</li>
</ul>
<p>事業開発チームとプロダクトマネジメントチームはもともとビジネスチームとしてひとつに数えられていました。
人数が増えるに従い開発チームの役割をすこし分担したプロダクトマネジメントチームと、新企画に専念する事業開発チームが出来ました。</p>
<h2 id="すべての開発のスタート地点提起">すべての開発のスタート地点：提起</h2>
<p>いかなるプロダクトも改善も、課題や要望があって始まります。
新規プロダクトの立ち上げであればプロダクトで実現したい内容が最初にあります。
改善要望であれば現在のプロダクトで困っている状況と実現したい希望があります。
リファクタリングでも、現在のコードの課題点と、こう変えたらこの実装がスムーズになるというビジョンがあります。
これら開発を始める動機を示すことをまとめて提起と呼びます。</p>
<p>リファクタリングやパフォーマンス改善を別として、提起は開発チーム外からやってきます。
社内の他チームの場合も顧客企業のスタッフの場合も、エンドユーザの場合もあります。
私達はこれらの意見に耳を傾けるべきですが、一から十まですべて鵜呑みにすべきではありません。
提案者は業務のプロフェッショナルであってもシステム設計や実装のプロではありません。
注意深く要望を聴取すると、当初案より包括的で効率の良い案が出てくることがよくあります。</p>
<p>kippでは提案者の要望を理解すること、要望を実現するとどう変わるのか理解してもらうことに時間を使っています。
規模の大きい変更や新機能の提案であればドキュメントにまとめ、議論の全体が一箇所でわかるようにします。
提起されたプロジェクトでやるべきことを整理するドキュメントなので、Project Scope Document (PSD)と呼びます。
PSDは設計や実装でも提起の原点を振り返る資料として有用です。
このステップは事業開発チームが中心となって進行します。</p>
<p>関係者との調整と並行して、法的な確認やパートナーの選定も行います。
ビジネス案を多数の金融関連法に照らし、弁護士とともに懸念点を潰します。
カードの印刷や決済ネットワークへの接続といった自社で賄えない機能を提供してくれるパートナーと交渉し、タイムスコープや収益性を確認します。</p>
<p>小規模な困りごとにはプロダクトマネジメントチームが対応します。
サービスを使っているうちに要件と現実が乖離してきたり、要件定義時に想定していなかったケースが出てきます。
エンドユーザ、利用社、オペレーション担当者から広くフィードバックを受け、疑問や希望を改善提案に繋げます。
事業開発チームが新しい価値を生み出す動力源であり、プロダクトマネジメントチームが既存の価値を高める精錬機であると対比できます。</p>
<h2 id="baasへの統合要件定義">BaaSへの統合：要件定義</h2>
<p>kippは全システムをBaaS (Banking as a Service)として提供しています。
すなわち、個々の利用社が別々のシステムを使うのではなく、ひとつのシステムの個々の側面を利用しています。
提起された課題はその関係者のためだけに実装するのではなく、BaaS全体の資産価値を高めるために実装します。
機能が提案者に提供されるのはもちろん、将来の利用社に対しても提供でき、kippのシステム、会社としての価値が高まります。</p>
<p>この目的のために、提起内容をBaaSの文脈に置き直す必要があります。
前提として既存の仕様、実装と矛盾しないことが求められます。
たとえば同一のカラムを異なる利用シーンで別々の解釈をしてしまうと今後全ての実装が分岐を意識しなければなりません。
よく似ているが実は異なる概念も実装ミスの遠因になります。
負債になりうる仕様は実装前に回避します。</p>
<p>さらにBaaSの発展性に寄与するかという観点から検討を加えます。
特定の業務フローのためだけに実装してしまうと、業務内容は同じでも手順が違うシーンで再利用できません。
フロントエンドの画面ステップなどに合わせず、BaaSとして妥当なように実装します。
この観点を取り入れた事例のひとつが<a href="https://engineering.kipp-corp.com/blog/graphql-hacks/">過去ご紹介したGraphQLの活用</a>です。</p>
<p>要件定義はプロダクトマネジメントチームが中心になります。
提起内容をBaaSの文脈で評価した結果、一部を変更してもらうこともあります。
したがって要件定義作業は提起作業に一部食い込む形で行い、手戻りを避けます。
同様に次の設計ステップも要件定義や提起に食い込む形で行います。
事業開発チーム、開発チームと相談しながら進行します。</p>
<p>最終的な要件はBusiness Requirements Document (BRD)にまとめます。
BRDはその名のとおりビジネスを成功させるために必要なことを過不足なく記述するものです。
情報を取得・保存するタイミング、バリデーションなど業務を間違いなく行うために必要な内容は記載しますが、テーブル設計など内部の実装方針は記載しません。
社外のチームと同時並行で実装を進める場合、このタイミングで仕様書や説明書を配布します。</p>
<h2 id="設計">設計</h2>
<p>BRDをもとに実装方針を立てます。
要件定義段階から開発チームのメンバー（おもにCTO）が議論に参加し、現在のコードベースに合う形でインテグレートする方法を想定しています。
設計段階ではその想定を書き起こし、開発チームの全メンバーに同じ方針を共有します。</p>
<p>大規模なプロジェクトでは新しくテーブルをたくさん作ったり、新しい通信相手にAPI定義を提供します。
これらの作業を実装する各人が一部ずつ進めるとコンフリクトが起きやすくなります。
また先行の作業が詰まって新しいタスクに着手できない状況も起きます。
トラブルを回避するため、新サービスを提供するような規模では、設計段階でテーブルとAPI定義を一気に作ります。</p>
<p>大規模なプロジェクトでは、タスクを複数人で分担する必要もあります。
Scalaで実装し、各コミットに実装した分のテストを義務づける環境ではタスクの分割や依存管理も重要です。
でたらめな単位でコミットするとコンパイルが失敗しマージできません。
利用者に提供するほど完成していなくてもテストで確認できるくらいの粒度で分割します。
各タスクの期日を設定し、全体スケジュールに対しては余裕があり、各メンバーの負担がきつくなりすぎないよう調整します。
依存関係によって手があいてしまうメンバーがでないよう工程を引くのも設計の一部です。</p>
<p>設計は開発チームが中心になりますが、要件定義に加わったメンバーがほぼ独力で行います。
内容は多岐に渡りますが作業量はさほど多くありません。
成果は、分量が多ければ実装メモという文書にまとめます。
それほどでもなければタスク管理の説明文に方針を記述します。
この準備によって作業中やレビュー時の手戻りを減らせます。</p>
<h2 id="実装">実装</h2>
<p>開発チームのメンバーはそれぞれのペースで未アサインのタスクに着手します。
タスクは自分ができる範囲で締切が近いものから順に行う決まりになっています。</p>
<p>これまでのステップでPSD、BRD、実装メモやタスク説明が準備されています。
担当者はこれらの文書を丁寧に読むことからスタートします。
文書化されているため休日や深夜早朝、他のメンバーが勤務していないタイミングでも着手できます。</p>
<p>わからないところ、疑問、誤解しそうなところがあれば質問します。
改善案もこのタイミングで提案します。
実装中に疑問が出てきた場合、都度チャットで質問します。
記述がわかりにくい部分は修正し、次以降に読む人がわかりやすいように改善します。
BRDに制約が足りなくて振舞が一意に定まらない場合、BRDに追記し明確に定めます。</p>
<p>既存のコードの類似箇所を参考にしながら実装者自身が処理を設計し実装を進めます。
実装しやすくするための小規模なリファクタリングは実装前にちょっとやってしまうことも多いです。
実装と同時にテストを書き、BRDに記述されているケースを中心に自信がない部分、複雑なケース、壊れそうなケースを保護します。
kippは機能レベルのすべての動作を自動テストで担保しています。</p>
<p>あらかじめテーブルやAPIを定義していても、実装をすすめていく上で不足していたテーブルやカラム、APIメソッドが出てくることがあります。
設計者と相談しつつ不足や間違いを訂正し、機能が正しく動くようにします。</p>
<p>実装したものは1人以上のコードレビューを受けます。
コードレビューでは挙動の正しさだけでなく、コメントの過不足、可読性、保守性、テストの網羅性まで詳細にチェックします。
とくにデッドロックやパフォーマンスの問題はテストで検知しにくいので他の開発者の目で丁寧に検査します。</p>
<h2 id="テストとデプロイ">テストとデプロイ</h2>
<p>Change Request（Gerritにおけるレビュー単位、GitHubのPull Requestに相当）がマージされると、自動的にAWS上の開発環境に展開されます。
そして次の週に自動的に本番環境に展開されます。</p>
<p>社内外の関係者はこのあいだに受け入れテストを行います。
実装者が追加したテストは、あくまで実装者の理解にもとづいたテストです。
丁寧にドキュメントを記述しレビューしても実装者の理解が間違っているリスクは残ります。
そこでプロダクトマネジメントチームを中心に関係者が変更された部分をテストし期待通りか確認します。
間違いがあった場合はBRDに照らして再確認し実装を修正します。
修正が次のリリースサイクルに間に合わない場合はrevertで該当部分を外します。</p>
<h2 id="開発プラクティスとして">開発プラクティスとして</h2>
<p>ここまでkippで実践している開発ワークフローを具体的に説明してきました。
最後にこのワークフローを開発プラクティスの観点でどう考えているか紹介します。</p>
<p>以上から分かるように、kippの開発プロセスはウォーターフォールに近く見えます。
継続的デリバリーを実践している点などアジャイルのプラクティスを取り入れている部分もあります。
私達はこれを、アジャイル志向のウォーターフォールと捉えています。</p>
<p>ここでアジャイル志向と言う理由は<a href="https://agilemanifesto.org/iso/ja/manifesto.html">Agile Manifesto</a>を参照している一点に尽きます。
要件定義と受け入れテストという顧客の意思を受け止めるべきステップでは協調を重視してます。
また要求を整理するドキュメントは丁寧に作成する一方、実装のためのドキュメントは最小限に抑え、開発成果物を早く、読みやすく作ることに注力しています。
提起から実装は速ければ2週間で完了します。
新規サービスの立ち上げも4ヶ月から6ヶ月で完了した実績があります。
ウォーターフォールのワークフローでありながら個々のトピックを高速で処理し、開発イテレーション数を稼ぐことで変化への対応力を身につけています。</p>
<p>私達が広く共有されている開発プラクティスを採用せず、このような手法を取っている理由をスクラムとの対比で説明します。
スクラムではチームの一体性を重視し、デイリースクラムをはじめ会話の場が多く準備されます。
イテレーションが軸となり、計画と振り返りを行います。</p>
<p>スクラム開発がkippにそぐわない第一の理由は直接の対話を重視する点にあります。
kippは個人主義が強く、業務委託メンバーも多いため、毎日決まった時間に全員が集合するのは不可能です。
正社員でさえ全員揃うタイミングがない日もあります。
時間、場所に囚われない働き方を前提としたチームには対話を強制できません。</p>
<p>第二の理由はイテレーションの考え方とkippのタスク管理がマッチしないことです。
スクラムのイテレーションの背景には単一の目標にむかってチームがイテレーションごとに前進していく像があります。
しかしkippのタスク管理表には緊急の障害対応から数カ月先のタスクまで規模も重要度もプロダクトも様々なタスクが混在しています。
これらをひとつのスプリント計画に落とし込むのは時間がかかるわりに無益な作業でしょう。</p>
<p>ではどのようにタスク管理をするのでしょうか。
答えは3つのルールだけでコントロールされています。</p>
<ul>
<li>充分な時間を取る</li>
<li>出来ることをやる</li>
<li>締め切り厳守</li>
</ul>
<p>タスクを作成するとき、すべての開発タスクは締切を2週以上先に置くルールがあります。
重厚なタスクでも、改善レベルであれば2週間で分担して終わらせられます。
大規模な新機能開発は例外で、戦略上のリリース時期から逆算して締切を順次設定します。</p>
<p>開発者はその時々で出来ることをやることを期待されています。
締切が短かったり不慣れなタスクで終わらなそうな場合は他のタスクを選択するべきです。
タスクの説明やBRDを読んですんなり理解できれば完遂できるだろうと見込めます。</p>
<p>締め切り厳守は一番身も蓋もないルールです。
スクラムで振り返りが必要なのは、締切を守れないことがあるからです。
初めから全ての締切を守れば振り返りは不要です。
一度タスクを取った以上、なにがあってもそれを完了させる責任があります。
完了する方策のなかには他のメンバーのヘルプを仰いで移譲することも含まれます。</p>
<p>締め切り厳守の仕組みとして、締切2日前から完了するまで、毎日Slackでメンションが飛んできます。
本人だけでなくチーム全員の目に触れるので、終了見込みを尋ねたりタスクを分担する契機にもなります。</p>
<p>これらのルールは開発者が高い実装能力を持つことを前提としています。
kippの開発者には行き詰まらず、常に見積もった日数で作業を完了する能力が求められます。
このワークフローに魅力を感じ、堅実で責任感ある開発ライフを送りたいと思った方はぜひ<code>people@kipp-corp.com</code>にご連絡ください。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[IPv6 と金融サービス]]></title>
            <link>/blog/ipv6</link>
            <guid isPermaLink="false">/blog/ipv6</guid>
            <pubDate>Tue, 01 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>こんにちは。CTOの冨田です。
また投稿間隔が開いてしまいました。
普段の業務の中から切り出して皆さんに発信できることは思ったほどありませんね。</p>
<p>以前よりアナウンスされていましたが、ついに2月1日よりNTTドコモによる<a href="https://www.nttdocomo.co.jp/service/spmode/function/ipv6/index.html">IPv6シングルスタック方式の提供が開始されました</a>。
ドコモ社は日本有数の移動体通信事業者です。
ドコモ社がIPv6シングルスタックを開始することでkippのサービスを利用されるお客様の端末の接続環境の無視できない割合が影響を受けると予想されます。
またドコモ社の取り組みに倣い、他の移動体通信事業者も同様の取り組みを検討するでしょう。</p>
<p>移動体通信に限らずIPv6活用の取り組みは広く行われています。
多くの固定回線プロバイダがIPv6のオプションを提供しています。
実際に<a href="https://www.google.com/intl/en/ipv6/statistics.html#tab=per-country-ipv6-adoption">Googleサービスへのアクセス統計</a>を見ると執筆時点で日本からのアクセスの43%がIPv6で行われていると分かります。</p>
<p>本記事ではIPv6が普及していく、さらにはIPv4を持たない端末が増えてくる状況で金融サービス提供者が何をするべきか考察します。</p>
<h2 id="ipアドレスと金融サービス">IPアドレスと金融サービス</h2>
<p>IPアドレスはインターネット通信において機器を識別する値です。
ある機器から宛先の機器にパケットを送信し、その返信を受け取るという通信の基本動作はIPアドレスによる発信元、送信先の指定によって実現されます。
なお本稿では金融サービスに限ってお話しするため、ローカルエリアネットワーク内でのIP通信に言及せずインターネット上の通信のみを扱います。</p>
<p>金融サービスをはじめとするインターネットサービスの多くはサービスプロバイダがサーバを提供し、エンドユーザがクライアントになります。
エンドユーザの機器からサーバに送られたIPパケットの送信元IPアドレスはエンドユーザの環境について一定の情報を持ちます。
この情報はサービスの利便性、安全性を向上する役に立つため、多くの事業者が送信元IPアドレスを記録、分析しています。</p>
<p>たとえばIPアドレス帯と国を紐づけるデータベースを利用し、送信元の国を推定できます。
kippは日本国内向けにサービスを提供しており、ほとんどの利用者は日本国内で利用しています。
直前まで日本国内からアクセスしていた利用者が突然国外の遠い地域からアクセスした場合、不正アクセスの被害に遭っている疑いがあります。
あるいは同一送信元IPアドレスから大量のアカウントを操作している場合、利用規約に反して複数アカウントを取得している懸念があります。</p>
<p>こういった推定は不確実です。
IPv4アドレスによる推定にまつわる誤検知のリスクはよく知られています。
たとえば学校や会社から複数の利用者が同時にアクセスすることで複数アカウント検知を発火させる恐れがあります。
IPv4アドレスと国の対応付けデータベースが更新されておらず、<a href="https://www.nuro.jp/article/kaigai-ip/">日本国内からのアクセスが国外からのアクセスと判定されサービスが利用できなくなった事例</a>もあります。</p>
<p>それでは、IPv6利用が普及した状況でIPアドレスによる推定はどんな影響を受けるのでしょうか。
サービスプロバイダはどう対応するべきでしょうか。</p>
<h2 id="ケースとリスク">ケースとリスク</h2>
<p>具体的にサーバ・クライアントのそれぞれの状況の組み合わせについてリスクを分析していきます。
まずサーバ側にはIPv6とIPv4両方に対応しているケース（IPv6/IPv4デュアルスタック）とIPv4のみに対応しているケースを考えます。
IPv6のみに対応する運用は現在のエンドユーザのIPv6利用可能率を考慮すると実用的ではないため省きます。</p>
<p>次にクライアント側はIPv4のみ利用できるケース、IPv6のみ利用できるケース（IPv6シングルスタック）、IPv6/IPv4両方利用できるケースがあります。
IPv6シングルスタックでは<a href="https://www.nttdocomo.co.jp/service/spmode/function/ipv6/index.html">ドコモの事例</a>のようにインターネットサービスプロバイダによってIPv4アドレス変換されることがほとんどですので、変換機能を仮定します。</p>
<p>サーバ2パターン、クライアント3パターンの掛け算をすると6パターンですが、今回興味があるのはサーバ側の対応です。
サーバの受け取るIPパケットがIPv6パケットかIPv4パケットかでおおまかに場合分けできます。</p>
<h3 id="ipv4通信の場合">IPv4通信の場合</h3>
<p>伝統的なケースです。
前の節で議論したようにIPv4アドレスの利用は長く行われており、弱点も含めてよく知られています。</p>
<h3 id="ipv6通信の場合">IPv6通信の場合</h3>
<p>サーバとクライアントがともにIPv6対応している場合、IPv6で通信します。
ともにIPv6/IPv4デュアルスタックの場合、<a href="https://datatracker.ietf.org/doc/html/rfc8305">Happy Eyeballs</a>によりIPv4が利用されることもあります。
上述の「IPv4通信の場合」に該当するので、取り立てて議論する必要はありません。</p>
<p>サーバがIPv6アドレスを記録、利用する場合、追加で注意すべきことがあります。</p>
<ol>
<li>IPアドレスの構造が違います。
IPv4アドレスは32ビット値であり、文字列表現としてはピリオド句切りの4つの10進数値の組がよく用いられます。
IPv6アドレスは128ビット値であり、文字列表現も16進数とコロンを使います。
IPアドレスを保存するデータベースカラムを32ビット値のみに対応した整数型や15文字までの文字列型にしているとIPv6アドレスを保存できません。
初歩的なミスなので、検証中にすぐ気がつけるでしょう。</li>
<li>同一拠点でも別機器のIPv6アドレスは異なります。
小規模なエンドユーザの拠点、すなわち一般的な家庭を想定しています。
一般的な家庭ではグローバルIPv4アドレスを1つだけ利用し、NAT（network address translastion）機能によって複数の端末が1つのグローバルIPv4を共用します。
IPv6環境の場合、拠点にはプロバイダからグローバルルーティングプレフィックスが割り振られます。
それにルータがサブネット識別子を付加し通常64ビットのサブネットプレフィックスを作ります。
さらに各機器が64ビットのインタフェース識別子を持ち、全体で128ビットのIPv6アドレスになります。
IPv6アドレスではグローバルルーティングプレフィックス部を把握する方法がなく、IPv4アドレスに比して同一拠点判定の信頼性が揺らぎます。
グローバルルーティングプレフィックスは64ビットがよく見られますが、48ビットを割り当てるケースもあるようです（<a href="https://www.iij.ad.jp/dev/tech/techweek/pdf/tw2011_08_ipv6_1.pdf">フレッツにおけるIPv6接続について(PDF)</a>。古い資料です）。</li>
<li>同一機器からのIPv6アドレスも経時的に変わります。
前項でも言及したインタフェース識別子が機器に対して固定だとIPv6アドレスが同じなら同一拠点の同一機器と識別できますし、機器を移動しても別拠点の同一機器と識別できます。
これはセキュリティ、プライバシーの観点で懸念があります。
この懸念を解消するため、いくつかのIPv6アドレスの自動設定技術は定期的にインタフェース識別子を変更します。
たとえば<a href="https://datatracker.ietf.org/doc/html/rfc8981">RFC8981</a>で定義されるTeporary IPv6アドレスは1日程度でIPv6アドレスを変更します。</li>
</ol>
<h3 id="クライアントがipv6シングルスタックでサーバがipv4のみ対応の場合">クライアントがIPv6シングルスタックでサーバがIPv4のみ対応の場合</h3>
<p>「IPv4通信の場合」があえて詳細に分析されずわずか2文で終わったことに違和感を感じた方は鋭いです。</p>
<blockquote>
<p>伝統的なケースです。</p>
</blockquote>
<p>大嘘です。
IPv6アドレスしか持っていない端末がIPv4アドレスしか持っていないサーバに接続したくても、直接はできません。
だれかが間を取り持つ必要があります。
<a href="https://www.nttdocomo.co.jp/info/news_release/2022/01/31_00.html">ドコモ社の資料の図</a>にもあるとおり、このケースでは交換局が端末のIPv6アドレスからIPv4アドレスに変換します。
サーバと交換局の間はIPv4通信、交換局と端末の間はIPv6通信になります。
<strong>IPv4アドレスを収集しているサーバは、端末のIPアドレスのつもりで交換局のIPアドレスを掴まされています</strong>。</p>
<p>実際に交換局がどの程度の数のIPv4アドレスを利用するのかわかりませんが、IPv6アドレスと同数ではありえないでしょう。
交換局はNAT的に動作していると仮定するのが自然です。
IPv6シングルスタッククライアントが多数いる状況では多くのネットワーク技術に詳しくないエンドユーザが交換局のIPv4アドレスでアクセスしてくる状況が生まれます。</p>
<p>従来もエンドユーザがプロキシサーバやVPNを利用していて、サーバの受け取るIPv4アドレスがエンドユーザ自身のIPアドレスでないケースはありました。
しかしその率は無視できるほど低く、エンドユーザ側もなんらかの意図があって利用していたと考えられます。</p>
<p>この状況で従来型のIPv4アドレス分析をすると多数の一般的なエンドユーザに対して偽陽性を発生させるでしょう。
同一拠点判定で利用停止を実施しているならば、実際には全く関係のない多数のユーザが同一拠点からのアクセスとされ、一斉に利用停止されてしまいます。
これが本稿で取り上げる最大のリスクです。</p>
<h2 id="対策">対策</h2>
<p>以上のリスクに対する対策を考えましょう。
まず「クライアントがIPv6シングルスタックでサーバがIPv4のみ対応の場合」ですが、サーバがIPv6に対応するか、IPv4アドレスの一致判定を止めるしかありません。
ドコモ社が利用しているIPv4帯を確認してそれだけ無視リストに登録するといった運用も考えられますが、今後IPv6シングルスタック通信を提供する通信事業者が増えることを想定すると無益な努力でしょう。</p>
<p>IPアドレスによる判定を止める、あるいは重要度を落とすのは価値ある判断です。
サービスプロバイダはIPアドレスの扱いにおいてもプライバシーを尊重するべきです。
非技術者のなかにはIPアドレスを住所と同じように本人や所在地を同定できる情報だと信じている人もいます。
この信念はIPv4アドレスのみ取っても大きな間違いであり、IPv6の普及に伴ってますます信頼性が揺いでいます。
IPv6シングルスタックの導入は信頼性が十分でない中でIPアドレスの分析がビジネス的価値を生むのか問い直す良い機会です。</p>
<p>IPアドレスの利用を続けるならば、サーバをIPv6/IPv4デュアルスタックに変更し、かつIPアドレス処理の仕組みをIPv6/IPv4両対応させましょう。
kippでは全てのサービスにALBを利用しています。
<a href="https://aws.amazon.com/jp/premiumsupport/knowledge-center/elb-configure-with-ipv6/">ALBは設定変更でデュアルスタックにできます</a>。</p>
<p>「IPv6通信の場合」の分析から、IPv6アドレスを取り扱う際に保存と比較に注意する必要があるとわかります。
値の長さだけでなく、比較方法に依存して値の保存方法を考慮すべき点にも注意しましょう。
たとえば先頭64ビットだけ比較すると決めたら、その比較をデータベース上で効率よく実施できる保存方式を採用します。
極端な例ですが、IPv6アドレス全体のハッシュ値を保存していた場合、検索不可能になります。
文字列表現で保存して前方一致検索するのか、数値表現で保存して数値の範囲で検索するのか、保存時に先頭64ビットを切り取って別カラムに保存するのか、データベース製品の特性を調査し決定します。</p>
<p>同一拠点のチェックのためIPv4アドレスが同一であるという推定を使っていたならば概ね先頭64ビットが同一であるという比較に置き換えてよいでしょう。
その上で過去にIPv4アドレスチェックで偽陽性が頻発していたならばIPv6の特性を生かし条件を調整する余地もあります。</p>
<p>すべての対応を実施してもなお、IPアドレスによる所在地や同一性の判定は不確実性のあるものです。
ビジネス設計全体にこの不確実性を織り込むべきです。
たとえばIPアドレスだけを根拠にアカウントを停止するのは悪手です。
決済行動やアクセスタイミングなど複数の情報ソースを参照し、重み付けして判断しましょう。</p>
<p>不正判定はどうしても偽陽性を生むことがあります。
ユーザの行動を制限する前にサービス運営者が判定結果をチェックしたり、ユーザに確認のメッセージを送るのも重要です。
ユーザからの異議があった場合はユーザの虚偽を警戒しつつ、システムの誤判定や実装ミスも考慮して誠実に対応すべきです。</p>
<p>kippでは業界や世間の動向に素早く反応してサービスを改善するため、開発者の採用を強化しています。
また不正利用判定のような多くのログ、属性情報を組み合わせて信頼性の高い判断を下すデータ分析も改善を計画しています。
このような業務に興味をお持ちの方はお気軽に<a href="mailto:people@kipp-corp.com">people@kipp-corp.com</a>までご連絡ください。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[CloudWatch Syntheticsを用いたgRPC APIの外形監視]]></title>
            <link>/blog/canary-grpc</link>
            <guid isPermaLink="false">/blog/canary-grpc</guid>
            <pubDate>Fri, 01 Oct 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>こんにちは。CTOの冨田です。 すこし期間が空いてしまいました。最近は新機能開発の比重は減り、かわりに運用の改善が大きなトピックになっています。kippはBanking as a Serviceという名称でSaaS提供をしています。金融サービスという性質も相まって、可用性は重大なトピックです。</p>
<h2 id="service-level-objective-sloと監視">Service Level Objective (SLO)と監視</h2>
<p><a href="https://cloud.google.com/architecture/defining-SLOs?hl=ja">GCPのドキュメント</a>でも述べられているように、SaaSの健全な発達にはSLOの策定が肝要です。SLOはService Level Indicator (SLI)と、その目標値で設定されます。</p>
<p>SLIはサービス全体のなかからサービスの継続にクリティカルな要素を洗い出し、その要素について測定可能な項目をピックアップする形で決定しました。例えば「正常に」といった定性的な要求は測定できないので、「内部エラーでない応答の数とリクエスト数の比率」といった定量的な指標に置き換えます。たくさんの値がSLIの候補になりますが、たくさんの値を精緻に追跡するのは負担が大きいので重要な値を絞り込みます。実際にkippで採用しているものの中にはAPIの処理成功率、日次のバッチ処理の終了時刻などがあります。</p>
<p>それぞれのSLIに対し閾値を定め、SLOを策定します。
閾値はサービスの性質上達成されなければならない目標と、一般的に要求される水準、そしてインフラ技術が提供する水準を考慮しました。
サービスの性質を考慮すべき代表的な例はバッチ処理です。
たとえば9時から始まるバッチ処理があり、次の社外の処理が12時に予定されていれば、3時間で必ず終了しなければなりません。
この場合、修正・復旧・リトライの時間を考慮し10:10までに終了することをSLOとします。
一般的な水準としては<a href="https://www.meti.go.jp/policy/netsecurity/secdoc/contents/seccontents_000078.html">経済産業省のSaaS向けSLAガイドライン</a>を参照しました。
顧客企業に開示や説明を求められたとき、客観的な妥当性のあるソースを示すことを重視しました。
最後に上限としてインフラ技術のSLAを確認しました。
例えば<a href="https://aws.amazon.com/compute/sla/">AWS FargateのSLA</a>はmonthly uptime 99.99%ですので、これより厳しい目標を保証するのは困難です。
各SLIに影響する要素技術を確認し、当社の閾値がそれらの最低値に収まるよう考慮しました。</p>
<h2 id="ログ監視と外形監視">ログ監視と外形監視</h2>
<p>SLOに沿って監視システムを構築しました。
社内ではSRE業務のノウハウが不足しており、作業に使える時間も不十分だったため、<a href="https://x-tech5.co.jp/">X-Tech 5</a>さんにご協力いただきました。
過敏すぎるPagerDutyが設定されていただけの状態からDatadogの導入を経て信頼性が高く誤報の少ない体制をわずか数週間で構築していただきました。
アラート頻度が減ると同時に問題があったときの調査スピードも改善できました。</p>
<p>X-Tech 5のメンバーの方との相談で、できるところから開始し、改善していこうという方針を確認しました。
最初、監視対象はログとCloudWatchなどのモニタリングを中心にしました。
これらは予め準備されていたので低コストでスタートでき、またデータ量が豊富なため除外設定など細かい調整が容易なためです。</p>
<p>それからALBの障害など、サーバアプリケーションが原因となる障害ではないが顧客企業に報告するため把握する必要のあるものを追加していきました。
このような障害ではリクエストがサーバに到達しないため、ログを分析しても現象を発見できません。
そこで外形監視を導入することになりました。</p>
<p>外形監視とはシンプルにはネットワーク外からサービスにアクセスするだけのことですから、別系統のサーバでCron jobを回す程度のことでも達成できます。
しかし運用の観点では結果の確認や監視システムとの統合の利便性のほうが重視されます。
この時もX-Tech 5さんにアドバイスを仰ぎ、<a href="https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries.html">CloudWatch Synthetics</a>を紹介していただきました。
Canary Scriptとして設定したJavaScriptを定期的に実行し結果を収集できます。</p>
<h2 id="grpc-apiをcanaryで監視する">gRPC APIをCanaryで監視する</h2>
<p>CanaryはPuppeteer環境を持ち、画面キャプチャを撮れるなどウェブサービスの監視用の機能が充実したサービスです。
APIの監視にも利用できます。
しかし私達の監視したいAPIはgRPCであり、通常のhttps APIのテンプレートは利用できませんでした。
gRPCはhttp/2.0を要求するため、Node.jsでhttp/2.0とgRPCプロトコルに向けた監視スクリプトを記述しました。</p>
<p>まず<a href="https://github.com/grpc/grpc/blob/master/doc/health-checking.md">GRPC Health Checking Protocol</a>で定められた<code>grpc.health.v1.Health/Check</code>に向けてリクエストを送ることにしました。
このgRPC API MethodはRDBMSなどバックエンドの接続性も考慮してAPIが正常に利用可能か応答するように実装してありました。
簡単のため、リクエストパラメタの<code>service</code>は無視し、ステータスコードが<code>200</code>のとき正常と定めました。</p>
<p>Canary Scriptではhttps/2.0でgRPCの空メッセージを<code>grpc.health.v1.Health/Check</code>に送信し、レスポンスの<code>:status</code>を確認すれば十分です。
gRPCのライブラリを導入する手間が省けます。</p>
<p>実際にCanary Scriptで上述の処理を記述するとこのようになります。
少し変わっているのは<code>\0\0\0\0\0</code>の部分です。これはgRPCプロトコルに沿って空メッセージをエンコードしたバイト列です。
少しhackyですが、簡単なものならばプロトコルに沿ったエンコード・デコード処理を静的バイト列や標準ライブラリによって実装することで1ファイルに収められます。</p>
<pre class="shiki one-dark-pro" style="background-color:#282c34;color:#abb2bf" tabindex="0"><code class="language-js"><span class="line"><span style="color:#C678DD">var</span><span style="color:#E06C75"> synthetics</span><span style="color:#56B6C2"> =</span><span style="color:#61AFEF"> require</span><span style="color:#ABB2BF">(</span><span style="color:#98C379">"Synthetics"</span><span style="color:#ABB2BF">);</span></span>
<span class="line"><span style="color:#C678DD">const</span><span style="color:#E5C07B"> log</span><span style="color:#56B6C2"> =</span><span style="color:#61AFEF"> require</span><span style="color:#ABB2BF">(</span><span style="color:#98C379">"SyntheticsLogger"</span><span style="color:#ABB2BF">);</span></span>
<span class="line"><span style="color:#C678DD">const</span><span style="color:#E5C07B"> http2</span><span style="color:#56B6C2"> =</span><span style="color:#61AFEF"> require</span><span style="color:#ABB2BF">(</span><span style="color:#98C379">"http2"</span><span style="color:#ABB2BF">);</span></span>
<span class="line"><span style="color:#C678DD">const</span><span style="color:#E5C07B"> syntheticsConfiguration</span><span style="color:#56B6C2"> =</span><span style="color:#E5C07B"> synthetics</span><span style="color:#ABB2BF">.</span><span style="color:#61AFEF">getConfiguration</span><span style="color:#ABB2BF">();</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C678DD">function</span><span style="color:#61AFEF"> sendGrpcHealthCheckRequest</span><span style="color:#ABB2BF">(</span><span style="color:#E06C75;font-style:italic">server</span><span style="color:#ABB2BF">) {</span></span>
<span class="line"><span style="color:#C678DD">  return</span><span style="color:#C678DD"> new</span><span style="color:#E5C07B"> Promise</span><span style="color:#ABB2BF">((</span><span style="color:#E06C75;font-style:italic">resolve</span><span style="color:#ABB2BF">, </span><span style="color:#E06C75;font-style:italic">reject</span><span style="color:#ABB2BF">) </span><span style="color:#C678DD">=></span><span style="color:#ABB2BF"> {</span></span>
<span class="line"><span style="color:#C678DD">    const</span><span style="color:#E5C07B"> client</span><span style="color:#56B6C2"> =</span><span style="color:#E5C07B"> http2</span><span style="color:#ABB2BF">.</span><span style="color:#61AFEF">connect</span><span style="color:#ABB2BF">(</span><span style="color:#E06C75">server</span><span style="color:#ABB2BF">);</span></span>
<span class="line"><span style="color:#E5C07B">    client</span><span style="color:#ABB2BF">.</span><span style="color:#61AFEF">on</span><span style="color:#ABB2BF">(</span><span style="color:#98C379">"error"</span><span style="color:#ABB2BF">, (</span><span style="color:#E06C75;font-style:italic">err</span><span style="color:#ABB2BF">) </span><span style="color:#C678DD">=></span><span style="color:#ABB2BF"> {</span></span>
<span class="line"><span style="color:#E5C07B">      synthetics</span><span style="color:#ABB2BF">.</span><span style="color:#61AFEF">addExecutionError</span><span style="color:#ABB2BF">(</span><span style="color:#98C379">"http2 error"</span><span style="color:#ABB2BF">, </span><span style="color:#E06C75">err</span><span style="color:#ABB2BF">);</span></span>
<span class="line"><span style="color:#61AFEF">      reject</span><span style="color:#ABB2BF">(</span><span style="color:#E06C75">err</span><span style="color:#ABB2BF">);</span></span>
<span class="line"><span style="color:#ABB2BF">    });</span></span>
<span class="line"></span>
<span class="line"><span style="color:#7F848E;font-style:italic">    // No compress (0u8) + Length 0 (0u32)</span></span>
<span class="line"><span style="color:#C678DD">    const</span><span style="color:#E5C07B"> buffer</span><span style="color:#56B6C2"> =</span><span style="color:#C678DD"> new</span><span style="color:#61AFEF"> Buffer</span><span style="color:#ABB2BF">(</span><span style="color:#98C379">"</span><span style="color:#56B6C2">\0\0\0\0\0</span><span style="color:#98C379">"</span><span style="color:#ABB2BF">);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C678DD">    const</span><span style="color:#E5C07B"> req</span><span style="color:#56B6C2"> =</span><span style="color:#E5C07B"> client</span><span style="color:#ABB2BF">.</span><span style="color:#61AFEF">request</span><span style="color:#ABB2BF">({</span></span>
<span class="line"><span style="color:#98C379">      ":scheme"</span><span style="color:#ABB2BF">: </span><span style="color:#98C379">"https"</span><span style="color:#ABB2BF">,</span></span>
<span class="line"><span style="color:#98C379">      ":path"</span><span style="color:#ABB2BF">: </span><span style="color:#98C379">"/grpc.health.v1.Health/Check"</span><span style="color:#ABB2BF">,</span></span>
<span class="line"><span style="color:#98C379">      ":method"</span><span style="color:#ABB2BF">: </span><span style="color:#98C379">"POST"</span><span style="color:#ABB2BF">,</span></span>
<span class="line"><span style="color:#98C379">      "content-type"</span><span style="color:#ABB2BF">: </span><span style="color:#98C379">"application/grpc+proto"</span><span style="color:#ABB2BF">,</span></span>
<span class="line"><span style="color:#98C379">      "content-length"</span><span style="color:#ABB2BF">: </span><span style="color:#E5C07B">buffer</span><span style="color:#ABB2BF">.</span><span style="color:#E06C75">length</span><span style="color:#ABB2BF">,</span></span>
<span class="line"><span style="color:#ABB2BF">    });</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C678DD">    let</span><span style="color:#E06C75"> resHeader</span><span style="color:#56B6C2"> =</span><span style="color:#D19A66"> null</span><span style="color:#ABB2BF">;</span></span>
<span class="line"><span style="color:#E5C07B">    req</span><span style="color:#ABB2BF">.</span><span style="color:#61AFEF">on</span><span style="color:#ABB2BF">(</span><span style="color:#98C379">"response"</span><span style="color:#ABB2BF">, (</span><span style="color:#E06C75;font-style:italic">headers</span><span style="color:#ABB2BF">, </span><span style="color:#E06C75;font-style:italic">flags</span><span style="color:#ABB2BF">) </span><span style="color:#C678DD">=></span><span style="color:#ABB2BF"> {</span></span>
<span class="line"><span style="color:#E06C75">      resHeader</span><span style="color:#56B6C2"> =</span><span style="color:#ABB2BF"> [</span><span style="color:#E06C75">headers</span><span style="color:#ABB2BF">, </span><span style="color:#E06C75">flags</span><span style="color:#ABB2BF">];</span></span>
<span class="line"><span style="color:#E5C07B">      log</span><span style="color:#ABB2BF">.</span><span style="color:#61AFEF">info</span><span style="color:#ABB2BF">(</span><span style="color:#98C379">`Res flags:</span><span style="color:#C678DD">${</span><span style="color:#E06C75">flags</span><span style="color:#C678DD">}</span><span style="color:#98C379">`</span><span style="color:#ABB2BF">);</span></span>
<span class="line"><span style="color:#C678DD">      for</span><span style="color:#ABB2BF"> (</span><span style="color:#C678DD">const</span><span style="color:#E5C07B"> name</span><span style="color:#C678DD"> in</span><span style="color:#E06C75"> headers</span><span style="color:#ABB2BF">) {</span></span>
<span class="line"><span style="color:#E5C07B">        log</span><span style="color:#ABB2BF">.</span><span style="color:#61AFEF">info</span><span style="color:#ABB2BF">(</span><span style="color:#98C379">`</span><span style="color:#C678DD">${</span><span style="color:#E06C75">name</span><span style="color:#C678DD">}</span><span style="color:#98C379">: </span><span style="color:#C678DD">${</span><span style="color:#E06C75">headers</span><span style="color:#ABB2BF">[</span><span style="color:#E06C75">name</span><span style="color:#ABB2BF">]</span><span style="color:#C678DD">}</span><span style="color:#98C379">`</span><span style="color:#ABB2BF">);</span></span>
<span class="line"><span style="color:#ABB2BF">      }</span></span>
<span class="line"><span style="color:#C678DD">      if</span><span style="color:#ABB2BF"> (</span><span style="color:#E06C75">headers</span><span style="color:#ABB2BF">[</span><span style="color:#98C379">":status"</span><span style="color:#ABB2BF">] </span><span style="color:#56B6C2">!==</span><span style="color:#D19A66"> 200</span><span style="color:#ABB2BF">) {</span></span>
<span class="line"><span style="color:#E5C07B">        synthetics</span><span style="color:#ABB2BF">.</span><span style="color:#61AFEF">addExecutionError</span><span style="color:#ABB2BF">(</span><span style="color:#98C379">"Response status is not 200"</span><span style="color:#ABB2BF">);</span></span>
<span class="line"><span style="color:#61AFEF">        reject</span><span style="color:#ABB2BF">();</span></span>
<span class="line"><span style="color:#ABB2BF">      }</span></span>
<span class="line"><span style="color:#ABB2BF">    });</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C678DD">    let</span><span style="color:#E06C75"> res</span><span style="color:#56B6C2"> =</span><span style="color:#98C379"> ""</span><span style="color:#ABB2BF">;</span></span>
<span class="line"><span style="color:#E5C07B">    req</span><span style="color:#ABB2BF">.</span><span style="color:#61AFEF">on</span><span style="color:#ABB2BF">(</span><span style="color:#98C379">"data"</span><span style="color:#ABB2BF">, (</span><span style="color:#E06C75;font-style:italic">chunk</span><span style="color:#ABB2BF">) </span><span style="color:#C678DD">=></span><span style="color:#ABB2BF"> {</span></span>
<span class="line"><span style="color:#E06C75">      res</span><span style="color:#56B6C2"> +=</span><span style="color:#E06C75"> chunk</span><span style="color:#ABB2BF">;</span></span>
<span class="line"><span style="color:#ABB2BF">    });</span></span>
<span class="line"><span style="color:#E5C07B">    req</span><span style="color:#ABB2BF">.</span><span style="color:#61AFEF">on</span><span style="color:#ABB2BF">(</span><span style="color:#98C379">"end"</span><span style="color:#ABB2BF">, () </span><span style="color:#C678DD">=></span><span style="color:#ABB2BF"> {</span></span>
<span class="line"><span style="color:#E5C07B">      log</span><span style="color:#ABB2BF">.</span><span style="color:#61AFEF">info</span><span style="color:#ABB2BF">(</span><span style="color:#E06C75">res</span><span style="color:#ABB2BF">);</span></span>
<span class="line"><span style="color:#E5C07B">      resHeader</span><span style="color:#ABB2BF">.</span><span style="color:#61AFEF">push</span><span style="color:#ABB2BF">(</span><span style="color:#E06C75">res</span><span style="color:#ABB2BF">);</span></span>
<span class="line"><span style="color:#61AFEF">      resolve</span><span style="color:#ABB2BF">(</span><span style="color:#E06C75">resHeader</span><span style="color:#ABB2BF">);</span></span>
<span class="line"><span style="color:#ABB2BF">    });</span></span>
<span class="line"></span>
<span class="line"><span style="color:#E5C07B">    req</span><span style="color:#ABB2BF">.</span><span style="color:#61AFEF">setEncoding</span><span style="color:#ABB2BF">(</span><span style="color:#98C379">"utf8"</span><span style="color:#ABB2BF">);</span></span>
<span class="line"><span style="color:#E5C07B">    req</span><span style="color:#ABB2BF">.</span><span style="color:#61AFEF">write</span><span style="color:#ABB2BF">(</span><span style="color:#E06C75">buffer</span><span style="color:#ABB2BF">);</span></span>
<span class="line"><span style="color:#E5C07B">    req</span><span style="color:#ABB2BF">.</span><span style="color:#61AFEF">end</span><span style="color:#ABB2BF">();</span></span>
<span class="line"><span style="color:#ABB2BF">  });</span></span>
<span class="line"><span style="color:#ABB2BF">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C678DD">const</span><span style="color:#61AFEF"> apiCanaryBlueprint</span><span style="color:#56B6C2"> =</span><span style="color:#C678DD"> async</span><span style="color:#C678DD"> function</span><span style="color:#ABB2BF"> () {</span></span>
<span class="line"><span style="color:#C678DD">  const</span><span style="color:#E5C07B"> server</span><span style="color:#56B6C2"> =</span><span style="color:#98C379"> "https://example.com:443"</span><span style="color:#ABB2BF">; </span><span style="color:#7F848E;font-style:italic">// server url</span></span>
<span class="line"></span>
<span class="line"><span style="color:#E5C07B">  syntheticsConfiguration</span><span style="color:#ABB2BF">.</span><span style="color:#61AFEF">setConfig</span><span style="color:#ABB2BF">({</span></span>
<span class="line"><span style="color:#E06C75">    restrictedHeaders</span><span style="color:#ABB2BF">: [],</span></span>
<span class="line"><span style="color:#E06C75">    restrictedUrlParameters</span><span style="color:#ABB2BF">: [],</span></span>
<span class="line"><span style="color:#ABB2BF">  });</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C678DD">  await</span><span style="color:#E5C07B"> synthetics</span><span style="color:#ABB2BF">.</span><span style="color:#61AFEF">executeStep</span><span style="color:#ABB2BF">(</span><span style="color:#98C379">`Verify </span><span style="color:#C678DD">${</span><span style="color:#E06C75">server</span><span style="color:#C678DD">}</span><span style="color:#98C379">`</span><span style="color:#ABB2BF">, </span><span style="color:#C678DD">async</span><span style="color:#ABB2BF"> () </span><span style="color:#C678DD">=></span><span style="color:#ABB2BF"> {</span></span>
<span class="line"><span style="color:#C678DD">    return</span><span style="color:#C678DD"> await</span><span style="color:#61AFEF"> sendGrpcHealthCheckRequest</span><span style="color:#ABB2BF">(</span><span style="color:#E06C75">server</span><span style="color:#ABB2BF">);</span></span>
<span class="line"><span style="color:#ABB2BF">  });</span></span>
<span class="line"><span style="color:#ABB2BF">};</span></span>
<span class="line"></span>
<span class="line"><span style="color:#E5C07B">exports</span><span style="color:#ABB2BF">.</span><span style="color:#61AFEF">handler</span><span style="color:#56B6C2"> =</span><span style="color:#C678DD"> async</span><span style="color:#ABB2BF"> () </span><span style="color:#C678DD">=></span><span style="color:#ABB2BF"> {</span></span>
<span class="line"><span style="color:#C678DD">  return</span><span style="color:#C678DD"> await</span><span style="color:#61AFEF"> apiCanaryBlueprint</span><span style="color:#ABB2BF">();</span></span>
<span class="line"><span style="color:#ABB2BF">};</span></span></code></pre>
<h2 id="導入の結果">導入の結果</h2>
<p>FargateやALBのSLAを考えれば半ば当たり前ですが、外形監視で問題が発見されることはほぼありません。
現在のところ監視スクリプトの設定ミス以外で問題が報告された事例はないです。
しかしアプリケーションログ、ALBのメトリクス、Canaryと複数段階で監視を持つことで障害時の切り分けが半自動的に出来るようになり、お客様に影響しうる事態を最も早く把握できるという自信が付きました。
「備えあれば憂いなし」のとおり、ひとつひとつの準備がいざという時の問題解決を簡単にします。
今後も監視を拡充し、高品質なBaaSを提供しながら枕を高くして眠れる体制を維持していきます。
kippではSRE分野に強味を持つ方も募集しています。
最強の体制の維持に興味をお持ちの方は<code>people@kipp-corp.com</code>までお気軽にご連絡ください。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[kippエンジニアリングチームの作り方]]></title>
            <link>/blog/team</link>
            <guid isPermaLink="false">/blog/team</guid>
            <pubDate>Fri, 13 Aug 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>こんにちは。CTOの冨田です。
これまでのブログ記事では利用技術についてお話しました。
今回はこれまでの記事、あるいは今後の記事でkippに興味をお持ちいただける方がいることを期待して、エンジニアリングチームの作り方をお伝えします。
まずkippエンジニアリングチームの実情とその背景にある考え方を述べ、それから考え方を実現するための取り組みを説明します。
他の会社でチームビルディングをされている方の参考にもなれば嬉しいです。</p>
<h2 id="チームの構成">チームの構成</h2>
<p>エンジニアリングチームは正社員、個人の業務委託の方々、業務委託の外部会社の方々で構成されています。
正社員は冨田を含め3名で、開発から運用まで全体をまんべんなく担当します。
社内外からの要望を聞き、本番環境をモニタリングして改善していくことがメインミッションです。</p>
<p>個人の業務委託の方々がさらに4名おり、こちらはScalaで実装された中核システムの開発を専門的に行います。
Scala技術者ですぐに正社員採用できる方は少ないですが、業務委託であればお願いできる方がいらっしゃいます。
技術に対する高い専門性を生かし、ハイペースでアイディアを実装に変えていく役割をお願いしています。</p>
<p>外部の会社の方々は社内で手のまわらない専門知識を要する分野をお手伝いいただいています。
現時点ではスマートフォンアプリケーション開発やインフラ構築のサポート、オンコールの一次請けをお願いしています。
会社で直接採用しにくい分野、人数が必要な分野で実績のあるチームのサポートを得ることが目的です。</p>
<p>本記事でお話するエンジニアリングチームは外部会社の方を除いた、正社員と個人の業務委託の方々のチームです。</p>
<h2 id="チームコンセプト">チームコンセプト</h2>
<p>エンジニアリングチームが目指している状態は次のようなものです。</p>
<ul>
<li>責任が明確である</li>
<li>コミュニケーションが明確である</li>
<li>自由である</li>
</ul>
<p>責任が明確であるとは、個人に期待される職責が明文化されていて、理解されている状態です。
例としてオンコール制度を見てみましょう。
正社員はローテーションでオンコール担当になります。
オンコール担当中は電話を受けられるようにしておいて、インシデントが発生したらドキュメントを確認しつつ解決にあたります。</p>
<p>kippが大事にしているのはこのような責任を文書化し、全員に理解してもらうことです。
「オンコールを担当してください」とだけ言われても何をしたらいいかわかりませんね。
周囲の振舞から見よう見まねでキャッチアップすると時間がかかりますし、「あいつはやるべきことをやらない」など軋轢が生じかねません。
人によってやりかたが違って作業が矛盾し、トラブルになることもあります。
「ここに書いてあることをお願いします」と差し出せる文書を維持するよう努力しています。
べき・べからずに留まらず、具体的な手順、よくある問題なども載せ、手引きになるよう充実させています。</p>
<p>コミュニケーションが明確であるとは、相手の前提知識や暗黙の了解を当てにせず、一箇所を読めば主張が分かる状態です。
この価値が特に重要なのはタスクを依頼する時です。
概念の呼び名がまちまちだったり、タスクの説明に書いていない前提があると実装者は要求を正しく理解できません。
間違った理解にもとづいて作業した結果、実装を大幅に書き直さなければいけないという悲劇が何度もありました。
要求の文書やタスクの説明もレビューし、齟齬や言及不足、考慮不足を最も早い段階で直しています。</p>
<p>相談するときも何をしようとしていて、どのような方針を立てたかが明快だとすみやかに回答できます。
背景知識やプロダクトの状況、依頼者の思いから、適切な名詞、動詞の利用まで、内容のすべての層に注意を行きわたらせるようにしています。
さらにシステムの仕組みやプロダクトコンセプトをまとめた資料を作り、共通言語のプリセットとしています。
この資料はGoogle Driveに保存されているので、伝達内容のわからない部分はDriveで調べられます。</p>
<p>このやりかたはコミュニケーションが「硬い」ものになり、気軽な発話を阻害する面があります。
しっかり書こうと思って一人で長時間悩んでしまっては時間がもったいないですね。
そこでSlackのチャットは気軽な相談をする場として、思ったことを何でも、どこでも書けます。
社内外の最新状況をさっと書き出すヘッドライン的な場として効果を発揮しています。</p>
<p>それでもどれくらいのことまで書いていいか不安だったり、人のいるところで発言するのは恥ずかしかったりします。
そこで一部のメンバーはtimesチャンネル（個人の名前のついたチャンネル）を作っています。
timesではより発言の敷居が下るので、やってること、思ってること、気になったことが集まりやすくなります。
ちょっとしたぼやきからリファクタリングやルールの改善が生まれることもよくあります。
Slackを柔らかな空間に保つため、反対に依頼や合意はSlackで終わりにせず、文書にして残します。</p>
<p>コミュニケーションが明確であることの効果は意思伝達がしやすいに留まりません。
依頼の例では、依頼の文書が自己完結してコンパクトであることで、そこだけ読めば最近の議論、ビジネス動向などを理解していなくても作業できます。
フルタイムではない業務委託の方が週末だけ作業する場合でもタスクとコードを読めば実装を進められます。
すなわち属人化を防ぐとともに協力していただける方を増やしやすくなります。
新しく実装される部分が方針を理解して作られているのでチグハグになりにくいです。
次の実装がやりやすく、その次も、と長い期間を通して効率を維持できます。</p>
<p>最後の自由とは、責任の裏返しです。
責任とコミュニケーションが明確だとやるべきことが明確になります。
反対に、それ以外はやらなくていいとわかります。
他チームはやや状況が違いますが、エンジニアリングチームは正社員でも休みを自由に調整できます。
オンコール担当の時は連絡のつくようにするというルールがありますが、それ以外はどこにいてもかまいません。
真っ昼間からゲーセンに行くのも、唐突に箱根の温泉に浸かるのも、北海道で夏を満喫するのも自由です。
昼まで寝ている人も早朝から作業する人もいます。
土日に休みたい人も土日は仕事したい人もいます。
エンジニアリングチームはメンバーの自由意志を最大限許容することで多様な個性を認めようとしています。</p>
<p>あくまで職場での自由なので、恣意的な制限がついていることを気にとめておく必要があります。
たとえばテキストコミュニケーションをしない自由はありません。
なかにはテキストより口頭、対面の会話が得意な人もいます。
口頭のコミュニケーションを中心に構築されたチームがテキストコミュニケーションに劣らないことも知っています。
しかしチームはテキストコミュニケーションを中心に構築され、それを好むメンバーが集まっています。
全員の合意で方針を変えることはありますが、主張がかならず通るものではありません。
採用でもその時点のやり方に馴染むか確認する必要があります。</p>
<p>自由とはだらけている、いいかげんであるとは真逆の状態です。
オンコールやタスクを処理することは明確な責任です。
周囲との関係を保つなど、会社の一員として果たすべき義務もあります。
おたがいに責任を果たすという信頼があるから、それ以外の部分で一切の束縛をする必要がないのです。
自分を律する面でも社内外から信頼されつづける面でも、自由でありつづけることは非常に困難です。
週5日出勤して、その場でいわれたとおりにするほうがよっぽど楽でしょう。
後で怠けるために自動化を頑張るプログラマと、自由を得るために成果を出すチームはどこか類似を感じます。
エンジニアリングチームはこれからも自由であるために努力を続けます。</p>
<h2 id="チームの維持">チームの維持</h2>
<p>少しヒートアップしてしまいました。
地に足をつけて、以上のチームコンセプトを実現するための具体的な手段をお話します。
まずコンセプトの実装として、チーム維持のためにやっていることを説明します。
維持とは、チームをよい状態に保ち、業務しやすく改善していくことです。
それからチームメンバーを増やすための採用に触れます。</p>
<p>チームの維持に重要なのは意見を聞いて改善することです。
チームコンセプトの実現には様々なアプローチがあります。
長い時間の果てコンセプト自体がメンバーの希望と相反してくる恐れもあります。
今のやり方が適切か確認する場が必要です。</p>
<p>メンバーやビジネス状況や世相に対応するポイントとして、ふたつのミーティングを持っています。
ひとつは各チームリーダーが集まり、ビジネス状況をもとにエンジニアリングの方針とスケジュールを議論する場です。
隔週で行っています。
もうひとつはエンジニアリングチーム内での月1回の1on1です。
これらの場で細かいタスクの話から心情の話、大方針まで直接議論し、アップデートの原動力としています。</p>
<p>メンバーを不要に拘束しないよう、ミーティングは最低限にすることを心がけています。
議題がないのになんとなく集まる、大人数で集まって発言するのはごく数人というミーティングは無駄が多いです。
同期的なミーティングはその時間だけでなく前後の時間の自由も奪います。
この前提の上で最低限これらのミーティングはやるべきと考え、スタイルを見直しつつ行っています。</p>
<p>ビデオチャットでの1on1を開始したのは比較的最近で、今年の前半だったと記憶しています。
それまでは「なにかあったら言ってください」のスタイルで、意見を言う権利はありましたが機会はありませんでした。
「なにかあったら」のスタイルで意見を言える人もいますが言えない人も多いです。
わかったときには既に禍根となってしまっていたこともありました。</p>
<p>定期的な機会をつくることで相互理解が深まることを期待し設けました。
全体会議など人数の多い場にしてしまうとどうしても長くいるメンバー、今ホットな領域を担当しているメンバーに発言が集中します。
一対一の場にすることで気楽に話せると期待しています。
実際に私が見えていなかった課題を指摘してもらえています。
ひとつひとつは小さな問題でも、頻繁に同期することで大きな改善につながると信じています。</p>
<p>直接の改善からは少しずれますが、ときおり全体アイスブレイクを実施しています。
リモートワークが主体でミーティングの回数が少ないので他のメンバーと話す機会がほとんどありません。
自己紹介やレクリエーションを取り入れ、時間を一緒に過ごすことをテーマにしています。</p>
<p>チーム維持のためにもうひとつ重視しているのは文書化です。
完璧には程遠いものの、文書の品質を維持しつづける強い意志を持っています。
高速にシステムを改善しつづけるには良いコードと良い文書が両輪となります。
コミュニケーションをスムーズにするにも、責任を明確にするにも文書が基本の武器になります。</p>
<p>私自身とても忘れっぽいので、なんでも書いておかないと忘れてしまいます。
また何度も同じことを説明するのが嫌でもあります。
何度も聞かれるのはウェルカムですが、毎回説明するのが面倒です。
やったこと、考えたこと、伝えたいことはなんでもメモして、繰り返し参照されるものをたびたび更新しています。
よいドキュメントを作るぞと意気込んでとりかかると道半ばで挫折してしまいます。
とりあえずその時の用に足りるものをつくってすこしずつ手直ししていく感覚は内製ツールに似ていますね。</p>
<p>チームコンセプトのみっつめの要素、自由のための取り組みは難しいです。
これをしていれば自由になります、などという黄金律はありません。
思考停止は自分で自分を拘束している点で最大の不自由です。</p>
<p>私が気をかけているのは自分自身が満足して働ける環境であるか反省することと、メンバーにあなたは自由だと伝えることです。
人は他人の感情がわかりません。
リモートワークであればなおさらです。
自分については自分自身で反省し改善のきっかけを探せます。
しかし他のメンバーを見ていても不自由に感じているかわかりません。
それどころか、本人は仕方のないものと受容してしまっている恐れがあります。
「あなたは自由です」と伝えることで、こういう働き方はどうかな、これちょっと嫌だなと思いつくことが少しでも増えるのではないでしょうか。</p>
<p>しない自由と同じだけする自由にも注意を払っています。
例えばリモートワークの自由と同時に出社の自由があります。
オフィスはアクセスのよい大手町の<a href="https://finolab.tokyo/jp/">FINOLAB</a>に入っています。
フリーコーヒーもあり快適に作業できる環境です。
しかし現在は外出するだけでも健康リスクがある状態なので自由を制限し、出社を必要最低限にするようお願いしています。</p>
<h2 id="採用の考え方">採用の考え方</h2>
<p>会社の発展にともなってチームを拡大していくためには採用が必須です。
構成の部分で述べたとおり、正社員と業務委託はメインミッションが違います。
業務委託は補助ということは一切なく、どちらも同じウェイトで、違う価値観で採用しています。
ここでは正社員と業務委託に分けて採用のときに見ているポイントを紹介します。</p>
<p>正社員に求めるのは広範な知識と長期間にわたってシステムを改善する熱意です。
コンピュータサイエンスの基礎知識に加え、とくにkippの業務分野で重要なネットワーク、セキュリティ、RDBMS、パフォーマンス改善の知識が必要です。
じつはScalaが書けるかどうかは重視していません。
コンピュータサイエンスの知識にはプログラミング言語の仕組みや型理論、バーチャルマシンの理解も含まれます。
知識があれば文法を覚えるだけですぐにScalaを書けますから、ジョインしてから本を読めばよいです。
実際、kipp勤務以前にScalaの経験がなかったメンバーの方が多いです。</p>
<p>システムを改善する熱意はできるだけ長期間携わってほしい意図を反映したものです。
熱意というと大袈裟ですね。
なげやりにコードを積み重ねるのではなく、作業しやすい環境を維持する気持ちを持っていてほしいです。
具体的には使いづらい部分をリファクタリングしたりツールを作ることを期待しています。</p>
<p>業務委託の方々には反対に即戦力として働けることを求めます。
Scalaの業務経験がなくても、ライブラリも含めて一通り理解していて独力でコードを書ける必要があります。
タスクの説明とコードを読んで自力で業務を進めていくため、一定の問題解決能力も求められます。</p>
<p>性格や人物像の面では共通して責任感があり、実直であることを求めています。
金融システムの性質上、必ず踏まねばならない手順がいろんな面であります。
間違いや問題が発生した場合の報告手順も定められています。
手順をないがしろにしたり、間違いをもみ消すようなことがあると会社の存続に関わりうるので注意しています。
事業内容はBtoBtoCの性質が強いので、関係パートナーとの信頼を育むため確実に作業を完遂するのも重要な資質です。</p>
<h2 id="よりよいチームに向けて">よりよいチームに向けて</h2>
<p>今回はkippエンジニアリングチームをいろんな側面から紹介しました。
スタートアップ企業は事業も手探りですがチーム構築も手探りでした。
チームビルディングや運用については様々な情報が発信されていますが、5人程度の小さいチームについて述べているものは少ないようです。
人数が少ないと一人一人の個性による振れ幅が大きすぎて、なにも画一的なことが言えないからでしょう。</p>
<p>この記事の内容もそっくりそのままやって上手くいくものでは決してありません。
むしろかなり特殊な事例でしょう。
すこしでもお読みになった方の参考になる部分があれば幸いです。</p>
<p>自分自身、人と話したり仲良くなるのは苦手です。
かなり苦手です。
しかし苦手だからこそよりよくしたいと思い、なあなあにせず改善の意志を持ちつづけられていると感じます。
ここで書いたことは通過点であり、毎月毎年変わっていくでしょう。
組織が良くなっていけるもっとも幸せな契機は新しいメンバーが加わることです。
このような組織で一緒に働いてみたいと思った方はぜひ<code>people@kipp-corp.com</code>までご連絡ください。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[テストデータの準備を円滑化しよう]]></title>
            <link>/blog/soymilk</link>
            <guid isPermaLink="false">/blog/soymilk</guid>
            <pubDate>Tue, 20 Jul 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>こんにちは。CTOの冨田です。
<a href="/blog/graphql-hacks">前回のブログ記事</a>ではシステムからデータを円滑に取得するためにGraphQLを活用した取り組みを紹介しました。
今回は反対側、データを準備するプロセスについて紹介します。</p>
<h2 id="データ投入の課題">データ投入の課題</h2>
<p>今回のテーマであるテストデータとは本番同等構成でアプリケーションを検証するときに必要なものを指します。
このような環境はステージング環境、開発環境、試験環境などと呼ばれます。
当ブログでは社内の呼び方にあわせて開発環境（dev環境）とします。</p>
<p>開発環境は本番同等構成なので原則スマートフォンアプリやウェブサイトの操作をおこなってデータを準備するルールにしています。
しかし遠い過去のデータが欲しい、大量のデータが欲しい、SMS番号など外部リソースに依存する部分を迂回したいなど、ルールどおりにできないケースがあります。
このため開発環境にはデータ投入用のスクリプトを用意することが多いです。</p>
<p>データ投入用のスクリプトはSQLなどデータ永続化層の操作言語か、アプリケーションの実装言語で書くのが一般的です。
kippでも一番最初は開発者が開発環境のみMySQLに直接ログインできるよう構成し、手書きのSQLを実行していました。
この方法は属人的で誤操作のリスクが高いのみならず、行暗号化されるテーブルのデータを用意できない欠点がありました。
そこでScalaスクリプトを受け取りサーバ上で実行する仕組みを用意しました。
開発者はデータを用意したり更新するスクリプトを準備し、リモート実行コマンドを呼び出すことでサーバ上のデータを自由に改変できます。
スクリプトをテスト、レビュー、共有することで最低限のエコシステムが出来ました。
しかしスクリプトはアプリケーションコードと同じコードベースを使って実行するので、十分な知識が必要です。
スクリプトがIDEを使ってかけない点も開発体験が悪く、あまり使われませんでした。</p>
<p>あらためてすべてのチームに聞き取りを行いニーズと問題点を整理しました。</p>
<ul>
<li>データ更新の要望は少なく、ほとんどが追加である。追加だけできれば90%以上の要求に応えられる</li>
<li>パートナーによるテスト期間内にあたらしいデータが必要になることも多く、すばやく対応してほしい</li>
<li>Scalaスクリプトは書きにくくSQLで無理やり作業している。SQLで対応できないものは発生しないよう回避している。結果パートナーとの軋轢が生まれる</li>
<li>パートナーから渡される要望が曖昧で開発者には理解できない。意思疎通のための時間がかかる</li>
<li>テスト期間はこまかい単位で何度も依頼が来て開発者の集中が妨げられる</li>
</ul>
<h2 id="プロセスの制度化">プロセスの制度化</h2>
<p>以上の課題を解決するため、まず投入のプロセスを制度化しました。
ルールとマニュアルを作成すれば属人性を排除でき、自動化も容易になります。
投入で重要なのはデータフォーマットです。
自然言語でニーズを表明されると、意味を解釈しデータを作成するという二段階が余計に挟まります。
時間がかかりミスも起きやすいです。
そこで最初から作成したいデータの全体を指定し、テキストファイルで受け渡すようにしました。</p>
<p>データといっても、ほとんどただのScalaファイルです。
次のような構造体を想定します。</p>
<pre class="shiki one-dark-pro" style="background-color:#282c34;color:#abb2bf" tabindex="0"><code class="language-scala"><span class="line"><span style="color:#C678DD">type</span><span style="color:#E5C07B"> UserId</span><span style="color:#56B6C2"> =</span><span style="color:#E5C07B"> String</span></span>
<span class="line"><span style="color:#C678DD">type</span><span style="color:#E5C07B"> CardId</span><span style="color:#56B6C2"> =</span><span style="color:#E5C07B"> String</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C678DD">final</span><span style="color:#C678DD"> case</span><span style="color:#C678DD"> class</span><span style="color:#E5C07B"> User</span><span style="color:#ABB2BF">(</span></span>
<span class="line"><span style="color:#7F848E;font-style:italic">  // Userの識別子</span></span>
<span class="line"><span style="color:#E06C75;font-style:italic">  userId</span><span style="color:#ABB2BF">: </span><span style="color:#E5C07B">UserId</span><span style="color:#ABB2BF">,</span></span>
<span class="line"><span style="color:#7F848E;font-style:italic">  // 登録日時</span></span>
<span class="line"><span style="color:#E06C75;font-style:italic">  createdMs</span><span style="color:#ABB2BF">: </span><span style="color:#E5C07B">Long</span><span style="color:#ABB2BF">,</span></span>
<span class="line"><span style="color:#7F848E;font-style:italic">  // 暗号化済み個人情報</span></span>
<span class="line"><span style="color:#E06C75;font-style:italic">  encryptedPersonalData</span><span style="color:#ABB2BF">: </span><span style="color:#E5C07B">EncryptedMessage</span><span style="color:#ABB2BF">[</span><span style="color:#E5C07B">PersonalData</span><span style="color:#ABB2BF">],</span></span>
<span class="line"><span style="color:#ABB2BF">)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#7F848E;font-style:italic">// 個人情報、要行単位暗号化</span></span>
<span class="line"><span style="color:#C678DD">final</span><span style="color:#C678DD"> case</span><span style="color:#C678DD"> class</span><span style="color:#E5C07B"> PersonalData</span><span style="color:#ABB2BF">(</span></span>
<span class="line"><span style="color:#7F848E;font-style:italic">  // Userの氏名</span></span>
<span class="line"><span style="color:#E06C75;font-style:italic">  name</span><span style="color:#ABB2BF">: </span><span style="color:#E5C07B">String</span></span>
<span class="line"><span style="color:#ABB2BF">)</span></span></code></pre>
<p>このとき投入データは次のようになります。
操作したいテーブルとファイル名は対応づけ、<code>user.txt</code>とするルールです。</p>
<pre class="shiki one-dark-pro" style="background-color:#282c34;color:#abb2bf" tabindex="0"><code class="language-scala"><span class="line"><span style="color:#E5C07B">User</span><span style="color:#ABB2BF">(userId </span><span style="color:#56B6C2">=</span><span style="color:#98C379"> "XXX"</span><span style="color:#ABB2BF">, createdMs </span><span style="color:#56B6C2">=</span><span style="color:#D19A66"> 1626663292000</span><span style="color:#ABB2BF">, personalData </span><span style="color:#56B6C2">=</span><span style="color:#E5C07B"> PersonalData</span><span style="color:#ABB2BF">(name </span><span style="color:#56B6C2">=</span><span style="color:#98C379"> "Yamada"</span><span style="color:#ABB2BF">))</span></span>
<span class="line"><span style="color:#E5C07B">User</span><span style="color:#ABB2BF">(userId </span><span style="color:#56B6C2">=</span><span style="color:#98C379"> "YYY"</span><span style="color:#ABB2BF">, createdMs </span><span style="color:#56B6C2">=</span><span style="color:#D19A66"> 1626663292000</span><span style="color:#ABB2BF">, personalData </span><span style="color:#56B6C2">=</span><span style="color:#E5C07B"> PersonalData</span><span style="color:#ABB2BF">(name </span><span style="color:#56B6C2">=</span><span style="color:#98C379"> "Sato"</span><span style="color:#ABB2BF">))</span></span></code></pre>
<p>ご覧のとおりただのScalaファイルです。
EncryptedMessageなど内部的な仕組みは隠蔽し、GraphQLの構造に近づけています。
この形式ならGraphQLやサンプルファイルを見ながらデータ作成できるだろうと算段しました。
大量に連番のデータを作りたいときは表計算ソフトでデータテーブルを作成し文字列結合してテキストを準備できます。
開発者は具体的なデータで受け取るので、コピペしてすこし整形するだけでスクリプト化できます。</p>
<h2 id="soymilkの開発">Soymilkの開発</h2>
<p>フォーマットを決めたことで開発者の作業は簡単になり、自動化もできそうに思えました。
書き換えやAWSコマンドの実行を自動化し開発環境を不要にすればパートナーとコミュニケーションしている支援チームが作業できます。
この自動化をScalaで実装し、fat-jar形式で社内に配布しました。
社内で愛されるように、投入と豆乳をかけてSoymilkと名前をつけ、アイコンも作りました。</p>
<img src="/blog/soymilk/icon.png" alt="soymilk icon" width="128" height="128">
<p>Soymilkを利用すると次の1コマンドでデータフォーマットの確認、スクリプトの生成、AWS ECS上での実行が完了します。</p>
<pre class="shiki one-dark-pro" style="background-color:#282c34;color:#abb2bf" tabindex="0"><code class="language-sh"><span class="line"><span style="color:#61AFEF">java</span><span style="color:#D19A66"> -jar</span><span style="color:#98C379"> soymilk.jar</span><span style="color:#98C379"> user.txt</span></span></code></pre>
<p>Soymilkはデータを受け取りサーバに準備されたコマンドが解釈できるスクリプトに変換する部分、AWS ECS APIを呼び出す部分の2つで構成されます。
前者は<a href="https://scalameta.org/">Scalameta</a>を利用します。
上の例のように、入力ファイルは1行ずつ見れば文法レベルで正しいScala式ですが、型は合っていません。
また一行ずつはただの式で、それをまとめる構造がないのでファイル全体が正しいScalaファイルではありません。
便宜的に<code>object</code>で包んだものを構文解析（<code>parse</code>）し、型を利用せず名前にもとづいたパターンマッチで整形します。</p>
<pre class="shiki one-dark-pro" style="background-color:#282c34;color:#abb2bf" tabindex="0"><code class="language-scala"><span class="line"><span style="color:#C678DD">s</span><span style="color:#98C379">"object X {</span><span style="color:#C678DD">$</span><span style="color:#ABB2BF">lines</span><span style="color:#98C379">}"</span><span style="color:#ABB2BF">.parse[</span><span style="color:#E5C07B">Source</span><span style="color:#ABB2BF">] </span><span style="color:#C678DD">match</span><span style="color:#ABB2BF"> {</span></span>
<span class="line"><span style="color:#C678DD">  case</span><span style="color:#E06C75;font-style:italic"> src</span><span style="color:#ABB2BF">: </span><span style="color:#E5C07B">Parsed</span><span style="color:#ABB2BF">.</span><span style="color:#E5C07B">Success</span><span style="color:#ABB2BF">[</span><span style="color:#E5C07B">Source</span><span style="color:#ABB2BF">] =></span></span>
<span class="line"><span style="color:#ABB2BF">    src</span></span>
<span class="line"><span style="color:#ABB2BF">      .get</span></span>
<span class="line"><span style="color:#ABB2BF">      .collect { </span><span style="color:#C678DD">case</span><span style="color:#C678DD"> q</span><span style="color:#98C379">"object X { ..</span><span style="color:#C678DD">$</span><span style="color:#ABB2BF">stats</span><span style="color:#98C379"> }"</span><span style="color:#ABB2BF"> =></span></span>
<span class="line"><span style="color:#C678DD">        val</span><span style="color:#E06C75"> inserts</span><span style="color:#56B6C2"> =</span><span style="color:#ABB2BF"> stats.zipWithIndex.collect { </span><span style="color:#C678DD">case</span><span style="color:#ABB2BF"> (</span><span style="color:#E06C75;font-style:italic">s</span><span style="color:#ABB2BF">: </span><span style="color:#E5C07B">Term</span><span style="color:#ABB2BF">, i) =></span></span>
<span class="line"><span style="color:#C678DD">          val</span><span style="color:#E06C75"> msg</span><span style="color:#56B6C2"> =</span><span style="color:#C678DD"> s</span><span style="color:#98C379">"Insert </span><span style="color:#C678DD">$</span><span style="color:#ABB2BF">fileName</span><span style="color:#C678DD"> $</span><span style="color:#ABB2BF">i</span><span style="color:#98C379">-th record"</span></span>
<span class="line"><span style="color:#C678DD">          val</span><span style="color:#E06C75"> adjusted</span><span style="color:#56B6C2"> =</span><span style="color:#ABB2BF"> wrapEncryptedFields(s)</span></span>
<span class="line"><span style="color:#E5C07B">          Seq</span><span style="color:#ABB2BF">(</span></span>
<span class="line"><span style="color:#C678DD">            q</span><span style="color:#98C379">"println(</span><span style="color:#C678DD">$</span><span style="color:#ABB2BF">msg</span><span style="color:#98C379">)"</span><span style="color:#ABB2BF">,</span></span>
<span class="line"><span style="color:#C678DD">            q</span><span style="color:#98C379">"Await.result(db.run(tables.</span><span style="color:#C678DD">${</span><span style="color:#E5C07B">Term</span><span style="color:#ABB2BF">.</span><span style="color:#E5C07B">Name</span><span style="color:#ABB2BF">(fileName)</span><span style="color:#C678DD">}</span><span style="color:#98C379"> += </span><span style="color:#C678DD">$</span><span style="color:#ABB2BF">adjusted</span><span style="color:#98C379">), Duration.Inf)"</span><span style="color:#ABB2BF">,</span></span>
<span class="line"><span style="color:#ABB2BF">          )</span></span>
<span class="line"><span style="color:#ABB2BF">        }</span></span>
<span class="line"><span style="color:#ABB2BF">        inserts.flatten</span></span>
<span class="line"><span style="color:#ABB2BF">      }</span></span>
<span class="line"><span style="color:#ABB2BF">      .flatten</span></span>
<span class="line"><span style="color:#C678DD">  case</span><span style="color:#E06C75;font-style:italic"> err</span><span style="color:#ABB2BF">: </span><span style="color:#E5C07B">Parsed</span><span style="color:#ABB2BF">.</span><span style="color:#E5C07B">Error</span><span style="color:#ABB2BF"> =></span></span>
<span class="line"><span style="color:#ABB2BF">    error(</span><span style="color:#C678DD">s</span><span style="color:#98C379">"Cannot parse </span><span style="color:#C678DD">$</span><span style="color:#ABB2BF">path</span><span style="color:#98C379">, Check file format is correct. </span><span style="color:#C678DD">$</span><span style="color:#ABB2BF">err</span><span style="color:#98C379">"</span><span style="color:#ABB2BF">)</span></span>
<span class="line"><span style="color:#ABB2BF">}</span></span></code></pre>
<p>ECS APIの呼び出しは<a href="https://aws.amazon.com/jp/sdk-for-java/">AWS SDK for Java</a>を用います。
ECS Taskを実行するには様々なパラメータを指定しますが、すでに動いているタスクから取得するなどして隠蔽しています。
aws-cliのインストールなどを排し、Java実行環境だけあれば作業できるようにしました。
AWS credentialsのセットアップやコマンドの利用方法は社内ドキュメントに事例つきでまとめています。</p>
<h2 id="効果">効果</h2>
<p>フォーマットの策定は効果的で、当初の課題はすべて解決できました。
開発者は一切作業しなくてよくなり、新規開発に集中できます。
要求へのレスポンスが大幅に改善し、投入できるかどうかの不自然な制約がなくなり、パートナーとのやりとりが円滑になりました。
たしかに投入したいデータを作成する人の負担は増えていますが、データの決定はテスト設計でなされるので元来必要なものを整形するのみです。
頻繁に利用されるテーブルにはSpreadsheetのテンプレートを用意し穴埋めで投入データが作れる仕組みも用意しました。
ミスや不整合があるときもこの行のこの部分と具体的に会話できます。</p>
<p>より高い視点では技術の民主化を推し進められた成果がありました。
データを書き下すのは煩雑にも見えますが、支援チームのデータ構造への理解が深まる効果をもたらしました。
だれでも、いつでも作業できるようにドキュメントを整備する意識も向上しました。
これらの効果はデータ投入だけでなく他の業務も効率化し、短時間で楽しく作業できるようになりました。</p>
<p>今後改善すべき点はテーブルをまたいだ整合性の問題です。
たとえば取引を通知するには取引テーブル、通知全体のテーブル、通知と通知先（メール、プッシュなど）を関連づけるテーブルの3つにデータが必要です。
サービスを企画する視点では取引テーブルに注意が集中し、他のテーブルを忘れがちです。
一方で大量に取引を追加するためにいちいち通知を送信したくない場合もあります。
さらにこのような複数のテーブルで一連の事象を表すケースは多数あり、ひとつずつルール化するには相当な手間がかかります。
現在はファイルを受け取った時に意思確認をしていますが、ゆくゆくは徐々にプログラムでカバーしたいです。</p>
<p>Kippでは他にも業務効率化、コミュニケーション効率化に取り組んでいます。
ルールの策定とScalaやRustを利用した自動化で楽しく円滑に働ける環境を目指しています。
そんな環境で働いてみたい、環境をつくってみたいという開発者の方はお気軽に<code>people@kipp-corp.com</code>までご連絡ください。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[ScalaのマクロをつかったGraphQLサーバの構築]]></title>
            <link>/blog/graphql-hacks</link>
            <guid isPermaLink="false">/blog/graphql-hacks</guid>
            <pubDate>Fri, 09 Jul 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>はじめまして。冨田です。
Kipp Financial TechnologiesのCo-Founderであり、CTOに就いています。
大学卒業後、ウェブペイ株式会社に勤務し、その後買収されLINE株式会社に勤めました。
この2社でクレジットカードやプリペイドカードを扱うアプリケーションを開発しました。
業務外の時間を利用して様々なプロジェクトにも関係させてもらい、サーバサイドを中心にいろんな技術スタックを経験しました。
現在はこれまでの知見を生かして、仕様策定から実装、インフラ、運用までKippの技術全体を統括しています。</p>
<p>スタートアップ企業は限られたリソースで高度なアプリケーションの開発を求められるのが常です。
Kippは社全体で少人数精鋭チームを目指しています。
開発チームでは生産性が高い技術を採用し、社内でより発展させてこの目標にアプローチしています。
今回のブログ記事ではその実例としてGraphQLサーバの構築事例を紹介します。</p>
<h2 id="graphqlの立ち位置">GraphQLの立ち位置</h2>
<p><a href="/blog/hello-world">前回の記事</a>でも紹介しましたが、GraphQLは管理用のデータベースアクセスインタフェースを担います。
社内で利用する管理用なので細かなアクセス権限管理が不要、ほぼすべてのデータを参照できる必要があるという特徴があります。
現時点でAurora MySQLには300程度のテーブルがあります。
GraphQLを通じてこの8割程度を参照できます。</p>
<p>200を超えるテーブルへのアクセスインタフェースを提供するのは気が遠くなる作業です。
<a href="https://www.prisma.io/">Prisma</a>などRDBのスキーマから自動的にGraphQLサーバを運用するような仕組みも検討しました。
しかし複合主キーを持つテーブルがある、独自の行暗号化を行うテーブルがあるなどの理由で利用できませんでした。</p>
<p>そこでScala上でGraphQLサーバを構築することを考えました。
サーバ全体の実装にScalaを使っているため、DBアクセス、暗号レコード処理などで既存コードを再利用できる利点があります。
他方ScalaにはPrismaのようなワンストップフレームワークはありませんでした。
検索したなかでモデルが理解しやすかった<a href="https://github.com/sangria-graphql/sangria">Sangria</a>を使って独自に構築することになりました。</p>
<h2 id="graphql-schemaの要素と組み立て方">GraphQL Schemaの要素と組み立て方</h2>
<p>実際にkippのモデルのサブセットを使い、SangriaでGraphQLサーバを提供する方法を確認しましょう。
まず今後のコード例で利用するモデルを定義します。
実際にkippで使っているコードの一部を抽出して次のようなモデルを考えましょう。</p>
<pre class="shiki one-dark-pro" style="background-color:#282c34;color:#abb2bf" tabindex="0"><code class="language-scala"><span class="line"><span style="color:#C678DD">type</span><span style="color:#E5C07B"> UserId</span><span style="color:#56B6C2"> =</span><span style="color:#E5C07B"> String</span></span>
<span class="line"><span style="color:#C678DD">type</span><span style="color:#E5C07B"> CardId</span><span style="color:#56B6C2"> =</span><span style="color:#E5C07B"> String</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C678DD">final</span><span style="color:#C678DD"> case</span><span style="color:#C678DD"> class</span><span style="color:#E5C07B"> User</span><span style="color:#ABB2BF">(</span></span>
<span class="line"><span style="color:#7F848E;font-style:italic">  // Userの識別子</span></span>
<span class="line"><span style="color:#E06C75;font-style:italic">  userId</span><span style="color:#ABB2BF">: </span><span style="color:#E5C07B">UserId</span><span style="color:#ABB2BF">,</span></span>
<span class="line"><span style="color:#7F848E;font-style:italic">  // 登録日時</span></span>
<span class="line"><span style="color:#E06C75;font-style:italic">  createdMs</span><span style="color:#ABB2BF">: </span><span style="color:#E5C07B">Long</span><span style="color:#ABB2BF">,</span></span>
<span class="line"><span style="color:#7F848E;font-style:italic">  // 暗号化済み個人情報</span></span>
<span class="line"><span style="color:#E06C75;font-style:italic">  encryptedPersonalData</span><span style="color:#ABB2BF">: </span><span style="color:#E5C07B">EncryptedMessage</span><span style="color:#ABB2BF">[</span><span style="color:#E5C07B">PersonalData</span><span style="color:#ABB2BF">],</span></span>
<span class="line"><span style="color:#ABB2BF">)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#7F848E;font-style:italic">// 個人情報、要行単位暗号化</span></span>
<span class="line"><span style="color:#C678DD">final</span><span style="color:#C678DD"> case</span><span style="color:#C678DD"> class</span><span style="color:#E5C07B"> PersonalData</span><span style="color:#ABB2BF">(</span></span>
<span class="line"><span style="color:#7F848E;font-style:italic">  // Userの氏名</span></span>
<span class="line"><span style="color:#E06C75;font-style:italic">  name</span><span style="color:#ABB2BF">: </span><span style="color:#E5C07B">String</span></span>
<span class="line"><span style="color:#ABB2BF">)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C678DD">final</span><span style="color:#C678DD"> case</span><span style="color:#C678DD"> class</span><span style="color:#E5C07B"> PrepaidCard</span><span style="color:#ABB2BF">(</span></span>
<span class="line"><span style="color:#7F848E;font-style:italic">  // PrepaidCardの識別子、Primary key</span></span>
<span class="line"><span style="color:#E06C75;font-style:italic">  cardId</span><span style="color:#ABB2BF">: </span><span style="color:#E5C07B">CardId</span><span style="color:#ABB2BF">,</span></span>
<span class="line"><span style="color:#7F848E;font-style:italic">  // PrepaidCardが属するUserのID</span></span>
<span class="line"><span style="color:#E06C75;font-style:italic">  userId</span><span style="color:#ABB2BF">: </span><span style="color:#E5C07B">String</span><span style="color:#ABB2BF">,</span></span>
<span class="line"><span style="color:#7F848E;font-style:italic">  // 作成日時</span></span>
<span class="line"><span style="color:#E06C75;font-style:italic">  createdMs</span><span style="color:#ABB2BF">: </span><span style="color:#E5C07B">Long</span><span style="color:#ABB2BF">,</span></span>
<span class="line"><span style="color:#7F848E;font-style:italic">  // PrepaidCardに割り当てらたPANの下4桁</span></span>
<span class="line"><span style="color:#E06C75;font-style:italic">  last4</span><span style="color:#ABB2BF">: </span><span style="color:#E5C07B">String</span><span style="color:#ABB2BF">,</span></span>
<span class="line"><span style="color:#ABB2BF">)</span></span></code></pre>
<p>Userは複数枚のPrepaidCardを持てます。
kippのサービスで同時に有効なカードは1ユーザ1枚ですが、再発行やカード種類の変更の際には複数枚が関連づきます。
これらのモデルを取得するクエリは次のようになるでしょう。</p>
<pre class="shiki one-dark-pro" style="background-color:#282c34;color:#abb2bf" tabindex="0"><code class="language-graphql"><span class="line"><span style="color:#ABB2BF">{</span></span>
<span class="line"><span style="color:#E06C75">  users</span><span style="color:#ABB2BF"> {</span></span>
<span class="line"><span style="color:#E06C75">    userId</span></span>
<span class="line"><span style="color:#E06C75">    createdMs</span></span>
<span class="line"><span style="color:#E06C75">    personalData</span><span style="color:#ABB2BF"> {</span></span>
<span class="line"><span style="color:#E06C75">      name</span></span>
<span class="line"><span style="color:#ABB2BF">    }</span></span>
<span class="line"><span style="color:#E06C75">    prepaidCards</span><span style="color:#ABB2BF"> {</span></span>
<span class="line"><span style="color:#E06C75">      cardId</span></span>
<span class="line"><span style="color:#E06C75">      createdMs</span></span>
<span class="line"><span style="color:#E06C75">      last4</span></span>
<span class="line"><span style="color:#ABB2BF">    }</span></span>
<span class="line"><span style="color:#ABB2BF">  }</span></span>
<span class="line"><span style="color:#7F848E;font-style:italic">  # PrepaidCardからUserを引くパターン</span></span>
<span class="line"><span style="color:#E06C75">  prepaidCards</span><span style="color:#ABB2BF"> {</span></span>
<span class="line"><span style="color:#E06C75">    cardId</span></span>
<span class="line"><span style="color:#E06C75">    last4</span></span>
<span class="line"><span style="color:#E06C75">    user</span><span style="color:#ABB2BF"> {</span></span>
<span class="line"><span style="color:#E06C75">      personalData</span><span style="color:#ABB2BF"> {</span></span>
<span class="line"><span style="color:#E06C75">        name</span></span>
<span class="line"><span style="color:#ABB2BF">      }</span></span>
<span class="line"><span style="color:#ABB2BF">    }</span></span>
<span class="line"><span style="color:#ABB2BF">  }</span></span>
<span class="line"><span style="color:#ABB2BF">}</span></span></code></pre>
<p>Sangriaでこのクエリを処理するために必要な部品を解説します。
まず<code>users</code>と<code>prepaidCards</code>のフィールド定義が必要です。
フィールド定義は次のようになります。</p>
<pre class="shiki one-dark-pro" style="background-color:#282c34;color:#abb2bf" tabindex="0"><code class="language-scala"><span class="line"><span style="color:#E5C07B">Field</span><span style="color:#ABB2BF">(</span><span style="color:#98C379">"userType"</span><span style="color:#ABB2BF">,</span></span>
<span class="line"><span style="color:#E5C07B">  UserGraphQLType</span><span style="color:#ABB2BF">,</span></span>
<span class="line"><span style="color:#ABB2BF">  resolve </span><span style="color:#56B6C2">=</span><span style="color:#ABB2BF"> ctx => ctx.ctx.db.run(ctx.ctx.tables.users.result))</span></span></code></pre>
<p><code>resolve</code>はcontextを用いてデータを取得する方法を定義します。
この処理はDBを読み出していると受け取ってください。
<code>UserGraphQLType</code>は上で定義した<code>case class User</code>をSangriaの<code>ObjectType</code>に写像したものです。
Sangriaにあらかじめ実装されたマクロをつかって次のように定義できます。</p>
<pre class="shiki one-dark-pro" style="background-color:#282c34;color:#abb2bf" tabindex="0"><code class="language-scala"><span class="line"><span style="color:#C678DD">implicit</span><span style="color:#C678DD"> lazy</span><span style="color:#C678DD"> val</span><span style="color:#E06C75"> UserGraphQLType</span><span style="color:#ABB2BF">: </span><span style="color:#E5C07B">ObjectType</span><span style="color:#ABB2BF">[</span><span style="color:#E5C07B">Ctx</span><span style="color:#ABB2BF">, </span><span style="color:#E5C07B">User</span><span style="color:#ABB2BF">] </span><span style="color:#56B6C2">=</span></span>
<span class="line"><span style="color:#ABB2BF">  macros.derive.deriveObjectType</span></span></code></pre>
<p><code>User</code>の定義を再確認すると<code>encryptedPersonalData</code>というフィールドを持っています。
しかし取得したいのは復号済みの<code>personalData</code>です。
<code>deriveObjectType</code>を修飾しマクロにフィールドを置き換えるよう指示します。</p>
<pre class="shiki one-dark-pro" style="background-color:#282c34;color:#abb2bf" tabindex="0"><code class="language-scala"><span class="line"><span style="color:#C678DD">implicit</span><span style="color:#C678DD"> lazy</span><span style="color:#C678DD"> val</span><span style="color:#E06C75"> UserGraphQLType</span><span style="color:#ABB2BF">: </span><span style="color:#E5C07B">ObjectType</span><span style="color:#ABB2BF">[</span><span style="color:#E5C07B">Ctx</span><span style="color:#ABB2BF">, </span><span style="color:#E5C07B">User</span><span style="color:#ABB2BF">] </span><span style="color:#56B6C2">=</span></span>
<span class="line"><span style="color:#ABB2BF">  macros.derive.deriveObjectType(</span></span>
<span class="line"><span style="color:#E5C07B">    ReplaceField</span><span style="color:#ABB2BF">(</span><span style="color:#98C379">"encryptedPersonalData"</span><span style="color:#ABB2BF">,</span></span>
<span class="line"><span style="color:#E5C07B">      Field</span><span style="color:#ABB2BF">(</span></span>
<span class="line"><span style="color:#ABB2BF">        name </span><span style="color:#56B6C2">=</span><span style="color:#98C379"> "personalData"</span><span style="color:#ABB2BF">,</span></span>
<span class="line"><span style="color:#ABB2BF">        fieldType </span><span style="color:#56B6C2">=</span><span style="color:#E5C07B"> OptionType</span><span style="color:#ABB2BF">(implicitly[</span><span style="color:#E5C07B">OutputType</span><span style="color:#ABB2BF">[</span><span style="color:#E5C07B">PersonalData</span><span style="color:#ABB2BF">]]),</span></span>
<span class="line"><span style="color:#ABB2BF">        resolve </span><span style="color:#56B6C2">=</span><span style="color:#ABB2BF"> ((</span><span style="color:#E06C75;font-style:italic">ctx</span><span style="color:#ABB2BF">: </span><span style="color:#E5C07B">Context</span><span style="color:#ABB2BF">[</span><span style="color:#E5C07B">Ctx</span><span style="color:#ABB2BF">, </span><span style="color:#E5C07B">User</span><span style="color:#ABB2BF">]) =></span></span>
<span class="line"><span style="color:#ABB2BF">          ctx.value.decryptPersonalData(ctx.ctx.crypto)</span></span>
<span class="line"><span style="color:#ABB2BF">        )))</span></span></code></pre>
<p>このコードを動かすためにさらに2つ修正が必要です。
1つはPersonalDataのOutputTypeの宣言、もう1つはPersonalDataの復号の実装です。
Userと同様にPersonalDataにも<code>deriveObjectType</code>を利用します。
<code>ObjectType is-a OutputType</code>の関係があります。</p>
<pre class="shiki one-dark-pro" style="background-color:#282c34;color:#abb2bf" tabindex="0"><code class="language-scala"><span class="line"><span style="color:#C678DD">implicit</span><span style="color:#C678DD"> lazy</span><span style="color:#C678DD"> val</span><span style="color:#E06C75"> PersonalDataGraphQLType</span><span style="color:#ABB2BF">: </span><span style="color:#E5C07B">ObjectType</span><span style="color:#ABB2BF">[</span><span style="color:#E5C07B">Ctx</span><span style="color:#ABB2BF">, </span><span style="color:#E5C07B">PersonalData</span><span style="color:#ABB2BF">] </span><span style="color:#56B6C2">=</span></span>
<span class="line"><span style="color:#ABB2BF">  macros.derive.deriveObjectType</span></span></code></pre>
<p>復号のため<code>User</code>に<code>decryptPersonalData</code>を追加します。
<code>Crypto</code>は行レベル暗号を処理するモジュールです。
鍵などを持つでしょう。
<code>decrypt</code>は<code>EncryptedMessage[T] => Option[T]</code>の型を持ちます。</p>
<pre class="shiki one-dark-pro" style="background-color:#282c34;color:#abb2bf" tabindex="0"><code class="language-scala"><span class="line"><span style="color:#C678DD">final</span><span style="color:#C678DD"> case</span><span style="color:#C678DD"> class</span><span style="color:#E5C07B"> User</span><span style="color:#ABB2BF">(...) {</span></span>
<span class="line"><span style="color:#C678DD">  def</span><span style="color:#61AFEF"> decryptPersonalData</span><span style="color:#ABB2BF">(</span><span style="color:#E06C75;font-style:italic">crypto</span><span style="color:#ABB2BF">: </span><span style="color:#E5C07B">Crypto</span><span style="color:#ABB2BF">): </span><span style="color:#E5C07B">Option</span><span style="color:#ABB2BF">[</span><span style="color:#E5C07B">PersonalData</span><span style="color:#ABB2BF">] </span><span style="color:#56B6C2">=</span></span>
<span class="line"><span style="color:#ABB2BF">    crypto.decrypt(encryptedPersonalData)</span></span>
<span class="line"><span style="color:#ABB2BF">}</span></span></code></pre>
<p>また<code>UserGraphQLType</code>の定義のなかの<code>ctx.ctx.crypto</code>は<code>Ctx</code>型の<code>crypto: Crypto</code>フィールドを呼び出す処理です。
<code>Ctx</code>型はSchemaで出てくる<code>resolve</code>を実装するために必要なオブジェクトを持つ構造です。
<code>crypto</code>のほかにデータベースへのアクセサなどを持つでしょう。
ここまでで以下のクエリが実行できるようになります。</p>
<pre class="shiki one-dark-pro" style="background-color:#282c34;color:#abb2bf" tabindex="0"><code class="language-graphql"><span class="line"><span style="color:#ABB2BF">{</span></span>
<span class="line"><span style="color:#E06C75">  users</span><span style="color:#ABB2BF"> {</span></span>
<span class="line"><span style="color:#E06C75">    userId</span></span>
<span class="line"><span style="color:#E06C75">    createdMs</span></span>
<span class="line"><span style="color:#E06C75">    personalData</span><span style="color:#ABB2BF"> {</span></span>
<span class="line"><span style="color:#E06C75">      name</span></span>
<span class="line"><span style="color:#ABB2BF">    }</span></span>
<span class="line"><span style="color:#ABB2BF">  }</span></span>
<span class="line"><span style="color:#ABB2BF">}</span></span></code></pre>
<p>次に<code>users</code>のなかでUserに関連づくPrepaidCardを取得できるようにしましょう。
Sangriaは<code>Fetcher</code>という仕組みを提供しています。</p>
<pre class="shiki one-dark-pro" style="background-color:#282c34;color:#abb2bf" tabindex="0"><code class="language-scala"><span class="line"><span style="color:#7F848E;font-style:italic">// オブジェクトとID (Primary key)の関係を定義</span></span>
<span class="line"><span style="color:#C678DD">val</span><span style="color:#E06C75"> relUserUserId</span><span style="color:#56B6C2"> =</span></span>
<span class="line"><span style="color:#E5C07B">  Relation</span><span style="color:#ABB2BF">[</span><span style="color:#E5C07B">User</span><span style="color:#ABB2BF">, </span><span style="color:#E5C07B">UserId</span><span style="color:#ABB2BF">](</span><span style="color:#98C379">"User-userId"</span><span style="color:#ABB2BF">, ((</span><span style="color:#E06C75;font-style:italic">x</span><span style="color:#ABB2BF">: </span><span style="color:#E5C07B">User</span><span style="color:#ABB2BF">) => </span><span style="color:#E5C07B">Seq</span><span style="color:#ABB2BF">(x.userId)))</span></span>
<span class="line"><span style="color:#C678DD">val</span><span style="color:#E06C75"> relPrepaidCardUserId</span><span style="color:#56B6C2"> =</span></span>
<span class="line"><span style="color:#E5C07B">  Relation</span><span style="color:#ABB2BF">[</span><span style="color:#E5C07B">PrepaidCard</span><span style="color:#ABB2BF">, </span><span style="color:#E5C07B">UserId</span><span style="color:#ABB2BF">](</span></span>
<span class="line"><span style="color:#98C379">    "PrepaidCard-userId"</span><span style="color:#ABB2BF">,</span></span>
<span class="line"><span style="color:#ABB2BF">    ((</span><span style="color:#E06C75;font-style:italic">x</span><span style="color:#ABB2BF">: </span><span style="color:#E5C07B">PrepaidCard</span><span style="color:#ABB2BF">) => (x.userId)),</span></span>
<span class="line"><span style="color:#ABB2BF">  )</span></span>
<span class="line"><span style="color:#C678DD">implicit</span><span style="color:#C678DD"> val</span><span style="color:#E06C75"> hasIdPrepaidCard</span><span style="color:#ABB2BF">: </span><span style="color:#E5C07B">HasId</span><span style="color:#ABB2BF">[</span><span style="color:#E5C07B">PrepaidCard</span><span style="color:#ABB2BF">, </span><span style="color:#E5C07B">CardId</span><span style="color:#ABB2BF">] </span><span style="color:#56B6C2">=</span></span>
<span class="line"><span style="color:#E5C07B">  HasId</span><span style="color:#ABB2BF">[</span><span style="color:#E5C07B">PrepaidCard</span><span style="color:#ABB2BF">, </span><span style="color:#E5C07B">CardId</span><span style="color:#ABB2BF">]((</span><span style="color:#E06C75;font-style:italic">x</span><span style="color:#ABB2BF">: </span><span style="color:#E5C07B">PrepaidCard</span><span style="color:#ABB2BF">) => x.cardId)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#7F848E;font-style:italic">// CardIdからPrepaidCardを取得するDB呼び出しを定義</span></span>
<span class="line"><span style="color:#C678DD">val</span><span style="color:#E06C75"> fetchPrepaidCard</span><span style="color:#56B6C2"> =</span><span style="color:#ABB2BF"> ((</span><span style="color:#E06C75;font-style:italic">ctx</span><span style="color:#ABB2BF">: </span><span style="color:#E5C07B">Ctx</span><span style="color:#ABB2BF">, </span><span style="color:#E06C75;font-style:italic">ids</span><span style="color:#ABB2BF">: </span><span style="color:#E5C07B">Seq</span><span style="color:#ABB2BF">[</span><span style="color:#E5C07B">CardId</span><span style="color:#ABB2BF">]) =></span></span>
<span class="line"><span style="color:#ABB2BF">  ctx.db.run(</span></span>
<span class="line"><span style="color:#ABB2BF">    ctx.tables.prepaidCard.query.filter(x => x.cardId.inSet(ids)).result</span></span>
<span class="line"><span style="color:#ABB2BF">  )</span></span>
<span class="line"><span style="color:#ABB2BF">)</span></span>
<span class="line"><span style="color:#7F848E;font-style:italic">// UserIdからPrepaidCardを取得するDB呼び出しを定義</span></span>
<span class="line"><span style="color:#C678DD">val</span><span style="color:#E06C75"> fetcherPrepaidCardUserId</span><span style="color:#56B6C2"> =</span><span style="color:#E5C07B"> Fetcher</span><span style="color:#ABB2BF">.rel(</span></span>
<span class="line"><span style="color:#ABB2BF">  fetchPrepaidCard,</span></span>
<span class="line"><span style="color:#ABB2BF">  {(</span><span style="color:#E06C75;font-style:italic">ctx</span><span style="color:#ABB2BF">: </span><span style="color:#E5C07B">Ctx</span><span style="color:#ABB2BF">, </span><span style="color:#E06C75;font-style:italic">arg</span><span style="color:#ABB2BF">: </span><span style="color:#E5C07B">RelationIds</span><span style="color:#ABB2BF">[</span><span style="color:#E5C07B">PrepaidCard</span><span style="color:#ABB2BF">]) =></span></span>
<span class="line"><span style="color:#C678DD">    val</span><span style="color:#E06C75"> ids</span><span style="color:#56B6C2"> =</span><span style="color:#ABB2BF"> arg(relPrepaidCardUserId)</span></span>
<span class="line"><span style="color:#ABB2BF">    ctx.db.run(</span></span>
<span class="line"><span style="color:#ABB2BF">      ctx.tables.prepaidCard.query.filter(x => x.userId.inSet(ids)).result</span></span>
<span class="line"><span style="color:#ABB2BF">    )</span></span>
<span class="line"><span style="color:#ABB2BF">  }</span></span>
<span class="line"><span style="color:#ABB2BF">)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#7F848E;font-style:italic">// User typeにprepaidCardsフィールドを追加</span></span>
<span class="line"><span style="color:#C678DD">implicit</span><span style="color:#C678DD"> lazy</span><span style="color:#C678DD"> val</span><span style="color:#E06C75"> UserGraphQLType</span><span style="color:#ABB2BF">: </span><span style="color:#E5C07B">ObjectType</span><span style="color:#ABB2BF">[</span><span style="color:#E5C07B">Ctx</span><span style="color:#ABB2BF">, </span><span style="color:#E5C07B">User</span><span style="color:#ABB2BF">] </span><span style="color:#56B6C2">=</span></span>
<span class="line"><span style="color:#ABB2BF">  macros.derive.deriveObjectType(</span></span>
<span class="line"><span style="color:#7F848E;font-style:italic">    // 中略</span></span>
<span class="line"><span style="color:#E5C07B">    AddFields</span><span style="color:#ABB2BF">(</span><span style="color:#E5C07B">Field</span><span style="color:#ABB2BF">(</span></span>
<span class="line"><span style="color:#98C379">      "prepaidCards"</span><span style="color:#ABB2BF">,</span></span>
<span class="line"><span style="color:#E5C07B">      ListType</span><span style="color:#ABB2BF">(</span><span style="color:#E5C07B">PrepaidCardGraphQLType</span><span style="color:#ABB2BF">),</span></span>
<span class="line"><span style="color:#ABB2BF">      resolve </span><span style="color:#56B6C2">=</span><span style="color:#ABB2BF"> ((ctx) =></span></span>
<span class="line"><span style="color:#ABB2BF">        fetcherPrepaidCardUserId.deferRelSeq(relPrepaidCardUserId, ctx.value.userId)</span></span>
<span class="line"><span style="color:#ABB2BF">      )</span></span>
<span class="line"><span style="color:#ABB2BF">    ))</span></span>
<span class="line"><span style="color:#ABB2BF">  )</span></span></code></pre>
<p><code>PrepaidCardGraphQLType</code>は<code>UserGraphQLType</code>とおなじように<code>deriveObjectType</code>で定義します。
<code>fetcherPrepaidCardUserId.deferRelSeq</code>の部分「PrepaidCardにとってuserIdは関連先」であり「1：Nで関連づく」ことを定義しています。
関連先とはforeign key側のようなニュアンスです。
逆にUserにとってuserIdはprimary keyなので、<code>defer</code>を使います。
また1:0か1:1であれば<code>deferRelOpt</code>を使います。
このように宣言しわけることでGraphQLのスキーマ型をより正確なものにでき、利用者によく意図が伝わります。</p>
<p>このように一部のモデルで試したところ、Sangriaはやりたい事に対して比較的記述量の多い印象でした。
Sangriaで欲しい機能が実現できると確認しましたが、200超のテーブルの定義を書き下すのは非現実的ともわかりました。
新しいサービス展開に伴ってテーブルが更新、追加されたときGraphQLのメンテナンスコストが開発者に重くのしかかると予想されました。</p>
<h2 id="自動化最適化">自動化=最適化</h2>
<p>Sangriaがうまく活用できるだろうと見通しがたったところでGraphQLサーバ部分を自動的に構築する方法を検討しました。
私達は自動化=最適化というポリシーを持っています。
自動化は言語やライブラリを用途にカスタマイズし、作業内容を最適化する行為という意味です。
「早すぎる最適化は悪」と言われるように、早すぎる自動化も悪です。
上の例のようにSangriaを直接使ってみて、簡単なケースで期待どおりに動くことを確認しました。
この時作成したコードは自動コード生成を実装するときのサンプルにもなります。</p>
<p>自動化=最適化の観点から、自動化した部分のカプセル化を目指しました。
実行コードのパフォーマンス最適化の場合、ボトルネック箇所を特定し、ブラックボックスとして扱えるよう切り離して最適化すべきです。
自動コード生成においては手書きコードが生成されたコードの中身を気にするべきではありません。
簡単なインタフェースを用意し、グルーコードは一箇所に集中させて生成の知識を持たずに扱えるのが理想です。</p>
<p>Sangriaの仕組みはこの目標に適します。
スキーマ宣言は<code>Schema[Ctx, Unit]</code>という型で簡潔に表現されます。
手書きコードは<code>Schema</code>型のみを意識すればよく、自動生成コードは<code>Schema</code>型を返せば中身はなんでもよいです。
まず自動生成の入力の与え方を検討し、次に<code>Schema</code>の自動生成方法を検討することにしました。</p>
<h2 id="最小レベルの表現">最小レベルの表現</h2>
<p>SangriaはオブジェクトやIDの関係をすべて明示する必要があります。
明示する必要があるのはやむ無しとしても、繰り返しを排し、記述を簡略化したいと感じます。
そのため最低限必要な要素を整理しました。</p>
<ol>
<li>GraphQLで参照したいテーブル</li>
<li>テーブルの主キー</li>
<li>テーブル同士の関連</li>
<li>復号して返すべき項目</li>
</ol>
<p>「1.GraphQLで参照したいテーブル」は明示的に宣言する必要があります。
一部のトークンなどを扱うテーブルは管理者であっても見えてはならないからです。</p>
<p>「2.テーブルの主キー」はテーブルの宣言を解析して取得できますが、明示的に宣言することにしました。
テーブル定義に使っているSlickライブラリがテーブル定義のメソッドのいずれかに主キーが宣言されているという解析しづらい構造だったためです。
「1.参照したいテーブル」を宣言する際にそのテーブルの主キーをあわせて与えればよいので、明示しても煩雑にならないと考えました。</p>
<p>以上の2つは次のように宣言します。
なお<code>tables</code>はすべてのテーブル定義（Slickの<code>TableQuery</code>）を束ねた構造体です。</p>
<pre class="shiki one-dark-pro" style="background-color:#282c34;color:#abb2bf" tabindex="0"><code class="language-scala"><span class="line"><span style="color:#7F848E;font-style:italic">// userテーブルはクエリ可能。主キーはuserId</span></span>
<span class="line"><span style="color:#ABB2BF">id(tables.user)(_.userId)</span></span></code></pre>
<p>「3. テーブル同士の関連」も明示的に宣言することにしました。
Foreign keyをつかっていれば自動化も可能でした。
しかしデータ投入の都合や1つのテーブルが複数のテーブルに従属する構造があることからForeign keyは使わないコーディング規約としています。
関連を簡潔に定義する次の構文を考えました。</p>
<pre class="shiki one-dark-pro" style="background-color:#282c34;color:#abb2bf" tabindex="0"><code class="language-scala"><span class="line"><span style="color:#ABB2BF">table(tables.user).key(_.userId).has(</span></span>
<span class="line"><span style="color:#ABB2BF">  many(tables.prepaidCard)(_.userId),</span></span>
<span class="line"><span style="color:#ABB2BF">)</span></span></code></pre>
<p>fluent interfaceを意識したDSLになっています。
<code>many</code>のほかに<code>zeroOrOne</code>、<code>exactlyOne</code>などの関連が定義できます。</p>
<p>「4. 復号して返すべき項目」はConvention over Configurationでフィールド名に一貫性を持たせ、自動処理しました。
case classの<code>encryptedXxx</code>フィールドと<code>decryptXxx</code>メソッドが定義されていたら前者のフィールドを後者のメソッド呼び出しで置換するというルールです。
結果的にEncryptedMessageの扱い全体が統一感あるものになり、コードの品質も良くなりました。</p>
<p>この表現ではさきほどの20行程度の記述がわずか3行で済むようになりました。</p>
<h2 id="自動化">自動化</h2>
<p>利用したい表現と出力したいコードがきまったら、その間の自動化を実装するだけです。
Scalaでコード生成する方法は複数あります。</p>
<ol>
<li><a href="https://docs.scala-lang.org/ja/overviews/macros/overview.html">マクロ</a>はもっとも基本的なAST変換方法です。コンパイル時に与えられた記述を別のASTに置き換えます。式の位置に立つので、クラスを定義できないなどの制約があります。生成されたコードを直接確認できないので大規模になるとデバッグがとてつもなく大変です。</li>
<li><a href="https://docs.scala-lang.org/ja/overviews/macros/annotations.html">マクロアノテーション</a>は簡単に言うとクラスを定義できるマクロです。マクロがexpressionレベルで作用するのに対しマクロアノテーションは定義レベルで作用します。定義を書き換える性質上このマクロはtyper以前に実行されます。したがってこのマクロの中では型推論の恩恵が一切受けられません。上の例では<code>tables.user</code>が<code>User</code>型と関連するものだとわかりません。型パラメータや型注釈で明示する必要があります。これも大変な手間です。生成コードを確認できない点も同じです。</li>
<li><a href="https://docs.scala-lang.org/overviews/plugins/index.html">コンパイラプラグイン</a>はscalacに追加の処理ステップを設ける方法です。代表的な使用例は<a href="https://github.com/wartremover/wartremover">WartRemover</a>などのlinterです。コンパイルされたすべてのASTを見られるので、ASTを見て新しくコードを作成する処理を実装できます。</li>
<li>Scalaファイルを扱うScalaプログラムを作ることもできます。Scalaファイルを構文解析し、別のScalaファイルを出力するプログラムを実装します。コンパイラプラグインはコンパイルフェイズのなかで実行されるのに対し独立の処理として実行されます。</li>
</ol>
<p>今回は生成するコードの規模がとても大きいことから4の独立したプログラムの方法を採用しました。
すべて試したのですが、この方法がいちばんデバッグしやすく、ビルドに使っているsbtのキャッシュとも相性がよかったです。
sbtがマルチプロジェクト構成になっていて、メインのプロジェクトをビルドした後に自動生成のタスクが実行されます。
出力されたコードは<code>generated</code>というサブプロジェクトの<code>src</code>となり、自動生成後にsbtでビルドされます。
この構成では生成されたコードもsbtや最終的な成果物から見ればただのScalaコードなので、コンパイルエラーやデバッグ出力が読みやすいです。</p>
<p>ScalaはマクロをはじめASTの取り扱いに長けた言語です。
標準ライブラリにインタプリタ実装、AST、Quasi-quoteなど必要なツールセットがすべて組み込まれています。
すこしコツを掴めば簡単にAST変換で言語内DSLが実装できます。</p>
<p>まずScalaファイルを解析するため<code>IMain</code> (interpreter main)を作成します。
jarなどから実行する場合は単に作成するだけでよいのですが、sbtで実行する場合2つ余分に考えるべきことがあります。
1つはsbtがclass loaderを<code>LayeredClassLoader</code>に差し替えており、単純に作成するとScalaの標準ライブラリが見つからないという妙な状態になります。
次に<code>IMain</code>にメインのプロジェクトの型解析をやらせるにはメインのプロジェクトをclasspathに追加する必要があります。
メインのプロジェクトはすでにビルド済みで<code>.class</code>が作成されていますから、sbtのdependencyを辿って出力先ディレクトリをclasspathに組み込みます。
<code>LayeredClassLoader</code>の面倒をみつつ以上の目標を達成するコードはこのようになります。
このコードは試行錯誤して書いているので、不要な部分を含む恐れがあります。
また最後のところで<code>typer</code>フェイズまで実行して止まるようにしています。
型のついたASTだけあれば十分だからです。</p>
<pre class="shiki one-dark-pro" style="background-color:#282c34;color:#abb2bf" tabindex="0"><code class="language-scala"><span class="line"><span style="color:#C678DD">def</span><span style="color:#61AFEF"> makeInterpreter</span><span style="color:#ABB2BF">: </span><span style="color:#E5C07B">IMain</span><span style="color:#56B6C2"> =</span><span style="color:#ABB2BF"> {</span></span>
<span class="line"><span style="color:#C678DD">  val</span><span style="color:#E06C75"> settings</span><span style="color:#56B6C2"> =</span><span style="color:#C678DD"> new</span><span style="color:#E5C07B"> Settings</span></span>
<span class="line"><span style="color:#C678DD">  val</span><span style="color:#E06C75"> matcher</span><span style="color:#56B6C2"> =</span><span style="color:#98C379"> "file:(.*?scala-library.*?</span><span style="color:#56B6C2">\\</span><span style="color:#98C379">.jar)"</span><span style="color:#ABB2BF">.r.unanchored</span></span>
<span class="line"><span style="color:#ABB2BF">  @tailrec</span></span>
<span class="line"><span style="color:#C678DD">  def</span><span style="color:#61AFEF"> findPath</span><span style="color:#ABB2BF">(</span><span style="color:#E06C75;font-style:italic">loader</span><span style="color:#ABB2BF">: </span><span style="color:#E5C07B">ClassLoader</span><span style="color:#ABB2BF">): </span><span style="color:#E5C07B">String</span><span style="color:#56B6C2"> =</span><span style="color:#ABB2BF"> {</span></span>
<span class="line"><span style="color:#C678DD">    val</span><span style="color:#E06C75"> description</span><span style="color:#56B6C2"> =</span><span style="color:#ABB2BF"> loader.toString</span></span>
<span class="line"><span style="color:#ABB2BF">    description </span><span style="color:#C678DD">match</span><span style="color:#ABB2BF"> {</span></span>
<span class="line"><span style="color:#C678DD">      case</span><span style="color:#ABB2BF"> matcher(path)</span></span>
<span class="line"><span style="color:#C678DD">          if</span><span style="color:#ABB2BF"> scala.util.</span><span style="color:#E5C07B">Try</span><span style="color:#ABB2BF">(</span><span style="color:#E5C07B">Files</span><span style="color:#ABB2BF">.exists(</span><span style="color:#E5C07B">Paths</span><span style="color:#ABB2BF">.get(</span><span style="color:#E5C07B">URI</span><span style="color:#ABB2BF">.create(</span><span style="color:#98C379">"file:"</span><span style="color:#56B6C2"> +</span><span style="color:#ABB2BF"> path))))</span></span>
<span class="line"><span style="color:#ABB2BF">            .toOption.getOrElse(</span><span style="color:#D19A66">false</span><span style="color:#ABB2BF">) =></span></span>
<span class="line"><span style="color:#ABB2BF">        path</span></span>
<span class="line"><span style="color:#C678DD">      case</span><span style="color:#ABB2BF"> _ => findPath(loader.getParent)</span></span>
<span class="line"><span style="color:#ABB2BF">    }</span></span>
<span class="line"><span style="color:#ABB2BF">  }</span></span>
<span class="line"><span style="color:#C678DD">  def</span><span style="color:#61AFEF"> findClassPaths</span><span style="color:#ABB2BF">(</span><span style="color:#E06C75;font-style:italic">cl</span><span style="color:#ABB2BF">: </span><span style="color:#E5C07B">ClassLoader</span><span style="color:#ABB2BF">): </span><span style="color:#E5C07B">Option</span><span style="color:#ABB2BF">[</span><span style="color:#E5C07B">URLClassLoader</span><span style="color:#ABB2BF">] </span><span style="color:#56B6C2">=</span><span style="color:#ABB2BF"> {</span></span>
<span class="line"><span style="color:#ABB2BF">    cl </span><span style="color:#C678DD">match</span><span style="color:#ABB2BF"> {</span></span>
<span class="line"><span style="color:#C678DD">      case</span><span style="color:#E06C75;font-style:italic"> c</span><span style="color:#ABB2BF">: </span><span style="color:#E5C07B">URLClassLoader</span><span style="color:#ABB2BF"> => </span><span style="color:#E5C07B">Some</span><span style="color:#ABB2BF">(c)</span></span>
<span class="line"><span style="color:#C678DD">      case</span><span style="color:#D19A66"> null</span><span style="color:#ABB2BF">              => </span><span style="color:#E5C07B">None</span></span>
<span class="line"><span style="color:#C678DD">      case</span><span style="color:#ABB2BF"> other             => findClassPaths(other.getParent)</span></span>
<span class="line"><span style="color:#ABB2BF">    }</span></span>
<span class="line"><span style="color:#ABB2BF">  }</span></span>
<span class="line"><span style="color:#ABB2BF">  settings.bootclasspath.value +=</span></span>
<span class="line"><span style="color:#E5C07B">    Environment</span><span style="color:#ABB2BF">.javaBootClassPath </span><span style="color:#56B6C2">+</span></span>
<span class="line"><span style="color:#E5C07B">      File</span><span style="color:#ABB2BF">.pathSeparator </span><span style="color:#56B6C2">+</span></span>
<span class="line"><span style="color:#ABB2BF">      findPath(</span><span style="color:#E5C07B">GraphQLPlugin</span><span style="color:#ABB2BF">.getClass.getClassLoader) </span><span style="color:#56B6C2">+</span></span>
<span class="line"><span style="color:#E5C07B">      File</span><span style="color:#ABB2BF">.pathSeparator </span><span style="color:#56B6C2">+</span></span>
<span class="line"><span style="color:#ABB2BF">      findClassPaths(</span><span style="color:#E5C07B">Thread</span><span style="color:#ABB2BF">.currentThread().getContextClassLoader)</span></span>
<span class="line"><span style="color:#ABB2BF">        .map(_.getURLs</span></span>
<span class="line"><span style="color:#ABB2BF">          .map(_.toString.replaceFirst(</span><span style="color:#98C379">"file:"</span><span style="color:#ABB2BF">, </span><span style="color:#98C379">""</span><span style="color:#ABB2BF">))</span></span>
<span class="line"><span style="color:#ABB2BF">          .mkString(</span><span style="color:#E5C07B">File</span><span style="color:#ABB2BF">.pathSeparator))</span></span>
<span class="line"><span style="color:#ABB2BF">        .getOrElse(</span><span style="color:#98C379">""</span><span style="color:#ABB2BF">)</span></span>
<span class="line"><span style="color:#ABB2BF">  settings.stopAfter.value </span><span style="color:#56B6C2">=</span><span style="color:#E5C07B"> List</span><span style="color:#ABB2BF">(</span><span style="color:#98C379">"typer"</span><span style="color:#ABB2BF">)</span></span>
<span class="line"><span style="color:#C678DD">  new</span><span style="color:#E5C07B"> IMain</span><span style="color:#ABB2BF">(</span></span>
<span class="line"><span style="color:#ABB2BF">    settings,</span></span>
<span class="line"><span style="color:#E5C07B">    Some</span><span style="color:#ABB2BF">(</span><span style="color:#E5C07B">Thread</span><span style="color:#ABB2BF">.currentThread().getContextClassLoader),</span></span>
<span class="line"><span style="color:#ABB2BF">    settings,</span></span>
<span class="line"><span style="color:#C678DD">    new</span><span style="color:#E5C07B"> ReplReporterImpl</span><span style="color:#ABB2BF">(settings),</span></span>
<span class="line"><span style="color:#ABB2BF">  )</span></span>
<span class="line"><span style="color:#ABB2BF">}</span></span></code></pre>
<p><code>interpreter</code>が作成できたらソースコードを詠み込み<code>interpreter</code>に処理させます。
結果は<code>CompilationUnit</code>というASTのルートオブジェクトになるので、これを解析していきます。</p>
<pre class="shiki one-dark-pro" style="background-color:#282c34;color:#abb2bf" tabindex="0"><code class="language-scala"><span class="line"><span style="color:#C678DD">val</span><span style="color:#ABB2BF"> (result, run) </span><span style="color:#56B6C2">=</span><span style="color:#ABB2BF"> interpreter.compileSourcesKeepingRun(getSourceFile(file))</span></span>
<span class="line"><span style="color:#ABB2BF">require(result, </span><span style="color:#98C379">"Compilation error"</span><span style="color:#ABB2BF">)</span></span>
<span class="line"><span style="color:#ABB2BF">run.typerPhase.next</span></span>
<span class="line"><span style="color:#ABB2BF">run.units.foreach(x => process(x.</span><span style="color:#56B6C2">asInstanceOf</span><span style="color:#ABB2BF">[global.</span><span style="color:#E5C07B">CompilationUnit</span><span style="color:#ABB2BF">]))</span></span></code></pre>
<p>Scala ASTは<code>scala-reflect</code>の<code>Tree</code>として表現されます。
<code>Tree</code>解析の基本は次の形です。</p>
<pre class="shiki one-dark-pro" style="background-color:#282c34;color:#abb2bf" tabindex="0"><code class="language-scala"><span class="line"><span style="color:#ABB2BF">tree.map {</span></span>
<span class="line"><span style="color:#C678DD">  case</span><span style="color:#C678DD"> q</span><span style="color:#98C379">"&#x3C;条件>"</span><span style="color:#ABB2BF"> => </span><span style="color:#7F848E;font-style:italic">// &#x3C;処理></span></span>
<span class="line"><span style="color:#ABB2BF">}</span></span></code></pre>
<p><code>Tree</code>のtraverse系メソッドはVisitor pattern式に<code>Tree</code>内の全要素を探索します。
条件の部分に対象としたいASTを指定します。</p>
<p>今回は次のように関連を宣言しました。
<code>defClass</code>、<code>id()</code>などはすべて型だけ合わせるようになっていて、実装は空です。
最終的な成果物で呼び出されることはありません。</p>
<pre class="shiki one-dark-pro" style="background-color:#282c34;color:#abb2bf" tabindex="0"><code class="language-scala"><span class="line"><span style="color:#C678DD">class</span><span style="color:#E5C07B"> GraphQLRelationship</span><span style="color:#ABB2BF"> {</span></span>
<span class="line"><span style="color:#C678DD">  def</span><span style="color:#61AFEF"> kipp</span><span style="color:#56B6C2"> =</span><span style="color:#E5C07B"> GraphQLHelper</span><span style="color:#ABB2BF">.defClass(</span></span>
<span class="line"><span style="color:#E5C07B">    Seq</span><span style="color:#ABB2BF">(</span></span>
<span class="line"><span style="color:#ABB2BF">      id(tables.users)(_.userId)</span></span>
<span class="line"><span style="color:#ABB2BF">    ),</span></span>
<span class="line"><span style="color:#E5C07B">    Seq</span><span style="color:#ABB2BF">(</span></span>
<span class="line"><span style="color:#ABB2BF">      table(tables.user).key(_.userId).has(</span></span>
<span class="line"><span style="color:#ABB2BF">        many(tables.prepaidCard)(_.userId),</span></span>
<span class="line"><span style="color:#ABB2BF">      )</span></span>
<span class="line"><span style="color:#ABB2BF">    ),</span></span>
<span class="line"><span style="color:#E5C07B">    Seq</span><span style="color:#ABB2BF">(</span></span>
<span class="line"><span style="color:#7F848E;font-style:italic">      // アクセス制限、本稿では割愛</span></span>
<span class="line"><span style="color:#ABB2BF">    )</span></span>
<span class="line"><span style="color:#ABB2BF">}</span></span></code></pre>
<p>これにマッチする条件は次のとおりです。
可読性のため複数行にしています。</p>
<pre class="shiki one-dark-pro" style="background-color:#282c34;color:#abb2bf" tabindex="0"><code class="language-scala"><span class="line"><span style="color:#C678DD">q</span><span style="color:#98C379">"""</span></span>
<span class="line"><span style="color:#98C379">def </span><span style="color:#C678DD">$</span><span style="color:#ABB2BF">name</span><span style="color:#98C379">(): </span><span style="color:#C678DD">$</span><span style="color:#ABB2BF">_</span><span style="color:#98C379"> =</span></span>
<span class="line"><span style="color:#C678DD">  $</span><span style="color:#ABB2BF">_</span><span style="color:#98C379">.GraphQLHelper.defClass(</span><span style="color:#C678DD">$</span><span style="color:#ABB2BF">_</span><span style="color:#98C379">.apply[</span><span style="color:#C678DD">$</span><span style="color:#ABB2BF">_</span><span style="color:#98C379">, </span><span style="color:#C678DD">$</span><span style="color:#ABB2BF">_</span><span style="color:#98C379">, </span><span style="color:#C678DD">$</span><span style="color:#ABB2BF">_</span><span style="color:#98C379">](</span></span>
<span class="line"><span style="color:#C678DD">    $</span><span style="color:#ABB2BF">searChes</span><span style="color:#98C379">,</span></span>
<span class="line"><span style="color:#C678DD">    $</span><span style="color:#ABB2BF">relations</span><span style="color:#98C379">,</span></span>
<span class="line"><span style="color:#C678DD">    $</span><span style="color:#ABB2BF">restrictAccesses</span><span style="color:#98C379">))</span></span>
<span class="line"><span style="color:#98C379">"""</span></span></code></pre>
<p>展開結果は次です。</p>
<pre class="shiki one-dark-pro" style="background-color:#282c34;color:#abb2bf" tabindex="0"><code class="language-scala"><span class="line"><span style="color:#ABB2BF">name </span><span style="color:#56B6C2">=</span><span style="color:#ABB2BF"> kipp</span></span>
<span class="line"><span style="color:#ABB2BF">searches </span><span style="color:#56B6C2">=</span><span style="color:#E5C07B"> Seq</span><span style="color:#ABB2BF">(id(tables.users)(_.userId))</span></span>
<span class="line"><span style="color:#ABB2BF">relations </span><span style="color:#56B6C2">=</span><span style="color:#E5C07B"> Seq</span><span style="color:#ABB2BF">(tables(...)...)</span></span>
<span class="line"><span style="color:#ABB2BF">restrictAccesses </span><span style="color:#56B6C2">=</span><span style="color:#E5C07B"> Seq</span><span style="color:#ABB2BF">()</span></span></code></pre>
<p>あとは同様にsearchesに<code>id</code>などマッチさせて宣言を解析し、ASTからRDBの関連の内部表現に変換します。</p>
<pre class="shiki one-dark-pro" style="background-color:#282c34;color:#abb2bf" tabindex="0"><code class="language-scala"><span class="line"><span style="color:#7F848E;font-style:italic">// matcherの例</span></span>
<span class="line"><span style="color:#C678DD">q</span><span style="color:#98C379">"id(tables.</span><span style="color:#C678DD">$</span><span style="color:#ABB2BF">table</span><span style="color:#98C379">)(</span><span style="color:#C678DD">$</span><span style="color:#ABB2BF">fn</span><span style="color:#98C379">)"</span></span></code></pre>
<p>最後に関連データから冒頭のSangriaの仕組みに準拠したコードを生成し、.scalaファイルとして保存します。</p>
<h2 id="その他の機能">その他の機能</h2>
<p>実際のGraphQLインタフェースでは絞り込みや権限管理が要求されます。
これらも自動生成の仕組に閉じ込めることで生成されたSchemaを他のプログラムでいじらなくていいようにしています。
絞り込みは<code>searches</code>に次のように記述できます。</p>
<pre class="shiki one-dark-pro" style="background-color:#282c34;color:#abb2bf" tabindex="0"><code class="language-scala"><span class="line"><span style="color:#ABB2BF">many(tables.prepaidCard)(_.userId)</span></span>
<span class="line"><span style="color:#7F848E;font-style:italic">// => userId: "123" のようなargumentが作られます</span></span>
<span class="line"><span style="color:#ABB2BF">range(tables.prepaidCard(_.createdMs)</span></span>
<span class="line"><span style="color:#7F848E;font-style:italic">// => createdMs: {lt: 1625762709, gt: 1625762709} のようなargumentが作られます</span></span></code></pre>
<p>ほかにネストした検索条件、オプショナル値が定義されているかなど業務で頻出の条件が定義できます。</p>
<p>権限管理は管理画面を操作する人物（オペレータ）の職権に応じて表示可能なテーブルを制限します。
部長はオペレータの操作履歴を見られるが一般のオペレータは見られないなどのアクセスコントロールが可能です。</p>
<p>表に出てこない機能として、エラーを起こしかねない状況を発見することにも注力しています。
インデックスを張っていない項目で絞り込みを行うと性能が悪化します。
一度に数百万件を取得しようとするなども同様です。
インデックスの問題であればコンパイル時に、件数の問題はクエリ実行前に検出しエラーとします。
自動化したことで網羅性のある機械的チェックが可能になり、より信頼性の高いシステムが構築できました。</p>
<h2 id="効果">効果</h2>
<p>前回の記事にもあったように、当初はgRPCで管理画面にあわせてクエリを実装していました。
今回の例のUserとPrepaidCardのように、画面に応じてさまざまな組み合わせでデータを返す必要があります。
ページングや絞り込み条件もそれぞれに定義されているので、1APIずつ手で記述していました。
工数がかかるのみならず変更を忌避する気持ちが強くなっていました。</p>
<p>GraphQLを導入したのは呼出側が関連や絞り込みを柔軟に記述できると期待しての事でした。
社外のエンジニアの友人と夕食を共にしているとき、導入しやすいbackend for frontendとしてレクチャーされた記憶があります。
可能な限り少ない工数で最大の効果を上げることを目標として、RDBと密結合したクエリインタフェースを実装しました。</p>
<p>結果はすばらしいものでした。
多くのメンテナンスしにくい管理画面用のクエリ実装をすべて消し去りコードサイズが縮小しました。
新しい条件やテーブルの追加も数分から数十分でGraphQLに反映でき、開発者体験が改善しました。</p>
<p>さらにデバッグ用に<a href="https://github.com/graphql/graphiql">GraphiQL</a>を管理画面につけておいたところ、開発者だけでなく運用チームもデバッグや集計に使ってくれるようになりました。
すばらしい化学反応です。
管理画面は実装者に明確なリクエストを出し実装を待つ必要がありますが、GraphiQLは独力でちょっと調べることができます。
デバッグのために見られる情報が増え、開発者にトラブルシューティングが依頼される頻度も減りました。
GraphiQLのスキーマ表示や補完が充実していて非開発者に比較的使いやすかったこともプラスに働きました。
GraphQLというメジャーなシステムを利用した恩恵です。
最近ではGraphQLを通じてBigQueryやSpreadsheetにデータを書き出し、集計やモニタリングを自動化するところまで進んでいます。</p>
<p>ひとつ欠点を挙げるならば、AST変換実装がメタレベルのScalaコードとなるためやや理解しづらく、いざ変換コードに手をいれる時、担当者が限定されがちです。
Scala ASTとたわむれたい方はぜひお力をお貸しください。
正社員だけでなく時短や業務委託で活躍されているエンジニアの方も多数いらっしゃいます。
お気軽に<code>people@kipp-corp.com</code>までご連絡ください。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Kipp Engineering Blogを始めます]]></title>
            <link>/blog/hello-world</link>
            <guid isPermaLink="false">/blog/hello-world</guid>
            <pubDate>Fri, 18 Jun 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>はじめまして。xyzです。</p>
<p>この度プロダクトのローンチを受け、またKipp社内で取り扱っている技術の知見やノウハウの発信を目的として、エンジニアブログを始めるに至りました。
ブログを通して組織の雰囲気やKippという会社に興味を持っていただければ幸いです。</p>
<p>私自身の最初の投稿でもあるので簡単に自己紹介をします。
中高時代からHTMLやCSS・JS・Androidアプリ開発を独学し、大学では情報系の学部を卒業しました。
卒業後はアルバイトとして働いていた会社にそのまま就職後、転職を二度経験し現在に至ります。
前職、前々職ともフロントエンドエンジニアとして業務に携わっていました。
私個人としての主な興味はフロントエンドですが、Kippではフロントエンド・バックエンドと言った区分はなく、フルスタックエンジニアという肩書でバックエンドやインフラストラクチャにも携わっています。
少し前までは実質バックエンドエンジニアと言っていいほどもっぱらバックエンドの実装をしていました。
最近ではこのブログの構築等、フロントエンドが多くなっています。</p>
<p>さて、記念すべき1エントリ目ではKippが何をやっている会社なのかと、ある少し変わった実装について紹介します。</p>
<h2 id="kippという会社について">Kippという会社について</h2>
<p>Kippでは現在、主にBanking as a Serivce（以下BaaS）と呼ばれる金融サービスを開発・運用をしています。</p>
<ul>
<li><a href="https://prtimes.jp/main/html/rd/p/000000001.000076658.html">Kipp、金融機関向けにBaaS（Banking as a Service）を提供開始、並びにシードラウンドで累計5億円の資金調達を実施｜Kipp Financial Technologies株式会社のプレスリリース</a></li>
<li><a href="https://www.nikkei.com/article/DGKKZO70925370S1A410C2EE9000/">キップ、金融基盤の提供開始　伊藤忠などから5億円: 日本経済新聞</a></li>
</ul>
<p>弊社オフィスは日本の金融の中心街である東京都大手町の、数多くのFinTechベンチャー企業が集うFINOLAB内にあります。
オフィスは東京ですが、CEOは福岡県、CTOは北海道在住と柔軟な働き方ができる会社です。
エンジニアはコアタイムなしの完全フレックス制かつ完全リモートで、勤務日も柔軟に調整が可能です（稀に出社が必要になることがあります）。
業務において、普段のコミュニケーションはSlackを使ったテキスト中心の非同期なやりとりがほとんどです。
時間が取られがちである会議など同期的なものは極力避けて業務を遂行できるような環境・仕組み作りを行っています。</p>
<p>私自身、このような環境で働き始めてから半年以上経ちますが、プライベートとの両立がしやすくなり助かっています。
普段の生活に必要な買い物などでも気兼ねなく外出できますし、病院などが予約が必要な予定も入れやすいです。
普段の業務であれば離席時に逐一報告等も必要なく、仕事中の外出の心理的障壁が低いのも利点です。
仕事の合間に散歩してリフレッシュしつつ、何食わぬ顔でまた業務に戻る…という雰囲気で自由にやらせてもらっています。</p>
<h3 id="baasとは">BaaSとは</h3>
<p>BaaSとは多岐にわたる金融サービスをワンストップで提供する中央集権的なSaaS (Software as a Service) です。
利用企業やエンドユーザーはそれぞれのAPIを通じてBaaSを利用します。
すでにリリースされているサービスではプリペイドカードの発行と残高管理・決済・送金機能・債権管理など多数の機能を提供しています。</p>
<h3 id="なぜbaasとして提供するのか">なぜBaaSとして提供するのか</h3>
<p>これまで金融サービスの開発と運用は利用企業ごとにデザインやワークフローをカスタマイズしたい需要が強いもので、一社一社に合わせて製品を提供していました。
しかし、金融サービスの提供事業者にとっては開発に時間とコストがかかるだけではなく、その基盤の上で新機能の追加や外部連携といった機能を追加するのは更にコストがかかってしまいます。
Kippはカスタマイズをした製品を売るのではなく、どんな企業にも受け入れられる機能セットを備えた1つのサービスを提供します。
利用者はAPIを通して提供したい機能を集中して開発し、エンドユーザーにサービスを届けることができます。</p>
<h3 id="どのようにbaasを提供しているのか">どのようにBaaSを提供しているのか</h3>
<p>Kippのコア部分は主に定期的なジョブを処理するプログラムとAPIサーバとして処理を待ち受けるプログラムで成り立っています。
定期的なジョブはプリペイドカードの発行処理・財務局への提出が必要な日次・月次レポートの作成など多岐にわたります。
また、よく使われる管理機能を提供するウェブアプリケーション（管理画面）があります。
管理画面はカスタマイズのニーズが少なく、低いイニシャルコストで効率の良いものを使えることが望まれるので標準提供しています。</p>
<p>以上を踏まえて、今回は一例としてKippの管理画面を交えながら、技術スタックと少し変わった実装について軽く触れて終わりにします。</p>
<h2 id="管理画面について">管理画面について</h2>
<p>変わった実装の前に軽く技術スタックを紹介します。</p>
<p>管理画面のバックエンドとなるAPIサーバーはScalaで実装され、データベースには<a href="https://aws.amazon.com/jp/rds/aurora/">Aurora MySQL</a>を利用しています。
バックエンドでは<a href="https://www.playframework.com">Play Framework</a>のようなフルスタックフレームワークを用いず、データベースとの接続に<a href="https://scala-slick.org">Slick</a>を、外部との通信は主に<a href="https://grpc.io">gRPC</a>を利用するシンプルな構成になっています。
また、REST APIにおける純粋なGETのようなデータの取得がメインになる操作については<a href="https://sangria-graphql.github.io">Sangria</a>と呼ばれるGraphQLライブラリを介してデータの取得、レスポンスをしています。</p>
<p>次に実際にユーザーが操作することになるフロントエンド部分についてです。
言語はTypeScriptを採用しており、フレームワークは古いプロジェクトではNuxt.js、最近のプロジェクトではNext.jsを採用しています。
管理画面のUIには<a href="https://vuetifyjs.com/ja/">Vuetify</a>や<a href="https://material-ui.com">Material-UI</a>と言ったマテリアルデザインベースのUIライブラリを採用しています。
UIライブラリは無数にありますが、マテリアルデザインベースのものを選択したのには3つ理由があります。</p>
<ol>
<li>既存のコンポーネントが充実しています。
今日UIとしてありがちなモーダル・テーブル・カードなど必要不可欠なものは一通り揃っています。
これらを組み合わせることで開発速度を上げることができます。</li>
<li>実装方針が示されています。
既存コンポーネントが充実しているだけではどのように使うことがユーザーにとって最適なのかが判断しづらく、ときにちぐはぐなUIを構築してしまうことがあります。</li>
<li>マテリアルデザインというそこそこメジャーなUIを提供することで、使用者が既に同じようなUIを操作している可能性が高くなり、どんな操作をすれば良いかを自然と判断できることが期待できます。</li>
</ol>
<p>3つ目に関しては、マテリアルデザインを使うとどうしても同じように見えて退屈に感じてしまうデメリットとも読み取れるので、コーポレートサイトなどブランドを前面に出したい場合は少し難しい場合もあります。
しかし、カスタマーに直接見えない管理画面の類ではあまり気にする必要がないですし、綺麗さ・使いやすさ・開発スピードのバランスが取れた優秀なUIと言えるでしょう。</p>
<h3 id="grpcメソッドのリクエストにgraphqlクエリをのせる">gRPCメソッドのリクエストにGraphQLクエリをのせる</h3>
<p>フロントエンドアプリケーションである管理画面はバックエンドとの通信の際、取得にはGraphQL、作成・更新にはgRPCという少し変わった戦略を取っています。
正確に言えば、データを取得する際のGraphQLのクエリもgRPCのリクエストの文字列として送信・受信をしているので、基本的には全てgRPCの上で通信していることになります。</p>
<p>なぜこのような実装になっているかと言うと、フロントエンドがデータの取得に関しては画面の構成に応じて柔軟に関連するデータを組み合わせることを要求することが理由です。
逐一クライアント側に合わせてgRPCのメッセージを作っていてはキリがありませんし、protoを変更すればサーバーとクライアントの両方を変更する必要があり、開発者の手間が増えます。</p>
<p>例としてユーザー情報を取得するアプリを考えてみます。</p>
<pre class="shiki one-dark-pro" style="background-color:#282c34;color:#abb2bf" tabindex="0"><code class="language-proto"><span class="line"><span style="color:#C678DD">service</span><span style="color:#E5C07B"> UserAPI</span><span style="color:#ABB2BF"> {</span></span>
<span class="line"><span style="color:#C678DD">  rpc</span><span style="color:#61AFEF"> fetch</span><span style="color:#ABB2BF">(</span><span style="color:#E5C07B">Request</span><span style="color:#ABB2BF">) </span><span style="color:#C678DD">returns</span><span style="color:#ABB2BF"> (</span><span style="color:#E5C07B">Response</span><span style="color:#ABB2BF">) {}</span></span>
<span class="line"><span style="color:#ABB2BF">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C678DD">message</span><span style="color:#E5C07B"> Request</span><span style="color:#ABB2BF"> {</span></span>
<span class="line"><span style="color:#C678DD">  string</span><span style="color:#E06C75"> id</span><span style="color:#56B6C2"> =</span><span style="color:#D19A66"> 1</span><span style="color:#ABB2BF">;</span></span>
<span class="line"><span style="color:#ABB2BF">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C678DD">message</span><span style="color:#E5C07B"> Response</span><span style="color:#ABB2BF"> {</span></span>
<span class="line"><span style="color:#C678DD">  string</span><span style="color:#E06C75"> id</span><span style="color:#56B6C2"> =</span><span style="color:#D19A66"> 1</span><span style="color:#ABB2BF">;</span></span>
<span class="line"><span style="color:#C678DD">  string</span><span style="color:#E06C75"> name</span><span style="color:#56B6C2"> =</span><span style="color:#D19A66"> 2</span><span style="color:#ABB2BF">;</span></span>
<span class="line"><span style="color:#C678DD">  string</span><span style="color:#E06C75"> email</span><span style="color:#56B6C2"> =</span><span style="color:#D19A66"> 3</span><span style="color:#ABB2BF">;</span></span>
<span class="line"><span style="color:#7F848E;font-style:italic">  // DBのフィールドが増え、クライアントにも必要になったとき</span></span>
<span class="line"><span style="color:#7F848E;font-style:italic">  // 逐一サーバーとクライアントで変更をする必要がある</span></span>
<span class="line"><span style="color:#ABB2BF">}</span></span></code></pre>
<p>コメントにあるように、フィールドが増えたときに変更を要します。</p>
<p>更にここからユーザーが記事を投稿できるような機能が追加され、ユーザー情報取得時に記事も取得したくなったとき、このような変更を加えることになるでしょう。</p>
<pre class="shiki one-dark-pro" style="background-color:#282c34;color:#abb2bf" tabindex="0"><code class="language-proto"><span class="line"><span style="color:#C678DD">service</span><span style="color:#E5C07B"> UserAPI</span><span style="color:#ABB2BF"> {</span></span>
<span class="line"><span style="color:#C678DD">  rpc</span><span style="color:#61AFEF"> fetch</span><span style="color:#ABB2BF">(</span><span style="color:#E5C07B">Request</span><span style="color:#ABB2BF">) </span><span style="color:#C678DD">returns</span><span style="color:#ABB2BF"> (</span><span style="color:#E5C07B">Response</span><span style="color:#ABB2BF">) {}</span></span>
<span class="line"><span style="color:#ABB2BF">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C678DD">message</span><span style="color:#E5C07B"> Request</span><span style="color:#ABB2BF"> {</span></span>
<span class="line"><span style="color:#C678DD">  string</span><span style="color:#E06C75"> id</span><span style="color:#56B6C2"> =</span><span style="color:#D19A66"> 1</span><span style="color:#ABB2BF">;</span></span>
<span class="line"><span style="color:#ABB2BF">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C678DD">message</span><span style="color:#E5C07B"> Response</span><span style="color:#ABB2BF"> {</span></span>
<span class="line"><span style="color:#C678DD">  string</span><span style="color:#E06C75"> id</span><span style="color:#56B6C2"> =</span><span style="color:#D19A66"> 1</span><span style="color:#ABB2BF">;</span></span>
<span class="line"><span style="color:#C678DD">  string</span><span style="color:#E06C75"> name</span><span style="color:#56B6C2"> =</span><span style="color:#D19A66"> 2</span><span style="color:#ABB2BF">;</span></span>
<span class="line"><span style="color:#C678DD">  string</span><span style="color:#E06C75"> email</span><span style="color:#56B6C2"> =</span><span style="color:#D19A66"> 3</span><span style="color:#ABB2BF">;</span></span>
<span class="line"><span style="color:#C678DD">  repeated</span><span style="color:#C678DD"> Article</span><span style="color:#E06C75"> articles</span><span style="color:#56B6C2"> =</span><span style="color:#D19A66"> 4</span><span style="color:#ABB2BF">;</span></span>
<span class="line"><span style="color:#ABB2BF">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C678DD">message</span><span style="color:#E5C07B"> Article</span><span style="color:#ABB2BF"> {</span></span>
<span class="line"><span style="color:#C678DD">  string</span><span style="color:#E06C75"> title</span><span style="color:#56B6C2"> =</span><span style="color:#D19A66"> 1</span><span style="color:#ABB2BF">;</span></span>
<span class="line"><span style="color:#C678DD">  string</span><span style="color:#E06C75"> body</span><span style="color:#56B6C2"> =</span><span style="color:#D19A66"> 2</span><span style="color:#ABB2BF">;</span></span>
<span class="line"><span style="color:#ABB2BF">}</span></span></code></pre>
<p>gRPCメッセージ自体に関連を定義してしまうと、今後クライアントの画面構成次第でprotoファイルに変更をする可能性があります。
その結果、サーバーでも実装の変更が発生するのは手間がかかってしまいますし、ナンセンスと言えるでしょう。
また、Webアプリでは記事情報が必要だけどモバイルアプリでは必要ない、と言った状況ではフィールドをoptionalにするのも、似たようなAPIをもう1つ生やすのも良い実装に感じられません。</p>
<p>そこで、データの取得に関してはGraphQLのデータをやり取りをするgRPCメソッドを用意することで、クライアントは自由にデータの型を定義できるようになります。
gRPCメソッドのリクエストとレスポンスはstring型のフィールドを持つシンプルなメッセージです。
サーバーは、リクエストに乗っているGraphQLのクエリ文字列（ミューテーションは含みません）の処理をSangriaで行い、結果をJSON文字列として返します。</p>
<pre class="shiki one-dark-pro" style="background-color:#282c34;color:#abb2bf" tabindex="0"><code class="language-proto"><span class="line"><span style="color:#C678DD">service</span><span style="color:#E5C07B"> GraphQLAPI</span><span style="color:#ABB2BF"> {</span></span>
<span class="line"><span style="color:#C678DD">  rpc</span><span style="color:#61AFEF"> query</span><span style="color:#ABB2BF">(</span><span style="color:#E5C07B">Request</span><span style="color:#ABB2BF">) </span><span style="color:#C678DD">returns</span><span style="color:#ABB2BF"> (</span><span style="color:#E5C07B">Response</span><span style="color:#ABB2BF">) {}</span></span>
<span class="line"><span style="color:#ABB2BF">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C678DD">message</span><span style="color:#E5C07B"> Request</span><span style="color:#ABB2BF"> {</span></span>
<span class="line"><span style="color:#7F848E;font-style:italic">  // 欲しいフィールドのGraphQLクエリをqueryの中に文字列として格納する</span></span>
<span class="line"><span style="color:#C678DD">  string</span><span style="color:#E06C75"> query</span><span style="color:#56B6C2"> =</span><span style="color:#D19A66"> 1</span><span style="color:#ABB2BF">;</span></span>
<span class="line"><span style="color:#ABB2BF">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C678DD">message</span><span style="color:#E5C07B"> Response</span><span style="color:#ABB2BF"> {</span></span>
<span class="line"><span style="color:#7F848E;font-style:italic">  // queryに基づいたJSON文字列が返ってくる</span></span>
<span class="line"><span style="color:#C678DD">  string</span><span style="color:#E06C75"> result</span><span style="color:#56B6C2"> =</span><span style="color:#D19A66"> 1</span><span style="color:#ABB2BF">;</span></span>
<span class="line"><span style="color:#ABB2BF">}</span></span></code></pre>
<p>こうすることでサーバーとクライアントの間でgRPCとしての型を気にする必要はなくなり、分担して開発を進めることが可能になります。
勿論、サーバーの実装としてDBのカラム情報をgRPCのフィールドへのデータの詰替え作業がなくなった分、GraphQLとしてのリレーションを定義する必要性があります。
ですが、interfaceの実装が必要なgRPCメソッドを直接いじらずにサーバーとクライアントが各々に作業できるのは利点でしょう。
これはある種のCQRS (Command Query Responsibility Segregation) 的発想と言えます。</p>
<p>さらにリレーションの定義はScalaの機能を生かした内製ライブラリで大幅に簡略化できています。
このライブラリについては次回ご紹介します。</p>
<h2 id="終わりに">終わりに</h2>
<p>今回は少ない作業量で高品質な管理画面を提供するコツを紹介しました。
次回はGraphQL APIを提供するサーバ側の仕組みをより詳細に紹介する予定です。
今後もKippのテクノロジーや開発チームについて続々と情報発信していきますので、ぜひお読みください。
弊社に興味をもたれた方はお気軽に<code>people@kipp-corp.com</code>にコンタクトください。</p>
<p>今後ともよろしくお願いします。</p>]]></content:encoded>
        </item>
    </channel>
</rss>