LayerX エンジニアブログ

LayerX の エンジニアブログです。

多種多様な会計ソフトと接続するためのSaaSの作り方

こんにちは。LayerX エンジニアの花村(@naomasabit)です。

プレスリリースでも多く出していますが、請求書処理を行うLayerX インボイスは多くの会計ソフトと連携しています。他ソフトウェアと連携するという機能は複雑性を伴いますが、この記事では私が会計ソフト連携部分の開発を実施してきてぶつかった課題、と対応してきた内容をご紹介します。

多くの会計ソフトと連携しているLayerX インボイス

f:id:naomasabit:20210518114604p:plain

具体的な連携内容は

  • 会計ソフトからマスタデータの取り込み
  • そして会計ソフトへの仕訳データの取り込み

です。「仕訳」とは経済活動を一定のルールで表した概念で、例えば 5/1に銀行の普通預金から現金を10,000円引き出したら「5/1 現金 10,000 普通預金 10,000」といった形式で記述します。実際には会計ソフトによって仕訳のフォーマットは多種多様であり、環境が異なるため、ユーザーの利用ソフトに合わせて多くの連携を考える必要があります。

しかし、会計ソフト対応といっても一筋縄にはいきません。これまで多くの会計ソフトに対応する中であたった

  • 課題1. 会計ソフトが持つマスタがそれぞれ異なる
  • 課題2. 仕訳出力フォーマットの違いによる実装パターンの増加への対応
  • 課題3. チーム内における仕様理解の混乱

について、それぞれへの対応を述べていきます。

課題1: 会計ソフトが持つマスタがそれぞれ異なる

基本的に、「勘定科目」、「部門」といったマスタデータはどの会計ソフトも持っているのですが、会計ソフトによって保持するマスタが異なり、例えば「取引先」は会計ソフトごとに持っていたり持っていなかったりします。また、「補助科目」という勘定科目をより細かく分類するためにつけるグループについては、freeeでは同様の役割を「品目」や「メモタグ」等の概念で表すなど、保持マスタは千差万別です。

対応: 地道な調査による対応と、マスタの利用設定を管理する仕組みの提供による対応

これらはまず、地道な調査による会計ソフトが持つマスタ群の理解を行いました。

f:id:naomasabit:20210517112014p:plain
マスタ早見表

会計ソフトが持つマスタ群についてそれぞれ理解し、上記のような早見表にしています。(内容は伏せます)会計ソフトのドキュメントに加えて、実機の動作を確認して調査します。調査して仕様を切る人と実装者が別々だとスピードが遅くなってしまうので、実装者自身が調査してクイックに実装しています。

調査した結果を環境に反映するために、マスタの利用設定を管理する機能を用意しています。LayerX インボイス利用ユーザーの環境毎にfreee利用ユーザーではこのマスタとこのマスタを利用する、マネーフォワード利用ユーザーではこのマスタとこのマスタを利用する等、対応する会計ソフト毎に設定できる実装にしています。

課題2: 仕訳出力フォーマットの違いによる実装パターンの増加への対応

仕訳出力フォーマットも大きく異なります。会計ソフトにインポートするために、さまざまなフォーマットでの出力対応が必要です。対応する会計ソフトが増えるごとにそれぞれフォーマットが必要です。

なお、@yyoshikiがfreeeのAPIライブラリを先日公開しましたが、こちらのようにLayerX インボイスが仕訳APIも連携している会計ソフトについては別の実装になるのですが、ここでは主にCSV出力による連携を述べます。

対応1: 仕訳テンプレートを定義したjsonの追加だけで仕訳出力の対応を可能にしてスケールしやすくした

f:id:naomasabit:20210518114637p:plain

当初は会計ソフトに合わせた加工のコードを書いていたのですが、対応会計ソフトが増える毎に実装が増えていき、スケールしづらくなってきました。そこで現在は、様々な仕訳フォーマットに対応するために、仕訳出力テンプレートをjson形式で設定し、マッピング・加工して出力するようにしています。これにより、新しい会計ソフトへの対応が必要になった時も、新しくjsonを定義するだけで新しい会計ソフトの仕訳出力に対応できます。

対応2: 横展開可能な仕訳テストの標準化による品質とスピードの両立をした

f:id:naomasabit:20210519093524p:plain

定義した出力が正しいかを確認する必要があります。LayerX インボイスでは、会計ソフト横断で同一のインプットデータを用意し、それぞれの会計ソフトフォーマットに合わせたCSVが出力されるかというに期待値テストを行います。

f:id:naomasabit:20210517120546p:plain

GoではTable Driven Testを使ったテストがメジャーですが、仕訳構造のような複雑なデータはコード内に書くと可読性が下がるため、図のようにCSVで期待値を定義して、こちらを読み込むようにしています。CSVで書くことで、数十列あるような仕訳フォーマットでも期待値を作りやすくしています。

また、同一ケースを使い、仕訳を組み立てて比較する部分は共通モジュールにしているので、新しい会計ソフトが出てきても以下のような数行を書けば横展開できるようにしています。

