AppEngineについてそこはかとなく考えたこと

AppEngine大好きっ子で1年近くAppEngineをPaaSに採用した環境で開発を行ってきた人間としてちょっと考えたことをつらつら書いてみます。
※1 このエントリは今日(2018/09/21)たまたま同僚の人と色々話してた内容をベースにしてます。
※2 あくまで個人の感想、というかAppEngine信者の戯言です。

サマリ

特に長いつもりもないですけど言いたいことは以下のようなことです。

  • 最近のGCP界隈はKubernetes推し
  • 爆速サービス開発はFirebaseで十分になってきた
  • AppEngineがどんどんGKEとFirebaseの勢いに押されて目立たない
  • AppEngine最高だからもっと使われてほしい

最近のGCPの流れ

GKE推し

昨日一昨日開かれてた Google Cloud Next Tokyo にも参加してきましたが、とにかくKubernetesが強いです。
7月にサンフランシスコで行われたGoogle Cloud Next からKubernetes推しの流れはいまだに止まらないなぁと。

国内でもGCP/Goで勢いのあるメルカリ社もGKEを採用してるし、その派生サービスでブラックホールのごとく人を集めてるメルペイもGKEを採用しているとのこと。(公式ブログ をウォッチしてる限りの情報)

あとはCAのやってるAbemaTVでもインフラにGKEを採用しているし、とにかくGKEの流れがすごいと思います。
Google Cloud Next Tokyo セッションもGKEのセッション数は20数個ある一方で、真っ当なAppEngineのセッションはその半分くらい。ここでまず勢いの差を感じます。

Firebaseが高機能になってきた

ユーザー認証、FireStore、CloudFunctions、FirebaseHostingなど機能が充実してきててもうこれさえあれば簡単にサービス開発できるようになってきているなと思います。
いつだったか、「これからの時代はFirebase使えるPaaSエンジニアの給料が〇〇百万円〜」みたいなツイートがバズってましたが、まぁそれもわかるなと思うくらい多機能で基本的な機能を備えたサービスであれば爆速で開発が可能になっています。

わざわざGKEやAppEngine選ばなくてもFirebaseさえあればサービスを作るには事足りそうな時代はもう来てるなという印象。

僕個人としてそこはかとなく思ってること

