DIGGLE開発者ブログ

予実管理クラウドDIGGLEのプロダクトチームが、技術や製品開発について発信します

YAPC::Hiroshima 2024に行ってきたよ

ご無沙汰しています。DIGGLEのhirataです。 今回初めてYAPCに行ってきたのでその時の事を書いてみようと思います。

YAPCとは「Yet Another Perl Conferenceの略で、Perlを軸としたITに関わる全ての人のためのカンファレンスです」(サイトからの引用)でPerlを軸にはしますが、Perlにとどまらず技術的な話題を取り扱うカンファレンスとのことです。

関係ないですがタイトル画像は広島のアーケード街にあったゲームセンターの入口です。手をパチパチ叩くロボットが面白かったので撮ってしまいました。

事の始まり

YAPC::Hiroshimaが開催される数ヶ月前の事。いつものようにデイリーの朝会に参加していると、「YAPCに行きたい人居ないですか?」というCTOのお言葉。 ありがたいことに希望があれば経費で行かせてくれるという事でしたが、そのときはYAPCがPerlに関連するイベントということしか知らなかったために全く関心がありませんでした。

ですが、よく話を聞くとPerlに限ったイベントではなくプログラミングやソフトウェア技術全般のイベントとのこと。後でスケジュールを見てみると面白そうな講演もいくつかあり、折角の機会なので参加させていただくことにしました。やった!会社のお金で旅行だ!

前夜祭

前日2/9の午後から新幹線で東京から広島まで移動して、どうせならということで前夜祭にも参加しました。

受付を済ませて着席し周りを見渡すと、いかにもソフトウェアエンジニアの集まりという雰囲気で、これは期待が持てる感じです。

前夜祭が始まると、まず開発者さん自身によるHonoというJavascriptのフレームワークの熱い紹介がありました。私があまり強くない分野に関連する話題が多かったため内容の理解が難しかったのですが、とにかく開発のスピード感と熱量が凄そうなフレームワークでした。

その後も登壇者の方が各々のテーマでお話いただいていました。会場ではお酒が振る舞われており、登壇者の方のお話を聞きながらお酒を飲むスタイルです。観客の皆さんはお酒が入っているため好きにガヤガヤ話していて、しかも登壇者もそれを気にしないで話し続けるというカオスな状態で盛り上がっており、前夜祭の雰囲気を十二分に味わうことができました。

本編

2/10は朝から参加です。受付を済ませた後、まだ時間があったので協賛企業のブースを廻りました。ここでDeNAさんが朝食用におにぎりを配っていて美味しそうでしたが、ホテルの朝食でたっぷり食べてしまっていたため、お腹いっぱいで食べることができませんでした。

一通りブースを回り、早めに会場に向かうと既に何やら始まっている雰囲気。うっかりオープニングの事を忘れてしまっていた…。折角だから見ておけばよかったです。それはともかく、席に座ると昨日と打って変わってサラリーマンっぽい雰囲気の人が増えてました。

午前の講演で面白いと思ったのは、「VISAカードの裏側と “手が掛かる” 決済システムの育て方」と「My Favorite Protocol: Idempotency-Key Header」の2つでした。 特にVISAカードの方は普段触れることのないカード決済の仕組みを知ることができ、とても興味深かったです。やはり昔から続くサービスはいろいろ泥臭い部分があり、歴史の積み重ねを感じました。ネットもない時代からあるのでどうしても過去の経緯を引きずらざるを得ないのだと思います。こちらはベストLTに選ばれていました。 またもう一つの「My Favorite Protocol」の方はWebでAPIを開発する上でよく遭遇する問題である、「確実に一回だけリクエストを送りたい」問題に対する解の一つだと思いました。

お昼は弁当が配られたのですが、どれも美味しそうでした。私が食べたのは穴子弁当。写真撮ったけど画質が宜しく無いですね。美味しくいただきました。

昼のおべんとう

午後の部では「理解容易性と変更容易性を支える自動テスト戦略」が面白かったです。もしかしたらテストに詳しい方だと当然の内容なのかもしれませんが、テストをネットワークアクセスやDBアクセス等の有無によってlarge, medium, smallに分けてピラミッドにするという話はなるほどと思いました。業務でテストを書く上で単体テスト、統合テスト等は書き分けて意味があるのかと感じることもありましたが、この分類であればsmallは基本的にパッと終わるテスト、largeは時間の掛かるテストになるので、普段はsmallだけテストしてぱぱっと確認し、PRを出す前にlargeも含めたテストを実施してより網羅的に確認といった事ができそうです。登壇者の方が翻訳に参加された「テスト駆動開発」という本も気になって買ってしまいました。

そして最後にあの伝説のとほほさん(敢えてひらがな)の講演がありました。登壇する前に座ってた席が実は近くだったのでびっくりしました。まさかインターネットの黎明期にPerlだったりHTMLでお世話になった伝説の人にお目にかかることができるなんて…。講演の内容もとても面白かったです。

その後は懇親会まで参加しましたが、体調があんまり良くなかったのもあり、最初に近くにいた人としか話せなかったのが心残りです。次に参加する機会があれば体調を整えて交流を楽しめるようにしたいと思いました。

懇親会場

その後

懇親会で人から聞いて知ったのですが、次の日にもいろいろ関連イベントがありました。しかし既に枠が一杯で参加できず。 なので翌日は広島城に行ったり、庭園を散策したり、広島のアーケード街を散歩したりで1日中観光してから帰りました。知らない街を散策するのはとても楽しいです。

途中で食べた備後府中焼きは広島風お好み焼きのバリエーションらしいのですがとても美味しかったです。語彙が少なくて具体的にどこがとは言えないのですが、個人的には普通のお好み焼きよりも好みの味でした。

備後府中焼き

タイミング悪く私自身の体調が良くなかったのが残念ですが、イベントは盛り上がっておりとても楽しめました。オンラインでも見ることはできますが、生の会場の雰囲気はまた違ったものがあってYAPCに参加できて良かったと思いました。

おしまい。

We're hiring!

DIGGLE では共にプロダクトを開発してくれるエンジニアを大募集中です。このように気軽に技術者の集いに参加させてもらえます。

少しでも興味があれば、ぜひ下記採用サイトからエントリーください。
カジュアル面談も実施しているので、気軽なお気持ちでご応募いただければと思います!

herp.careers

パフォーマンス分析基盤の構築。S3やAthenaを活用して柔軟性の高いデータの取り扱いを実現した話

はじめに

DIGGLEではすでにDatadogを導入してパフォーマンス指標のトレースデータ*1は取れていますが、以下の課題がありました。

  • ユースケースを分けてパフォーマンス指標を取得したい
  • DIGGLEがターゲットとしたい企業やペルソナユーザーのデータのみを取り出すのが面倒
  • 開発時点でCIが動いた時に、分析結果をプルリクのコメントに出力したい
  • Datadogに保存されているパフォーマンス指標のトレースデータには有効期限がある

上記に対応する為にまずは調査を行いました。

Datadog API Clientによるデータの取得

Datadogに保存されているデータをAPI経由で取得することによって、必要なデータが得られないかを調べました。

結果としては、RUMデータは取得できそうでしたが、APMのトレースデータは現状ではAPI提供されていませんでした。

そしてCI起点でのパフォーマンス分析を考えると、リアルタイムにパフォーマンス指標データを取れない所からDatadogベースでは厳しいです。 またDatadogを選択すると有効期限の問題や、ターゲットとしたいデータを取り出すのが面倒という問題が解消できません。

上記などの理由によりDIGGLEでは、独自でパフォーマンス指標を取得する方針としました。 DatadogはDatadogで有用なのでそちらの利用は継続し、別途パフォーマンス分析基盤を構築して、そちらに分析用のデータを保存するイメージです。

パフォーマンス分析の概要

対象データ

顧客への価値提供に影響が強い部分の、ユースケース別の実行時間などのデータになります。 特定処理の実行時間から、細かい粒度だとSQLの実行時間まで対象としています。 またDIGGLEがターゲットとする、代表的なペルソナユーザーが処理したデータになります。

分析方法

それぞれのユースケース別の実行時間を分析する形になります。 パフォーマンス改善施策などの効果を測定する為に、リリース対応前後の実行時間をクエリ抽出して効果測定します。 また、代表的なペルソナユーザーのデータを定量データとして保持して、予期せぬタイミングでパフォーマンスが悪化していないかなどの時系列分析も行います。

パフォーマンス分析基盤の構成の検討

データ保存先

トレースデータの保存先としては以下のような選択肢が考えられます。

  • DB
  • NoSQL系
  • Bigquery
  • Redshift
  • S3
  • Athena

どの選択肢も一長一短がありますが、DIGGLEではS3へログデータとして転送する方針となりました。 既存システム性能への影響が小さく、S3から先へもAthenaでデータソース化するなど色々な選択肢が取れるためです。 またS3へ保存することにより、Datadogの有効期限の問題は解消されます。

構成

S3へのデータ保存が決まりましたので、パフォーマンス分析基盤の構成はおおまかに以下のようになりました。 3と4の構成についてはデータ保存後も柔軟に変更可能です。

1のRailsから標準出力への出力先を実ファイルへ変更すると、CIでの分析も可能になります。また独自プログラムで実行するのでユースケース分けや、データのフィルタリングについてもやりやすいという利点があります。

  1. ECSのRailsから標準出力にパフォーマンス指標データをログ出力
  2. Firelensを経由してFluent bitでログをS3へ転送
  3. S3に保存されたJSONデータをAthenaでデータソース化
  4. AthenaをデータソースとしてMetabaseからクエリでデータ分析を実施