// ほげほげ会計ソフトのテスト実装
func TestJournalsHogeHogeAccountingSoft(t *testing.T) {
  // 期待値のCSVを読み込んで共通の型にする共通関数
  expectedJournals := importExpectedJournals(ExpectedHogeHogeAccountingSoftCSV)
  var eTests = setTestDataTable(expectedJournals)
  // テストを行う関数
  testExpenseCase(t, model.HogeHogeAccountingSoft, eTests)
}

課題3: チーム内における仕様理解の混乱

いくつもの会計ソフトに対応していることで、チームメンバーの仕様理解にハードルがいくつもあります。メンバー間で知識差分があると意思決定のスピードが遅くなり、精度も悪くなりますし無駄な体力を使うことになります。

対応: 各会計ソフトの知見をPlaybookとして整備し、チーム内へ知見の共有

詳細仕様について会計ソフトを理解するためのPlaybookを作成し、共有できるようにしています。

f:id:naomasabit:20210518113340p:plain

上記はPlaybookの一部抜粋で、勘定奉行シリーズについて書いています。もっとも、Playbookだけでは理解に繋がらず、実機を触ってもらわないと理解が浅く終わってしまうため、定期的に実機を触ってもらうような取り組みを始めています。

まとめ

f:id:naomasabit:20210517123258p:plain
まとめ

多くのソフトウェアと繋がるソフトウェアは考慮事項が多く複雑になりがちです。1つ1つ課題を解きほぐして、打ち手を立てていくことでこれまで対応してきました。ユーザーに合わせた環境提供の柔軟性、仕訳出力の共通化と品質向上施策など実装に加え、チームメンバー間でドメイン知識の差分をなくすための仕組みやドキュメントなども整理しています。実装、チームへのナレッジ共有とバランス感が求められる仕事をしています。

このようなSaaSの課題を解いていきたい方、また会計についても学んでいきたい知的好奇心の強い方、ぜひ下記のリンクからご応募ください。お待ちしております。

herp.careers

最後までお読みいただきありがとうございます。

追記: 図中のGopherアイコンはTakuya Ueda(@tenntenn)さん作のものを利用させていただきました。当初作者表記が漏れていたのをご指摘いただき、修正追記いたしました。

AI-OCRを支える非同期処理アーキテクチャ

こんにちは!LayerXエンジニアの高際 @shun_tak です!

この記事では、LayerX インボイスの請求書AI-OCRを支える非同期処理の仕組みについて解説したいと思います。

いきなりサマリーですが、今回お伝えしたいのは以下の2点です。

  • 請求書は突然大量にアップロードされるので(大歓迎です!)、Amazon SQSとGoの machinery を活用して非同期処理しているよ!
  • AI-OCRの処理は重たいけど、AWS Lambdaを活用してシステム全体の負荷を分散し、スケーラビリティと可用性を確保し、コストも抑えることができたよ!

では早速ですが、前回のブログ LayerX インボイスにおける請求書AI-OCRの概要 の復習です。LayerX インボイスの請求書AI-OCRは、以下の図のように複数の処理によって構成されています。

AI-OCR処理フロー

図にするとあっさりしてますが、前処理も後処理も複数の処理で構成され、その中にはOCRの処理だけでなく、重たい画像処理等も含まれています。そのため、もちろん速度改善に努力してはいるものの、全部の処理が終了するまで請求書1件あたり数秒かかります。

このような重たい処理をアップロード処理と同期的におこなってしまうと様々な弊害が現れるため、処理の一部をメッセージキューとジョブワーカーを活用した非同期処理に分離しています。

ちなみにLayerX インボイス関連サービスにおいては、メッセージキューとしてAmazon SQSを、メッセージの送受信やジョブワーカーの管理にはGoの machinery というライブラリを利用しています。このあたりはまた別の記事で解説したいと思います。

アーキテクチャ概要

以下の図は、AI-OCRにまつわる処理を管理する様子を簡単に示したものです。

AI-OCRアーキテクチャ概要

この図には、APIサーバーとジョブワーカーの2種類のサーバーが存在します。これら2つのサーバーはともにSQSにメッセージを送信します。

一方で、メッセージを受信して処理するのはジョブワーカーだけです。APIサーバーはユーザーアクション起因で動きますが、ジョブワーカーはイベント駆動で動きます。

APIサーバーはストレージへの請求書アップロードが成功すると、メッセージをキューに送信し、一旦クライアントに成功レスポンスを返します。ジョブワーカーは受信できるメッセージがないかキューを監視しており、メッセージを受信すると該当のタスクを実行します。

最初のタスクには処理の最後に新たなメッセージをキューに送信するような実装をしておくことで、AI-OCRを構成する処理が次々と動くようになっています。最後のタスクはキューにメッセージを送信しないため、そこで一連の処理も終了します。

より詳しいAI-OCRの処理の様子を次節で紹介します。

AI-OCRの処理を支える具体的な構成

以下の図はAI-OCRの処理をさらに具体的に図示したものです。アイコンがたくさんあって複雑に見えるかもしれませんが、やってることは先程の概要図で示したものの繰り返しです。

AI-OCRアーキテクチャ詳細

1クライアントから大量の請求書が同時にアップロードされることは日常茶飯事で、ジョブワーカーは大量のタスクを管理しなければなりません。そのタスクの管理だけでも相当負荷が上がります。

