% cat

kubernetes で Ruby on Rails を動かして kubernetes を解説する

巷で話題の kubernetes ですが、とってもとってもとっつきにくいですよね
そんな kubernetes ですが手元で動かすことができたので解説してみます
(情報が間違ってたらごめんなさい! 🙇)

目標はこちら

  • kubernetes の後述する基本的な概念を雰囲気理解すること
  • ローカルのクラスタにデプロイして Ruby on Rails ( Rails ) の 「 Yay! 」のページを見ること

コードはこちら
https://github.com/tkhr0/hello-rails-on-k8s

早速行きましょう

今回構築する環境

図にするとこんな感じです

kubernetes 要素を抜くとこんな Ngix + Rails + MySQL のよくある形です

構築する環境

http サーバとして Nginx を立てて、 Rails (Puma) にリバースプロキシします
DB には MySQL を用意して Rails とやりとりします

ここに kubernetes 要素を加味してこうです

完成形

これが完成形になります

なんかいろいろでてきましたね
次で解説していきます

kubernetes の基本的な概念を雰囲気理解する

そこで今回紹介する概念はこちら

  1. Container
  2. Pod
  3. ReplicaSet
  4. Deployment
  5. Service
  6. Node
  7. Label

いっぱいありますね
雰囲気をお伝えするためにざっくり説明します
( kubernetes が管理する↑こういうやつをオブジェクトと呼びます)

Container

まずは Container です。
図での黄色です
これは Docker コンテナのことです
1 つの Container には 1 プロセスとすることが推奨されています
直接 kubernetes の概念ではないですが、 kubernetes はこいつを取り扱うためのツールですね

Pod

そして Pod です
図での明るい方の青です
これは Container をまとめる最小単位となります
僕の中のイメージは AWS の EC2 インスタンスです
インスタンスの中にミドルウェアとしてアプリケーションが入ってるように、 Pod の中に Container としてアプリケーションが入っているイメージです

ここで Pod は冗長化には向いていません
例えば Nginx を 2つ立てる場合には Pod を 2つ用意する必要があります
それは手間ですよね

それを解決するのが ReplicaSet です

ReplicaSet

図での緑色です
ReplicaSet は Pod をまとめて扱う役割があります
Pod を ReplicaSet 配下に置くことで ReplicaSet 単位で複製して群として扱うことができます

Deployment

次に Deployment です
図でのねずみ色です
Deployment はアプリケーションのデプロイをしてくれます
Docker Image から Pod や ReplicaSet を生成して Node に配置します
また RollingUpdate 戦略が用いられるため、ダウンタイムを発生させずにデプロイすることができます

Service

そして Service です
図での暗い方の青色です
Pod が外部とやりとりするために必要になるオブジェクトです
WAN からのリクエストを Pod に割り振ったり、 Pod から Pod への通信を仲介したりしてくれます
また Service がいることでサービスディスカバリを提供できます
※ サービスディスカバリとは、サーバでネットワークの変更があってもクライアントからは同じようにアクセスし続けられる仕組みのこと。 DNS もひとつですね。

Node

次は先ほど出てきた Node です
図でのシルバーです
Node は… Node です
物理サーバとニアリーイコールなイメージです
うまく説明できませんが kubernetes が管理している領域を表します

Label

最後に Label です
Service, ReplicaSet, Pod などのオブジェクトは Label を用いてグルーピングをすることができます
リクエストの送信先や所属関係はこの Label によるグルーピングで表します
なので、逆に入れ子関係や親子関係のような概念は kubernetes では出てきません
概念的にはすべてのオブジェクトは並列に並んでおり Label によってグルーピングがなされています

kubernetes を構成する要素はこのようになっています
kubernetes はサーバを管理するための要素を一般化するためにたくさんの機能を持っています
それらを Pod や Deployment などの単位に区切ることで疎結合なシステムを提供しています
要素数はたくさんありますが kubernetes なしでのサーバ環境と近いところがあるので少しづつ理解しましょう

またここでは紹介しませんでしたが他にもいろいろな機能が提供されています
crontab のように定期的に動作を行う Job や環境ごとの設定の違いを吸収する ConfigMap など多数の要素があります
実際に本番環境での運用をする場合には必要になってくるので一緒にググってみてください

ローカルで kubernetes を動かす

先ほどざっくりお伝えした雰囲気を携えて実際に kubernetes を動かしてみましょう

手元の環境はこちらです