対応内容

サーバーサイド

こちらはDatadogのソースなどを参考にしながら、Rails標準のActive Support Instrumentationで取得できるパフォーマンス指標はこちらで取得し、それ以外については独自で対象のメソッドをフックして、パフォーマンス指標をリクエストなどの処理ごとに取得するように対応しました。

インフラ

ECSのRailsからは標準出力でログ出力を行い、それがFirelensからFlunet bitに渡り、Fluent bitのrewrite_tagでS3へ振り分けを行います。

Flunet bitの設定例)

jsonデータのhogeキーの値がhogehogeの場合にS3に転送される設定例になります。

# rewrite tag
[FILTER]
    Name          rewrite_tag
    Match         *-firelens-*
    Rule          $hoge hogehoge target true

# output to s3
[OUTPUT]
    Name            s3
    Match           target
    bucket          target-bucket
    region          ap-northeast-1
    use_put_object  On
    total_file_size 1M
    upload_timeout  1m
    retry_limit     False
    compression     gzip

S3に保存したJSON形式のログデータはAthenaで以下のようなクエリでテーブル化が可能です。

CREATE EXTERNAL TABLE IF NOT EXISTS hoge (
  id int,
  name string
)
ROW FORMAT SERDE 'org.openx.data.jsonserde.JsonSerDe'
WITH SERDEPROPERTIES (
    'null.format' = ''
)
LOCATION 's3://target-bucket/'
TBLPROPERTIES ('has_encrypted_data'='true');

注意点としてはinteger型を定義した場合は、空白データを許容してくれなくなるので、ログ出力側で0などを出力する必要があります。

最終的にDIGGLEではAthenaをデータソースにMetabaseでクエリを実行する形式にしています。

フロントエンド

フロントエンドはサーバーサイドで作った仕組みに乗っかる形で、処理単位で複数のトレースデータをサーバーサイドに送信する形を取りました。

最後に

今回はパフォーマンス分析基盤の構築について検討から実際の構築まで紹介しました。性能観点と後々の柔軟性という部分で今回はS3へデータを保存する形になりましたが、他にも様々な選択肢があると思います。

この記事が、分析基盤構築などの一助となれば幸いです。

DIGGLEのエンジニアのchikugoがお送りしました。

We're hiring!

DIGGLE では共にプロダクトを開発してくれるエンジニアを大募集中です。

少しでも興味があれば、ぜひ下記採用サイトからエントリーください。
カジュアル面談も実施しているので、気軽なお気持ちでご応募いただければと思います!

https://herp.careers/v1/diggle/_dgvcOQcFfeqherp.careers

*1:トレースデータとはアプリケーションやサービスが実行する一連の操作を表す実行時間などを含むデータです

なりすまし/迷惑メールと思われない為のDMARC導入手順

はじめに

DIGGLEエンジニアのaki0344です。
2024/02/01より、Gmailのガイドラインが変更されました。 support.google.com

Gmailに対して1日5000件以上のメールを送信しているドメインはSPF、DKIM、DMARCによる認証を設定していない場合なりすましや迷惑メールと判断されて送信先に届かない可能性があります。
今回、DIGGLEではSalesチームやプロダクトから送信したメールが迷惑メールと判定されないよう、全社のメールでDMARCに対応しました。
本記事ではその際に行った作業についてまとめてみました。
実際の手順のみ知りたい方はDMARC対応の流れからお読みください。

DMARC対応作業を始めるまで

それは一通の問い合わせから始まった

ある日、Salesチームから1通の問い合わせが届きました。

翌日、開発チームのデイリースクラムで問い合わせについて共有され、対応が必要だねという話になりました。
メールのセキュリティについて全く知識が無かった私は静観していたのですが、生憎他メンバーは直近の作業に追われており、急ぎの作業のなかった私に押し付けられましたが対応することにしました。
何のことか良くわからないけど、何を設定すれば良いかは明示されてるしそんなに大変な作業でもないでしょと思っていました。実際に作業を始めるまでは…。

そもそもDMARCって何だ?

さて、DMARCに対応するためには当然DMARCが何者なのかを知らなければいけません。
まずはDMARCで検索をかけてみたのですが、SPF/DKIMというこれまた聞いたことのない言葉が出てきました。
いきなり調べることが増えてしまった…。
様々なサイトを眺めていましたが、『構造計画研究所』さんのブログ記事がとても分かりやすかったです。

sendgrid.kke.co.jp

sendgrid.kke.co.jp

それぞれを一言で表すと以下のようになります。

  • SPF : 正規のサーバから送信されていることを証明する
  • DKIM : ドメイン管理者がメールを送信していることを証明する
  • DMARC : SPF/DKIMで認証失敗した場合にメールをどう扱うかを決める

DMARCに対応するためには、その前段階としてSPF/DKIMに対応しなければいけません。
※DMARCはSPF/DKIMいずれかのみと組み合わせての対応も可能ですが、今回DIGGLEでは全て対応することを前提に作業を進めました。

対応時の注意点

DMARCに対応するにあたり、いくつか注意点があります。

メール送信方法によって対応内容が異なる

メール送信を行うサービスを利用している場合、それぞれに対応が必要となります。
一部設定が漏れていた場合、そのサービスから送信されたメールが送付先でなりすましと判定される可能性があります。

SPF、DMARCの対応は複数サービスで同じ場所に修正を加える必要がある

詳細は後述しますが、DNSに設定するレコードはSPF、DMARCでそれぞれ1行です。
複数サービスでSPF、DMARCに対応する場合、各サービスの案内に記載されている内容をそのまま転記するとそれぞれ複数行追加してしまう可能性があります。
作業完了時、SPF、およびDMARC用のDNSのレコードはそれぞれ1行ずつと覚えてください。
なお、DKIMは各サービスごとにレコードが必要となります。

SPF/DKIM未対応の状態でDMARCに対応してはいけない

先述の通り、DMARCはSPF/DKIMの認証結果を基に次のアクションを決定する機能となります。
SPF/DKIMに未対応の状態でDMARCのみ追加した場合、送信したメールがすべてなりすましと判定される可能性があります。
DMARCは対応が必要な全てのサービスでSPF/DKIMに対応し、最後に設定を行いましょう。

定義を間違えると他のサービスにも影響を及ぼす危険性がある

複数サービスで共通する箇所に修正を入れるため、1か所失敗すると全てのサービスが正常に動作しなくなる可能性があります。
かならずクロスチェックをしましょう。

DMARC対応の流れ

ここからは実際にDMARCに対応する際に行ったことを書いていきます。

自社で保有しているドメイン一覧を特定する

複数のドメインを保有している場合、各ドメインでメールを送信している可能性があります。
今回の対応はドメイン毎に必要となるため、対応漏れが無いよう自社ドメインを全て洗い出します。

メールアドレスに使用されているドメインを特定する

メールの送信元(From)となり得る全てのドメインで対応要否の判定が必要となります。
社内の有識者に確認し、どのドメインがメール送信に使用されているかを絞り込みます。
DIGGLEでは以下の方法で絞り込みを行いました。

  • バックオフィスに確認
    各社員に割り当てられたメールアドレスを管理している部署に、他のドメインを利用しているかを確認します。
  • プロダクト管理者に確認
    プロダクトの機能でユーザーへメール送信する際に利用しているドメインを確認します。

ドメイン毎にDMARC対応が必要か判定する

Googleのガイドラインでは、1日5000件以上のメールを送信する場合にDMARCへの対応が必要となっています。
毎日大量のメールを送信しないことが明らかな場合はDMARCの対応を行わなくてもガイドラインには抵触しません。
今回のDIGGLEの対応ではドメイン毎に送信しているメールの件数について確認はしていませんが、使用用途から現在、あるいは今後1日5000件以上のメールを送信すると判断したドメインに対して対応を行うことにしました。

メール送信を行っているサービスを洗い出す

SPF/DKIMはメールサーバ毎に設定を行う必要があります。
そのため、自社内で先ほど絞り込んだドメインを送信元としてメール送信している外部サービスを確認します。
バックオフィスなど各部署の利用しているサービスを管理している部署に、自社ドメインでメール送信を行っているサービスについて問い合わせましょう。
DIGGLEでは念のためslackの全社員が登録しているチャンネルでも確認を行いました。
※文中にある各個人からのメール送信はGmailのSMTPサーバを利用しており、過去にSPF、DKIM対応済みだったため今回は対応不要としています。

各サービスでSPF、DKIMの対応方法を確認する

SPF、DKIMの対応方法はサービスによって異なるため、各サービスのマニュアルや設定画面から対応方法を特定します。
DIGGLEで利用していたサービスでは大別して4パターンあることがわかりました。

SMTPサーバを指定している

Metabaseのように自分でサーバを用意するシステムの場合、メールはSMTPサーバを指定するのが一般的です。

この場合はSMTPサーバを管理しているサービスで次に記載する内容を確認します。

SPF/DKIMを有効化するレコードをDNSに追加する

最も一般的な対応方法です。 各サービス毎に、SPF、およびDKIMを設定するための値が記載されているページを探します。 SPFは一般に公開されていることが多く、マニュアル等やFAQで記載されている個所を探します。

