Next CurrencyとGAE/Go

sonatard氏:では、「Next CurrencyとGAE/Go」と題して発表させていただきます。まずは私の自己紹介ですね。

学生時代はインフラをやっていて、その後はC言語を書いてTCP/IP stackを書いていました。その後、メルカリ/ソウゾウ社に入社して、Google App Engine Go(GAE/Go)を触ってみて開発を行っていました。今はネクストカレンシー社でGoogle App Engine Goのエンジニアをやっています。

本日の話ですが、スタートアップでは良く「GAE/Goが良い」という話を聞くようになってきたと思います。オートスケールが便利だとかスピンアップが早いだとか、OSの管理がいらないだとか、プロダクトの開発に集中できるだったりとか。実際に私も使ってみて、やはり便利なんですが、検討が必要なポイントも多いです。

スタートアップでありがちなんですが、ほぼ間違いなくエンジニアが不足していると思います。その中でもプロダクトの開発、とくに差別化要因となるところだけにリソースを割きたいのが本音だと思います。プロダクトの開発においてやらなきゃいけないことはいろいろあると思います。DBの設計やAPIの設計、ビジネスロジックのコーディングなど、ここらへんに頭を使いたいというのが正直なところです。

もちろん他の部分も必ずやらなければいけませんし、手を抜いて良いところではないですが、重視するところは少し違うのかなと思います。それで、実際にこれらを考えていく上で、私たちネクストカレンシー社がどのようにGAE/Goを利用して、どのように技術選定してきたのかを紹介したいと思います。

技術選定の方針は「シンプル」

技術選定なんですが、まず、技術選定の基本方針はシンプルなものを選ぶことを心がけています。必要以上に利便性を上げるためにフレームワークやライブラリを採用しない。フレームワークを採用することで理解が容易になるものだったら採用する。シンプルなものだったらですね。ただ、コードの記述を短くするであるとか作業量を減らすことを目的としたものは採用しないことを心がけています。

実際にGAE/Goの話に入ります。まず、プロダクトの構成要素ですが、Google App Engineのスタンダード・エディションでは、1つのアプリごとに1つのプロジェクトを作ります。うちでは本番環境・QA・開発者ごとの個人環境の3つがあります。

サービスの構成です。

サービスというものがGAEにはありまして、サービスの単位でアプリケーションを分割することができます。そして、サービスの単位でインスタンスが作成されていきます。そして、サービスごとにスケールします。

(スライドを指して)ここにapiというのが3つありますが、このapiはアクセスが増えれば増えるほど自動的にスケールする。これがオートスケールと言われるものです。また、サービスごとにインスタンスの種類を選択できるので、apiであったりバッチ処理はスケールは必要ではないがパワーが必要であったり。そういう場合はCPUやメモリがたくさん乗っているものを選ぶということをします。

そしてGAEなんですが、L3のネットワーク、IPアドレスをほとんど意識することがありません。IPアドレスに対して何かをするということはほとんどなくて、GAEのサービスごとに自動的にドメインが割り振られます。

ここにルールが書いてあるんですが、「http://サービス名-dot-プロジェクト名.appspot.com/」となり、apiだったら「https://api-dot-project-.appspot.com」となります。さらに自分で独自に取得したドメインを紐付けることが可能で、「https://api.example.com」みたいなものを紐付けることもできます。

なぜサービスを分割しているか?

ネクストカレンシーではマイクロサービスではないですがサービスの分割をしています。マイクロサービスではないので、ソースコードは全サービス共通です。そして、サービス間の通信は基本的にありません。ただ、ユーザーからの入り口として分けています。

では、なぜサービスを分割しているかというところなんですが、スケールアップの単位であったりインスタンスの能力などによって、先ほど説明したとおり分けています。また、開発用のサービスを作っていて、例えばデバッグ用のサービスは本番にデプロイしたくないというものがあったら、サービスを分けて自分の開発環境だったりQA環境にだけデプロイするという使い方をしています。

あと、認証の有無ですね。例えばadmin系のサービスで社員だけが使う場合はGoogleのアカウント認証が必要なります。そういうときはそこだけサービスを分けて認証を描けるということをしています。

ただ、サービスを分けすぎるとそれぞれデプロイが必要になるので、デプロイに時間がかかって開発が不便になるのでそこは気をつける必要があります。

マイクロサービスが必要かどうか