$ docker -v
Docker version 18.06.1-ce, build e68fc7a
$ kubectl version
Client Version: version.Info{Major:"1", Minor:"13", GitVersion:"v1.13.2", GitCommit:"cff46ab41ff0bb44d8584413b598ad8360ec1def", GitTreeState:"clean", BuildDate:"2019-01-25T08:47:41Z", GoVersion:"go1.11.4", Compiler:"gc", Platform:"darwin/amd64"}
Server Version: version.Info{Major:"1", Minor:"10", GitVersion:"v1.10.3", GitCommit:"2bba0127d85d5a46ab4b778548be28623b32d0b0", GitTreeState:"clean", BuildDate:"2018-05-21T09:05:37Z", GoVersion:"go1.9.3", Compiler:"gc", Platform:"linux/amd64"}

下準備

まずはこちらのリポジトリをクローンしてください
https://github.com/tkhr0/hello-rails-on-k8s

そしてローカルの kubernetes 環境を起動します
Docker Desktop for Mac 18.06.0-ce-mac70 CE から kubernetes が同梱されているのでそれを起動します

Docker の設定画面から
Kubernetes > Enable Kubernetes > Kubernetes
とクリックして kubernetes を起動します

k8s 起動

Docker のバージョンが古い場合には minikube というツールで代替してください

最後に kubectl というコマンドが必要になるのでマシンにインストールしてください
これは docker コマンドのように kubernetes エンジンとやりとりするためのコマンドです
こちらを参考にしてみてください
https://kubernetes.io/docs/tasks/tools/install-kubectl/

mac & Homebrew ならこのコマンドでインストールできます

$ brew install kubernetes-cli

とりあえず動かす

とりあえず kubernetes にデプロイしてみましょう
クローンしたリポジトリ内で以下のコマンドを打ってみてください

各種 Docker イメージの pull と build が走るので少し時間がかかります

$ docker build -f .docker/containers/app/Dockerfile   -t hello-rails-on-k8s-app:latest .
$ docker build -f .docker/containers/nginx/Dockerfile -t hello-rails-on-k8s-nginx:latest .

$ kubectl apply -f .docker/kubernetes/webserver.yml
$ kubectl apply -f .docker/kubernetes/dbserver.yml

最後に下記のコマンドを打ちます
kubernetes が管理するオブジェクトを確認できます

$ kubectl get all

こんな感じの結果が返ってきたら成功です

NAME                                        READY   STATUS              RESTARTS   AGE
pod/dbserver-deployment-6dd6fbf959-hf52l    0/1     ErrImageNeverPull   0          4m
pod/webserver-deployment-65d64cb4d8-x9vdt   1/2     ErrImageNeverPull   0          4m

NAME                        TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE
service/dbserver-service    ClusterIP   10.100.147.224   <none>        3306/TCP   4m
service/kubernetes          ClusterIP   10.96.0.1        <none>        443/TCP    109d
service/webserver-service   ClusterIP   10.106.16.206    <none>        84/TCP     4m

NAME                                   DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/dbserver-deployment    1         1         1            0           4m
deployment.apps/webserver-deployment   1         1         1            0           4m

NAME                                              DESIRED   CURRENT   READY   AGE
replicaset.apps/dbserver-deployment-6dd6fbf959    1         1         0       4m
replicaset.apps/webserver-deployment-65d64cb4d8   1         1         0       4m

Pod x2, Service x2, Deployment x2, ReplicaSet x2 が動いているのがわかります

いろいろイジる

kubernetes はオブジェクトの定義に基づいてオブジェクトを展開・管理します
その定義ファイルをみていじってみましょう

定義を見る

こちらが web サーバの Service, Deployment, ReplicaSet, Pod の定義です

# Service の定義
apiVersion: v1
kind: Service
metadata:
  # この Service の名前
  name: webserver-service

spec:
  type: ClusterIP
  # Label がこれらにマッチした Pod にアクセスを流す
  selector:
    app: hello-rails-on-k8s
    server: web
  # ポートの指定
  ports:
    - name: http
      protocol: TCP
      # 受信するポート
      port: 84
      # 送信するポート
      targetPort: http

---
# Deployment の定義
apiVersion: apps/v1
kind: Deployment
metadata:
  # この Deployment の名前
  name: webserver-deployment
  # この Deployment につけるラベル
  labels:
    app: hello-rails-on-k8s

# 管理する ReplicaSet の定義
spec:
  # 展開するレプリカ数
  replicas: 1
  # Label がこれらにマッチした Pod をこの ReplicaSet の配下に置く
  selector:
    matchLabels:
      app: hello-rails-on-k8s
      server: web

  # 展開する Pod の定義
  template:
    metadata:
      # この Pod につける Label
      labels:
        app: hello-rails-on-k8s
        server: web

    spec:
      volumes:
        - name: sockets
          emptyDir: {}
      # Container の定義
      containers:
        - name: nginx
          # イメージの指定
          image: hello-rails-on-k8s-nginx:latest
          imagePullPolicy: Never
          ports:
            # Service から受けるポート
            - name: http
              containerPort: 8080
          volumeMounts:
            - mountPath: /sockets
              name: sockets

        - name: rails
          # イメージの指定
          image: hello-rails-on-k8s-app:latest
          imagePullPolicy: Never
          env:
            - name: DB_HOST
              # Service にはドメインが割り振られる
              value: dbserver-service.default.svc.cluster.local
          volumeMounts:
            - mountPath: /sockets
              name: sockets

