皆さまこんばんは、ikaです。ここ最近の仕事面では開発する時間も増えてきて、中々インフラの話で話題になれそうなものがあんまり出てこないなーという矢先の記事です。
なんだかんだ言って、弊社のインフラはまだ問題が山積みなため生々しい声(主に自分の運用の辛さ<-)も含めて書いています。
今日はその中でも今流行りの(?) Kubernetes によるDockerコンテナをクラスタ化の試験運用を行なっているので、その体験談、プラス自社の既存の運用方針と照らし合わせて結局Kubernetesってどうなの?と言う記事を書いてみたいと思います。それではいってみましょう。
コンテナ型仮想化について
Dockerコンテナと言っていますが、そもそもコンテナってなんでしょうか。Dockerの復習となりますが、一般的な仮想化と言えば、KVM や Xen と言ったようなハイパーバイザーがホストOSの中で動いています。このハイパーバイザーがホストOSの上でVM(仮想マシン)を動かせるように制御しています。そのため、VMのOSはホストOSとは違うOS(ゲストOS)を動かすことができ、またゲストOSの中で独立したアプリケーションを動かすことができます(ハイパーバイザーには他のOSで動くものと、専用のOSの上で動くものの二種類があります)。しかし、ハイパーバイザーの上でVMを作成するにはハードウェアのリソースを割り当てる必要があるので、VMの個数が多くなるとリソースの割り当てが難しくなってきます。
一方、Dockerのようなコンテナ型仮想化と呼ばれているものはホストOSの上にコンテナと呼ばれる仮装的なユーザ空間を提供します。ユーザ空間とは、ユーザがアプリケーションを実行するためのリソースが提供される空間です。そのため、ホストOSからはコンテナというものが単純な一つのプロセスに見えるため非常に管理がしやすくもあります。その反面、コンテナはホストOSに依存するユーザ空間が提供されるためゲストOSはホストOSのものと同じでなければいけません(コンテナの例としては他にも jail
や LXC
、もっと言えば chroot
から考えても歴史は古くからあります)。
サーバ仮想化とコンテナ型仮想化の使い分けの話ですが、ハードウェアのリソースが充分にあれば、従来通りVMによるサーバ運用や物理サーバでの運用で良いと思われます。
コンテナとして仮想化する場合は開発環境で使うのが良いかと思われます。案件や会社の予算にもよりますが、開発環境などのリソースを割くのが難しいものにはコンテナ型仮想化はビジネス的にも適しているかと思われますし、なおかつ今後の開発としても環境をコンテナで独立させられるので開発環境を使い回しする場合は使い回しによる影響が少ないはずです(逆に本番環境ではリソースは充分に割いて欲しいのでコンテナ型仮想化による本番環境の運用は基本的には反対しています)。
Kubernetesとは
さて、Dockerが発案されてコンテナ型仮想化が非常に普及してきたのは言うまでもないですが、コンテナを運用していくためには専用のプラットフォームが必要となります。ここで言うプラットフォームが Docker Engine
になります。弊社では
Docker Engine (以下、Dockerホストと呼ぶ) が導入されている Linux OSを何台か運用しその上でたくさんのコンテナが動いています。しかし、Dockerホストの台数が大きくなり、その上で動くコンテナが多くなるに連れて、管理が煩雑化してきてしまっています。そのため、「VMと違ってリソースを割く必要性を感じなくなったのは良いが、コンテナ自体は比較しても多くなっているので結局管理の複雑さはVM運用の時と変わっていない、むしろ余計に複雑になってきている」感覚です。
そこで多少なりとも、現在のDockerホストの運用の手助けになればと思い私が着目したものが Kubernetes です。
Kubernetesとは、Linuxコンテナの操作を自動化するオープンソース・プラットフォームです。KubernetesはLinuxコンテナをまとめてクラスタ化できます。コンテナをクラスタ化することでアプリケーションのデプロイやスケーリングなどをより効率的に、容易に管理できます。
そのため、Kubernetesを用いることでそのような用途で利用する上では非常に役立つでしょう。
...と、ここまでは軽くKubernetesを用いることでできること、クラスタ化することによるメリットから着目した説明です。では、これが弊社のような「リソースを割くまでに至らないが必要なコンテナを手作業で運用」という用途でDocker Engineを利用している場合でも通用するのでしょうか。
私が一番注目したのはやはりコンテナのクラスタ化ができるという所です。これだけでも手作業での運用の管理と比較して、楽になるか調べていきます。
Kubernetes(Minikube)の導入
Kubernetesの導入のために、弊社で運用しているハイパーバイザーから一つVMを生成し、その上で Minikube を導入することにしました。
Minikube とは、Kubernetesを一つのコンピュータで容易に動かせるように設計された簡易版プラットフォームです。
説明不足でしたが、クラスタ化することでアプリケーションデプロイやスケーリングが容易となるというメリットに関して充分に活用しようとするのであれば、コンテナを動かすサーバとKubernetesを動かすサーバは最低限分けて構築する方がいいでしょう。`
いずれにせよ、そもそもそういう形でのKubernetesを使う予定がないため、Minikubeによる簡易版プラットフォームで「Docker管理ツール + Dockerコンテナ」の構成を行います。
Docker Engineの導入
Minikubeを動かすために事前にDockerをホストOSに導入します。
$ sudo apt update
$ sudo apt install apt-transport-https
ca-certificates
curl
software-properties-common
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
$ sudo apt-key fingerprint 0EBFCD88
$ sudo add-apt-repository
"deb [arch=amd64] https://download.docker.com/linux/ubuntu
$(lsb_release -cs)
stable"
$ sudo apt update
$ sudo apt install docker-ce
$ systemctl enable docker
Synchronizing state of docker.service with SysV service script with /lib/systemd/systemd-sysv-install.
Executing: /lib/systemd/systemd-sysv-install enable docker
$ systemctl restart docker
Minikubeの導入
$ sudo su -
$ curl -Lo minikube https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
$ chmod +x minikube
$ mv minikube /usr/bin
$ curl -Lo kubectl https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl
$ chmod +x kubectl
$ kubectl /usr/bin
$ export MINIKUBE_WANTUPDATENOTIFICATION=false
$ export MINIKUBE_WANTREPORTERRORPROMPT=false
$ export MINIKUBE_HOME=$HOME
$ export CHANGE_MINIKUBE_NONE_USER=true
$ mkdir $HOME/.kube || true
$ touch $HOME/.kube/config
$ export KUBECONFIG=$HOME/.kube/config
$ minikube start --vm-driver=none
KubernetesのTOP画面は上記のようになります。KubernetesはGoogleエンジニアによって開発・設計されたため、すごくGoogle的な画面になっていますね。
さて、今度はクラスタ作成とコンテナ作成を行なっていきます。
TechRachoの開発環境をKubernetesで作ってみる
まずは単体のコンテナ一つでクラスタを作るようにします。試しにTechRachoの開発環境をここに作ってみましょう。
(なお、ここでは社内プライベートのDockerレジストリからimageを取得できるように事前にレジストリの設定を行なっております。
レジストリを簡単に作成できるようにコンテナで運用しています: https://hub.docker.com/_/registry/)
apiVersion: v1
kind: Pod
metadata:
name: techracho-dev
labels:
run: techracho-dev
spec:
containers:
- name: techracho-dev
image: "*******:5000/techracho-dev:ver1.0"
command: [ "/usr/bin/supervisord" ]
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
name: http
- containerPort: 22
name: ssh
restartPolicy: Never
上記は techracho-dev
のコンテナを作成します。Kubernetesで作成するコンテナは、Podと呼ばれる単位でNode(Podが動作する物理もしくは仮想マシン)に配置されます。
そのため、依存関係のあるコンテナ同士は同一のPodに所属させて管理します。
今回は techracho-dev 単体なためPodは一つのコンテナだけです。また、コンテナで起動するサービスを後にポートフォワーディングで外部に公開させるようにしたいため、HTTPとSSHのウェルノウンポートを指定しておきます。また、Dockerコンテナの起動時に与えられるコマンドとして supervisord
を与えます。
supervisordで、TechRachoの中で動いているサービスを一元管理しています。
上記のファイルをymlで保存して実行してみましょう。
kubectl apply -f techracho.yml
# kubectl get pods techracho-dev
NAME READY STATUS RESTARTS AGE
techracho-dev 1/1 Running 0 7m
techracho-devという名前のPodに一つコンテナが実行されていることがわかります。
より詳細的な情報が欲しい場合は、
# kubectl describe pods techracho-dev
Name: techracho-dev
Namespace: default
Node: *******/*******
Start Time: Tue, 13 Feb 2018 18:15:18 +0900
Labels: <none>
Annotations: kubectl.kubernetes.io/last-applied-configuration={"apiVersion":"v1","kind":"Pod","metadata":{"annotations":{},"name":"techracho-dev","namespace":"default"},"spec":{"containers":[{"command":["/usr/bin/...
Status: Running
IP: 172.17.0.4
Containers:
techracho-dev:
Container ID: docker://4f06f4b907069cce848dae248ef42d213cc73ab70cfb864e83cf52b842da5a0c
Image: *******:5000/techracho-dev:ver1.0
Image ID: docker-pullable://*******:5000/techracho-dev@sha256:021bdd2c1ac9f1afeef42cea6935d2c9c70adf512c84cc9d0ed74f20e5f4fcf8
Ports: 80/TCP, 22/TCP
Command:
/usr/bin/supervisord
State: Running
Started: Tue, 13 Feb 2018 18:15:19 +0900
Ready: True
Restart Count: 0
Environment: <none>
Mounts:
/var/run/secrets/kubernetes.io/serviceaccount from default-token-5xkpk (ro)
Conditions:
Type Status
Initialized True
Ready True
PodScheduled True
Volumes:
default-token-5xkpk:
Type: Secret (a volume populated by a Secret)
SecretName: default-token-5xkpk
Optional: false
QoS Class: BestEffort
Node-Selectors: <none>
Tolerations: <none>
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 10m default-scheduler Successfully assigned techracho-dev to ********
Normal SuccessfulMountVolume 10m kubelet, ******** MountVolume.SetUp succeeded for volume "default-token-5xkpk"
Normal Pulled 10m kubelet, ******** Container image "*******:5000/techracho-dev:ver1.0" already present on machine
Normal Created 10m kubelet, ******** Created container
Normal Started 10m kubelet, ******** Started container
これだけではまだ、外部からサービスを公開できません。そのため、Kubernetesホストとコンテナとのポートをフォワーディングします。
Serviceの作成
Kubernetesの中で動作しているPodsですが、これを外部から公開するために Service
を作成します。Serviceとは、Kubernetesの定義の一つで、PodsをKubernetes外からアクセスできるように中継したり、Pods間のロードバランサーの機能を持っているようです。
こちらもymlファイルから作成できるため、以下のものを作りました。
apiVersion: v1
kind: Service
metadata:
name: techracho-dev
spec:
selector:
run: techracho-dev
type: NodePort
ports:
- protocol: TCP
port: 10080
targetPort: 80
nodePort: 30080
name: http
- protocol: TCP
port: 10022
targetPort: 22
nodePort: 30022
name: ssh
着目する部分は selector
でServiceに対応するPodsを紐付けます。また、type: NodePort
とすることでホストのIPとKubernetesのIPと紐付けができます。
portsは下記のように用途が分けられています。
port
: localhostからPodへの接続用ポートtargetPort
: コンテナ(Pod)内のポートnodePort
: Kubernetesホスト外部からPodへの接続ポート
簡単に設定するにはnodePort
とtargetPort
さえ指定すれば、Kubernetes外からtechracho-devに接続できそうです(port
だとどうしてもNAPT設定が必要になりますね)。
ただし、NodePort
はあらかじめ利用できるポート番号が決まっているようです。
kubectl apply -f techracho_service.yml
The Service "techracho-dev" is invalid: spec.ports[0].nodePort: Invalid value: 10080: provided port is not in the valid range. The range of valid ports is 30000-32767
(30000-32767だけしか使えないのは少なすぎるような気もしますが、、、)
以上で、KubernetesのホストIPからtechracho-devへのアクセスが可能となります。
http://(kubernerntesが入ったホストのIP):30080/
さて、単体Podでの運用手順はこれくらいにして、今度はクラスタ配下に複数のPodを構築できるような運用をやってみましょう。
複数のPodを構築して運用してみる
弊社のサーバの中でクラスタにできそうなものとして、「同じRailsアプリだが、MySQLは案件毎に違うサーバ」という条件で手順方法を作ってみます。
この条件だと、Kubernetesの同じクラスタ名で所属させたとしても一つ一つは違うDockerイメージが必要になってきます。
そのため、Dockerイメージをその都度作成しないといけないため大変です。
そこで、「Railsアプリを動かす基盤となるシステムだけ先にイメージとして作成」しておき、後々イメージをコミットするようにします。
まずは、基盤となるイメージから同じPodsを二つ作成し、この二つに個々のデータをコピーし、イメージをコミットするようにします。
今回、ここで定義するのは ReplicaSet
と Deployment
の二つです。
ReplicaSet
とは、同じ仕様のPodの複製や指定された数のPodを常に起動できるように管理します。
Deployment
は Replicaset
の作成・管理を行います。
まず、Podを2つ作成するReplicaSetを作成するためのDeploymentを作成します。
設定ファイルは以下のようになります。
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: example
spec:
replicas: 2
template:
metadata:
labels:
run: example
spec:
containers:
- name: example
image: example:latest
command: [ "/usr/bin/supervisord" ]
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
name: http
- containerPort: 22
name: ssh
上記の設定ファイルでDeploymentを作成します。
# kubectl apply -f example_deploy.yml
deployment "example" created
# kubectl get deployments example
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
example 2 2 2 2 49s
# kubectl get pods
NAME READY STATUS RESTARTS AGE
example-59457cf5f9-6mpcc 1/1 Running 0 31m
example-59457cf5f9-f9xlb 1/1 Running 0 31m
techracho-dev 1/1 Running 0 3d
Podが二つ作成されていますね。そしてそのPodを管理するDeploymentも作成されています。
Podに応じて、データを反映させたらイメージを作成します。ここはKubernetesではなくDockerの方でcommitします。
# docker commit 31647b53184c example-dev:ver1.0
# docker commit 1d833a8cb373 example-dev2:ver1.0
commitでイメージを保存したら、最初に作ったdeploymentは削除してしまいましょう。
deploymentを削除すれば、配下にあるPodも自動的に消せます。
# kubectl delete deployment example
deployment "example" deleted
# kubectl get pods
NAME READY STATUS RESTARTS AGE
techracho-dev 1/1 Running 0 3d
commitして更新したイメージでPodを作ります。
- example-dev:ver1.0
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: example-dev
spec:
replicas: 1
template:
metadata:
labels:
app: example-dev
spec:
containers:
- name: example-dev
image: example-dev:latest
command: [ "/usr/bin/supervisord" ]
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
name: http
- containerPort: 22
name: ssh
- example-dev2:ver1.0
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: example-dev2
spec:
replicas: 1
template:
metadata:
labels:
app: example-dev2
spec:
containers:
- name: example-dev2
image: example-dev2:latest
command: [ "/usr/bin/supervisord" ]
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
name: http
- containerPort: 22
name: ssh
2つの独立したPodを作成する事ができました。二つを PodではなくDeploymentで作っている理由は、replicas
の部分で常に起動するPod数を指定するためです。これにより、Podが最低一つは常に起動される事が保証されます。
作成したPodをPort Forwardingで外部から公開しつつ、同じクラスタに配属させます。
apiVersion: v1
kind: Service
metadata:
name: example-dev
spec:
selector:
app: example-dev
type: NodePort
ports:
- protocol: TCP
port: 10022
targetPort: 22
nodePort: 31022
name: ssh
- protocol: TCP
port: 10080
targetPort: 80
nodePort: 31080
name: http
apiVersion: v1
kind: Service
metadata:
name: example-dev2
spec:
selector:
app: example-dev2
type: NodePort
ports:
- protocol: TCP
port: 10022
targetPort: 22
nodePort: 32022
name: ssh
- protocol: TCP
port: 10080
targetPort: 80
nodePort: 32080
name: http
一つ注意する点として、二つのPodで公開しているポート番号が 22, 80と重複していますので、ホストのポート番号と紐付けるために、selector
をPod毎に作成する必要があります。非常に複雑な設定をしていますがこれにより、コンテナポート毎にホストのポートが紐づけられます。
以上の設定で外部からPodにアクセスできます。
- example-dev
http://(kubernerntesが入ったホストのIP):31080/
- example-dev2
http://(kubernerntesが入ったホストのIP):32080/
更に、これらは管理画面より全てGUI上で把握できます。
http://(keubernetesが入ったホストのIP):30000/
こういうのが一目で見えるようになるのは良いですね。GUIからこれまでのステータスを再作成するとなると辛いですが。
まとめ
という訳でKubernetesを使ってみた感想も含めてですが、まずVMを構築・運用するような形で導入しようと考えたのが間違いでした。
まずKubernetesでちゃんと運用できるようにするのであれば、最低3台のサーバが必要になってきます。
- Kubernetesで運用するNodeサーバ
- KubernetesでNodeを制御するコントロールサーバ
- imageを取得するDockerレジストリサーバ(Docker Hubの有料プランでもOK)
正直、弊社のように急にリソースを増やすような提案が難しい場合には、Kubernetesを導入するのは逆に複雑になるような気がします。そのため、まずリソースを考慮するという点で手作業での運用の方が優越です。
更に単体のPodの運用の場合も、やはり手作業での運用の方が有利でしょう。一応、ReplicaSetで常時起動するPodを最低1台にするという運用も可能ですが、それなら docker update --restart=always
で良いでしょう。
一方、同じ環境を複製して操作する事が多い場合は、クラスタ化は非常に有意義です。
今回のような基盤イメージからRailsアプリのイメージをその都度作っていくというような泥臭い事していますが、必要な分をあらかじめ決めて作成できるので精神的にも楽なところがあります。
ただ、今回の条件は正直に言うと凄くマイナー的と言いますか、本来同じ環境、同じデータが入ったものをクラスタ化して、スケーリング管理したいなどのクラスタ化する事で得られる一番メジャーな利点(?)のものを本件で試していないのがちょっと問題あります。
そのため、ここまで読んでいただいて申し訳ないのですが、最初の条件の時点でKubernetesを使うというスキーマにはならないような気もします(そもそもスケーリングしたいのであれば、AWSやGCPの機能で任せた方が良いのでは?という疑問もあったのですが)。
とは言え、今までのサーバ運用がDockerコンテナへ、そしてクラスタ運用がKubernetesでできる事になったため、弊社のような小さな会社でもコストをかけずに運用できるという意味では非常に有意義ではないでしょうか。
今後とも触っていく需要はありそうな気がします。
ところで、FreeBSDのコンテナ仮想化と言えばjail
ですが、jailのクラスタ化で検索したらこのような記事が出てきました。
ちょっと古い記事ではありますが、まだFreeBSD界隈は進んでいない気がする?...ただ、jailは標準の機能としてカーネルに入ったので今後発展していく可能性は大いにある...はず。
20180308 追記
後々、この設定だと致命的な問題があったので追記します。この設定だと Deployment
で example-dev:latest
、example-dev2:latest
の imageからDockerコンテナを作ろうと試みます。
しかし、この設定ではPodsに更新があった差分がimageに反映されないため、Minikubeでは常に古いDocker Imageでコンテナを起動してしまう問題が発生してしまいます(AWSでAMIからEC2を起動する際にもself-deployする手段がないと、古いコードでアプリが動いてしまいますね、感覚としてはそれに似ています)。
そこで、 私の方では cron
で毎日 Docker imageを commitするようにしました。
#!/bin/sh
#
# docker image commit daily
set -e
if [ -x /usr/bin/docker ]; then
container1=$(docker ps -q -f 'name=example_dev_example-dev')
docker commit ${container1} example-dev:latest
container2=$(docker ps -q -f 'name=example_dev2_example-dev2')
docker commit ${container2} example-dev2:latest
fi
exit 0
docker ps -q
でコンテナIDを標準出力として受け取り、変数に代入してcommitするという単純なcronです。
一先ず、これで日毎の差分バックアップは完了しているので、何時までも古い Docker imageでコンテナが起動するということはなくなりました。
ところで、こういうDockerイメージの差分バックアップって皆さんどうしているんでしょうね。
やはり、プライベートなレジストリサーバに毎日イメージ送るのが一番理想的なのでしょうか。
参考文献
- http://cn.teldevice.co.jp/column/detail/id/102
- https://www.redhat.com/ja/topics/containers/what-is-kubernetes
- https://github.com/kubernetes/minikube
- https://qiita.com/nirasan/items/6207cf7ef94e04640fbf
- https://kubernetesbootcamp.github.io/kubernetes-bootcamp