これはSmartHR Advent Calender 2020 19日目のエントリです。

背景

AWS ECS、Azure Web AppのようなPaaSはアプリケーションエンジニアにとって、完璧なサービスに見える。 高可用性、オートスケーリング、デプロイのローリングアップデート、モニタリング、ロギングなどの機能ははじめから付いており、 エンジニアがソースコードやDockerイメージを作れば、あとはリリースするだけだ。あとはコンテナを管理するAWSやAzureがよしなにやってくれる。

なぜ世間はわざわざ高いインフラ管理と学習のコストを払って、わざわざコンテナを自分で管理するkubernetes(k8s)に移行しているんだろうか。

1年ほどk8sを使ったWebアプリケーションの開発に携わり、やっと世間がk8sを祭り上げる理由がわかってきた気がするので、k8s知らん勢に向けて、その理由について書いてみるよー
(実際色んな理由で使ってるんだろうけど、自分が特にすごいと思ったところをかくよー)

tldr

  • 宣言的設定とコントロールループ
  • (凄まじく)高い拡張性
  • 外部オブジェクトとkubernetesオブジェクトのマッピング

やってみよう

実はkubernetesを使ってみるのは、とても簡単。 minikubeは簡単に動かせるkubernetesクラスター。単一のバイナリで構成され、MacやWindowでも動作する。(以下はMac用)

curl -Lo minikube https://storage.googleapis.com/minikube/releases/latest/minikube-darwin-amd64 \
  && chmod +x minikube
sudo mv minikube /usr/local/bin
minikube start

(本物のk8sで試してみたい方には、Digital Oceanや、Azureの無料枠が安くておすすめ)

kubectlコマンドでnode(dockerコンテナの入るホストマシン)の状態を見てみる。 (手元のマシンだけがk8sクラスターに組み込まれているので1つだけになるはず。)

$ kubectl get node
NAME       STATUS   ROLES    AGE     VERSION
minikube   Ready    master   2m59s   v1.19.4

ここまでできてれば、もうk8sにアプリケーションをデプロイすることができる
こんな感じで、deployment.yamlを定義してnginxをデプロイしてみよう!

$ cat << EOF > deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.18.0-alpine
        ports:
        - containerPort: 80
EOF

$ kubectl apply -f deployment.yaml

このdeployment.yamlは、簡単に言うとどのようなPod(コンテナの集まり)の定義とそれを何個つくるかをきめるDeploymentというものの設定だ。 以下からわかるように、このDeploymentはPodを3つ作るものになる。

spec:
  replicas: 3

その下の部分ではPodに入るコンテナが宣言されていて、今回の場合は 1.18.0-alpine になっている。

    spec:
      containers:
      - name: nginx
        image: nginx:1.18.0-alpine
        ports:
        - containerPort: 80

このコマンドが終わったら、下のコマンドで、このDeploymentの状態を確認してみよう

$ kubectl get deployment
NAME               READY   UP-TO-DATE   AVAILABLE   AGE
nginx-deployment   3/3     3            3           5d9h

READYが3/3ということは、3つのPodすべてが動いているということになる。Podの状態を見てみよう。

$ kubectl get pod
nginx-deployment-66b6c48dd5-cxgq8   1/1     Running   1          5d9h
nginx-deployment-66b6c48dd5-dz7mr   1/1     Running   1          5d9h
nginx-deployment-66b6c48dd5-qrq2h   1/1     Running   1          5d9h

生きているのが確認できたなら、実際にアクセスしてみよう。以下のコマンドで、localhost:8080への通信を今作ったDeploymentの80番ポート(nginxが動いてる) にフォワードすることができる。

$ kubectl port-forward deployment/nginx-deployment 8080:80

localhost:8080を参照してみるとnginxが動いていることがわかるはず。

nginx-welcome

宣言的設定とコントロールループ

Deploymentはk8sでアプリケーションを管理するために利用される基本的なオブジェクトで、おもに ①どんなPodがほしいか②何個ほしいかを宣言するものになっている。
さっき作成したのとおなじdeployment.yamlをつかって、これが崩れたときに何が起こるかを見てみよう。
いま、下のようなDeploymentによって3つのPodが作成されている。