一方で、請求書のアップロードタイミングやそのボリュームは分単位では予測不可能なため、ジョブワーカーに重たい処理を担わせるとオートスケーリングでも負荷分散が間に合わなくなります。

そのため、汎用OCRの実行やデータベースへの結果の保存など、ジョブワーカーでタスクが完結することもありますが、前処理、ラベル検出、一部の後処理はAWS Lambdaにオフロードしています。

データベースアクセスなしで動く重たい機能(特に画像処理)は、並列起動が可能で可用性が高く使った分だけ課金されるAWS Lambdaを利用することで、システム全体の負荷削減、スケーラビリティ向上、可用性の確保、コストの最適化を実現することができます。一石四鳥!

エンジニア募集中!

AI-OCRはうまく運用できてるように見えるかもしれないですが、まだまだ改善したいことがたくさんあります。

Go machinery じゃなくてAWS Step Functions使えばいいじゃんって思ったそこのあなた!ぜひ下記のリンクからご応募ください!面接やカジュアル面談にてディスカッションさせてください!

herp.careers

https://herp.careers/v1/layerx/71h6Nu8eSDYC

最後までお読みいただきありがとうございました!

【イベントレポート】DX Tech Talk #2 「難しいことに挑戦するから面白い」開発組織のリアル_CADDi×LayerX

LayerXの石黒(@takaya_i)です。LayerXでは採用や組織づくり、広報の面から開発チームをサポートしています。

今回は、CADDiとLayerXの共催で4月28日にオンラインで実施した【DX Tech Talk #2 「難しいことに挑戦するから面白い」開発組織のリアル】のオンラインイベントをレポートします。開発組織の課題感や、向き合う課題について興味をお持ちいただくきっかけになれば嬉しいです。

layerx.connpass.com

記事の最後に、当日の動画(YouTube)リンクもありますのでぜひご視聴ください。

イベント概要

DX Tech Talk とは

「もう一つのDX」と呼ばれる、DX(=Developer eXperience)を題材に、開発組織やTechnologyのトークが大好きなゲストをお迎えし、Developer eXperienceを中心にトークするイベントがDX Tech Talkです。

今回は「重い産業」のDXに挑戦するCADDiとLayerXの共催で、下記2つのトークセッションに分けてそれぞれ対談を行いました。(2021年4月28日 オンライン開催) Talk Session 1 「開発組織のリアル」(開発メンバー対談) Talk Session 2 「難しいことに挑戦するから面白い」(CTO対談) 100名を超えるご応募をいただき、急遽参加枠を増やすなど、好評をいただいたイベントとなりました。

f:id:t_1496:20210514181137p:plain
パネラーの様子。雰囲気良くtalkが進みました

本エントリでは、当日の様子を各セッションのハイライトと共にお届けします。

当日の資料

speakerdeck.com

Talk Session 1 開発組織のリアル

事前に寄せられた質問に、大原(CADDi・フロントエンドエンジニア)、花村(LayerX・シニアアーキテクト)が回答しました。

Q1:CADDi / LayerXに入ったきっかけは?

Q1:入社背景
ぶっちゃけCADDi / LayerXに入ったのはなぜ? どんな形で興味を持ったのですか?

大原:CTOのアキさん(小橋)に声をかけていただいて、(入社前から)開発に関わらせてもらっていたのがきっかけですね。

石黒:自身が(キャリアを)広げていきたい方向と入社の理由がマッチしたんですね。

花村:もともとブロックチェーンが大好きで、ブロックチェーンを用いてビジネスのコアな部分をやる、というところに興味を持ちました。テクノロジードリブンで、技術をベースに業務の生産性を上げる、というところを考えられるのが個人的にマッチしているなと思います。

Q2:toB特有の面白さとは?

Q2:toBとソフトウェアエンジニアリング
『toB』特有の面白さをソフトウェア・エンジニアリングの観点からお聞きしてみたいです

花村:(toC、例えばゲームなどは理想を描いてそれを現実世界に実装しやすいという理解に対し)toBは、(経理SaaSに関して言えば)例えば会社ごとに会計処理が異なったりと、トップダウン的に理想を考える力と、ボトムアップ的に実務から作っていく、という両面をフィットさせるのが面白いところだし、toB特有だと思いますね。

大原:製造業はデジタル化があまり進んでいないという現実から、いかにひとつずつステップアップして使っていただく(プロダクトを作る)かを考えるのが面白いところですね。

Q3:開発組織のリアル、Q4:開発組織の特徴 など

Talk Session1後半では、以下の質問にも回答しました。(詳細は本記事下部のアーカイブ動画にて公開中です)

Q3:開発組織のリアル
どんなメンバーが多いですか? 「人が増えたらやりたいこと(=課題)」をお二人の目線で教えて下さい

Q4:開発組織の特徴
開発組織の特徴、ユニークなポイントを教えて下さい また、それが顧客にどのような価値として還元されているか教えて下さい

その他、当日に参加者から以下の質問をいただき、それぞれ回答しました。

  • toBだとUI/UXの重要性があると思うが、それを開発メンバーに意識させる、もしくはチームみんなで意識していることは何か?
  • お二人が一緒に働く方に求めるものは何ですか?

