スクエニ ITエンジニア ブログ

Cloud Runを初めて導入してみた!

みなさん、こんにちは!初投稿の hamachan です。

私は、クラウドの技術で、一例として

  • 急増するユーザアクセスも良い感じで捌く(CM等のプロモーション打つよ~ → どのくらいサーバ増強しておきましょうか?…みたいな議論から不毛だと思ってます。そんな見積もり、とても難しいですよね?)
  • 各種メンテナンスも不要(セキュリティパッチ適用から、バージョンアップ対応などなど面倒くさいのです。)

といった、「何もしなくて良い状態」に極力したい人、憧れる人です!

さてさて、今や弊社の複数のオンラインゲームタイトルで、GCPのCloud Runが採用されていますが、今回はそれを初めて導入したときのお話です。

背景など

とあるオンラインゲームタイトルで、国産クラウドから、GCPへ移設するお仕事がありました。この際、ユーザ様からのアクセスを受けるフロント部分に関し、別プロダクト(流行りもあってGKE)にしようという流れがあったのですが、結果Cloud Runにしました。これが初めての導入でした。

Cloud Runは冒頭自己紹介部分にも書いた

  • 急増するユーザアクセスも良い感じで捌く
  • 各種メンテナンスも不要

を叶えてくれる可能性があり、私の推しプロダクトでした。

この初導入を実現させたく、ずっとこのCloud Runを触っていた時期があります。プロデューサー, 開発担当, チームメンバらにその良さを説明し、共感してもらい、初導入を実現するには自分で手を動かさないとですよね。

そんなこんなで今回は、

  • 推しプロダクト(今回だとCloud Run)があって、本番環境に導入してみたい
  • その推しプロダクトの良さを関連メンバに伝えたい
  • 開発担当など、自分以外の人が指定したプロダクトを言われたとおり準備するのではなく、一石を投じたい

みたいなことを考えている人には刺さるかもしれない内容となっています。 なお、本記事では、TCO(Total Cost of Ownership)の試算や、Cloud Runの説明、構築/導入手順といったものは割愛させていただきます。 ただ、移設前の国産クラウドで稼働していたことあり、アクセス規模とか正確に分かったので、費用見積もりはやり易かったですね。

本題「推しプロダクト(Cloud Run)を関連メンバに伝える手順」

以下のステップで、関連メンバ(プロデューサー,開発担当,チームメンバら)に説明等を行い、口説き落としていきました。 ここから、この「関連メンバ」を『みんなー!』とか『皆』で呼びます。

みんなー!デプロイとか体験してみてー!

まず長いサービス運用の中で生じる、デプロイ作業を皆に体験して貰いました。 体験用のプログラムなどは私の方で準備し、上からコマンドを叩けば、ひととおりの流れが掴める…という体験環境を整備し、以下のように案内しました。

体験用プログラムなど(java,Dockerfileといったもの)は適切な位置に設置済であり、
皆さんに利用の流れを体験してもらうための手順になっています。
最後まで進めれば、ネイティブイメージのビルド→GCRへの格納
→Cloud Runへのデプロイの流れがつかめます。

1. ↓作業ディレクトリに移動します
  $ cd ~/getting-started.spring/spring-graal-native/spring-graal-native-samples/webflux-netty

2. ↓ネイティブイメージをビルドします
  $ ./build.sh
  →以下のような文字列が出ます
  === Building webflux-netty sample ===
  Packaging webflux-netty with Maven
  Unpacking webflux-netty-0.0.1-SNAPSHOT.jar
  Compiling webflux-netty with GraalVM Version 19.2.1 CE
  SUCCESS

  [Tips]
  以下のファイルが出来上がり、下の方が実際に使うネイティブイメージになります
  ./target/webflux-netty-0.0.1-SNAPSHOT.jar
  ./target/webflux-netty
  それぞれ、以下のようなコマンドで「Hello! HANAKO!」を返すウェブサーバが起動しますが
  下の方が起動時間が圧倒的に速いことが分かります。(この速い方のDockerfile作るのに試行錯誤しました)

  $ java -jar ./target/webflux-netty-0.0.1-SNAPSHOT.jar
  →Started DemoApplication in 2.402 seconds
  ※Ctrl+C連打とかで適当に止めてください。

  $ ./target/webflux-netty
  →Started DemoApplication in 0.039 seconds (JVM running for 0.041)
  ※Ctrl+C連打とかで適当に止めてください。

