LayerX エンジニアブログ

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

本番稼働でわかった秘匿化技術のチャレンジングなこと

こんにちは!LayerX LabsでAnonifyを開発しているエンジニアの恩田(さいぺ)です。

この記事はLayerX 2021アドベントカレンダー 13日目の記事です。昨日はmosaさんのLayerXのカルチャーと行動指針 (2021年版)でした。明日はken5さんの記事が公開される予定です。

Anonify概略

Anonifyについては以前、秘匿化モジュールAnonifyにおけるRust featuresを活用した開発 - LayerX エンジニアブログ で紹介させて頂いただいたので詳細はブログをご覧ください。要約するとAnonifyは「秘匿性」と「透明性」という相反する2つの性質を両立する秘匿化モジュールです。要素技術はTrusted Execution Environment(TEE)で、より具体的にはIntel SGX®︎を採用しています。

いよいよ始まったAnonify本番稼働

海外では、Intel SGXを用いたDemetics社の医療イノベーションの事例なども登場してきており、徐々にIntel SGXの本番稼働が色づいてきています。

そんな中、9月21日から10月11日にかけて実施されたつくば市様の『つくば市科学技術・イノベーション振興指針』の策定に向けたWEBアンケートでAnonifyを秘匿集計基盤として採用いただきました。本件は、実際に一般の方々が触れる基盤として、Anonifyが本番で利用される一号案件となりました。

f:id:cipepser:20211012200555p:plain

https://www.city.tsukuba.lg.jp/_res/projects/default_project/_page_/001/015/751/flyer.pdf より引用)

本番稼働となるわけなので、当然に正常系・異常系を網羅的に実施し、負荷試験、障害試験といった試験も十分行いました。その他にも運用・監視体制も組み、結果として無事、実施期間を終えることができました。ただしこれらはシステムの本番稼働では一般的な話です。私自身金融SIer出身であったり、メンバーもサービス運用経験者だったので、これらの事前準備は当たり前のことを当たり前にやるというものでした。1

TEEならではの難しさ

いざAnonifyを本番稼働させるとなったことで、TEEならではの難しさにも直面しました。冒頭で言及したブログで触れたようなno-std制約による開発難易度の高さという話もあるのですが、本番稼働という文脈においては「いざ障害が起こったときに復旧が可能なのか」が一番の困難でした。

TEEを始めとする秘匿化技術はその名の通り、秘匿化が売りです。

Anonifyでいえば、クライアントからのリクエストは公開鍵暗号2で暗号化されます。この公開鍵に対応する秘密鍵はTEEの中から一切外に出ることはありません。 さて、本番サービスを運用されている方であれば、いざというときに原因究明、復旧ができるよう、サーバやアプリケーションのログやバックアップを入念に準備するのではないでしょうか。これらログやバックアップはサービス継続の最後の砦であり、一エンジニアとしても大きな心の拠り所です。しかしAnonifyではTEEの中から一切秘密鍵が出ないという売りが牙を剥きます。リクエストが復号され、平文となるのはTEEの中だけです。メモリ上でもenclaveと呼ばれる隔離保護された領域で暗号化されており、平文になるのはCPUで計算する時にだけです。

つまりいくらデータのバックアップを取っていても、それは暗号化されたリクエストであり、TEEに対応するCPUがなければ復旧できない無用の長物でしかないのです。

特にAnonifyは Azure Confidential Computing VMでAnonifyを動かそう - LayerX エンジニアブログ で紹介した通り、Azure Confidential Computing VM上に構築しています。そのため、メンテナンス等でVMと物理マシン(より詳細にはCPU)の紐付けが一度解除されてしまうと、再度同じCPUが割り当てられる保証がありません。そして何より、クラウドプロバイダー側でスケジュールされるメンテナンスは我々がコントロールできるものではありません。メンテナンスは2020年の実績で言えば3回ほど発生しています。

チームメンバー一同、サービス運用経験者だったからこそ、いざというときに復旧ができないかもしれないという事実は大きな恐怖でした。

Anonifyでの対策

Anonifyでは、これに対応するためkey-vaultノードを設計に組み込んでいます。key-vaultノードもAnonifyノードと同様にIntel SGX上に構築されますが、Anonifyノードとは異なるVM(CPUも異なる)上に構築します。実際にはAzure Kubernetes Service(AKS)を用いて構築しているので、Anonify podとkey-vault podが存在し、それぞれのpodが別のnode上で稼働するアーキテクチャです。

そして、Anonifyノード〜key-vaultノード間はmutual-TLS(mTLS)で接続しています。

f:id:cipepser:20211012202126p:plain

みなさんが普段ブラウザなどでWebサイトを閲覧する際のTLSでは、サーバ証明書がルート証明書から辿ることのできる正規の証明書であることを検証し、セキュアなコネクションを張ります。Anonifyノードとkey-vaultノード間のmTLSコネクションでは、お互いに想定通りのプログラムが動作していることを検証しています。

この「お互いに想定通りのプログラムが動作していることを検証」を実現するために、Intel SGXで提供されているRemote Attestationと呼ばれる機能を利用します。

f:id:cipepser:20211012201910p:plain

Remote Attestaitonでは、Intelが工場製造時に焼き付けた鍵を使って正規のCPUであることを検証の上、実行コード3にIntel秘密鍵で署名を付与してくれます。そしてこの署名はpodのdocker imageビルド時とruntimeに得ることができます。Anonifyノードとkey-vaultノードはimageビルド時点で、お互いのmeasumentを知ることができます。そして実際に秘密鍵をバックアップするruntimeに、相手が事前に取り決めた相手であることを認証の上、mTLSコネクションを結びます。このmTLSコネクションはAnonifyノードのTEE内〜key-vaultノードのTEE内で結ばれており、このコネクションを介して秘密鍵をセキュアにバックアップすることができます。このようにして、万が一Anonifyノードに障害が発生した場合やメンテナンスに備えています。