認証について簡単に紹介します。

サービスを分けると、app.yamlというファイルがサービスごとにできます。認証ありの場合はlogin :requiredというのを書いてHTTPSの設定をして、あとは右のようにコードを書くだけで認証ができるようになる、とても便利なものになっています。

マイクロサービスが必要かどうかはプロジェクトを始める時に考えると思いまが、メルカリのCTOの方が発表されていた「オーナーシップを持つため」というのはとてもいい考えだと思います。API仕様さえ共有すれば、内部の技術選定は自由になります。ソースコードであったりAPIのプロトコルであったりDBであったり。機能に合わせた適切な技術選定が可能になります。AIならPythonを使うとか、APIならGoを使うみたいなことができます。

複数のサービスから、共通基盤として利用するもの。認証サービスなどはマイクロサービスとして分割してあると便利に使えるのかなと思います。ただ、マイクロサービスを使う弊害として、検討要素が増えます。コードリポジトリをサービスごとに分割するべきなのかどうかや、分割した場合に共通のドメインの変更をどのように管理するか、各サービスのログの管理はどうするかなど、検討要素が増えます。

ネクストカレンシー社としては、マイクロサービスを採用していません。そもそもサーバーサイドエンジニアが3名しかいないので、オーナーシップを持つために分割する必要がありません。将来を想定した共通基盤も今の段階で想定するのは困難だと思っています。あと、組織を分割してサーバと1対1で紐づくことも逆にリスクが有ると思っています。

横断的な大規模な変更施策が打ちづらくなりますし、組織が分割されると組織の部分最適に陥りやすくなるので、可能な限り少人数でモノリシックなシステムで運用していきたい。そして、マイクロサービスは目指すものではなくて、必要に迫られたら採るべき選択肢の1つとして捉えています。

デプロイのメカニズム

続いてデプロイの方法です。Google App Engineはこれだけでデプロイができます。

そうすると、先ほどサービス-dot-でURLが生成されると説明したんですが、今度はその前にバージョンごとにURLが発行されます。なので、バージョン単位でblue green developmentを実現する機能が標準で用意されています。

そして、下の図。バージョン名ごとにトラフィックを割り当てられるようになっていて、コンソールから簡単に100パーセントトラフィックの切り替えとか、コマンドからもできます。いざという時はロールバックも簡単にできるようになっています。バージョンの決め方は、開発中は基本的にブランチ名でやっています。そして、デプロイするごとに上書きされていきます。本番は日付とコミットハッシュでバージョンを作っていて、デプロイごとにバージョンを残してロールバック可能な状態にしています。

デプロイ後は必ずヘルスチェックをしたほうが良いです。デプロイスクリプトの最後に自動でチェックするのがおすすめです。そして、失敗したらマイグレーションしない。マイグレーションしなければ本番のユーザーのリクエストは飛ばないので。そして、デプロイできているかの確認と、実際にカールであるURLを叩いて、HTTPステータス200が返ってくるかどうかを確認しておくと、初期化ミスみたいなものは最低限発見することができます。

デプロイする上で注意することなんですが、タスクキューやcronの設定は1プロジェクトで1つしか管理されません。マイグレーションをしなくてもデプロイすると書き換わってしまうことがあるので気をつける必要があります。

アーキテクチャを決める目的

続いて、アーキテクチャの話をします。

アーキテクチャはよくあるレイヤードアーキテクチャを採用しています。アーキテクチャを検討する価値なんですが、アプリケーション開発に集中できる、機能追加のたびにアーキテクチャを考えるのは非効率、統一した方針のコードを書いておけば新メンバーのキャッチアップが容易であるということです。

正解はないとおもいます。決めること自体にすごく価値があると思っていて、必要以上に正解を追い求めることはリスクであると思っています。「正しいDDDとは何か?」という議論がよくあると思いますが、これを目的にするとアーキテクチャを変えすぎて開発が進まないという事が起きてしまうので、何を目的にアーキテクチャを決定するかがとても大切だと思っています。

そのアーキテクチャを変えようとしたり、なにか選択したときはかならず目的を達成する必要があると思っています。例えばテスタビリティを上げるために、DIできるようにレイヤー分割しようと思ったなら、かならずテストを書きましょうという話ですね。コードがきれいになるのでそれに満足するのであればやらないで機能開発を優先したほうが良いのではないかと思っています。