Talk Session2:CTO対談「難しいから面白い」

事前に寄せられた質問に小橋(CADDi・CTO)、松本(LayerX・CTO)が回答しました。

Q1:お互いの会社、どう見えていますか?

Q1: お互いの会社、どう見えていますか? 「ここは盗みたい」「ここはもっと深堀りして聴いてみたい」所があればお話ください

小橋:LayerXとは1年前にもイベントをさせていただいて、「古き良き産業」と向き合ってどんどんシフトしているな、という印象です。そんな中で、どうやってアジリティを担保しているかをぜひ伺いたいですね。小さなチームで、大手(企業)と向き合っていて、(社内やチームは)どういう雰囲気なのか、といったところも深堀りたいなと。

松本:ひとつは、とにかくみんなが何でもやるんです。エンジニアが証券外務員を取ったり、簿記を取ったり。その環境が面白いなと思います。「みんなが柔軟に何でも取り組む」というのが、何かを検証するとき、細かく施策を動かすときに重要な手段の一つになっていますね。これがアジリティの源泉になっているなと思います。 もうひとつは、とにかくデモドリブン開発をしています。「動くもので会話しよう」という文化が強いですね。「とりあえず作って、違ったら捨てる」というアクションの中で、ファクトを拾って、意思決定するのが強みだなと。

松本:CADDiは、創業時から製造業にある種の勘所を持っていて、そこにまっすぐ向かって組織作りをしてきていますよね。発信に揺るぎない自信を感じるし、尊敬しています。組織設計をどうやっているか伺いたいです。

小橋:(製造業界も広いので)私たちも決して全てわかっているわけではなく、新しい領域に入るたびに全く違う困難に向き合っています。常に事業拡大する中で、学ぶというのは大事にしています。

Q2:伝統産業/伝統業務の難しさ

Q2: ソフトウェアは伝統産業/伝統業務をどう変えていますか? どんな時に「ソフトウェアの力」を感じますか?

松本:自分たちがユーザーになる機会が少ないので、まず当たり前を知るところからスタートですね。資格を取ったり、勉強をして、(ユーザーと)同じ言葉を話せるようになって、ようやくいいプロダクトが設計できる、といった感じですね。

小橋:そうですね。私たちがユーザーでないことが多いですね。かつ伝統産業は、UXの考え方が違いますね。紙とFAXとか。(そのUXの良さも理解しているので、)それをリプレイスするのか補助するのか、といったところが重要かなと。

Q3:CTOの時間の使い方、Q4:これからの「むずかしさ」や「面白さ」について

Talk Session2後半では、以下の質問にも回答しました。(詳細は本記事下部のアーカイブ動画にて公開中です)

Q3:CTOの時間の使い方 今はどのような時間の使い方をしていますか? 将来的にどのようにその時間の使い方を変えていきたいですか?

Q4:これから これから「むずかしさ」はどのように変化していきますか? 今のフェーズでジョインする面白さ、これからの面白さを教えてください

最後に