最後に

まずTEEの本番稼働というチャレンジングな経験ができたことは、チームとしても大きな財産です。 そして、今回紹介したkey-vaultによる秘密鍵バックアップは、あくまでTEEの本番適用で出てきた課題の一つです。他にも度々話題に上がるIntel SGXのEPCサイズ制限も涙なくしては語れません(つくば市様のWEBアンケートに向けては、負荷試験を実施し、キャパシティプラニングをした上で臨んでいます)。このように様々な難しさがあるIntel SGXを本番環境で稼働させる実績が積めたことは、これからのAnonify・秘匿化技術に向けた意義深い一歩だと感じています。

今回お話したようなm-TLSのお話だけでなく、実際にサンプルコードを動かすことができるAnonify解体新書もチームメンバーがscrapboxで執筆しています。私も隔週で計算機科学やプライバシー技術の論文紹介をしています。そして、難しい秘匿化技術やプライバシー技術を社会に実装していくには、技術視点だけでなく、Biz視点も車の両輪のように重要です。LayerX Labs Newsletterでは毎週Biz/Techの両面から執筆していますので、ぜひ以下からご購読ください。なんと無料です。

https://layerxnews.substack.com/

We are Hiring!

LayerXではエンジニアはもちろん、全職種で積極的に採用中です。 ぜひカジュアル面談からでもお話しましょう!


  1. ただしこれはこれで難しい

  2. 正確には認証付き公開鍵暗号方式

  3. 厳密にはQUOTE構造体

Zendesk Guide テーマのDX(Dev Exp)がすごい件

はじめに

こんにちは、LayerXから三井物産デジタル・アセットマネジメントに出向している武市(@tacke_jp)です。最近、ALTERNAのサポートページ作成のためZendesk Guide (helpcenter) を利用したFAQページの開発を行いました。その際にZendesk Apps Toolsを利用したローカルでのライブプレビューの機能が良い開発体験だったため、皆さんに紹介したくこの記事を書きました。

Zendesk Guideについて

はじめにZendesk Guideの機能紹介をします。Zendesk GuideはいわばFAQサイトの構築に特化したCMSで、カテゴリやセクションごとにFAQの記事を管理し、それを公開することができます。また今回私達のサイトでは利用しませんでしたが、ユーザーが記事にコメントしたり、別途記事投稿も可能といったようなコミュニティサイトやフォーラムといった用途で利用することも可能です。

このGuideのデザイン(「テーマ」呼ばれています)は自由度の高い独自カスタマイズが可能です。デフォルトのCopenhagenテーマのソースコードはgithubに公開されているため、このコードを変更してZendeskにカスタムテーマとして登録することでこれを利用することができます。このリポジトリは各ページのHTMLテンプレート(というテンプレートエンジンを利用)、CSSファイル(SASS)、JSファイルから成り立っています。CSSファイルとJSファイルはそのままZendeskのCDNから配信され、HTMLテンプレートのレンダリングはZendesk側のwebサーバーで行われます。

カスタムテーマは、テーマのディレクトリをzipファイルに固めてZendesk Guideの管理画面からアップロードすることで設定します。またgithub連携して、pushされたタイミングで変更が反映されるようにもできます。(Zendeskがすべてのprivate repository要求するため、今回の開発では利用しませんでした。)

f:id:tacke_jp:20211004214117p:plain
図1

zat コマンドでのローカルプレビュー

さて、このようにテーマをコードを編集することで自由度高くカスタマイズできるのはありがたいですが、開発の過程で毎回zipに固めて管理画面からアップロードし動作確認しているのでは骨が折れます。そのようなことをしなくても良いようにZendeskはZendesk Apps Tools (zat コマンド) でテーマのローカルプレビューを提供しています。

このプレビューは一風変わった機能で、FAQ記事のコンテンツはZendesk側に登録されたデータを使いつつ、HTMLテンプレートやスタイルファイルはlocalでの変更がwatchされており、ファイルを保存したタイミングで毎回live reloadされ、即座にデザインをブラウザ上で確認できる仕組みになっています。(zatコマンドの本機能の詳しい利用方法についてはクラスメソッドの方が詳しく解説されているので、そちらの記事をご覧頂くのがよいかと思います。)

この仕組みのおかげで、開発者はモックサーバーを手元に建てることなく、またlocalのDBやwebサーバーを立ち上げることなく、Zendesk側に登録されたデータをそのまま用い、テーマの開発を行うことができます。これはとてもよい開発体験です。ここから先は、この仕組みがどのように動いているか、プレビュー時のサイト構成を紐解きつつ少しだけ内部を覗いてみましょう。

Under the hood

まずCSSファイルとJSファイルなどの静的ファイルですが、zatコマンドの起動時にWebサーバーが起動し、CDNに変わりそちらからファイルが配信されます。ローカルプレビューモードでプレビューを表示するとHTMLテンプレートに記述されたCSSファイルやJSファイルの参照先がlocalhostに変更されます。filesystemがwatchされ、手元のファイルに更新があった場合にwebsocket経由で開いているページに更新がnotifyされ、ブラウザ側でリロードが走り最新のファイルが適用される仕組みになっています。

次にHTMLテンプレートですが、これは本来Zendesk側のwebサーバーにデプロイされて、そこでのレンダリング時に利用されるものです。しかし、プレビュー時にアクセスするwebサーバーの向き先はあくまでZendeskのものになっています。ではどうしているのかというと、手元で毎回内容に更新があったことを検知して、internalなAPIをcallして内容を毎回登録しています。(若干の力技感を感じますが、手元でのHTMLテンプレートのレンダリングを避けるとなるとこの方法が現実的に思います。)