$ kubectl get deployment
NAME               READY   UP-TO-DATE   AVAILABLE   AGE
nginx-deployment   3/3     3            3           5s

$ kubectl get pod
NAME                               READY   STATUS    RESTARTS   AGE
nginx-deployment-5cb95ccc7-6hl8g   1/1     Running   0          5s
nginx-deployment-5cb95ccc7-6vk6m   1/1     Running   0          5s
nginx-deployment-5cb95ccc7-rxgj9   1/1     Running   0          5s

このPodの中の1つを削除してみよう。

$ kubectl delete pod nginx-deployment-5cb95ccc7-6vk6m

そうすると、削除したPodは、Terminating状態になる、とほぼ同時に、新しいPodが立ち上がりはじめる。 (AGEが1つだけ、8sのがあるので新しいPodができたことがわかる。)

$ kubectl get pod
NAME                               READY   STATUS        RESTARTS   AGE
nginx-deployment-5cb95ccc7-6hl8g   1/1     Running       0          20s
nginx-deployment-5cb95ccc7-6vk6m   0/1     Terminating   0          20s ★消されているPod
nginx-deployment-5cb95ccc7-rxgj9   1/1     Running       0          20s
nginx-deployment-5cb95ccc7-z4wbf   1/1     Running       0          8s  ★新しいPod

もうしばらくすると、Terminating状態のPodはなくなり、Podの数は3つになる。

$ kubectl get pod
NAME                               READY   STATUS    RESTARTS   AGE
nginx-deployment-5cb95ccc7-6hl8g   1/1     Running   0          34s
nginx-deployment-5cb95ccc7-rxgj9   1/1     Running   0          34s
nginx-deployment-5cb95ccc7-z4wbf   1/1     Running   0          22s    ★新しいPod

deployment.yamlで下のように、Podの数(replicas)を3つと宣言しているので、Deploymentは Pod3つの状態を保つためにPodが削除された時に、新しいPodを作り直してれる。

spec:
  replicas: 3

今度は、逆にDeploymentのreplicasの数を5に変えてみよう。Deploymentの定義は以下のようにkubectl editを使うとエディタで編集することができる。

edit-replicas

しばらくして、参照するとPodの数は5つに増えている。

$ kubectl get pod
NAME                               READY   STATUS    RESTARTS   AGE
nginx-deployment-5cb95ccc7-6hl8g   1/1     Running   0          89m
nginx-deployment-5cb95ccc7-dd8dc   1/1     Running   0          57s
nginx-deployment-5cb95ccc7-rp55x   1/1     Running   0          57s
nginx-deployment-5cb95ccc7-rxgj9   1/1     Running   0          89m
nginx-deployment-5cb95ccc7-z4wbf   1/1     Running   0          89m

もう少し別な変更の仕方をしてみよう。今度はreplicasの数を5から1に変更し、同時に 使用するイメージを nginx:1.18.0-alpine から nginx:1.19.5-alpine に変更してみる。

edit-version

すると、5つのPodの内の4つがTerminating状態になり、あたらしく1つのPodが作られ始める。

$ kubectl get pod
NAME                               READY   STATUS              RESTARTS   AGE
nginx-deployment-5cb95ccc7-6hl8g   1/1     Running             0          113m
nginx-deployment-5cb95ccc7-b8qtj   0/1     Terminating         0          2m48s
nginx-deployment-5cb95ccc7-psmk7   0/1     Terminating         0          2m48s
nginx-deployment-5cb95ccc7-t8t7f   0/1     Terminating         0          2m48s
nginx-deployment-5cb95ccc7-wz7s9   0/1     Terminating         0          2m48s
nginx-deployment-8dbdfb87c-r8dqt   0/1     ContainerCreating   0          8s      ★新しく作られているPod

最終的には、Podは1つだけになる。

$ kubectl get pod
NAME                               READY   STATUS    RESTARTS   AGE
nginx-deployment-8dbdfb87c-r8dqt   1/1     Running   0          26s

このPodのイメージを参照してみると nginx:1.19.5-alpine になっている。

