Kubernetes実践入門。基本的なyamlとコマンドから学ぶサービス運用効率化術

サイバーエージェントで社内のプライベートクラウド構築に携わるほか、Kubernetesのマネージドサービスもオンプレミス上で実装して提供している青山真也(@amsy810)と申します。外部でもKubernetesの仕事を複数行っているほか、コミュニティ活動、DockerやKubernetesに関する本を2冊執筆するなど積極的に布教活動を行っています。

以前「Dockerとコンテナがざっくりわかる!『Kubernetes完全ガイド』青山さんにFLEXYの麻衣子お姉さんが聞く! #Docker編」でDockerとKubernetesの基本についてお伝えしました。今回はさらに次のステップに進めるような内容をお伝えします。

ユーザーの追加・表示をするGo製アプリケーションを作ってみる

今回題材にするのは、簡易的なGoのアプリケーションです。中身はシンプルなもので、メイン関数で8080番ポートにリクエストが来たときのハンドラを用意しています。それぞれのパスに関するハンドラは、ルート直下の / は200を返すだけのもので、今回実際に利用するのは/getuserと/adduserの2種類のパスに対応する関数です。

addUserは新規のユーザーを1つ追加する動作をさせます。go-randomdataというライブラリを使い、ランダムな名前を生成してデータベースに追加します。また、同時にデータベースにIDが自動採番されるようになっています。

getUser関数は名前の通り、ユーザー名を取得してきて表示します。その際は、データベースに登録されている複数のユーザー名からランダムに選択した1件のidとnameを取得します。

Goで用意したアプリケーションはこちらです。

package main

import (
"database/sql"
"fmt"
"github.com/Pallinder/go-randomdata"
_ "github.com/go-sql-driver/mysql"
"net/http"
"os"
)

var (
dbuser string
dbpass string
dbhost string
dbport string
dbname string
hostname string
)

func rootHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "/ is requested")
}

func getUserHandler(w http.ResponseWriter, r *http.Request) {
id, name := getUser()
fmt.Fprintf(w, fmt.Sprintf("%s: name is [%d %s]\n", hostname, id, name))
fmt.Printf(fmt.Sprintf("%s: name is [%d %s]\n", hostname, id, name))

}

func addUserHandler(w http.ResponseWriter, r *http.Request) {
name := addUser()
fmt.Fprintf(w, fmt.Sprintf("%s: added user [%s]\n", hostname, name))
fmt.Printf(fmt.Sprintf("%s: added user [%s]\n", hostname, name))

}

func main() {
getFromEnv()
http.HandleFunc("/", rootHandler)
http.HandleFunc("/getuser", getUserHandler)
http.HandleFunc("/adduser", addUserHandler)
http.ListenAndServe(":8080", nil)
}

func getFromEnv() {
dbuser = os.Getenv("DBUSER")
dbpass = os.Getenv("DBPASS")
dbhost = os.Getenv("DBHOST")
dbport = os.Getenv("DBPORT")
dbname = os.Getenv("DBNAME")
hostname, _ = os.Hostname()
}

func getUser() (int, string) {
db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", dbuser, dbpass, dbhost, dbport, dbname))
if err != nil {
panic(err.Error())
}
defer db.Close()

stmtOut, err := db.Prepare("SELECT id,name FROM users ORDER BY RAND() LIMIT 1;")
if err != nil {
panic(err.Error())
}
defer stmtOut.Close()

var name string
var id int

err = stmtOut.QueryRow().Scan(&id, &name)
if err != nil {
panic(err.Error())
}

return id, name
}

func addUser() string {
db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", dbuser, dbpass, dbhost, dbport, dbname))
if err != nil {
return err.Error()
}
defer db.Close()

stmtIns, err := db.Prepare("INSERT INTO users(name) VALUES(?)")
if err != nil {
return err.Error()
}
defer stmtIns.Close()

name := randomdata.SillyName()
_, err = stmtIns.Exec(name)
if err != nil {
return err.Error()
}

return name
}

コンテナに対応しやすいアプリケーションを作るために、データベースのユーザー名・パスワード・ポートなどは環境変数で指定するようにしてあります。