SPFの設定値サンプル

DKIMで設定する値はドメイン毎に異なるため、管理者画面からのみ確認が可能となっています。
ドメインや配信時のメールアドレスを設定するページで確認できることが多いです。

emaildocs.netcorecloud.com

サービス側のDNSサーバを参照するためのレコードをDNSに追加する

SendGridなどの一部サービスでは、サービスが用意しているDNSサーバがSPF/DKIMの認証を行う機能があります。

sendgrid.kke.co.jp

その場合、自社で用意しているDNSではサービス側のDNSサーバを参照するためのレコードを定義する必要があるため、その値が記載されているページを探します。
レコードの値はドメイン毎に異なるため、管理者画面からのみ確認が可能となっています。
ドメインや配信時のメールアドレスを設定するページで確認できることが多いです。

他サーバのDNSサーバ参照値サンプル

対応方法が何も書かれていない場合

DIGGLEで利用しているサービスの中には、FAQ等のドキュメントや管理者用設定画面のどこを探してもSPF、DKIM、DMARCに関する記載が一切存在しないサービスがありました。
こちらは実際にメールを配信する手順で社員へメールを送信し、ヘッダ情報でSPF、DKIM、DMARCに対応しているかを確認し、未対応の場合は問い合わせる方向で作業を進めました。
※ヘッダ情報の確認方法は後述しています。
結果的には全て対応済みだったため事なきを得ました。

DNSで各ドメインにSPF、DKIM、DMARCのレコードを追加する

各サービスで追加する内容が明確になったので、ドメイン毎にレコードを追加していきます。
ここで2点注意事項があります。
※サブドメイン毎に設定する場合はこの限りではありません。

  • SPFのレコードは1ドメインに1つ
    複数のサービスを利用している場合、『SPF/DKIMを有効化するレコードをDNSに追加する』でサービス毎のSPFを確認していると思いますが、DNSには1つのレコードにまとめて登録する必要があります。
    複数の場合はincludeをサービス数分定義し、各サービスの値を設定します。

    • 誤った設定例
      v=spf1 include:spf.corp01.value ~all
      v=spf1 include:spf.corp02.value ~all
      v=spf1 include:spf.corp03.value ~all
    • 正しい設定例
      v=spf1 include:spf.corp01.value include:spf.corp02.value include:spf.corp03.value ~all
  • DMARCのレコードは1ドメインに1つ
    DMARCはサービス数に関わらず共通の値を使用します。

SPF、DKIMのレコードを追加する

ドメインを管理しているDNSで、ドメイン毎にSPF、DKIMを有効にするためのレコードを追加します。
SPFは1つにまとめた値を、DKIMはサービス毎に指定された値の数だけレコードを追加します。
値に誤りがあると他サービスにも影響する可能性があるため、クロスチェックを行う等、確認は念入りに行ってください。
レコード追加後、各サービスからメールを送信し、ヘッダの内容からSPF、DKIMが有効になっているか確認します。
※有効になるには一定時間を要する場合があります。
Gmailの場合は『メッセージのソースを表示』で確認することが可能です。

DMARCのレコードを追加する

ドメインを管理しているDNSで、ドメイン毎にDMARCを有効にするためのレコードを追加します。
パラメータは必要に応じた内容を設定します。
www.naritai.jp

認証失敗時の動作pについてはいきなりrejectなどの厳しい制限を指定すると今までメールを受け取れていた相手に届かなくなる可能性があります。
一旦noneを設定し、レポートで状況を確認しながら段階的に制限を強めていくのがおすすめです。
以下は設定値の一例です。

v=DMARC1; p=none; rua=mailto:report-rua@my.corp.com; ruf=mailto:report-ruf@my.corp.com;

ruarufを設定しておくとメールでレポートが送られてくるため、後から到達率等の振り返りが可能となります。
レコード追加後、各サービスからメールを送信し、ヘッダの内容からDMARCが有効になっているか確認します。
※有効になるには一定時間を要する場合があります。
SPF、DKIMと同様に、Gmailの場合は『メッセージのソースを表示』で確認することが可能です。

サービス導入時の対応手順を整備する

以上の手順で現在利用しているサービスについてはSPF、DKIM、DMARCが有効になりました。
一方、今後各部署で利用するサービスを追加した場合は、今回と同様の手順でSPF、DKIMの追加が必要な可能性があります。
そのため、社内でまとめているサービス導入時の手続き等の文書にDNS管理者へ設定の依頼を行う手順を追加しておくことをお勧めします。
DIGGLEでは以下の条件を満たすサービスの場合にはDNS管理者へ通知するよう手順をまとめました。

  • 自社ドメインを利用してメールを送信する
  • 初期セットアップで送信メールサーバを指定しない

これで今後の新規サービス導入がスムーズに進むことが期待できます。

おわりに

今回はDIGGLEから送信したメールがGmailでなりすましや迷惑メールと判定されないよう、SPF、DKIM、DMARCを有効化するために行った作業をまとめてみました。
本記事では効率的で漏れのない手順をまとめていますが、実際には私の知識不足により各手順を行ったり来たりしながら無駄の多い作業となってしまいました。
幸い、DIGGLEでは最近立ち上がったSREチームにセキュリティに強い方がいたため、その方に叱られながらアドバイスをいただきながら無事作業を完了させることが出来ました。
本記事が、メールのセキュリティ強化に対応する際の一助となれば幸いです。

We're hiring!

DIGGLE では共にプロダクトを開発してくれるエンジニアを大募集中です。

少しでも興味があれば、ぜひ下記採用サイトからエントリーください。
カジュアル面談も実施しているので、気軽なお気持ちでご応募いただければと思います!

herp.careers

Rustで型付Webアプリケーション開発

この記事は Rust Advent Calendar 2023 25日目の記事です。

お久しぶりです。DIGGLEのhirataです。

今回はDIGGLEとは関係ないですが、個人的にRustの利用を促進すべくRustでのWebアプリケーションの開発経験について書きたいと思います。

所感としては一人で開発するならRustを使うべし、と思える経験でした。やはりあちこちで言われている事ではありますが、静的型とライフタイムチェックによりコンパイルを通ってしまえばほぼバグが無く動くという安心感がとても心地よいものでした。コンパイラが代わりにテストをしてくれているような心持ちです。 また、Rustで作成したサーバはとてもメモリ消費量が小さく、Railsアプリであれば下手をすると1GB程まで行ってしまいますが、数十MBで十分動くというのも個人にとってありがたいものでした。 ちなみにとても安定して動作しており、今までバグやアップデート以外の再起動はありません。

参考までに私は元組み込み系のエンジニアでして、その影響で未だにメモリを沢山使うことに罪の意識を覚えます。 そして現在の仕事のメインはRailsでメモリ潤沢な大富豪バックエンドの開発をしています。その影響で随所にRailsとの比較が出てくるかなと思います。

どうしてRustだったのか。そしてどんなWebアプリケーションか。

それまでRustでいくつか小さなプログラムを書いたことはありましたがWebアプリケーションを書くとどんな感じなのだろうかという好奇心が全てでした。

おかげで少々苦労することにはなりましたが。

プロジェクトとしては個人での副業で、開発者は一人だったのでインフラも含めて全て自由に決めることができました。運用費はできるだけ安く (できればタダ) にしたかったので、Railsだとリソース的に厳しかろうということでRustを選んだ一面もあります。 用途はとある小規模なジムで使う来客管理システムで、会員登録やその事前登録、お客様の入退場、入場料等を管理できるものになっています。 一人で開発するならSPAじゃないほうが開発は速かろうということで、マルチページでのアプケーションとしました。 使用しているクレート (Rustでは公開・共有されているライブラリの事をこう呼びます) は開発開始当初は大まかに以下の通りでした。

後は必要に応じて追加していくことになります。

まずは見切り発車での開発。

とりあえずactix-webはRustではその当時(2年ほど前)一番メジャーで高速そうなクレートだったので採用しました。確かどこかのベンチマークサイトで一番になっていたことも有ったはずです。他のも大体人気順に決めた感じでした。 あまり知識がない状態だとこんなものです。

実際に使ってみるとactix-webやteraは特に問題なく使えたのですが、dieselはなかなか難解でした。JOINの無い状態だと簡単に使えるのですがJOINした場合の型が以下のように大変なことに。。。 これはJOIN有りでSELECTした結果を引数として受け取る関数です。とても自分で書けるものではなかったので、エラーメッセージからコピペしました。

fn admittance_search_query<'a>(params: &CustomerAdmittanceSearchParams)
 -> diesel::query_builder::BoxedSelectStatement<'a, 
        (schema::customer_admittances::SqlType, schema::staffs::SqlType, schema::customers::SqlType),
        diesel::query_source::joins::JoinOn<diesel::query_source::joins::Join<diesel::query_source::joins::JoinOn<diesel::query_source::joins::Join<schema::customer_admittances::table,
        schema::staffs::table,
        diesel::query_source::joins::Inner>,
        diesel::expression::operators::Eq<diesel::expression::nullable::Nullable<schema::customer_admittances::columns::processed_by_id>,
        diesel::expression::nullable::Nullable<schema::staffs::columns::id>>>,
        schema::customers::table, diesel::query_source::joins::Inner>, 
        diesel::expression::operators::Eq<diesel::expression::nullable::Nullable<schema::customer_admittances::columns::customer_id>,
        diesel::expression::nullable::Nullable<schema::customers::columns::id>>>,
        diesel::pg::Pg>