3. ↓コンテナイメージを作成し、GCR(Goolgle Container Registry)に格納します。
  $ gcloud builds submit --tag gcr.io/hamachan-test01/spring
  →最後に以下のような文字列が出ます
    gcr.io/hamachan-test01/spring (+1 more)  SUCCESS

4. ↓GCRに格納されたものをCloud Runにデプロイします。
  $ gcloud run deploy spring --image gcr.io/hamachan-test01/spring --platform=managed --region=asia-northeast1 --allow-unauthenticated
  →最後に以下のような文字列が出ます
    Service [spring] revision [spring-XXXXX-XXX] has been deployed

5. 以下のURLに飛んで、Hello!TARO! ←→ Hello!HANAKO! を自由にやってみてください。
  https://console.cloud.google.com/run/detail/asia-northeast1/spring/metrics?authuser=1&hl=ja&project=hamachan-test01
   Cloud Runにデプロイした[spring-XXXXX-XXX]が見えるハズ。
   [変更内容]→[トラフィックを管理]で%コントロールができ、
   [リビジョンのURLを追加]でテスト用URLが発行できます。

デプロイ前は元々デプロイされていた「Hello!TARO!」が表示され、デプロイ後は「Hello!HANAKO!」がウェブで表示される体験版ですね。

長いサービス運用で発生し続けるデプロイ作業にまずは注目してもらいました。初期構築/設定やサンプルプログラム(ここではjava)の用意は私の方で準備し、そこは皆に意識させませんでした。

Cloud Run自体のバージョンアップに伴う弊社側の対応がないこと、たまにテレビコマーシャル等も行うこのオンラインゲームで事前のサーバ増強の議論もほぼ不要になりそう(総じて、皆が楽になる)ということも併せて共有したこともあり、ここで皆の💛は掴めたような気がしました。

みんなー!ユーザ様に返すレスポンススピードも問題ないよー!

次は、皆が不安に思うだろうな、懸念として挙げられそうだな、という点を予測/先回りして回答を準備し、問題ないということを数字で示しました。

Cloud Runだと、オートスケーリングがあるわけですから、そこで妙に待たされる人がいたりしないかは皆が気になっていたことの一例だったと思います。 移設前の国産クラウドで実際に発生していたアクセス急増時のデータを実際にCloud Runにも投げてみて、サービスとして問題がないかを以下のようにして数字で皆に示しました。

heyを使った負荷及び瞬発力(レスポンススピード)の試験例です。

$ hey -c 100 -n 1000 https://spring-hmqeae57ta-an.a.run.app/
Summary:
  Total:        0.7689 secs
  Slowest:      0.5999 secs
  Fastest:      0.0053 secs
  Average:      0.0526 secs
  Requests/sec: 1300.4863
  Total data:   13000 bytes
  Size/request: 13 bytes
Response time histogram:
  0.005 [1]     |
  0.065 [754]   |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ ■■■■■
  0.124 [110]   |■■■■■■
  0.184 [92]    |■■■■■
  0.243 [27]    |■
  0.303 [4]     |
  0.362 [3]     |
  0.422 [3]     |
  0.481 [4]     |
  0.540 [1]     |
  0.600 [1]     |
Latency distribution:
  10% in 0.0064 secs
  25% in 0.0088 secs
  50% in 0.0259 secs
  75% in 0.0632 secs
  90% in 0.1557 secs
  95% in 0.1781 secs
  99% in 0.3337 secs