このアプリケーションをコンテナ化するためにまず下記のようなDockerfileを作成します。なお、今回は Go 1.13をベースイメージとして利用します。

FROM golang:1.13

WORKDIR /go/src/app
COPY ./main.go .

RUN go get -d -v github.com/go-sql-driver/mysql
RUN go get -d -v github.com/Pallinder/go-randomdata

RUN go build -o /app ./main.go

CMD ["/app"]

今回のファイルはGitHub上で公開しているのでご覧ください。

Dockerfileが書けたあとは、いよいよコンテナイメージのビルドを行います。ビルドはdocker buildコマンドを利用します。ビルドの完了後、それをコンテナレジストリに追加すればネットワーク経由で使えるようになります。 コンテナのイメージ作りはこれで完成です。このイメージがあればGKEでもローカル環境でもどこでも同じものを動かすことができます。

Kubernetesの基本的な使い方

ここからKubernetesが登場です。Kubernetesでコンテナをいい感じに配置したりしていきます。まずはスモールスタートで先ほどのアプリケーションを動かしていきましょう。

Podを作る

Kubernetesの最小単位のリソースはPodです。1つのPodの中にコンテナを1つだけ入れる 場合もあれば、複数のコンテナが入っている場合もあります。複数のコンテナを入れる際には、 同じコンテナを複数組み合わせるのではなく、役割が違うものを組み合わせて使います。

VM的な考え方と照らし合わせると、PodはVM、コンテナはプロセスに相当する関係と言えま す。Podとコンテナの関係は、VMとプロセスの考え方に近いのです。そのため、初心者はこの 考え方に基づいたマニフェストになりがち。ここがポイントになるのですが、実はこういう 使い方をすることはKubernetesでは良い使い方ではないのです。

よくある例では、アプリケーションサーバとデータベースが一緒に入ってしまっているパターンで、VM時代であれば1台の中にデータベースもアプリケーションも入れてしまっているのと同じような使い方をしてしまう形ですね。Kubernetesでは「こういった使い方は良くない」のですが、今回はとっつきやすさを優先した第一歩ということで、このパターンでやっていきましょう。

Podにはコンテナを複数入れられるので、アプリケーションコンテナとMySQLのコンテナを用意し、Pod定義の中にコンテナを追加していくことで同等の構成がとれます。ちょうどVMの中に2つのプロセスを起動させるのと同じです。 アプリケーションコンテナには環境変数を用意し、どこのデータベースに接続しにいくかの情報を持たせています。VMの場合と同じように、データベースの認証情報やホスト情報を指定します。

yaml形式で書かれたマニフェストファイルで示すとこのようになります。

---
apiVersion: v1
kind: ConfigMap
metadata:
name: init-db-sql
data:
create_usertable.sql: |
CREATE TABLE IF NOT EXISTS mydb.users (id INT AUTO_INCREMENT NOT NULL PRIMARY KEY, name VARCHAR(50));
---
apiVersion: v1
kind: Pod
metadata:
name: sample-pod
labels:
role: all-in-one
spec:
containers:
- name: app-container
image: masayaaoyama/flexy-demo-app:v1.0
imagePullPolicy: Always
env:
- name: DBHOST
value: 127.0.0.1
- name: DBPORT
value: "3306"
- name: DBUSER
value: myuser
- name: DBPASS
value: mypass
- name: DBNAME
value: mydb
- name: mysql-container
image: mysql:8.0
env:
- name: MYSQL_ROOT_PASSWORD
value: rootpass
- name: MYSQL_DATABASE
value: mydb
- name: MYSQL_USER
value: myuser
- name: MYSQL_PASSWORD
value: mypass
volumeMounts:
- name: init-sql-configmap
mountPath: /docker-entrypoint-initdb.d
volumes:
- name: init-sql-configmap
configMap:
name: init-db-sql
---
apiVersion: v1
kind: Service
metadata:
name: flexy-demo-all-in-one
spec:
type: LoadBalancer
ports:
- name: "http-port"
protocol: "TCP"
port: 8080
targetPort: 8080
selector:
role: all-in-one