うちとしてはアーキテクチャを決める一番の目的として、お金を扱うのでドメインの計算がけっこうあるんですね。ビジネスロジックを持っているので、そこを重点的にテストしたかったというのがあります。ドメインのテストを独立して実施できるようにしてきました。

それで、もしビジネスロジックが独立していない場合のテストは、HTTPリクエストを作成したり、DBの環境を作らなきゃいけなかったり、DBのデータを準備しないといけなかったり、いろいろ手間な事が多かったり実行速度が落ちるという問題があります。

あと、大量のモックを挿入しなきゃいけないとか、テスト環境を作るのがめんどくさいとか。テストってそもそもめんどくさいと思っている人がいると思いますし、実行が遅くなって環境構築が手間になると、よりテストを書くための心理的ハードルが上がるので良くないなと思っています。

アプリケーション層の構造

うちでは、この中ではアプリケーション層だけとくに重要なので説明します。

アプリケーション層では基本的にユーザーのリクエストに応じてデータベースからデータを取得して、何かドメインを生成して、ドメインのメモリを変更して保存するということだけをやっています。そして、こちらが悪い例です。

アプリケーション層にビジネスロジックを書いてしまった場合。下のコードはビジネスロジックが漏れていて、購入処理をテストしようと思った時にアプリケーション層が必須になってしまっています。そうなると、DBのモックの挿入が必須になるのでテストのハードルが上がります。

結構アプリケーションとドメインの境界とか、ドメインサービスって話はいろいろ議論されているところだと思いますが、『実装ドメイン駆動設計』には「アプリケーションレイヤに位置づけられるアプリケーションサービスは、ドメインモデルの直接の利用者ではあるが、自分自身はビジネスロジックを持たない」というこの言葉に背中を押されました。

フレームワークの設計について

次に、フレームワークの設計です。先ほども出てきましたがシンプルなものを選びたいと思っています。そんな中で、Webアプリケーションフレームワークに求める機能は何かと言うと、私たちに必要なのはミドルウェアを挿入できるということと、ルーティングの設定が簡単にできるということだけでした。その中で選定した結果としては、chiというものが一番良かったかなと思っています。

下に書いてあるとおり、echoやginは標準のnet/httpのServeHTTPメソッドとはインターフェースが異なりますが、chiは異なりません。なので、いままで書いてきたコードとほとんど同じようにミドルウェアを書くことができるので、とてもおすすめです。

gorpはORMではないんですが、DBのライブラリでGoの構造体とDBのテーブルをマッピングするだけのとてもシンプルなライブラリです。gormと比較すると、テーブル間のリレーションを解決してくれないので機能は少ないんですが、とてもシンプルで使いやすいです。

これも「これでいいのかな?」とあまり自信がなかったんですが、Goで有名なmattnさんがこれを使っているということで、今は安心して使っています。

APIのプロトコル選定

続いて、APIのプロトコルの選定です。実際にサーバーサイドを作り始める時に一番最初に考えなければいけないことだと思うんですが、クライアントと何で通信するかということです。一般的に、RESTful APIかJSON-RPCかgRPCが多いのかなと思います。ただ、RESTful APIはRESTful APIの仕様に則ってHTTPメソッドを使い分けるのは設計に苦労する割にあまり恩恵はないのかなと思ったので採用しませんでした。

JSON-RPCかgRPCとなると、GAEはgRPC未対応なのでJSON-RPCなのかというと、GRPCを使いたいです。なぜgRPCを使いたかったというと、Protocol Buffersで定義してクライアントとAPIのソースコードを自動生成できるところが使いたかったということで、GRPCというプロトコルが使いたかったわけではなかったということです。

どうしたかと言うと、Protocol Buffersを使いながら、通信は普通のHTTPメソッドを使うということをやっています。いいとこ取りな感じですね。URLで実行するメソッドを指定していて、このようにhttp://api.exanole.com/v1/Exchange/Buy/みたいなものが実行されたらそれに対応したRPCが実行されるというものです。

HTTPメソッドはPOSTのみを利用していて、リクエスト、メソッド、レスポンスの方はProtocol Buffersで定義しています。これはgRPCと同じなので、gRPCのツールがだいたい使えます。それで、Protocol Buffersの定義ファイルから、サーバーサイドのGoのコードと、フロントのTypeScriptとアプリのSwift、Javaのコードを生成しています。

