Kubernetes上でアプリケーションデプロイをサクッと試してみる(コマンドあり)

<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-116132394-1"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());

  gtag('config', 'UA-116132394-1');
</script>
# 背景
最近アプリケーション実行環境としてコンテナ仮想環境は**当たり前**になりつつあると感じています。個人的にもサクッと新しい技術を(OSSなりツールなり)試したりするのにもローカルを汚さず素早く環境構築を行えるので、これがないと逆に効率が悪くなるなと思います。  
そんな風潮の中で社内でもKubernetesによるアプリケーション環境構築や運用保守がなされており、(コンテナ仮想化ソフトはもう当たり前として)社内外でk8sの存在感が絶大です。  
僕自身はアプリケーションエンジニア(フロントエンド/バックエンドどちらも見る)ではありますが、k8sの知見を増やしたいと思い「[Kubernetes実践入門](https://gihyo.jp/book/2019/978-4-297-10438-2)」という書籍を参考にローカルにk8sでアプリケーションデプロイまで構築してみました。目次は書籍の章立てとはあまり関係ないですが特に重要と思われる部分だけピックアップし順序だてていますので試しに触って動かしてみたい方には参考になるかなと思います。

# 目次
- [前提](#前提)
- [minikubeでクラスタを構築してみる](#minikubeでクラスタを構築してみる)
  - インストール方法
  - クラスタ構築
  - クラスタの停止、削除する
  - アドオンを管理する
- [k8s上にアプリケーションをデプロイしてみる](#k8s上にアプリケーションをデプロイしてみる)
  - 簡易的にデプロイする 
  - アプリケーションのマニフェストを作成してデプロイする

# 前提
- MacbookPro, MacOS Mojave(version=10.14.3)
- VirtualBox(version=5.2.20 r125813)
- Homebrew(version=2.1.0)
- k8sのオブジェクトについて概ね理解している。

k8sには内部構造を抽象化しk8s内で利用しやすくするために独自のオブジェクトの概念があります。僕自身は以前k8sについて調べたことがあったので(知見として頭に入れただけで使えるようにはなってませんでした)あえてそこには触れませんが、記事内では固有名詞で頻出しますのでわからない方は以下の記事などで先に理解しておくことをお勧めします。(まぁそれを理解するのがもっとも難しいのですが。。。)

【参考記事】

- [Kubernetes の Pod / ReplicaSet / Deployment について、ようやく整理できた （入門k8s 読書メモ）](https://blog.a-know.me/entry/2018/08/14/185324)
- [Kubernetesを始めよう！](https://inside.dmm.com/entry/2018/04/13/hello-kubernetes)
- [基本用語集](https://k8s.dkbk.jp/jp/docs/reference/glossary/?fundamental=true)

# minikubeでクラスタを構築してみる

ローカル環境でk8sを動かすために[minikube](https://github.com/kubernetes/minikube)を利用します。k8sの公式で提供しているので操作についてほとんど差異はないはずです。

## インストール方法

```
# minikubeをインストール
$ brew cask install minikube

# kubernetes-cliをインストール
$ brew install kubernetes-cli
```

## クラスタ構築
初回実行時はVMイメージのダウンロードから始まるので時間がかかるようです。
（実は、一度失敗してしまったので[こちらの記事](https://qiita.com/sotoiwa/items/18053c86ce7a47faca83)を参考に再実行しました。)

```
$ minikube start
😄  minikube v1.0.0 on darwin (amd64)
🤹  Downloading Kubernetes v1.14.0 images in the background ...
🔥  Creating virtualbox VM (CPUs=2, Memory=2048MB, Disk=20000MB) ...
💿  Downloading Minikube ISO ...
 142.88 MB / 142.88 MB [============================================] 100.00% 0s
📶  "minikube" IP address is 192.168.99.100
🐳  Configuring Docker as the container runtime ...
🐳  Version of container runtime is 18.06.2-ce
⌛  Waiting for image downloads to complete ...
✨  Preparing Kubernetes environment ...
💾  Downloading kubeadm v1.14.0
💾  Downloading kubelet v1.14.0
🚜  Pulling images required by Kubernetes v1.14.0 ...
🚀  Launching Kubernetes v1.14.0 using kubeadm ... 
⌛  Waiting for pods: apiserver proxy etcd scheduler controller dns
🔑  Configuring cluster permissions ...
🤔  Verifying component health .....
💗  kubectl is now configured to use "minikube"
🏄  Done! Thank you for using minikube!
```

クラスタの確認をしてみる

```
$ kubectl get nodes
NAME       STATUS   ROLES    AGE     VERSION
minikube   Ready    master   2m23s   v1.14.0
```

`minikube`という1台のNodeが稼働しています。

## クラスタの停止、削除する

minikubeはローカルでクラスタを実行しているため実行中ノードのリソースを占有するようです。よって利用しない場合はクラスを終了しておいた方がいいでしょう。
クラスタを停止して再度`minikube start`した際は、停止前の状態のクラスタが起動します。削除した場合は初期状態のクラスタが起動されますのでリセットしたい場合に利用できます。

```
# クラスタを停止する
$ minikube stop

# クラスタを削除する
$ minikube delete
```

## アドオンを管理する

minikubeにはよく使われる機能を拡張機能として利用できるアドオンがあります。

```
# アドオンの一覧を列挙する
$ minikube addons list
- addon-manager: enabled
- dashboard: disabled
- default-storageclass: enabled
- efk: disabled
- freshpod: disabled
- gvisor: disabled
- heapster: disabled
- ingress: disabled
- logviewer: disabled
- metrics-server: disabled
- nvidia-driver-installer: disabled
- nvidia-gpu-device-plugin: disabled
- registry: disabled
- registry-creds: disabled
- storage-provisioner: enabled
- storage-provisioner-gluster: disabled
```

デフォルトでいくつかのアドオンが有効になっています。アドオンを有効/無効にする場合は以下を実行します。

```
# アドオンを有効にする
$ minikube addons enable {addon-name}

# アドオンを無効にする
$ minikube addons disable {addon-name}
```

たとえば、クラスタのWEB管理画面である[**dashboard**](https://github.com/kubernetes/dashboard)は`minikube dashboard`を実行することでブラウザで表示されます。

**k8s dashboard**の画面
![k8s dashboardの画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/56388/32adfe59-456c-6376-5b6d-43b4c36dc1fa.png)

# k8s上にアプリケーションをデプロイしてみる

サンプルアプリには[**Mattermost**](https://mattermost.com/)というSlackライクなチャットアプリケーションを利用します。

## 簡易的にデプロイする 

ここからは、k8s上へのアプリデプロイを素早くやってみたい、デプロイされた後の動きをdashboardで確認してみたいと思い、簡易的なデプロイ手順を記載しています。本番環境では利用をおすすめしませんのでより本番に近い方法は [こちら](#アプリケーションのマニフェストを作成してデプロイする) に進んでください。

### アプリケーションをデプロイする - `kubectl create deployment`

k8上にアプリケーションをデプロイする簡単な方法は`kubectl create deployment`を実行することです。アプリケーションの識別名とコンテナイメージの指定は必須です。
`created`が返れば`Deployment`オブジェクトが作成され、デプロイが開始したことを意味します。

```
$ kubectl create deployment mattermost-preview --image k8spracticalguide/mattermost-preview:4.10.2
deployment.apps/mattermost-preview created
```

デプロイが開始されれば、`kubectl get deployment`を実行してDeploymentの状態を確認しましょう。何度か実行しているうちにAVAILABLEが1に変わると思います。

```
$ kubectl get deployment mattermost-preview
NAME                 READY   UP-TO-DATE   AVAILABLE   AGE
mattermost-preview   1/1     1            1           69s
```

### アプリケーションを公開する - `kubectl expose`

デプロイが完了しても、クラスタ外からはアクセスできません。クラスタ外からアクセスできるようにするには`kubectl expose`コマンドを`--type`オプションに`NodePort`を指定して実行する必要があります。
現段階では「Nodeportはクラスタ外からNodeへアクセスするためのポート」という理解で良いと思います。。。

```
$ kubectl expose --type NodePort --port 8065 deployment mattermost-preview
service/mattermost-preview exposed
```

以上でクラスタ外からアクセスできる状態になったので、ブラウザから確認してみます。ブラウザからアクセスするには、アプリケーションのアドレスとポート番号を調べ、ブラウザを開き・・という少々面倒ですので、それらを一括で実行してくれる`minikube service`コマンドを使います。

```
$ minikube service mattermost-preview
🎉  Opening kubernetes service default/mattermost-preview in default browser...
```

mattermostの画面
![mattermostの画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/56388/56b1d438-843f-efa1-5e0c-3243d6477133.png)

## アプリケーションのマニフェストを作成してデプロイする

上記では`kubectl create deployment`を利用して簡易的にDeploymentを作成しましたが、これではファイルとして履歴が残らないため、レビューしにくく、再利用ができず、問題発生時に原因究明しにくいなど本番環境で利用するには問題が起きそうです。
そのため本番利用を見据えては、Deploymentなどの宣言的設定をyamlやjsonなどで記述しバージョン管理を行うのが一般的かと思います。それらの設定ファイルをマニフェストと呼びます。

ここからアプリコンテナは、mattermostのお試し用(mattermost-preview)ではなく、実利用版(mattermost)を利用していきます。  

### Deploymentマニフェストを作成する

いきなり0から作るのは難しいため、`--dry-run`オプションで雛形を作成し、修正しながら作成していくこととします。  
※ `--dry-run`オプションは雛形作成のためのオプションではなく、実際にkubectrlがAPIを叩く前のリクエスト内容を取得するためのコマンドです。外部出力の`--output`オプションと併用して雛形作成を行います。

```
# mattermost(アプリ)の雛形を作成
$ kubectl create deployment mattermost --image k8spracticalguide/mattermost:4.10.2 -o yaml --dry-run > mattermost-deploy.yaml

# mattermost-db(db)の雛形を作成
$ kubectl create deployment mattermost-db --image k8spracticalguide/mysql:5.7.22 -o yaml --dry-run > mattermost-db-deploy.yaml
```

### DeploymetnマニュフェストのPodテンプレートに環境変数を追加する

mattermostのイメージはDB用の設定値を環境変数で定義できるようになっています。

これらはデプロイする環境や構成によって変更する可能性があるため、コンテナイメージに直接持つのではなく、Deploymentオブジェクト作成時に設定するようにしたいと思います。

#### ConfigMapを使って設定値を管理、読み込みする

複数のPodテンプレート内に共通の設定値を与える場合、ConfigMapというオブジェクトに設定値を集約して設定値のばらつきによる不具合を抑えることができます。

```
# ConfigMapのマニフェストを作成する
$ kubectl create cm common-env -o yaml --dry-run \
 --from-literal MYSQL_USER=mm_user \
 --from-literal MYSQL_PASSWORD=P@ssw0rd \
 --from-literal MYSQL_DATABASE=mattermost > cm.yaml
```

Deploymentの方は以下のように修正する。

```diff:mattermost-deploy.yaml
(1~18行目は略)
    spec:
      containers:
      - image: k8spracticalguide/mattermost:4.10.2
        name: mattermost
+       env:
+       - name: MM_USERNAME
+         valueFrom:            # <- valueではなくvalueFromを使う
+           configMapKeyRef:
+             name: common-env  # <- ConfigMap名を指定する
+             key: MYSQL_USER   # <- dataマップのキーを指定する
+       - name: MM_PASSWORD
+         valueFrom:
+           configMapKeyRef:
+             name: common-env
+             key: MYSQL_PASSWORD
+       - name: DB_NAME
+         valueFrom:
+           configMapKeyRef:
+             name: common-env
+             key: MYSQL_DATABASE
+       - name: DB_HOST
+         value: mattermost-db
        resources: {}
(略)
```

```diff:mattermost-db-deploy.yaml
(1~18行目は略)
    spec:
      containers:
      - image: k8spracticalguide/mysql:5.7.22
        name: mysql
+       env:
+       - name: MYSQL_ROOT_PASSWORD
+         value: rootpassword
+       envFrom:                # <- envではなくenvFromに置き換える
+       - configMapRef:
+           name: common-env    # <- ConfigMap名を指定する
        resources: {}
(略)
```

#### 別ファイルで設定値を管理、読み込みする

さらに、設定が膨大に増えると環境変数として渡すのではなく設定ファイルとしてまとめて設定値を渡す方が管理しやすくなることもあります。

```
# mattermostの設定ファイルをダウンロードしておく
$ curl -L -O https://raw.githubusercontent.com/kubernetes-practical-guide/examples/master/ch3.4.2.2/config.json

# ファイルからConfigMapのマニフェストを作成する
$ kubectl create cm mm-config-file -o yaml --dry-run \
 --from-file ./config.json > cm-file.yaml

# ダウンロードした設定ファイルは不要なので削除する
$ rm ./config.json
```

Deploymentの方は以下のように修正する。

```diff:mattermost-deploy.yaml
(1~38行目は略)
        - name: DB_HOST
          value: mattermost-db
        resources: {}
+       volumeMounts:
+       - name: cm-volume       # <- マウントするVolume名を指定(a)
+         mountPath: /mm/config # <- コンテナ内のマウントポイント
+     volumes:
+     - name: cm-volume         # <- (a)で指定するVolume名を定義
+       configMap:
+         name: mm-config-file
+         items:                # <- itemsを省略するとConfigMap内の全データがマウントされる
+         - key: config.json    # <- ファイルとしてマウントするデータのキーを指定
+           path: config.json   # <- コンテナ内でのファイル名を指定
(略)
```

### Serviceマニフェストを作成する

各PodはデプロイされるとIPアドレスが一時的に付与されますが、Podを上げ下げするたびにIPアドレスが変更され書き換えが必要になります。
その無駄を回避するため、PodのIPアドレスを仮想IPに束ね内部DNSで名前解決するServieオブジェクトが存在しています。

#### クラスタ内部のためのServiceを作成する

```
$ kubectl create service clusterip mattermost-db --tcp 3306 -o yaml --dry-run > mattermost-db-service.yaml
```

仮想IPに来たアクセスをどのPodへ振り分けするかはservie.yamlの`spec.selector`で指定します。

```mattermost-db-service.yaml
apiVersion: v1
kind: Service
metadata:
  creationTimestamp: null
  labels:
    app: mattermost-db
  name: mattermost-db
spec:
  ports:
  - name: "3306"
    port: 3306
    protocol: TCP
    targetPort: 3306
  selector:                 # <- リクエストの振り分け先を指定するlabelセレクタ
    app: mattermost-db      # <- DeploymentのPodテンプレートのlabelと合わせる必要がある
  type: ClusterIP
status:
  loadBalancer: {}
```

#### クラスタ外のための(公開用)Serviceを作成する

```
# mattermostのDeploymentにServiceを付与するので事前に立ち上げておく
$ kubectl apply -f .

# クラスタ外のための(公開用)Serviceを作成する
$ kubectl expose --type NodePort --port 8065 deploy mattermost -o yaml --dry-run > mattermost-service.yaml
```

※ NodePortを使えば手軽に外部公開できるようになりますが、直接Serviceを晒す事になったり、アクセスするNodeをどう選択するか問題になりがちです。いくらマニフェスト化して管理しやすくしてもまだ本番利用には不向きだと思いました。開発環境をサクッと構築したい時には便利です。
よって本番では、外部のロードバランサを使って公開するか、Ingressというクラスタ内部のロードバランサを使って公開する方法が有用ですが、今回は構築は実施しません。
（両者の違いについては[こちら](https://sff8.hatenablog.com/entry/2018/10/27/234757)が参考になりました。）

### マニフェストを適用する

上記の通り操作してくるとマニフェストファイル構成は以下のようになっているはず。

```
┌ cm-file.yaml  
├ cm.yaml  
├ mattermost-db-deploy.yaml  
├ mattermost-db-service.yaml  
├ mattermost-deploy.yaml  
└ mattermost-service.yaml  
```

マニフェストファイルを適用して各種k8sオブジェクトを作成する

```
$ kubectl apply -f .
```

ブラウザからアクセスできることを確認するためにNodeのIP、Nodeに割り当てられたポート番号を調べます。

```
# NodeのIPアドレスを調べる
$ minikube ip
192.168.99.100

# NodePortに割り当てられたのポート番号を調べる
$ kubectl get svc mattermost
NAME         TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
mattermost   NodePort   10.102.242.69   <none>        8065:31837/TCP   3m50s
```

PORT(s)のカラムをみると、２つのポート番号が表示されていますが、`{アプリが待ち受けしているポート}:{Nodeに割り当てられたポート}`を表しています。  

よって`http://192.168.99.100:31837`でブラウザからアクセスできるはずです。

今回は以上です。  

サクッと利用してみたいのであればk8sのマネージドサービスも一般的かもしれませんが、利便性の反面、内部の仕組みが隠蔽されて仕組みを理解する題材には向いていないかなと思いましてローカルにminikubeでk8s環境を立ててみました。今回の内容はまだまだ入り口かとは思いますが、実際にアプリケーションをデプロイすることを実践してみてk8sの足がかりになったかなと思います。