WebAPIからバッチ処理までの工夫

真野隼記氏:ここから話は変わって、実際に使っている、導入の部分で工夫しているところをいくつか紹介したいと思います。(スライドを示して)Goの導入ですが、Goのバックエンド中心で強いのはだいたいそのとおりで、WebのAPIサーバやバッチ処理、BOTのアプリやCLIツールなどをいくつか導入しています。

細かな工夫について、WebのAPIやバッチで特に生産性に加味しそうなところを話していきたいと思います。

(スライドを示して)まずWebのAPIですが、特に技術選定のリーダーポジションでよく思うことです。デファクトスタンダードなWebのフレームワークがないことで、自分の会社の中なのに採用技術がバラバラなことがけっこうあります。echoやginやchiやopenapi-feneratorといったところです。

「これはどげんかせんといかん」と一瞬思ったのですが、よくよく考えてみるとそのナレッジはほかで活きることが多いです。隣がchiを使っていて、うちがgo-swaggerだったのですが、すぐ開発できます。というのも、どのWebフレームワークもだいたい標準のnet/httpのラッパーにすぎないという話があります。

(スライドを示して)下側にアプリケーションにおけるコードの比率があります。Goはnet/http自体がかなりWebフレームワーク寄りになっているので、あまり採用技術を気にしなくても大丈夫なのかなと思っています。そのため、ここを標準化しようということではなく、入れている前提で別のところに注力してもいいのかなと考えられるのが、いいところだと思っています。

(スライドを示して)デファクトスタンダードがないライブラリがあまりないというところの補足です。ないこと自体は私も別にいいかなと思っていて、逆に多様な開発ポリシーに沿って、目指したライブラリやツールを自由に選べるのが利点だと感じています。

そのため、いろいろなライブラリが乱立しているように見えますが、ポリシーが似ていればそんなに種類はありません。ポリシーがだいたい同じであれば、同じような選定になるのもおもしろいポイントだと思っています。

(スライドを示して)コードから生成したいのか、スキーマから生成したいのかで、下の例だとddlからsqlのデータベースに何かスキーマを作って、SQLBoilerでコードを生成するといった話や、薄い責務にしたいのかフルスタックにしたいのかなど、いろいろあるかと思っています。

フルスタックや薄い責務もいろいろあると思います。例えば、スキーママイグレーション機能が必要なところによっては、もう少し薄いライブラリにするというところです。

スキーママイグレーションは、FlywayやJavaのツールを使おうとなると、「それならGoはこちらのライブラリ」と(選択肢が)だいたい狭まってきて、自分たちチームの制約や前提に応じて選べるのが、Goの本当にいい文化かなと個人的には思っています。

AWS Lambdaを活用したTips

サーバーサイドの話です。今回はAWS Lambdaに絞って話しますが、構築する場合によく出てくる要望で生産性にすごく響くと思ったので、ここだけ説明したいと思います。よくある要望は、ローカルのSPA(Single Page Application)開発。画面開発だとポートをListenするサーバとして起動させたい、WebのAPIを叩きたいというものです。

プロダクションはAPI GatewayやLabdaやAppSyncかもしれないですが、そういった構成で、2つの起動モードで動かしたいということがよく出てくる要望かなと思っています。

最初はけっこう詰まりました。最近は社内でも広まっていますが、AWS Lambda Go Api ProxyパッケージというAWS Labsが出しているパッケージを使うと、通常のWebアプリフレームワークで構築したアプリもLambda化できて便利なので紹介します。

これはどういったノリかという話です。コードは小さいのでザクっとという感じですが、httpadapterパッケージを利用して、今回使うWebフレームワークのHandlerクラスをラップします。

ProxywithContextで例えばAPIGatewayのリクエストを引数に渡すと、例えばAWSなどのパッケージの中身で、クラウドベンダー固有のリクエストを標準のhttpのリクエストやレスポンスライターに変換してくれるものです。

これを使えば、普通のWebフレームワークで作ったとしても、AWS Lambdaにそのコードをそのまま乗せられるのが大きなメリットだと思っています。

そのため、先ほど言っていたローカルでは、画面からローカルでAPIを叩く時に固定することも(でき)、コードが同じでもポートをListenして動かせて、そのままデプロイすればLambda上でも動かせます。何回も言っているのですが、相当なイノベーションなのではないのかと個人的に思っています。

多用途向けのシングルバイナリと言っていますが、私がよくやるのはLambdaやlambda.goという、コマンドをパッケージ配下に作ることと、example、example.goのような普通のメイン関数を2つ別々のファイルで作ることです。

例えば、がんばればAWS_LAMBDA_FUNCTION_NAMEのような環境変数でスイッチさせて、1つのバイナリにできます。Lambdaの環境変数がなければそのままポートListenするサーバアプリが立ち上がり、そのまま同じバイナリをAWS上にデプロイすればLambdaとしても動きます。逆に悩む気もしましたが、シンプルな構造にもできると思っています。

AWS Lambda Go Api Proxyですが、最近どういうパターンかを整理しました。(スライド下部に書かれている)Qiitaの@yuno_miyakoさんの分類でいくと、WebFrameworkパターンで、けっこう特殊かと思いましたがわりと標準的にパターン化されています。

(スライドを示して)この記事にも書かれてますが、エンドポイント単位でIAMの制御などをしないのであれば、ローカルでも慣れているものをそのまま使えるので、開発生産性(としても)手堅い選択肢ではないかとあらためて思いました。

パッケージ構成とバッチアプリについて

