「Kubernetes実践入門」でKubernetesの知識をキャッチアップされる方へ

先日、書籍「Kubernetes実践入門」でKubernetesの門を叩きました。

ステップバイステップで本格的な構成に近づけていく流れとなっており、個人的にはこの書籍でかなりkubernetesのことを理解できました。

具体的には、簡単なKubernetesクラスタの構築から始まり、セキュリティの設定やFluentd/Elastcsearch/Kibanaでのログ集約環境の構築、Prometheus/Grafanaでのメトリクス監視といったところまで学べます。

高額な研修などを受講しなくても、この書籍で学習すれば、右も左も分からないという状態からは脱して、現場で「もがける」状態になれる。そんな感覚を持ちました。

ただ、中にはスムーズにいかないところもありました。書籍「Kubernetes実践入門」でKubernetesに入門される方に、私と同じような苦しみを回避していただきたいと思い、この記事を書いてみました。

なお、本記事にはmac（macOS Catalina）でKubernetes(minikube)を動作させる場合の問題についてのみ記載されています。他のOSでは試しておりません。

私の個人的なメモも含まれますが、参考になればと思います。

# 全体を通して

### 書籍のソースコード
https://github.com/kubernetes-practical-guide/examples

### トラブルシューティングのためにPodの状態を確認する方法

書籍に書いてある通りにやっているはずなのに、うまく動かない・・・ということが何度もあると思います。そんな場合に参考にしてください。

以下のコマンドでは、各Podの定義情報に加え、コンテナ生成などのイベント履歴を確認できます。イベント履歴にエラーの状況や原因が記録されていることがあります。

```
$ kubectl describe pods
```

Pod上（コンテナ上）のアプリのログを確認するには、

```
$ kubectl get pod
```
でPodのコンテナ名を確認してから、

```
$ kubectl logs <Podのコンテナ名>
```
で、Podのログを確認しましょう。

この時、以下のような仕組みで、ログがコマンドラインに出力されます。

アプリが標準出力/標準エラーにログを出力する。
↓
このログを、Dockerランタイムがノードにログファイルとして保存する。
↓
`kubectl logs <Podのコンテナ名>`を実行すると、Kubernetes APIサーバーが対象ノードのkubeletを呼び出す。
↓
ノードのkubletがログファイルを読み出し、Kubernetes APIサーバーにログファイルの内容を返す。
↓
コマンドラインにログファイルの内容が出力される。


Podのコンテナにログインして、コンテナ内部でコマンドを実行したい場合は、以下のとおりです。

```
$ kubectl exec -it <Podのコンテナ名> /bin/bash
```

Podが起動するのを待つときは、以下のコマンドで状況をウォッチすると良いです。

```
$ kubectl get pod -w
```

### うまくいかないときは・・・
うまくいかないときは、原因の切り分けが難しくなるので、関連するオブジェクトを削除して作り直した方が良いです。

# 第１章 Hello Kubernetes world! コンテナオーケストレーションとKubernetes

特に困ることはありませんでした。とっても分かりやすいです。

# 第２章 Kubernetesを構築する

私のmac(macOS Catalina)では、kubectlとminikubeのインストールで少し手間取りました。

書籍ではHomebrewを使った手順が紹介されていますが、実際にはエラーが発生して使えませんでした。エラー内容は理由は省略しますが、結論としては解決は無理そうでした。

そこで、Kubernetes公式サイトの手順どおりインストールしました。具体的には以下の通りです。

### kubectrlのインストール手順

書籍で指定されているバージョン（v1.11.3）をインストールする場合、手順は以下の通りです。

```
$ curl -LO https://storage.googleapis.com/kubernetes-release/release/v1.11.3/bin/darwin/amd64/kubectl

$ chmod +x ./kubectl

$ sudo mv ./kubectl /usr/local/bin/kubectl

$ kubectl version --client
```

公式サイト：https://kubernetes.io/docs/tasks/tools/install-kubectl/#install-kubectl-on-macos


### minikubeのインストール手順

書籍で指定されているバージョン（v0.28.2）を取得する場合、VirtualBoxをインストールした後、以下の手順を実施します。

```
$ curl -Lo minikube https://storage.googleapis.com/minikube/releases/v0.28.2/minikube-darwin-amd64

$ chmod +x minikube

$ sudo mkdir -p /usr/local/bin/

$ sudo install minikube /usr/local/bin/

$ minikube version
```