RDBを使っている以上、JOINが無いことは考えられないと思いますのでこれは避けて通れなさそうです。当時はver1.x系しか無くてそれを使っていたのですが、今はver2になっているのでもしかしたら現在は解決されているかもしれません。もしくはもっと良い書き方があるよ!という方は教えていただけると助かります。

初期段階で一番苦労したのは、Railsのように1から10までレールが引かれている訳では無いので、開発の基盤を安定させるところでした。例えば以下の事は自分で決める必要がありました。

  • モジュールをどのような構造で構成するか。MVCにするか、レイヤードアーキテクチャにするか、はたまたオリジナルにするか。そこまで複雑にならない予定だったのでRailsで慣れているMVC構成としました。
  • 構成したモジュール間の初期化処理をどのように繋げるか。簡単なようですがついついエレガントさを求めてしまい、意外に時間を取られます。そして最終的には悩んだ割にあんまりエレガントじゃないという。。。
  • エラーログはどう出力するか。どのライブラリを使うのか。エラーレベルはどう定義するのか。最終的にはenv_loggerとlogというクレートを使いました。
  • 各種設定や開発環境、本番環境の切り分けは環境変数で行うのか、設定ファイルを作るのか。コンテナで動かすつもりだったので環境変数で全て設定するようにしています。
  • JavaScript側の構成はどうするのか?JavaScriptのライブラリを使いたかったので一応webpackも入れました。

このように事ある毎に自分でレールを敷く必要があるので、このやり方でいいのかという自問との戦いでした。下手に悩むくらいなら実は酒でも飲みながら進めた方が捗ったかもしれないですね。

開発を終え保守のステージ。

無事リリースし以降数回の修正を重ねることになりますが、幾度かの修正を重ねる中、HTMLテンプレートのチェックをコンパイル時に行ってくれないteraだと、実際に動かしてみるまで些細なミスが発見できないのが苦痛になってきました。 修正後にサーバを起動しページをレンダリングしようとした段階で、結構な頻度でテンプレート内の式でエラーになってしまいます。

ということで、コンパイル時にテンプレートについても静的チェックを行ってくれるクレートを探していたところ発見しました。

github.com

これがとても良く私の用途にマッチしてくれて、おかげでテンプレート内の式の書き間違いに苦しむ事はなくなりました。一度コンパイルが通ればページレンダリングの段階でエラーになることはありません。

このようにしてHTML出力に関してはほぼ動的にエラーが発生することはなくなり、コンパイル時に気づくことができるようになりました。 ただまだHTML上でシステムにリクエストを行うような下記の部分はサーバを起動して動的にチェックする必要があります。

  • aタグやその他に書かれているシステムのURL
  • formタグの中身のinput

これらをコードで生成するライブラリを作ることができればJavaScirpt以外は全てコンパイル時にチェックされることになり動的なエラーを撲滅できそうです。 実現できれば後はJavaScriptをTypeScript化すれば全てに型が付いてコンパイラがチェックしてくれるWebアプリケーション開発環境のできあがりです。夢が膨らみます。暇が無くてできてませんが。

またコードを書いている中でどうしても、formを受け取るための構造体と、ORMで使うための構造体に差が出てきてしまうので最初は手書きでメンバをコピーするコードを書いていました。 これが苦痛でたまらない上に、ミスに気づきにくかったのですが、構造体のコピーをしてくれるようなクレートが見つからなかったため、自分でマクロを作りました。

https://crates.io/crates/clone_into_derive

これでかなりコードが簡略化できたのと、どうしてもコピーするメンバを書き忘れることがあったのですがそれが無くなって保守性があがりました。

こんなコードが有った場合、直接コピーできないのでどうしても冗長に書く必要があります。わかりやすくするために簡略化しています。

// formから変更内容を受け取る用の構造体
pub struct UpdateCustomer {
    pub second_name: String,
    pub first_name: String,
    ...
    pub blood_type: i16,
    pub memo: String,
    pub monthly_path_limit: Option<NaiveDate>,
}

// ORM用の構造体定義
pub struct Customer {
    pub id: i64,
    pub assigned_id: Option<i32>,
    pub email: String,
    pub sex: i16,
    pub birthday: NaiveDate,
    pub post_number: String,
    pub prefecture: String,
    pub municipalitie: String,
    pub address: String,
    pub tel: String,
    pub created_at: NaiveDateTime,
    pub updated_at: NaiveDateTime,
    pub blood_type: i16,
    pub membered_at: Option<NaiveDateTime>,
    pub memo: String,
    pub second_name: String,
    pub first_name: String,
    pub second_name_furigana: String,
}

pub fn change_it(con: &Connection, customer_id: i64, update_customer: UpdateCustomer) {
    let customer = customer::find(con, customer_id);
    customer.second_name = update_customer.second_name.clone();
    customer.first_name = update_customer.first_name.clone();
    ...
    customer.blood_type = update_customer.blood_type;
    customer.update(con);
}

こうなります (変更部分だけ)。違いはUpdateCustomer#[derive(CloneInto)]というマクロを使用するための記述が追加されたのと、そのマクロupdate_customer_clone_into!chjange_it内でメンバのコピーに使っていることです。かなり短くなりました。

// formから変更内容を受け取る用の構造体
#[derive(CloneInto)]
pub struct UpdateCustomer {
    pub second_name: String,
    pub first_name: String,
    ...
    pub blood_type: i16,
    pub memo: String,
    pub monthly_path_limit: Option<NaiveDate>,
}

pub fn change_it(con: &Connection, customer_id: i64, update_customer: UpdateCustomer) {
    let customer = customer::find(con, customer_id);
    update_customer_clone_into!(param.customer, customer);
    customer.update(con);
}

よし!かなり使えるライブラリだ!、と思ってクレートとして公開したのですが、全然アクセスがありません。私の想定では人気のクレートになってRust界の有名人になる筈だったのですが。

是非使っていただければ。

あとがき。

という訳で、全てでは無いですがHTMLテンプレートの中までコンパイラが型チェック等をしてくれる状態になりました。 一人で開発しているとやることが多いので、動作確認の一部とはいえ肩代わりしてくれる存在が心強いです。

実装面での感想ですが、Rustでは俗に言う継承(inheritance)が無く、代わりに委譲(composition)が推奨されているのでRubyのような純粋オブジェクト指向の世界から来ると戸惑いを覚えます。 共通部分だけベースクラスとして定義して、それを継承するということは結構やってしまいがちだと思いますが、一切使えません。代わりに委譲を使いましょう。

また安心してマクロが使えるというのは特筆すべきことかなと思います。マクロとはコードを生成するコードの事で、Rustではコンパイル時にマクロからコードに展開します。 マクロはデバッグも厄介で時にバグの温床になるのですが、Rustでは生成されたコードが問題ないかどうかを型の整合性の面と変数のライフタイムからコンパイラがチェックしてくれるので、コンパイルが通ればほぼ大丈夫という安心感があります。 私の書いたマクロは単にコピー元の変数の構造体定義にあるメンバをコピー先として指定された変数の同名のメンバに代入するコードを生成するというものですが、このチェックのおかげで問題があればコンパイラが検出してくれます。なので私のクレートではコピー先の型は指定しなくても良いようにしました。 このようにマクロとコンパイラでの静的チェックはとても相性が良さそうに思います。

そしてRustはいい!コンパイルが通れば動くという安心感に加えて、クレート=ライブラリも比較的充実しており、あまり開発時に困ることもなさそうです。 私はCommon Lispも好きなのですが、欲しいライブラリが無いことがままあって悲しい思いをすることが多いです。。。

ただ、マクロが必要になることが結構ありそうなこと、ライフタイムがあること、そもそも型の概念が難しい等々で、やはりRustを扱う人には高い技術が求められそうです。 個人的にはそれに伴うリターンはあると思うので、高い技術力をもったメンバーを集めるのであれば、Rustでの開発も選択肢に入れてもいいのではないかと考えます。

以上RustでSPAでは無いアプリケーションを作ったときのお話でした。 今どきはHTMLを直接出力するWebアプリを作る機会は少ないのかもしれませんが、何かの参考にしていただければ。

We're hiring!

DIGGLE では共にプロダクトを開発してくれるエンジニアを大募集中です。

少しでも興味があれば、ぜひ下記採用サイトからエントリーください。 カジュアル面談も実施しているので、気軽なお気持ちでご応募いただければと思います!

herp.careers

OWASP Top Ten に見る Web セキュリティの20年

セキュリティ

こんにちは、DIGGLE エンジニアの庵です。決して三澤ではありません。
はじめてのアドベントカレンダー Advent Calendar 2023 の 24日目の記事です。

 

はじめに

みなさんは Web セキュリティについて、どのようなイメージを持たれていますか?
サービスダウンや情報漏洩等、利用者としても運営者としても怖いものですね。

そんな Web セキュリティについて、2003年から数年おきに提供されている OWASP Top Ten というレポートについて少し調べてみた事をお話ししたいと思います。

 

脆弱性の変遷

ある時 OWASP Top Ten を眺めていると、ふと思いました。
昔のバージョンでも名前が違うけど似たような項目があったような‥‥と。
そこで、全バージョンを表にして、変遷を見やすくしてみましょう。

OWASP Top Ten 2003 - 2021

