Kubernetesに触れる

# Kubernetesとは？

Kubernetesは、[コンテナ化したアプリケーションのデプロイ、スケーリング、および管理を行うための、オープンソースのコンテナオーケストレーションシステムである](https://ja.wikipedia.org/wiki/Kubernetes)(Wikipediaからの引用)。

Kubernetesが何をもたらしてくれるのかは、書籍[Cloud Native DevOps with Kubernetes](https://www.oreilly.com/library/view/cloud-native-devops/9781492040750/)にある文章が端的であった。

* https://www.oreilly.com/library/view/cloud-native-devops/9781492040750/ch01.html (いずれアクセスできなくなるかも)

Kubernetesのコミュニティの"伝説"であるGoogleのKelsey Hightower氏によれば、

> Kubernetes does the things that the very best system administrator would do: automation, failover, centralized logging, monitoring. 
> Kubernetesは、自動化やフェイルオーバー、ログの中央管理、モニタリングのような、システム管理者の業務に対してベストなシステムを提供してくれる。

これに加えて、Kubernetesがもたらす価値は何か？という問いに対しては、

> Many of the traditional sysadmin tasks like upgrading servers, installing security patches, configuring networks, running backups, and so on, are less of a concern in the cloud native world. Kubernetes can automate these things for you so that your team can concentrate on doing its core work.
> サーバーをアップグレードしたり、セキュリティパッチのインストール、ネットワーク設定、バックアップを走らせる、などのシステム管理者の伝統的なタスクは、クラウドネイティブの世界では大して心配事ではなくなる。Kubernetesはこれらを自動化してくれるので、君たちはコアな仕事に集中できるのだ。

# Kubernetesの学習について

## 学習の困難さ

上記でKubernetesとは？の解答を引用したけれど、わかったようでわかった気にならないのではと思う。私はそうでした。

Kubernetesは巨大で、できることが多い。またKubernetesを説明するために必要な固有名詞も多い。Kubernetesを利用する開発者とKubernetesを運用する人によって語り口も異なる。Kubernetesは複数のコンポーネントが組み合わさったものを指すので、以下のようにアーキテクチャ自体も複雑である。

https://kubernetes.io/docs/concepts/architecture/cloud-controller/#design
![image.png](https://qiita-image-store.s3.amazonaws.com/0/285861/a47c07a0-0d73-d051-bf2b-f01c72daae8e.png)


こうしたことから、「Kubernetes とは」でググって1時間ほど粘ったけれど、全体像が見えず、なんともわからない、といった経験をする。

Kubernetesの学習は、ひとつのフレームワークを学習する感覚に似ている。「Springとは？」と聞かれて答えに詰まってしまうように、「Kubernetesとは？」と聞かれて一言で説明できないかんじ。

さらに学習を困難にさせるのが、Kubernetes周辺の話題である。Kubernetes単体を導入するということはほぼ無く、例えばKubernetesのパッケージマネージャであるHelmや、CI/CDを提供するConcourseやJenkins、ログ収集基盤のDatadogやELK、モニタリング基盤を提供するPrometheusなど、これらすべてを同時に考慮することになる。また設計や開発に関しては、microserviceといったものや、DevOpsという文脈からはBlue/Greenデプロイ、カナリヤリリース、AgileやScrumにも言及される。「Kubernetesを学習しよう」という言葉には、こうしたKubernetes以外のものにも言及することが暗に含まれている。

そんな中で、これからKubernetesについて数回に分けて説明を試みるけれど、全てに触れることはない。個人的に"主要な概念"と思うことを書いていく。

ではどのように学習を進めるか。フレームワークの学習、という比喩に沿って言えば、Kubernetesの学習には、

* 実行環境を準備して、
* とりあえずチュートリアルに沿って動かしてみる、

また、

* Kubernetesを説明している書籍を通読する、

というのが王道だろうか。

## k8sの実行環境

Kubernetesを手元のサーバー環境で構築する手段は複数があるが、その一つが[kubeadm](https://kubernetes.io/docs/setup/independent/install-kubeadm/)である。このセットアップ手順に従えば割と簡単に構築ができる。環境としては、少なくともサーバーが2台あればよい。一台は**Kubernetes Master**という役割のサーバーで、ここでKubernetes上で動かすContainerの操作などを行う。もう一台は**Kubernetes Node**という役割のサーバーで、ここで実際にContainerが動く。

よりお手軽なKubernetesの実行環境として、まず [minikube](https://kubernetes.io/docs/tasks/tools/install-minikube/) が考えられる。minikubeとはローカルのお試しKubernetes環境である。WindowsでもVirtualBoxがあれば動くようになっている(らしい。いれたことがないのでわからない)。

ブラウザですぐにKubernetesを触りたければ、[Kubernetesのplayground](https://labs.play-with-k8s.com/)がある。[こちらの記事](https://dev.classmethod.jp/etc/kubernetes-playground/)が非常にわかりやすく、これに従えば簡単にKubernetesで遊べる。（実際kubeadmを使って構築するのとそんなに変わらない手順）

自分の好き勝手できる環境でありかつ手軽に、ということならば、本家GoogleのKubernetes Engineを契約して使うのがよい。多少お金は必要だが、1か月1000円くらいあれば楽しめる(使い方によるので真に受けない)。-> [GKE](https://cloud.google.com/kubernetes-engine/?hl=ja)

## k8sのチュートリアル

チュートリアルは沢山ある。Kubernetesの公式サイトにもTutorialサイトをまとめたページがある。-> https://kubernetes.io/docs/tutorials/

Udemyにもめちゃくちゃコースが並んでいるし([link](https://www.udemy.com/topic/kubernetes/?locale=ja_JP))、Youtubeにもたくさん動画が転がっている。これにそって上で準備した実行環境で試すと学習が進むかと思う。

## k8sの書籍

書籍も沢山あるが、少し古いのだと既に使われていない名称なんかもあって混乱のもとになるので、なるべく最新のものを選ぶのがよい。一番は公式ドキュメントを流し読みすることかと思う -> https://kubernetes.io/docs/concepts/

# Pod

ここから具体的なKubernetesで何ができるのかの説明に入る。

まず**Pod**という言葉が意味するところを理解する。-> https://kubernetes.io/docs/concepts/workloads/pods/pod-overview/

PodはContainerの集まりを意味し、ContainerをマネジメントするKubernetesにとっては最も基礎的な概念となる。

「Kubernetes上でContainerを動かす」行為は「Podをデプロイする」という風に解釈される。

まず実際にnginxのContainerをKubernetes上で動かしてみよう。

Kubernetes Masterサーバーにログインし、カレントディレクトリに以下のような**Manifestファイル**と呼ばれるyamlファイルを作成する。今回の環境では`node1`というホストネームのサーバーがMasterになっている。

```yaml:mynginx-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: mynginx-pod
  labels:
    app: mynginx
spec:
  containers:
  - name: mynginx-container
    image: nginx:1.12.0
```

yamlの中身はさておき、このファイルは「NginxのVersion 1.12.0のContainerを(一つだけ)デプロイする」ということを主張している。

このファイルに書かれた内容をKubernetes上に反映するには、以下のようなコマンドを実行する。

```
[node1 ~]$ kubectl apply -f mynginx-pod.yaml
pod/mynginx-pod created
```

すると、Podがデプロイされる。

```
[node1 ~]$ kubectl get pods  -o wide
NAME          READY     STATUS    RESTARTS   AGE       IP          NODE      NOMINATED NODE
mynginx-pod   1/1       Running   0          1m        10.46.0.1   node3     <none>
```

`NODE`のカラムに注目したい。ここでは`node3`と呼ばれるノードでこのPodが動いている。この環境ではKubernetes Nodeは`node2`と`node3`という2台のサーバーからなる。今回は、指定したPodをどのノードで動かすかはMasterが決めている。が、もちろんノードのスペックに依存して動かしたいPodもあるであろう。例えば、サーバーのシステムログを収集するContainerはサーバーに1台ずつ配置したい。今回は例示しないがそういうこともできる。

`docker exec`のように、Containerの中にログインしてNginxのバージョンを確かめてみる。

```
[node1 ~]$ kubectl exec -it mynginx-pod /bin/sh
# nginx -V
nginx version: nginx/1.12.0
built by gcc 6.3.0 20170205 (Debian 6.3.0-6)
built with OpenSSL 1.1.0e  16 Feb 2017 (running with OpenSSL 1.1.0f  25 May 2017)
TLS SNI support enabled
configure arguments: --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --pid-path=/var/run/nginx.pid --lock-path=/var/run/nginx.lock --http-client-body-temp-path=/var/cache/nginx/client_temp --http-proxy-temp-path=/var/cache/nginx/proxy_temp --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp --http-scgi-temp-path=/var/cache/nginx/scgi_temp --user=nginx --group=nginx --with-compat --with-file-aio --with-threads --with-http_addition_module --with-http_auth_request_module --with-http_dav_module --with-http_flv_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_mp4_module --with-http_random_index_module --with-http_realip_module --with-http_secure_link_module --with-http_slice_module --with-http_ssl_module --with-http_stub_status_module --with-http_sub_module --with-http_v2_module --with-mail --with-mail_ssl_module --with-stream --with-stream_realip_module --with-stream_ssl_module --with-stream_ssl_preread_module --with-cc-opt='-g -O2 -fdebug-prefix-map=/data/builder/debuild/nginx-1.12.0/debian/debuild-base/nginx-1.12.0=. -specs=/usr/share/dpkg/no-pie-compile.specs -fstack-protector-strong -Wformat -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -fPIC' --with-ld-opt='-specs=/usr/share/dpkg/no-pie-link.specs -Wl,-z,relro -Wl,-z,now -Wl,--as-needed -pie'
```

指定したVersion 1.12.0が立ち上がっている。

ここで`kubectl exec`というコマンドでContainerの中に入ったけれど、ここでうれしいのは、Containerがどこで立ち上がっているかに依らず、Masterからであれば一律このコマンドでContainerの中に入れるところにある。同様に`kubectl logs`というコマンドで、Containerがどこで動いているかに依らずログを参照できる。

`kubectl delete`というコマンドを実行すれば、このPodは消える。

```
[node1 ~]$ kubectl delete pods mynginx-pod
pod "mynginx-pod" deleted
[node1 ~]$ kubectl get pods
No resources found.
```

ここで、最初に「PodとはContainerの集まりである」と述べた。上記の例ではPod一つにあたりContainer一つ、の構成となっている。例えばPod一つにあたり、nginxとredisを含める場合には、以下のようにManifestファイルを記述する。

```yaml:nginx-redis.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx-redis
  labels:
    app: myapp
spec:
  containers:
  - name: mynginx
    image: nginx:latest
  - name: myredis
    image: redis:latest
```

`kubectl apply`で適用して`kubectl pods`でデプロイされた様子を見てみる。

```
[node1 ~]$ kubectl apply -f nginx-redis.yaml
pod/nginx-redis created
[node1 ~]$ kubectl get pods
NAME          READY     STATUS    RESTARTS   AGE
nginx-redis   2/2       Running   0          8s
```

`READY`のカラムが`2/2`になっていることに注目する。

このように、KubernetesにとってはPodが操作の基本単位となる。今回は例示のためにnginxとredisを同一Pod内に含めたが、実際はこうした構成は意味がないかもしれない。実際はPodには一つのContainerを含むことがほとんどであろうが、Podに複数のContainerを含むようなデザインパターンも存在する。例えば**Sidecar-Pattern**や**Ambassador-Pattern**といった具合に。以下の記事が非常にわかりすい -> https://qiita.com/MahoTakara/items/03fc0afe29379026c1f3

# Deployment

上記で例示したPodのManifestファイルでは、例えばそのPodが動いているノードが落ちたとき、そのPodも落ちる。これだけだとdockerコマンドで動かしているのと対して変わらない。

Kubernetesはこれまでも何度か述べたように、セルフヒーリング機能がある。つまり、あるPodのノードが動かなくなった場合に、それを検知し、別のノードで勝手に動かしてくれる。このあたりを定義するのが**Deployment**である。

以下のようなManifestファイルを記述する。

```yaml:mynginx-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nginx
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:latest
```

これもyamlの中身はさておき、「nginxの最新バージョン(latest)のContainerを含むPodを3つデプロイする」ということを意味している。「Podを3つ」というのは`replicas: 3`で記述された部分を意味し、これはKubernetes上では**ReplicaSets**と呼ばれている。

これを適用して、Podの様子をみる。

```
[node1 ~]$ kubectl apply -f mynginx-deployment.yaml
deployment.apps/my-nginx configured

[node1 ~]$ kubectl get pods  -o wide
NAME                       READY     STATUS    RESTARTS   AGE       IP          NODE      NOMINATED NODE
my-nginx-b685dc965-cnwb5   1/1       Running   0          16s       10.36.0.1   node2     <none>
my-nginx-b685dc965-nrrgl   1/1       Running   0          16s       10.44.0.1   node3     <none>
my-nginx-b685dc965-nwn9j   1/1       Running   0          16s       10.44.0.2   node3     <none>
```

3つPodが立ち上がっている。

ここで`my-nginx-b685dc965-cnwb5`のPodを削除してみる。

```
[node1 ~]$ kubectl get pods  -o wide
NAME                       READY     STATUS        RESTARTS   AGE       IP          NODE      NOMINATED NODE
my-nginx-b685dc965-5gpl8   1/1       Running       0          7s        10.36.0.1   node2     <none>
my-nginx-b685dc965-cnwb5   0/1       Terminating   0          1m        10.36.0.1   node2     <none>
my-nginx-b685dc965-nrrgl   1/1       Running       0          1m        10.44.0.1   node3     <none>
my-nginx-b685dc965-nwn9j   1/1       Running       0          1m        10.44.0.2   node3     <none>
```

すると、別のPodがすぐに立ち上がり始めた。

Deploymentはデフォルトでローリングアップデートの機能を備えている。例えば、nginxのバージョンを以下のように`latest`から`1.12.0`に変更するとしよう。

```yaml:mynginx-deployment-1.12.0.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nginx
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.12.0
```

すると以下のように、新しいPodを起動しはじめる。

```
[node1 ~]$ kubectl get pods
NAME                        READY     STATUS              RESTARTS   AGE
my-nginx-5d5b7b9c8b-h2ddw   0/1       ContainerCreating   0          5s
my-nginx-b685dc965-5gpl8    1/1       Running             0          2m
my-nginx-b685dc965-nrrgl    1/1       Running             0          3m
my-nginx-b685dc965-nwn9j    1/1       Running             0          3m
```

文では表現しずらいが、上では 新しいバージョン(1.12.0)のPodを一つだけデプロイし -> 旧バージョンをPodを一つだけ削除し -> また新しいバージョンのPodを一つだけデプロイし -> 旧バージョンのPodを一つだけ削除し -> ... というのを繰り返している。

こうしたデプロイの戦略はもっと細かに設定できる。

# Service

ここまでnginxのPodをデプロイしてきたが、ここにHTTPリクエストができなければ意味がない。こうしたネットワークを管理するものとして**Service**を紹介する。

Serviceはロードバランバランシングの機能を提供するもので、例えば上にあげた3つのnginxに対してロードバランシングを行いたい場合は以下のようなManifestファイルを記述する。

```yaml:mynginx-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: my-nginx-svc
  labels:
    app: nginx
spec:
  type: ClusterIP
  ports:
  - port: 8080
    targetPort: 80
  selector:
    app: nginx
```

これは「selectorでnginxと指定されたPodの集まりに対して、`8080`ポートのアクセスをPodの`80`に負荷分散してフォワーディングする」ことを主張している。

これを適用して確かめる。

```
[node1 ~]$ kubectl apply -f mynginx-service.yaml
service/my-nginx-svc created

[node1 ~]$ kubectl get svc
NAME           TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)    AGE
kubernetes     ClusterIP   10.96.0.1     <none>        443/TCP    20m
my-nginx-svc   ClusterIP   10.106.9.75   <none>        8080/TCP   5s
```

ここで勝手に採番された`CLUSTER-IP`である`10.106.9.75`にアクセスしてみる。

```
[node1 ~]$ curl -s 10.106.9.75:8080 | head -n4
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
```

nginxのwelcomページが返ってきた。

ロードバランスされている様子もみてみる。以下のように異なるリクエストを10個なげて、それぞれのPodの中のnginx Containerのアクセスログをみる。

```
[node1 ~]$ for i in `seq 10`; do curl 10.106.9.75:8080/${i[@]}; done

[node1 ~]$ for i in `kubectl get pod -o name`; do echo **********${i[@]}**********; kubectl logs ${i[@]}; done

**********pod/my-nginx-5d5b7b9c8b-4cpdl**********
10.32.0.1 - - [03/Mar/2019:03:33:58 +0000] "GET / HTTP/1.1" 200 612 "-" "curl/7.29.0" "-"10.32.0.1 - - [03/Mar/2019:03:34:04 +0000] "GET / HTTP/1.1" 200 612 "-" "curl/7.29.0" "-"
10.32.0.1 - - [03/Mar/2019:03:34:09 +0000] "GET / HTTP/1.1" 200 612 "-" "curl/7.29.0" "-"10.32.0.1 - - [03/Mar/2019:03:35:35 +0000] "GET /1 HTTP/1.1" 404 169 "-" "curl/7.29.0" "-"
2019/03/03 03:35:35 [error] 8#8: *4 open() "/usr/share/nginx/html/1" failed (2: No such file or directory), client: 10.32.0.1, server: localhost, request: "GET /1
HTTP/1.1", host: "10.106.9.75:8080"10.32.0.1 - - [03/Mar/2019:03:35:35 +0000] "GET /2 HTTP/1.1" 404 169 "-" "curl/7.29.0" "-"
2019/03/03 03:35:35 [error] 8#8: *5 open() "/usr/share/nginx/html/2" failed (2: No such file or directory), client: 10.32.0.1, server: localhost, request: "GET /2
HTTP/1.1", host: "10.106.9.75:8080"
2019/03/03 03:35:35 [error] 8#8: *6 open() "/usr/share/nginx/html/5" failed (2: No such file or directory), client: 10.32.0.1, server: localhost, request: "GET /5
HTTP/1.1", host: "10.106.9.75:8080"
10.32.0.1 - - [03/Mar/2019:03:35:35 +0000] "GET /5 HTTP/1.1" 404 169 "-" "curl/7.29.0" "-"2019/03/03 03:35:35 [error] 8#8: *7 open() "/usr/share/nginx/html/9" failed (2: No such file or directory), client: 10.32.0.1, server: localhost, request: "GET /9
HTTP/1.1", host: "10.106.9.75:8080"10.32.0.1 - - [03/Mar/2019:03:35:35 +0000] "GET /9 HTTP/1.1" 404 169 "-" "curl/7.29.0" "-"

**********pod/my-nginx-5d5b7b9c8b-c6ckv**********
10.32.0.1 - - [03/Mar/2019:03:33:55 +0000] "GET / HTTP/1.1" 200 612 "-" "curl/7.29.0" "-"
10.32.0.1 - - [03/Mar/2019:03:35:35 +0000] "GET /4 HTTP/1.1" 404 169 "-" "curl/7.29.0" "-"
2019/03/03 03:35:35 [error] 7#7: *2 open() "/usr/share/nginx/html/4" failed (2: No such file or directory), client: 10.32.0.1, server: localhost, request: "GET /4HTTP/1.1", host: "10.106.9.75:8080"
2019/03/03 03:35:35 [error] 7#7: *3 open() "/usr/share/nginx/html/10" failed (2: No such file or directory), client: 10.32.0.1, server: localhost, request: "GET /1
0 HTTP/1.1", host: "10.106.9.75:8080"
10.32.0.1 - - [03/Mar/2019:03:35:35 +0000] "GET /10 HTTP/1.1" 404 169 "-" "curl/7.29.0" "-"

**********pod/my-nginx-5d5b7b9c8b-h2ddw**********
10.32.0.1 - - [03/Mar/2019:03:33:46 +0000] "GET / HTTP/1.1" 200 612 "-" "curl/7.29.0" "-"
2019/03/03 03:35:35 [error] 13#13: *2 open() "/usr/share/nginx/html/3" failed (2: No such file or directory), client: 10.32.0.1, server: localhost, request: "GET /3 HTTP/1.1", host: "10.106.9.75:8080"
10.32.0.1 - - [03/Mar/2019:03:35:35 +0000] "GET /3 HTTP/1.1" 404 169 "-" "curl/7.29.0" "-"10.32.0.1 - - [03/Mar/2019:03:35:35 +0000] "GET /6 HTTP/1.1" 404 169 "-" "curl/7.29.0" "-"
2019/03/03 03:35:35 [error] 13#13: *3 open() "/usr/share/nginx/html/6" failed (2: No such file or directory), client: 10.32.0.1, server: localhost, request: "GET /
6 HTTP/1.1", host: "10.106.9.75:8080"
10.32.0.1 - - [03/Mar/2019:03:35:35 +0000] "GET /7 HTTP/1.1" 404 169 "-" "curl/7.29.0" "-"
2019/03/03 03:35:35 [error] 13#13: *4 open() "/usr/share/nginx/html/7" failed (2: No such file or directory), client: 10.32.0.1, server: localhost, request: "GET /7 HTTP/1.1", host: "10.106.9.75:8080"
2019/03/03 03:35:35 [error] 13#13: *5 open() "/usr/share/nginx/html/8" failed (2: No such file or directory), client: 10.32.0.1, server: localhost, request: "GET /8 HTTP/1.1", host: "10.106.9.75:8080"
10.32.0.1 - - [03/Mar/2019:03:35:35 +0000] "GET /8 HTTP/1.1" 404 169 "-" "curl/7.29.0" "-"
```

ロードバランスされている。

# 宣言的な定義

今回はもっとも基本的と思える**Pod**, **Deployment**, **Service**について例示した。

思えば、すべてのオペレーションは以下しかない：

* Manifestファイルを記述して、
* `kubectl apply`コマンドを適用する。

ここには、Kubernetesの**宣言的な設定**という思想が反映されている。ユーザーは、期待する状態をManifestファイルに宣言する。そしてそれを適用すれば、以前の状態がどうであれ、宣言した状態になる、という考え方である。また、こうした状態をテキストで定義できるのもよい。これによって例えばGitなどで、インフラの状態をソースコードとして管理できる。これはInfrastructure as Codeという文脈でよく語られているものである。

またPodの数やロードバランシングの設定もすべて`kubectl apply`というコマンドで反映できた。この統一した操作であらゆる種類の設定を反映できることで、なんとなく他のCI/CDとの相性のよさが想像できるのではなかろうか。