Rails にアクセスしてみる

動作確認のために Rails にアクセスしてみましょう
と、言いたいところですが今はアクセスできません

kubernetes のネットワークを司る Serivce の状況を見てみましょう

$ kubectl get service
NAME                TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE
dbserver-service    ClusterIP   10.100.147.224   <none>        3306/TCP   18h
kubernetes          ClusterIP   10.96.0.1        <none>        443/TCP    110d
webserver-service   ClusterIP   10.107.232.16    <none>        84/TCP     7s

ここで TYPE 列に ClusterIP と記載があります
ClusterIP は kubernetes の中でのみ解決できる IP になります
そのため kubernetes の外、つまりローカルマシンからアクセスできる IP が存在しない状態です
PORT(S) 列をみるとポートを EXPOSE しているだけなのがわかります

これを解決するためには NodePort という type を設定します
NodePort は kubernetes の外からもアクセスできる IP です

ということで NodePort を設定してみましょう
webserver-servicedbserver-service の 2つの Service がありますが、 webserver-service のみに設定します
webserver-service は Nginx に繋がっていて dbserver-service は MySQL に繋がっています
MySQL を外に出す必要はないので dbserver-service は ClusterIP を使うべきです
なので webserver-service のみ設定します
kubernetes という Service もありますが、これは kubernetes が内部的に使用しているものです)

さて .docker/kubernetes/webserver.yml を編集します
Service の定義の中で .spec.type の値を ClusterIP から NodePort に変更します

そして下記のコマンドで変更を適用します

$ kubectl delete svc webserver-service
service "webserver-service" deleted
$ kubectl apply -f .docker/kubernetes/webserver.yml
service/webserver-service created
deployment.apps/webserver-deployment unchanged

変更を確認しましょう

$ kubectl get service
NAME                TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
dbserver-service    ClusterIP   10.100.147.224   <none>        3306/TCP       19h
kubernetes          ClusterIP   10.96.0.1        <none>        443/TCP        110d
webserver-service   NodePort    10.107.232.16    <none>        84:30956/TCP   20m

webserver-servicePORT(S) 列が変わりました
84:30956 となっているので 30956 へのアクセスを 84 へ流します

実際に http://localhost:30956/ にブラウザでアクセスしてみましょう
おなじみの Yay! You're on Rails! を見れるかと思います

冗長化してみる

ReplicaSet を 2つにすることで冗長化してみます

まずは現状を見てみます

$ kubectl get pod
NAME                                    READY   STATUS    RESTARTS   AGE
dbserver-deployment-685f496464-97qk7    1/1     Running   0          1h
webserver-deployment-5595df784c-sv5q9   2/2     Running   0          1h

webserver で始まる Pod が 1つあります

次に .docker/kubernetes/webserver.yml を編集します
ReplicaSet の設定の中で .spec.replicas の値を 1 から 2 に変更します
そして kubectl apply コマンドで変更を適用します

$ kubectl apply -f .docker/kubernetes/webserver.yml

kubectl get pod コマンドで変更を確認します

$ kubectl get pod
NAME                                    READY   STATUS    RESTARTS   AGE
dbserver-deployment-685f496464-97qk7    1/1     Running   0          98m
webserver-deployment-5595df784c-crgkx   2/2     Running   0          4s
webserver-deployment-5595df784c-sv5q9   2/2     Running   0          93m

webserver で始まる Pod が 2つになりました

この状態で puma のアクセスログをみてみましょう

$ kubectl logs -f webserver-deployment-5595df784c-crgkx rails
# 別のウィンドウで
$ kubectl logs -f webserver-deployment-5595df784c-sv5q9 rails

ブラウザでリロードしまくると kubernetes が各 Pod に対してロードバランシングするためそれぞれにアクセスがあることが確認できます

デプロイしてみる

Deployment を使って新しいバージョンのアプリケーションをデプロイしてみましょう

タグを hello-rails-on-k8s-app:v2 に変更してビルドします

docker build -f .docker/containers/app/Dockerfile -t hello-rails-on-k8s-app:v2 .

.docker/kubernetes/webserver.yml を編集します
Deployment の設定の中で .spec.template.spec.containers[1].image の値を変更します
hello-rails-on-k8s-app:latesthello-rails-on-k8s-app:v2 にします

まず、現状を確認するために kubectl describe コマンドを打ちます