そうですね、あまり見やすくはないですね。
レイアウトの限界という事でどうぞご理解ください。

表の見方の簡単な説明は、以下のような感じです。

  • 縦方向に上から下に1位〜10位、横方向に左から右に2003年版〜2021年版です。
  • 版をまたいで同じ分類のものについて、項目の箱を同じ色にしています。
    • 分割された項目や統合先と表記が乖離している項目は、同系統の暗めの色にしています。
  • 左下に判例があるアイコンで、それぞれの項目に補足情報を付与しています。
    • 新設 新設
    • 廃止 廃止
    • 分割 分割
    • 統合 統合
    • 長くランクインし続けている項目 (この記事の焦点) 長くランクインし続けている項目 (この記事の焦点)

OWASP Top Ten は、2003年2004年2007年2010年2013年2017年2021年と、これまでに7回提供されています。
また、Mobile 向けや API 向けの Top Ten も別途提供されています。

それでは、Web 向けの最新版である2021年度版に基づいて、各脆弱性の変遷を見ていきたいと思います。
各項目名に OWASP の解説ページへのリンクを設定しているので、詳細はそちらをご参照ください。


脆弱性カウントダウン

第10位

変遷
入選 版数 年度 順位 名称
1. #7 2021 A10 Server-Side Request Forgery (SSRF)

第7版の2021年に新たに設けられた分類として10位にランクインしました。

発生数は比較的少ないものの被害が平均よりも大きくなりがちである事から、注意喚起の為にも選出されたようです。


第9位

変遷
入選 版数 年度 順位 名称
1. #6 2017 A10 Insufficient Logging & Monitoring
2. #7 2021 A09 Security Logging and Monitoring Failures

第6版の2017年に10位でランクインし、
2021年は9位に順位を上げています。

ロギングとモニタリングはテストが難しい上に、CVE / CVSS (*1) のような脆弱性情報として現れる事が少ないですが、うまく機能しなかった場合、説明責任やフォレンジック等影響が大きいです。このような点から選出されたようです。

*1: 分かり易さの為に CVE / CVSS の管理者では無く第三者の運営するサイトにリンクしています。


第8位

変遷
入選 版数 年度 順位 名称
1. #6 2017 A08 Insecure Deserialization
2. #7 2021 A08 Software and Data Integrity Failures

第6版の2017年に8位でランクインし、
2021年も8位でフィニッシュです。

CI/CD パイプラインで整合性未検証のデータを取り込んでしまう事により、必然的に脆弱性を作り込んでしまうという影響の大きさから選出されたようです。まるでスパイに潜入されてしまうようなイメージですね。対策としては、例えば SBOM の活用を検討する等が挙げられるのではないかと思います。もちろん OWASP が挙げている防止方法もチェックしたいですね。


第7位

変遷
入選 版数 年度 順位 名称
1. #1 2003 A03 Broken Account and Session Management
2. #2 2004 A03 Broken Authentication and Session Management
3. #3 2007 A07 Broken Authentication and Session Management
4. #4 2010 A03 Broken Authentication and Session Management
5. #5 2013 A02 Broken Authentication and Session Management
6. #6 2017 A02 Broken Authentication
7. #7 2021 A07 Identification and Authentication Failures

第1版の2003年に3位でランクインし、
2007年は7位に転落、
2010年はまた3位に返り咲き、
2013年からは2位まで上昇しましたが、
2021年はまたしても7位に転落しました。

以前は認証の不備としてカテゴライズされていた項目で、不適切な証明書の問題やセッション管理の問題も内包しています。本人確認が重要である事はオフラインの社会でも同じですね。継続的に選出されている事も納得できると思います。


第6位

変遷
入選 版数 年度 順位 名称
1. #1 2003 A08 Insecure Use of Cryptography
2. #2 2004 A08 Insecure Storage
3. #3 2007 A08 Insecure Cryptographic Storage
4. #4 2010 A07 Insecure Cryptographic Storage
5. #5 2013 A09 Using Known Vulnerable Components
6. #6 2017 A09 Using Components with Known Vulnerabilities
7. #7 2021 A06 Vulnerable and Outdated Components

第1版の2003年に8位でランクインし、
2010年に7位へ上昇しましたが、
2013年2017年と9位へ転落、
2021年過去最高位の6位に上昇しました。

簡潔に言うと、ライブラリ・ミドルウェア・ OS 等のバージョンを最新に保てていない問題です。現実問題として、隅々まで漏れなくアップデート済みかというと不安が残る方もいらっしゃるのではないでしょうか。そのような点から選出されたようですね。こちらについても SBOMVuls のようなツールが有用だと思われます。AWS の ECR もイメージスキャン機能を提供しているので、ぜひ活用したいですね。


第5位

変遷
入選 版数 年度 順位 名称
1. #1 2003 A10 Web and Application Server Misconfiguration
2. #2 2004 A10 Insecure Configuration Management
3. #4 2010 A06 Security Misconfiguration
4. #5 2013 A05 Security Misconfiguration
5. #6 2017 A06 Security Misconfiguration
A04 XML External Entities (XXE)
6. #7 2021 A05 Security Misconfiguration

第1版の2003年に10位でランクインし、
2007年に一度ランク外になったにも関わらず、
2010年に再度6位にランクイン、 
そこから5位と6位を行き来しており、 
2021年は5位となっています。

統計的にアプリケーションの90%に何らかの設定ミスがあるとの事です。年々設定自体も複雑化していく事もあり、重要度が高い事から選出されたようです。


第4位

変遷
入選 版数 年度 順位 名称
1. #7 2021 A04 Insecure Design

第7版の2021年に新たに設けられた分類として4位にランクインしました。

設計やアーキテクチャの欠陥に関するリスクに焦点を当て、選出されたようです。 OWASP の概要において注目すべき CWE として、 CWE-209: エラーメッセージからの情報漏洩 や CWE-522: 適切に保護されていないクレデンシャル 等が挙げられています。他にも、ログにパスワード等が出力されている環境もまだまだあるのではないでしょうか。


第3位

変遷
入選 版数 年度 順位 名称
1. #1 2003 A04 Cross-Site Scripting (XSS) Flaws
A06 Command Injection Flaws
2. #2 2004 A04 Cross-Site Scripting (XSS) Flaws
A06 Injection Flaws
3. #3 2007 A01 Cross Site Scripting (XSS)
A02 Injection Flaws
4. #4 2010 A01 Injection
A02 Cross-Site Scripting (XSS)
5. #5 2013 A01 Injection
A03 Cross-Site Scripting (XSS)
6. #6 2017 A01 Injection
A07 Cross-Site Scripting (XSS)
7. #7 2021 A03 Injection

第1版の2003年に4位でランクインし、
2007年から2017年までの4回1位を独占し、
2021年は減速しながらも3位につけています。

統計的にアプリケーションの94%に何らかのインジェクションに関する問題があるとの事です。 SQL インジェクションや OS コマンドインジェクション等、何かしら一度は聞いた事があるかもしれません。選出は必然といったところではないかと思います。


第2位

変遷
入選 版数 年度 順位 名称
1. #3 2007 A09 Insecure Communications
2. #4 2010 A09 Insufficient Transport Layer Protection
3. #5 2013 A06 Sensitive Data Exposure
4. #6 2017 A03 Sensitive Data Exposure
5. #7 2021 A02 Cryptographic Failures

第3版の2007年に9位でランクインし、
2013年に6位へ躍進し、
2017年に3位、
2021年に2位と着実に順位を上げてきています。

機微情報の露出に直接的に関係する事や暗号化の不適切な設定や使用方法が散見される事等から選出されたようです。アリスとボブも納得ですね。


第1位

変遷
入選 版数 年度 順位 名称
1. #1 2003 A02 Broken Access Control
2. #2 2004 A02 Broken Access Control
3. #3 2007 A04 Insecure Direct Object Reference
A10 Failure to Restrict URL Access
4. #4 2010 A04 Insecure Direct Object Reference
A08 Failure to Restrict URL Access
5. #5 2013 A04 Insecure Direct Object Reference
A07 Failure to Restrict URL Access
6. #6 2017 A05 Broken Access Control
7. #7 2021 A01 Broken Access Control

第1版の2003年に2位でランクインし、
2007年に2項目に分割された後、
2017年に再び統合され、
2021年にはとうとう念願(?)の1位を獲得しました。

統計的にアプリケーションの94%に何らかのアクセス制御の不備があるとの事です。一時期大きなニュースになっていたキャッシュサーバのトラブルに伴う情報漏洩やクロスサイトリクエストフォージェリ (CSRF) 等もこちらの項目に該当します。はまちちゃん、栄光の1位選出です!

 

まとめ

全10項目の内、過半数の6項目が長くランクインし続けていて、特に4項目は第1版の2003年から毎回ランクインしています。

2003年から2021年の間に、セキュリティに対する意識や組織的な取り組みは着実に向上してきているものと思われますが、バリデーションや設定・設計の不備等からくる脆弱性は残念ながら残り続けているようですね。
IaC や CI/CD 等により自動化された取り組みを積み上げていく事が、改善の道標だと考えられますが、2021年に8位となった項目で、 CI/CD の管理上の問題により脆弱性を抱えてしまうという問題に言及されている点も注目すべきだと思われます。

また、OWASP Top Ten はあくまでも分かりやすさを重視した形でのレポートだと思いますので、これだけやっておけば完璧というものではありませんが、まずは何に注意すべきかという意味ではとてもありがたい情報ですね。

