この記事では Docker Desktop 上で Kubernetes クラスタを作り、実際に動かしながら、Kubernetes の基本的な仕組みについて説明していく。
動作確認は以下の環境で行った。
- Docker Desktop 4.22.1
- Kubernetes 1.27.2
事前準備
Kubernetes の有効化
Docker Desktop のダッシュボードから設定画面を開き、Enable Kubernetes
のような項目を有効にすると Kubernetes を使えるようになる。
kubectl
コマンドが使えるようになっていれば問題ないはず。
$ kubectl -h kubectl controls the Kubernetes cluster manager.
コンテナイメージの用意
sample
という名前で、以下の内容のウェブサーバが動くコンテナイメージを作成しておく。
import http from "http"; http .createServer(function (_, res) { res.writeHead(200, { "Content-Type": "text/plain" }); res.end("Hello World\n"); }) .listen(3000);
Docker を使ったコンテナイメージの作り方は以下を参照。
クラスタとリソース
Kubernetes は、コンテナ化されたアプリケーションのデプロイや運用を効果的に行うためのプラットフォーム。
複数(場合によってはひとつ)のコンピュータ(物理マシンや仮想マシン)上でコンテナを動かす。このコンピュータのことをノードといい、ノードの集合体のことをクラスタという。
Docker Desktop で Kubernetes を動かす場合docker-desktop
という名前のクラスタが作られるが、このクラスタを構成するノードは物理マシンや仮想マシンではない。Kubernetes を動かすために必要な各種コンポーネントが Docker Desktop 上でコンテナとして作られ、それらがノードの役割を果たす。
そのため、ローカルマシン上にクラスタが存在するが、ローカルマシンそのものがノードというわけではない。
Kubernetes には「リソース」という概念があり、それを使ってクラスタの状態や動作を記述する。そうすることで Kubernetes は、記述された内容を実現・維持しようとする。
リソースには様々な種類があり、適切なリソースに対して適切な記述をすることで、クラスタを意図通りに動作させることができる。
具体的には、マニフェストファイルと呼ばれる定義ファイルにリソースの設定を記述し、それを Kubernetes に伝える。本記事ではkubectl apply
コマンドを使ってマニフェストファイルの内容を Kubernetes に伝える。
早速manifestfile.yaml
という名前のマニフェストファイルを作ってみる。
内容の説明は必要に応じて後から行うので、今は読まなくてもよい。
apiVersion: apps/v1 kind: Deployment metadata: name: my-dep # Deployment の名前 spec: selector: # Deployment が管理する Pod をどのように選択するか定義する matchLabels: app: node-app # app:node-app というラベルの Pod をこの Deploymentが 管理する replicas: 3 # 保ちたい Pod の数 template: # 作成する Pod の情報を書いていく metadata: labels: app: node-app spec: containers: - name: my-pod image: sample:latest # 事前に作成した container image imagePullPolicy: IfNotPresent ports: - containerPort: 3000 --- apiVersion: v1 kind: Service metadata: name: my-ser # Service の名前 spec: type: NodePort ports: - name: my-ser-port port: 8099 # Service の port targetPort: 3000 # Pod の port nodePort: 32660 # ワーカーノードの port selector: app: node-app # Service が転送を行う Pod を指定
そして$ kubectl apply -f マニフェストファイルのパス
を実行する。
$ kubectl apply -f manifestfile.yaml deployment.apps/my-dep created service/my-ser created
これでsample
から作られたコンテナが Kubernetes 上で動いているはずなので、curl で動作確認してみる。なぜポート番号が32660
なのかは後述するので気にしなくてよい。
$ curl localhost:32660
Hello World
意図通りのレスポンスが返ってきた。
つまり、コンテナが動作しており、それに対してクラスタ外からアクセスできるようになっている。
そしてこれも後述するが、sample
から作られたコンテナは 3 つ存在しており、クラスタはその数を維持しようとする。
本記事ではこれ以降、このクラスタがなぜそのような動作をしているのか、そしてそれを実現させる上で各リソースがどんな役割を果たしているのか、といったことを見ていく。そしてそれを通して Kubernetes の初歩を学んでいく。
一旦、クラスタの状態を元に戻しておく。反映させたマニフェストファイルの設定を取り消すためには、$ kubectl delete -f マニフェストファイルのパス
を実行する。
$ kubectl delete -f manifestfile.yaml deployment.apps "my-dep" deleted service "my-ser" deleted
Pod と Deployment
Kubernetes は、Pod というリソースでコンテナを管理する。
ひとつの Pod で複数のコンテナを管理することもできるが、この記事ではひとつの Pod はひとつのコンテナのみを扱うことにする。
さらに、Pod を管理するための Deployment というリソースがある。Deployment を使うことでコンテナの管理が容易になる。
マニフェストファイルに Deployment について記述してみる。その場合、Deployment の設定のなかに Pod の設定を記述する。
manifestfile.yaml
というファイルを作成し、そこに以下の内容を書いていく。
apiVersion: apps/v1 kind: Deployment metadata: name: my-dep # Deployment の名前 spec: selector: # Deployment が管理する Pod をどのように選択するか定義する matchLabels: app: node-app # app:node-app というラベルの Pod をこの Deploymentが 管理する replicas: 3 # 保ちたい Pod の数 template: # 作成する Pod の情報を書いていく metadata: labels: app: node-app spec: containers: - name: my-pod image: sample:latest # 事前に作成した container image imagePullPolicy: IfNotPresent ports: - containerPort: 3000
このマニフェストファイルでは以下の設定を行っている。
my-dep
という名前の Deployment を作る- この Deployment は
app:node-app
というラベルの Pod を管理する - Pod を 3 つ作りそれを維持する
- この Deployment は
my-pod
という名前の Pod を作る- この Pod のラベルは
app:node-app
- コンテナイメージとして
sample:latest
を使う imagePullPolicy: IfNotPresent
は、該当するコンテナイメージがないか、まずローカルを探すようにするための設定- コンテナは
3000
ポートで通信を受け付ける- 冒頭で示したように
sample
は3000
ポートで通信を受け付けるので、それに合わせている
- 冒頭で示したように
- この Pod のラベルは
apply
を実行して上記の設定をクラスタに反映させる。
$ kubectl apply -f manifestfile.yaml deployment.apps/my-dep created
$ kubectl get deployments
でクラスタ内の Deployment 一覧を見れるので、確認してみる。
$ kubectl get deployments NAME READY UP-TO-DATE AVAILABLE AGE my-dep 3/3 3 3 34s
my-dep
が作られている。
同じ要領でクラスタ内の Pod 一覧も見れる。
$ kubectl get pods NAME READY STATUS RESTARTS AGE my-dep-765c757fd8-4gxdp 1/1 Running 0 42s my-dep-765c757fd8-8p7m9 1/1 Running 0 42s my-dep-765c757fd8-nxsck 1/1 Running 0 42s
Pod が 3 つ作られている。
$ kubectl describe pod Pod名
で Pod の詳細情報を得られる。
多くの情報が表示されるので一部を抜粋して載せるが、sample:latest
から作られたmy-pod
が3000
ポートを開けて動いていることが分かる。
$ kubectl describe pod my-dep-765c757fd8-4gxdp (省略) Containers: my-pod: Container ID: docker://671805cbdd2a40895c135fd6e0d8d5438351d96fc2f1c0ea5e8f973610a4872a Image: sample:latest Image ID: docker://sha256:3ce5b6edbe20936ef2b9d495e107555f584760e1960d2257a4452ee4100230a1 Port: 3000/TCP Host Port: 0/TCP State: Running (省略)
Pod の数は維持される
先程「Pod を 3 つ作りそれを維持する」と書いたが、Deployment は指定された数の Pod を維持しようとする。
試しに Pod をひとつ削除してみる。
$ kubectl delete pod my-dep-765c757fd8-4gxdp
pod "my-dep-765c757fd8-4gxdp" deleted
そのあとに$ kubectl get pods
を実行すると以下の結果になる。
$ kubectl get pods NAME READY STATUS RESTARTS AGE my-dep-765c757fd8-8p7m9 1/1 Running 0 14m my-dep-765c757fd8-cr46q 1/1 Running 0 4s my-dep-765c757fd8-nxsck 1/1 Running 0 14m
my-dep-765c757fd8-4gxdp
は確かに削除されたが、その代わりにmy-dep-765c757fd8-cr46q
が新しく作られ、Pod の数は 3 つに保たれている。
では、何らかの理由で数を変えたいときはどうすればよいのか。
マニフェストファイルのspec.replicas
の記述を変え、再度 apply すればいい。
--- a/manifestfile.yaml +++ b/manifestfile.yaml @@ -6,7 +6,7 @@ spec: selector: # Deployment が管理する Pod をどのように選択するか定義する matchLabels: app: node-app # app:node-app というラベルの Pod をこの Deploymentが 管理する - replicas: 3 # 保ちたい Pod の数 + replicas: 1 # 保ちたい Pod の数 template: # 作成する Pod の情報を書いていく metadata: labels:
$ kubectl apply -f manifestfile.yaml deployment.apps/my-dep configured $ kubectl get pods NAME READY STATUS RESTARTS AGE my-dep-765c757fd8-8p7m9 1/1 Running 0 17m
1 つになっている。
spec.replicas
を3
に戻して apply すればまた 3 つになる。
$ kubectl apply -f manifestfile.yaml deployment.apps/my-dep configured $ kubectl get pods NAME READY STATUS RESTARTS AGE my-dep-765c757fd8-68m8d 1/1 Running 0 2s my-dep-765c757fd8-8p7m9 1/1 Running 0 20m my-dep-765c757fd8-ss7gm 1/1 Running 0 2s
Pod の IP アドレス
Pod にはそれぞれ IP アドレスが振られている。
先程紹介した$ kubectl describe pod
でもその Pod の IP アドレスを見れるが、$ kubectl get pods -o wide
を使うと各 Pod の IP アドレスを一覧で見れる。
$ kubectl get pods -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES my-dep-765c757fd8-68m8d 1/1 Running 0 101s 10.1.0.152 docker-desktop <none> <none> my-dep-765c757fd8-8p7m9 1/1 Running 0 22m 10.1.0.149 docker-desktop <none> <none> my-dep-765c757fd8-ss7gm 1/1 Running 0 101s 10.1.0.151 docker-desktop <none> <none>
だがこの IP アドレスはクラスタ内部で通信するためのものであり、クラスタ外からこの IP アドレスを使って Pod と通信することはできない。
$ curl 10.1.0.152:3000
を実行してもレスポンスは得られない。
クラスタ内部では通信できることを確認するため、Pod から Pod にリクエストを送ってみる。
kubectl exec -it Podの名前 -- 実行したいコマンド
でコンテナ内でコマンドを実行できるので、それを使う。
$ kubectl exec -it my-dep-765c757fd8-8p7m9 -- curl 10.1.0.152:3000 Hello World
my-dep-765c757fd8-8p7m9
からmy-dep-765c757fd8-68m8d
(IP アドレス10.1.0.152:3000
)にリクエストを送り、レスポンスを得られた。
しかし、いくらクラスタ内部で通信できたところで、外部からアクセスできないのでは、ウェブアプリケーションとしての実用性はない。
Service というリソースを使うことで、外部からアクセスできるようになる。
Service
Service は通信に関する様々な役割を担う。
Deployment のときと同様、マニフェストファイルに Service を記述する。
リソース毎にマニフェストファイルを用意することもできるが、この記事では全てのリソースについてひとつのマニフェストファイル(manifestfile.yaml
)に書くことにする。
複数のリソースをひとつのマニフェストファイルに書くときは---
で区切る必要があるので、その下に Service を書いていく。
# 既に説明した Deployment の設定がここに書かれてある --- apiVersion: v1 kind: Service metadata: name: my-ser # Service の名前 spec: type: NodePort ports: - name: my-ser-port port: 8099 # Service の port targetPort: 3000 # Pod の port nodePort: 32660 # ワーカーノードの port selector: app: node-app # Service が転送を行う Pod を指定
spec.type
としてNodePort
を設定した。spec.type
には他の設定もあるが、この記事ではNodePort
である前提で話を進めていく。
apply してmy-ser
が作られていることを確認する。
$ kubectl apply -f manifestfile.yaml deployment.apps/my-dep unchanged service/my-ser created $ kubectl get services my-ser NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE my-ser NodePort 10.96.104.45 <none> 8099:32660/TCP 3s
Service は自身にアクセスがあると、それを Pod に転送してくれる。
どの Pod に転送するのかは、spec.selector
で指定する。今回はapp: node-app
というラベルの Pod に転送したいので、そのように書いた。
また、Pod のポート番号をspec.ports[].targetPort
に書く必要があるので、これも Pod の設定に合わせて3000
を書いている。
これで、Service にリクエストがあるとapp: node-app
ラベルの Pod、つまり先程 Deployment で作った Pod に転送されるようになった。
この設定が意図した通りに行われているか、$ kubectl get endpoints サービス名
で確認できる。
$ kubectl get endpoints my-ser NAME ENDPOINTS AGE my-ser 10.1.0.149:3000,10.1.0.151:3000,10.1.0.152:3000 3m15s
my-ser
は、自身にリクエストがあったときにこのEndpoints
のいずれかに転送してくれる。
そしてこれらのエンドポイントは、既に見た Pod の IP アドレスにspec.ports[].targetPort
で設定したポート番号を組み合わせたものと一致している。
$ kubectl get pods -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES my-dep-765c757fd8-68m8d 1/1 Running 0 33m 10.1.0.152 docker-desktop <none> <none> my-dep-765c757fd8-8p7m9 1/1 Running 0 53m 10.1.0.149 docker-desktop <none> <none> my-dep-765c757fd8-ss7gm 1/1 Running 0 33m 10.1.0.151 docker-desktop <none> <none>
Service にリクエストを送ると Pod に転送してくれることは分かったが、そもそも Service へのリクエストはどのように行えばよいのか。
Service が作られると自動的に IP アドレスが割り振られるので、それを使って Service と通信を行うことができる。
kubectl get services サービス名
で表示されるCLUSTER-IP
が、その IP アドレスである。
$ kubectl get services my-ser NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE my-ser NodePort 10.96.104.45 <none> 8099:32660/TCP 10m
そしてspec.ports[].port
で指定した値が Service のポート番号になる。
つまり今回の場合、10.96.104.45:8099
で Service と通信できる。
Pod 間通信のときと同様、Pod から Service にリクエストを送ってみる。
$ kubectl exec -it my-dep-765c757fd8-8p7m9 -- curl 10.96.104.45:8099 Hello World
Hello World
が返ってきた。
my-dep-765c757fd8-8p7m9
という Pod から Service にリクエストを送り、それを受け取った Service がいずれかの Pod にリクエストを転送、Pod 内で動いているコンテナがレスポンスを返したため、このような結果になった。
ノードへのアクセス
わざわざ Pod 経由で Service にリクエストを送ったことで気付いている方もいるかもしれないが、Service にも、クラスタ外からアクセスすることはできない。
spec.type
がNodePort
の場合、ノードに対してリクエストを送ることで、外部からクラスタにアクセスできるようになる。
NodePort
のときは全てのワーカーノード(ノードのうち、実際にその上でコンテナが動いているノード。他にマスターノードがある。)のポートが開くので、そこに対してリクエストを送ればよい。
具体的にはspec.ports[].nodePort
で指定した値が開かれる。そのため今回は32660
になる。
Docker Desktop の場合、Docker Desktop を動かしているローカルマシン上にノードがひとつ存在し、そのノードがマスターノードとワーカーノードも兼ねており、ローカルマシンのIPアドレス:32660
で外部からアクセスできるようになる。
$ curl localhost:32660
Hello World
まとめると、「ノード -> Service -> Pod」という順番にルーティングされていくことで、Pod のなかで動いているコンテナがリクエストを受け取ることができるのである。