下のようにProtocol Buffersの定義で、リクエストとレスポンスとメソッドの定義を書くと自動的にコードが生成されます。

Protocol Buffersを使う利点

何が良いかと言うと、RESTful APIと比較すると、設計が楽です。RESTだとHTTPメソッドを何にするとかURLをどうするかとかちょっと面倒なんですが、基本的にRPCならコードの名前を使えばいいので簡単です。

JSON-RPCと比較するとProtocol Buffersを使っているのでクライアントはAPIの変更をコンパイル時に検出できます。一方の悪い点は、標準ではないので他社との通信や公開APIには向いていません。

gRPCと比較して良い点は、HTTPなので従来の環境で動きます。あと、今まで書いていたコードがそのまま動きます。gRPCはインターフェースが違うので、gRPCのコードを書く必要があります。ミドルウェアもgRPC専用のインターセプターを使わなければいけません。悪い点は、streamなどgRPCの機能が使えないところですね。

実際に使ってみて地味に良かった点ですが、Stackdriverのログが見やすいことです。

URLだけでメソッドを特定できます。RESTだとPUTというメソッドと、左のURLを見て初めて何の処理をしているかがわかりますが、JSON-RPCだと、エントリーのURLがすべておなじになってしまうので、全部「/」で書いてあって中のボディを見て初めて何のメソッドを実行しているかわかる。

そして今回の仕組みだと、メソッドを実行しているのでそれがそのまま実行されていることがすぐに分かるのが以外に良かったところです。

Protocol Buffersを使っている場合、gRPCも同じなんですがJSONではないので気軽にデバッグができなくなってしまうという問題があります。これに対応するために、HTTPヘッダーを見てContent-TypeがProtocol Buffersのタイプではなかったら、JSONでデコードしてJSONで返すということをやっています。なので、普通に普段どおりのデバッグもできるようになっています。

GRPCではgrpc-gatewayを使って実現していることですね。

今まで独自のやり方をやってきて、使っていてとても便利だったのでなにも困っていなかったんですが、またこれを後押ししてくれるフレームワークがちょうど出てきました。twich社のRPCフレームワークで「twirp」というものが出てきて、まさにうちがやってきたことと同じことをしているフレームワークがリリースされました。

ただ、マイクロサービス間の通信を想定して作られたフレームワークなので、まだGoしか対応していません。クライアントの利用はできないのがデメリットですが、もしそういったものが出てきたら、これを使うのはありかなと思っています。

実際、GRPCを使うのかHTTPでProtocol Buffersを使うのかというところなんですが、目的は構造化された定義ファイルからソースコードを生成したいだけなのか、streamみたいにGRPCを使いたいのか。あと、GRPCを使うとけっこう変更があるので、十分な開発リソースがあるかどうかも重要なのかなと思います。

APIの開発方針

続いて、APIの開発方針を紹介します。APIは変わる前提で、クライアントの開発を止めないように書いていました。3回ほど大幅な変更をお願いしています。私の持論なんですが、最初から仕様を網羅することは無理だと思っていて、「リリースまで変わり続けるのは許して」という感じで開発していました。ただ、逆にクライアントからの要望は極力受けるようにしてきました。

そして、仕様を網羅される瞬間はリリース直前で、リリース直前が一番いいAPIを作れる瞬間なのかなと思っていまして、それまで直し続けるということをしてきました。良いものを作りたいという意識が共有されているメンバーなので、これは感謝しています。

続いて、APIクライアントとドキュメントですね。開発中のAPIを叩く手段は、みなさんどうやって提供しているかというと、curlのコマンドを叩いたり、開発者ごとに独自クライアントをこっそり持っているとか、開発中のアプリから叩くとかいろいろあると思いますが、Postmanというツールがすごく便利です。

これをチームで共有すると、サーバーサイド、クライアントサイドのエンジニアが気軽にAPIを叩ける環境を用意できます。

GUIのツールなんですが、左のExchangeというところにいくつかメソッドが書いてあって、BuyとSellというメソッドがあります。そして、リクエストを指定するとレスポンスが返ってきます。保存しておくとチーム全員で自動的に共有されていつでも叩ける環境が用意されます。

また、GAEではデブロイするだけでブランチ専用のURLが発行されるため、アクセスするURLを変数にしておくことで気軽にアクセス先を変えられます。