身も蓋も無い結論ではありますが、結局はいかに確実に管理し続けられるかという問題に取り組みつつ、新しい脆弱性にも対応していくしかないようですね。

We're hiring!

予実管理の DIGGLE をより良くする為、リモートで一緒に開発しませんか?

「予実管理って何?」等少しでも興味をお持ち頂いたら、ぜひカジュアル面談しましょう!

herp.careers

私たち DIGGLE についてもっと知りたいと思って下さった方は以下をご覧ください。

diggle-jp.notion.site

複式簿記のアーキテクチャを理解する

この記事ははじめてのアドベントカレンダー Advent Calendar 2023 23日目の記事です。

こんにちは。DIGGLEのCTO水上です。アドカレのために何を書こうか悩んでいたら投稿前日になってしまいました。 悩んだ末、社内勉強会で一度話した内容を書くことにしました。(省力化・・)

まえがき

フリーランス等で、少し複式簿記をかじったことがある方は、とりあえずネットで調べた方法で仕訳を書いてみたりしたものの、結局本質的に何をやっているのかがよく分からないという方も多いのではないでしょうか。 私も過去にフリーランスだった時期があり、自分で理屈を理解するためにスプレッドシートで実装してみたりしていました。色々と書いてみた経験を通して、私は自分なりに本質的な仕組みがどういうものなのかを理解することができましたが、軽く触れてもそれを理解しづらかった要因として、検索しても普遍的な仕組みの説明にたどりつくことはほぼなく、財務的なルールに関する記事がほとんどだからではないかと感じています。

そこで、(私の知る限りで)複式簿記のシステムのうち最も基礎となる部分だけにフォーカスして、技術的(あるいは数理的)に抽象化して切り離し、データベース上で実装することを通じて、エンジニアにとって馴染み深い形で仕組みを理解する手助けになるような記事を書いてみました。

※ その道の専門家ではないので、細かい部分での誤りがありましたらご指摘いただけると幸いです。

定義と設計

データベース上で実装するといっても、設計なしには何も記述できません。まずはいくつかの概念を定義しながら、具体の実装方針を検討していきます。

※定義が難解に感じるようであれば、実装の部分まで読み飛ばしていただいて構いません

お金を定義する

ひとまずお金という概念を定義します。お金は、

  • 定量的に認識できる(つまり、100や200など私たちの知っている数字表記で表せるということ)
  • この世界のお金の総量は、増えたり減ったりしない

という性質をみたすものとします。 総量が変わらないという制約はともかく、技術的にはInteger型を採用することにすれば要件を概ね満たすものとなるでしょう。実は、総量が変わらないという制約は複式簿記と最も大きく関係する特徴ですが、それは最後に触れます。

箱を定義する

つぎに、 を定義します。箱は、

  • 箱の中には、お金が無数に存在し(しかし無限ではない!)、誰かがそこからお金を動かしたとしても無くなるようなことはない
  • この世界のすべてのお金は、いつもどれか一つの箱に入っている
  • この世界には箱が複数存在できる

とします。

各箱は、常に無数のお金が入っているわけですが、操作をして底が尽きることがないということを考えて、初期値を0と考え、マイナス値になっても扱えるということにしてしまいましょう。(体重計に箱を載せた状態でONにするようなイメージ)

たとえば世界に箱が3つしかないとき、その箱の全体は、(0, 0, 0)と表すことができそうです。ただ、単に底を突く心配をしなくても良いように扱いたいだけなので、初期値がいくらなのかというのは重要ではありません。

(お金の)移動を定義する

次に、箱から箱へのお金の移動について定義します。移動は

  • 移動元の箱
  • 移動先の箱
  • 金額

で定められます。

素朴にコードに落としてみるとすると、イメージとしては { from: Box, to: Box, amount: Integer } のような構造体が想像できると思います。実装上はこの方式を採用します。

一方で、別の表現方法として、各箱の増減分を並べたものでも表すことができます。たとえば、3つ箱がある世界で、1つめの箱から2つめの箱へ、50移動することを表すには (-50, +50, 0) と表すことができます。箱と同じようにInteger型のタプルで表現可能になります。数学でいう点とベクトルみたいなものですね。

集計結果を定義する

移動が何度も起きるわけですが、結果として各箱がどれくらい増減したのか、というものを 集計結果 ということにします。 集計結果というものを具体的にどういうユースケースで、何を意味するものとして使うのかは後半の例で紹介します。

ここで、移動をタプル=ベクトルで表現できるメリットとして、ベクトルの自然な加法に基づいて、足し合わせを表現することができる点があります。つまり移動Aと移動Bを両方行うことを、単にA+Bで表現できます。実際、1つめの箱から2つめの箱へ、50移動し、2つめの箱から3つめの箱へ、30移動することは (-50, +50, 0) + (0, -30, +30) = (-50, +20, +30) で表すことができます。その意味で、移動の足し合わせもまた広義には移動ということにします。この世界のお金が増えたり減ったりしないという条件は、この移動を表すベクトルの要素の総和が0になっていることから保証されます。

上記の (-50, +20, +30) という移動は、それぞれの箱について50減少、20増加、30増加という増減があったということになります。

後のために少しだけ符号の扱いについて取り決めておきます。

  • ある集計結果を ある箱の 増加分 であると定義する場合、移動の合計を計算した上で、当該の箱の増減値そのものを指すとします
  • ある集計結果を ある箱の 減少分 であると定義する場合、移動の合計を計算した上で、当該の箱の増減値から符号を反転した値を指すものとします

実装上では移動はタプルではなく { from: Box, to: Box, amount: Integer } の表現形式を使うので、これを足し合わせた結果に相当する集計結果は少しややこしくなりますが、下記の擬似コードのようなイメージで計算します。

N個目の箱の増加分 の場合

箱に入ってきた分(to)から出ていった分(from)を引きます

擬似コード例: moves.filter(m => m.to == N).sum() - moves.filter(m => m.from == N).sum()

N個目の箱の減少分 の場合

箱から出ていった分(from)から箱に入ってきた分(to)を引きます

擬似コード例: moves.filter(m => m.from == N).sum() - moves.filter(m => m.to == N).sum()

実装する

お金はInteger、そして箱は識別可能ならば十分であることを考えて、箱は単に文字列リテラルで表現するとしましょう。そうすると、さっそく移動から実装し始められます。

CREATE TABLE moves
( "from" TEXT NOT NULL
, "to" TEXT NOT NULL
, "amount" INTEGER NOT NULL
)
;

集計結果は、次のようなSQLで表現できます。

箱"foo"の増加分 の場合

SELECT
( (SELECT SUM(amount) FROM moves m WHERE m."to" = 'foo')
- (SELECT SUM(amount) FROM moves m WHERE m."from" = 'foo')
)

箱"foo"の減少分 の場合

SELECT
( (SELECT SUM(amount) FROM moves m WHERE m."from" = 'foo')
- (SELECT SUM(amount) FROM moves m WHERE m."to" = 'foo')
)

以上で実装の基本形は完了です!

ここまでの実装だけでは、一体何が実現できるのか全くわかりません。そこで、次から簡単な例をもとに模擬的なデータを構成してみます。

例①:3つの箱でP/L(損益計算書)を表現する

P/L損益計算書)とは、ある期間における収益と費用、そしてその差し引きの利益を表示した表のことです。

まずは、3つの箱 売上現金費用 を用意します。

次に移動データを使って、事業活動を次のルールで表現してみましょう。

  • 売上が発生した場合、 売上 から 現金 への移動 で表現する
  • 費用が発生した場合、 現金 から 費用 への移動 で表現する

以下はサンプルのデータです。 (摘要欄のようなカラムがないので、代わりにコメントを入れていますが、どのようなデータであるか何となくイメージできると思います。)

INSERT INTO moves("from", "to", "amount")
VALUES
  ('現金', '費用', 600) -- 諸々の初期費用がかかりました
, ('現金', '費用', 500) -- 仕入れがありました
, ('売上', '現金', 200) -- 200円のコーヒーが売れました
, ('売上', '現金', 300) -- 300円のクッキーが売れました
, ('売上', '現金', 200) -- 200円のコーヒーが売れました
, ('売上', '現金', 200) -- 200円のコーヒーが売れました
, ('売上', '現金', 500) -- 500円のケーキが売れました
, ('売上', '現金', 200) -- 200円のコーヒーが売れました
, ('売上', '現金', 300) -- 300円のクッキーが売れました
, ('現金', '費用', 600) -- 給料を支払いました
;

最後に、簡単なP/Lとして、収益、コストを集計結果として以下で定義します。

  • 収益 = 売上 の箱 の 減少
  • コスト = 費用 の箱 の 増加

使いまわしやすくするためにVIEWにしてしまいましょう。

-- 収益 = 売上 の箱 の減少分
CREATE VIEW sales(value) AS
  SELECT
  ( (SELECT COALESCE(SUM(amount), 0) FROM moves m WHERE m."from" = '売上')
  - (SELECT COALESCE(SUM(amount), 0) FROM moves m WHERE m."to" = '売上')
  ) AS value
;
-- コスト = 費用 の箱 の増加分
CREATE VIEW cost(value) AS
  SELECT
  ( (SELECT COALESCE(SUM(amount), 0) FROM moves m WHERE m."to" = '費用')
  - (SELECT COALESCE(SUM(amount), 0) FROM moves m WHERE m."from" = '費用')
  ) AS value
