こんにちは。サイオステクノロジー武井です。今回はkubernetesによるコンテナクラスタ管理について書いてみようと思います。
Kubernetesとは?
Kubernetes(クーべネティスと呼ぶようで、略称はk8sと書いたりします)とは、Dockerを始めとするコンテナを管理するオープンソース・ソフトウェアです。具体的に以下のことができます。
- コマンド一発でコンテナを複製し、クラスタ環境を構築できる
- Rolling Update(無停止更新)ができる
- Self-Healingする。障害が発生してコンテナが稼働できなくなると、なんとか既定の台数を維持しようとして、Kubernetesが頑張る。
などなど、アプリケーションを運用するための嬉しい機能がたくさんです。
本ブログでは、実際の開発におけるユースケース(開発、リリース、障害発生等)を想定し、そのユースケースごとにKubernetesがどのように役に立つかを書いてみたいと思います。
Kubernetesの構成
Kubernetesの構成をざっくり解説します。
KubernetesはMasterとNodeの複数台の構成になります。Nodeは実際にコンテナが動作するマシンで、そのNodeたちの状態をMasterが管理しています。さらにkube-proxyやAPI Serverという複数のプロセスが連携して動作する複雑な構成となっています。以下にそれらの構成要素について一つずつ解説します。
API Server
Restのインタフェースを持つプロセスで、Kubernetesのリソース情報(Pod似に関する情報やCluster IPなど)を管理します。kube-proxyやkubeletなどのプロセスは、このAPI Serverを介して、お互いの情報をやり取りします。
etcd
kubernetesの情報を保存する高信頼分散KVSです。API Serverは、etcdに情報を保存します。他にもflannel(Pod間の通信を実現するVXLAN管理サービス)も利用します。
kubelet
Podの生成や停止を行います。定期的にAPI Serverに問い合わせて、生成や停止の数やタイミングを決めています。
scheduler
各Nodeの空き状況や状態を監視して、その結果をAPI Serverを通じて、kubeletに伝える役割を持っています。この情報を参考にして、kubeletはコンテナを生成します。例えば、後述しますが、ReplicaSetで複数台スケールするクラスタを作成したときは、このSchedulerが開いているNodeにコンテナを作成するようkubeletに指示します。そして、コンテナは複数のNodeに配置されます。
kube-proxy
Cluster IPの管理を行います。Cluster IPとは、サービスにアクセスするための代表的なIPアドレスになります。Load BalancerなどのVIPに近いイメージです。このIPアドレスにアクセスすると、そのリクエストは複数のPodに分散されます。kube-proxyのお仕事は、定期的にAPI Server経由でKubernetesのリソース情報にアクセスし、必要に応じてiptableseのルールを作成し、iptablesの機能によって負荷分散を行います。
コマンド群
kubectlなどのコマンド群です。ユーザーのインターフェースになります。このコマンドを使って、クラスタを作成したり削除したりとか、Kubernetesにおける全ての管理をします。実際はAPI Serverにリクエストを投げているます。
Pod
Kubernetesのデプロイの単位になります。Podは複数のコンテナをまとめる論理的な単位です。
実際の開発フローと照らし合わせてみる
さて、ここからは、実際のアプリケーション開発フローの中で、Kubernetesがどのように使われるのか、実際にKubernetesの構築や管理をしながら、説明していきたいと思います。
Kubernetesの出番は、明らかに設計フェーズ以降ですので、ここでは要件定義、基本設計、詳細設計などの設計フェーズは終了していると仮定します。その場合、開発フローは以下のようになるかと思います。
開発環境構築
これから複数のプログラマにガリガリコーディングを行ってもらうために、彼ら専用の開発環境を用意します。Javaの開発で言えば、Eclipseで開発環境構築したり、Gitリポジトリ作成したりです。一般的には、インフラとアプリケーションの両方に精通するエンジニア(以降、アプリケーションアーキテクトと呼称)が担当することが多いです。
この作業は、今回で言えば、開発環境専用のDockerイメージを作成、事前に構築したDocker Private Registry(後述)にPushするといった作業になります。
開発・テスト
プログラマが、事前に用意された開発環境を使ってガリガリコーディングとテストを繰り返す作業です。
この作業は、今回で言えば、先述のDocker Private Registryから開発環境専用のDockerイメージをPullして、自分のPCでガリガリコーディングをするといった感じです。
本番環境構築
開発したアプリケーションを動作させるための本番環境を構築します。今回の要であるKubernetesの構築になります。
リリース
「本番環境構築」のフェーズで構築した環境に、Docker Private RegistryからPullしたイメージをデプロイするだけで、簡単に本番環境が出来上がります。ここでポイントなのは、開発環境と本番環境で同じDockerイメージを利用していることです。Dockerを使う以前のレガシーな開発でよくあった「開発環境では動いたのだけど、本番環境では動かないよー」という問題は解決します。このフェーズを担当するのは「開発環境構築」を担当したアーキテクトになることが多いと思います。
機能追加
ユーザーからの無茶な要望があり、機能追加をするフェーズになります。
再リリース
プログラマが機能追加したアプリケーションをDocker Private RegistryにPush、Kubernetesにデプロイします。
ここでのポイントは、KurbenetesはRolling Updateつまりアプリケーションを無停止で更新が可能ということです。ユーザー影響なく、アプリケーションをリリースできるというのは運用上非常に大きなメリットがあります。
リソース増強
アプリケーションの利用者が増え、レスポンスが遅くなったことへの対応策として、リソースを増強するケースを想定しています。KubernetesはReplicaSetと概念があり、設定ファイルでReplicaSetを増やすだけで、簡単にスケールアウトが可能です。指定したReplicaSetの数を維持するよう、Podが各ノードに立ち上がります。
上記のフローをイメージとしたのは以下の図になります。
今回開発するアプリケーション
先程説明した開発フローの中で開発するアプリケーションは以下の様なものになります。
<?php echo "Server IP address\n\n"; echo $_SERVER['SERVER_ADDR']; ?>PHPで記述した簡単なアプリケーションで、このアプリケーションが動作しているサーバーのIPアドレスを表示するものです。後ほど、Cluseter IPを使った負荷分散の設定を行いますが、これであれば、きちんと負荷分散されていることを確認できます。
今回構築する環境
本ブログを記載するために、今回Azure上に以下の環境を構築します。
Master
KubernetesのMasterです。Nodeを管理します。node01及びnode02
Kubernetesのnodeです。先程紹介したPHPのアプリケーションを、Docker公式のhttpsdDocker Private Registry
DockerのPrivate Registryになります。Kubernetesは、Registry上のDockerイメージをPullして、デプロイを行い、Node上にコンテナを配置します。なので、何らかのRegistryが必要なのです。Dockerには公式のRegistryであるDocker Hubがありますが、作成したDockerイメージをPublicなところに上げるのが抵抗あることが多いと思いますので、今回はPrivateなRegistryを立てます。開発環境構築
それではいよいよ開発フローに基づき、Kubernetesを利用していきます。まずは開発環境構築です。
Docker Private Registryを構築します。docker-distributionをインストールします。
# yum install docker-distribution設定ファイルを修正して、Docker Private Registryが稼働するIPアドレスとポート番号を定義します。
# vi /etc/docker-distribution/registry/config.yml addr: :5000 ↓ addr: 10.4.4.8:5000docker-distributionを起動し、さらにOS起動時に自動起動しておくようにします。
# systemctl start docker-distribution # systemctl enable docker-distributionこれで、Docker Private Registryの構築は完了です。
次に、開発環境用のDockerイメージを作成します。Dockerの稼働している環境で以下のDockerfileを作成します。
FROM php:apacheApache上でPHPが動作するDockerイメージをDocker HubからPullします。実際はもっと色々なものをインストールしたりして、複雑な開発環境になると思いますが、ここでは説明を簡略化するために、上記のように単純にしております。
次に、このイメージをDocker Private RegistryにPushします。
# docker tag web 10.4.4.8:5000/web:dev # docker push 10.4.4.8:5000/web:devイメージ名は、必ず上記のように
Docker Private RegistryのIPアドレス:ポート番号/イメージ名:タグ名
でなくてはなりません。
これで開発環境の構築は完了です。
開発・テスト
次にプログラマが開発・テストを行うケースを想定します。
プログラマは、まず、Docker Private Registryから開発環境用のDockerイメージをPullします。プログラマの開発環境用のPC(以降、開発PCと呼称)には、Docker及びDocker Composeがインストールされていることが条件になります。開発PCの任意のディレクトリでdocker-compose.ymlという名前のファイル名を作成し、以下のような内容を記述します。
web: image: 10.4.4.8:5000/web:dev # (1) container_name: "web" ports: - "80:80" # (2) volumes: - "$PWD/src:/var/www/html" (3)(1)では、Docker Private Registryに上げた開発用のDockerイメージをPullしています。
(2)では、ホストの80番ポートからコンテナの80番ポートにポートフォワードしております。内部のWebサーバーにアクセスするためです。
(3)は、ホストのsrcディレクトリをコンテナ内の/var/www/htmlにマウントしています。こうすることで、ホストのsrcディレクトリにPHPのファイルを置けば、すぐに実行可能になります。ADDやCOPYでは、ソースを変更するためにビルドのし直しが発生するのでかなり不便になります。
次に、srcディレクトリ配下にtest.phpという名前で以下の内容のファイルを作成します。
<?php echo "Server IP address\n\n"; echo $_SERVER['SERVER_ADDR']; ?>test.phpにcurlコマンドでアクセスして、以下のようになることを確認します。
# curl localhost/test.php Server IP address [コンテナのIPアドレス]このソースをソースコード管理リポジトリにPushします。実際の案件では、開発したソースは何らかのソースコード管理リポジトリ(Gitやsvn)にて管理することが通常ですが、本記事では、そのあたりの説明は割愛させて頂きます。
本番環境構築
Kubernetesの本番環境を構築します。
はじめにMasterでの作業です。Kubertenes、etcd、flannelをインストールします。
# yum install etcd kubernetes flannel/etc/hostsにMaster、Node01、Node02のホスト名を定義します。
10.4.4.4 master 10.4.4.5 node01 10.4.4.7 node02/etc/etcd/etcd.confを以下のように修正し、etcdがどのクライアントからでも接続を受け付けるようにします。(実際の本番環境では、適宜適切に制限を行って下さい)
ETCD_LISTEN_CLIENT_URLS="https://localhost:2379" ↓ ETCD_LISTEN_CLIENT_URLS="https://0.0.0.0:2379"etcdを起動し、OS再起動後も自動起動するようにします。
# systemctl start etcd # systemctl enable etcd次に、flannelの設定を行います。flannelは詳細な説明はここでは割愛させて頂きます(後日、別記事でご紹介します)。Pod間の通信をVXLANという方式で実現するためのソフトフェアです。ここでは、Pod全体のネットワークを172.30.0.0/16、Pod内部のネットワークのIPアドレスのレンジを24ビットで構築するということを前提とします。
/etc/sysconfig/flanneld内に定義されているFLANNEL_ETCD_PREFIXの値を確認します。
FLANNEL_ETCD_PREFIX="/atomic.io/network"上記の値を覚えておいた上で、etcdに以下の設定を登録します。
# etcdctl mk /atomic.io/network/config '{"Network":"172.30.0.0/16", "SubnetLen":24, "Backend":{"Type":"vxlan"} }'flannelを起動し、OS再起動後も自動起動するようにします。
# systemctl start flanneld # systemctl enable flanneldこれでflannelの設定は完了です。
次にAPI Serverと各種コンポーネントが通信するために必要な鍵を作成します。
# openssl genrsa -out /etc/kubernetes/serviceaccount.key 2048/etc/kubernetes/apiserverを修正して、以下のように変更します。
KUBE_API_ADDRESS="--insecure-bind-address=127.0.0.1" ↓ KUBE_API_ADDRESS="--address=10.4.4.4" ... KUBE_API_ARGS="" ↓ KUBE_API_ARGS="--service_account_key_file=/etc/kubernetes/serviceaccount.key"API Serverの稼働するサーバーのIPアドレスを指定し、合わせて先程作成した鍵のパスも指定します。
/etc/kubernetes/controller-managerを以下のように修正します。
KUBE_CONTROLLER_MANAGER_ARGS="--service_account_private_key_file=”" ↓ KUBE_CONTROLLER_MANAGER_ARGS="--service_account_private_key_file=”/etc/kubernetes/serviceaccount.key"先程作成した鍵のパスを指定します。
/etc/kubernetes/configを以下のように修正します。
KUBE_MASTER="--master=https://127.0.0.1:8080" ↓ KUBE_MASTER="--master=https://master:8080"各種コンポーネントの起動、OS再起動後の自動起動設定を行います。
# systemctl start kube-apiserver # systemctl start kube-controller-manager # systemctl start kube-scheduler # systemctl start kube-proxy # systemctl enable kube-apiserver # systemctl enable kube-controller-manager # systemctl enable kube-scheduler # systemctl enable kube-proxyKubernetesのコマンドラインツールkubectlの設定を行います。
$ kubectl config set-credentials myself --username=admin --password=<適当なパスワード> $ kubectl config set-cluster local-server --server=https://master:8080 $ kubectl config set-context default-context --cluster=local-server --user=myself $ kubectl config use-context default-context $ kubectl config set contexts.default-context.namespace defaultこれでMasterの設定は完了です。
次に、Node01、Node02の設定を行います。2つのNodeで同じ設定を行います。
/etc/hostsにMaster、Node01、Node02のホスト名を定義します。
10.4.4.4 master 10.4.4.5 node01 10.4.4.7 node02/etc/sysconfig/dockerに以下の設定を追加します。
INSECURE_REGISTRY='--insecure-registry registry.access.redhat.com --insecure-registry 10.4.4.8:5000'これは、先程構築したDocker Private Registryにアクセスするための設定になります。
Docker、Kurbernetes、flannelをインストールします。
# yum install docker kubernetes flannel/etc/sysconfig/flanneldを以下のように修正します。
FLANNEL_ETCD_ENDPOINTS="https://127.0.0.1:2379" ↓ FLANNEL_ETCD_ENDPOINTS="https://master:2379"これは、flannelが参照するetcdのアクセス情報を設定しています。flannelは自身の設定情報をetcdに保存する仕様なのです。
flannel、dockerを起動し、OS再起動後の自動起動設定をします。
# systemctl start flanneld # systemctl enable flanneld # systemctl start docker # systemctl enable docker/etc/kubernetes/config
KUBE_MASTER=”–master=https://127.0.0.1:8080″
↓
KUBE_MASTER=”–master=https://master:8080″/etc/kubernetes/kubeletを以下のように修正します。
KUBELET_ADDRESS="--address=127.0.0.1" ↓ KUBELET_ADDRESS="--address=各NodeのIPアドレス" ... KUBELET_HOSTNAME="--hostname-override=127.0.0.1" ↓ KUBELET_HOSTNAME="--hostname-override=" ... KUBELET_API_SERVER="--api-servers=https://127.0.0.1:8080" ↓ KUBELET_API_SERVER="--api-servers=https://master:8080"それぞれkubletの稼働するサーバーのIPアドレス、ホスト名、API Serverへの接続先を定義しています。
kube-proxy、kubletを起動し、OS再起動後の自動起動設定をします。
# systemctl start kube-proxy # systemctl enable kube-proxy # systemctl start kubelet # systemctl enable kubelet以上で、Node01、Node02の設定が完了し、本番環境構築が終わりました。
リリース
先程作成したアプリケーションを本番環境にリリースします。
本番稼働用のイメージを作成します。Dockerの稼働している環境の任意のディレクトリで、以下のDockerfileを作成します。
FROM 10.4.4.8:5000/web:dev COPY src/test.php /var/www/htmlこれは、先程作成した開発用のDockerイメージをPullして、test.phpのソースをコンテナにコピーしているという内容になります。
このイメージをビルドし、Docker Private Registryにアップロードします。
# docker build -t 10.4.4.8:5000/web:1.0 . # docker push 10.4.4.8:5000/web:1.01.0というタグを付けました。これは、このアプリケーションのリリースバージョンになります。以後、機能追加等でリリースをするたびに、このバージョン番号を増やしていきます。
次に、Masterにて、先程作成したイメージをKubernetesにデプロイする作業を行います。Masterの任意の場所で、deploy.ymlというファイル名の以下の内容のファイルを作成します。
apiVersion: extensions/v1beta1 kind: Deployment # (1) metadata: name: web-deployment # (2) spec: replicas: 1 # (3) template: metadata: labels: app: web # (4) spec: containers: - name: web # (5) image: 10.4.4.8:5000/web:1.0 # (6) ports: - containerPort: 80 # (7)Kubernetesの設定ファイルは一般的にYAMLで記載することが多いです。今回もそのようにしています。Kubernetesの設定は、このような設定ファイルを作成し、kubectlでこのファイルを引数に指定するという作業になります。
(1)は、設定ファイルの種別を表します。この設定ファイルはでデプロイするための設定なので、Deploymetを記述します。
(2)は、このデプロイの設定を一意に識別するためのキーになります。
(3)は、Podを起動する数です。この値に基づき、Podが各Nodeに作成されます。リリース所期時のPod数は、まず1とします。適宜、利用者のアクセスが増え次第、この値を変更していきます。
(4)は、このデプロイを適用するコンテナのラベルになります。(5)でコンテナのラベルを定義していますが、ここで定義したラベルのコンテナがデプロイ対象ということになります。
(5)は、Pod内に作成するコンテナのコンテナ名です。
(6)は、Pod内に作成するコンテナのイメージです。先程作成したアプリケーションのイメージのバージョン1.0を指定していることがわかります。
(7)は、Pod内で開放するポート番号です。HTTP経由でアクセスさせるので、80を指定しています。
kubectlコマンドで、いよいよKubernetesにデプロイします。
# kubectl create -f deploy.ymlこれで終わりです。確認するためには以下のようにします。
$ kubectl get pods NAME READY STATUS RESTARTS AGE web-deployment-2835659986-88mf6 1/1 Running 0 2h web-deployment-2835659986-bp4tk 1/1 Running 1 13h2つのPodが作成されていることが確認できます。Statusが2つとも「Running」となっていればOKです。
しかし、これだけでは、サービスにアクセスできません。このサービスにアクセスするためには、Nodeのインターフェース(IPアドレスで言えば、10.4.4.5、10.4.4.7)を通じて、アクセスできるようにする必要があり、このための仕組みがKubernetesには用意されています。Serviceという仕組みです。
Serviceを作成すると、上記のように仮想的なIPアドレス(10.254.108.241)、port、nodePort、targetPortが作成されます。外部からアクセスする場合は、まず、eth0のtargetPort(3000)にアクセスし、Serviceインターフェースのport(80)を通じて、PodのインターフェースのtargetPort(80)にアクセスします。このあたりの処理は、iptablesのNATによって実現されておりますが、複雑な処理ですので、ここでは詳細を割愛させて頂きます。
このServiceを作成するためには、service.ymlというファイル名(ファイル名は任意)で、以下のようなファイルを作成します。
apiVersion: v1 kind: Service # (1) metadata: name: web-np # (2) spec: selector: app: web # (3) type: NodePort # (4) ports: - protocol: TCP port: 80 # (5) targetPort: 80 # (6) nodePort: 30000 # (7)(1)は、種別を指定しています。ここではServiceを定義しますので、Serviceとします。
(2)は、このServiceを一意に識別するための名称です。任意の名前でOKです。
(3)は、ラベルを指定しています。ここで指定したラベルがService経由でアクセスする対象のPodになります。デプロイする時にPodに「app: web」というラベルを指定したので、その値をここで指定します。
(4)は、Serviceのタイプを定義します。Nodeのインターフェース経由でもアクセスさせたい場合は、「NodePort」と指定します。
(5)、(6)、(7)はそれぞれ、先のServiceのイメージ図で示したport、targetPort、nodePortになります。
確認方法は以下になります。
$ kubectl get services NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes 10.254.0.1 443/TCP 13d web-np 10.254.105.2 80:30000/TCP 23m先程指定したweb-npの値が上記のように表示されていればOKです。
では、Masterからアクセス確認してみましょう。
$ curl 10.4.4.5:30000/test.php Server IP address 172.30.98.2上記のように表示されていてばOKです。
これでリリースは完了です。
機能追加
先程作成したアプリケーションに対して、何らかの機能追加要望があったとします。
test.phpを以下のように修正します。
<?php echo "v2.0"; echo "Server IP address\n\n"; echo $_SERVER['SERVER_ADDR']; ?>冒頭にv2.0と表示するようにしました。こちらをソースコード管理システムにPushします。実際の案件では、開発したソースは何らかのソースコード管理リポジトリ(Gitやsvn)にて管理することが通常ですが、本記事では、そのあたりの説明は割愛させて頂きます。
再リリース
先程機能追加したアプリケーションを再リリースします。
このイメージをビルドし、Docker Private Registryにアップロードします。
「リリース」の章でリリースした方法と基本的には変わりません。test.phpのファイルを新しいのに入れ替えて、以下のコマンドを実行します。# docker build -t 10.4.4.8:5000/web:2.0 . # docker push 10.4.4.8:5000/web:2.0tagに記載のバージョンを2.0に変更しています。これで、バージョン2.0のタグがついたDockerイメージがDocker Private ReigstryにPushされます。
次に、Masterにて、最初のリリース時に利用したdeploy.ymlを以下のように修正します。
apiVersion: extensions/v1beta1 kind: Deployment metadata: name: web-deployment spec: replicas: 1 template: metadata: labels: app: web spec: containers: - name: web image: 10.4.4.8:5000/web:2.0 ports: - containerPort: 80imageに指定するDockerイメージのタグを2.0(先程Docker Private RegistryにPushしたもの)に変更をしています。
そしてこの設定を以下のコマンドでKubernentesに適用します。
$ kubectl apply -f deploy.yaml動作確認を実施します。
$ curl 10.4.4.5:30000/test.php v2.0 Server IP address 172.30.98.2v2.0が冒頭に表示されるようになりました。上記のように表示されていればOKです。
このとき、Rolling Update(無停止更新)が行われています。サービスの停止なく、リリースができるのは、嬉しい限りですね。
内部的な処理としては、新しいバージョンのPodの作成を先に行い、順次、切り替えていくらしいですが、詳細は、私もよくわかっておりません^^;
リソース増強
幸いなことに、サービスの利用者が増え、レスポンスが遅くなってきたので、リソースを増強することにしました。今まで1台のPodで動かしていましたが、2台のPodで動かすようにしましょう。
リリース時に利用したdeploy.ymlを以下のように修正します。
apiVersion: extensions/v1beta1 kind: Deployment metadata: name: web-deployment spec: replicas: 2 template: metadata: labels: app: web spec: containers: - name: web image: 10.4.4.8:5000/web:2.0 ports: - containerPort: 80変更点はreplicasです。1から2に変更しました。これで2台のPodで稼働するようになります。早速いつものように適用してみましょう。
$ kubectl apply -f deploy.yaml本当にPodが2台で稼働しているかどうか確認してみます。
$ kubectl get pods NAME READY STATUS RESTARTS AGE web-deployment-2917055699-h2jm6 1/1 Running 0 16m web-deployment-2917055699-m87st 1/1 Running 0 16m上記のように表示されていればOKです。
では、実際にサービスにアクセスしてみましょう。
$ curl 10.4.4.5:30000/test.php v2.0 Server IP address 172.30.2.2もう一度アクセスしてみます。
$ curl 10.4.4.5:30000/test.php v2.0 Server IP address 172.30.98.2一度目とはIPアドレスが異なってますね。つまり、適切にロードバランスされていることがわかりました(^o^)
この時のイメージは以下の通りになります。
labelにapp:webが付与されているPodがServiceの配下にぶら下がりロードバランスされます。node01もしくはnode02どちらのインターフェースにアクセスしても2台のPodに負荷分散されます。
最後に
いかがでしたしょうか?実際の開発フローに合わせて、Kubernetesの利用ケースを書いてみました。皆さんのお役に立てれば幸いです。