以上、イベントの様子についてお届けしました。 今後のイベントについては随時LayerXのconnpassページにUP予定です!(https://layerx.connpass.com/

アーカイブ動画はこちら

www.youtube.com

一緒に働く仲間を募集しています!

herp.careers

「時間・学び・セキュリティ」新規プロダクト開発で意識したことーAWSイベント登壇を終えてー

こんにちは、LayerXでインフラを担当している高江です。

今回は、ちょうど今週に開催されていたAWS Summit Online 2021に登壇させていただいた内容と、その事前イベント的な位置付けで先月の27日に行われたAWS Startup Communityのイベントでお話させていただいた内容について、要点をまとめてお伝えできればと思います。

AWS Startup Tech Meetup Online #4

まずは先月末にこちらのイベントに登壇させていただきました。

aws-startup-community.connpass.com

ここでは、LayerX インボイスのインフラを開発する上で、開発初期のコンセプト段階から正式リリースまでの間にどのような方針に基づいてどのように設計・開発したかをご紹介しました。

コンセプト段階では 開発 → ユーザーテスト → 学習 → 開発 のイテレーションを素早く回すことが重要で、その過程で作っては壊しを繰り返すため、とにかくシンプルに保つことを意識しました。
ここでお伝えしたかったポイントは以下です。

  • 機能不足やUI/UXが貧弱だと思っていたとしても我慢する。作り込みすぎない
    • いろいろ試して学びを得ることが最優先
    • 作るべきものがどんどん変わっていくので、作り込んでも無駄になる可能性が高い
  • 開発・運用コストを積極的に下げる
    • AWSのマネージドサービスや各種SaaSを活用して本質ではない部分(ノンコア領域)にコストをかけない
    • 時間をお金で買ってコア領域の開発に注力する
  • 開発したソフトウェアは資産としてストックする
    • 開発が進むほどその資産を流用することでレバレッジを効かせることができる
    • 「結果的にたまたま流用できた」ではなく「意図的に流用しやすくする」ためにプロジェクトとして『型』を持つ
    • ここでいう『型』とは、プロジェクトテンプレートやボイラープレートと呼ばれるようなもの

とにかく時間をうまく節約してコア領域の開発に集中することが一貫した方針となっています。 具体的なインフラ構成の変遷についてはイベントでご説明しておりますので、詳しい内容にご興味のある方はぜひイベントの様子や当日の資料をご覧ください。

www.youtube.com

speakerdeck.com

AWS Summit Online 2021 事例セッション

続いて、5/11-12に開催されたこちらのイベントにも登壇させていただきました。(視聴には登録が必要)

summits-japan.virtual.awsevents.com

資料はこちらです。

speakerdeck.com

こちらのセッションでは、正式リリースから約2ヶ月後の収録時点におけるインフラ構成について、CI/CDパイプラインやAWSマルチアカウント運用などについても取り上げてご紹介いたしました。

マネージドサービスやSaaSを活用したり、技術選定など開発チームが最高のパフォーマンスを発揮できるための意思決定をするといった点は、前述のTech Meetupでお話した時間を重視するというところにつながりますが、加えてこのセッションでお伝えしたかったポイントは以下です。

  • セキュリティは初期段階から手を抜かずにきっちり設計・実装する
    • 後付けでセキュリティ対策しようとすると、アーキテクチャーやデプロイパイプラインなどに大幅な変更が発生する可能性が高い
    • せっかく学びを得てPMFを達成してアクセルを踏む段階に辿り着けても、本番運用に入るために停滞してしまうのは非常にもったいない
    • 極端な話「明日本番リリース」と言われても対応できるように日頃から準備しておく

一通りの過程を経て感じるのは、やはりセキュリティは最初から対応しておいてよかったということです。
仮にアプリ開発だけに集中してセキュリティは特にケアしなかった場合を想像すると、今と同じレベルの構成にするために1ヶ月程度は追加でかかっただろうなと思います。
PMFに近づくほど、試用してくださっているユーザー様の期待値も上がるので、「今すぐ使いたい」と言ってくださる声に対してタイムリーに応えられるかどうかはスピードが命のスタートアップにとって非常に重要です。
そのためにも、最初から本番運用を想定してセキュリティ対策をするというのは欠かせないポイントになります。

まとめ

まとめのまとめみたいになりますが、一連のイベント登壇を通してお伝えしたかったことは以下です。

  • 時間は最も貴重なリソース。他社にお任せできるところはお任せして、自分たちはやるべきことに集中
  • 学びを得るのが最優先。色々手を入れたくなる気持ちはよく分かりますがぐっと我慢してシンプルに
  • セキュリティは最初から手を抜かない。後から付け足すのは最初から組み込むより何倍もコストがかかる

最後になってしまいましたが、このような機会を与えてくださったAWS様やコミュニティの皆様に改めて感謝申し上げます。

We are hiring!!

幸いにもこれまで大きなインシデントはなく、安定して稼働させることができておりますが、まだまだやることは山積みです。
0 → 1 の段階を経て 1 → 10 に向かうこれからのLayerX インボイスについて、エンジニアとして力を奮ってみたいという方、絶賛募集中です。
もちろんインフラに限ったものではなく、フロントエンドやバックエンドな方々もお待ちしておりますので、少しでもご興味のある方はぜひご連絡ください!

herp.careers

freee API のGoクライアントライブラリを公開しました

DX事業部の @yyoshiki41(中川佳希)です。 現在は主に、LayerX インボイス という経理業務を行う方を対象ユーザーにした SaaS プロダクトを開発しています。

先日、会計freee API のGoクライアントを公開しました 🎉 github.com 今回はそのGo実装内部について紹介していきます。

会計ソフトとLayerX インボイス

LayerX インボイスは、以下のような業務をサポートするプロダクトになっています。

  1. 請求書の受領
  2. 請求書から仕訳を行う
  3. オンラインバンキングに取り込むためのデータを出力する
  4. 会計ソフトに仕訳データを連携する

LayerX インボイス上で扱うデータは最終的に会計ソフトに取り込まれます。 プロトコルや出力形式などは、会計ソフトにより異なります。

今回紹介する会計ソフトfreee の場合、Public API が公開されています。 freee アプリストアに登録した開発者の方であれば、自由に開発することが出来ます。ドキュメントも手厚く公開されており、デベロッパーフレンドリーさに非常に感謝しております 🙏

会計ソフトfreeeをお使いのLayerX インボイスユーザーの方にとって、API 連携は

  • 手作業がなくなる
  • 会計ソフトの連携履歴がシステム上に残る

などのメリットがあり、より良い体験を提供する上でなくてはならない機能です。連携機能のアップデートも日々、力を注いでいます。 layerx.co.jp

Go ライブラリの開発

会計freee API 用のSDKは、PHP, Java などで既に公開されているものがあります。 github.com

LayerX インボイスはGoで開発しているため、APIコールをラップするクライアントライブラリを実装することにしました。 会計freee API は OpenAPI v3 のスキーマファイルも公開されており、はじめは openapi-generator などの自動生成を用いようとしましたが、一部スキーマで生成コードをそのまま利用することが出来ない箇所があり、実装ロジックもほぼないためスクラッチで実装することにしました。

ライブラリの初期化

OAuth2 アプリの client_id, client_secret, redirect_url をセットすることで初期化します。

import (
    "log"
    "os"

    freee "github.com/LayerXcom/freee-go"
)

func NewClient(clientID, clientSecret, redirectURL string) *freee.Client {
    conf := freee.NewConfig(clientID, clientSecret, redirectURL)
    conf.Log = log.New(os.Stdout, "", log.LstdFlags)
    client := freee.NewClient(conf)
    return client
}

ライブラリ内部でのログ出力用のロガーもセットすることが出来ます。認証部分やAPIエラーではない処理エラーの出力や、freee API サーバーがHTTPレスポンスヘッダーに付与してくれる X-Freee-Request-ID というAPIリクエスト毎にユニークなIDをログ出力するなどに役立ちます。

OAuth2トークンはユーザーごとに異なるため初期化の際ではなく、API呼び出し時にセットする必要があります。(ここでのユーザーは、会計freee のユーザーを指します。)

OAuth2.0 実装

認証方式は、OAuth2.0 です。 ライブラリ内部では、golang.org/x/oauth2 を使用しています。 ライブラリユーザーが、セキュアにかつ、OAuth ダンスで考慮すべきことが少なくなるよう実装しています。

例えば、OAuth 2.0 プロバイダー(freee)の認可ページURLの取得や、認可コードからOAuth2トークンの払い出しなどを行う関数もクライアント内で提供しています。

package freee

import (
    "context"

    "golang.org/x/oauth2"
)

const (
    Oauth2TokenURL = "https://accounts.secure.freee.co.jp/public_api/token"
    Oauth2AuthURL  = "https://accounts.secure.freee.co.jp/public_api/authorize"
)

func (c *Client) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string {
    return c.config.Oauth2.AuthCodeURL(state, opts...)
}

func (c *Client) Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
    return c.config.Oauth2.Exchange(ctx, code, opts...)
}

