このブログを検索

この記事の内容は、個人の見解、検証の範囲のものであり、誤りがある可能性があります。
個人の責任において情報活用をお願いします。


2019年12月9日月曜日

Kubernetesでマイクロサービスなアプリを作れるようになるため、ちょっと試した(自作Webアプリ+Kubernetes+負荷分散)

※このブログは、Advent Calendar 2019 大國魂(ITブログ)の9日目です。

最近、「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アプリを作ります。
  1. 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
    
  2. ソースを変更する

    以下のとおりソースを変更します。
    (お好きなエディタやIDEでどうぞ。自分は最近だと IntelliJ Community Edition を使ってます。無償です )

     ・環境変数「HOSTNAME」を返却するAPIを作成する。
     ・サーバはポート「80」でリクエストを受け付けるようにする。
      (今回はWebサーバとAPサーバで分けたりしないので)

    Application.java
    package 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
    

  3. ビルドしてJARファイルを作成する

    コマンドプロンプトで以下のコマンドを実行してJARファイルを作成します。
    (自分と同じディレクトリ構成であれば「C:\_work\bff\build\libs\」に『bff-0.0.1-SNAPSHOT.jar』が作成されます)

    > cd c:\_work\bff
    > gradle build
    

  4. 動作を確認する

    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の作り方は以下を参考にしてます。

 【参考】

  1. Dockerfileを作成する

    Spring Boot のプロジェクトがあるディレクトリの直下(自分の場合は「c:\_work\bff」の直下)に『Dockerfile』というファイルを作成し、内容を以下のとおりにします。

    Dockerfile
    FROM openjdk:13-jdk-alpine
    VOLUME /tmp
    ARG JAR_FILE
    COPY ${JAR_FILE} app.jar
    ENTRYPOINT ["java","-jar","/app.jar"]
    

  2. 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
    

  3. イメージをDockerHubにプッシュする

    コマンドプロンプトで以下のコマンドを実行し、DockerHubにイメージをアップロードします。

    > docker push ysmmt/bff-server:0.0.1
    

    DockerHubのリポジトリ一覧を見て、「ysmmt/bff-server」が表示されればOKです。



Kubernetesを準備してロードバランサを構築する


次に、Kubernetesの環境を準備して、ロードバランサの構築までやります。
  1. 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
    
  2. ロードバランサのマニフェストを作成する

    任意の場所(自分は「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
    
  3. マニフェストを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アプリのデプロイができる形になります。
  1. 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
    
  2. マニフェストを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で色々と試して業務で使えそうか確認していこうと思ってます。

感想が長くなっちゃいましたが、以上!