最近、「Kubernetes(K8s)」と「マイクロサービスアーキテクチャ」に興味が湧いています。
いろいろな記事を見ているとどちらもメリット・デメリットがあったりしますが、自分で触れてみないとわからないこともあるため、触れてみることにしました。
触れてみるにあたって、現在は「Kubernetes」をまだ勉強中なのですが、とりあえずロードバランサを作って負荷分散させるところまで学んだため、今回は『自分が作ったWebアプリをKubernetesにデプロイして負荷分散する』ことにチャレンジしようと思います。
主に使うもの
本記事では Windows 10 で開発します。
主に使うものは以下になります。
(インストールが必要なものは事前に導入されていることを前提とします)
- 開発面
- Java (ver: openjdk 13.0.1)
- Webアプリの開発言語として利用。
- Spring Initializr
- Spring のプロジェクトを簡単に作れるサービス。
- Spring Boot のプロジェクト作成に利用。
- Spring Boot
- 最小限の手間でSpringベースのアプリを作れるようにしたJavaフレームワーク。
- Webアプリの開発に利用。
- Gradle (ver: 6.0)
- ビルドツール。
- Spring Boot の成果物(JAR)を作成するために利用。
- Docker (ver: 19.03.5)
- 非常に軽量なコンテナ型の仮想化環境。
- Kubernetesにデプロイするために必要。
- Docker Desktop for Windows
- WindowsでDockerを実行できるようにするツール。
- DockerHub
- コンテナ化されたイメージをアップロードして公開・共有できるサービス。
- インフラ面
- GKE
- GCPでKubernetesの環境を簡単に構築するためのサービス。
- GCPのプロモーションクレジットが残ってたので今回はGKEを選びましたが、Kubernetesが使えれば何でもOKです。(AWSの「Amazon EKS」でもAzureの「AKS」でもOKです)
- Kubernetes(K8s)
- コンテナオーケストレーションツール。
- コンテナ化したアプリケーションのデプロイ、スケーリング、および管理が行える。
- gcloud
- GCPを操作できるCLIツール。
- kubectl
- Kubernetesクラスタに対してコマンドを実行することができるツール。
GKEではCloudShellでkubectlを使う方法もありますが、本記事ではローカルのgcloudのコンポーネントとしてインストールしたkubectlを使います。(コマンドプロンプトでkubectlを使います)
適当なWebアプリを作る
最初に適当なWebアプリを作ります。
- Spring Boot のプロジェクトを作成する
Spring Initializr を開いて「Gradle Project」「Java」「Spring Web」を選択して『Generate』を押します。他は任意です。
(今回作るWebアプリは一応BFFの役割なので、自分はArtifactを「bff」にしてます)
するとZipファイルがダウンロードされるので、任意の場所に保存して展開します。
展開したファイルが Spring Boot のプロジェクトになります。
以下のようなディレクトリ構成になります。
(自分は「C:\_work\」に展開しました)
> tree C:\_work\Bff /f C:\_WORK\BFF │ .gitignore │ build.gradle │ gradlew │ gradlew.bat │ HELP.md │ settings.gradle │ ├─gradle │ └─wrapper │ gradle-wrapper.jar │ gradle-wrapper.properties │ └─src ├─main │ ├─java │ │ └─com │ │ └─example │ │ └─bff │ │ BffApplication.java │ │ │ └─resources │ │ application.properties │ │ │ ├─static │ └─templates └─test └─java └─com └─example └─bff BffApplicationTests.java
- ソースを変更する
以下のとおりソースを変更します。
(お好きなエディタやIDEでどうぞ。自分は最近だと IntelliJ Community Edition を使ってます。無償です )
・環境変数「HOSTNAME」を返却するAPIを作成する。
・サーバはポート「80」でリクエストを受け付けるようにする。
(今回はWebサーバとAPサーバで分けたりしないので)
Application.javapackage com.example.bff; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.RequestMapping; // ←★追加 import org.springframework.web.bind.annotation.RestController; // ←★追加 @SpringBootApplication @RestController // ←★追加 public class BffApplication { public static void main(String[] args) { SpringApplication.run(BffApplication.class, args); } /* ↓★追加 ここから */ @RequestMapping("/") public String index() { String hostname = System.getenv("HOSTNAME"); return "HOSTNAME: " + hostname; } /* ↑★追加 ここまで */ }
application.properties
↓
application.yaml にファイル名を変更。
server: port: 80
- ビルドしてJARファイルを作成する
コマンドプロンプトで以下のコマンドを実行してJARファイルを作成します。
(自分と同じディレクトリ構成であれば「C:\_work\bff\build\libs\」に『bff-0.0.1-SNAPSHOT.jar』が作成されます)
> cd c:\_work\bff > gradle build
- 動作を確認する
Webアプリが動くかどうか確認してみます。
まずは、コマンドプロンプトで以下のコマンドを実行します。
> cd c:\_work\bff > java -jar build\libs\bff-0.0.1-SNAPSHOT.jar
実行結果:
. ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.2.1.RELEASE) 2019-11-23 07:02:13.850 INFO 1812 --- [ main] com.example.bff.BffApplication : Starting BffApplication on DESKTOP-PITSELN with PID 1812 (C:\_work\bff\build\libs\bff-0.0.1-SNAPSHOT.jar started by SYamada in c:\_work\bff) … 省略 … 2019-11-23 07:02:16.211 INFO 1812 --- [ main] com.example.bff.BffApplication : Started BffApplication in 2.844 seconds (JVM running for 3.55)
するとWebサーバが起動するので、ブラウザを開いて「http://localhost/」にアクセスします。
『HOSTNAME: 〇〇』が表示されればOKです。
(環境変数に「HOSTNAME」を定義していない場合は、『HOSTNAME: null』が表示されます)
WebアプリをDockerHubにプッシュする
次に、作ったWebアプリをDockerHubにプッシュします。
Dockerfileの作り方は以下を参考にしてます。
【参考】
- Dockerfileを作成する
Spring Boot のプロジェクトがあるディレクトリの直下(自分の場合は「c:\_work\bff」の直下)に『Dockerfile』というファイルを作成し、内容を以下のとおりにします。
DockerfileFROM openjdk:13-jdk-alpine VOLUME /tmp ARG JAR_FILE COPY ${JAR_FILE} app.jar ENTRYPOINT ["java","-jar","/app.jar"]
- Dockerのイメージを作成する
コマンドプロンプトで以下のコマンドを実行し、Dockerのイメージを作成します。
※「ysmmt」はご自身のDockerHubのアカウント名にしてください。自分の場合は「ysmmt」になります。
> cd c:\_work\bff > docker build --build-arg JAR_FILE=build/libs/bff-0.0.1-SNAPSHOT.jar -t ysmmt/bff-server:0.0.1 .
Dockerのイメージ一覧を確認(以下のコマンドを実行)して、「ysmmt/bff-server:0.0.1」が表示されればOKです。
> docker images
実行結果:
REPOSITORY TAG IMAGE ID CREATED SIZE ysmmt/bff-server 0.0.1 25e3b883ceef 14 seconds ago 354MB openjdk 13-jdk-alpine c4b0433a01ac 3 months ago 336MB
- イメージをDockerHubにプッシュする
コマンドプロンプトで以下のコマンドを実行し、DockerHubにイメージをアップロードします。
> docker push ysmmt/bff-server:0.0.1
DockerHubのリポジトリ一覧を見て、「ysmmt/bff-server」が表示されればOKです。
Kubernetesを準備してロードバランサを構築する
次に、Kubernetesの環境を準備して、ロードバランサの構築までやります。
- GKEでKubernetesのクラスタを作成する
GCPにログインし、[Kubernetes Engine] > [クラスタ] > [クラスタを作成] を押下します。
すると、クラスタテンプレートを選択する画面が表示されるので、[標準クラスタ] > [ゾーン]で[asia-northeast1-a (東京)]を選択 > [作成] を押下します。
※今回はそんなに凝ったものではないので、基本はデフォルトです。ゾーンはなんとなくで東京を選んでます。
これで、クラスタの作成は完了です。
クラスタサイズがノード数(VMインスタンス数)です。3台作成されており、クラスタに紐づけされてます。
この状態でkubectlにアクセスできるようになるので、アクセスしてみます。
上の画面の[接続]を押下すると、作成したクラスタにgcloudでアクセスするためのコマンドが表示されるので、コマンドプロンプトで実行します。
すると、作成したクラスタをkubectlコマンドで操作できるようになるので、試しに以下のコマンドを実行してみます。
「kubernetes」というサービスが一覧に表示されればOKです。
> kubectl get services
実行結果:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.4.0.1 <none> 443/TCP 17m
- ロードバランサのマニフェストを作成する
任意の場所(自分は「C:\_work\k8s-manifests」の直下)にロードバランサのマニフェストを作成します。
内容は以下のとおりです。
sample-lb.yaml
apiVersion: v1 kind: Service metadata: name: sample-lb spec: type: LoadBalancer ports: - name: "http-port" protocol: "TCP" port: 8080 targetPort: 80 nodePort: 30080 selector: app: sample-app
- マニフェストをKubernetesに渡してロードバランサを構築する
以下のコマンドを実行して、Kubernetesにロードバランサのマニフェストを適用させます。
> cd c:\_work\k8s-manifests\ > kubectl apply -f sample-lb.yaml
以下のコマンドで、"sample-lb"というサービスが見えればOKです。
これで、ロードバランサの構築は完了です。
> kubectl get services
実行結果:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.4.0.1 <none> 443/TCP 3d15h sample-lb LoadBalancer 10.4.13.158 34.84.89.145 8080:30080/TCP 118s
「EXTERNAL-IP」と「PORT」が外部に公開しているIPアドレスとポートになりますが、まだWebサーバの構築&Webアプリのデプロイをしてないので「http://34.84.89.145:8080/」をブラウザで開いても何も表示されません。
KubernetesにWebサーバを構築&Webアプリをデプロイする
最後に、Webサーバの構築&Webアプリのデプロイをやります。
Webサーバ(Tomcat)は Spring Boot に組み込まれているので、今回の場合はKubernetesのPodのコンテナイメージとして、作ったWebアプリを指定すればWebサーバの構築&Webアプリのデプロイができる形になります。
- Webサーバのマニフェストを作成する
任意の場所(自分は「C:\_work\k8s-manifests」の直下)にWebサーバ&Webアプリのマニフェストを作成します。
内容は以下のとおりです。
sample-lb.yaml
apiVersion: apps/v1 kind: Deployment metadata: name: sample-deployment spec: replicas: 3 selector: matchLabels: app: sample-app template: metadata: labels: app: sample-app spec: containers: - name: bff-server image: ysmmt/bff-server:0.0.1 ports: - containerPort: 80
- マニフェストをKubernetesに適用してWebサーバを構築する
以下のコマンドを実行して、KubernetesにWebサーバ&Webアプリのマニフェストを適用させます。
> cd c:\_work\k8s-manifests\ > kubectl apply -f sample-deployment.yaml
以下のコマンドで、"sample-deployment-〇〇"というPodが3つ見えて、STATUSが「Running」になっていればOKです。
> kubectl get pods -o wide
実行結果:
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES sample-deployment-cdf7d878d-2dv9w 1/1 Running 0 3m 10.0.0.2 gke-standard-cluster-1-default-pool-946a7330-2llj <none> <none> sample-deployment-cdf7d878d-l7pgd 1/1 Running 0 3m 10.0.1.3 gke-standard-cluster-1-default-pool-946a7330-34rm <none> <none> sample-deployment-cdf7d878d-q2w6n 1/1 Running 0 3m 10.0.1.2 gke-standard-cluster-1-default-pool-946a7330-34rm <none> <none>
「NODE」が、実際にPodが配置されたノード(VMインスタンス)になります。
ノードは全部で3つあるはずなのに、2つに対してだけPodが割り振られている理由は、割り振られなかった1つのノードで既にKubernetesが「システムで使うためのPod」を起動しており、リソースに空きがないためとなります。
以下のコマンドで各ノードのリソース使用状況を確認できます。
> kubectl top node
実行結果:
NAME CPU(cores) CPU% MEMORY(bytes) MEMORY% gke-standard-cluster-1-default-pool-946a7330-2llj 38m 4% 710Mi 26% gke-standard-cluster-1-default-pool-946a7330-34rm 41m 4% 832Mi 31% gke-standard-cluster-1-default-pool-946a7330-ggs9 126m 13% 873Mi 33%
次のコマンドでKubernetesのシステムが起動しているPodが見れます。
割り振られなかったノード「〇〇-ggs9」に大半が起動しています。
> kubectl -n kube-system get pod -o wide
実行結果:
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES event-exporter-v0.2.4-5f88c66fb7-2ggqv 2/2 Running 0 4d15h 10.0.2.5 gke-standard-cluster-1-default-pool-946a7330-ggs9 <none> <none> fluentd-gcp-scaler-59b7b75cd7-l27k7 1/1 Running 0 4d15h 10.0.2.2 gke-standard-cluster-1-default-pool-946a7330-ggs9 <none> <none> fluentd-gcp-v3.2.0-dkjlq 2/2 Running 0 2d 10.146.0.29 gke-standard-cluster-1-default-pool-946a7330-34rm <none> <none> fluentd-gcp-v3.2.0-q7d9s 2/2 Running 0 2d 10.146.0.30 gke-standard-cluster-1-default-pool-946a7330-ggs9 <none> <none> fluentd-gcp-v3.2.0-tbnh7 2/2 Running 0 2d 10.146.0.28 gke-standard-cluster-1-default-pool-946a7330-2llj <none> <none> heapster-75bc7bd966-ftqdp 3/3 Running 0 3d14h 10.0.2.6 gke-standard-cluster-1-default-pool-946a7330-ggs9 <none> <none> kube-dns-79868f54c5-dm8hf 4/4 Running 0 4d15h 10.0.2.7 gke-standard-cluster-1-default-pool-946a7330-ggs9 <none> <none> kube-dns-79868f54c5-zs47s 4/4 Running 0 4d15h 10.0.2.3 gke-standard-cluster-1-default-pool-946a7330-ggs9 <none> <none> kube-dns-autoscaler-bb58c6784-smc62 1/1 Running 0 4d15h 10.0.2.4 gke-standard-cluster-1-default-pool-946a7330-ggs9 <none> <none> kube-proxy-gke-standard-cluster-1-default-pool-946a7330-2llj 1/1 Running 0 2d 10.146.0.28 gke-standard-cluster-1-default-pool-946a7330-2llj <none> <none> kube-proxy-gke-standard-cluster-1-default-pool-946a7330-34rm 1/1 Running 0 2d 10.146.0.29 gke-standard-cluster-1-default-pool-946a7330-34rm <none> <none> kube-proxy-gke-standard-cluster-1-default-pool-946a7330-ggs9 1/1 Running 0 2d 10.146.0.30 gke-standard-cluster-1-default-pool-946a7330-ggs9 <none> <none> l7-default-backend-fd59995cd-rg6lh 1/1 Running 0 4d15h 10.0.2.9 gke-standard-cluster-1-default-pool-946a7330-ggs9 <none> <none> metrics-server-v0.3.1-57c75779f-tmlf4 2/2 Running 0 4d15h 10.0.2.8 gke-standard-cluster-1-default-pool-946a7330-ggs9 <none> <none> prometheus-to-sd-blkhv 2/2 Running 0 2d 10.146.0.29 gke-standard-cluster-1-default-pool-946a7330-34rm <none> <none> prometheus-to-sd-fz47w 2/2 Running 0 2d 10.146.0.28 gke-standard-cluster-1-default-pool-946a7330-2llj <none> <none> prometheus-to-sd-jzrs9 2/2 Running 0 2d 10.146.0.30 gke-standard-cluster-1-default-pool-946a7330-ggs9 <none> <none>
次のコマンドでKubernetesのシステムが起動しているPodのリソース使用量を確認できます。
> kubectl -n kube-system top pod
実行結果:
NAME CPU(cores) MEMORY(bytes) event-exporter-v0.2.4-5f88c66fb7-2ggqv 1m 17Mi fluentd-gcp-scaler-59b7b75cd7-l27k7 0m 40Mi fluentd-gcp-v3.2.0-dkjlq 5m 152Mi fluentd-gcp-v3.2.0-q7d9s 9m 152Mi fluentd-gcp-v3.2.0-tbnh7 5m 146Mi heapster-75bc7bd966-ftqdp 2m 42Mi kube-dns-79868f54c5-dm8hf 2m 31Mi kube-dns-79868f54c5-zs47s 2m 31Mi kube-dns-autoscaler-bb58c6784-smc62 1m 7Mi kube-proxy-gke-standard-cluster-1-default-pool-946a7330-2llj 5m 13Mi kube-proxy-gke-standard-cluster-1-default-pool-946a7330-34rm 4m 14Mi kube-proxy-gke-standard-cluster-1-default-pool-946a7330-ggs9 5m 14Mi l7-default-backend-fd59995cd-rg6lh 1m 1Mi metrics-server-v0.3.1-57c75779f-tmlf4 3m 21Mi prometheus-to-sd-blkhv 0m 13Mi prometheus-to-sd-fz47w 1m 13Mi prometheus-to-sd-jzrs9 1m 13Mi
とりあえず、これでWebサーバの構築&Webアプリのデプロイは完了です。
動作を確認する
ブラウザを開き、「http://34.84.89.145:8080/」にアクセスしてみます。
すると、先ほどと異なり今度はHOSTNAMEがきちんと表示されてます。
このHOSTNAMEはKubernetesが自動でつける環境変数であり、Podの名前が設定されます。
なので、どのPod(=Webサーバ)にアクセスされたかが判断できます。
定期的にブラウザを更新(=F5押下)してみると、アクセスされたHOSTNAMEが変わります。
問題なく負荷分散できてるようですね。
ブラウザだとなかなかアクセス先が変わらないので、linuxコマンド使えるアプリ等(=Gitbash等)で以下のコマンドを実行したほうが確認しやすいです。
$ curl -s http://34.84.89.145:8080/ HOSTNAME: sample-deployment-cdf7d878d-2dv9w $ curl -s http://34.84.89.145:8080/ HOSTNAME: sample-deployment-cdf7d878d-q2w6n $ curl -s http://34.84.89.145:8080/ HOSTNAME: sample-deployment-cdf7d878d-l7pgd
感想
実際に手を動かしてみても思いましたが、Kubernetesを使うとインフラ構築が圧倒的に楽ですね。
「やりたいこと」を少しの手間で実現できてしまうあたり、Spring Boot とかのフレームワークと同じだなと感じました。
Spring Boot とかは『アプリ開発のフレームワーク』とすると、Kubernetesは『インフラ構築のフレームワーク』というところでしょうか。
いろいろな記事を見てるとKubernetesは事前学習のコストがかかるというのがデメリットとして挙げられていますが、自分からすると、アプリ開発でフレームワークを使うのは当たり前なので、デメリットって感じはしないかなぁと。
事前学習した分、インフラ構築の実作業時間を削減できるので、事前学習で使った時間は取り戻せる気がします。
(急を要するプロジェクトだと使わない方がいいと思いますが。。)
あと、もう一ついいと思ったのは、やはり「インフラの構成をコードに残せる」ところですかね。
バージョン管理できますし、Kubernetesを知っている人がコード(マニフェスト)を見れば、どういう構成で成り立っているのか分かるので属人化の対策にもなります。Kubernetesを使わない場合でも地道に各サーバにアクセスして構成を見ればわかることかとは思いますが、「一つ一つのサーバにアクセスして確認する工数」と「コードを見て確認する工数」では、後者のほうが時間かからないと思いますし。
…と、ここまで学習&実装してみて感じたところはそんなところですかね。
今のままだとまだKubernetesやインフラの知識が浅いので、業務で使うかどうかはもっと色々と試してからにしようと思います。
業務で使えなくとも、個人で色々とアプリの動作を確認したいときでもKubernetesは便利なので、今後も個人的には使っていこうと思います。(よく、「負荷分散を意識したWebアプリ作って試したいけど、インフラ構築が面倒…」と思い、試せないまま終わることがあるので。。アプリの動作を見たいのに、インフラ構築に時間がかかるとなるとモチベーションが一気に下がっちゃいます)
今後は、BFFの後ろのリソースサーバ作ったり、作ったAPIをSPAから利用したり、openid_connectを使った認証を実装したり、セッションストレージを用意して負荷分散したBFFのセッション管理したりと、Kubernetesで色々と試して業務で使えそうか確認していこうと思ってます。
感想が長くなっちゃいましたが、以上!