トークンのリフレッシュ

freee API のアクセストークンの有効期限は、発行から24時間になっています。リフレッシュトークンは無期限です。 ライブラリユーザーがリクエストの度に、アクセストークン有効期限の確認を行い、期限切れの場合リフレッシュを行うなどは非常に煩雑なため、避けたいはずです。

golang.org/x/oauth2oauth2.NewClient にて、リフレッシュを Transport 層で行うよう実装された http.Client を提供しています。 このクライアントを使ってリクエストすれば、トークンリフレッシュをライブラリユーザー側で気にせずともよくなります。 内部の実装を見ると、 oauth2.Token から有効なトークンか(Expiry: 有効期限を用いている)を判断してリクエストを行う RoundTripper を実装しています。

またトークンのリフレッシュ実装を type TokenSource のインターフェイスを実装することも出来ます。 例えば、golang.org/x/oauth2/google には Google Compute Engine の実行環境上からクレデンシャルを取得する実装が用意されています。 簡単な実装例は以下の記事でも紹介しています。 qiita.com

API コール例

freee-go からのレスポンスとして、 token が常に返されます。これは、API コール前の token が有効期限切れの場合にはリフレッシュされた token が返ってきます。 下記ではdefer などで必ずリフレッシュされた token を保存するように実装しておく例です。

import (
    "log"

    freee "github.com/LayerXcom/freee-go"
)

func main() {
    ...
    // データストアから、token を取得してくる
    token, err := retrieveYourTokenFromDataStore(ctx, token)
    // defer 内で、データストアへの保存を行う
    defer func() {
        if token.Valid() {
            saveYourTokenInDataStore(ctx, token)
        }
    }()

    // API Call
    me, token, err := client.GetUsersMe(ctx, token, freee.GetUsersMeOpts{})
    if err != nil {
        log.Fatal(err)
    }
    // リフレッシュされた token
    log.Printf("%v", token)
}

エラーハンドリング

API サーバーからのエラーか、access_token, refresh_token が共に revoke されており再度 OAuth認証が必要なエラーか、を切り分けてハンドリングすることは、エンドユーザーの体験に関わる重要な点です。 freee-go では、以下のような Error 構造体を定義してハンドリング出来るように実装しています。

type Error struct {
    StatusCode              int
    RawError                string
    IsAuthorizationRequired bool
}

例えば、ライブラリユーザー側で下記のようなチェックを行うことが出来ます。

   // API Call
    me, token, err := client.GetUsersMe(ctx, token, freee.GetUsersMeOpts{})
    if err != nil {
        if e, ok := err.(*freee.Error); ok {
            if e.IsAuthorizationRequired {
                // required re-authentication
            }
            // other errors
        }
        return err
    }

おわりに

LayerX では今後も自社で開発したものを公開して、エンドユーザーのみならずコミュニティなどへの貢献も進めていきます。 Github で公開されているレポジトリなども複数あります。興味が湧いたという方は、ぜひ一度話を聞きに来てください!

pkg.go.dev

herp.careers

LayerX インボイスのメール受信の仕組み

こんにちは!LayerXの mosa(榎本)です。

今日は、LayerXインボイスの機能であるメール受信について解説していきます。

