Skaffoldを用いてローカルでk8sにデプロイするJavaアプリの開発を行なう

はじめに

最近身の回りでSkaffoldという名前をよく聞くようになりまして、ちょっと気になって調べたら面白そうだったし、今後使っていきそうな雰囲気を感じたので、ちょっとさわっておこうかと思います。

Skaffoldとは?

Skaffoldはk8sネイティブなアプリケーションの開発をサポートしてくれるコマンドラインツールです。
k8sに対するBuild、Push、Deploy等をサポートしてくれます。 大まかには以下のような機能や特徴があります。

  • ローカルでの開発において、のソースコードの変更を検知して、自動でBuild、Push、Deployまでのサポート。
  • ローカルの開発において、ログに対するサポートとポートフォワードのサポート
  • git cloneskaffold runの実行で様々な環境でアプリを動作させることが可能
  • Skaffoldのprofile, local user config, environment variables, flags などの機能を使って環境ごとの設定を組み込むことが可能
  • skaffold renderコマンドを用いてKubernetesマニフェストのテンプレートをレンダリングすることによって、GitOpsワークフローをサポート
  • Clusterは無くクライアント再度のみで独立している
  • skaffold.yamlファイルによって宣言的で、プラガブルな設定が可能

Skaffold自体はCI/CDにおけるワークフローのサポートも行っているようですが、今回のこのブログではローカルでのアプリケーション開発におけるいくつかの機能を試してみたいと思います。
また、今回はJavaアプリケーションで開発を行ってみようと思います。

使ってみる

動作環境

ローカルのクラスタはMinikube(Docker Drive)を用いて構築します。

$ uname -srvmpio
Linux 5.4.0-77-generic #86-Ubuntu SMP Thu Jun 17 02:35:03 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

$ lsb_release -a
LSB Version:    core-11.1.0ubuntu2-noarch:security-11.1.0ubuntu2-noarch
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.2 LTS
Release:    20.04


$ minikube version
minikube version: v1.21.0
commit: 76d74191d82c47883dc7e1319ef7cebd3e00ee11

$ kubectl version -o yaml
clientVersion:
  buildDate: "2021-03-18T01:10:43Z"
  compiler: gc
  gitCommit: 6b1d87acf3c8253c123756b9e61dac642678305f
  gitTreeState: clean
  gitVersion: v1.20.5
  goVersion: go1.15.8
  major: "1"
  minor: "20"
  platform: linux/amd64
serverVersion:
  buildDate: "2021-05-12T12:32:49Z"
  compiler: gc
  gitCommit: 132a687512d7fb058d0f5890f07d4121b3f0a2e2
  gitTreeState: clean
  gitVersion: v1.20.7
  goVersion: go1.15.12
  major: "1"
  minor: "20"
  platform: linux/amd64

Skaffoldのインストール

Skaffoldをインストールするためには以下のコマンドを叩きます。

$ curl -Lo skaffold https://storage.googleapis.com/skaffold/releases/latest/skaffold-linux-amd64 && \
   sudo install skaffold /usr/local/bin/

$ skaffold version
v1.27.0

その他、MacWindows、Dockerでのインストールはこちらをご確認ください。

サンプルアプリを作成しておく

Skaffoldを使ってデプロイやディバグなどの機能を試すために開発対象となるアプリを作っておきます。
特に深い意図はないのですがSpringを使ってやろうかと思います。
Spring Initializrで以下の設定でアプリを作成します。

f:id:yuya_hirooka:20210709030915p:plain

依存はWebだけを追加してます。

Skaffoldプロジェクトを初期化する

ダウンロードしてきたプロジェクトを解凍して、プロジェクトのルートに移動し以下のコマンドを実行します。
Skaffoldプロジェクトを初期化するには、skaffold initコマンドを用います。
skaffold initコマンドは実行するとプロジェクトをスキャンし、以下のようなファイルを見つけるとその構成に合わせた設定を行ってくれます。

  • Dockerfile
  • build.gradle/pom.xml
  • package.json
  • requirements.txt
  • go.mod

ちなみに500MB以上のファイルは無視されるようです。
例えば、こんかいのケースではいくつかの選択肢を提示してくれます。

$  skaffold init --generate-manifests
? Select port to forward for pom-xml-image (leave blank for none): 8080