公式サイト：https://kubernetes.io/docs/tasks/tools/install-kubectl/#install-kubectl-on-macos

# 第３章 Kubernetes上にアプリケーションをデプロイする

この書籍のメインとなる章です。この章だけで全ページの25%くらいを占めており、ものすごく長いです。

特に分かりづらかったところについて、私の理解を記載させていただきます。

## 「3.5 クラスタ内のアプリケーション間で通信する」

### Headless Service

何と言うか、直感的に理解しづらかったのがHeadless Serviceという概念でした。

普通のServiceはClusetIPというIPアドレスを持っていて、外部からClusterIPにアクセスされると、Serviceは配下のPod達に処理を振り分けます（ロードバランシング）。

一方、Headless Serviceは、ClusterIPを持たず、外部からDNSリクエストされると、配下のPod達のIPアドレスの一覧を呼び出し元に返します。

参考：https://cstoku.dev/posts/2018/k8sdojo-09/

「Pod名.Service名」というホスト名と、そのPodのIPアドレスがDNSに登録されます。

つまり、普通のServiceは「頭(head)」として配下のPod達にロードバランシングするのですが、Headless Serviceには、そういう「頭(head)」が無い、ということだと理解しました。

Headless Serviceの呼び出し元が、Headless Service配下の特定のPodにリクエストを送りたい場合に使われるようです。たとえばMySQLをMaster/Slave構成で構築する場合には、どのPodがMasterで、どのPodがSlaveかを呼び出し元が意識する必要があります。書籍では、Headless Serviceのこのような特徴を利用して、MySQLのMaste/Slave構成を構築しています（StatefulSetはHeadless Serviceを使って実現される、と解釈しました）。

## 「3.6 アプリケーションを外部に公開する」

### NodePort

私は、書籍の説明だけでは「NodePort」なるものの概念を理解できませんでした。

簡単に言うと、特定のポート番号(これがNodePort)に送られてきた外部リクエストを、そのポート番号に紐づけられたNodePortサービスにkubernetes内部で転送する、というものらしいです。

具体的に言いますと、外部からのリクエストは以下の経路を辿ります。

* クライアントがHTTPリクエストを送信

　↓（送信）

* kubernetesクラスターのIPアドレス：NodePortのポート番号（NodePortサービスのyaml項目：`nodePort`） でリクエストを受ける。

　↓(転送)

* NodePortサービスのIPアドレス:同サービスのポート番号（NodePortサービスのyaml項目：`port`）

　↓(転送)

* NodePortサービスが管理するPodのIPアドレス:同Podのポート番号（NodePortサービスのyaml項目：`targetPort`）

Podで動作するアプリを、かなりお手軽に外部に公開できるそうですね。しっかりとしたセキュリティが要求される場合には適さないと思いますが、ライトな用途であれば良さそうです。

### Ingress

Ingressについても書籍の説明だけではよく分かりませんでした・・・。

IngressはServiceの一種ではありません（yamlで`kind:Ingress`と指定します）。
Serviceの前に配置される、クラスター内部のロードバランサーです。

こちら（15ページ目）の図が直感的には分かりやすいと思います。
https://www.slideshare.net/nobu0001/kubernetes-119605097

Ingressは以下の機能を持ちます。

* ルーティング
アクセスされるホスト名と、転送先のServiceの組み合わせを定義しておきます。その定義に従って、特定のホスト名に送られたリクエストを、特定のServiceに転送します。これにより、ルーティングを実現します。

* 負荷分散
複数ノードに存在するServiceに、リクエストを振り分けます。これにより、負荷分散を実現します。

### 外部ロードバランサー

NodePortやIngressとは別に、外部ロードバランサーという概念があります。（Ingressも外部に置けるらしいですが・・）

こちら（14ページ目）の図が直感的には分かりやすいと思います。
https://www.slideshare.net/nobu0001/kubernetes-119605097

ただ、Ingressや外部ロードバランサーも、NodePortの仕組みを使っているように見えますが、どうなんでしょうか。また、分かったときに追記したいと思います。

### 「3.7.3 ストレージの準備」でのNFSサーバー構築手順
macOSにバンドルされているNFSサーバーを利用する手順があるのですが、macOS Catalinaではうまくいきませんでした。

書籍には以下の手順が記載されています。

```
$ sudo mkdir /share
$ sudo chmod 777 /share
$ minikube ip
192.168.99.100
$ sudo vi /etc/exports
/share -mapall=nobody:wheel -network 192.168.99.0 -mask 255.255.255.0
```