僕個人としは、やはりAppEngineはいいソリューションだと思っています。というのも

  • フルマネージド
  • (ランタイムにもよりますが)数十ms〜数百msでの高速なインスタンスの起動
  • Auto Scaling
  • 標準でデプロイパイプラインを装備(gcloud app deply
  • 高機能なコンソール画面

というのは一般的にメリットとして挙げられますが、個人の経験談としては

  • AppEngineのチームにjoinして一年以上productionに向けてsshコマンドを叩いていない
  • とにかく細かいところを気にしなくていいので、アプリの開発に集中できる

というところがとても開発者体験として大きかったです。
ssh を叩かないだけで本当に心理的にはいい影響しかありません。

デメリットとして挙げられることもある outboundできない問題(urlfetchの制約) ももうすぐ出ると言われてる gVisor 対応した AppEngine 2nd Generation によりなくなる(かもしれない) と言われてるので、AppEngineならではの制約からは少しずつですが解放されていくのではないかと思います。
(※ javaやnodeのランタイムではすでに gVisor対応してます。)

そういえば今日こんなコードを一緒に話してた同僚の方が見つけていて 「おお 👀 !!」 と思いました。

github.com

特に https://github.com/GoogleCloudPlatform/golang-samples/blob/master/appengine/go11x/helloworld/helloworld.go#L11-L16 の部分。

import (
    "fmt"
    "log"
    "net/http" //これ!!!
    "os"
)

AppEngineでは urlfetch が必要になっていたのに app.yamlにある runtime: go111 のversionからは net/http という標準パッケージを使っているので、サンプル実装ではありますが、これが本当になればAppEngineならではの制約で書いてたコードもその制約から外れますし、標準的なgoの実装に合わせることができるので、デメリットはなくなると思います。

伝統的なGoogle謹製のコンテナで歴史あるプロダクトですが、ここに来て少しずつ色んな制約が取り払われてる(gVisorとか)ので、あとはAppEngineを使った事例がもっとでてくればいいなと思います。
AppEngineは分類的にはサーバレス、ではないのかもしれませんが、サーバーを意識せずに開発できるという観点ではサーバレスだと思いますし、とても使いやすいサービスだと思っています。

メリットにも書いてますが、高速なspinupとデプロイパイプラインまで標準で装備されているので、書いたコードをすぐにデプロイして *.appspot.com で確認することが可能です。
Standard Environment でサポートされてるランタイムを使うのであれば、デプロイも高速に完了します。サービスを開発したいのに、CI/CD周り整備したりとか開発以外の運用や開発プロセス整備みたいな今まで手間だったものまるっと意識しなくてよくなります。

サポートされてるランタイムの上に乗っかる前提ですが、ここ数年来勢いのあった、Railsを使った高速開発〜!といった流れを変えうる(もしかしたらすでに変えている?!)プロダクトだと思います。
(Railsはいいんですけど、やはり大規模化した時本当にずっと使い続けるの?っていう疑問は思ってますし、ruby嫌いじゃないけど、型のある安心感からgoやjavaがやはり大規模開発には向いてる気がするんですよねぇ...コンパイラで怒ってくれる安心感?みたいなもの。)

そういった言語をサポートしていて、かつメリットに記載したような内容を享受できて、GKEほどエコシステムの学習コストは高くなく、実運用ベースでFirebaseに比べればまだ実績があって(多分?)、かつ高速にサービス開発できる可能性のあるAppEngine、もっと広がってもいいんじゃないかなーと思ってます。

まとめ

とりあえず僕はAppEngineを初めて使った時から感動しっぱなしで、これは本当に最高のツールだと思っています。GKEやFirebaseの流れに負けずに存在感を出してって欲しいと思います。以上。

Intellijで古いversionのpluginをinstallする

Intellij をはじめとした jetbrains 系のIDEで現在installしているpluginよりも古いversionのpluginを入れようと思ったのでそのメモです。

背景

そもそもなぜわざわざ古いversionのpluginを入れようかと思ったかというと、jetbrains系のIDEでUIをカスタマイズするときに Material UI というのを使うのが一般的なんですが、先日IDEをアップデートして合わせてpluginもアップデートした直後から、Intellijを起動するたびにMaterial UIの設定wizardが毎回出るようになってしまい、ものすごくうざかったので、その原因と対処を調べてたというのが理由です。

原因

ありきたりなんですが、IDEのアップデートに合わせて Material UI の pluginもアップデートしたら、pluginのpreview版がinstallされてしまったことが原因でした。
どうやら jetbrains系のIDEに使われる pluginはpreview版であってもstatable状態であればIDEEAP(Early Access Preview)版でなくてもpluginのpreview版がインストールされてしまう、ということがわかりました。

Mateial UIのページを見ると、僕がこの問題に直面した 2018年9月18日時点では以下のようになっていました。

plugins.jetbrains.com

これの Download Plugin を見ると最新版が Material Theme UI 2.9.0-pre1 でstatableになっていました。どうやらこれが原因らしいです。

Changelog 2.9-pre PS: This is a very early release which has not be tested yet, but since I'm taking a break I wanted to release it before I go. If something goes wrong you can still revert back to 2.8.3.

ちなみに注意書きにこれが書いてあって、「じゃあそもそもなんでstatableやねんw」っていう気もしましたが、仕方ないので、このエントリを書いてる時点の1つ前のversionに戻します。

古いversionのPluginをinstallする

jetbrains系のIDEでPluginをDLする場合は3つの方法があります。

f:id:ema_hiro:20180920025923p:plain

  • Install jetbrains plugin ... デフォルトで指定されてる公式が指定しているplugin。各種言語のpluginはこれに当たる
  • browse repositories ... 公式指定に限らず、一般の人も公開している全てのpluginをinstallできる。
  • install plugin from disk ... 端末からバイナリを指定してinstallする。jetbrainsIDEに内包されてるpluginのinstallerを使わない。

今回は3つ目の install plugin from disk を選択してpluginをinstallします。この手順は以下に掲載されています。

www.jetbrains.com

手順

  1. https://plugins.jetbrains.com/plugin/8006-material-theme-ui からこのエントリ執筆時点の最新安定verは2.8.3をDLする
  2. Preferences > Plugin > install plugin from disk を選択する
  3. 1でDLしたzipファイルをそのまま選択してください。取り込めるのは zip or jar だけみたいです。
  4. 正常に取り込めたらIDEを再起動します。

結果

2.8.3に戻したら毎回 Material UI の 設定ウィザードが開かなくなったので元どおりに戻りました。jetbrains系のIDEは結構罠が多いなと思います。

『ファイナンス思考』を読んだ

ファイナンス思考』を読み終えました。
会計というか事業に置けるファイナンスの考え方に興味があるのでとても面白い内容が詰まっていました。

ファイナンスを実際にすることはなくても、どういう考え方を持っておけばいいのか知りたい人にはいい書籍になるのではないかと思いました。

よかったところ

  • 説明されるとまぁ確かにと思うようなPLの考え方やそれに基づく実際やってしまいがちな思考が整理されていたこと。
  • ユースケースが多く実際に行われたことベースで理解することができること
    • 特に実際に伸びてきた外資IT企業、国内でも危機的な状況から復活した企業など自分が知らない企業の裏側も解説していたので、実際のケースに照らし合わせてみて理解しやすいこと。
  • PLになりやすい原因まで記載があったこと
    • 僕も振り返ると結構PL脳になってることが多かったと思います。
    • そして日本企業がどうしてPL脳になりがちなのか、という点も歴史的な経緯も含めて「なるほど」と思わされてしまいました。

感想

mixiの朝倉さんが書いた本をちゃんと読んだのは初めてでしたが、事前知識なくても読めるくらいに丁寧に書かれていました。とても読みやすい書籍だったので

も買って読んでみようかなと思いました。

最近仕事で意識していること

最近仕事をしている中で意識していることをまとめてみました。

Overview

  • 前提をすり合わせること
  • 細かいところにこだわってみること
  • 意図を説明すること

前提をすり合わせること

PullRequestに対してレビューしてもらう場合にdescriptionをちゃんと書くようにしたり、レビューを依頼するときに、「どこにどんな変更を加えたのか」をチャットでも一言添えるようにしています。

と言うのも、設計やレビューを依頼するときに、レビュアーにとって、コンテキストがわからない状態でレビューするのはとても負荷の高い作業になるので、極力みてもらう場合には前段のコンテキストや前提を伝えるようにしています。

同じ土俵に立ってはじめて、身のあるレビューをしてもらえるようになっているし、チームで仕事をする以上、相手のことも考えながら仕事した方が全体としては生産性はプラスに働くと考えています。
(本当はそう言う小さい気遣いを評価してもらえるようになるともっといいんだろうなと思っています)

いいfeedbackをもらうには、同じ土俵に立ってこそだと思いますし、同じ土俵にあげる努力を怠らないようにしていきたいと思います。

余談ですが、同じ土俵に上げることを 巻き込み力 とか言うのかな?とちょっと思い始めました。

細かいところにこだわってみること

今まで意識してこなかったところ、もしくは脳死的、簡単にスルーしてきたことにちょっとこだわってみています。

commitメッセージにこだわること

  • commitメッセージの内容
    • なぜ?とか書いたコードの意図をなるべくコミットメッセージに込めてみること
  • PullRequest全体のcommitの構成
    • わかりやすい粒度でコミットを分けること
    • それに伴ってgitの知識をつけること
  • squash & merge で大きなコミットをみやすくする

コミットメッセージって自分がわかればいいかなと思っていた派ではあるんですが、少しでも粒度や差分を一言で表すように意識してみると、コードを書く前にどう言う粒度にしようとかとか考えるようになってきました。
その結果コード書く前に、これから書くコードについてある程度見通しを立てることができるようになってきたと思います。

reviewdogに従ってみること

プロジェクトでコードレビューするときに reveiewdog を使っています。これがすごく良くて、Go Wayに則ってなかったり潜在的なバグが潜んでそうなところを指摘してくれるので、コードレビューの負荷がすごく減っていると思います。

一方でレビュイーからすると結構細かいところまで指摘されるので、直すのがめんどくさかったり後回しになってしまうPullRequestもしばしば見かけます。

PullRequestをどう運用していくのはレビュイーに任されていると考えているので、reviewdogで指摘されたコメントはすぐに対応するか後でまとめて対応するかに意見はあまりこだわりはないんですが、自分はおおよそのケースで指摘された次のコミットですぐに対応するようにしています。

結果、常にreviewdog意識しながらコード書くようになったためか、犬に吠えられない(望ましい)コードをかけるようになってきました。

意図を説明すること

最近

  • 「なぜその設計にしたのか?」
  • 「なぜそのコードを書いたのか?」
  • 「なぜhogehoge??」

をちゃんと説明できるようにしています。
意図があるから、設計レビューやコードレビュー時にfeedbackをもらったときにその意図を説明しますし、議論が生まれますし、結果新しい観点をinputできるようになりつつあるので、前よりもずっと開発するのが楽しくなってきました。

意図をもつなんて仕事するときに当たり前のことかもしれませんがどうしても仕事をしていると、現状の実装に合わせたり、特に考えず脳死的に要件だけを満たそうとしたりするので、難しですがなるべく意図を持ったと言い切れるように、説明できること、と言うことを意識しています。

まとめ

これは普段業務で意識していることの一例に過ぎませんが、ただ仕様を満たすのでなく、自分が どう言う意図を持って、どうその仕様を満たすのかと言うプロセスを言語化できる ようになっているのが少しわかるようになってきました。

そして、少しですが 細かいところを意識すると開発が楽しくなる と言うことを実感しています。

神は細部に宿る と言いますが、こう言うところもこだわっていくと少しいいことあるのかなと思います。

「エンジェル投資家」を読んだ

エンジェル投資家が実際に投資する上での思考回路を赤裸々に記録している書籍です。

著者のジェイソン・カラカニスという人は僕は知りませんでしたが、この人が投資した or 投資し損ねた会社は僕らが知っているサンフランシスコの超優良スタートアップばかりでした。

僕自身は特にエンジェル投資家になりたいわけでも、そういう人から支援を受けたい(起業したい)とか考えたことがあるわけではありませんが、普段どういう人が出資を受けているのか、エンジェル投資家ってなんなのか、何が仕事なのか興味があったので、通読程度ですが一気にガッと読みきりました。

読んでみて思ったのは、エンジェル投資家は投資家であると同時に、投資先の代表の最大の壁打ち相手なんだなぁと思いました。
また、流行りそうならどんなサービスにでも投資するものかと思ってたんですが、思った以上に属人的というか、「人」を重視しているのか、ということはわかりました。

普段あんまり意識することがない職業のことや、細かいことをつらつら書いてましたが、書籍の内容としては面白かったです。

Test時に値を書き換えて元に戻すサンプル

メルカリさんのこのブログを呼んで表題のテスト時に置ける値の一時的な書き換えとresetの方法がとても便利だったのでメモりました。

tech.mercari.com

テストを回すときに実際のURLでなく適当なダミーURLを叩いてHTTPのレスポンスをモックしたい場合があると思います。 その際によくやるのが、実際のURLをテスト時だけ書き換える処理です。テスト時のみ書き換えるに当たって、求められるのは

  • テスト中だけ動くこと
  • テストを抜けたら(完了したら)元のURLにresetすること

です。
これをgoのdeferを使って簡単に実現します。

deferを使った書き換え方法

サンプルコードは以下です。

// main.go
package main

import "fmt"

var rewriter = "hoge"

func main() {
    fmt.Println(rewriter)
}


// main_test.go
package main

import (
    "testing"
)

func TestSample(t *testing.T) {
    t.Run("rewrite test", func(t *testing.T) {
        defer rewriteString("fuga")()
    })
}

func rewriteString(s string) func() {
    var tmp string
    tmp, rewriter := rewriter, s
    return func() {
        rewriter = tmp
    }
}

subtest関数において

defer rewriteString("fuga")()

この1行で値の差し替えとresetを行なっています。

  • returnがfunc型であること
  • deferが呼ばれるのは関数 or サブテスト関数を抜ける時でreturnのfuncが呼ばれるのも同じタイミングであること

この2つの挙動をうまく使っているなと思いました。

最初、このコードを見たときにどうしてそのように動くのか全く理解できなかったんですが、これは

  • defer で rerwrite("fuga")を呼んだ時点で rewritehoge -> fugaに書き換わっている。
  • サブテスト関数内から抜ける(deferが呼ばれる)まで rewrite = fuga の状態になる。
  • deferで呼ばれるのは rewriteString メソッドのreturnのfunc。
  • サブテストを抜けるタイミングで return func() の中身が呼ばれるので、ここで変数がresetされる。

という挙動で意図した挙動になっていると理解しました。
改めて考えるとちゃんと動いていますが、これ実際に使われてるテクニックだとするととても綺麗な書き方だと思いました。

テスト時だけ特定の値に変数を書き換えるときに専用のメソッドを用意してましたが、テスト全体で共通で使えるメソッドを export_test.go に書いておいて、同一packageないから呼べるようにしておけばテストコードの重複もなさそうです。

使う場面は多々ありそうなので、これは今後使っていきたいtipsです。

載せるようなものでもないですが、一応、ってことでコードはこちら

github.com

Table Driven Testのテストケースの書き方について

Goでは単体テストを書く場合に Table Driven Test が可読性、保守性の観点から推奨されています。

そのTableDrivenTestにおいてテストケースの書き方について備忘録です。

テストケースの書き方

ケースごとのstructを定義する。

以下のようなコードを想定します。

type sampleUseCase struct{
  field1 int
  field2 string
}

func TestHoge(t *testing.T){
  cases := []sampleUseCases {
    {
      field1:1,
      filed2:"hoge",
    },
    // 各テストケース
  }
  
  for _ , tt := range cases {
    // テストをゴリゴリ回す
  }
}

Pros/Cons

  • テストのユースケースがわかりやすい
  • 一方でテストケースをいちいち外部に出さなくてもよく、Gopぽくない。
  • たまに公開structにしているケースもあるが、テストケースを公開structとして外部package向けにも公開するべきではない。
    • 影響範囲を最小にするため

Testメソッドの中に非公開structを定義する

こちらが一般的な方法。コードは以下。

func TestHoge(t *testing.T){
  
  cases := []strict{
    field1 int
    field2 string
  }{
    {
      field1: 1,
      field2: "hoge",
    },
    // 各テストケース追加
  }
  
  for _, tt := ranga cases {
    // テストをゴリゴリ回す
  }
}

Pros/Cons

  • GoらしいTableDrivenTest
  • テストメソッド内で非公開structにしているので、外部への影響がない。

structのslice or map[string]struct

これは好みだと思いますが、ユニットテストを書くケースにおいてstructのsliceにするか、mapでテストケースを書くか2パターンがあると思います。
Goのテストではテストメソッドごとsubtestにしてテストするのがいいとされています。

理由は

  • subtestの第一引数に「なんのテストを実行するのか」名前をつけられること
  • deferが書きやすいこと
  • リクエストなどを含まない単純なパターンのテストの場合、並列実行により高速が直列実行するときよりもテストのパフォーマンスを向上されらるため。

この1つ目のテストケースに名前をつけられる、というところですが具体的なコードだと以下のようになります。

func TestHoge(t *testing.T){
  
  cases := []strict{
    name   string
    field1 int
    field2 string
  }{
    {
      name:   "test case 1"
      field1: 1,
      field2: "hoge",
    },
    // 各テストケース追加
  }
  
  for _, tt := ranga cases {
    tt := tt
    // サブテスト
    t.Run(tt.name, func(t *testing.T){
      // テストを書く
    })
  }
}

これでも十分わかりやすいですが、テストの名前がわかるためだけに name filedを増やすのも微妙だと思ってました。 そこで考えたのが、nameをkeyにした map[string]struct を作成してテストケースにするプランです。

func TestHoge(t *testing.T){
  
  cases := map[string]strict{
    field1 int
    field2 string
  }{
    "field1 case": {
      field1: 1,
      field2: "hoge",
    },
    // 各テストケース追加
  }
  
  for k, tt := ranga cases {
    tt := tt
    // サブテスト
    t.Run(k, func(t *testing.T){
      // mapのkeyがそのままテストケースになる。
      // テストを書く
    })
  }
}

このようにテストケースのstructの定義の仕方はいくつかありますが、mapが使いやすいので今後はmapでテストケースを書いていこうと思います。