;

最後に上記の差し引きとして利益を算出してみます。結果を整形すると次のようになります。

収益1,900
コスト1,700
利益200

しかし、ここまでの話ならば、箱の移動などと面倒なことを考えず、単に売上と費用をそれぞれ別々に計上すればよさそうですよね。そこで次の例を紹介します。

例②:5つの箱でB/S(貸借対照表)を表現する

B/S貸借対照表)とは、ある時点における資産の内訳と資産の帰属する先(あるいは、調達方法)をそれぞれ左右に並べた表のことです。この表の左右それぞれの合計は一致(バランス)するべきで、資金の提供元(右側)から見て、資金が現在どういう状態になっているのか(左側)を表します。

前の例と同じ要領で、5つの箱 売上費用資産負債純資産 の箱を用意します。

事業活動の内容を次のように表現します。

  • 売上が発生した場合、 売上 から 資産 への移動 で表現する
  • 費用が発生した場合、 資産 から 費用 への移動 で表現する
  • (銀行などからの借り入れにより)資金の貸し手に帰属する資産が発生した場合、 負債 から 資産 への移動 で表現する
  • (資本金や株式による調達により)株主に帰属する資産が発生した場合、 純資産 から 資産 への移動 で表現する

以下はサンプルのデータです。

INSERT INTO moves("from", "to", "amount")
VALUES
  ('純資産', '資産', 10000000) --  事業開始のための初期投資
, ('負債', '資産', 5000000) -- 事業開始のための借り入れ
, ('資産', '費用', 3000000) -- 費用発生など
, ('売上', '資産', 1000000) -- 売上
, ('資産', '費用', 50000) -- 借り入れ利息の支払い
, ('資産', '費用', 1500000) -- 費用発生など
, ('売上', '資産', 1300000) -- 売上
, ('資産', '費用', 50000) -- 借り入れ利息の支払い
, ('資産', '費用', 1200000) -- 費用発生など
, ('売上', '資産', 2000000) -- 売上
, ('資産', '費用', 50000) -- 借り入れ利息の支払い
;

以上を用いて、ひとまずP/Lを出力してみましょう。実は、前の章のP/Lを出力するときのSQLから形を変える必要は一切ありません。

収益4,300,000
コスト5,850,000
利益-1,550,000

これに加えて、いわゆるB/Sを作ってみます。集計結果を次のように定義します。

  • 資産 = 資産 の箱 の増加
  • 負債 = 負債 の箱 の 減少
  • 純資産 = 純資産 の箱 の 減少

同じくVIEWを作ってみます。

-- 資産 = 資産 の箱 の増加分
CREATE VIEW assets(value) AS
  SELECT
    ( (SELECT COALESCE(SUM(amount), 0) FROM moves m WHERE m."to" = '資産')
    - (SELECT COALESCE(SUM(amount), 0) FROM moves m WHERE m."from" = '資産')
    ) AS value
;
-- 負債 = 負債 の箱 の減少分
CREATE VIEW liabilities(value) AS
  SELECT
    ( (SELECT COALESCE(SUM(amount), 0) FROM moves m WHERE m."from" = '負債')
    - (SELECT COALESCE(SUM(amount), 0) FROM moves m WHERE m."to" = '負債')
    ) AS value
;
-- 純資産 = 純資産 の箱 の減少分
CREATE VIEW equities(value) AS
  SELECT
    ( (SELECT COALESCE(SUM(amount), 0) FROM moves m WHERE m."from" = '純資産')
    - (SELECT COALESCE(SUM(amount), 0) FROM moves m WHERE m."to" = '純資産')
    ) AS value
;

これらの結果を集計すると、次のようになります。

資産13,450,000負債5,000,000
純資産10,000,000
資産合計13,450,000負債+純資産合計15,000,000

はい、バランスしていませんね。差額の1,550,000はどこにいるかというと、実はP/L側の利益にいます。P/Lで利益がマイナスであるということは、純資産が縮小しなければいけません。そこで、P/Lの利益は純資産に振り替える(一般には利益剰余金という純資産科目に)移動が必要があります。このためのルールを新たに2つ追加します。この移動は、実際に決算振替仕訳と言われています。

  • 売上を確定する場合、 純資産 から 売上 への移動で表現する
  • 費用を確定する場合、 費用 から 純資産 への移動で表現する

売上と費用を確定させるため、P/Lの結果を参照しながら次のようなデータを作成します。

INSERT INTO moves("from", "to", "amount")
VALUES
  ('純資産', '売上', 4300000) -- 決算振替仕訳による売上の確定
, ('費用', '純資産', 5850000) -- 決算振替仕訳による費用の確定
;

上記を追加した上で、改めてP/LとB/Sを計算してみると、次のようになります。

P/L

収益0
コスト0
利益0

B/S

資産13,450,000負債5,000,000
純資産8,450,000
資産合計13,450,000負債+純資産合計13,450,000

これでB/Sがバランスしました。一方でP/Lが0になってしまいました。 変に思うかもしれませんが、もし再度P/Lを表示したい場合には、決算振替仕訳を含めないで集計すればいいだけなので、P/LとB/Sを表示するという観点からいえば、これで十分といえるでしょう。

複式簿記との関係性

実は、この記事で定義した 移動 が複式簿記における 仕訳 と同じものになります。さらに 移動元移動先 はそれぞれ 貸方借方 に相当します。(貸し手から借り手への移動だと考えると、分かりやすいですよね)

通常の簿記ではより細かく勘定科目が分かれているものですが、単に箱を細分化していくだけで同じことを説明することができます。

さらに、 貸借平均の原理 とよばれる次の式があります。

資産 + 費用 = 負債 + 純資産 + 収益

ここで実は、左辺に来ているものは、箱の 増加 分で、右辺に来ているものは、箱の 減少 分として本記事において定義したものでした。減少分であるということは符号を反転させれば増加分を表しているわけなので

資産 + 費用 + (-負債) + (-純資産) + (-収益) = 0

と書き換えると、これは各箱からの(広義の)移動があったとしても、お金が増えたり減ったりはしないという制約条件そのものを指していると言えます。

まとめ

それで、一体このアーキテクチャのなにが嬉しいのでしょうか?まず一つが、P/LもB/Sも、この仕訳の集合体を唯一の入力として写像できるという点にあります。今回例には載せていませんが、集計の仕方や箱の設定を工夫するだけで、例えばP/Lといっても発生主義・実現主義・現金主義など、それぞれ微妙に異なる見方に写像できたりします。このように、入力データのシンプルさを保ちつつ、多くのアウトプットに対応することができるのが強い利点になります。もう一つは一貫性を持てることです。売上の帳簿と財布の帳簿がそれぞれ別々のデータだと、売上の発生と財布への追加が一致しているようにデータを担保しないといけませんが、全てにおいて移動を根拠にしている場合はそのような心配をする必要はありません。

実は、複式簿記の本質は、開発者にとっては単に自分自身で会計処理をする際の理解をスムーズにできることだけではなく、一つのデータ表現手段として他の領域にも設計上応用できる可能性を秘めていると思います。例えば、お金以外の定量データの場面や、フローとストックの表現が必要な場面などがあります。

さいごに

この記事で紹介している実装をsqliteで実行可能な形でGitHub上に公開しておきました。 GitHub - mizukami234/simple-DEB

DIGGLEでは、管理会計という(複式簿記とは遠からずも近からずな)領域において、最高のプロダクトを作るメンバーを募集しています。興味がありましたら、是非お声がけください!

herp.careers

自前のReact UI コンポーネントライブラリを Tailwind CSS で再構築した話

"The logo for the React open source JavaScript library" © Facebook (Licensed under CC BY 1.0)
"Tailwind CSS Logo" © Tailwind CSS (Licensed under CC BY 4.0)

この記事はReact Advent Calendar 2023 23日目の記事です。

はじめに

弊社DIGGLEではフロントエンド開発に向けたUIコンポーネントライブラリ*1を用意しています。

開発者はUIコンポーネントライブラリ上に用意されているコンポーネントを用いてUIを構成することができるため、 作業の効率化や一定以上のデザインの一貫性を担保できます。 私自身用意されたコンポーネントベースにUIの構築を行うことでデザインに関しての悩み事が削減でき、より本質的な部分について取り組むことができていると感じています。

UIコンポーネントライブラリはDIGGLEの持っているデザインシステムの一部として、デザイナーとエンジニア両名にとって重要な役割を担ってくれています。

以前まで、DIGGLEではそのUIコンポーネントライブラリをSemantic UI Reactをベースに組んでいました。 react.semantic-ui.com

ですがシステムや組織が大きくなるにつれて、Semantic UI Reactでは対応が難しい部分が見えてきました。 現在その難しい部分の解消に向けてデザインコンポーネントをTailwind CSSを用いて刷新する取り組みを行なっています。

tailwindcss.com

今回は、なぜDIGGLEではUIコンポーネントライブラリのベースをSemantic UI ReactからTailwind CSSに移したのか?という点についてお話しさせていただきます。

Semantic UI ReactベースのUIコンポーネントライブラリについて

Semantic UI React は Semantic UI というCSSフレームワークをベースに構築されたUIコンポーネントライブラリです。

semantic-ui.com