今回の場合だと、localhostを指定して接続するようにしています。

Kubernetes01
アプリケーションコンテナからlocalhostの3306ポートでMySQLに接続させます。

なお、MySQLのコンテナはわざわざ自分でイメージを作るのではなく、公式に提供されているイメージを使います。コンテナはこのような公式のイメージがいろいろと提供されており、なにか機能を使いたいときには自ら用意しなくても公式のイメージを利用できるのは利点の一つです。もちろん、そのイメージが脆弱なものでないかは利用前に確認は必ず行いましょう。

今回のMySQLのコンテナは、rootのパスワードや起動初期に作るデータベース、ユーザー・パスワードなどは環境変数として指定できるようになっています。そのため、今回の例だとMySQLコンテナを起動した段階でmydbというデータベースが作られ、myuserがmypassというパスワードを持って接続できるようになります。

次に、volumeMountsという部分に注目してみましょう。これはMySQLコンテナが起動するタイミングで、MySQLコンテナの /docker-entrypoint-initdb.d にこのボリュームファイルを関連付ける設定です。これによって指定のボリュームの中身をマウントしてくれます。どのファイルを使うかは、ConfigMapにて指定します。

今回の例だと、create_usertable.sql を指定しているように、コンテナ起動時にMySQLのテーブルを作って、アプリケーションコンテナからそこにアクセスできる状況を作り出しています。

本来であればアプリケーションのマイグレーションもあるので、適切なやり方ではありませんが、今回は簡単なサンプルのためこのようにしています。

また、KubernetesにはServiceというリソースがあります。いろいろな種類がありますが、今回はL4のロードバランサーを使っています。このロードバランサーで8080できたリクエストをコンテナのポート、今回は同じく8080に転送します。

どのPodに対してリクエストを転送するかはselectorで指定します。

Kubernetes02
それでは、Kubernetesで作ったマニフェスト使ってデプロイしていきます。まず最初にKubernetesのノードと、その上で動いているPodを確認してみます。

$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
gke-flexy-demo-default-pool-5d473a6b-3pz7 Ready 8m28s v1.15.4-gke.22
gke-flexy-demo-default-pool-5d473a6b-h3s5 Ready 8m22s v1.15.4-gke.22
gke-flexy-demo-default-pool-5d473a6b-l580 Ready 8m28s v1.15.4-gke.22

$ kubectl get pods
No resources found in default namespace.

この段階ではまだPodはありません。

いよいよ作成したマニフェストをKubernetesに登録します。

$ kubectl apply -f 1_pod.yaml
configmap/init-db-sql created
pod/sample-pod created
service/flexy-demo-all-in-one created

$ kubectl get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
flexy-demo-all-in-one LoadBalancer XXX.XXX.XXX.XXX XXX.XXX.XXX.XXX 8080:31945/TCP 2m45s
kubernetes ClusterIP XXX.XXX.XXX.XXX 443/TCP 12m

$ kubectl get pods
NAME READY STATUS RESTARTS AGE
sample-pod 2/2 Running 0 5m22s

すると、2つのコンテナが入ったPodが1個立ち上がってきます。合わせてロードバランサーも払い出され、インターネットからアクセス可能なグローバルIPアドレスも払い出されています。

次にユーザーの追加と削除を試します。最初の時点ではユーザーは一切登録されていないため、ブラウザなどから http://XXX.XXX.XXX.XXX:8080/adduser に5回アクセスして初期ユーザーを作成しましょう。 その後、以下のように http://XXX.XXX.XXX.XXX:8080/getuser に対して十分な数のリクエストを送ると、5種類のランダムなユーザーが生成されていることが確認できます。

$ for i in `seq 1 100`; do curl -s http://XXX.XXX.XXX.XXX:8080/getuser; done | sort | uniq
sample-pod: name is [1 Gorillascythe]
sample-pod: name is [2 Cubrowan]
sample-pod: name is [3 Chintabby]
sample-pod: name is [4 Cloudbutter]
sample-pod: name is [5 Samurailily]