--XXenableJibInitフラグや--XXenableBuildpacksInitフラグを使えば、それぞれJibやBuildpacksを用いた構成を作ることも可能なようです。
--generate-manifestsフラグはマニフェストの生成まで行ってもらうために使用しています。このフラグを使用しない場合は自分で作成したdeployment.yamlを用いることになります。
今回の場合はpom.xmlのみが検知され、Buildpackを用いた設定がされます。

$  skaffold init --generate-manifests
? Select port to forward for pom-xml-image (leave blank for none): 8080
adding manifest path deployment.yaml for image pom-xml-image
apiVersion: skaffold/v2beta18
kind: Config
metadata:
  name: skaffold-sample
build:
  artifacts:
  - image: pom-xml-image
    buildpacks:
      builder: gcr.io/buildpacks/builder:v1
deploy:
  kubectl:
    manifests:
    - deployment.yaml
portForward:
- resourceType: service
  resourceName: pom-xml-image
  port: 8080

deployment.yaml - apiVersion: v1
kind: Service
metadata:
  name: pom-xml-image
  labels:
    app: pom-xml-image
spec:
  ports:
  - port: 8080
    protocol: TCP
  clusterIP: None
  selector:
    app: pom-xml-image
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: pom-xml-image
  labels:
    app: pom-xml-image
spec:
  replicas: 1
  selector:
    matchLabels:
      app: pom-xml-image
  template:
    metadata:
      labels:
        app: pom-xml-image
    spec:
      containers:
      - name: pom-xml-image
        image: pom-xml-image

? Do you want to write this configuration, along with the generated k8s manifests, to skaffold.yaml? Yes
Generated manifest deployment.yaml was written
Configuration skaffold.yaml was written
You can now run [skaffold build] to build the artifacts
or [skaffold run] to build and deploy
or [skaffold dev] to enter development mode, with auto-redeploy

skaffold.yamlは以下のようになります。

apiVersion: skaffold/v2beta18
kind: Config
metadata:
  name: skaffold-sample
build:
  artifacts:
  - image: pom-xml-image
    buildpacks:
      builder: gcr.io/buildpacks/builder:v1
deploy:
  kubectl:
    manifests:
    - deployment.yaml
portForward:
- resourceType: service
  resourceName: pom-xml-image
  port: 8080

上記のskaffold.yamlではビルドやデプロイ、ポートフォワードの設定が行われています。
その他、ここで使われていない項目や設定の説明はこちらをご覧ください。

次に進む前にイメージの名前がpom-xml-sampleだとあまりにもあまりになので以下のように書き換えておきます。
また、Buildkitを有効にしておきます。

apiVersion: skaffold/v2beta18
kind: Config
metadata:
  name: skaffold-sample
build:
  artifacts:
  - image: spring-app
    buildpacks:
      builder: gcr.io/buildpacks/builder:v1
deploy:
  kubectl:
    manifests:
    - deployment.yaml
portForward:
- resourceType: service
  resourceName: pom-xml-image
  port: 8080

作成されたk8sマニフェストpom-xml-sampleの部分もspring-appに書き換えておきます。

apiVersion: v1
kind: Service
metadata:
  name: spring-app
  labels:
    app: spring-app
spec:
  ports:
  - port: 8080
    protocol: TCP
  clusterIP: None
  selector:
    app: spring-app
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: spring-app
  labels:
    app: spring-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: spring-app
  template:
    metadata:
      labels:
        app: spring-app
    spec:
      containers:
      - name: spring-app
        image: spring-app

devモードをで開発を行なう

前述したとおり、Skaffoldはローカルでの開発に置いてソースコードの変更を検知して、自動でBuild、Push、Deployまでのサポートまでをサポートしてくれます。
devモードで起動するとその機能が利用可能で、以下のコマンドでdevモードで起動します。

$ skaffold dev

(省略)

Starting test...
Tags used in deployment:
 - spring-app -> spring-app:e0b79f2a42356a8de0ba7b3da0f0f74903c0d9b99ddf1db39ed36a872a90d577
Starting deploy...
 - service/spring-app created
 - deployment.apps/spring-app created
Waiting for deployments to stabilize...
 - deployment/spring-app is ready.