$ kubectl get pod
NAME                                    READY   STATUS              RESTARTS   AGE
dbserver-deployment-6dd6fbf959-hf52l    0/1     ErrImageNeverPull   0          17h
webserver-deployment-65d64cb4d8-gwmk5   2/2     Running             6          1h
$ kubectl describe pod webserver-deployment-544cb4c74f-4qsds
~~ 略 ~~
  rails:
    Container ID:   docker://881c9508e1df9dfeb21df52e2293ac8f75fea5b15f6a1de0a18b7ec9dbbc32d9
    Image:          hello-rails-on-k8s-app:latest
    Image ID:       docker://sha256:c482a73c4174c8e1005b8ffa58bea421ae27b4ca08be350bec31f52502e2b9fa
~~ 略 ~~

rails のイメージには hello-rails-on-k8s-app:latest が使われています

そして kubectl apply コマンドで変更を適用します…の前にひとつコマンドを打ちましょう

$ kubectl get pod -w
NAME                                    READY   STATUS    RESTARTS   AGE
dbserver-deployment-685f496464-97qk7    1/1     Running   0          98s
webserver-deployment-65d64cb4d8-gwmk5   2/2     Running   0          67s

ログが流れてくるので別のウィンドウを開いて kubectl apply コマンドを打ちましょう

$ kubectl apply -f .docker/kubernetes/webserver.yml

そうすると kubectl get pod -w を打ったウィンドウで Pod の作成状況が流れてきます

webserver-deployment-5595df784c-sv5q9   0/2   Pending   0     0s
webserver-deployment-5595df784c-sv5q9   0/2   Pending   0     0s
webserver-deployment-5595df784c-sv5q9   0/2   ContainerCreating   0     0s
webserver-deployment-5595df784c-sv5q9   2/2   Running   0     3s
webserver-deployment-65d64cb4d8-gwmk5   2/2   Terminating   0     4m6s
webserver-deployment-65d64cb4d8-gwmk5   0/2   Terminating   0     4m38s

webserver が 2つあって RunningTerminating だったりします
Deployment が RollingUpdate して新しいコンテナを展開して、展開できたら古いコンテナを削除する、という動きが見れますね

kubectl describe を打ちましょう

~~ 略 ~~
  rails:
    Container ID:   docker://651ad2790d7d103dbc24df0a84dbaa2e833a2180cdc384cc684090be4c20f668
    Image:          hello-rails-on-k8s-app:v2
    Image ID:       docker://sha256:2722376b4a713a3660ec74bf9febd63269dfa973f31a2dc10e48935b419952fe
~~ 略 ~~

ちゃんと rails のイメージには hello-rails-on-k8s-app:v2 が使われています

Deployment にて、使用するイメージを指定して apply したら kubernetes がよしなにやってくれることがわかりました

オブジェクトを削除する

kubectl delete コマンドできれいさっぱり削除できます

$ kubectl delete -f .docker/kubernetes/webserver.yml
$ kubectl delete -f .docker/kubernetes/dbserver.yml

まとめ

いかがでしたでしょうか?
この目標は達成できましたか?

  • kubernetes の後述する基本的な概念を雰囲気理解すること
  • ローカルのクラスタにデプロイして Ruby on Rails ( Rails ) の 「 Yay! 」のページを見ること

kubernetes の入り口をざっと解説してみました
ですが、解説できなかったものもあります
なぜ ClusterIP が 2つあるのか、 socket をどうやって繋げているのか、定義ファイルをコメントで逃げた、などなど
また、 tkhr/hello-rails-onlk8s のリポジトリでは fluentd と kibana を動かしていたり、 kubernetes のデプロイを helm で行っていたりします
それらについても触れたかったのですが、読みごたえがすごいことになってしまうので割愛しました

もっといろいろできるのが kubernetes とのことです
なので僕もこれから勉強していきたいと思います
できる人教えてください

参考

最新記事

大量アクセスに耐え得る在庫管理システムの構成を考え実装してみた

皆さん「在庫管理」ってどうしてます?itemsテーブルに、stockカラム作ってdecrementしてますか? まぁ正直、それでも良い感じしますよね。楽だし何やってるかわかりやすい。 しかし! **超人気商品に超ア...

mmusasabi
2019年03月13日

kubernetes で Ruby on Rails を動かして kubern...

巷で話題の kubernetes ですが、とってもとってもとっつきにくいですよね そんな kubernetes ですが手元で動かすことができたので解説してみます (情報が間違ってたらごめんなさい! 🙇) 目標はこちら ...

tkhr0
2019年03月07日

初めて社内ISUCONを開催したらめっちゃ盛り上がった

みなさん ISUCON というものをご存知でしょうか? ISUCON とは... > 「お題となる Webサービスを決められたレギュレーションの中で限界まで高速化を図るチューニングバトル、それが ISUCON です。」 ...

ykyk1218
2019年02月26日