ステップ1の実験が終わったら一旦不要なものは削除します。

$ kubectl delete -f 1_pod.yaml
configmap "init-db-sql" deleted
pod "sample-pod" deleted
service "flexy-demo-all-in-one" deleted

ここまでが最小構成のものです。

Deploymentでスケーリング

Podが1個だけでは何もうまみが感じられません。スケーリングさせることにより、利便性が出てきます。その際、アプリケーションとデータベースの機能の両方ともをスケールさせる機会は少ないでしょう。アプリケーションはいくつ、データベースはいくつと、それぞれ分割して考えていくことでしょう。そのため、アプリケーションとデータベースのコンテナは別々のPodにするべきです。

設定をマニフェストに落とし込むとこのようになります。

---
apiVersion: apps/v1
kind: Deployment
metadata:
name: flexy-demo-app
spec:
replicas: 1
selector:
matchLabels:
role: app
template:
metadata:
labels:
role: app
spec:
containers:
- name: app-container
image: masayaaoyama/flexy-demo-app:v1.0
imagePullPolicy: Always
env:
- name: DBHOST
value: mysql.default.svc.cluster.local
- name: DBPORT
value: "3306"
- name: DBUSER
value: myuser
- name: DBPASS
value: mypass
- name: DBNAME
value: mydb
---
apiVersion: v1
kind: Service
metadata:
name: flexy-demo-app
spec:
type: LoadBalancer
ports:
- name: "http-port"
protocol: "TCP"
port: 8080
targetPort: 8080
selector:
role: app
---
apiVersion: v1
kind: ConfigMap
metadata:
name: init-db-sql
data:
create_usertable.sql: |
CREATE TABLE IF NOT EXISTS mydb.users (id INT AUTO_INCREMENT NOT NULL PRIMARY KEY, name VARCHAR(50));
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: mysql
spec:
replicas: 1
selector:
matchLabels:
role: db
template:
metadata:
labels:
role: db
spec:
containers:
- name: mysql-container
image: mysql:8.0
env:
- name: MYSQL_ROOT_PASSWORD
value: rootpass
- name: MYSQL_DATABASE
value: mydb
- name: MYSQL_USER
value: myuser
- name: MYSQL_PASSWORD
value: mypass
volumeMounts:
- name: init-sql-configmap
mountPath: /docker-entrypoint-initdb.d
volumes:
- name: init-sql-configmap
configMap:
name: init-db-sql
---
apiVersion: v1
kind: Service
metadata:
name: mysql
spec:
type: ClusterIP
ports:
- name: "mysql-port"
protocol: "TCP"
port: 3306
targetPort: 3306
selector:
role: db

よく見ると先ほどのものと書いてある内容は大きく変わっておらず、アプリケーションとデータベースがそれぞれ分割されて細かく構造化されていったものです。また、先程はPodだったものが、Deploymentに変わっています。このDeploymentは、templateで指定されたPodの構成を複製するための機能になります。VMの場合は、インスタンスグループに近いイメージですね。Deploymentでは、レプリカの数を指定したり下記の図のようにどのPodをカウントするのかをselectorで指定します。templateにはPodを起動するときにrole:appラベルが付与されて起動してくるように指定されているので正しくカウントできるようになっています。

Kubernetes03
さてここで懸念点が発生します。アプリケーション用のコンテナから、データベース用のコンテナに対してはどのように接続したら良いのでしょうか。

最初の段階ではlocalhost宛に接続していましたが、今はもう同じPod内にはデータベースがいません。ここでもServiceを用いたロードバランサーを利用していきます。先程はグローバルIPアドレスを持ったロードバランサーを作成しましたが、実はKubernetesクラスタ内のみで利用可能な内部ロードバランサーを作成することも可能です。外部向けのロードバランサーを作成するときはServiceの設定時に spec.type: LoadBalancer、内部向けの場合には spec.type: ClusterIP を指定しましょう。また、内部向けロードバランサーの場合には、払い出されたIPアドレスの名前解決も行えるようになっています。いちいちIPアドレスを確認する必要がなくなるため、積極的に利用しましょう。