HTMLテンプレートに手元で更新が起こった場合、internal APIを叩いた後即livereloadが走り、ブラウザからZendesk webサーバーへのリクエストが行われますが、そのタイミングで最新のテンプレートを利用してレンダリングが行われます。Zendeskの規模でのwebサーバーにこのような挙動を実装するのは簡単ではないはずで、恐らく裏側にetcdのようなwebサーバーが利用するconfigurationを管理する分散data storeのようなミドルウェアがいるのではないかと思います。(あくまで私の推測の域を出ません。)

f:id:tacke_jp:20211004214041p:plain
図2

むすび

SaaSを利用するにあたっては、その機能やデザインをどの程度カスタマイズできるかが、利用者にとては導入に際しクリティカルな点となることが多いかと思います。0から自前で開発するのは骨が折れるためSaaSを利用したいが、一方で自社の要件や要望にフィットする形でカスタマイズしたいというケースはよくあります。

今回のようにサイトのデザインをコードによりカスタマイズすることのできる機能は、利用者にとってはありがたい一方で、SaaS提供者にとってはどこまでをカスタマイズ可能とするかは頭の痛い問題かと思います。また、インフラアーキテクチャは所与のものでありつつも、利用者に最適な開発体験を提供することも大きな課題の一つと思われます。しかしながら、それを達成することでプロダクトの導入促進やcharnの抑止に寄与する部分はあるかと思います。今回のローカルプレビューは、その1つのケーススタディと捉えることができそうです。

また、みなさんが自社のプロダクト開発されるにあたって、例えば非SPA構成時にデザイナーの方に簡易的な開発環境を提供したい際などに、今回の構成は参考になるかもしれません。

最後までお読みいただき、ありがとうございました。 少しでもLayerXに興味を持っていただけたら、お気軽にご連絡ください! エンジニアはもちろん各ポジションで積極採用中です。

meety.net

mysqlsh (MySQL Shell): Dump and Restore in AWS Aurora

LayerX インボイス を開発しているDX事業部の @yyoshiki41(中川佳希)です。

DX事業部ではデータベースとして MySQL(Amazon Aurora)を利用しています。 今回のブログは、mysqlsh (MySQL Shell) を用いて、Dumpデータ取得とリストアを行う際に気をつける点です。

mysqldump, mysqlpump

Dumpデータ取得を行う際に、広く知られているのが mysqldump かと思います。

MySQL 5.7.8 からは、 mysqlpump という別のクライアントツールも提供されるようになりました。 主に下記のような特徴があります。

  • 並列での処理が行われる(Parallel)
  • Dump Progress がみれる
  • 圧縮方式は、LZ4 と ZLIB が使用可能(mysqlpump Ver 1.0.0 Distrib 5.7.35)
  • TABLE スキーマとINSERT 文の両方を出力する場合、INDEX を貼るクエリをデータリストア(INSERT文)の後に出力してくれる
    • INSERT毎での INDEX構築や KEY CHECKS なども不要になり、高速化が期待できる

実行例)

$ mysqlpump -uroot -p -B sandbox --set-gtid-purged=OFF --no-create-db --include-tables 'tests' --result-file=results.sql
Dump progress: n/n' tables, m/m' rows
Dump completed in x milliseconds

以下のようなファイルが出力されます。