DIGGLEにおいての利用方法は、基本Semantic UI ReactをベースにDIGGLEでの使い方にマッチするよう薄くラップしたり、UIで頻出するコンポーネントを用意しておくといったものでした。

DIGGLEでの使い方にマッチするよう薄くラップ

Semantic UI ReactはとてもよくできたUIコンポーネントライブラリではあるのですが、 汎用的なものである以上細かい部分などどうしてもDIGGLEとマッチしていない部分があります。

そのためDIGGLEでは薄くラップする形でカスタマイズをしています。 どのようなカスタマイズを入れていたかについて具体的な例を列挙したいと思います。

DIGGLEではDropdownをさまざまな場面で利用しています。

react.semantic-ui.com

Semantic UI ReactではDropdownコンポーネントも用意してくれており、 Semantic UI ReactのデフォルトではtrueになっているselectOnNavigationというpropsのデフォルトを、DIGGLEでは false に変更して利用しています。

こちらのパラメータはDropdownを開いている際に矢印キーを押すと、矢印で移動した先を選択状態にするかどうかというパラメータです。 DIGGLEではDropdownで選択が行われるとすぐにAPIを叩き、選択内容を反映するUIを多く採用しています。その影響で、selectOnNavigationtrueになっているとユーザーが選択のために矢印キーを連打すると連打した分だけ反映のAPIを叩いてしまいます。

そのため多くの場合はselectOnNavigationというpropsをfalseに変更して利用しています。 コンポーネントを利用する側で対応することもできますが、新規参画していただいた方や久しぶりにDropdownコンポーネントを触る際に間違ってselectOnNavigationtrueのままにしてしまわないようUIコンポーネントライブラリ側でデフォルトの値を変更しています。

TextAreaの高さを自動で調整できるように変更

説明やコメントなど長い文章を書く際にTextAreaを活用しています。

react.semantic-ui.com

Semantic UI Reactで用意されているTextAreaは高さを指定して利用するためユーザーの入力に応じてコンポーネントの高さを変えることができませんでした。

DIGGLEではユーザーの入力に応じて高さを変えたい場合はreact-textarea-autosizeライブラリを利用するよう変更しています。

www.npmjs.com

UIで頻出するコンポーネントを用意

テーブルのヘッダーを固定

DIGGLEでは巨大なテーブルを扱う機会が多く、UXの観点からテーブルのスクロール時に行/列を固定したいことがありました。

固定の方法も2行まとめて固定したい、ヘッダーとフッター両方を固定したいなど多岐に渡ったため、Semantic UI ReactのTableを拡張して複雑な固定をできるコンポーネントをSSTable(Sugoi Scroll Tableの略。記憶に残るコンポーネント名で弊社エンジニアのセンスが光ります)という名前で用意していました。

SSTable

Semantic UI React が だんだん DIGGLE にマッチしなくなってきた話

色々なカスタマイズを加えながら大切に育てていたUIコンポーネントライブラリですが、だんだんとDIGGLEの方向性にマッチしなくなってきました。

だんだん表現したいこととSemantic UI Reactで整合性が取れなくなってきた

DIGGLEのUIはSemantic UI Reactのデザインをベースに構築していたのですが、 P/L ( 損益計算書*2 ) を表示するレポートのデザインなど一部Semantic UI Reactを一切使わないコンポーネントが現れました。

つまり、DIGGLEが表現したいUIの中にSemantic UI Reactの表現ではカバーできないコンポーネントが現れ始めたのです。

カスタマイズが複雑になるコンポーネントが増えてきた

DIGGLEで実装したいUIを実現するために複雑なカスタマイズが必要なコンポーネントが出てきました。

例えば、計算式に関わるコンポーネントです。 DIGGLEではさまざまな場面で計算式を利用することができ、 計算式を使うことで勘定科目A勘定科目Bの月次の値を表示した行の下に A+B の計算結果の行を表示させるといったことができます。

こちらの機能はとても活用いただいているのですが、実装時にはユーザーにどういったUIで入力をしていただくのか?という点が問題になりました。 結論としては、Semantic UI ReactのDropdownをカスタマイズで表現したのですが、結果として複雑なカスタマイズが必要になりました。

複雑なカスタマイズは保守性を下げてしまいますし、そもそもSemantic UI Reactで表現することが適切なのか?という疑問を抱かせることになりました。

Tailwind CSSベースのUIコンポーネントライブラリについて

Tailwind CSSはSemantic UI と同様にCSSフレームワークと呼ばれるフレームワークです。

tailwindcss.com

A utility-first CSS framework packed with classes like flex, pt-4, text-center and rotate-90 that can be composed to build any design, directly in your markup.

と記載されている通り「ユーティリティファースト」なCSSフレームワークであり、flex, pt-4, text-centerといったTailwind CSS側が用意したユーティリティクラスを利用することでスタイルを適用することができます。 Tailwind CSSを既存CSSと比較した際の有用性については色々な記事で言及されています*3

DIGGLEでは Tailwind CSSを使うに当たって Tailwind UIの実装をベースにしています。

tailwindui.com

Tailwind UI はまとまったコンポーネントを提供しているわけではなく、Tailwind CSSとHeadless UIを組み合わせてどのようなUIコンポーネントが構築できるかを見せてくれるものになっており、各種コンポーネントのページでは基本的なhtmlタグとユーティリティクラスを組み合わせたコードを見ることができます。

例えば、バッジでは下記のコードを見ることができます。

<span class="inline-flex items-center rounded-md bg-gray-50 px-2 py-1 text-xs font-medium text-gray-600 ring-1 ring-inset ring-gray-500/10">Badge</span>

そのため、UIコンポーネントライブラリ側では上記のコードをコピーしつつ、DIGGLEにマッチするよう調整するような形になります。 Semantic UI Reactよりも自前で実装する必要がある部分が増えるものの、同時にカスタマイズ性が上がっています。

自前実装のコストを払ってカスタマイズ性を上げることで、よりDIGGLEの求めるUIを実現できるようになります。 さらに、基本的なhtmlタグとユーティリティクラスを組み合わせたものをベースとしているため計算式を入力するためのDropdownを作る際にも、 ライブラリ側の実装に振り回されるといったことなく、シンプルな実装で実現できるようになると思っています。

Tailwind CSS を使うにあたって工夫している部分

Tailwind CSSが現在のDIGGLEの方向性に合っていそうではあったのですが、Tailwind CSSはそのまま使うには自由度が高すぎました。

そのため一旦下記の運用を行なっています。

ユーティリティクラスの分割

Tailwind CSS を使うと発生する問題の一つに、ユーティリティクラスの肥大化があると思います。

DIGGLEもすでにその問題に当たっており、先ほどのバッジではどのユーティリティクラスがどういった影響を与えているのかをぱっと見で判断することは困難です。 ReactコンポーネントのPropsで見た目を後から調整しようと思った際に、想定と異なるスタイルに影響が出てしまう恐れがあります。

そのため、各種コンポーネントのpropsに注目してユーティリティクラスの切り分けを行うようにしています。

const getSizeClasses = (
  size?: 'x-small' | 'small' | 'large' | 'medium' | 'x-large'
) => {
  switch (size) {
    case 'x-small': {
      return 'px-2 py-1 text-xs';
    }
    case 'small': {
      return 'px-2 py-1 text-sm';
    }
    case 'large': {
      return 'px-3 py-2 text-sm';
    }
    case 'x-large': {
      return 'px-3.5 py-2.5';
    }
    // "medium"
    default: {
      return 'px-2.5 py-1.5 text-sm';
    }
  }
};

lintによるユーティリティクラスの順番整理

ユーティリティクラスを人間が綺麗に管理するには限界があります。

順番などを定めようものならコードレビューのたびに間違いがあるかもわからない間違い探しが始まってしまいます。

DIGGLEではprettierとeslintのルールを導入することである程度はlinterに任せるようにしています。

github.com

github.com

まとめ

今回はUIコンポーネントライブラリを置き換えるにあたって、Tailwind CSSを採用しました。 DIGGLEを運用していく中で求めるUIコンポーネントの姿がSemantic UI Reactを導入したタイミングよりもクッキリと見えるようになったことでTailwind CSSの採用に踏み切りました。 そのため、Semantic UI Reactを採用した以前の意思決定にも間違いはないと思っています。

これからTailwind CSSを本格的に運用していく中でさまざまな問題に直面すると思いますが、フェーズごとに適切な対応が何かを模索することでユーザーにより良い価値を提供できると信じて取り組んでいこうと思います。

この記事が、プロジェクトのUIコンポーネントライブラリの構築に悩んでいる方の一助となれば幸いです。

DIGGLEのエンジニアのitoがお送りしました。

We're hiring!

DIGGLE では共にプロダクトを開発してくれるエンジニアを大募集中です。

少しでも興味があれば、ぜひ下記採用サイトからエントリーください。
カジュアル面談も実施しているので、気軽なお気持ちでご応募いただければと思います!

herp.careers

*1:UIコンポーネントライブラリは、UIを実装する際に利用できるReactコンポーネント群のことを指しています。GitHubでいうPrimerのようなものを社内向けに構築/公開しています。

*2:DIGGLEは管理会計のSaaSのため、ここでの損益計算書は変動損益計算書などの管理会計向けのものになります。

“変動損益計算書”の確認が優良企業への第一歩 | 情報誌「戦略経営者」 | 経営者の皆様へ | TKCグループ

*3:https://udemy.benesse.co.jp/development/system/tailwind-css.html