今回の場合には、アプリケーションから接続するデータベースの接続先はmysql.default.svc.cluster.localです。この名前の解釈の仕方は、先頭の「mysql」はMySQLのデータベースに対応するServiceの名前であることを示し、その後ろにネームスペース、その後に svc.cluster.local と続きます。

Kubernetes04
では実際に試してみましょう。

$ kubectl apply -f 2_deployment.yaml
deployment.apps/flexy-demo-app created
service/flexy-demo-app created
configmap/init-db-sql created
deployment.apps/mysql created
service/mysql created

$ kubectl get pods
NAME READY STATUS RESTARTS AGE
flexy-demo-app-5cb4bb64cd-md6zr 1/1 Running 0 8m36s
mysql-5dd5b5c644-vtsjp 1/1 Running 0 8m35s

$ kubectl get service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
flexy-demo-app LoadBalancer XXX.XXX.XXX.XXX XXX.XXX.XXX.XXX 8080:32591/TCP 8m37s
kubernetes ClusterIP XXX.XXX.XXX.XXX 443/TCP 24m
mysql ClusterIP XXX.XXX.XXX.XXX 3306/TCP 8m37s

このように2つのPodとServiceが立ち上がります。

今回1つのPodに対して1つのコンテナしか入ってないので「1/1」の表示ですが、先程のようにPodの中に複数のコンテナがあればここの表示が「2/2」になります。

アプリケーション宛のロードバランサーは外部向けのIPアドレスが払い出さるので、ここにアクセスすると先ほど同様にアプケーションが動作します。

先程はブラウザでやりましたが、もちろん curlコマンドを使ってもuserの登録は可能です。

$ curl http://XXX.XXX.XXX.XXX:8080/adduser
flexy-demo-app-5cb4bb64cd-md6zr: added user [Houndbird]
$ curl http://XXX.XXX.XXX.XXX:8080/adduser
flexy-demo-app-5cb4bb64cd-md6zr: added user [Stallionsticky]
$ curl http://XXX.XXX.XXX.XXX:8080/adduser
flexy-demo-app-5cb4bb64cd-md6zr: added user [Racernimble]
$ curl http://XXX.XXX.XXX.XXX:8080/adduser
flexy-demo-app-5cb4bb64cd-md6zr: added user [Salmonautumn]
$ curl http://XXX.XXX.XXX.XXX:8080/adduser
flexy-demo-app-5cb4bb64cd-md6zr: added user [Fingeode]

$ for i in `seq 1 100`; do curl -s curl http://XXX.XXX.XXX.XXX:8080/getuser; done | sort | uniq
flexy-demo-app-5cb4bb64cd-md6zr: name is [1 Houndbird]
flexy-demo-app-5cb4bb64cd-md6zr: name is [2 Stallionsticky]
flexy-demo-app-5cb4bb64cd-md6zr: name is [3 Racernimble]
flexy-demo-app-5cb4bb64cd-md6zr: name is [4 Salmonautumn]
flexy-demo-app-5cb4bb64cd-md6zr: name is [5 Fingeode]

Deploymentはレプリカ数をスケールさせることも可能です。 例えば、レプリカを3つにするには次のようなコマンドを実行しましょう。データベースは1つのままで、アプリケーションだけが3つになったことが確認できます。

$ kubectl scale deployment flexy-demo-app --replicas 3
deployment.extensions/flexy-demo-app scaled

$ kubectl get pods
NAME READY STATUS RESTARTS AGE
flexy-demo-app-5cb4bb64cd-64t56 1/1 Running 0 47s
flexy-demo-app-5cb4bb64cd-hphvf 1/1 Running 0 47s
flexy-demo-app-5cb4bb64cd-md6zr 1/1 Running 0 10m
mysql-5dd5b5c644-vtsjp 1/1 Running 0 10m

また同様にアプリケーションに対してリクエストを送ってみると、アプリケーションのホスト名も出力されているため、3*5 個の出力結果が確認できるかと思います。