環境ごとに環境変数を設定でき、本番とQA環境と自分のローカル環境の切り替えが簡単にできて便利です。あと、さらに良い点として、レスポンスを先ほどの環境変数に設定できるので、Access Tokenの管理がすごく楽です。だから、Tokenを取得するAPIを勝手にアクセストークンが設定されて、実際にリクエストを送る際のHTTPの環境変数は、このAccess Tokenの変数を参照しているので、気軽にAPIを叩けるメリットがあります。

このメリットは、問題発生時に問題の切り分けが容易なるということですね。サーバーサイドに問題があればクライアントには問題がないことがわかるので、クライアントサイドのエンジニアも自分が叩いてエラーになった時にどこにエラーがあるのかわからずサーバーサイドエンジニアにいきなり頼みづらくなってしまうと思うんですが、そういうことがなくなります。

また、実際に動くものがあるのでこれ自体がドキュメントの変わりになります。Protocol Buffersの定義ファイルと合わせると、仕様の理解がだいたいできるようになります。また、最悪わからないことがあれば聞いてもらって解決しています。そして、Swaggerも必要ないという状況です。

configの実現方法

今度はconfigの話です。開発プロジェクトごとにconfigを設定したいことはどのプロジェクトでもよくあることかなと思います。例えばSQLの接続先が本番とQAとローカルでは違うというケースですね。

configの実現方法として、yamlやtomlで独自のconfigモジュールを作るというものがありますが、GAEではapp.yamlの環境変数に設定するのがセオリーです。

では、それをどうやって切り替えるようにするかというと、configの切替方法としてcomfig.yamlという別のファイルをapp.yamlから作っておいてincludeします。そしてデプロイ時にconfig.yamlをシンボリックリンクで切り替えるということを私たちはしています。

そして、configの読み込み方なんですが、環境変数を読み込むためにenvconfigというモジュールを採用しています。そして、configの値を必要になったタイミングで環境変数から読み込むのは処理としては遅いので、必ずinit時に構造体に読み込んでおくようにしています。

一番上のconfigという構造体を定義して、あとはenvconfig.Processを実行するだけで読み込めるようになっています。あと、configで読み込んだ値をapplication層から直接参照することはせず、init時にapplication層の構造体にセットしておきます。これをしないと、テスト時にconfigモジュールが必須になってしまうので、テストしづらくなってしまうことを避けています。つまり、configモジュールが必要になるということは、環境変数の設定が必要ということになるので、そういうことは避けたほうがいいと思います。

セキュリティとCI環境

では、ちょっとセキュリティの話をします。GCP(Google Cloud Platform)のサービスでCloud KMSというサービスがあります。個人情報や外部に流出してはいけない情報を暗号化することができます。

ちょっと長くなるので省略しますが、gcpug/nouhauというページがあり、メルカリのsinmetalさんがまとめてくれたんですが、どこかで発表すると言っていたのでそれを待ってください。

開発環境についてはとくに制限していなくて、goimportsを使ってね、ということだけ言っています。また基本的にGAEの開発環境はコマンドラインでだいたいすべての操作ができるので、お試し以外では基本的に触らず、コマンドで実行するようにしています。コマンドであればすべての手順が残るので、調べれば何をやっているかがわかります。

コマンドラインで管理できれば環境構築も構成管理可能になるので、Infrastructure as a codeが実現できています。また、一度に順番にコマンドを叩くだけなので、shell scriptで順番に叩いているだけです。OSの状態を管理していたらdocker, ansible, chefが必要になるかと思いますが、それ用のツールは使っていません。

CIの環境はCircle CIを使っていますという話です。

スライドの真ん中に、「GAE/Go用のCIイメージ mercari/docker-appengine-go」というものがあります。私がアッテで実際に使っていたイメージで、メルカリの方がメンテしてくれています。これを使うだけで、goapp testやgcloudによるデプロイが可能になります。不足しているツールは、protocのイメージとかはこのイメージには入っていないので、このイメージをベースにして独自のイメージを使うことをすると便利に使えます。

そんな感じで、あと1つだけ。datastoreのバックアップはGCSに取れるんですが、今度はそれをBigQueryに入れるというツールがあって、これもメルカリのsinmetalさんが作っています。「favclip/ds2bq」というツールなので、ぜひ使ってみてください。

一応まとめをすると、うちはこういうツールを使っています。

以上です。ありがとうございました。