LayerXインボイスでは、様々な方法で請求書ファイルを取り込むことができます。直接経理の方がアップロードできるのはもちろん、特定のメールアドレスに添付ファイル付きで送ることで、ファイルを取り込む仕組みがあります。流れとしては、以下の図のようになります。

  1. システムがメールアドレスを発行する
  2. ユーザー様が取引先にメールアドレスをお伝えする
  3. 取引先が月末に、当該アドレスに請求書を送付する

これにより、ユーザー様を介さずに、直接請求書をシステムに取り込むことができます。もともと取引先がメールで請求書を送っている場合、送り先のアドレスを変えるだけで済むため、業務フローを変更することなく運用にのせることができます。

このアーキテクチャをどのように組んだかを解説します。 (なんだかんだでメールって難しいので、開発の一助になれば幸いです。)

SendGrid

当時重視したのは、最速で開発できることです。

メールのプロトコルに不慣れなので、「HTTPで受けられること」「パースがある程度終わっているもの」がないか探した結果、SendGridの Inbound Email Parse Webhook機能に行き着きました。どちらの特徴も備えた上で、spamやなりすましチェックもやってくれます。(SendGridはメール送信用のSaaSだと思っていたので、受信もできることをその時知りました。)

使い方は単純で、ドメインを認証して向き先を設定し、受けるAPIを用意するのみ。

実際には、以下のペイロードがmutlipartで渡ってきます。大体パース済なのが嬉しいです。

  • メールヘッダー
  • To
  • From
  • Subject
  • Envelope
  • 本文(HTML)
  • 本文(TEXT)
  • charsets
  • attachments(添付ファイル数)
  • Sender IP
  • Spam Score, Spam Report
  • DKIM, SPFの結果

ヘッダーや添付ファイルなどをさらにパースするための各言語のヘルパーがあり、LayerX インボイスでは Goの sendgrid/sendgrid-go を利用しました。

取りこぼさない設計

このメール受取機能は、なるべくメールを取りこぼさないことが要件に入ります。通常のサービス側はアップデートの際、メンテナンスを告知した上で停止する選択肢を取ることができます。しかしメール受取機能は、お客様のさらに取引先(サービスを利用していません)から直に送られてくるため、メンテナンスであることを伝える術がなく、基本的に無停止であることが求められます。そのために、以下のことに気をつけました。

  1. 通常のサービスAPIから切り離すこと
  2. 必ず最初に生データをS3に上げ、後続処理に流すこと

サービスAPIとの切り離し

通常のサービスAPIから切り離すことで、薄く安定したコンポーネントとし、独立したメンテナンスを可能にしました。

はじめはAPI Gateway ⇒ Lambda のマネージドな構成にしましたが、 API Gatewayの制約上10MB以上のデータを受けられない関係から、現在は独立したECSをたててwebhookを受けています。(受けられないことそのものよりも、音もなく受信に失敗してしまい、送信側が気付けないことを問題視しました。)

最初に生データをS3に上げる

f:id:mosa_siru:20210510151434p:plain

受けたwebhookはまず生データをS3にあげて、そのS3 object keyを後続のジョブワーカーのためにenqueueします。実際の処理はワーカー側が行います。これにより、何かあったときでもリトライを可能にしています。また、生データが残っていることが調査を可能にしたケースも何件もありました。

以下は薄いメール受信APIの例です。

func inboundParseWebhookHandler(c echo.Context) error {
    bucket := "some-bucket"
    s3Key := fmt.Sprintf(fileKeyFormat, time.Now().Format("2006/01/02/15"), id)

    body, err := ioutil.ReadAll(c.Request().Body)
    if err != nil {
        return err
    }
    contentType := c.Request().Header.Get("Content-Type")
    err = putS3Object(bucket, s3Key, bytes.NewReader(body), contentType)
    if err != nil {
        return err
    }
    err = enqueueSendGridInboundParseWebhook(s3Key)
    if err != nil {
        return err
    }
    return c.String(http.StatusOK, s3Key)
}

メンテナンス

どうしても停止したい場合はどうしたら良いでしょうか。

SendGridのInbound Email Parse Webhook機能は、ステータスコード2xxを返さない限り、3日以内なら何度でもリトライしてくれる機能があります。そのため、メンテ中には503を返すことで、メンテ明けに問題なく受け取ることができるようにしています。便利ですね!

最後に

というわけで、メール受信のアーキテクチャを紹介しました。

実際にはなんだかんだでパースに苦労したり、泥臭い文字化けとの戦いがあったりしましたが、それは折をみて紹介しようと思います。メールって難しい……

今回はもろもろの前提をふまえSendGridを選びましたが、AWSならSESの受信機能をつかうのも全然ありだと思います。(東京リージョン来ないかなー)

最後に、LayerXでは現実的な設計をもとに高速に開発する仲間を募集しております!

We are hiring!

herp.careers

Micro Hardeningに参加した話

CTO室で社内基盤や全体的なセキュリティ担当をしている @ken5scal(鈴木研吾)です。 今回のブログはGW中(5/4)に参加したMicro Hardeningについて書きます。

microhardening.connpass.com

言うまでもないですが、インシデントが起きて嬉しい企業はありません。 我々としても取り組んでいる分野(請求書、アセットマネジメント、秘匿化技術等を用いたスマートシティ関連領域)で起きることは全く望んでいません。 特にセキュリティインシデントの発生確率はゼロにしたいものです。

