Kubernetesとは?
Kubernetesは、コンテナ化したアプリケーションのデプロイ、スケーリング、および管理を行うための、オープンソースのコンテナオーケストレーションシステムである(Wikipediaからの引用)。
Kubernetesが何をもたらしてくれるのかは、書籍Cloud Native DevOps with Kubernetesにある文章が端的であった。
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
こうしたことから、「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である。このセットアップ手順に従えば割と簡単に構築ができる。環境としては、少なくともサーバーが2台あればよい。一台はKubernetes Masterという役割のサーバーで、ここでKubernetes上で動かすContainerの操作などを行う。もう一台はKubernetes Nodeという役割のサーバーで、ここで実際にContainerが動く。
よりお手軽なKubernetesの実行環境として、まず minikube が考えられる。minikubeとはローカルのお試しKubernetes環境である。WindowsでもVirtualBoxがあれば動くようになっている(らしい。いれたことがないのでわからない)。
ブラウザですぐにKubernetesを触りたければ、Kubernetesのplaygroundがある。こちらの記事が非常にわかりやすく、これに従えば簡単にKubernetesで遊べる。(実際kubeadmを使って構築するのとそんなに変わらない手順)
自分の好き勝手できる環境でありかつ手軽に、ということならば、本家GoogleのKubernetes Engineを契約して使うのがよい。多少お金は必要だが、1か月1000円くらいあれば楽しめる(使い方によるので真に受けない)。-> GKE
k8sのチュートリアル
チュートリアルは沢山ある。Kubernetesの公式サイトにもTutorialサイトをまとめたページがある。-> https://kubernetes.io/docs/tutorials/
Udemyにもめちゃくちゃコースが並んでいるし(link)、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になっている。
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ファイルを記述する。
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ファイルを記述する。
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
に変更するとしよう。
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ファイルを記述する。
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との相性のよさが想像できるのではなかろうか。