しかし、このまま実行しようとすると、以下のとおりうまくいきませんでした。

```
$ sudo mkdir /share
mkdir: /share: Read-only file system
```

macOS Catalinaからファイルシステムが変更されたため、こうなってしまうようです。

※詳しくは以下をご覧ください。大変参考になります。
　https://applech2.com/archives/20190610-read-only-system-volume-apfs-refresh.html

そこで、NFSとして共有するディレクトリを、Catalinaのファイルシステムのルールに従って設ける必要があります。具体的には以下の２点です。

##### ポイント１
NFSの共有ディレクトリを、ホームディレクトリの直下あたりにつくりましょう(例：`~/nfs_share`)。

`~/Documents`などの配下に作ってしまうと、ルール違反となってしまうようです。

参考：https://www.firehydrant.io/blog/nfs-with-docker-on-macos-catalina/

その結果、Podの`mysql-1`でinit-slave.shが実行される時に、`/mnt/backup`にDBダンプを出力しようとするのですが、`Permission denied`となり、Pod`mysql-1`がスレーブとして正常に起動しません。

一通り構築した後の動作確認で悲しいことになりますので、ご注意ください。

##### ポイント２
macOS側の`/etc/exports`と、kubernetes側の`mysql-pv.yaml`にはNFS共有ディレクトリのパスを指定しますが、先頭に`/System/Volumes/Data/`を付けましょう。

例：`/System/Volumes/Data/Users/your_user_name/nfs_share`

上記に従って書籍を読み替えますと、以下のようになります。

```:macOSのターミナル
$ mkdir ~/nfs_share
$ chmod 777 ~/nfs_share
$ minikube ip
192.168.99.100
$ sudo vi /etc/exports
/System/Volumes/Data/Users/your_user_name/nfs_share -mapall=nobody:wheel -network 192.168.99.0 -mask 255.255.255.0
```

```yaml:mysql-pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: backup
spec:
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteMany
  persistentVolumeReclaimPolicy: Retain
  storageClassName: nfs
  mountOptions:
    - hard
  nfs:
    # ★★★こちらです！★★★
    path: /System/Volumes/Data/Users/your_user_name/nfs_share
    server: 192.168.99.1
```

### nfsのPVC（PersistentVolumeClaim）を削除する方法

nfsのPVC（PersistentVolumeClaim）を誤って作成してしまい、削除したい場合は、Protectionを外す必要があります。以下の記事が大変参考になりました。
https://qiita.com/devs_hd/items/8cbf834c504e57fbe1ff

### StatefulSetのボリュームの削除

Master/Slave構成のMySQLを構築するときに、うまくいかなくてやり直そうとしました。その際、MySQLのデータファイルを保存するボリュームを削除できず、困りました。（テーブルを再度作ろうとすると、ERROR 1050 (42S01) at line 1: Table 'test' already exists　と出てしまいました。）

具体的には、単に`kubectl delete -f mysql-sts.yaml`としてStatefulSetを削除しただけでは、Podが削除されるだけで、マウントされているボリュームは削除されませんでした。

そこで、

```
$ kubectl get pvc

# 上記コマンドで調べたpvc名を使って、pvcを削除する。
$ kubectl delete pvc {pvc名}
```

とすると、削除されました。PersistentVolumeClaim(pvc)を削除すると、それに紐付くPersistentVolume(pv)も併せて削除されました。つまり、`kubectl delete pv {pv名}`は不要でした。

参考：https://kubernetes.io/ja/docs/tasks/run-application/delete-stateful-set/


# 第８章 アプリケーションを運用する

### EFKスタックの構築（Elasticsearch, Fluentd, Kibana）

結論から言いますと、物理メモリ8Gの私のmacでは、efkは動きませんでした・・・。


# おまけ（書籍に登場するコマンドたち）

私の復習用です。簡単なコマンドは省略していますので100%ではないですが、書籍中の80%くらいのコマンドについては記載されているはずです。

長すぎてタイプミスしやすいものもありますので、コピペ用途などで使ってください。

### 第３章 Kubernetes上にアプリケーションをデプロイする

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

$ kubectl expose --type NodePort --port 8065 deployment mattermost-preview

$ minikube service mattermost-preview

$ kubectl get event -w -o custom-columns=KIND:.involvedObject.kind,NAME:.metadata.name,SOURCE:.source.component,REASON:.reason,MESSAGE:.message