$ for i in `seq 1 100`; do curl -s curl http://XXX.XXX.XXX.XXX:8080/getuser; done | sort | uniq
flexy-demo-app-5cb4bb64cd-64t56: name is [1 Houndbird]
flexy-demo-app-5cb4bb64cd-64t56: name is [2 Stallionsticky]
flexy-demo-app-5cb4bb64cd-64t56: name is [3 Racernimble]
flexy-demo-app-5cb4bb64cd-64t56: name is [4 Salmonautumn]
flexy-demo-app-5cb4bb64cd-64t56: name is [5 Fingeode]
flexy-demo-app-5cb4bb64cd-hphvf: name is [1 Houndbird]
flexy-demo-app-5cb4bb64cd-hphvf: name is [2 Stallionsticky]
flexy-demo-app-5cb4bb64cd-hphvf: name is [3 Racernimble]
flexy-demo-app-5cb4bb64cd-hphvf: name is [4 Salmonautumn]
flexy-demo-app-5cb4bb64cd-hphvf: name is [5 Fingeode]
flexy-demo-app-5cb4bb64cd-md6zr: name is [1 Houndbird]
flexy-demo-app-5cb4bb64cd-md6zr: name is [2 Stallionsticky]
flexy-demo-app-5cb4bb64cd-md6zr: name is [3 Racernimble]
flexy-demo-app-5cb4bb64cd-md6zr: name is [4 Salmonautumn]
flexy-demo-app-5cb4bb64cd-md6zr: name is [5 Fingeode]

図で表すと3つのPodが1つのMySQLを見に行くのでこのような状態です。

Kubernetes05
この状態では実際のサービスとしてはまだ不完全です。MySQLのPodを消した場合、データは消えてしまいます。 試しに一度MySQLのPodを削除してみましょう。自動的に新しいMySQLのPodは立ち上がってくるため、データベース自体は存在しています。しかし、ユーザーの一覧を取得しようとしても、データが全て消えてしまっていることが確認できるかと思います。

$ kubectl delete pods mysql-5dd5b5c644-vtsjp
pod "mysql-5dd5b5c644-vtsjp" deleted

$ kubectl get pods
NAME READY STATUS RESTARTS AGE
flexy-demo-app-5cb4bb64cd-64t56 1/1 Running 0 10m
flexy-demo-app-5cb4bb64cd-hphvf 1/1 Running 0 10m
flexy-demo-app-5cb4bb64cd-md6zr 1/1 Running 0 20m
mysql-5dd5b5c644-hiskx 1/1 Running 0 1m

$ curl http://XXX.XXX.XXX.XXX:8080/getuser;
curl: (52) Empty reply from server

この問題を解決するため、次はデータの永続化を行っていきましょう。その前に、このステップで作成したものを下記のコマンドを用いて削除しておきましょう。

$ kubectl delete -f 2_deployment.yaml
deployment.apps "flexy-demo-app" deleted
service "flexy-demo-app" deleted
configmap "init-db-sql" deleted
deployment.apps "mysql" deleted
service "mysql" deleted

Statefulで永続化

構成ファイルはほとんど同じですが、kindをStatefulSetにします。そのほかに、永続化Volumeの指定とマウント先の指定が必要になります。

ほとんどのKubernetes環境では、StatefulSetを使うことでPodを作る時にネットワーク越しに存在する永続化ボリュームを切り出してPodにアタッチしてくれます。今回はGKEを利用しているため、GCPのPersistentDiskから払い出されたディスクがPodの /var/lib/mysql にマウントされるような設定を行っています。

apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
replicas: 1
serviceName: mysql
selector:
matchLabels:
role: db
template:
metadata:
labels:
role: db
spec:
containers:
- name: mysql-container
image: mysql:8.0
env:
- name: MYSQL_ROOT_PASSWORD
value: rootpass
- name: MYSQL_DATABASE
value: mydb
- name: MYSQL_USER
value: myuser
- name: MYSQL_PASSWORD
value: mypass
volumeMounts:
- name: init-sql-configmap
mountPath: /docker-entrypoint-initdb.d
- name: datadir
mountPath: /var/lib/mysql
volumes:
- name: init-sql-configmap
configMap:
name: init-db-sql
volumeClaimTemplates:
- metadata:
name: datadir
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5G

