GKE(Kubernetes)でTODO管理Linebot を作った話 【Flask, Python】

# はじめに
業務でKubernetesで運用しているシステムの開発と運用に携わってきました。0からKubernetesのシステム全体を構築する機会はなかったのでこれを機に簡単な**Flaskを使ったLineBotをGKE上で構築しました。**


個人制作における簡易的なLinebot作成の目的であれば、[Cloud functions](https://cloud.google.com/functions/docs/concepts/overview?hl=ja)や[AWS Lambda](https://aws.amazon.com/jp/lambda/)などサーバーレスな技術的を用いて迅速にデプロイできるため、Kubernetesを使うのは少しオーバースペックに思うかも知れません。
今回は、Kubernetes(on GKE)を使って0からシステムを構築し、**Kubernetesを使ったアプリの全体像を把握したい**こと、及び、Kubernetesの各オブジェクトへの理解を深めることを大きな目的としています。

とはいえKubernetesを採用するメリットもあり代表的には以下が挙げられます。
- **リクエスト数が増えた際にスケールアウトしやすい**こと。
-\> Podの設定(replicas数など)を変更するだけで容易にスケールアウトが可能であること。リソースを増強する際いちいち新たなサーバーの構築作業に時間をかける必要がありません。
- リクエスト数が少ないときに**最小限のリソースを元にサービスを運用することが可能**であること。
-\> 例えばサービスを立ち上げたばかりでユーザーのリクエスト数が少ないときは、Pod数を1台にするなどPod数を限りなく少なくして運用することができます。多くのパブリッククラウドにおいて、NodeやPodの使用数によって課金額が決まるため、システムの負荷に応じてリソースの調整が可能です。

本記事では`Deployment`や`Service`などKubernetesでデプロイする際に使うであろう一通りのオブジェクトを使用して、**TODO管理用LineBot**を作成したシステムの紹介します。
アプリのロジックは簡易的なのでKubernetesの記述方法にフォーカスしてキャッチアップしやすい内容であると思います。

加えて```docker-compose```を使って[**ローカルでアプリケーション立ち上げる**](#ローカル環境での立ち上げ)ことができるようにしました。
`docker-compose`を使ったコンテナ連携の方法とKubernetesの方法の違いを手を動かしながら比較することができます。[^1]

# 全体のソースコード
`secret.yaml`などの一部のファイルを除き、全体のソースコードを公開しているので、適宜参照してください。
- [github/kotaaaa/linebot](https://github.com/kotaaaa/linebot)

## 使用した技術とバージョン一覧
| Tech       | Version  |
| ---------- | -------- |
| Python     | 3.9.7    |
| Flask      | 1.1.4    |
| Kubernetes | 1.23.5   |
| Docker     | 20.10.12 |
| Nginx      | 1.21.6   |
| Mysql      | 5.7      |

# [Kubernetes](https://kubernetes.io/ja/docs/concepts/workloads/controllers/deployment/)(k8s)とは
> Kubernetes (K8s)は、デプロイやスケーリングを自動化したり、コンテナ化されたアプリケーションを管理したりするための、オープンソースのシステムです。

近年、Infrastructure as Code(IaC)と総称されるインフラの設定をコードで管理する技術が数多く導入されています。宣言的な記述を元にサーバースペックなどのインフラパラメータをコードで記述することで冪等性を考慮でき、インフラ構築の再現性を高めることができます。加えて、保守にかかるコストを削減することができ、属人化を避けることにも繋がることがメリットとして挙げられ数多くのプロダクトで導入が進んでいます。
その中でもk8sはコンテナオーケストレーションのプロダクトとしてデファクトスタンダードとなっている[CNCF](https://ja.wikipedia.org/wiki/Cloud_Native_Computing_Foundation)の代表的なOSSの一つです。

# [GKE](https://cloud.google.com/kubernetes-engine/docs/concepts/kubernetes-engine-overview?hl=ja)とは
> Google Kubernetes Engine（GKE）は、Google のインフラストラクチャを使用して、コンテナ化されたアプリケーションのデプロイ、管理、スケーリングを行うマネージド環境を提供します。GKE 環境は複数のマシン（具体的には、Compute Engine インスタンス）で構成され、これらのマシンがグループ化されてクラスタを形成します。

GCPの提供するKubernetes のマネージドサービスです。本システムではGKE上で各Kubernetesオブジェクトを作成しアプリケーションをデプロイします。


# TODO管理LineBotの要件
* テキストを送るとそのタスクを含めたタスクの一覧を教えてくれます。
* 改行区切りで複数のタスクを同時に入力可能であり、既存のTODOタスク名と同じテキストを送信することで、TODOタスクからそのタスクを消去できます。
* 特定の文字列を送信することでタスクの一斉削除も可能です。

## トークサンプル
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239544/ed37b3ee-47d7-ab7e-aaca-3c2176cba1be.png" width="40%">


使い方としては、Lineの友達追加をした後、トーク画面でTODOの追加と削除ができます。


# システム構成図
![](https://raw.githubusercontent.com/kotaaaa/linebot/master/architecture_linebot.drawio.svg)

Appサーバー、Webサーバー、DBサーバーをそれぞれ別の`Pod`(コンテナ)上で立ち上げる3階層システムを構築しました。
`Service`間の通信においては`metadata`を用いてKubernetes内部で名前解決を行います。
GCPのVPCネットワークにより、グローバルIP静的アドレスを予約し、またそれ用の独自ドメインを取得しておきます。
それらをドメイン登録事業者内のDNS設定でAレコードを使ったDNS設定により、紐付けをします。
リクエストは、`ドメイン`-> `グローバルIPアドレス(Ingress)` -> `Service(linebot-nginx)`を経てコンテナが起動されているPodにたどり着き、コンテナで処理されます。

使用した **GCPのサービス** は以下のとおりです。
* Google Kubernetes Engine(Node はGoogle Compute Engine上で稼働する)
* Google Cloud Load Balancing
* Google Domain
* Google VPC network
* Google Artifact Registry


# ディレクトリ構成

```
$ tree
.
├── LICENSE
├── README.md
├── __pycache__
│   └── app.cpython-38.pyc
├── app               <-------------------- Flask Application　関連のディレクトリ
│   ├── Dockerfile
│   ├── __pycache__
│   │   ├── app.cpython-39.pyc
│   │   └── wsgi.cpython-39.pyc
│   ├── app.py
│   ├── application.log
│   ├── deployment.yaml
│   ├── development.json
│   ├── requirements.txt
│   ├── service.yaml
│   └── uwsgi.ini
├── architecture_linebot.drawio.svg
├── db               <-------------------- Mysql を使ったDBサーバーの設定を記述
│   ├── configmap.yaml
│   ├── deployment.yaml
│   ├── initdb.d
│   │   └── init.sql
│   ├── persistent-volume.yaml
│   ├── secret.yaml
│   └── service.yaml
├── docker-compose.yaml　　　　　　　　　　　<-------------- ローカル実行用docker-compose.yaml
├── ingress　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　<--------------　　SSL証明書、Ingress設定を記述
│   ├── managed-cert-ingress.yaml
│   └── managed-cert.yaml
└── web               <-------------------- Nginx を使ったWebサーバーの設定を記述
    ├── Dockerfile
    ├── default.conf
    ├── deployment.yaml
    └── service.yaml
```

# ローカル環境での立ち上げ
`docker-compose`コマンドによりApp、Web、DBのコンテナをローカル環境で立ち上げることができます。
```
$ pwd
/path/to/dir/linebot/
// App,Web,DBの3つのアプリケーションを起動
$ docker-compose up
```

# 事前準備

GKEへアプリをデプロイする以外に設定や作成することを以下に列挙します。

## LINE Developers へのアカウント登録、アクセスキーの取得、疎通確認
アクセストークンとシークレットトークンを取得します。自分の場合は、app/development.json内の環境変数で値を保持します。
Line Developersへの登録とLinebotのCallBackの実装に関しては本記事では触れていません。Githubの実装もしくは、他の方の記事をご参照ください。

## 公開ドメインに対するSSL証明書の取得
[GoogleマネージドSSL証明書の使用](https://cloud.google.com/kubernetes-engine/docs/how-to/managed-certs?hl=ja#gcloud)を参考に`Ingress`オブジェクトを用いたGoogleマネージドSSL証明書を構築しました。

### [静的外部グローバルIPアドレスの予約](https://cloud.google.com/compute/docs/ip-addresses/reserve-static-external-ip-address?hl=ja)

```
gcloud compute addresses create linebot-ingress --global --ip-version IPV4
```
`linebot-ingress` には、好きなIPアドレスの名前を入力します。ここで作成するアドレスはリージョンタイプでなくグローバルタイプを指定する必要があります。
これは後の`Ingress`オブジェクト内のglobal-static-ip-nameで指定するのでメモしておいてください。

### 一般公開向けドメインの取得

公開用ドメインの取得をします。筆者は[Google Domain](https://domains.google/intl/ja_jp/?gclsrc=ds&gclsrc=ds)を使用しています。取得したドメイン名も後の`Ingress`オブジェクト内で指定します。

### 取得したドメインのDNS設定
取得したドメインと予約済静的アドレスの紐付けを行うため、DNSの設定を修正します。
今回はドメインに対してIPアドレスを紐つけることなので、Aレコードを新規で追加します。
Google Domain であれば、`Google Domain` -> `DNS` -> `カスタムレコード`　-> `カスタムレコードを管理`から新規レコードの追加を行うことができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239544/4df1f1b7-48af-a994-061c-82202d04c4cb.png)

TTLはデフォルトの1時間に設定しました。

# GKEクラスタへのデプロイの流れ

```
GCP Artifact Registry の作成
            ↓
GCP クラスタの作成とNodeの作成
            ↓
Dockerfileのビルドと作成されたイメージをGCP Artifact Registryへ保存
            ↓
Deployment, Service など各k8sオブジェクトのデプロイ
            ↓
動作確認(+GCPクラスタの削除)
```


## GCP Artifact Registry の作成
`PROJECT_ID`にGCPのプロジェクトIDを指定して、GCP Artifact Registry を作成します。
Artifact RegistryはBuildして作成したDocker Imageの保存場所です。後に`Deployment`オブジェクトでArtifact Registry のイメージを指定します。
```
$ gcloud artifacts repositories create linebot-repo \
    --project={PROJECT_ID} \
    --repository-format=docker \
    --location=asia-northeast1 \
    --description="Docker repository"
```

## GCP クラスタの作成とNodeの作成

```
gcloud container clusters create linebot-gke --num-nodes 3 --zone asia-northeast1-a
```
ここでは`linebot-gke`というクラスタを作成しました。

```
$ kubectl get nodes
NAME                                         STATUS   ROLES    AGE   VERSION
gke-linebot-gke-xxxxxxxxxxxxxxxxxxxxxxxxxx   Ready    <none>   2d    v1.21.9-gke.1002
gke-linebot-gke-yyyyyyyyyyyyyyyyyyyyyyyyyy   Ready    <none>   2d    v1.21.9-gke.1002
gke-linebot-gke-zzzzzzzzzzzzzzzzzzzzzzzzzz   Ready    <none>   2d    v1.21.9-gke.1002
```

## Dockerfileのビルドと作成されたイメージをGCP Artifact Registryへ保存

### Dockerfile 
uwsgiを使ってFlaskアプリケーションをDockerコンテナ内で立ち上げます。コンテナのポートに5000番を使用します。
Nginx からは`http://app-server:5000`でFlaskアプリケーションにアクセスします。
後の`deployment`の`metadata`により、Kubernetes内部で`app-server`の名前解決を行い、Nginx<=>uwsgi(Flask)間の通信を行います。

#### app/Dockerfile
```Dockerfile
FROM python:3.9.7
WORKDIR /app
COPY . /app
RUN pip install --upgrade pip
RUN pip install -r requirements.txt
ENV FLASK_APP=app
ENV FLASK_DEBUG=1
EXPOSE 5000
CMD ["uwsgi","--ini","/app/uwsgi.ini"]
```
Nginxの初期状態の設定を`./default.conf`のファイル内に記述します。
`location /`の`location`ディレクティブによりhttp通信(Port80を経由した)の `/` 配下のすべてのアクセスに対してFlaskへリクエストを渡しています。

#### web/Dockerfile
```Dockerfile
FROM nginx:latest
COPY ./default.conf /etc/nginx/conf.d/
CMD ["nginx", "-g", "daemon off;", "-c", "/etc/nginx/nginx.conf"]
```

```web/default.conf
server {
    listen       80;
    location / {
        proxy_pass http://app-server:5000;
    }
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}

```

### ビルドとイメージのPush
次のコマンドでDockerfileのビルドとArtifact Registryへ保存します。ビルドはGCP上で行われます。
```
// Build image and deploy
$ pwd
/path/to/dir/linebot/
// Flask App image build and push
$ gcloud builds submit \
    --tag {PROJECT_ID}/linebot-repo/app-server:1.0.0 ./app/
// Nginx container image build and push
$ gcloud builds submit \
    --tag {PROJECT_ID}/linebot-repo/linebot-nginx:1.0.0 ./web/
```


## 各Kubernetes オブジェクトを作成

次に各アプリケーションのKubernetes オブジェクトを作成していきます。
今回使用したk8sのオブジェクトは以下のとおりです。

| Object                  | Explanation                                                 |
| --------------------- | ------------------------------------------------------- |
| Deployment            | Pod の各リソースの値を定義する                            |
| Service               | Kubernetes クラスタ内外のネットワークを定義する         |
| ClusterIP(Service)    | Service の種類の一つ。直接外部と通信を行わない                |
| NodePort(Service)     | Service の種類の一つ。直接外部と通信が可能                    |
| Ingress               | SSL 終端やロードバランシングの機能を持つオブジェクト        |
| Pod                   | Kubernetes 実行時の最小単位                             |
| ConfigMap             | 環境変数などのパラメータを保持する                      |
| Secret                | 環境変数などの機密性の高いパラメータを保持する          |
| ManagedCertificate    | SSL 証明書の管理を行う                                  |
| PersistentVolume      | Kubernetes クラスタ上に Volume マウントの割当を行う     |
| PersistentVolumeClaim | PersistentVolume の実際に必要とするリソースを記述するを |

また本システムにおいて各アプリケーションの`metadata`を以下としています。
- App(Flask): `app-server`
- Web(Nginx): `linebot-nginx`
- DB(Mysql): `mysql-db`

まずApp、Web、DBアプリのため`Deployment`と`Service`が必要です。
`Deployment`では各コンテナが実行される最小単位である`Pod`の状態を定義します。
`Service`は、その`Pod`がクラスタ外、またクラスタ内の**オブジェクトと通信するために必要なネットワーク設定を定義**します。

`ConfigMap`と`Secret`は、DBのユーザー名やパスワードを保持するために設置しました。`ConfigMap`は特に公開しても問題ないパラメータを保持しているのに対して、`Secret`では`機密度の高い情報を保持`しています。
また`PersistentVolume`と`PersistentVolumeClaim`を使って、Mysqlの永続化ボリュームをKubernetesクラスタ上へ割り当てを行います。
Kubernetesによって作成された`Pod`のデータは`Pod`が削除されると消えてしまうため、再起動のたびにDBデータが初期化されます。
`Pod`が削除された際にMysqlのデータが消えないように、`PersistentVolume`のオブジェクトを使ってデータの永続化を行います。
本格的に本番環境に対してDBサーバーを構築する場合は、[**Cloud SQL**](https://cloud.google.com/sql/docs/mysql?hl=ja)などのマネージドサービスを使うことが推奨されます。本システムではあくまで第一目的が学習であるため、上記のようなシステム構成としました。

`Ingress`の代わりに`Service`の`Loadbalancer`タイプを使用することで外部にアプリを公開することが可能ですが、
今回はSSL証明書の設定を管理するために設置しました。`Ingress`の中で`ManagedCertificate`のオブジェクトを参照します。


### [Deployment](https://kubernetes.io/ja/docs/concepts/workloads/controllers/deployment/)
> Deployment はPodとReplicaSetの宣言的なアップデート機能を提供します。Deploymentにおいて 理想的な状態を記述すると、Deploymentコントローラーは指定された頻度で現在の状態を理想的な状態に変更します。Deploymentを定義することによって、新しいReplicaSetを作成したり、既存のDeploymentを削除して新しいDeploymentで全てのリソースを適用できます。

`Deployment`により`Pod`のあるべき状態を定義します。仮に`Pod`が削除されたり、内部でエラーが発生した場合、Kubernetesは、`Deployment`の記述を元に`Pod`の再作成を行います。本システムにおいては`Nginx`、`Flask`、`Mysql`コンテナをそれぞれを持つ`Pod`の状態を記述するために使用しています。

`Deployment`は`Pod`の理想的な状態を定義する宣言的なオブジェクトであり、実態としてCPUやメモリリソースを消費するオブジェクトではありません。そのため図においては`Node`の外側から`Pod`に対して矢印を向けています。

```web/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: linebot-nginx
spec:
  selector:
    matchLabels:
      app: linebot-nginx
  replicas: 1
  template:
    metadata:
      labels:
        app: linebot-nginx
    spec:
      containers:
        - name: linebot-nginx
          image: asia-northeast1-docker.pkg.dev/gcp-compute-engine-343613/linebot-repo/linebot-nginx:1.0.0
          imagePullPolicy: Always
          ports:
            - containerPort: 80

```
```app/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-server
spec:
  selector:
    matchLabels:
      app: app-server
  replicas: 1
  template:
    metadata:
      labels:
        app: app-server
    spec:
      containers:
        - name: app-server
          image: asia-northeast1-docker.pkg.dev/gcp-compute-engine-343613/linebot-repo/app-server:1.0.0
          imagePullPolicy: Always
          ports:
            - containerPort: 5000

```
```db/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mysql-db
spec:
  selector:
    matchLabels:
      app: mysql-db
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: mysql-db
    spec:
      containers:
        - image: mysql:5.7
          name: mysql
          resources:
          env:
            - name: MYSQL_ROOT_PASSWORD
              value: password
            - name: MYSQL_DATABASE
              value: linebot
            - name: MYSQL_USER
              value: kota1110
            - name: MYSQL_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: pass-secret
                  key: password
          ports:
            - containerPort: 3306
              name: mysql
          volumeMounts:
            - name: mysql-persistent-storage
              mountPath: /var/lib/mysql
            - name: init-sql-configmap
              mountPath: /docker-entrypoint-initdb.d
      volumes:
        - name: mysql-persistent-storage
          persistentVolumeClaim:
            claimName: mysql-pv-claim
            readOnly: false
        - name: init-sql-configmap
          configMap:
            name: init-db-sql
            items:
              - key: init.sql
                path: init.sql

```


### [Service](https://kubernetes.io/ja/docs/concepts/services-networking/service/)
> Podの集合で実行されているアプリケーションをネットワークサービスとして公開する抽象的な方法です。
Kubernetesではなじみのないサービスディスカバリーのメカニズムを使用するためにユーザーがアプリケーションの修正をする必要はありません。 KubernetesはPodにそれぞれのIPアドレス割り振りや、Podのセットに対する単一のDNS名を提供したり、それらのPodのセットに対する負荷分散が可能です。

`Service`は`Pod`間の通信制御のパラメータを記述します。`Deployment`では`Pod`は外部との通信を行うことができません。`Service`で`Pod`間のネットワークを記述し外部から参照できる名前、もしくは、IPアドレス（`LoadBalancer`タイプの場合）を発行することで、Kubernetes外部と内部の間でのネットワークの橋渡しを行う役割を担っています。

`Service`はTypeにより識別する異なるネットワーク構成を提供します。
`ClusterIP`、`LoadBalancer`、`NodePort` が代表的です。
それぞれの違いは、[Link](https://cstoku.dev/posts/2018/k8sdojo-09/)の記事にとてもわかり易くまとめられています。
`ClusterIP`は、クラスタ外からアクセスすることができないIPアドレスを発行します。一方、`NodePort`と`LoadBalancer`の`Service`はクラスタ外からアクセスすることが可能です。`NodePort`と`LoadBalancer`の違いは、
各ノードに個別でアクセスを行う場合は`NodePort`を利用し、各ノードへの振り分けをロードバランサが行う場合は`LoadBalancer`を利用します。

本システムにおいては、Nginxを外部からのリクエストを捌くためのリバースプロキシとして外部に公開したいこと、また、AppコンテナとDBコンテナは外部に公開する必要がないことから
- Webコンテナ(`Nginx`) -> `NodePort`
- Appコンテナ、DBコンテナ-> `ClusterIP`
としました。


```app/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: app-server
  labels:
    app: app-server
spec:
  type: ClusterIP
  ports:
    - name: http-port
      protocol: TCP
      port: 5000
      targetPort: 5000
  selector:
    app: app-server

```

```web/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: linebot-nginx
  labels:
    app: linebot-nginx
spec:
  type: NodePort
  ports:
    - name: http-port
      protocol: TCP
      port: 80
      targetPort: 80
      nodePort: 30082
  selector:
    app: linebot-nginx

```


```db/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: mysql-db
spec:
  type: ClusterIP
  ports:
    - name: mysql
      port: 3306
      targetPort: 3306
      protocol: TCP
  selector:
    app: mysql-db

```
`web/service.yaml`のPortの意味するところは以下のとおりです。

```
port: 80 　　　　　　　　　　　　　　　　　　　　　　-> 外部からのアクセスを受け取るコンテナ内のアプリケーションのPort番号
targetPort: 80　　　　　　　　　　　　->　　転送先のPort番号
nodePort: 30082　　　　　　　　　　-> Podが割り当てられた外部からアクセスされるNodeのPort番号
```

また後ほど各オブジェクトをデプロイし`Pod`を立ち上げた後、
`linebot-nginx`の`Pod`に入り、`app-server`の`metadata`の名前解決ができることを確認できます。
```
$ kubectl get pod
NAME                      READY   STATUS    RESTARTS   AGE
app-server1-xxxxxxxxx     1/1     Running   0          23m
linebot-nginx-yyyyyyyyy   1/1     Running   0          23m
mysql-db-zzzzzzzzz        1/1     Running   0          165m
$ kubectl exec -it linebot-nginx-yyyyyyyyy -- bash
root@linebot-nginx-yyyyyyyyy:/# curl "http://app-server:5000"
hello world!
```
オブジェクトに名前を付与する似た概念に`Label`と`Selector`がありますがリソースをグループ化し、そのまとまったリソースを使う際に参照されるもののようです。ネットワークの名前解決の際には使われていないようでした。[参考](https://thinkit.co.jp/article/17535)


### [Ingress](https://kubernetes.io/ja/docs/concepts/services-networking/ingress/)
> クラスター内のServiceに対する外部からのアクセス(主にHTTP)を管理するAPIオブジェクトです。Ingressは負荷分散、SSL終端、名前ベースの仮想ホスティングの機能を提供します。

`Ingress`は、外部からのアクセスを内部`Service`に転送する役割を担っています。本システムではGKEのGoogleマネージドSSL証明書を使用する`ManagedCertificate`オブジェクトを作成し、SSL証明書を使用する機能を`Ingress`で参照することで、`mydomain.com` サーバーのSSL暗号化を実現しています。


```ingress/managed-cert-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: managed-cert-ingress
  annotations:
    kubernetes.io/ingress.global-static-ip-name: linebot-ingress　　　　　　　　　　　# 作成した静的アドレス名
    networking.gke.io/managed-certificates: managed-cert
    kubernetes.io/ingress.class: "gce"
spec:
  defaultBackend:
    service:
      name: linebot-nginx　　　　　　　# バックエンドのService名。ここでは　Webアプリケーションのlinebot-nginx　を指定する。
      port:
        number: 80

```


```managed-cert.yaml
apiVersion: networking.gke.io/v1
kind: ManagedCertificate
metadata:
  name: managed-cert
spec:
  domains:
    - mydomain.com           # 取得したドメイン名
```

### [Pod](https://kubernetes.io/ja/docs/concepts/workloads/pods/pod-overview/)
> `Pod` は、Kubernetesアプリケーションの基本的な実行単位です。これは、作成またはデプロイするKubernetesオブジェクトモデルの中で最小かつ最も単純な単位です。`Pod`は、クラスターで実行されているプロセスを表します。

各アプリケーションコンテナは`Pod`内で作成されます。
`Deployment`で記述されたリソースの分だけ、`Node`上に実際のリソースが割り当てられます。基本的には、1コンテナ1Podでデプロイします。サイドカーコンテナを使うことで1Podに2コンテナ以上デプロイすることも可能です。

今回、`Pod`の定義は`Deployment`オブジェクトで管理したため`Pod`自体のオブジェクトは作成しませんでした。



### [ConfigMap](https://kubernetes.io/ja/docs/concepts/configuration/configmap/)
> ConfigMapは、 機密性のないデータをキーと値のペアで保存するために使用されるAPIオブジェクトです。Podは、環境変数、コマンドライン引数、またはボリューム内の設定ファイルとしてConfigMapを使用できます。ConfigMapを使用すると、環境固有の設定をコンテナイメージから分離できるため、アプリケーションを簡単に移植できるようになります。

アプリケーション固有のパラメータを管理するのに役に立つオブジェクトが`ConfigMap`です。生テキストのままテキストを保持するので機密性の高いデータを保存するには適しませんが**オプションなどの設定値を保存できる**オブジェクトです。
本システムでは`Mysql`コンテナ立ち上げ時に実行したいSQLを参照するために使用しました。
`metadata: init-db-sql`にて`db/deployment.yaml`により参照されます。
([この記事](https://qiita.com/higakin/items/b562eac58714d6681fa6)が参考になりました。)

```db/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: init-db-sql
data:
  init.sql: |
    use linebot;
    CREATE TABLE if not exists `testTable`(
        `id` int(11),
        `name` varchar(30)
    ) ENGINE = InnoDB DEFAULT CHARSET = utf8;
    CREATE TABLE if not exists `itemTable`(
        `user_id` varchar(50),
        `item` varchar(50),
        `todo_flg` int(11),
        PRIMARY KEY (`user_id`, `item`)
    ) ENGINE = InnoDB DEFAULT CHARSET = utf8;
```

### [Secret](https://kubernetes.io/ja/docs/concepts/configuration/secret/)
> KubernetesのSecretはパスワード、OAuthトークン、SSHキーのような機密情報を保存し、管理できるようにします。 Secretに機密情報を保存することは、それらをPodの定義やコンテナイメージに直接記載するより、安全で柔軟です。 詳しくはSecretの設計文書を参照してください。Secretはパスワード、トークン、キーのような小容量の機密データを含むオブジェクトです。 他の方法としては、そのような情報はPodの定義やイメージに含めることができます。 ユーザーはSecretを作ることができ、またシステムが作るSecretもあります。

`Base64`などでエンコードを行いパスワードなどの機密情報を保持します。あくまで`Base64`でエンコードしたテキストを保持するだけで、復号すると元のテキストを参照できます。そのため、暗号化のためには[kubesec](https://github.com/shyiko/kubesec)など別の機能を連携する必要があります。
パスワードなどの機密性の高い設定値を生データで保持せず扱えることがメリットです。
本システムでは`Mysql`のパスワードを`Secret`で管理しました。


```db/secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: pass-secret
type: Opaque
data:
  password: XXXXXXXXXXXXXX  # Base64でエンコードしたMysql　Passwordの値
```

[永続化ボリューム(PersistentVolume,PersistentVolumeClaim](https://kubernetes.io/ja/docs/concepts/storage/persistent-volumes/)
> ストレージを管理することはインスタンスを管理することとは全くの別物です。`PersistentVolume`サブシステムは、ストレージが何から提供されているか、どのように消費されているかをユーザーと管理者から抽象化するAPIを提供します。これを実現するための`PersistentVolume`と`PersistentVolumeClaim`という2つの新しいAPIリソースを紹介します。

### PersistentVolume
> PersistentVolume (PV)はストレージクラスを使って管理者もしくは動的にプロビジョニングされるクラスターのストレージの一部です。これはNodeと同じようにクラスターリソースの一部です。PVはVolumeのようなボリュームプラグインですが、PVを使う個別のPodとは独立したライフサイクルを持っています。このAPIオブジェクトはNFS、iSCSIやクラウドプロバイダー固有のストレージシステムの実装の詳細を捕捉します。

### PersistentVolumeClaim
> PersistentVolumeClaim(PVC)はユーザーによって要求されるストレージです。これはPodと似ています。PodはNodeリソースを消費し、PVCはPVリソースを消費します。Podは特定レベルのCPUとメモリーリソースを要求することができます。クレームは特定のサイズやアクセスモード(例えば、ReadWriteOnce、ReadOnlyMany、ReadWriteManyにマウントできます。詳しくはアクセスモードを参照してください)を要求することができます。

`PersistentVolume`　は、Kubernetesクラスタ上に割り当てられるボリュームを定義します。Kubernetesクラスタが削除されると`PersistentVolume`も削除されますが、Podが再起動されてもVolume自体は残るので、`Pod`の再起動のたびにMysqlのデータが消えてしまうことはありません。

`PersistentVolumeClaim`で要求するリソースを定義します。`PersistentVolume`と合わせて使用する必要があるようです。
([この記事](https://qiita.com/witchy/items/3a39b674097b86a44546)が参考になりました)

```db/persistent-volume.yaml
kind: PersistentVolume
apiVersion: v1
metadata:
  name: mysql-pv-volume # PVの名前
  labels:
    type: local
spec:
  storageClassName: manual # PVCと一致させる必要がある
  capacity:
    storage: 5Gi
  accessModes:
    - ReadWriteMany # 複数Node読み書き可
  hostPath:
    path: /var/lib/data

---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mysql-pv-claim
spec:
  # storageClassName=manualのPVを探してマウントする
  storageClassName: manual
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 5Gi # PVが持っている容量のうち5GBを使用する

```

### 各k8sオブジェクトのデプロイ
```
# Build & Run containers
kubectl apply \
    -f app/deployment.yaml \
    -f app/service.yaml \
    -f web/deployment.yaml \
    -f web/service.yaml  \
    -f db/deployment.yaml  \
    -f db/service.yaml  \
    -f db/secret.yaml  \
    -f db/configmap.yaml \
    -f db/persistent-volume.yaml \
    -f ingress/managed-cert.yaml \
    -f ingress/managed-cert-ingress.yaml
```

## GCPクラスタの削除

```
# Make sure delete cluster not to be billed!
$ gcloud container clusters delete linebot-gke --zone asia-northeast1
```

# おわりに

GKEで稼働するコンテナベースの簡易なFlaskアプリケーションを作成しました。
TODO管理Botとしては、あくまで個人使用を目的として考えていますが、
GKE上で構築された3層構造のアプリケーションは、LineBot以外にも他展開ができるかと思います。
- k8s上にAPIサーバーとフロントエンドのアプリケーションを作成し、GKEベースのWebアプリケーションを作成する。
- TODO管理のみならず、他のAPIと連携したLineBot等

GKEでシステムを運用するにはサーバーレスアプリケーションに比べてKubernetesの学習コストと運用費用がかかります。
スケール前提の個人開発のアプリケーション、もしくは中規模以上のチームで運用するシステムにおいてはKubernetesを導入するメリットもあるかと。


次はKubernetesの最適なリソース管理の方法を模索したり、Terraformを利用したGCPリソースの管理等も試していきたいです。


# その他の参考文献
- [Kubernetes公式ドキュメント](https://kubernetes.io/ja/)
- [Kubernetes道場](https://cstoku.dev/tags/kubernetes/)(各k8sオブジェクトが非常にわかりやすくまとめられています。)

[^1]:docker-composeとKubernetesの記述方法を変換するツールとして、[Kompose](https://kubernetes.io/docs/tasks/configure-pod-container/translate-compose-kubernetes/)といった両者間のフォーマット変換ツールがあるようですが、細かい設定までは変換できなかったです。