少人数での爆速開発を目指してgolang×GCPの技術選定をした話
この1年くらいでgolangとGCPを使ったWebアプリケーションをフルスクラッチで開発したので、その際の技術選定の理由だったりを言語化して残しておきたいと思い、HHKBを手に取りました。
少し長くなってしまいましたが、どなたかの参考になればと思います。
どんな人が書いてるの?
立ち上げ期のスタートアップCTOをしています。雑に言うとフルスタックエンジニアです。
開発歴はざっくり、Androidアプリの開発歴が一番長くて3年、バックエンド開発(Elixir × GCP)に転身して1年ほど担当、その後、これから言語化するプロジェクトを1年くらいかけてgolangで構築したところです。
今回の範囲からは外れますが、並行してNuxt.js×TypeScriptで書かれたフロントエンド開発も行っていたので、今はその辺りも一通り習得しています。
1. 方針
表題にもある通り、少人数での爆速開発を目指して、技術選定を行いました。
ここでいう爆速開発というのは、素早く機能を開発し、どんどんリリースするということです。
少人数で素早く機能をリリースするためには、開発・運用にかかる工数を極力減らす事がとても重要です。特に運用の工数が膨らむと、そもそも開発の時間が取れないという自体に陥ってしまうので、運用の工数を極力減らせる構成を目指しました。
爆速開発のための開発方針
爆速開発のために、以下の開発方針で開発しています。
- クラウド、XaaS、OSSをフル活用
- 自動化できるならする
- 管理するものは最低限
個別に説明します。
クラウド、XaaS、OSSをフル活用
今ではAmazonさんやGoogleさんがGCPやAWSといっためちゃくちゃ便利なインフラ基盤をクラウドで提供してくださっているので積極的に使います。また、認証基盤や決済などの要素技術や機能もXaaSという形で機能を提供しているサービスが多くあります。それらを効率的に使うことで、爆速で機能を提供する事ができます。
また、クラウドやXaaSといったマネージドなサービスを使うことで、運用時のコストを格段に下げることができるので、「金銭面の問題で無理!」とかでなければマネージドなサービスを使うことをオススメします。「金銭面の問題で無理!」と言いましたが、開発・管理する人件費を計算すると、マネージドなサービスを導入した方が「結局安くあがる」というケースも多いので、人件費も考慮して、試算することをオススメします。
OSSも同様に積極的に使っていきます。エンジニアなので、自分で作りたくなる衝動にかられる時もありますが、作りたいという気持ちにグッと抑えて、車輪の再開発は駄目という強い意志を持ちます。例外として、欲しい機能がOSSのごく一部みたいな場合は、OSSの実装を参考に自分でサクッと作ってもいいとは思います。
自動化できるならする
自動化できるものは極力自動化します。人がやらなくて良いことは機械にやってもらうのは大事です。
具体例としては、テストやlintの自動化は勿論のこと、APIのスキーマ定義からコードやAPIドキュメントが自動生成できたり、DBスキーマからER図が生成できたり、今では自動化のツールが多くあるので、それらを積極的に使うことで開発の工数を減らしています。
管理するものは最低限
管理するものは極力減らします。本当に必要になるまで増やしません。
クラウドとかを使っていると、逆に気軽にコンポーネントが増やせちゃったりするんですが、障害点が増えたり、構成が複雑になってしまったりすることがあるので、必要に迫られるまで増やさないという方針で開発しています。
2. 使用している技術一覧
使用している技術はざっくりと以下の通りです。
クラウドのサービスやOSSが混じっていますが、技術選定においては、クラウドサービスの足りないところをOSSで補ったりするので、あえて分けずに書いています。
- アプリケーション全般
- クラウド: GCP
- 言語: golang
- 実行基盤: GAE
- インタフェースと認証
- 認証基盤: Firebase Authentication
- WEB API: protocol buffers on HTTP 1.1
- ルーティングFW: Echo
- DB周り
- DB: Cloud SQL for MySQL
- ORM: sqlboiler
- ER図生成ツール: schemaspy
- マイグレーション: golang-migrate
- CI/CD基盤 & テスト
- CI・CD: Github Actions
- アサーション・モック: testify
- データ生成ツール: testfixtures
- コード診断: gosec & reviewdog
- ログ周り
- 収集基盤: Cloud Logging
- ログライブラリ: zerolog
- 監視: Cloud Monitoring
- 分析: BigQuery
これから個別に深堀りしていきます。
3.アプリケーション全般
まず、表題にもなっているgolang × GCPの構成をなぜ選んだかについて触れて行きたいと思います。
3.1 クラウド: GCP
GCPを選んだ理由ですが、単純に前プロジェクトがGCPでAWSやAzureより慣れていたからです。
また、今回作成したサービスが比較的なニッチな領域を狙ったサービスだったので、GCPの常時無料枠が多い料金体系と相性が良いと感じたのも理由の一つですが、ちゃんと比較していないので大きなことは言えません。
3.2 言語: golang
次になぜgolangを選んだかですが、筆者が思うgolangの利点は以下の通りです。
- サーバーレスとの相性が良い(⭐️最も重要)
- 静的型付け言語
- 開発コミュニティが一定サイズ以上で、人気がある
- パフォーマンスがいい
それぞれ詳しく見ていきます。
サーバーレスとの相性が良い(最も重要)
サーバーレス構成(=オートスケール)にすると、サーバーの運用コストをかなり減らすことができるので、この点は個人的に最も重要でした。
具体的に、golangは以下の理由でサーバーレスと相性が良いと思ってます。
- 起動のオーバーヘッドが小さい
- Cloud Functions, GAE(Standard)で実行基盤が用意されている
- シングルバイナリでDockerとの相性も良く、Cloud Runでも使いやすい
静的型付け言語
筆者は以下の利点から、圧倒的に静的型付け言語派です。
- コードの認知コストが下がる
- テスト時に型が限定できるので、検査する項目が減る
- 実行時エラーより、コンパイル時エラーの方がフィードバックループが小さく、開発体験が良い
開発コミュニティが一定サイズ以上で、人気がある
以前、Elixirを使ってる時に苦労したことが多かったので、、、、大事です。(Elixir自体は凄く良い言語だと思ってます。)
- 開発コミュニティが大きいことは、OSSの種類の豊富さや、OSSのバグが少なくなることにつながる
- 採用募集時に人を集めやすい
普通は会社や個人として言語のナレッジも大きな選定要因となると思うのですが、筆者のナレッジとしては、ElixirやKotlinで上記の条件にすべて揃うものがなかったため、今回はgolangを選択しました。
TypeScriptも上記の観点を満たしており迷ったんですが、
- 純粋に処理が早い。
- code formatが標準。(linterとかの設定考えなくていい)
- Typescriptだと実行時にJSなので、実行時エラーとか出たら運用辛そう。
- 直近で書いてたElixirと文法が近い(エラーハンドリングとか)
- golangの思想に触れてみたい。書いてみたい。というモチベーション。
等の理由から、golangを選択しました。
今の所は非常に満足しています。
2.3 実行基盤: GAE
アプリケーションの実行基盤として、GAEを選んだ理由です。
GCP上でgolangで選択するサーバーレスプラットフォームとしては、GAE, Cloud Run, Cloud Functionsなどがありますが、モノリスなメインのWebアプリケーションの用途として考えると、ルーティングによるロジックの共通化やDBコネクションの再利用などがしたいので、GAE or Cloud Runの二択になります。
比較記事は優れた先人のものがあるので詳細は譲りますが、今はよりデプロイなどの管理が簡単なGAEで事足りているので、GAEを利用しています。
今後は必要に応じ、Cloud RunやCloud Functionsを併用していくことも考えています。
3 インタフェースと認証
インタフェースと認証はフロントエンドとのつなぎ込みに関わる部分です。
今回のプロダクトでは「Nuxt.js × TypeScript」でフロントエンドを開発しており、フロントエンドも含めた開発効率を考えて技術選定を行いました。
また、今回のプロダクトではユーザーのロールにより、求められている機能やUIが大きく異なっていたため、ロールごとのAPIの提供が求められていました。
3.1 認証基盤: Firebase Authentication
認証基盤(IDaaS)はFirebase Authenticationを使用しています。
- フルマネージドで管理が楽
- 様々なSNSの認証も簡単に取り込む事ができる
- カスタムクレームの機能で権限の管理などができる
- Webフロント、バックエンドともにSDKがある
以前に別プロジェクトで使用したことがあり、上記の点で非常に満足していたため、そのまま採用しました。パスワードの管理を自前のDBで管理しなくて良くなり、手軽にセキュリティを高められるのがとても良いです。
3.2 WEB API: protocol buffers on HTTP 1.1
WEB APIの管理をどうするかの部分です。
筆者の過去の経験上、WEB APIのドキュメント管理を継続的に続けていくことは、工夫をしないとなかなかの工数を奪われます。過去にAndoroidのエンジニアをしていたときには、WEB APIを使う側でしたが、「ドキュメントの修正漏れで実態と乖離している」、「型の情報が正しくない」、「バージョニングが管理さていない」等のWEB APIの不備に泣かされ、開発効率が著しく落ちた経験があります。
今はOpenAPIやgRPCなどのスキーマ言語とコードの自動生成を取り入れることで、それらを防ぐ事ができます。
- スキーマ定義によるWEB APIの一元管理
- ドキュメントとWEB APIの反映漏れがなくなる
- 型情報が明確になる
- バックエンドのgolang、フロントエンドのTypeScriptの型ファイルが自動で生成できる
具体的な実現方法としては、「OpenAPI 2.0 × go-swagger」や「gRPC × gRPC WEB」、「protocol buffers on HTTP 1.1」を検討しました。「protocol buffers on HTTP 1.1」というのが少しわかりにくいですが、スキーマ定義やドキュメント生成はprotocol buffersで行いつつ、通信自体はピュアなHTTPを使う方式です。
本プロジェクトでは最終的に「protocol buffers on HTTP 1.1」の方式を採用しました。
理由としては、純粋なドキュメント管理として「Open API」vs「protocol buffers」で比べた時に、protocol buffersの方が軽量で管理しやすいと感じたこと。
また、「gRPC」については、WEBとの通信の際、Web BrowserがgRPCの内部で使用しているHTTP/2の使用を強制することができないため、HTTP/1.1とHTTP/2を変換するためのWebプロキシを用意する必要があります。2020年の頭の段階で筆者が調べた範囲では、個別でEnvoyのプロキシサーバーを立てて管理する必要があり、管理するコンポーネントを増やしたくなかったため、採用を断念しました。
「protocol buffers on HTTP/1.1」の方式では、protocol buffersの利点を用いつつ、通信は普通のHTTPサーバーと変わらないので、ルーティングを自分で書く必要があること以外は特に不便を感じていません。
また、スキーマ定義言語といえばGraphQLも人気ですが、この時は慣れ親しんだREST APIから離れて新たにインプットする余裕がなかったため断念しました。
※ 2021年現在、Cloud EndpointsとESPv2の連携でgRPCとHTTP/JSONの変換層が比較的簡単に作成・管理できそうなので、新しくプロダクト作る際には、gRPCで作るかもしれません。
3.3 ルーティングFW: Echo
3.2で「protocol buffers on HTTP/1.1」を採用したため、ルーティングの実装はgolangで独自に行う必要があります。
ルーティングのFWとして、Echoを採用しました。
golangの標準ライブラリで書いてもいいのですが、使えるものは使うスタイルです。
選んだ理由は以下の通りです。
- pathをgroup化でき、middlewareが適用できる
- query paramsやpath paramsの取り出しが楽
- 速い
ginと迷った記憶がありますが、処理が速いのを売りにしていたEchoを選びました。
正直、上記の機能を備えていれば何でも良いとは思います。graphqlやgRPCに乗り換えたら移し替えたりする部分でもあるので、依存し過ぎない形で作るのが大事だと思います。
4. DB周り
次に、何かと気を使うDB周りの技術選定についてです。
DB周りの開発効率化としては、DBのスキーマを起点として、コマンド一つでER図やらgolangのソースソースやらが自動生成されるようにして、開発スピードの向上を図っています。
4.1 DB: Cloud SQL for MySQL
DBはCloudSQL for MySQLを利用しています。
MySQLを選んだ理由は純粋に慣れていたからです。
RDBだと辛いようなアクセス量が来る要件なら他のDBを使えば良いと思いますが、正規化するデータを扱うのであれば、個人的にはRDBが扱いやすいです。
4.2 ORM: sqlboiler
ORマッパーの技術選定は、実は一番紆余曲折した部分になりますが、最終的にsqlboilerを選びました。
プロトタイプの作成も含めると、GORM/v1 ⇒ gorp & squirrel ⇒ sqlboilerと途中で置き換えている部分になります。
sqlboilerの選定理由としては、DBスキーマからgolangのコードを自動生成できる部分が決定的で、sqlboilerを選択しました。これにより、開発効率がかなり上げられたと思います。
DB周りの自動生成の方向性としては、**1.モデル起点(DBスキーマ ⇒ golangのモデル)と2.スキーマ起点(golangのモデル ⇒ DBスキーマ)**の二方向がありますが、GOLM/v1のマイグレーション機能が既存のカラムやインデックスの変更ができないという事情もあり、後者を選択しました。
また、QueryBuilderも非常に使いやすく生のSQLに近い形で楽に整形できる点、template機能でforkをせずともコード拡張ができる点も気に入っています。
4.3 ER図生成ツール: schemaspy
ER図の自動生成ツールとして、schemaspyを利用しています。
起動しているDBに向けて実行することで、ER図を自動で生成してくれます。
楽しながらインデックスや外部キーなども確認しやすくなり、とても便利でオススメです。
4.4 マイグレーション: golang-migrate
マイグレーションツールはgolang-migrateです。
DBマイグレーションツールはSQLが書けて、バージョン管理ができれば何でも良いと思い、Star数で選んだ記憶があります。選定にはそんなに脳みそ使ってないです。
4.5 開発の流れ
実際にDBスキーマに変更がある時の開発の流れとしては、以下のような流れになっています。
-
golang-migrate
のマイグレーションファイルの作成 - shellコマンドの実行
- 生成されたER図とgolangのソースコード(sqlboiler)を確認
- 生成されたコードを使って、golangアプリケーションの実装
マイグレーション用のSQLを用意してコマンドを実行するだけで、ER図やらソースコードやらが勝手に作られるので、めちゃくちゃ楽ですし、DBのスキーマとソースコードのズレがなくなるので、外部キーの貼り忘れなど開発の漏れも少なくなり、開発体験としてとてもいい状態を保てています。
2のshellコマンドの中身に簡単に触れておくと、以下のようなステップになっています。
-
docker-compose
でlocalにMySQLサーバーを構築 -
golang-migrate
でMySQLのスキーマを更新 -
sqlboiler
でgolangのモデルを自動生成 -
schemaspy
でER図の作成
この自動化はかなりオススメなので、よかったら試して見てください。需要があれば、そのうち記事化しようと思います。
5. CI/CD & テスト
自動化の効用が高いCI/CDの部分です。
Github Actionsを利用し、GitHubのトリガー起点でテストの実行、gaeへのデプロイ、gosecによるコードチェックなどを行っています。
ちゃんと序盤から導入したおかげで高い開発効率を維持できた部分であります。
5.1 CI/CD基盤: Github Actions
CI/CDの実行自体はCloud BuildでもCircle CIでも何でも良いと思いますが、GitHub一つでCIまでできるのはとても便利です。
5.2 アサーション・モック: testify
テストライブラリはgolangのアサーションライブラリのデファクトスタンダードであるtestifyです。
testifyには、アサーション、モック、テストスイートの機能がありますが、アサーションとモックの機能を利用しています。
アサーションはテスト修正時のコストに結構影響があると思っているので、標準のものより見やすいものを使ったほうが良いかと思います。
5.3 データ生成ツール: testfixtures
テスト開始時のテストデータの生成としてtestfixturesを利用しています。
yamlでfixtureデータを定義することができるので、便利です。
ただ、細かい条件でテストしたい場合には向かないので、そういったテストは個別にtestコード内で生成した方が良いと思います。
5.4 コード診断: gosec & reviewdog
最近入れた部分ではありますが、簡単にコード診断をして、セキュリティをチェックしてくれるツールとして、gosecを利用しています。
GitHub Actionsでの実行はreviewdogで行っています。
詳しくは、下記を参考にさせていただいたので、御覧ください。
6. ログ・監視周り
最後にログ周りの技術選定になります。
Cloud Loggingを起点で行っています。
6.1 収集基盤: Cloud Logging
ログの収集基盤はCloud Loggingを利用しています。
GAEやCloud Runを使っていると、標準出力にログを吐くだけでCloud Loggingが収集してくれ、sinkという機能を使えば、Cloud StorageやBigQueryにそのままデータを移してくれるので、とても便利に使わせて頂いています。
6.2 ログライブラリ: zerolog
golangのログライブラリはzerologを使用しています。
選定した理由は以下の通りです。
- 速いこと
- Cloud Loggingの出力形式とマッチした形で出力できること
ログ出力において、速いことは最も重要です。ログ出力はユーザー体験に直接影響しない機能なので、ログ出力の処理が重くてAPIレスポンスが遅くなり、ユーザー体験に悪影響を与えてしまっては本末転倒になります。速いログライブラリとしては、zerolog, zapが挙げられますが、Cloud Loggingの構造化ロギングに適したログ出力がzerologの方が行いやすかったため、zerologを採用しました。
6.3 監視: Cloud Monitoring
GAEやDBの負荷項目の可視化とアラートの設定には、Cloud Monitoringを使用しています。
負荷以外にも、slow-queryの検知などもCloud LoggingとCloud Monitoringを組み合わせて行っています。
6.4 分析: BigQuery
分析ツールとしてはBigQueryを利用しています。
正直、BigQueryはまだ使いこなせているとは言えないので、知見があれば教えて欲しいです、、、。
7. 最後に
今回は、少人数での爆速開発を目指してgolang×GCPの技術選定をした話ということで、その技術選定の結果とその過程をつらつらと書き起こしてみました。フルスクラッチで作れたのもあり、総じて開発効率としては結構良い技術選定ができたのではないかと思っております。
最後に、少人数でのと書きましたが実はこれらの開発はすべて一人で行っております。
なので、もっとこうした方がいいよ。等のフィードバックがありましたら、コメント欄でもTwitterのDMでも、気軽にいただけると嬉しいです!
よろしければTwitterの方も覗いてやってください。
Discussion