それでは実際にこのマニフェストを登録してみましょう。

$ kubectl apply -f 3_statefulset.yaml
deployment.apps/flexy-demo-app created
service/flexy-demo-app created
configmap/init-db-sql created
statefulset.apps/mysql created
service/mysql created

kubectl get pods
NAME READY STATUS RESTARTS AGE
flexy-demo-app-5cb4bb64cd-qxmwc 1/1 Running 0 14m
mysql-0 1/1 Running 0 14m

$ kubectl scale deployment flexy-demo-app --replicas 3
deployment.extensions/flexy-demo-app scaled

$ kubectl get pods
NAME READY STATUS RESTARTS AGE
flexy-demo-app-5cb4bb64cd-2hgbv 1/1 Running 0 11s
flexy-demo-app-5cb4bb64cd-dwkgd 1/1 Running 0 11s
flexy-demo-app-5cb4bb64cd-qxmwc 1/1 Running 0 14m
mysql-0 1/1 Running 0 14m

$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
flexy-demo-app LoadBalancer XXX.XXX.XXX.XXX XXX.XXX.XXX.XXX 8080:31831/TCP 14m
kubernetes ClusterIP XXX.XXX.XXX.XXX 443/TCP 43m
mysql ClusterIP XXX.XXX.XXX.XXX 3306/TCP 14m

先ほどまでは mysql−乱数 となっていたPod名が mysql-0 になっています。StatefulSetをスケールアウトさせると mysql-1 のようにカウントアップして増えていきます。逆にデータベースをスケールインする時には後ろの数が大きいものから消えていきます。このように、0は最初に作られて最後に消されるので、これをマスターにし、1以降をスレーブにするといった使い方が可能です。

それでは先程と同様の手順でにMySQL Podを停止してデータがそのままになるか試してみましょう。

$ curl http://XXX.XXX.XXX.XXX:8080/adduser
flexy-demo-app-5cb4bb64cd-dwkgd: added user [Spriteriver]
$ curl http://XXX.XXX.XXX.XXX :8080/adduser
flexy-demo-app-5cb4bb64cd-qxmwc: added user [Handpuddle]
$ curl http://XXX.XXX.XXX.XXX :8080/adduser
flexy-demo-app-5cb4bb64cd-dwkgd: added user [Wingruby]
$ curl http://XXX.XXX.XXX.XXX :8080/adduser
flexy-demo-app-5cb4bb64cd-dwkgd: added user [Ferretbattle]
$ curl http://XXX.XXX.XXX.XXX :8080/adduser
flexy-demo-app-5cb4bb64cd-dwkgd: added user [Maskroot]

$ for i in `seq 1 100`; do curl -s curl http://XXX.XXX.XXX.XXX:8080/getuser; done | sort | uniq
flexy-demo-app-5cb4bb64cd-2hgbv: name is [1 Spriteriver]
flexy-demo-app-5cb4bb64cd-2hgbv: name is [2 Handpuddle]
flexy-demo-app-5cb4bb64cd-2hgbv: name is [3 Wingruby]
flexy-demo-app-5cb4bb64cd-2hgbv: name is [4 Ferretbattle]
flexy-demo-app-5cb4bb64cd-2hgbv: name is [5 Maskroot]
flexy-demo-app-5cb4bb64cd-dwkgd: name is [1 Spriteriver]
flexy-demo-app-5cb4bb64cd-dwkgd: name is [2 Handpuddle]
flexy-demo-app-5cb4bb64cd-dwkgd: name is [3 Wingruby]
flexy-demo-app-5cb4bb64cd-dwkgd: name is [4 Ferretbattle]
flexy-demo-app-5cb4bb64cd-dwkgd: name is [5 Maskroot]
flexy-demo-app-5cb4bb64cd-qxmwc: name is [1 Spriteriver]
flexy-demo-app-5cb4bb64cd-qxmwc: name is [2 Handpuddle]
flexy-demo-app-5cb4bb64cd-qxmwc: name is [3 Wingruby]
flexy-demo-app-5cb4bb64cd-qxmwc: name is [4 Ferretbattle]
flexy-demo-app-5cb4bb64cd-qxmwc: name is [5 Maskroot]