$ kubectl create deployment dive-mattermost-preview --image k8spracticalguide/mattermost-preview:4.10.2 --v=8

$ kubectl get deploy,rs,pod -v=6 2>&1 | grep -e dive-mattermost -e https

$ kubectl get po -w | grep -e dive-mattermost -e NAME

$ kubectl scale rs $(kubectl get rs|grep dive-mattermost|awk '{print $1}') --replicas=1 -v=6 2>&1 | grep -e mattermost -e https

$ kubectl delete po --all -v=6 2>&1 | grep DELETE

$ kubectl get rs $(kubectl get rs|grep dive-mattermost|awk '{print $1}') -o template --template='{{.spec.selector.matchLabels}}'

$ kubectl get po $(kubectl get po|grep dive-mattermost|head -n 1|awk '{print $1}') -o template --template='{{.metadata.labels}}'

$ kubectl edit po $(kubectl get po|grep dive-mattermost|head -n 1|awk '{print $1}')

$ kubectl get po {直前のコマンドの実行結果に表示される、PodのID} -o template --template='{{.metadata.labels}}'

$ kubectl get po | grep -e dive-mattermost -e NAME

$ kubectl edit po {直前のコマンドの実行結果に表示される、PodのID}

$ kubectl get rs dive-mattermost-preview-7785c477c9 -o template --template='{{.metadata.ownerReferences}}'

$ kubectl delete deploy dive-mattermost-preview --cascade=false -v=8

$ kubectl create deploy mattermost --image nyandora/mattermost:4.10.2 -o yaml --dry-run > mattermost-deploy.yaml

$ kubectl create deploy db --image k8spracticalguide/mysql:5.7.22 -o yaml --dry-run > db-deploy.yaml

$ kubectl create cm common-env -o yaml --dry-run --from-literal MYSQL_USER=myuser --from-literal MYSQL_PASSWORD=mypassword --from-literal MYSQL_DATABASE=mattermost > cm.yaml

$ curl -L -O https://raw.githubusercontent.com/kubernetes-practical-guide/examples/master/ch3.4.2.2/config.json

$ kubectl create secret generic common-env -o yaml --dry-run --from-literal MYSQL_ROOT_PASSWORD=rootpassword --from-literal MYSQL_PASSWORD=mypassword > secret.yaml

# applyはcreateと違い、初回作成だけでなく変更にも使える。
$ kubectl apply -f .

$ kubectl logs $(kubectl get po | grep mattermost | awk '{print $1}')

$ kubectl get po -o wide

$ kubectl run test1 -i --rm --image k8spracticalguide/busybox:1.28 --restart=Never -- ping -c 1 172.17.0.7

# Labelセレクタで、app=dbに該当するPodの情報を取得。
$ kubectl get po -l app=db

$ kubectl apply -f db-service.yaml 

$ kubectl get svc,ep mattermost-db

$ kubectl run -i --rm test2 --image=k8spracticalguide/busybox:1.28 --restart=Never -- nslookup mattermost-db

$ kubectl create svc externalname ext-mattermost-db --external-name example.com

$ kubectl get svc,ep ext-mattermost-db -o wide

$ kubectl run -i --rm test4 --image=k8spracticalguide/busybox:1.28 --restart=Never -- nslookup headless-test

$ kubectl expose --type NodePort --port 8065 deploy mattermost --dry-run -o yaml > mattermost-service.yaml

$ kubectl apply -f mattermost-service.yaml

$ kubectl get svc mattermost -o wide

$ minikube ip

$ curl http://$(minikube ip):$(kubectl get svc mattermost -o jsonpath="{.spec.ports[0].nodePort}")

$ minikube addons enable ingress

$ kubectl get deploy -n kube-system -w

$ kubectl apply -f mattermost-ingress.yaml 

$ curl http://chat.$(minikube ip).nip.io

$ kubectl run -ti --image k8spracticalguide/busybox:1.28 dns-test --restart=Never --rm /bin/sh

$ kubectl run mysql-client --image=k8spracticalguide/mysql:5.7.22 -i --rm --restart=Never -- \
mysql -h mysql-0.mysql --user=root --password=rootpassword <<EOF
CREATE TABLE mattermost.test (msg VARCHAR(64));
INSERT INTO mattermost.test VALUES ('hello');
EOF

$ kubectl run mysql-loop --image=k8spracticalguide/mysql:5.7.22 -ti --rm --restart=Never -- \
/bin/bash -ic "while sleep 1; do mysql -h mysql-read --user=root --password=rootpassword \
-e 'SELECT @@server_id, msg from mattermost.test'; done"