$ kubectl get pod nginx-deployment-8dbdfb87c-r8dqt -o yaml
# (kubectl get に -o yaml オプションをつけるとk8s上のPodの全定義がyaml形式で見れる)

# 略
spec:
  containers:
  - image: nginx:1.19.5-alpine
    imagePullPolicy: IfNotPresent
    name: nginx
    ports:
# 略

このDeploymentの例では、Podが削除された時に、新たに足りないPodが立ち上がり、逆にDeploymentが修正された時には、 それに合わせた、数、イメージのPodが立ち上がった。 Deploymentは望ましい状態(Podの数、イメージのバージョンのようなPodの定義) を宣言するためのオブジェクトで、現在の状態が そこからずれてしまった場合には、k8sは自動的にそのズレを修正してくれるようになっている。 このように、現在の状態を望ましい状態に近づけてくれるためのk8sの仕組みはコントロールループ(Reconciliationループとも)と呼ばれている。
(制御工学っぽい)

control loop

wikipedia より

PodやDeploymentだけでなくk8sの他のオブジェクトも、基本的にこのコントロールループによって実現されている。 例えば、Horizontal Pod Autoscaler(HPA) は、負荷に合わせたPodのスケールアウトを実現するためのオブジェクトで下の様に対象のDeploymentと平均CPU使用率のしきい値(averageUtilization)を記載することができる。

# 略
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: [デプロイメントの名前]
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 50

HPAは、特定のDeploymentに所属するPodの平均CPU使用率を監視し、それが特定の値を超えた場合、 Deploymentで以下の様に宣言していたreplicasの値を書き換えて増やしてしまう。

spec:
  replicas: 3

そうするとDeploymentはPodの数を増やすので、結果としてPodの平均CPU使用率が下がるという仕掛けだ。 このように、HPAは平均CPU使用率のあるべき状態を保つため、Deploymentのreplicasをコントロールループで調整するオブジェクトになっている。

現実のWebアプリケーションでは、マシンの障害でコンテナがいきなり消えたり、負荷の増大でスケールアウトが必要になったり、 バグの修正のため新しいバージョンをリリースしたりする必要がある。 k8sでは、それぞれの要件に対して別々の機構を持っているわけではなく、全ては事前に定義されたあるべき状態を保とうとするコントロールループの作用によって対処される。 (すごい!すごくない?)

(凄まじく)高い拡張性

k8sはコンテナをホスティングするためのプラットフォームだ。PodやDeploymentは、その基本となるオブジェクトでAWSでいうとEC2に相当するものだ。 しかし、AWSでいう、RDS,ELB,Elastic Cache,S3….のようなサービスはk8sには入っていない。 なぜかというと、そのようなサービスはk8s自体のソースコードを変更することなく自由に付け足すことができるためだ。

これを実現するためのオブジェクトがCustomResourceDefinition(CRD)だ。CRDはその名の通りユーザーが カスタムしたオブジェクトをk8s上に定義するためのオブジェクトだ。
以下のような、crd.yamlを用いることで、k8s上にHogeという種類のオブジェクトを定義できるようになる。

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: hoges.stable.example.com
spec:
  group: stable.example.com
  versions:
    - name: v1
      served: true
      storage: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                fuga:
                  type: string
  scope: Namespaced
  names:
    plural: hoges
    singular: hoge
    kind: Hoge
    shortNames:
      - hg

以下のように適用することができる。

$ kubectl apply -f crd.yaml

例えば、以下のような hoge.yaml を作成すれば、上記と同様にkubectl apply -f hoge.yamlでこのHogeという種類の オブジェクトを作成することができる。(kind: Hoge のオブジェクトになっている)

apiVersion: stable.example.com/v1
kind: Hoge
metadata:
  name: test-hoge
spec:
  fuga: hugahuga

定義したHogeのオブジェクトはPodやDeploymentと同様にkubectl getで参照することができる。

$ kubectl get hoge
NAME        AGE
test-hoge   80s

