Spring Bootで開発したアプリをKubernetes環境へ展開してみる

# 概要
Spring Bootで開発したアプリケーションをKubernetes環境上へ展開してみたため、備忘録も兼ねてまとめます。
なお、jarのビルドやコンテナイメージ作成などを1つ1つ順に進める形式で実施しております。
（実際はCloud Native Buildpacks等を利用するとより簡単かと思います）

## 本記事の対象
* Kubernetesを触り始めた方
* SpringアプリケーションをKubernetes上へ展開したい方

# 用意するもの
* Springで開発したアプリケーション
* Kubernetes環境
* コンテナリポジトリ

## Springで開発したアプリケーション
ご自身で展開してみたいSpringアプリケーションをご用意ください。
なお、本記事では過去作った以下のアプリケーションを展開してみております。

https://github.com/murajo/RandomGreetingSpring

こちらのアプリケーションはビルドはMavenでJavaのバージョンは11を指定しており、本記事でもそれベースで記載しています。
そのため、必要に応じてご自身のアプリケーションのものと置き換えていただけたらと思います。

## Kubernetes環境
以下図のようなKubernetes環境となります。
本来はコンテナイメージを作成する環境と展開先のKubernetes環境は分けた方がよいと思いますが、今回はまとめて展開先のMasterNode上でイメージ作成なども実施しております。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406495/2ef37f15-caa2-2370-6576-dbfdfa6810f7.png)
一応以下記事に同じ構成を作成する手順をまとめております。

https://qiita.com/strawberryjam/items/f0ff7bef425f6a711b10

## コンテナリポジトリ
コンテナイメージを作成したあとに格納しておくためのコンテナリポジトリが必要となります。
本記事ではDocker Hubを利用しておりますが、プライベートリポジトリなどがあればそちらでも問題ないと思います。

# 手順
それではここからアプリケーションをKubernetesへ展開するまでの手順を記載します。
流れは以下となります。
* アプリケーションのビルド
* コンテナイメージの作成
* コンテナリポジトリへ登録
* Kubernetesへデプロイ

なお前述でも記載した通り、本記事では上記手順すべてMasterNode上で実施しています。

## アプリケーションのビルド
まず対象のアプリケーションを用意します。
ここではgit hubからクローンします。
```shell
git clone https://github.com/murajo/RandomGreetingSpring.git
```

続いてビルドするためのパッケージをインストールします。
※対象のアプリケーション側に合わせてMavenをインストールしています
```shell
sudo apt install maven
```
```shell
# 動作確認
mvn --version

# 出力
Apache Maven 3.6.3
Maven home: /usr/share/maven
Java version: 11.0.17, vendor: Ubuntu, runtime: /usr/lib/jvm/java-11-openjdk-amd64
Default locale: en_US, platform encoding: UTF-8
OS name: "linux", version: "5.4.0-135-generic", arch: "amd64", family: "unix"
```

ビルドツールの準備が出来たため、用意したアプリケーションのフォルダへ移動してビルド用のコマンドを実行します。
```shell
mvn install

# 出力抜粋（若干時間がかかる）
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
```

無事にビルドが完了しますと、targetフォルダの中にjarファイルが作成されます。
作成されたjarファイルを試しに実行してみて動作を確認してみます。
私のアプリケーションではREST APIで3種類のJSONから1つをランダムで返す仕組があるため、試しにcurlを実行してみます。
```shell
# jarを実行
java -jar target/RandomGreeting-0.0.1-SNAPSHOT.jar

# curlで動作確認
curl -X POST http://localhost:8080/greeting/search

# 出力
{"greetingId":3,"greeting":"Good evening","imagePath":"http://urx3.nu/xUVy"}
```

問題なく実行できていることが確認できました。
以上でビルドは完了となります。

## コンテナイメージの作成
ここからは先程ビルドしたjarを用いてコンテナイメージを作成していきます。