Deployments stabilized in 1.129 second
Press Ctrl+C to exit
Watching for changes...
[spring-app] 
[spring-app]   .   ____          _            __ _ _
[spring-app]  /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
[spring-app] ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
[spring-app]  \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
[spring-app]   '  |____| .__|_| |_|_| |_\__, | / / / /
[spring-app]  =========|_|==============|___/=/_/_/_/
[spring-app]  :: Spring Boot ::                (v2.5.2)
[spring-app] 
[spring-app] 2021-07-08 17:17:30.213  INFO 20 --- [           main] d.h.s.SkaffoldSampleApplication          : Starting SkaffoldSampleApplication v0.0.1-SNAPSHOT using Java 11.0.11 on spring-app-6d5b5c74c4-2q6xs with PID 20 (/workspace/target/skaffold-sample-0.0.1-SNAPSHOT.jar started by cnb in /workspace)
[spring-app] 2021-07-08 17:17:30.215  INFO 20 --- [           main] d.h.s.SkaffoldSampleApplication          : No active profile set, falling back to default profiles: default
[spring-app] 2021-07-08 17:17:31.073  INFO 20 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
[spring-app] 2021-07-08 17:17:31.085  INFO 20 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
[spring-app] 2021-07-08 17:17:31.086  INFO 20 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.48]
[spring-app] 2021-07-08 17:17:31.156  INFO 20 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
[spring-app] 2021-07-08 17:17:31.157  INFO 20 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 880 ms
[spring-app] 2021-07-08 17:17:31.683  INFO 20 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
[spring-app] 2021-07-08 17:17:31.691  INFO 20 --- [           main] d.h.s.SkaffoldSampleApplication          : Started SkaffoldSampleApplication in 2.022 seconds (JVM running for 2.444)

コマンドを実行するとビルドが始まり少し待つとKubernetes上にデプロイされます。
また、devモードで起動するとローカルマシンへのポートフォワードも自動的に行ってくれます

ここまででServiceとDeploymentがローカルのMinikubeで作ったクラスタに作成されリソースが作られている状態でかつホストマシンへのポートフォワードまで行われています。

$ kubectl get svc
NAME         TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)    AGE
kubernetes   ClusterIP   10.96.0.1    <none>        443/TCP    7d3h
spring-app   ClusterIP   None         <none>        8080/TCP   4m48s


$ kubectl get deploy
NAME         READY   UP-TO-DATE   AVAILABLE   AGE
spring-app   1/1     1            1           5m17s

8080ポートフォワードされているのでcURLでアクセスしてみます。

$ curl localhost:8080
{"timestamp":"2021-07-08T17:28:57.961+00:00","status":404,"error":"Not Found","path":"/"}

現状はコントローラーを作成していないので404が返ってきます。
以下のクラスを作成してコントローラーを1つ作ってみます。

@RestController
public class SampleController {

    @GetMapping("/")
    public String helle(){
        return "Hello, Skaffold";
    }
}

コードを修正すると自動でビルドが走りクラスターにデプロイされます。
再度、cURLでアクセスすると今度はHello, Skaffoldの文字列が返ってきます。

$ curl localhost:8080
Hello, Skaffold

debugモードで起動して、IntelliJを用いてDebugする

Skaffoldのdebugモードはdevモードと同じように動作しますが、debug用のPodが立ち上がりlanguage runtimeに応じたdebug用のポートがホストにポートフォワードされます。
Javaの場合はJDWPを用いてdebugが可能となるようです。
ここで、debug自動デプロイ機能が無効になるので注意が必要です。
以下のコマンドでdebugモードで起動します。

$ skaffold debug
(省略)
[spring-app] Picked up JAVA_TOOL_OPTIONS: -agentlib:jdwp=transport=dt_socket,server=y,address=5005,suspend=n,quiet=y
(省略)
Port forwarding pod/spring-app-7b74d66d9-42wns in namespace default, remote port 5005 -> 127.0.0.1:5005

ログに出力されているように出力されJWDPのポートが5005で公開されてるのがわかります。

InteliJからリモートdebug用のプロセスに接続します。
Run/Debug Configurationを開き左上の+ボタンからRemote JVM Debugを選択します。

f:id:yuya_hirooka:20210709025735p:plain

基本はデフォルトのままの設定で大丈夫ですが、名前の部分だけspring-appにしておきます。
Applyを押してIntelliJをDebug実行をすると起動します。
先程のコントローラーにブレークポイントを置いておきます。

f:id:yuya_hirooka:20210709030116p:plain

この状態で再度cURLでリクエストを投げると置いたブレークポイントで停止することが確認できます。

f:id:yuya_hirooka:20210709030224p:plain