ここだけであれば、ただのカスタムしたオブジェクトが見れるだけで、なんの意味もない。 しかし、このオブジェクトに対応するコントロールループが実装できたらどうだろうか? 実はk8sではコントロールループを実装したPodをクラスタ上にデプロイすることで、これが実現できてしまう。 コントロールループ内で、CRDを参照し、その定義に従ってk8s内の他のオブジェクトを作ったり消したり設定 変更することによって、好みのオブジェクトを作ることができる。
例えば、databaseという名前のCRDを作って、その定義に従ってPostgreSQLやMYSQLのイメージからPodを作り出す コントロールループを実装すれば、AWSでいうRDS相当の機能が実現できてしまう。 このようなCRDに対するコントロールループは特にOperatorと呼ばれ、CRDとOperatorを作って、k8sを拡張することは Operator pattern と呼ばれている。 いろんなk8sのOperatorが作られていて、例えば以下のようなものがあるよ。

  • zalando/postgres-operator はその名の通りPostgreSQLを作ってくれるオペレーターで、AWSで言うところのRDSに相当するものになる。 レプリケーション、バックアップ、ポイントインタイムリカバリ、コネクションプーリングなどなどのデータベースサービスに期待される 機能が一通り揃っている。
  • knative はFaaS(Function as a service)を実現するオペレーターでAWSで言うところのLambdaだ。リクエストが来るとPodを立ち上げて それをさばき、暇な時はPodを落としてくれる。アプリケーションによってはリソース効率を大きく上げることができる。
  • Istio はマイクロサービス用のサービスメッシュの代表格で、これもOperatorを使って実現されている。

他にもいろんなOperatorがOperatorHub で公開されている。

もちろん、このような仕組みはkubebuilder などのフレームワークを使うことで自分で実装することができる。 (自分もatコマンド相当のOperatorを作ったことがあるので良かったら過去記事 を読んでね。)

このようなコントロールループを利用したOperatorのエコシステムがあり、便利なものが作られ続けることがk8sの利点の一つだ。

外部オブジェクトとkubernetesオブジェクトのマッピング

Kubernetes (K8s)は、デプロイやスケーリングを自動化したり、コンテナ化されたアプリケーションを管理したりするための、オープンソースのシステムです。

公式サイトより

公式サイトの謳い文句の通りk8sはコンテナを管理するためのものだ。 だが、現実的なWebアプリケーションはコンテナを作るだけでは実現できない。 静的ファイルを高速に世界各地に届けるためにはS3やCDNを使う必要があるし、 大量のデータを扱うためにはDynamodbのようなほぼ際限なくスケールしてくれるKVSを使いたい。 k8s上にデータベースを作るためには、そもそもコンテナが消えてもデータが残るストレージが必要になるし、あるいはk8s上にデータベース を作るのではなく、RDSを使うのが良いのかもしれない。

このようなk8sの外のオブジェクトも、実は先程まで紹介していたOperatorを使うことでk8s上のオブジェクトとして扱うことができる。 例えば、アプリケーションがファイルをアップロードするためのS3のバケットが必要な場合、S3というCRDとそれを参照して 実際にAWS上にS3のバケットを作成するコントロールループを実装すれば良い。 このようなオペレータもいろんなものが実装されており、例えば以下のようなものがある。

  • Azure Service Operator はAzure公式ののいろいろなサービスを作ってくれるOperator。過去記事 でAzure Postgresをつかってみてるよ。
  • AWS Controllers for Kubernetes はAWSが作っているAWSのサービスを作ってくれるOperator。まだdeveloper preview。
  • Crossplane はAWS,GCP,Azureを含むいろんなクラウド上のリソースを定義してくれるオペレーター

このようなCRDを作っておくことで、AWSコンソールやTeraformでAWS上の設定をして、その情報をk8sの定義に 記載するといった作業がなくなり、k8sの定義のみで必要な設定のすべてを記述することができる。
(Infrastructure as Codeの完成形だ!)

まとめ

k8sはコントロールループとそれを拡張可能にするというとても賢い仕組みがあり、 必要なサービスを作ったり外部のオブジェクトをk8s上のオブジェクトとして扱うことができるようになっている。 この拡張はさまざまなところで開発されていて、エコシステムは今後も大きくなりk8sはより便利になっていくと思う。 この辺が他のコンテナ管理用のプラットフォームやPaaSではなくk8sを使う理由になると思う。