まずはイメージ作成用に以下のようなDockerfileをアプリケーションのフォルダ直下に作成します。
※ FROMのjdkのバージョンはビルドしたアプリケーションに合わせて記載ください
```Dockerfile
FROM openjdk:11
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
```

作成したDockerfileでイメージを作成します。
※タグ名は後程コンテナリポジトリに合わせてタグ付けしなおします
```shell
sudo docker build -t {任意の名前} .

#出力
Successfully built 8b9de8bdb44b
Successfully tagged {前述で記載した名前}:latest
```

コンテナイメージが作成されたか確認します。
```shell
sudo docker images

# 出力
REPOSITORY                     TAG       IMAGE ID       CREATED        SIZE
randomgreeting                 latest    8b9de8bdb44b   2 hours ago    691MB
```
無事に作成されていることが確認できました。
それでは作成されたイメージでコンテナを起動して、動作を見てみます。
※実行コマンド内の「randomgreeting」の箇所は設定したタグ名に変更ください。
```shell
# コンテナを起動
sudo docker run -p 80:8080 randomgreeting

# curlで動作確認
curl -X POST http://localhost/greeting/search

# 出力
{"greetingId":3,"greeting":"Good evening","imagePath":"http://urx3.nu/xUVy"}
```

問題なく実行できていることが確認できました。
以上でコンテナイメージの作成は完了となります。

## コンテナリポジトリへ登録
ここから作成したコンテナイメージをコンテナリポジトリへ登録していきます。
なお本記事ではDocker hubをベースで説明します。

まずブラウザからDocker Hubへログインを行い、リポジトリの作成をしておきます。
Create repositoryの画面へ飛び、Nameを入力してCreateボタンを押します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406495/cad421e0-cb8d-3f33-1c8f-3357154b7f4c.png)

リポジトリの作成が完了しましたら、ユーザー名込みのリポジトリ名を確認しておきます。
以下図ですと **storoveryjam/randomgreeting** となります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406495/aa9a4454-2f86-74cd-e310-8da2d43f36de.png)

それでは先程作成したコンテナイメージをリポジトリへ上げる準備を進めます。
まず作成したリポジトリとコンテナイメージの名前を合わせておきます。
```shell
sudo docker tag {作成したコンテナイメージ名} {リポジトリ名}

# 本記事上のサンプル
# sudo docker tag randomgreeting storoveryjam/randomgreeting
```

イメージの一覧を確認すると、同じIDでもう1つイメージが出来ていることが確認できます。
```shell
sudo docker images

# 出力
REPOSITORY                     TAG       IMAGE ID       CREATED        SIZE
randomgreeting                 latest    8b9de8bdb44b   2 hours ago    691MB
storoveryjam/randomgreeting    latest    8b9de8bdb44b   2 hours ago    691MB
```

それでは利用しているDocker hubにログインを行い、Pushを実行します。
```shell
# dockerへログイン
# ユーザー名とパスワードを聞かれるため、入力してください
sudo docker login

# Docker hubへPush
sudo docker push storoveryjam/randomgreeting
```

Push完了後、ブラウザのDocker hubへ戻り、イメージが追加されていれば完了となります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406495/c9f4dee2-ae96-ec3b-8c0c-ce521b84c1e1.png)

**補足**
今回手順としてDocker hub上でリポジトリを作成してからその名前に合わせてPushを実施しましたが、実際にはこの部分は省略可能です。
あらかじめコンテナイメージの名前を{リポジトリのユーザー名/イメージ名}のようにPush先のユーザー名が入っていれば、自動でDocker hub上にリポジトリも作成されます。
本記事上ではわかりやすくるために丁寧に行いましたが、省いていただいて問題ございません。

## Kubernetesへデプロイ
それでは最後にKuberntesへのデプロイを行っていきます。