Details (average, fastest, slowest):
  DNS+dialup:   0.0096 secs, 0.0053 secs, 0.5999 secs
  DNS-lookup:   0.0043 secs, 0.0000 secs, 0.0455 secs
  req write:    0.0001 secs, 0.0000 secs, 0.0061 secs
  resp wait:    0.0386 secs, 0.0052 secs, 0.4662 secs
  resp read:    0.0001 secs, 0.0000 secs, 0.0012 secs
Status code distribution:
  [200] 1000 responses

皆に指摘される前に、先回りして情報を出すことが大事だったかなと思います。 そうすると、スピード感も出ますし、「既に色々想定されている/考えられている」という印象も持たれたのではないかと思います。

みんなー!でもここは気を付けてねー!

ここは気を付けて!というポイントも同時に伝えておかなければ、皆が「あれ、説明を受けていたものと違うぞ…」となってしまう可能性があります。 そこはきちんと皆に伝えました。Cloud Runだと、新規インスタンス起動からコンテナ内のウェブサーバがPortListenとなってユーザのアクセスを実際に受けられるまでの時間が長ければ、レスポンススピードは出ない、と以下のようにして伝えました。

Cloud Runで瞬発力(レスポンススピード)を出すためのポイントです。

---- Cloud Run 体験環境手順より抜粋(ここから) ----
  [Tips]
  以下のファイルが出来上がり、下の方が実際に使うネイティブイメージになります
  ./target/webflux-netty-0.0.1-SNAPSHOT.jar
  ./target/webflux-netty
  それぞれ、以下のようなコマンドで「Hello!HANAKO!」を返すウェブサーバが起動しますが
  下の方が起動時間が圧倒的に速いことが分かります。
  (下の方はOracle GraalVMといったものも組み合わせ、高速化しています)
  $ java -jar ./target/webflux-netty-0.0.1-SNAPSHOT.jar
  →Started DemoApplication in 2.402 seconds
  $ ./target/webflux-netty
  →Started DemoApplication in 0.039 seconds (JVM running for 0.041)
---- Cloud Run 体験環境手順より抜粋(ここまで) ----

上記の上の方のPortListenまで「2.402 seconds」かかるものを使えば、レスポンスタイムにばらつきが出るのは確実。
下の方のPortListenまで「0.039 seconds」かかるものを使えば、以下のように気にならないレベル。

---- PortListenまで「0.039 seconds」のものを使って同時接続100,
     累計1000アクセスを実施(minimum instanceはゼロ)(ここから) ----
$ hey -c 100 -n 1000 https://spring-hmqeae57ta-an.a.run.app/
Summary:
  Total:        0.7689 secs
  Slowest:      0.5999 secs
  Fastest:      0.0053 secs
  Average:      0.0526 secs
  Requests/sec: 1300.4863
  Total data:   13000 bytes
  Size/request: 13 bytes
Response time histogram:
  0.005 [1]     |
  0.065 [754]   |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ ■■■■■
  0.124 [110]   |■■■■■■
  0.184 [92]    |■■■■■
  0.243 [27]    |■
  0.303 [4]     |
  0.362 [3]     |
  0.422 [3]     |
  0.481 [4]     |
  0.540 [1]     |
  0.600 [1]     |
Latency distribution:
  10% in 0.0064 secs
  25% in 0.0088 secs
  50% in 0.0259 secs
  75% in 0.0632 secs
  90% in 0.1557 secs
  95% in 0.1781 secs
  99% in 0.3337 secs
Details (average, fastest, slowest):
  DNS+dialup:   0.0096 secs, 0.0053 secs, 0.5999 secs
  DNS-lookup:   0.0043 secs, 0.0000 secs, 0.0455 secs
  req write:    0.0001 secs, 0.0000 secs, 0.0061 secs
  resp wait:    0.0386 secs, 0.0052 secs, 0.4662 secs
  resp read:    0.0001 secs, 0.0000 secs, 0.0012 secs