しかし、残念ながらセキュリティインシデントは起きるものです。 意図しない結果がユーザー側でいきなり現れるか、あるいは多層防御のどこかでアラートとして顕在化するかは分かりませんが(もちろん後者を望んでいます)、起きるものです。

我々はそれに備えねばなりません。

セキュリティ・インシデントへの準備

こういった不確実性への対処には常日頃からの準備が重要なのは、ご存知かと思われます。 しかし、意図した不具合を引き起こすこと訓練環境を用意するにはそれなりにハードルが高いです。 特に、小さい会社では社内のリソースやスキルセット的に難しいのですが、そんな主体に最適なイベントがHardening/Mini Hardening/Micro Hardeningです。

今回、参加したMicro Hardeningは、攻撃者(運営)から45分間、EC事業を守ることを3回繰り返す競技です。5〜6人のチームがECサイトを割り当てられ、Web攻撃やサービス停止攻撃などを予防・検知・回復します。本業や家庭、そしてインシデント経験の有無を踏まえて、初めての攻撃を受ける訓練として、Micro Hardening以上に良い場はなかなかないでしょう。

自分自身はMicro Hardeningには2回目の参加したことがありますが、LayerX全体のレジリエンス力を高めたい理由から、各事業部に声をかけたところ、幸いにも全部署から均等にメンバーが集まり2チームを結成することができました。

f:id:kengoscal:20210507021122p:plain

また、新卒、元フリーランス、スタートアップ出身、大企業出身と色々な経験とバックグラウンドを持つメンバーが当社持ち前の「Be Animal」を発揮し参加してくれました。参加経験が各部と各年齢層に均等にフィードバックされ、つまり、LayerX全体のレベルアップに繋がったことでしょう。

f:id:kengoscal:20210507021147p:plain
チーム分け

当社の参加目的

一般的に(競技でなくても)Hardeningは事前準備をするのがセオリーですが、Micro Hardeningと当社の技術スタックがかなり違います。 当社のブログにもある通り、コンテナをメインのワークロードに利用し、動作環境もIaaSのマネージド環境を使っています。

tech.layerx.co.jp

ブログもポピュラーなCMSを使っていませんし、メールサーバーも完全にSaaSです。プログラム言語やフレームワークもMicro Hardeningとは違うため、参加経験を当社の知見にするには適切に目的設定をする必要がありました。

そこで、当社(というより発起人である自分)の目的は、以下に設定しました。

  • 攻撃された際の対応経験を詰み、振り返る
  • 攻撃を受ける雰囲気(無力さと焦燥)を経験する
  • 効果的な対応に何が重要かを理解してもらう

インシデントは当事者でないと、日頃の準備の重要性を実感しづらいものです。 そこでわかりやすく攻撃されている状況に身を置くことで、攻撃に気づくためにどうすればいいか、気づいた後にどうコミュニケーションをとり、どう判断するか、一通り経験して、攻撃とその影響について身を持って体験することが大切になります。

同時にプレッシャーを感じてもらいます。悪意からのプレッシャー、他社(チーム)からのプレッシャー、復旧へのプレッシャーと、インシデントでしか感じられないものを訓練で感じてもらいます。いざというときにある程度のメンタル耐性があるとないとでは、かなり初動に差がでるでしょう。

また、慣れない環境では何をみて防御し、何に設定を投入すればいいか、すぐにはわからないでしょう。 この経験により、日頃の資産棚卸しや現在の構成状況・設定状況の理解と把握が、それらの資産やサービスを守る上での最初の一歩であることを理解しやすくできます。

これらの目的達成のために、バックアップ取得とパスワード変更だけを事前準備とししてやるべきと伝え、あとは各チームの自主性に委譲してみました。

その結果が以下です。じゃじゃん!

結果

LayerXはteam14とteam15です。

f:id:kengoscal:20210507022832p:plain

team14は(言い訳をするとBackupの戻し判断が遅れたことで3セット目のスコアが下がってしまいましたが)1~2セット目のスコアと3セット目の防御点で健闘し、team15は3セット目で非常に良いスコアを叩きだしました。トップになれなかったのは悔しいですが(本当に悔しい)、トップ1とトップ2がMicro Hardening経験者チームであること、当社2チームが自分を除いて初参加メンバーしかいない & 技術スタックが違うことを考えると、非常によいスコアだったのではないでしょうか。

個人的には当社の技術力の高さに改めて驚きました。同時に、自分自身もどんどんパワーアップせねば、、、という思いに駆られています。

まとめ

スコアは良かったとはいえ、トップではありません。そもそも訓練にすぎなく、本番はこれからです。それでも、Micro Hardeningでの参加は、今後さまざまな活動をデジタル化していくLayerXがユーザーに提供する価値を維持していく上で、非常に意義があったと思います。

この場を借りて、Micro Hardening運営者様、休日にもかかわらず快く参加してくれたLayerXのメンバー(そしてご家族)に感謝をお伝えしたいと思います。

ありがとうございました。

また、是非、こういったことに取り組んでいきたい方は、ご連絡いただけると嬉しいです。