```

### 第５章 アプリケーションを更新する

```
$ kubectl create serviceaccount my-service
$ kubectl get sa
$ kubectl run -it --rm --restart=Never --serviceaccount "my-service" --image k8s.gcr.io/hyperkube-amd64:v1.13.3 kubectl

sh-4.4# env | grep KUBERNETES_SERVICE
sh-4.4# ls /var/run/secrets/kubernetes.io/serviceaccount/

$ kubectl create rolebinding my-service-edit --clusterrole edit --serviceaccount default:my-service

$ kubectl get rolebinding -o wide

sh-4.4# kubectl run mynginx --image k8spracticalguide/nginx:1.15.5
sh-4.4# kubectl delete deploy mynginx
sh-4.4# kubectl get pods -n kube-system

$ kubectl delete rolebinding my-service-edit

$ kubectl create clusterrolebinding my-service-view --clusterrole view --serviceaccount default:my-service

sh-4.4# kubectl get pods -n kube-system
sh-4.4# kubectl get deploy,pod,svc --all-namespaces
sh-4.4# kubectl get secret

$ kubectl create rolebinding my-service-edit --clusterrole edit --serviceaccount default:my-service --dry-run -o yaml

$ kubectl get sa my-service -o yaml
〜〜略〜〜
secrets:
- name: my-service-token-6q87b
$ kubectl get secret my-service-token-6q87b -o yaml

```

### 第６章 アプリケーションの安定性をあげる

```
$ kubectl get pod -l app=mattermost -o wide -w &
$ kubectl get endpoints -l app=mattermost -w &
$ kubectl apply -f mattermost-deploy.yaml

$ kubectl get pod -l app=mysql -o wide -w &
$ kubectl get endpoints -l app=mysql -w &
$ kubectl apply -f mysql-sts.yaml 

$ kubectl scale deployment mattermost --replicas 3
$ kubectl get po -l app=mattermost

$ minikube addons enable metrics-server

$ kubectl get pod -n kube-system -l k8s-app=metrics-server

$ kubectl run cpu-max --image=k8spracticalguide/busybox:1.28 --requests=cpu=50m --limits=cpu=100m -- dd if=/dev/zero of=/dev/null

$ kubectl autoscale deployment cpu-max --cpu-percent=70 --min=1 --max=10

$ kubectl get horizontalpodautoscaler
$ kubectl describe hpa cpu-max

```

### 第７章 アプリケーションのセキュリティを強化する

```
$ minikube delete
$ minikube start --extra-config=kubelet.network-plugin=cni --kubernetes-version=v1.11.3
$ kubectl apply -f "https://cloud.weave.works/k8s/net?k8s-version=$(kubectl version | base64 | tr -d '\n')"

$ kubectl get deployment -n kube-system
$ sudo kubectl port-forward -n kube-system deployment/nginx-ingress-controller 80

$ kubectl run -it --rm --image="k8spracticalguide/busybox:1.28" --restart=Never test -- wget -T5 "http://mattermost:8065"

$ kubectl label namespace kube-system system=true

```

### 第８章 アプリケーションを運用する

```
$ minikube ssh
$ sudo find /var/log/pods | grep mattermost
```

##### efk

```
$ minikube delete
$ minikube start --kubernetes-version=v1.11.3 --memory 5012
$ kubectl apply -f .
$ minikube addons enable efk
$ kubectl get po -n kube-system -w
$ kubectl logs -n kube-system kibana-logging-8ldj4
$ kubectl describe pod -n kube-system kibana-logging-6v9st
```

##### Prometheus

```
$ minikube addons enable metrics-server
$ kubectl top node
$ kubectl top pod

$ minikube start --kubernetes-version=v1.11.3 --extra-config=kubelet.authentication-token-webhook=true

$ minikube service prometheus

$ kubectl exec -it mysql-0 /bin/bash
# mysql -u root -p
mysql> CREATE USER 'exporter'@'%' IDENTIFIED BY 'exporterpassword' WITH MAX_USER_CONNECTIONS 3;
mysql> GRANT PROCESS, REPLICATION CLIENT, SELECT ON *.* TO 'exporter'@'%';

$ echo -n "exporter:exporterpassword@(mysql.default.svc.cluster.local:3306)/" | base64
```

##### Grafana

PromQL
`sum(mysql_info_schema_table_size{schema="mattermost"}) by (table)`