Status code distribution:
  [200] 1000 responses
---- PortListenまで「0.039 seconds」のものを使って同時接続100,
     累計1000アクセスを実施(minimum instanceはゼロ)(ここまで) ----

あとあとになって、このポイントを皆が知ったりすると、後戻りする可能性があります。 皆に知っておいてもらいたいポイントや得たノウハウは、当然伝え、後戻りや車輪の再開発にならないよう、気を付けて進めました。

みんなー!こんなことも出来るっス!

該当のゲームプロジェクトで使えそうな追加ネタがあれば、それも出しました。 例えば本件だと、「Cloud Runでwebsocketも受けられるよ。どこかに使えないかな?」と皆に説明し、実際に動いたサンプルプログラムも共有しました。

/*
Cloud Run上でWebSocket(go)を動かしてみたときのプログラム

https://tutuz-tech.hatenablog.com/entry/2019/10/20/214517
を参考にしています。構築したCloud Run上で動作するよう、以下のような変更を加えています。
- ポート番号を8888から8080に変更
- プロトコルをWebSocketからWebSocket Secureに変更
*/

package main

import (
	"fmt"
	"log"
	"math/rand"
	"net/http"
	"strconv"
	"time"

	"golang.org/x/net/websocket"
)

var dataCh chan Data

type Data struct {
	MyData string `json:"myData"`
}

func main() {
	dataCh = make(chan Data, 1)
	http.HandleFunc("/", plotHandle)
	http.Handle("/data", websocket.Handler(dataHandler))
	err := http.ListenAndServe(":8080", nil)
	if err != nil {
		log.Fatal(err)
	}
}

func plotHandle(w http.ResponseWriter, r *http.Request) {
	go func() {
		for {
			dataCh <- Data{strconv.Itoa(rand.Int())}
			time.Sleep(500 * time.Millisecond)
		}
	}()
	fmt.Fprint(w, page)
}

func dataHandler(ws *websocket.Conn) {
	for data := range dataCh {
		err := websocket.JSON.Send(ws, data)
		if err != nil {
			log.Printf("error sending data: %v\n", err)
			return
		}
	}
}

const page = `
<html>
  <head>
      <title>Hello websocket</title>
      <script type="text/javascript">
      var sock = null;
      var myData = "";
      function update() {
          var p1 = document.getElementById("my-data-plot");
          p1.innerHTML = myData;
      };
      window.onload = function() {
          sock = new WebSocket("wss://" + location.host + "/data");
          sock.onmessage = function(event) {
              var data = JSON.parse(event.data);
              myData = data.myData;
              update();
          };
      };
      </script>
  </head>
  <body>
      <div id="header">
          <h1>Hello websocket</h1>
      </div>
      <div id="content">
          <div id="my-data-plot"></div>
      </div>
  </body>
</html>
`

何か便利そうな、使えそうな、皆が意識してなさそうな、そんな機能や面白いネタがあるなら、恐れず発信していきましょう。 そこから何かが生まれるかもしれません!

あとがき

上記のようなステップをもって進め、皆の理解もあり、晴れてCloud Runを初導入することができました。

導入してから数年運用していますが、当初の目論見通り、Cloud Run自体のバージョンアップに伴う弊社側の対応はなく、テレビコマーシャルといったイベント前のサーバ増強系の議論もほぼ不要となりました。楽ちんです!

この記事が、導入したい新規プロダクトがあるという野望を持った方に、少しでも参考/ヒントになれば幸いです。 なんやかんや言うても、大事なのは、折れない心と推進力だと思います!

今のやり方を過信しない。常に新しい挑戦を!(THE FRUGAL ARCHITECT より)

この記事を書いた人

記事一覧
SQUARE ENIXでは一緒に働く仲間を募集しています!
興味をお持ちいただけたら、ぜひ採用情報ページもご覧下さい!