$ kubectl delete pods mysql-0
pod "mysql-0" deleted

$ for i in `seq 1 100`; do curl -s curl http://XXX.XXX.XXX.XXX:8080/getuser; done | sort | uniq
flexy-demo-app-5cb4bb64cd-2hgbv: name is [1 Spriteriver]
flexy-demo-app-5cb4bb64cd-2hgbv: name is [2 Handpuddle]
flexy-demo-app-5cb4bb64cd-2hgbv: name is [3 Wingruby]
flexy-demo-app-5cb4bb64cd-2hgbv: name is [4 Ferretbattle]
flexy-demo-app-5cb4bb64cd-2hgbv: name is [5 Maskroot]
flexy-demo-app-5cb4bb64cd-dwkgd: name is [1 Spriteriver]
flexy-demo-app-5cb4bb64cd-dwkgd: name is [2 Handpuddle]
flexy-demo-app-5cb4bb64cd-dwkgd: name is [3 Wingruby]
flexy-demo-app-5cb4bb64cd-dwkgd: name is [4 Ferretbattle]
flexy-demo-app-5cb4bb64cd-dwkgd: name is [5 Maskroot]
flexy-demo-app-5cb4bb64cd-qxmwc: name is [1 Spriteriver]
flexy-demo-app-5cb4bb64cd-qxmwc: name is [2 Handpuddle]
flexy-demo-app-5cb4bb64cd-qxmwc: name is [3 Wingruby]
flexy-demo-app-5cb4bb64cd-qxmwc: name is [4 Ferretbattle]
flexy-demo-app-5cb4bb64cd-qxmwc: name is [5 Maskroot]

今回は正しくデータの永続化ができていますね。ここまでくると、ようやくよくある2層の構成ができました。 今回のMySQLはクラスタ化していませんが、MySQLもクラスタ化するようにしておくことで、データベースのStatefulSetもスケールさせることができるようになります。

Ingressでルーティング

最後に、Ingressについて紹介します。これは、Service を束ねる機能です。 簡単に言えば「リバプロ的な動きをするもの」で、アクセスを受けたホスト名やパスに対してどういうルーティングをするか指定できます。他にもKubernetesのSSL終端を任せることもできます。また、証明書をマニフェストとして登録しておくことも可能なため、証明書の交換も簡単に行うことが可能です。 下記の図だと productやuserの単位でマイクロサービスとしてどんどん横に増えていくイメージです。

Kubernetes06
ここまで来たら、マイクロサービスの土台ができたと言えるでしょう。

Kubernetesで効率的なサービス運用を!

今回は基本的な部分のみの紹介になりましたが、サービスを作っていく土台はできました。Kubernetesを駆使すればまだまだ便利で効率的なサービス運用ができるようになります。

いろいろと手を動かしながらKubernetesの素晴らしさを実感していってください。また、下記に様々なサンプルマニフェストも公開しているので、ぜひ利用してみてください。 https://github.com/MasayaAoyama/kubernetes-perfect-guide

IMG 9774

 

青山真也(@amsy810 株式会社サイバーエージェント AI事業本部 インフラエンジニア/Developer Experts(Kubernetes/CloudNative領域) OpenStackを使ったプライベートクラウドやGKE互換なコンテナプラットフォームをゼロから構築。国内カンファレンスでのKeynoteや海外カンファレンス等、登壇経験多数。世界で2番目にKubernetesの認定資格を取得。さくらインターネット研究所客員研究員、CREATIONLINE 技術アドバイザ、Contribute to K8s & OpenStack。著書に『Kubernetes完全ガイド』『みんなのDocker/Kubernetes』

FLEXYとはABOUT FLEXY

いい才能が、集まっている。いい仕事も、集まっている。

『FLEXY』はエンジニア・デザイナー・CTO・技術顧問を中心に
週2-3日 x 自社プロダクト案件を紹介するサービスです