マーケティングプラットフォーム本部 開発部の高橋です。
前回から2回に分けて、「Google Cloud Next `19 San Francisco(以下、Cloud Next)」で私が参加したブレイクアウトセッションの内容を紹介しています。
今回はGKE上での Java/Kotlin アプリケーションの効率的な開発についてのセッションを紹介します。
Effective Cloud-Native Java on GCP
Effective C++やMore Effective C++、Effective Java、Effective Pythonなどで育ってきた身としては、「見に行かないわけにはいかない!」ということで、こちらのセッションに参加しました。
このセッションは約50分間ほぼすべて、ライブコーディングでGKE上にKotlinでWebサービスを構築していく、というかなり硬派なもので、内容も以下の通りかなり充実したものでした!
・Spring Bootのプロジェクト作成
・Datastoreとの連携
・APIの作成
・分散トレーシングの設定
・jibを利用したアプリケーションのコンテナ化
・Kubernetes用のマニュフェストの作成
・Liveness Check, Readiness Checkの設定
・Skaffoldを利用した開発環境への自動デプロイ
・サービス公開
・ログ出力の設定
・Prometheusを利用したモニタリング
・Stackdriver Debuggerとの連携
セッションを聞いただけでもかなり参考になる点が多かったのですが、「これは是非自分でも手を動かして試してみたい!」と思ったので実際に試してみました。
本ブログでは実際のコードを交えて、このセッションを紹介したいと思います。
下準備
セッション内では省略されていましたが、下記を予め実行しておく必要があります。
GKEクラスタの作成
デモを試すだけであれば、マシンタイプやノード数は自由に設定しておいていいと思います。
Cloud DatastoreとStackdriver DebuggerをEnabledにしておく必要があります。
プロジェクトの設定と認証
gcloud config set project プロジェクト名 gcloud auth application-default login gcloud container clusters get-credentials クラスタ名 -z ゾーン名またはリージョン名
Spring Bootのプロジェクト作成
まずは、Spring、Spring Bootの雛形を生成してくれるSpring Initializr(https://start.spring.io/)で、下記の設定でプロジェクトを生成します。
項目 | 設定値 |
Project | Maven Projec |
Language | Kotlin |
Spring Boot | 2.1.4 |
Group ~ Package Name | any |
Packaging | Jar |
Java Version | 8 |
Dependencies | ・Web ・Rest Repositories ・Rest Repositories |
"Generate Project" で生成されたプロジェクトがダウンロードできます。
Datastoreとの連携
pom.xml の dependencies に以下を追加します。
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-gcp-starter-data-datastore</artifactId> </dependency>
さらに、 DemoApplication.kt に以下を追加します。
@RepositoryRestResource interface PersonRepository : DatastoreRepository<Person, Long> @Entity data class Person( @Id var id: Long? = null, var name: String? = null )
DatastoreRepositoryは、SpringのPagingAndSortingRepositoryを継承しているので、Datastoreを利用する場合でも、普通のSpringBootアプリケーション開発でのリポジトリと同様に扱うことができます。
この状態でアプリケーションを起動し、http://localhost:8080/personsにアクセスすると、下準備で設定したプロジェクトのDatastoreにアクセスし、そのperson一覧が表示されます。
{ "_embedded" : { "persons" : [ ] }, "_links" : { "self" : { "href" : "http://localhost:8080/persons{?page,size,sort}", "templated" : true }, "profile" : { "href" : "http://localhost:8080/profile/persons" } }, "page" : { "size" : 20, "totalElements" : 0, "totalPages" : 0, "number" : 0 } }
このエンドポイントはPOSTリクエスト等も受け付けることが可能です。
curl -X POST -H "Content-Type: application/json" -d '{"name":"Shuhei"}' http://localhost:8080/persons { "name" : "Shuhei", "_links" : { "self" : { "href" : "http://localhost:8080/persons/5639445604728832" }, "person" : { "href" : "http://localhost:8080/persons/5639445604728832" } } }
再度GETしたり、GCPのコンソールでpersonを表示すると新規データが追加されていることが確認できます。
APIの追加
リクエストで名前を受け取りhello xxxというレスポンスを返しつつ、その名前のPersonを新規保存する /hello という API と、内部で /hello を3回呼び出す /all というAPIを追加します。
package com.example.demo import org.slf4j.LoggerFactory import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication import org.springframework.cloud.gcp.data.datastore.core.mapping.Entity import org.springframework.cloud.gcp.data.datastore.repository.DatastoreRepository import org.springframework.context.annotation.Bean import org.springframework.data.annotation.Id import org.springframework.data.rest.core.annotation.RepositoryRestResource import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.client.RestTemplate import org.springframework.web.client.getForObject @SpringBootApplication class DemoApplication { @Bean fun restTemplate() = RestTemplate() } fun main(args: Array<String>) { runApplication<DemoApplication>(*args) } @RepositoryRestResource interface PersonRepository : DatastoreRepository<Person, Long> @Entity data class Person( @Id var id: Long? = null, var name: String? = null ) @RestController class HelloController( private val personRepository: PersonRepository, private val restTemplate: RestTemplate ) { private val logger = LoggerFactory.getLogger(javaClass) @GetMapping("/hello") fun hello(name: String): String { logger.info("Greeting to ${name}") personRepository.save(Person(name=name)) return "hello ${name}" } @GetMapping("/all") fun all() { listOf("ray", "mandy", "jisha").forEach { restTemplate.getForObject("http://localhost:8080/hello?name=${it}", String::class.java) } } }
この状態でhttp://localhost:8080/hello?name=NEXTにアクセスすると "hello NEXT" というレスポンスが返り、DatastoreにNEXTというデータが追加されているのが確認できると思います。
また、http://localhost:8080/allにアクセスすると、下記のようなログが出力され、Datastoreに新たに3件のデータが追加されていることが、確認できると思います。
2019-05-01 13:33:15.697 INFO 23809 --- [nio-8080-exec-2] com.example.demo.HelloController : Greeting to ray 2019-05-01 13:33:16.604 INFO 23809 --- [nio-8080-exec-3] com.example.demo.HelloController : Greeting to mandy 2019-05-01 13:33:16.870 INFO 23809 --- [nio-8080-exec-4] com.example.demo.HelloController : Greeting to jisha
分散トレーシング
Stackdriver Traceなどでトレースが表示できるように、リクエストにTraceIDを付与し伝播してくれるSpring Cloud Sleuth(https://spring.io/projects/spring-cloud-sleuth)を利用します。
下記の依存関係を追加します。
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-gcp-starter-trace</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-gcp-starter-logging</artifactId> </dependency>
デフォルトでは、全リクエストのうちの10%にしかTraceIDを付与しないようになっているのですが、デモでは全リクエストに付与したいため、application.propertiesに下記を追加していました。
spring.sleuth.sampler.probability=1.0
また、ログからトレースIDが確認できるように src/main/resources/logback-spring.xml を作成し下記を追加します。
<configuration> <include resource="org/springframework/boot/logging/logback/base.xml" /> <include resource="org/springframework/boot/logging/logback/console-appender.xml" /> <include resource="org/springframework/cloud/gcp/autoconfigure/logging/logback-appender.xml" /> <root level="INFO"> <appender-ref ref="CONSOLE"/> <appender-ref ref="STACKDRIVER"/> </root> </configuration>
これによって、ローカルの標準出力とStackdriverにTraceIDを含めたログが表示されるようになりました。
これによりStackdriver Traceの画面では /all を呼び出すと内部で /hello を3回呼び出している様子が確認できるようになりました。
jibを利用したコンテナ化
普通にDockerfileを書いてコンテナ化することももちろん可能ですが、例えばOpen JDKだとコンテナにどれだけメモリが割り当てられているか知ることができずOut Of Memory Killed が発生する問題等があるため注意が必要です(参考 : https://dzone.com/articles/why-my-java-application-is-oomkilled)。jibというMavenのプラグインを利用すると、このあたりの問題をよしなに解決してくれるそうです。
また、自分でDockerfileを書かなくてもよかったり、イメージ作成時にDockerのデーモンが不要だったりするようです。
このセッションではjibを利用して作成したアプリケーションをコンテナ化しています。
下記の設定をpom.xmlに追加します。
<plugin> <groupId>com.google.cloud.tools</groupId> <artifactId>jib-maven-plugin</artifactId> <version>1.0.2</version> <configuration> <from> <image>adoptopenjdk/openjdk8:slim</image> </from> <to> <image>asia.gcr.io/プロジェクト名/jib-demo</image> </to> </configuration> </plugin>
下記のコマンドを実行するとコンテナ化し、Google Container Registry(GCR)への登録までを実行してくれます。
~/dev/effective-java/demo$ gcloud components install docker-credential-gcr ~/dev/effective-java/demo$ docker-credential-gcr configure-docker ~/dev/effective-java/demo$ gcloud auth configure-docker ~/dev/effective-java/demo$ ./mvnw compile jib:build
Kubernetes用のマニュフェストの作成
下記コマンドを実行すると、Kubernetes用のデプロイメントの設定ファイルを生成できます。
~/dev/effective-java/demo$ mkdir k8s ~/dev/effective-java/demo$ kubectl run demoservice --image=asia.gcr.io/プロジェクト名/jib-demo --dry-run -oyaml > k8s/deployment.yaml
これによって、下記のようなファイルが生成されます。
apiVersion: extensions/v1beta1 kind: Deployment metadata: creationTimestamp: null labels: run: demoservice name: demoservice spec: replicas: 1 selector: matchLabels: run: demoservice strategy: {} template: metadata: creationTimestamp: null labels: run: demoservice spec: containers: - image: asia.gcr.io/adpencil-dev/jib-demo name: demoservice resources: {} status: {}
一部記述が古かったり不要なコードが存在するので、下記のように修正します。
apiVersion: apps/v1 kind: Deployment metadata: labels: run: demoservice name: demoservice spec: replicas: 1 selector: matchLabels: run: demoservice strategy: {} template: metadata: labels: run: demoservice spec: containers: - image: asia.gcr.io/adpencil-dev/jib-demo name: demoservice
Liveness Check, Readiness Check用のエンドポイント追加とGracefulシャットダウン
Kubernetesには、以下2種類のチェック機能が存在します。
・Liveness Check ... Podが正しく動作しているかのチェック
→ チェックに失敗したらPodは再起動される
・Readiness Check ... Podがリクエストを受け取れる状態かのチェック
→ チェックに成功するまでPodにリクエストは割り当てられない
また、終了処理中に新たなリクエストが割り振られないように、SIG_TERMが送られたらReadiness Checkが失敗するようにしておくということも重要です。
まずはLiveness Check用のエンドポイントをHelloController内に追加します。
@GetMapping("/alive") fun alive() = "ok"
Readiness Check は spring-boot-sterter-actuator を利用するのが良いそうです。
spring-boot-starter-actuator を追加すると様々なエンドポイントが自動で追加されます(https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-endpoints.html)。
この中の /health がReadiness Check に利用できます。
SIG_TERMのハンドル用に springboot-graceful-shutdown も利用します。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>ch.sbb</groupId> <artifactId>springboot-graceful-shutdown</artifactId> <version>2.0.1</version> </dependency>
mainを下記のように修正します。
fun main(args: Array<String>) { GracefulshutdownSpringApplication.run(DemoApplication::class.java, *args) }
Skaffoldを利用した開発環境への自動デプロイ
kubernetesでの開発は、コードを修正するたびに下記の操作が必要になり若干面倒です。
・コードを修正
・Docker イメージをビルド
・イメージをpush
・kubernetes環境へデプロイ
Skaffold(https://skaffold.dev/)というツールを利用すると、コードの変更を検知して ビルド・push・デプロイを自動で行ってくれます。
https://skaffold.dev/docs/how-tos/builders/ の手順に従って、Skaffold をインストールした上で、下記の内容で skaffold.yaml を作成します。
apiVersion: skaffold/v1beta7 kind: Config build: artifacts: - image: asia.gcr.io/プロジェクト名/jib-demo jibMaven: args: - "-DskipTests"
IntelliJ + Cloud Code を利用している場合、"Develop on Kubernetes"を実行するとSkaffoldが起動され、以降コードを修正するたびにビルドからデプロイまでが自動で実行されます。
サービス公開
前述のSkaffoldによってGKE上にアプリケーションがデプロイされましたが、この時点ではGKEのクラスタ外からはアクセスすることができません。
下記のコマンドでKubernetesのサービス用のマニュフェストを生成します。
kubectl expose deployment demoservice --target-port=8080 --port=8080 --dry-run -oyaml > k8s/service.yaml
これによって、下記のようなファイルが生成されます。
apiVersion: v1 kind: Service metadata: creationTimestamp: null labels: cleanup: "true" docker-api-version: "1.39" ide: Idea ideVersion: 2019.1.1.0.0 ijPluginVersion: unknown run: demoservice skaffold-builder: local skaffold-deployer: kubectl skaffold-tag-policy: git-commit tail: "true" name: demoservice spec: type: LoadBalancer # <- クラスタの外部からアクセスする場合はこの一行を追加する ports: - port: 8080 protocol: TCP targetPort: 8080 selector: run: demoservice status: loadBalancer: {}
このファイルを保存すると、Skaffoldにより自動でサービスが作成されます。
しばらくすると、外部IPアドレスが割り当てられます。
~/dev/effective-java/demo$ kubectl get services NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE demoservice LoadBalancer 10.55.247.62 34.85.127.134 8080:30631/TCP 52s kubernetes ClusterIP 10.55.240.1 <none> 443/TCP 4m
この場合、http://34.85.127.134/personsなどで、GKE上のアプリケーションへアクセスすることができるようになりました。
Liveness Check, Readiness Checkの設定
Liveness Check、Readiness Check用のエンドポイントはすでに準備しましたが、実際のチェックのための設定がまだなのでここで設定します。
deployment.yamlを、下記のように修正します。
apiVersion: apps/v1 kind: Deployment metadata: labels: run: demoservice name: demoservice spec: replicas: 1 selector: matchLabels: run: demoservice strategy: {} template: metadata: labels: run: demoservice spec: containers: - image: asia.gcr.io/adpencil-dev/jib-demo name: demoservice livenessProbe: initialDelaySeconds: 5 httpGet: port: 8080 path: /alive httpHeaders: - name: x-b3-sampled value: "0" readinessProbe: httpGet: port: 8080 path: /actuator/health
ログ出力の設定
logback-spring.xml を下記のように修正します。
<configuration> <include resource="org/springframework/boot/logging/logback/base.xml" /> <include resource="org/springframework/cloud/gcp/autoconfigure/logging/logback-json-appender.xml" /> <root level="INFO"> <appender-ref ref="CONSOLE_JSON"/> </root> </configuration>
Stackdriver Debuggerとの連携
GAEのスタンダード環境で動作するアプリケーションの場合などは、特に設定せずにStackdriver Debuggerを利用することができますが、GKE上のアプリケーションの場合少々作業が必要になります。
src/main/jib/agents というディレクトリを作成し、下記からダウンロードしたファイルの中身を配置しておきます。
・https://storage.googleapis.com/cloud-profiler/java/latest/profiler_java_agent.tar.gz
・https://storage.googleapis.com/cloud-debugger/compute-java/debian-wheezy/cdbg_java_agent_gce.tar.gz
ディレクトリの中身は下記のようになります。
~/dev/effective-java/demo$ ls src/main/jib/agents/ NOTICES cdbg_java_agent.so cdbg_java_agent_internals.jar profiler_java_agent.so
jibの設定を下記のように修正します。
<plugin> <groupId>com.google.cloud.tools</groupId> <artifactId>jib-maven-plugin</artifactId> <version>1.0.2</version> <configuration> <from> <image>adoptopenjdk/openjdk8:slim</image> </from> <to> <image>asia.gcr.io/adpencil-dev/jib-demo</image> </to> <container> <jvmFlags> <jvmFlag>-agentpath:/agents/profiler_java_agent.so=-cprof_service=${project.artifactId}</jvmFlag> <jvmFlag>-agentpath:/agents/cdbg_java_agent.so=--logtostderr=1</jvmFlag> <jvmFlag>-Dcom.google.cdbg.module=${project.artifactId}</jvmFlag> <jvmFlag>-Dcom.google.cdbg.version=${project.version}</jvmFlag> </jvmFlags> </container> </configuration> </plugin>
こちらも修正すると、Skaffoldが自動で反映してくれます。
Stackdriver Debuggerのコンソールでソースコードをアップロードすると、下記画面のとおりGKE上で動作しているアプリケーションにコードを変更することなくログを追加したり、実行中のアプリケーションの変数やコールスタックのキャプチャを取得することが可能となります。
まとめ
弊社では、いくつかのJavaやKotlinのアプリケーションをGKE上で動かしていますが、jibやSkaffoldなどのツールは利用していなかったので、これらを利用することで普段の開発を効率化できそう!だと感じました。
また、ログの設定等の設定ファイルを書いて自分たちで対応したものの中にも、Spring Cloud GCPによって提供されている機能を利用していれば開発工数を削減できたように思う内容もかなりあり、今後の製品開発に取り入れられそうな内容が豊富で、非常に勉強になるセッションでした。
最後に
Google Cloud Next 2019で私が参加したセッションのうち、特に参考になった3つのセッションについて詳細を紹介させていただきました。
弊社では、GAEやGKEを利用して自社サービスを一緒に開発してくれるエンジニアを募集しています!
www.brainpad.co.jp