続いて、パッケージ構成です。これはあまり触れると過去の発表の繰り返しになるのですが、正直なんでもいいのかなと思っています。過去いろいろ試して言っておいてなんなんですけど(笑)。これまで話した内容からすると、そこまで生産性に大きく影響するところでもなくて、自分たちのチームに合ったものを検討すればいいと思っています。

このあとパッケージについてはまた話しますが、いったんバッチアプリについて説明したいと思います。先ほどはWebのAPIでしたが、バッチの話をしたいと思います。

バッチアプリはどんなイメージか。私の言う今回のバッチアプリは、例えばあるタイミングでファイルがS3などのバケットに置かれて、SNSや時間トリガーでプログラムが起動してファイルを読み取って、データベースに書き込むとか、データを修正するとか。そういったバックグラウンドで動く処理を、バッチアプリと呼んでいます。

(スライドを示して)こういったところも普通にGoで書かれる方が多いと思いますが、1つだけ先ほどのパッケージと絡めて、おすすめというか、効果的だった戦略が、Webとバッチのパッケージ構成をなるべく揃えることは、非常に効果的だったと思っています。

利点としては、最初はちょっとずれていたものの、揃えることで機能配置、各コードでどこに何を書くかがまったく同じになるところです。テストを含めたり、コーディングのスタイルが利用できるところで、相当生産性に寄与するかなと思っています。

デメリットがあるとすると、バッチ側だけ見ると単純に読み取って書き込むだけなのに、いろいろと細かくパッケージが分かれるのは冗長かもしれません。しかし、揃えておくとかなりいいポイントがいっぱいあると思います。レビューも楽です。

ほかにもバッチアプリはいろいろ注意点があります。Goだからというわけではないので、簡単に書きます。1行ずつログを出さないとか、冪等な処理になっているかとか、いろいろあります。

(スライドを示して)最近よく見かけたのが、下から2つ目のところです。部分取り込みを許容する場合に、return errで全部中断になっていないかということです。例えば、読み込みが1,000件入っているCSVファイルを読み込んで、DBに書き込むようなバッチ処理を例に挙げると、そのうちの100行目に1行だけエラー行というか、不正なデータが入っていたとします。

よくWebのAPIだとそのまま上位までreturn errして500エラーなどで返すことがありますが、バッチ処理だとそこをスキップして、ほかの999行を取り込みたいということがあるかなと思っています。

そういった時に全部取り込まないのであれば、きちんとcontinueやignoreして最後にまとめてエラーを吐く設計にしなければいけないのですが、WebのAPIとパッケージを寄せているぶん、そのあたりがけっこうおざなりになりがちなので、注意して実装していければいいと思っています。

ほかにもいろいろ書いていますが、このあたりを話し出すとGoがあまり関係なくなってきているので、割愛したいと思っています。

テスト・品質

最後に、テストと品質の話です。最近ちょっと思うのですが、単体テストはGoだと、Table Driven Testsで書かれる方が有名だし、多いのではないかと思っています。

(スライドを示して)テーブルのはこういったデータのところで、ここのテストデータ、入力データや想定される出力結果を最初に宣言してあげて、そこでループを回して、今回テストしたい関数を引数で渡して確認するやり方だと思っています。

エンタープライズ系というか、どこでもそうだと思いますが、テストコードがものすごく長くなる問題が出てくると思っています。これはテストの想定結果が外部ファイル化されており、コードの中にはまだ入っていませんが、テーブル定義部分は小さくてもこれくらいだし、これでもぜんぜん少ないくらいです。

これで7件ぐらいですが、多いと20件とか30件といったテストのデータパターンがあるので、スクロールするのも大変なくらい、いろいろ出てきます。そのため、引数の構造体のフィールド数によっては容易に膨らみやすく、ケース数が少なかったとしても縦に伸びやすいことがけっこうあります。

注意点というかポイントとしては、正常系・異常系。正常系の中でも想定されるデータの型が違う場合は、関数自体を分けるといった工夫でトレースしやすい仕組みというか、構造を最初から押さえておかないと、テストのメンテナンスが相当大変になると思います。

Table Driven Testsはもちろんよくて、どのみちテストは大変だとは思いますが、関数の定義の流動を少し変えていけるとハッピーになれるのかなと思います。

あまり言い出すとあれなのですが、品質でよくあるのが、簡単なのがカバレッジを取る話かなと思っています。だいたいgo testのcoverprofileでHTMLファイルが生成されて、それを開くとどこが通っているか通っていないかがC0のカバレッジで出るので、簡単に見られます。

注意でもなんでもありませんが、Goだとearly returnですぐ返して(しまって)、どうしても起こしてしまうところがあります。ほかの言語に比べて低く出がちなので、注意かと思っています。

しかし、標準で出るので、観測したい範囲でパッケージを分けるなどで管理できるとすごく扱いやすいのです。ここはこまめに見ていくといいのかなと思っています。

Goを選定してよかった

まとめです。いろいろ話しましたが、リーダーやマネジメント視点、あと自分の観測範囲だと、Goだと比較的早く安定して生産性を出せるのではないかと思っていて、すごく魅力的です。

小さめのライブラリはうまく選定するとアウトプットもしやすくて、メンバーのモチベーションアップにつながって、うれしいこともいろいろあると思っています。

使ってみて実際に辛いところはいろいろ話したかもしれませんが、正直Goだから大変という話ではなくて、ほかの言語でも発生するツラミが大半だと思います。逆に言うと、自分たちのドメインなどの本当に難しいところに集中できているという意味で、Goは本当に選定してよかったなと思っています。

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