-- テーブル作成
CREATE TABLE `tests` (
`id` varchar(36) COLLATE utf8mb4_bin NOT NULL,
`json` json DEFAULT NULL,
`version` int(11) GENERATED ALWAYS AS (json_extract(`json`,'$.version')) VIRTUAL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB
;
-- INSERT 文
INSERT INTO `tests` (`id`,`json`) VALUES (1,"{\"props\": \"val\", \"version\": \"1\"}"),(2,"{\"props\": \"val\", \"version\": \"2\"}");
-- 最後に INDEX 作成
ALTER TABLE `tests` ADD KEY `version` (`version`);

MySQL :: MySQL 5.7 Reference Manual :: 4.5.6 mysqlpump — A Database Backup Program

mysqlsh (MySQL Shell)

mysqlsh でも、Dump & Load Utility が提供されています。

また下記のブログでは mysqlsh でのバックアップ取得とリストアがベンチマークとともに紹介されています。
特に、mysqlsh が parallel で動作することとデフォルトで使用する圧縮方式 zstd により、mysqlbump, mysqlpump 以上のパフォーマンスとなることがレポートされています。

blogs.oracle.com

AWS Aurora 環境で使ってみる

mysqlsh, Aurora(MySQL Server)のバージョンは、以下のとおりです。

$ mysqlsh --uri root@localhost:3306
...
MySQL Shell 8.0.26
...
Your MySQL connection id is 326244
Server version: 5.7.12-log MySQL Community Server (GPL)
...

いきなりですが、Dump データ取得に失敗します。

MySQL  localhost:3306 ssl  JS > util.dumpSchemas(["tests"], "/tmp")
Acquiring global read lock
ERROR: Failed to acquire global read lock: MySQL Error 1045 (28000): Access denied for user 'admin'@'%' (using password: YES)
Global read lock has been released
Util.dumpSchemas: Unable to acquire global read lock (RuntimeError)

これは AWS サポートブログ でも紹介されている、mysqldump で --master-data オプションを使用した場合と同じ原因のように見えます。 rdsadmin ユーザー以外は Super_priv を持たないため、FLUSH TABLES WITH READ LOCK が実行できずグローバルな読み取りロックを取得できないようです。

以下のようにオプションとして、 {consistent: false} を渡せば取得することは可能ですが、一貫性を保ったデータを取得するにはアプリケーションを止めるなどの必要があります。

MySQL  localhost:3306 ssl  JS > util.dumpSchemas(["tests"], "/tmp", {consistent: false})
Duration: 00:00:03s
Schemas dumped: 1
Tables dumped: 36
Uncompressed data size: 1.65 MB
Compressed data size: 342.14 KB
Compression ratio: 4.8
Rows written: 7058
Bytes written: 342.14 KB
Average uncompressed throughput: 501.88 KB/s
Average compressed throughput: 104.30 KB/s

consistent: [ true | false ] Enable (true) or disable (false) consistent data dumps by locking the instance for backup during the dump. The default is true. When true is set, the utility sets a global read lock using the FLUSH TABLES WITH READ LOCK statement (if the user ID used to run the utility has the RELOAD privilege), or a series of table locks using LOCK TABLES statements (if the user ID does not have the RELOAD privilege but does have LOCK TABLES). The transaction for each thread is started using the statements SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ and START TRANSACTION WITH CONSISTENT SNAPSHOT. When all threads have started their transactions, the instance is locked for backup (as described in LOCK INSTANCE FOR BACKUP and UNLOCK INSTANCE Statements) and the global read lock is released.

テーブル単位での Export / Import は、以下のように行えます。 exportTable 実行最終行に、 importTable コマンドを出力してくれて非常に親切です。

MySQL  localhost:3306 ssl  JS > util.exportTable("tests.table_A", "/tmp/table_A")
Gathering information - done
Preparing data dump for table `tests`.`table_A`
Data dump for table `dev_payer`.`clients` will use column `id` as an index
Running data dump using 1 thread.
NOTE: Progress information uses estimated values and may not be accurate.
Data dump for table `tests`.`table_A` will be written to 1 file
102% (2.38K rows / ~2.32K rows), 3.49K rows/s, 846.02 KB/s
Duration: 00:00:00s
Data size: 576.76 KB
Rows written: 2380
Bytes written: 576.76 KB
Average throughput: 576.76 KB/s

The dump can be loaded using:
util.importTable("/tmp/table_A", {
    "characterSet": "utf8mb4",
    "schema": "tests",
    "table": "table_A"
})

おわりに

単純なDBバックアップであれば、マネージドサービス側でサポートされていますが、
アプリケーション開発が進み、テーブル移行などが必要になってくるケースが出てくるとテーブル単位でのDumpと移行などが必要になってきます。

今回の FLUSH TABLES WITH READ LOCK や GTID などでハマることも多いかと思います。 ツールが変わっても気をつけるべき点としては、同じMySQL Server を使うならば基本的に同じだと感じました。 しかし、処理スピードやコマンドの使いやすさなど MySQL Shell の利点も多く、今後も楽しみなツールの1つです!


DX事業部では絶賛採用募集中です。 SaaS開発に興味があるという方は、ぜひ一度話を聞きに来てみてください!

herp.careers

エントリーはちょっとという方、こちらから中の話を聞くこともできます!

meety.net

配信メールのテンプレート管理をSendGirdからgo:embedを用いた方法に変更した話

みなさまこんにちはMDM事業部で金融DXに日々精進している @MasashiSalvadorです。 今回はメールのテンプレート管理法を変更しDX(Developer eXperience)を改善した話をします。

何をやったのか?

  • 顧客へ自動配信するメールのテンプレートの管理をSendGridから自社のGithubリポジトリに移行した。
  • 移行に際し Go 1.16から導入された go:embed (https://pkg.go.dev/embed) 機能を用いた。

お客様にサービスを利用していただくために、メールの配信機能をどんなサービスでも実装するかと思います。MDM事業部の開発しているサービス(公開されているものだと、プロ投資家の方々に不動産案件情報を定期的にお届けするあさどれ不動産 、別のサービスも絶賛開発中です)では仮登録完了をお知らせするメール、登録完了をお知らせするメール、ワンタイムトークンによる認証に用いるトークンをお知らせするメールなどが実装されています。

メールサーバを自前で立てて管理するのは骨が折れるので、SendGrid を利用しています。SendGridは開封・クリック計測の機能、メールの開封をwebhookで通知する機能、顧客リストを管理する機能、キャンペーンメールを予約する機能などがあり、痒いところに手が届く感じに機能が揃っています。

メールのテンプレート機能もその一環で、ユーザはGUIを利用してSendGrid上でHTMLメールを編集しテスト/実配信を行うことが可能です。 画像やボタンの配置なども自由にできるのでGUIはGUIで利点があり、それなりに使い勝手が良いです。

f:id:masashisalvador:20211005184552p:plain
SendGridのDynamicTemplateの編集画面

ところが、このテンプレート機能、非常に便利なのですが、当社のユースケースに合わなかったのです...😿。

なぜやったの?

  1. 開発、ステージング、本番の各環境ごとにテンプレートIDもテンプレートの実体も異なるので、各環境への反映&テストに手間がかかる。
  2. 現状のフェーズではHTMLメールをそこまでリッチに組まないためGUI上で編集できるという機能は(便利だが)オーバースペックだった

MDMのシステムでは環境ごとにSendGridのアカウントを切り分けています(主契約の下に子アカウントを作成した上で、2FAを設定する必要があるため各開発者 * 環境の数分アカウントを作成しています)。 SendGridのテンプレートや、テンプレートを利用するためのAPI Keyは各環境ごとに分かれていました。つまり、開発から本番のメールテンプレートを読み込もうとするとSendGridのAPIは40xのエラーを返します。逆もまたしかりです。

この切り分け自体は妥当なものなのですが、下記の問題が起こっていました。 1. 愚直にやるとメールのテンプレートをソースコード管理できない。 2. 各環境ごとに異なるテンプレートのIDを管理しないといけない(環境変数や設定ファイルに都度設定しないといけない) 3. 文言の変更 → 各環境への反映 → テスト(やPdMの確認)→ 再修正 などの改善プロセスを回すたびにGUIから操作する必要がある。

f:id:masashisalvador:20211005185255p:plain
GUIベースでテンプレートをいじる場合の変更プロセス(基本形)

1.はリリースの際のヒューマンエラーを引き起こし得ます(コピペミスなど)、コピペミスがないかエンジニアはドキドキしないといけません(個人的にはあんまり好きじゃない) 2は1つ新しいメールを追加する度に3つの設定値が増えることになり、チリツモで管理が煩雑化します。 特に3は、テンプレートに埋め込む変数の変更もセットで考えると、ソースコード→GUIの往復が増えてしまい、開発者の集中リソースを削ります。

MDMでは設定値の管理にtomlを用いており、実際は下記のように環境ごとの設定ファイルを管理し、そこにテンプレートのIDを記載しています。

[SENDGRID]
    [SENDGRID.TEMPLATE_ID]
    SIGNUP="d-XXXXXXXXXXXXXXXXXXX"
    INVITATION="d-YYYYYYYYYYYYYYYYYYY"
    ...
    ...
    ADD_HOGEHOGE="d-ZZZZZZZZZZZZ"

configはGitHub - spf13/viper: Go configuration with fangsで読み込んでいます。viper導入前はすべて .env ファイルで管理していたのですが、環境変数が増えすぎ問題が発生してつらかったので開発の途上で変更しました。このお話はまた今度...

どんな解決策があるの?

  1. テンプレートをソースコード管理し、SendGridのテンプレート更新のAPIを用いてCD経由で設定する
  2. テンプレート機能を使うのをやめて、Goのテンプレートでメールの本文を作成する

1も一度は検討しましたが、2を選択しました。 MDMでは各種インフラ設定はterraformで管理され、SREだけでなく開発者もterraformを普通に書く文化があるので、特段問題なく導入できるとは思ったのですが、 メールの文言を少し変更してテストするのに都度terraform applyするような世界線は(かっこいいかもしれませんが)あんまり開発者体験が良いとは思えませんでした。

今回は2.を選択し、Go1.16から導入されたembed機能を用いてテンプレート自体をGoのバイナリに埋めこんで利用することにしました。

実際にどうgo:embedを利用したか

Go 1.16 がリリースされて半年以上が経過していますので、embed機能はすでに各社実運用に載せられているのではないでしょうか。 普段からいろいろな記事を参考にさせていただいていますが、フューチャーさんの技術ブログがとてもわかりやすかったです。 future-architect.github.io

go:embedを用いることで画像、設定ファイル、テキストなど、多様なファイルをバイナリに含めることができ、Goのコード内から簡単に読み出すことができるようになります(embedしない場合に比べて記述量も減らすことができる)、ビルド成果物がシングルバイナリになるので、ただコピーして配布するだけで様々な場所で動かすことができるというGoのポータビリティ面での利点を活かすことができます。

余談ですが、静的ファイルのバイナリへの埋め込みはその昔はgo-bindataがあり、go-bindataの作者がGithubアカウントを突然削除するなどがあり、go-bindataを使っていた部分をgo-assetsに置き換える - PartyIXの記事のようにgo-assetsに置き換えるなどやったなぁなどと改めて懐かしくなりました。

実際のファイル構成とテンプレートの読み込みに関しては

main.go
- lib
    - mail
         - templates/signup_mail.tmpl
                    /reset_password.tmpl
                    /common/footer.tmpl   // 共通フッター
                    /titles.json
         - content.go   // ParseFSでテンプレートを読み込む部分
         - sendgrid.go // SendGridのAPIを叩く部分
         - template_variable.go  // 各テンプレートに埋め込む変数の構造体
 ....

content.go

package mail

import (
    "bytes"
    "context"
    "embed"
    "fmt"
    "text/template"

    "github.com/sendgrid/sendgrid-go"
    "github.com/sendgrid/sendgrid-go/helpers/mail"

    "github.com/mitsui-x/alterna-api/src/lib/env"
    "github.com/mitsui-x/alterna-api/src/lib/log"
)

//go:embed templates/*
var static embed.FS

func GenerateTemplateByTemplateName(tName string, input interface{}) (string, error) {
    templatePath := fmt.Sprintf("templates/%s", tName)
    tmpl, err := template.ParseFS(static, templatePath)
    if err != nil {
        return "", fmt.Errorf("failed to parse template path %s, %w", templatePath, err)
    }

    buf := &bytes.Buffer{}
    err = tmpl.Execute(buf, input)
    if err != nil {
        return "", fmt.Errorf("failed to execute template with input = %v, tName %s, %w", input, tName, err)
    }

        /* 共通部分のハンドリング、ブログ用にシンプルに変えています */
    footerBuf := &bytes.Buffer{}
    tmpl, err = template.ParseFS(static, "templates/footer.tmpl")
    if err != nil {
        return "", fmt.Errorf("failed to parse footer, %w", err)
    }
    err = tmpl.Execute(footerBuf, nil)
    if err != nil {
        return "", fmt.Errorf("failed to execute footer, %w", err)
    }

    return fmt.Sprintf("%s\n%s", buf.String(), footerBuf.String()), nil
}

func Send(ctx context.Context, subject, toAddress, toName string, plaintextContent, htmlContent string) error {
        /* 一部省略 */ 
    apiKey := env.GetVar("SENDGRID.API_KEY")
    client := sendgrid.NewSendClient(apiKey)

    fromEmail := mail.NewEmail(fromName, fromEmailAddress)
    toEmail := mail.NewEmail(toName, toAddress)
        // SingleEmai https://github.com/sendgrid/sendgrid-go のREADMEの先頭に載っている最もシンプルなやり方でメールを送る
    message := mail.NewSingleEmail(fromEmail, subject, toEmail, plaintextContent, htmlContent)
    response, err := client.Send(message)
    if err != nil {
                /* エラー処理 */
    }
    if response.StatusCode >= 300 {
                 /* エラー処理 */
        return err
    }

    return nil
}

signup_mail.tmpl

{{ .LastName }} {{ .FirstName }} 様<br>
<br>
この度はXXXにご登録いただき、誠にありがとうございます。<br>
... 略 ... 

テンプレートに埋め込む変数を定める構造体

package mail

type UserSignUpAtributes struct {
    LastName       string
    FirstName      string
}

実際にメールを送信するときに下記のように呼び出します。

input := &UserSignUpAtributes{ LastName: "MDM", FirstName "太郎" }
content := maillib.GenerateTemplateByTemplateName("signup_mail.tmpl", input) // 本文生成
err = maillib.Send(ctx, subject, toAddress, toName, content, content) // メール送信処理

これにより、メールのテンプレート管理をGithubに寄せることができ、各環境へのリリースも通常のソースコードと同様に扱うことができるようになりました。

おわりに

金融DXを実現するために様々な機能の開発をする必要がありますが、全てをゼロから作り上げるわけにはいかないのも事実で、セキュリティ面や開発者体験なども含めてSaaSを選定し使い倒さねばなりません。人数の少ないフェーズでは小さな認知負荷の削減やCI/CDまわりの開発者体験の向上が全体の生産性を大きく上げることにもつながるため、今後も小さな改善を繰り返していきたいと考えています。

こういった細かな改善も含め、MDMではやりたいことが山のようにあります。絶賛仲間を募集中です! herp.careers

meetyでカジュアルに色々話させていただくことも出来ますので、もしよければごらんくださいmm

meety.net

ぜひ一緒に、眠れる銭をアクティベイトしましょう!

【9/28-30開催】LayerXから2名のエンジニアがAWS Dev Day Online Japan 2021に登壇します! #AWSDevDay

f:id:shun_tak:20210921170151j:plain

こんにちは!いよいよ来週AWS Dev Dayが開催ですが、この度LayerXから2名のエンジニアが登壇することになりました。

どんな講演になるのか、本記事で簡単に紹介します!

追記:レポート記事も公開しました!
tech.layerx.co.jp

E-2 : LayerXインボイスのAI-OCRを支える非同期処理アーキテクチャ (9/29 15:10〜15:50)

スピーカー:高際 隼 (株式会社LayerX DX事業部 AI-OCRチーム Lead)

LayerX インボイスは、請求書ファイルを読み取りデータ化するAI-OCR機能を備えた請求書処理サービスで、お客様は任意のタイミングで請求書ファイルをいくらでもアップロードすることができます。

AI-OCRは処理が重たいですが、AWS LambdaやAmazon SQS等を活用し、スケーラビリティと可用性を確保しつつコスト削減も実現した手法について解説いたします。

このセッションでお伝えしたいことスライド アジェンダスライド

I-1 : やはりタグ タグは全てを解決する - Tagging Everything (9/30 14:15〜14:55)

スピーカー:鈴木 研吾 (株式会社LayerX シニアセキュリティアーキテクト)

すべての道は資産管理に通ず。17世紀のフランスの詩人はそう謳いました。 時は流れ、21世紀。

資産管理はいまだにITガバナンスにおける基礎となっています。 一方ツールは進化しており巨大なエクセルというテーブルから、資産そのものにタグという属性をつけ、そしてInfrastructure as Codeが実現したマスデプロイによって柔軟な運用を可能としました。

本講演では、LayerXにおけるガードレールを構築するうえで、どのように我々がタグ管理をして、今後発展させていくかをお話しようと思います。

本日のお題スライド

申込方法

ぜひ今すぐ参加登録しましょう!

AWS Dev Day Online Japan 参加登録ページ

ISMSもとったし、エンジニアだけどITガバナンス主導してきた話をする

CTO室 @ken5scal です。座右の銘は「当社はブロックチェーンの会社ではもうありません」です。 主にインフラ構築・運用をしたり、社内の基盤を整えたり、不具合を特定して git blame したら自分のcommitで泣いたりしています。

当社は「すべての経済活動を、デジタル化する」というミッションを掲げており、生産性向上の達成という価値を新しいサービスによって提供しています。

しかしながら、新しい価値にはリスクが伴います。信頼できない価値を提供するサービスを採用する合理的な判断はありませんので、お客様に価値を価値のまま提供しなければlose-loseな関係になってしまいます。

今回は2021/08/27に発表されたISMS認証の基準準拠を含む、「LayerXは信頼できる」ブランドを確立していくための当社のITガバナンス活動について触れようと思います。

prtimes.jp

ガバナンス体制、その遷移

当社は代表の福島が「LayerXはブロックチェーンの会社じゃありません、という話」で述べたとおり、以前はブロックチェーンのコンサル事業を主としていました。

LayerXはブロックチェーンの会社じゃありません、という話|福島良典 | LayerX|note

私が入社した2020年頃は、全員が機動的に配置を変え①Ethereumのシャーディング技術の研究開発 ②金融業界を中心としたブロックチェーンの設計・標準の調査およびPoCの開発に携わるような体制だったと記憶しています。

しかし、こういったコンサル活動を通して学んだことを基に、次のようなプロダクトの模索が始まります。大体、2020/04頃です。

  • Fintech事業:不動産などのアセマネを裏付けとした証券発行 (三井物産デジタル・アセットマネジメント)
  • SaaS事業:LayerX インボイス, LayerX ワークフロー
  • LayerX Labs:Anonifyを使った事業研究

このようにプロダクト領域が定まるとともに、チームも固定化されました。 それとともに、コンサル事業をしていたころのような社内 or Notな情報セキュリティ管理の二元論ではなくなり、 各部は各部の事業領域および成功にコミットする方針が打ち出されました。 言い方を変えると、仮に社内であっても職権や業務形態によって情報セキュリティ管理に強弱が発生したにほかなりません。 情報セキュリティ管理に毀損があればリスクが顕在化することに変わりはありませんが、 そのリスクの性質およびその変化に対する対策も大きく転換を強いられることになります。

最初のガバナンス態勢とゴール

そうしてまず2020/08に立ち上がったのが「ガバナンスをいい感じにし隊」です。

f:id:kengoscal:20210902015120p:plain
初回ガバナンス定例

この時点では、横断組織としてのCTO室がボトルネックにならないよう、各部におけるインフラ的なCommit(もちろんGit的な意味で)をしていたエンジニアを担当者として呼んでいます。 横断組織における中央集権化は早晩にボトルネック化し、結果的に場当たり的かつ非実践的な対応になりがちだからです。

そして私の経験上、横断組織はそもそも採用が難しい(≒候補が少ない)領域です。 もちろん採用を諦めるわけではないのですが、人の頭数によるスケールは悲観的に見ざるをえず、特に各部が独自の技術スタックを採用する当社においては、 事業リスクと対策・実装に詳しい現場が主導的に動いてもらう必要がありました。

ガバナンスをいい感じにし隊では、そういった現場の実装的なAsIsと世間一般で言われるベストプラクティスとのすり合わせをする場としての意識をしていました。 具体的には、以下のようなことを議題として取り上げています。

  • AWSリソースにつけるタグのルール
  • Secretsの管理方法
  • AWS環境へのログイン方法

参加者のリソースの使い方に関する議論もあるなど、この頃はunofficialな場ではありました。

ISMS取得が決まってからの変化

2021/02に経営レベルでISMSの取得が決定しました。ゴールは変わりませんが、体制とスコープが変わりました。

f:id:kengoscal:20210902021416p:plain
情報セキュリティ委員会

情報セキュリティ委員長に代表取締役である松本( @y_matsuwitter )を設置することで、 体制がかなりオフィシャルなものになりました。

同時にメンバーをエンジニアに限定せず、経営管理や人事広報も含め各部から情報セキュリティ担当者を任命しています。 特に事業部については、広く様々な観点が必要であるため、複数人にコミットをお願いしました。 例えばDX事業部ですと、エンジニアとBiz(営業)を1名ずつ確保した形になっています。

なお、三井物産様と当社が出資して共同設立した「三井物産デジタルアセットマネジメント(MDM)」でアセマネサービスを手掛けるMDM事業部は、 全員MDMに出向し、全て先方の契約・管理するシステム上で開発・運用することになっているため、LayerXのISMS審査対象からは外れています(この辺はまた別ブログにて)。

ちなみに、ISMSの審査自体は全てZoomによるリモート審査でした。 ただISMSの範囲に含まれるオフィスセキュリティなどの物理的な確認はZoomで画面共有をしながら審査してもらいました。 たまたま出社している社員が指名され「今、情報資産の扱いはどうなっているか」といった抜き打ち的審査もありました。

どういうやり方をやっているか

現在は月1度の定例を設け、下記の情報を共有・議論しています。

  • 各部施策の進捗状況確認
  • ヒヤリハットの共有
  • 最新の動向の共有

また、全社を巻き込む議論については、GitHubのIssueでの議論をしています。 例えば次のIssueでは、LayerXに参加した時点で追加されるグループメールを、 事業の変化とともにその役割をおえたということで廃止を提案したものです。

f:id:kengoscal:20210902092932p:plain
全社メーリス廃止に関する議論

ここで重要なのは、背景とその際に上がった意見、そして結論に至った仮定を残し、 タイミング関係なくそういった文脈が参照可能な形で保存されることです。 そういった意味では、だんだんとGitHubを使わないメンバーが増える中、また 異なるやり方を考えねばならないフェーズに来ていると思っています。

これから何をやるか

さて、ITガバナンスが代表取締役配下におかれたということは、PDCAのように執行および振り返りを計画的にやっていく必要があります。2021年度の対応としてエイヤっとやることは、以下の通りです! ちょっとまだブログで公開できるか自信がないので、まずは量が多いことだけお伝えします。

f:id:kengoscal:20210902011346p:plain

従来だとこのようなガバナンス活動は、エンジニア以外が手動でGUIを操作したり、チェックを年数回確認することが一般的だったかと思います。 しかし、不確実性が高いスタートアップでは、ソフトウェア化によるガバナンスのコード化によって、計測可能な形でアジリティの高いセキュアな活動をしていかねばなりません。 従って、人を募集しようにもなかなか「これ!」といったポジション名を特定できません。 そこで私たちは、あまり見ない求人を作成しました。名付けて「屋台骨エンジニア」です。

LayerXらしい「複数事業の」「社内基盤を整え」「 メンバーの生産性をより上げていく」エンジニアをお待ちしています - 株式会社LayerX

新しすぎて「?」になること間違い無いかと思いますが、要は横断的な活動によって事業を支えるポジションです。 本件について詳しく聞きたい方は当社のポジションにアプライいただくか、あるいは気軽にken5scal個人のmeetyでお話・議論させていただけると幸いです

meety.net

meety.net

お待ちしております!

Go: json パッケージ Marshaler/Unmarshaler の実装例

LayerX インボイス を開発しているDX事業部の @yyoshiki41(中川佳希)です。

今回は、json パッケージにある Marshaler, Unmarshaler インターフェイスを満たす構造体を用いたアプリケーション実装の例を紹介します。

Marshaler, Unmarshaler インターフェイス

Go ではインターフェイスを命名する際、実装されたメソッド名や構造体名の末尾に er を付ける慣習があります。 Marshaler, Unmarshaler も下記のようなメソッドを実装するインターフェイスとして定義されています。

type Marshaler interface {
    MarshalJSON() ([]byte, error)
}
type Unmarshaler interface {
    UnmarshalJSON([]byte) error
}

pkg.go.dev

UnmarshalJSON は、インプットの json データをハンドルして Go の型へ変換したい時など、
MarshalJSON は、その構造体を json データとしてシリアライズして出力する際などに利用できます。

例. 文字列 "2021-08-31" を time.Time 型へ。json 出力時には "2021-Aug-31" へ。

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "time"
)

type T struct {
    Time time.Time `json:"time"`
}

func (t *T) UnmarshalJSON(b []byte) error {
    var s string
    if err := json.Unmarshal(b, &s); err != nil {
        return err
    }
    v, err := time.Parse("2006-01-02", s)
    if err != nil {
        return err
    }
    t.Time = v
    return nil
}

func (t T) MarshalJSON() ([]byte, error) {
    return json.Marshal(t.Time.Format("2006-Jan-02"))
}

func main() {
    b := []byte(`["2021-08-31"]`)
    t := []T{}
    err := json.Unmarshal(b, &t)
    if err != nil {
        log.Fatal(err)
    }
    for _, v := range t {
        fmt.Printf("struct: %q\n", v)
     
        b, err := json.Marshal(v)
        if err != nil {
            log.Fatal(err)
        }
        fmt.Printf("json: %s\n", b)
    }
}
struct: {"2021-08-31 00:00:00 +0000 UTC"}
json: "2021-Aug-31"

アプリケーションでの実装例

店舗側画面での権限を管理するアプリケーションを想定してみましょう。 各ユーザー(店舗のスタッフ)に対して、画面ページ(やデータソース)毎に権限を与えるものです。

UI イメージ

f:id:yyoshiki41-lx:20210830104135p:plain

UIクライアント側では、真偽値としてデータを解釈することが先に決まります。

サーバーサイドAPI、DB側ではどのようなデータの持ち方をするでしょうか? 今後もUIが必要とするAPIやデータリソースは変化するという前提とします。

DB

UI で必要となるデータ構造は上でも述べたように真偽値です。

f:id:yyoshiki41-lx:20210830135024p:plain

クライアントからのリクエストを保存する際は、UI側と同じ json データでDBに保存しています。(UI側のフォームの部品などが変わった場合にはバージョニングなどで対応が必要です。)

{
  "shop": {
    "view": true,
    "update": false
  },
  "product": {
    "view": true,
    "create": false,
    "update": false,
    "delete": false
  }
}

サーバーサイドAPI

アプリケーションコード側ではどのようにこのデータを扱うか? 真偽値によって、アクションを制御できるよう検討してみます。

例えばショップ情報の閲覧権限(view)が、

  • true であれば、 GET /shops という RestAPI へのリクエストが可能
  • false であれば、リクエストに対してステータスコード 403 Forbidden を返す

という具合です。

f:id:yyoshiki41-lx:20210830135932p:plain

今回は、json パッケージのカスタム Marshaler/Unmarshaler で解決する方法を考えてみたいと思います。

許可されているAPIエンドポイントを表す構造体をスライスで持つ PermissionShop 構造体を実装します。 (この構造体は、 Marshaler/Unmarshaler インターフェイスを満たしています。)

type Path string
type Method string

type API struct {
    Path   Path
    Method Method
}

type PermissionShop struct {
    View   []API `json:"view"`
    Update []API `json:"update"`
}

func (p *PermissionShop) UnmarshalJSON(b []byte) error {
    s := map[string]bool{}
    if err := json.Unmarshal(b, &s); err != nil {
        return err
    }
    for k, flag := range s {
        switch k {
        case "view":
            if flag {
                p.View = []API{
                    {"/shops", "GET"},
                }
            }
        case "update":
            if flag {
                p.Update = []API{
                    {"/shops", "GET"},
                    {"/shops", "PUT"},
                }
            }
        }
    }
    return nil
}

func (p PermissionShop) MarshalJSON() ([]byte, error) {
    s := map[string]bool{
        "view":   bool(len(p.View) > 0),
        "update": bool(len(p.Update) > 0),
    }
    return json.Marshal(s)
}

真偽値を用いてアクセスコントロールを行うミドルウェアなどでは、DB から取得してきた json を Unmarshal して、API エンドポイント構造体のスライスにしてハンドル出来るようになります。(真偽値で false の場合には、スライスの要素数が0として表現されます。)

// DB に保存されていた json データ
b := []byte(`{"view": true, "update": false}`)
// Goの構造体へ変換を行い、ミドルウェアなどで使用する
p := PermissionShop{}
err := json.Unmarshal(b, &p)
// PermissionShop が、リクエストされたAPIエンドポイントをスライスに含んでいる場合、リクエストが許可される
allowed := p.Contains(API)

まとめ

Marshaler/Unmarshaler を実装する構造体の実装例を作成していきました。 raw そのままでは扱いにくい json データも、アプリケーションコードで扱いやすい構造体に変換することが出来ます。 もしくは、構造体から json にシリアライズするなどが出来ると必要なコードをスッキリさせたり、ロジックの変化にも対応しやすくなります。

LayerX インボイスでは、 go generate と組み合わせてアクセスコントロールなどに使っています。

  • API エンドポイント一覧は、swagger 定義から生成
  • PermissionResource は、 UI コンポーネントのフォームから生成
  • UnmarshalJSON, MarshalJSON メソッドの実装は、コード生成と相性の良い汎化できる分岐ロジック

開発スピードに耐えれる、変更に強い設計を今後も追求できればと考えています。

興味が湧いたという方は、ぜひ一度話を聞きに来てみてください。

herp.careers

エントリーはちょっとという方、こちらから中の話を聞くこともできます!

meety.net