おうちKubernetesにVue.jsで作ったポートフォリオを乗せる

## はじめに

[フューチャー Advent Calendar 2022](https://qiita.com/advent-calendar/2022/future)の7日目の記事です。
以前、[Future Tech Blog](https://future-architect.github.io/)の方で「[Raspberry Piをかき集めてKubernetesを体感する](https://future-architect.github.io/articles/20220908a/)」を書かせて頂き、おうちKubernetesを実装してみました。
今回はおうちKubernetesにVue.jsとFlaskを乗っけて自作のポートフォリオをデプロイしてみようと思います。

## おうちKubernetes

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271414/81ca35db-92dc-51b4-d276-ad53d6918827.png" width="500">

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271414/f2dd6306-b9ac-a237-a924-50055eecffea.png" width="500">


おうちKubernetesは、Raspberry Piを組み合わせたClusterです。
1台をMaster node（Control Plane）とし、残り2台をWorker nodeとして構成しています。

|  Node  |  Machine  |IPアドレス
| ---- | ---- | ---- |
|  Master  |  Raspberry Pi 4B (4GB)  |192.168.1.101|
|  Worker1  |  Raspberry Pi 4B (8GB)  |192.168.1.102|
|  Worker2  |  Raspberry Pi 4B (8GB)  |192.168.1.103|

[前回](https://future-architect.github.io/articles/20220908a/)からのアップデートとして、

- Worker nodeのマシンを統一
    - Raspberry Pi 3Bと交換しました。
- インスタ映えしそうな筐体を導入
    - 大きなファンがついたので、冷却性能が上がりました。
    - ファンが光ります。

の変更を加えました。
Raspberry Pi 4Bは当分買えないかなーと思っていましたが、[スイッチサイエンス](https://www.switch-science.com/products/6370)さんの方で入荷通知登録をしておくことで購入できました！

## 全体像

今回作成したコードは全てこちらのレポジトリで公開しています。
https://github.com/bigface0202/k8s-flask-vuejs

### 完成後のおうちKubernetes全体像

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271414/9f6bcd85-a0f9-669b-f355-85c37261310a.png" width="500">



### 完成後のディレクトリツリー

```
/k8s-flask-vuejs
├── README.md
├── app
│   ├── Dockerfile
│   ├── flask
│   │   ├── main.py
│   │   └── requirements.txt
│   └── vue
│       └── vue関係のファイル・ディレクトリ
└── k8s
    ├── deployment.yaml
    ├── metallb.yaml
    ├── namespace.yaml
    └── service.yaml
```

## ポートフォリオ側の準備

Vue.js側でのポートフォリオの実装は省略し、Vue.jsのHello Worldを利用します。

### vue-cliでVue.jsの初期環境を構築する

では早速やっていきましょう。npmやvue-cliのインストールは省略させて頂きます。

```bash:terminal
# "vue"というディレクトリ名で作成
vue create vue
# Vue 3を選択してエンター
Vue CLI v5.0.8
? Please pick a preset: (Use arrow keys)
❯ Default ([Vue 3] babel, eslint) 
  Default ([Vue 2] babel, eslint) 
  Manually select features
```

### Vue.jsのDelimterを変更する

Delimiterが初期設定のままだとFlaskのDelimiter(`'{{', '}}'`)が被ってしまい、Vue.js上で動的な表示を実装することができません。
`vue.config.js`に以下を追記することDelimiterを変更します（ついでにビルド先のディレクトリ構成も変更しています）。

```js:vue.config.js
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  transpileDependencies: true,
// 以下を追記
  outputDir: 'dist',
  assetsDir: 'static',
  indexPath: 'templates/index.html',
  chainWebpack: config => {
    config.module
      .rule('vue')
      .use('vue-loader')
      .loader('vue-loader')
      .tap(options => {
        options.compilerOptions = { 
          whitespace: 'condense',
          delimiters: ['[[', ']]']
        };
        return options;
      });
  }
})
```

Delimiterを変更したので、一部書き換えて検証してみましょう。

```diff:vue/src/components/HelloWorld.vue
...
  <div class="hello">
-    <h1>{{ msg }} </h1>
+    <h1>[[ msg ]] </h1>
    <p>
...
```

```diff:vue/src/App.vue
...
  <img alt="Vue logo" src="./assets/logo.png">
-  <HelloWorld msg="Welcome to Your Vue.js App"/>
+  <HelloWorld msg="このポートフォリオはおうちK8Sで運営されています。"/>
...
```

```terminal:terminal
# 確認する
npm run serve
# 以下が表示される
> vue@0.1.0 serve
> vue-cli-service serve

 INFO  Starting development server...

 DONE  Compiled successfully in 2778ms                                                                                                                                                                                 4:14:31 PM
  App running at:
  - Local:   http://localhost:8080/ 
  - Network: http://172.18.47.103:8080/

  Note that the development build is not optimized.
  To create a production build, run npm run build.
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271414/237c1648-3426-bfe0-9a0d-52e7d4b32be1.png" width="500">

上記の画面が表示されれば完了です。

### ビルドしておく

ビルドします。

```terminal:terminal
// ビルドコマンド
npm run build

 DONE  Compiled successfully in 2968ms                                                                                                                                                                                11:55:22 AM

  File                                        Size                                                                                    Gzipped

  dist/static/js/chunk-vendors.e5344afe.js    72.40 KiB                                                                               27.17 KiB
  dist/static/js/app.006afa24.js              13.05 KiB                                                                               8.40 KiB
  dist/static/css/app.2cf79ad6.css            0.33 KiB                                                                                0.23 KiB
```

`dist`ディレクトリが作成されていればOKです。

### Flask

WebフレームワークとしてPythonでFlaskを利用します。
Python3やライブラリ周りの環境構築は省略致します。

```python:main.py
import os
from flask import Flask, render_template

# Specify the directory of Vue.js building files
app = Flask(__name__, static_folder='../vue/dist/static', template_folder='../vue/dist/templates')

# For SPA
@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def index(path):
    return render_template('index.html')

if __name__ == "__main__":
    app.run(debug=True, host="0.0.0.0", port=int(os.environ.get("PORT", 8080)))
```

動作確認してみましょう。

```bash:terminal
# Flaskを起動してみる
python3 main.py

 * Serving Flask app 'main'
 * Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on all addresses (0.0.0.0)
 * Running on http://127.0.0.1:8080
 * Running on http://172.18.47.103:8080
Press CTRL+C to quit
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 846-044-410
```

`http://127.0.0.1:8080`にアクセスして、先程Vue.jsの検証の画面と同じ画面が表示されればOKです。

### Dockerイメージ

Dockerの環境構築は省略致します。
また、[Docker Hub](https://hub.docker.com/)を利用するのでアカウントを作成しておいてください。

Kubernetes上でDeploymentを作成するのにDockerイメージが必要となるため、上記で構築した環境をDocker上でも構築できるようにする必要があります。

```dockerfile:Dockerfile
# Multi-stage building
# Building Vue.js
FROM node:17-alpine3.14 as vue-build

COPY vue /app
WORKDIR /app
RUN npm install

RUN npm run build

# Building python3 flask environment and copy Vue.js from vue-build
FROM python:3.10-slim

ENV PYTHONUNBUFFERED True

ENV APP_HOME /app
COPY flask $APP_HOME
WORKDIR $APP_HOME
COPY --from=vue-build /app/dist ./html

RUN pip install --no-cache-dir -r requirements.txt

# Execute web server by gunicorn
ENV PORT 8080
CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 main:app
```

Vue.jsとFlaskをマルチステージにてビルドするようにしています。
Dockerfileをビルドする前に、Flask側で検証したコードを一部書き換える必要があります。

```diff:main.py
import os
from flask import Flask, render_template

# Specify the directory of Vue.js building files
- app = Flask(__name__, static_folder='../vue/dist/static', template_folder='../vue/dist/templates')
+ app = Flask(__name__, static_folder='html/static', template_folder='html/templates')

...
```

Flask検証時はローカルにて実行したため`static_folder`や`template_folder`をVue.jsのビルド先を指定しておりましたが、Dockerfile上ではビルドファイルのコピー先ディレクトリを指定する必要があり、そのように書き換えます。

では、ビルドしてみましょう。今回はpush先に[Docker Hub](https://hub.docker.com/)を利用するため、自身のDocker Hubとイメージ名をタグとして付けておきます。
**※検証の際には自身のWindows/Mac/Linux PCでビルド・動作検証を行えますが、おうちKubernetes上に載せる際はARM製CPUにてビルドする必要があります。**
**Raspberry Pi 4BなどARM製品でビルドしてください。**

```bash:terminal
# <Your-Docker-Account>に自身のアカウント名を入力してください
docker build -t <Your-Docker-Account>/flask-vuejs:v1.0 .
# ビルド完了したら、コンテナを作成し検証してみましょう。
docker run -p 8080:8080 d2a8815d10f9                                                                                                                                           
[2022-12-04 03:32:28 +0000] [1] [INFO] Starting gunicorn 20.1.0
[2022-12-04 03:32:28 +0000] [1] [INFO] Listening at: http://0.0.0.0:8080 (1)
[2022-12-04 03:32:28 +0000] [1] [INFO] Using worker: gthread
[2022-12-04 03:32:28 +0000] [7] [INFO] Booting worker with pid: 7
```

`http://0.0.0.0:8080`へアクセスしてVue.jsの画面が表示されればOKです。
これでポートフォリオとなるDockerイメージが作成できました。

## Kubernetes側の準備

既にマスターノード側へkube-flannelを導入し、ワーカーノードがjoinしている前提で始めます。

```bash:terminal
kubectl get nodes

NAME                STATUS   ROLES           AGE   VERSION
mas01.example.com   Ready    control-plane   16h   v1.24.4
work01              Ready    worker          16h   v1.25.0
work02              Ready    worker          16h   v1.25.0
```

### Namespaceの作成

おうちKubernetesでは様々なクラスタが共存する（予定）ため、Namespaceで分けることにします。
`myapp-space`というNamespaceを作成します。

```yaml:namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: myapp-space
```

```bash:terminal
kubectl get namespace
# 以下が表示される
NAME              STATUS   AGE
default           Active   17h
kube-flannel      Active   16h
kube-node-lease   Active   17h
kube-public       Active   17h
kube-system       Active   17h
myapp-space       Active   6m
```

### Deploymentの作成

おうちKubernetes上（Raspberry Pi上）に乗っける前に**ARM向けにビルドする必要**があるので、Raspberry PiへSSHするなどしてさきほどのDockerfileをビルドしましょう。

```bash:terminal(Raspberry Pi)
# ビルド
docker build -t bigface00/flask-vuejs:v1.1 .
# プッシュ
docker push <Your-Docker-Account>/flask-vuejs:v1.1
```

ビルドしたVue.jsとFlaskのイメージをKubernetes上にデプロイするためのDeploymentを作成していきます。

```yaml:deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-deployment
  namespace: myapp-space
spec:
  selector:
    matchLabels:
      app: myapp
  replicas: 2 # tells deployment to run 2 pods matching the template
  template:
    metadata:
        name: myapp
        namespace: myapp-space
        labels:
          app: myapp
    spec:
      containers:
        - name: myapp
          image: <Your-Docker-Account>/flask-vuejs:v1.1 # Please specify your docker account
          ports:
          - containerPort: 8080
          livenessProbe: # Check live or not of container
            httpGet:
              port: 8080
              path: /
            failureThreshold: 5
            periodSeconds: 5
```

Namespace`myapp-space`に`myapp-deployment`というdeployment作成します。
作成したdeploymentの中には`myapp`というコンテナがあり、コンテナイメージは今回pushしたDocker Hubのイメージを指定しています。
サービスを公開するポートに対して、`containers.ports.containerPort`を設定しています。
また、`livenessProve`で設定したポートを指定することでコンテナが生きているかどうかを判断します。生きていない場合は、コンテナが再起動されます。

applyしましょう。

```bash:terminal
# apply
kubectl apply -f deployment.yaml
# 以下が表示される
deployment.apps/myapp-deployment created
# 確認
kubectl get pods -n myapp-space
# Runningなことを確認する
NAME                               READY   STATUS    RESTARTS   AGE
myapp-deployment-fbc449488-mrqz6   1/1     Running   0          13s
myapp-deployment-fbc449488-vgf67   1/1     Running   0          13s
```

これでVue.jsとFlaskのコンテナがデプロイされました。
ただ、このままではコンテナへのアクセス先が無いため、デプロイしたアプリを見ることはできません。

### MetalLBの導入とServiceの作成

Serviceを作成することで、コンテナを外部に公開するためのエンドポイントを作成します。
外部公開と言ってもインターネット上にさらすことはなく、ローカルネットワーク上からのみアクセス可能となります。
また、GKEなどパブリッククラウドのKubernetesエンジンでは、ServiceにLBを指定するとCloud Load Balancingを使ってくれるなどよしなにやってくれるのですが、おうちKubernetes上ではよしなにやってくれません。
MetalLBを利用することでLoadbalanserTypeのServiceを使えるようにしてくれます。

[MetalLB, bare metal load-balancer for Kubernetes](https://metallb.universe.tf/installation/)を参考にインストールします。

```bash:terminal
# MetalLBのマニフェストをapply
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.13.5/config/manifests/metallb-native.yaml
# シークレットを作成
kubectl create secret generic -n metallb-system memberlist --from-literal=secretkey="$(openssl rand -base64 128)"
```

`memberlist`というシークレットを作成しているのは、MetalLBで[memberlist](https://github.com/hashicorp/memberlist)という機能を利用するため必要な手順となります。

参考：[MetalLB入門 ― オンプレKubernetesクラスタでLoadBalancerタイプのServiceを使う](https://www.kaitoy.xyz/2020/10/31/metallb/#metallb%E3%81%AE%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4)

確認します。

```bash:terminal
kubectl get pod -n metallb-system
# 以下が表示される
NAME                          READY   STATUS    RESTARTS   AGE
controller-8689779bc5-lnfnb   0/1     Pending   0          70s
speaker-4k4k4                  1/1     Running   0          70s
```

次にLBとしての設定用マニフェストファイルを作成します。

```yaml:metallb.yaml
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: pool-ips
  namespace: metallb-system
spec:
  addresses:
  - 192.168.1.210-192.168.1.215 # External IP range
  autoAssign: true
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: pool-ips
  namespace: metallb-system
spec:
  ipAddressPools:
  - pool-ips
```

1つめの`IPAddressPool`では外部IPとして利用するIPプールを作成します。
`spec.addresses`に記載されているIPレンジ（`192.168.1.210-192.168.1.215`）の中からオートマチックに外部IPを決定します。
今回のIPレンジはおうちKubernetesで利用しているローカルIPを避けて指定しています。
2つめの`L2Advertisement`は広報のための設定です。
Podが持つIPアドレスはCluster IPのため、Kubernetes外からでは認識することができません。
そのため、「`192.168.1.210`の宛先はこのPodですよ～」という感じで周りに**広報**する必要があります。
MetalLBではその役割も担ってくれます。

最後にServiceを作成します。

```yaml:service.yaml
apiVersion: v1
kind: Service
metadata:
  name: myapp-service-lb # Name of Service
  namespace: myapp-space
  annotations:
    metallb.universe.tf/address-pool: pool-ips # IP pool name of MetalLB
spec:
  type: LoadBalancer
  ports:
    - name: myapp-service-lb
      protocol: TCP
      port: 8080 # Listen port by Service's IP
      nodePort: 30080 # Listen port by node's IP (30000-32767)
      targetPort: 8080 # Listen port number by transfered destination (deployed container)
  selector: # service のselctorは、matchLabels 扱いになる
    app: myapp # Pod's label of transfered destination (deployed container)
```

外部IPとしてMetalLBのIPプールを利用するため、`annotations`にてIPプール名`pool-ips`を指定しています。
`selector.app`はdeploymentで利用するPodのラベル名を指定します。

ここまで作成したマニフェストファイルをapplyしましょう。

```bash:terminal
# MetalLBのapply
kubectl apply -f metallb.yaml
# 以下が表示される
ipaddresspool.metallb.io/pool-ips created
l2advertisement.metallb.io/pool-ips created
# Serviceのapply
kubectl apply -f service.yaml 
# 以下が表示される
service/myapp-service-lb created
# applyされた内容を確認しましょう
kubectl get svc -n myapp-space
# 以下が表示され、EXTERNAL-IPが割り振られていればOK
NAME               TYPE           CLUSTER-IP     EXTERNAL-IP     PORT(S)          AGE
myapp-service-lb   LoadBalancer   10.100.2.129   192.168.1.210   8080:30080/TCP   11s
```

これでコンテナへアクセスする準備が整いました。`192.168.1.210:8080`へアクセスしてみましょう。
Vue.jsの画面が表示されたでしょうか？これにてVue.jsのデプロイが完了しました。
残りは良い感じにポートフォリオを作成していきましょう！

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271414/fe995b04-b770-fa83-4ee9-f1e90571a2c3.png" width="500">


## まとめ

ここまでで自分のポートフォリオをおうちKubernetes上に乗っけることができました。
ポートフォリオ程度ではKubernetesの恩恵にあやかることはできなさそうですが、これにMySQLなどのDBをデプロイしてアプリ化することでKubernetesのパフォーマンスを発揮できるのではないかと思います。
また、ここから更にインターネット上に公開することで、実際に運用するまで持っていけると面白そうです。

記事には載せていませんが、今回の実装ではコンテナイメージを引っ張る際に、ローカル上から引っ張ろうとしてうまくいかず、Google CloudのArtifact Registryから引っ張ろうとしましたが、途中で403エラーになり原因も解明できずで思わぬところで苦戦してしまいました。結局、Docker Hubに落ち着いてからはスムーズにできました。

次のハードウェアアップデートとしてはPoEハットとPoE対応ハブを買って電源コードを廃止し、よりスリム化したおうちKubernetesにしたいところです。
明日は@orangekame3さんの記事になります！