まずデプロイのために各マニフェストを作成します。
ここではkubectl createコマンドでイメージを指定してマニフェストを生成しています。
```shell
# Podのマニフェスト作成
kubectl create deployment {任意の名前} --image {コンテナリポジトリの名前} -o yaml --dry-run=client > deployment.yaml

# Serviceのマニフェスト作成
kubectl create service clusterip {Pod側で指定した任意の名前} --tcp 80:8080 -o yaml --dry-run=client > service.yaml

# 本記事上のサンプル
# kubectl create deployment randomgreeting --image storoveryjam/randomgreeting -o yaml --dry-run=client > deployment.yaml
# kubectl create service clusterip randomgreeting --tcp 80:8080 -o yaml --dry-run=client > service.yaml
```

それではマニフェストの用意が出来たので、それぞれデプロイします。
合わせてデプロイした内容を確認して無事にデプロイがされていれば問題ないです。
```shell
kubectl apply -f deployment.yaml
kubectl apply -f service.yaml
```
```shell
kubectl get svc,pods

# 出力
NAME                     TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
service/randomgreeting   ClusterIP   10.106.228.69   <none>        80/TCP    2m21s

NAME                                  READY   STATUS    RESTARTS   AGE
pod/randomgreeting-849cb56466-s68dj   1/1     Running   0          2m22s
```
それではservice側に記載されている「CLUSTER-IP」へリクエストを投げて動作を見てみます。
```shell
# curlで動作確認
curl -X POST 10.106.228.69/greeting/search

# 出力
{"greetingId":2,"greeting":"Hello","imagePath":"http://urx3.nu/REm4"}
```
無事に動作していることが確認できました。
以上でKubernetsへのデプロイまで完了となります。

### おまけ
私のサンプルアプリケーションは一応Web UIもあり、せっかくなのでLBを設定してブラウザから見てみようと思います。
※同じようにWeb UIがあるアプリケーションで実施されてましたら、よければお試しください。
今回はLBをあらかじめ使用できる環境にKubernetesがなっている前提とします。
参考として以下記事のリンク先箇所にてMetallbを使ったクラスタ外への公開の仕方をまとめております。

https://qiita.com/strawberryjam/items/f0ff7bef425f6a711b10#nginx%E3%81%AE%E5%A4%96%E9%83%A8%E5%85%AC%E9%96%8B

それでは実際に設定していきます。
といいつつやることは少なく単純で、先程デプロイしたservice.yamlの下の方の行のTypeを**LoadBalancer**変更して再デプロイをするだけとなります。
```service.yaml
apiVersion: v1
kind: Service
metadata:
  creationTimestamp: null
  labels:
    app: randomgreeting
  name: randomgreeting
spec:
  ports:
  - name: 80-8080
    port: 80
    protocol: TCP
    targetPort: 8080
  selector:
    app: randomgreeting
  type: LoadBalancer
status:
  loadBalancer: {}
```

変更したら再デプロイをして情報を確認します。
```shell
kubectl apply -f service.yaml
kubectl get svc

# 出力
NAME             TYPE           CLUSTER-IP     EXTERNAL-IP     PORT(S)        AGE
randomgreeting   LoadBalancer   10.98.92.199   172.22.18.216   80:30279/TCP   5s
```

EXTERNAL-IPに記載されたIPアドレスへ同じネットワーク内のブラウザからアクセスを行いますと、対象のアプリケーションのWeb UIが確認できました。
※URLの後ろのパスなどは各々のアプリケーションに合わせて追記ください
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406495/5e5976d1-d19c-c7ca-0d49-c884c799e0c2.png)


# 最後に
今回はSpring Bootで開発したアプリケーションをKubernetes上に展開するまでの一連の流れを記載しました。
一方Cloud Native Buildpacksを使うとビルドからイメージ作成までより簡単に実現できるらしいので、手軽さという観点ではそちらを触ってみるのもよいと考えております。
（また、Tanzu Application Platoformではgithub上のソースコードのURLでそのまま公開URLの掃き出しまで行えるらしいです）

https://buildpacks.io/


ただし便利なツール群を使う前に実際はどのようなことを裏でやっているのかと知る上では、今回のように1つ1つ実施してみるのもよいかなと思っております。