--- title: Cloud Native BuildpacksでSpring BootアプリのGraalVM Native Imageを作成しKnativeにデプロイする tags: ["Cloud Native Buildpacks", "Spring Boot", "GraalVM", "Docker", "Knative", "cert-manager"] categories: ["Dev", "Infrastructure", "CloudNativeBuildpacks", "SpringBoot", "GraalVM", "Knative"] date: 2020-09-21T11:14:36Z updated: 2020-09-22T15:56:36Z --- [前の記事](/entries/547)でCloud Native BuildpacksでSpring BootアプリのGraalVM Native Imageを作成する方法を紹介しました。 本記事ではこのNative Imageを[Knative](https://knative.dev)にデプロイし、相性が良いことを確認します。 **目次** ### アプリケーションの作成 [この記事](/entries/547)と同じです。 ``` curl https://start.spring.io/starter.tgz \ -s \ -d javaVersion=11 \ -d artifactId=hello-world \ -d bootVersion=2.4.0-M2 \ -d baseDir=hello-world \ -d dependencies=webflux,actuator \ -d packageName=com.example \ -d applicationName=HelloWorldApplication | tar -xzvf - cd hello-world cat <<'EOF' > src/main/java/com/example/HelloWorldApplication.java package com.example; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @SpringBootApplication(proxyBeanMethods = false) @RestController public class HelloWorldApplication { @GetMapping("/") public String hello() { return "Hello World!"; } public static void main(String[] args) { SpringApplication.run(HelloWorldApplication.class, args); } } EOF sed -i '' -e 's|spring-boot-maven-plugin|spring-boot-maven-pluginpaketobuildpacks/builder:tiny1-Dspring.native.remove-yaml-support=true -Dspring.spel.ignore=true -Dspring.xml.ignore=true -Dspring.spel.ignore=true|' pom.xml ./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=making/hello-world ``` > Note: 自分の環境だと`./mvnw spring-boot:build-image`でイメージを作成する時間のうち、初回は5分くらいがgraalvmのダウンロード時間です。
> 2回目以降はキャッシュされますが、イメージ名を変えると再度ダウンロードが行われます。この時間を省略するには、初めからgraalvmを同梱したbuildpackが必要です。
> プロプライエタリな[Java Native Image Buildpack for VMware Tanzu](https://network.pivotal.io/products/tanzu-java-native-image-buildpack)(`registry.pivotal.io/tanzu-java-native-image-buildpack/java-native-image:3.4.1`)はこの用途に使えます。
> [VMware Tanzu Network](https://network.pivotal.io)のアカウントを作成して、`docker login registry.pivotal.io`すれば、イメージをpullできます。
> VMware Tanzu NetworkではBuilderは用意されていないので、このBuildpackを含むBuilderを自分で作成する必要があります。
> Builderの作成方法は[こちらの記事](https://blog.ik.am/entries/542)を参考にしてください。 作成したイメージを確認します。`tiny` Stackを使用しているので、サイズは小さいです。 ``` $ docker image ls making/hello-world REPOSITORY TAG IMAGE ID CREATED SIZE making/hello-world latest 3f581955b4b7 40 years ago 86.2MB ``` [`dive`](https://github.com/wagoodman/dive)でイメージレイヤーを確認すると、18MBがOS部分で66MBがnative image部分であることがわかります。 ![image](https://user-images.githubusercontent.com/106908/93753340-f95ac680-fc3a-11ea-89b3-cee6933d33b9.png) まずはこのイメージを`docker run`で起動します。 ``` $ docker run --rm -p 8080:8080 making/hello-world . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: 2020-09-21 09:28:20.747 INFO 1 --- [ main] com.example.HelloWorldApplication : Starting HelloWorldApplication using Java 11.0.8 on cbf8dc7af660 with PID 1 (/workspace/com.example.HelloWorldApplication started by cnb in /workspace) 2020-09-21 09:28:20.747 INFO 1 --- [ main] com.example.HelloWorldApplication : No active profile set, falling back to default profiles: default 2020-09-21 09:28:20.811 INFO 1 --- [ main] o.s.b.a.e.web.EndpointLinksResolver : Exposing 2 endpoint(s) beneath base path '/actuator' 2020-09-21 09:28:20.817 WARN 1 --- [ main] i.m.c.i.binder.jvm.JvmGcMetrics : GC notifications will not be available because MemoryPoolMXBeans are not provided by the JVM 2020-09-21 09:28:20.825 WARN 1 --- [ main] io.netty.channel.DefaultChannelId : Failed to find the current process ID from ''; using a random value: -401303669 2020-09-21 09:28:20.827 INFO 1 --- [ main] o.s.b.web.embedded.netty.NettyWebServer : Netty started on port(s): 8080 2020-09-21 09:28:20.829 INFO 1 --- [ main] com.example.HelloWorldApplication : Started HelloWorldApplication in 0.091 seconds (JVM running for 0.095) ``` 8080ポートにアクセスして"Hello World!"が返ることを確認します。 ``` $ curl -s http://localhost:8080 Hello World! ``` ### Knativeのインストール 次のこのイメージを[Knative](https://knative.dev)にデプロイします。 ここではKnativeをDocker for Macにデプロイする例を紹介します。Kubernetes 1.16です。 ``` $ kubectl get node -o wide NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME docker-desktop Ready master 2m39s v1.16.6-beta.0 192.168.65.3 Docker Desktop 4.19.76-linuxkit docker://19.3.12 ``` "Reset Kubernetes Cluster"をクリックして一旦環境をクリーンアップします。 ![image](https://user-images.githubusercontent.com/106908/93749656-3d4acd00-fc35-11ea-9da8-ea1d0855d8d8.png) [cert-manager](https://cert-manager.io)を使わない場合と使う場合、それぞれ紹介します。 Network LayerにはStableな[Istio](https://istio.io)を使用します。 `kubectl`の他に次のCLIをインストールしてください。 * [`istioctl`](https://istio.io/latest/docs/ops/diagnostic-tools/istioctl) (`brew install istioctl`) * [`kn`](https://knative.dev/docs/install/install-kn) (`brew install knative/client/kn`) * [`vegeta`](https://github.com/tsenart/vegeta) (`brew install vegeta`) 次のバージョンで動作確認しています。 ``` $ kubectl version --client Client Version: version.Info{Major:"1", Minor:"18", GitVersion:"v1.18.5", GitCommit:"e6503f8d8f769ace2f338794c914a96fc335df0f", GitTreeState:"clean", BuildDate:"2020-07-04T15:01:15Z", GoVersion:"go1.14.4", Compiler:"gc", Platform:"darwin/amd64"} $ istioctl version --remote=false 1.7.2 $ kn version Version: v0.17.0 Build Date: 2020-08-26 11:08:52 Git Revision: 8fcd25c3 Supported APIs: * Serving - serving.knative.dev/v1 (knative-serving v0.17.0) * Eventing - sources.knative.dev/v1alpha2 (knative-eventing v0.17.0) - eventing.knative.dev/v1beta1 (knative-eventing v0.17.0) $ vegeta -version Version: 12.8.3 Commit: Runtime: go1.14 darwin/amd64 Date: 2020-03-25T14:41:40Z ``` #### cert-managerを使わない場合 次のコマンドでKnativeとIstioをインストールします。 ``` kubectl apply -f https://github.com/knative/serving/releases/download/v0.17.0/serving-crds.yaml kubectl apply -f https://github.com/knative/serving/releases/download/v0.17.0/serving-core.yaml istioctl install -y kubectl apply -f https://github.com/knative/net-istio/releases/download/v0.17.0/release.yaml ``` Pod一覧は次のようになります。 ``` $ kubectl get pod -A NAMESPACE NAME READY STATUS RESTARTS AGE docker compose-78f95d4f8c-w4wrz 1/1 Running 0 58m docker compose-api-6ffb89dc58-lr9pk 1/1 Running 0 58m istio-system istio-ingressgateway-855cb8cb9c-jjxt5 1/1 Running 0 24s istio-system istiod-5f4bd68b45-bm8qn 1/1 Running 0 28s knative-serving activator-68cbc9b5c7-mtkfg 1/1 Running 0 31s knative-serving autoscaler-5cf649dbb-zbfkb 1/1 Running 0 31s knative-serving controller-bc8d75cbc-ws68r 1/1 Running 0 31s knative-serving istio-webhook-6dd89ff45d-247nl 1/1 Running 0 16s knative-serving networking-istio-5d68bb7d56-r2t5x 1/1 Running 0 16s knative-serving webhook-85758f4589-knj5q 1/1 Running 0 31s kube-system coredns-5644d7b6d9-pxpsd 1/1 Running 0 59m kube-system coredns-5644d7b6d9-wrgr7 1/1 Running 0 59m kube-system etcd-docker-desktop 1/1 Running 0 58m kube-system kube-apiserver-docker-desktop 1/1 Running 0 58m kube-system kube-controller-manager-docker-desktop 1/1 Running 0 58m kube-system kube-proxy-lpfkf 1/1 Running 0 59m kube-system kube-scheduler-docker-desktop 1/1 Running 0 58m kube-system storage-provisioner 1/1 Running 0 58m kube-system vpnkit-controller 1/1 Running 0 58m ``` `istio-system` Namespaceに`istio-ingressgateway`という名前のServiceが作成されます。 Docker for Macの場合、`type: LoadBalancer`なServiceのExternal IPは`localhost`になります。 ``` $ kubectl get -n istio-system service istio-ingressgateway NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE istio-ingressgateway LoadBalancer 10.99.131.155 localhost 15021:30391/TCP,80:30649/TCP,443:30572/TCP,15443:32059/TCP 15m ``` ここではカスタムドメインに`local.maki.lol`を使用します。`*.local.maki.lol`が`127.0.0.1`に解決されます。 ``` kubectl patch configmap/config-domain -n knative-serving --type merge --patch '{"data":{"local.maki.lol":""}}' ``` `local.maki.lol`ではなく、`vcap.me`も利用可能です。 次に`kn`コマンドで作成したアプリケーションをデプロイします。 ``` kn service create demo --image making/hello-world --env MANAGEMENT_ENDPOINTS_WEB_EXPOSURE_INCLUDE='*' ``` 次のように出力されればOKです。 ``` Creating service 'demo' in namespace 'default': 0.031s The Configuration is still working to reflect the latest desired specification. 0.087s The Route is still working to reflect the latest desired specification. 0.120s Configuration "demo" is waiting for a Revision to become ready. 15.434s ... 15.472s Ingress has not yet been reconciled. 15.591s Waiting for load balancer to be ready 15.741s Ready to serve. Service 'demo' created to latest revision 'demo-mdysv-1' is available at URL: http://demo.default.local.maki.lol ``` エンドポイントにアクセスして、"Hello World!"が返ることを確認します。 ``` $ curl http://demo.default.local.maki.lol Hello World! ``` 1分ほどアクセスがなければPodは自動で削除されます。native-imageの場合、0インスタンスから1インスタンスの立ち上がりが速いです。 次のGifアニメでデモを示します。 ![knative-http](https://user-images.githubusercontent.com/106908/93757028-0aa6d180-fc41-11ea-90de-72442af5a800.gif) Podが存在しない状態から2-3秒で画面が表示されます。 次にAuto Scalingを試します。[Vegeta](https://github.com/tsenart/vegeta)を使って負荷をかけます。 Podの立ち上がりを次のGifアニメでデモを示します。 ![auto-scaling-http](https://user-images.githubusercontent.com/106908/93758052-b56bbf80-fc42-11ea-9d37-a61a43a94089.gif) ``` $ echo "GET http://demo.default.local.maki.lol" | vegeta attack -duration=10s -rate=200 | tee results.bin | vegeta report Requests [total, rate, throughput] 2000, 200.12, 200.04 Duration [total, attack, wait] 9.998s, 9.994s, 4.083ms Latencies [min, mean, 50, 90, 95, 99, max] 2.494ms, 489.215ms, 3.959ms, 2.038s, 2.551s, 2.977s, 3.095s Bytes In [total, mean] 24000, 12.00 Bytes Out [total, mean] 0, 0.00 Success [ratio] 100.00% Status Codes [code:count] 200:2000 Error Set: ``` レスポンスは100%成功です。0 -> 1のスケールアウト時のレスポンスに約3秒(Max Latency)かかっていることがわかります。50パーセンタイル(中央値)は3.959msなので、立ち上がってしまえば十分レスポンスが速いことがわかります。 #### cert-managerを使う場合 cert-managerを使えば、デプロイしたサービスにLet's Encryptの証明書を自動で設定することができます。 cert-managerは[この記事](/entries/530)のようにインストールしてください。`letsencrypt`という名前の`ClusterIssuer`オブジェクトができている前提です。 "Reset Kubernetes Cluster"をクリックして一旦環境をクリーンアップした状態で始めます。 次のコマンドでKnativeとIstioをインストールします。 ``` kubectl apply -f https://github.com/knative/serving/releases/download/v0.17.0/serving-crds.yaml kubectl apply -f https://github.com/knative/serving/releases/download/v0.17.0/serving-core.yaml istioctl install -y kubectl apply -f https://github.com/knative/net-istio/releases/download/v0.17.0/release.yaml ``` Pod一覧は次のようになります。 ``` $ kubectl get pod -A NAMESPACE NAME READY STATUS RESTARTS AGE cert-manager cert-manager-58f6cf4b79-xb4nr 1/1 Running 0 2m21s cert-manager cert-manager-cainjector-cdb5d697c-6hdsc 1/1 Running 0 2m21s cert-manager cert-manager-webhook-565d8c759b-9rtds 1/1 Running 0 2m21s docker compose-78f95d4f8c-gcxtt 1/1 Running 0 3m51s docker compose-api-6ffb89dc58-hqlcw 1/1 Running 0 3m51s istio-system istio-ingressgateway-855cb8cb9c-4r7tg 1/1 Running 0 81s istio-system istiod-5f4bd68b45-gvj9g 1/1 Running 0 87s knative-serving activator-68cbc9b5c7-dttf8 1/1 Running 0 89s knative-serving autoscaler-5cf649dbb-lhx7w 1/1 Running 0 89s knative-serving controller-bc8d75cbc-z6cd9 1/1 Running 0 89s knative-serving istio-webhook-6dd89ff45d-pnmdt 1/1 Running 0 74s knative-serving networking-istio-5d68bb7d56-sdgcj 1/1 Running 0 74s knative-serving webhook-85758f4589-ztvmd 1/1 Running 0 89s kube-system coredns-5644d7b6d9-2sfgb 1/1 Running 0 5m8s kube-system coredns-5644d7b6d9-nv7qm 1/1 Running 0 5m8s kube-system etcd-docker-desktop 1/1 Running 0 3m53s kube-system kube-apiserver-docker-desktop 1/1 Running 0 4m18s kube-system kube-controller-manager-docker-desktop 1/1 Running 0 3m53s kube-system kube-proxy-tswjt 1/1 Running 0 5m8s kube-system kube-scheduler-docker-desktop 1/1 Running 0 4m11s kube-system storage-provisioner 1/1 Running 0 3m52s kube-system vpnkit-controller 1/1 Running 0 3m52s ``` `istio-system` Namespaceに`istio-ingressgateway`という名前のServiceが作成されます。 Docker for Macの場合、`type: LoadBalancer`なServiceのExternal IPは`localhost`になります。 ``` $ kubectl get -n istio-system service istio-ingressgateway NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE istio-ingressgateway LoadBalancer 10.99.131.155 localhost 15021:30391/TCP,80:30649/TCP,443:30572/TCP,15443:32059/TCP 15m ``` ここではカスタムドメインに`local.maki.lol`を使用します。`*.local.maki.lol`が`127.0.0.1`に解決されます。 このカスタムドメインでLet's Encryptの証明書を発行できるのは、ドメイン所有者の筆者だけなので、この手順を試す場合は独自ドメインに変更してください。 ``` kubectl patch configmap/config-domain -n knative-serving --type merge --patch '{"data":{"local.maki.lol":""}}' ``` cert-manager連携をインストールします。 ``` kubectl apply -f https://github.com/knative/net-certmanager/releases/download/v0.17.0/release.yaml kubectl apply -f https://github.com/knative/serving/releases/download/v0.17.0/serving-nscert.yaml kubectl patch configmap/config-certmanager -n knative-serving --type merge --patch '{"data":{"issuerRef":"kind: ClusterIssuer\nname: letsencrypt"}}' kubectl patch configmap/config-network -n knative-serving --type merge --patch '{"data":{"autoTLS": "Enabled", "httpProtocol": "Redirected"}}' ``` 次に`kn`コマンドで作成したアプリケーションをデプロイします。 ``` kn service create demo --image making/hello-world --env MANAGEMENT_ENDPOINTS_WEB_EXPOSURE_INCLUDE='*' ``` 次のように出力されればOKです。 ``` Creating service 'demo' in namespace 'default': 0.026s The Configuration is still working to reflect the latest desired specification. 0.055s The Route is still working to reflect the latest desired specification. 0.095s Configuration "demo" is waiting for a Revision to become ready. 9.023s ... 9.071s Ingress has not yet been reconciled. 9.161s Certificate default.local.maki.lol is not ready. 68.206s Ingress has not yet been reconciled. 68.281s Ready to serve. Service 'demo' created to latest revision 'demo-xjqks-1' is available at URL: https://demo.default.local.maki.lol ``` エンドポイントにアクセスして、"Hello World!"が返ることを確認します。 ``` $ curl https://demo.default.local.maki.lol Hello World! ``` 0インスタンスから1インスタンスの立ち上がりを次のGifアニメでデモを示します。 ![knative-https](https://user-images.githubusercontent.com/106908/93759812-e26da180-fc45-11ea-8f0a-eac3e2635f6c.gif) Podが存在しない状態から2-3秒で画面が表示されます。 次にAuto Scalingを試します。Podの立ち上がりを次のGifアニメでデモを示します。 ![auto-scaling-https](https://user-images.githubusercontent.com/106908/93760049-46906580-fc46-11ea-9263-b11a6afc900f.gif) ``` $ echo "GET https://demo.default.local.maki.lol" | vegeta attack -duration=10s -rate=200 | tee results.bin | vegeta report Requests [total, rate, throughput] 2000, 200.10, 200.03 Duration [total, attack, wait] 9.999s, 9.995s, 3.811ms Latencies [min, mean, 50, 90, 95, 99, max] 2.523ms, 610.734ms, 4.93ms, 2.42s, 2.786s, 3.318s, 3.436s Bytes In [total, mean] 24000, 12.00 Bytes Out [total, mean] 0, 0.00 Success [ratio] 100.00% Status Codes [code:count] 200:2000 Error Set: ``` レスポンスは100%成功です。0 -> 1のスケールアウト時のレスポンスに約3.5秒(Max Latency)かかっていることがわかります。50パーセンタイル(中央値)は4.93msなので、立ち上がってしまえば十分レスポンスが速いことがわかります。 ### ExternalDNSが設定されている場合 独自ドメインを使っていて、[ExternalDNS](https://github.com/kubernetes-sigs/external-dns)がインストールされている場合は、次のアノテーションを設定すればIsto Ingres GatewayへのAレコードを自動でDNSに登録できます。 独自ドメインが`kn.maki.lol`の場合、 ``` kubectl annotate -n istio-system service istio-ingressgateway "external-dns.alpha.kubernetes.io/hostname=*.kn.maki.lol." kubectl annotate -n istio-system service istio-ingressgateway "external-dns.alpha.kubernetes.io/ttl=60" ``` --- Cloud Native BuildpacksでSpring BootアプリのGraalVM native imageを作成しKnativeにデプロイしました。 Native ImageはKnativeのようなプラットフォームと相性が良いです。 Native Image化の詳細とロードマップは[SpringOne](https://springone.io)の"The Path Towards Spring Boot Native Applications"というセッションで説明されています。こちらも参照してください。 * 動画: https://springone.io/post-event/sessions/the-path-towards-spring-boot-native-applications * スライド: https://www.slideshare.net/Pivotal/the-path-towards-spring-boot-native-applications