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    <none>   8m28s   v1.15.4-gke.22
gke-flexy-demo-default-pool-5d473a6b-h3s5   Ready    <none>   8m22s   v1.15.4-gke.22
gke-flexy-demo-default-pool-5d473a6b-l580   Ready    <none>   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      <none>         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     <none>         443/TCP          24m
mysql            ClusterIP      XXX.XXX.XXX.XXX    <none>         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      <none>          443/TCP          43m
mysql            ClusterIP      XXX.XXX.XXX.XXX    <none>          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編集部
FLEXY編集部
ハイスキルIT人材への案件紹介サービス
FLEXYメディアは、テックメディアとしてテクノロジーの推進に役立つコンテンツを提供しています。FLEXYメディアを運営するのは、ITに関連するプロシェアリングサービスを提供するFLEXY。経営課題をITで解決するためのCTOや技術顧問のご紹介、ハイスペックエンジニアやクリエイターと企業をマッチングしています。【FLEXYのサービス詳細】求人を募集している法人様向け/お仕事をしたいご登録希望の個人様向け

週1日~/リモートの案件に興味はありませんか?

週1日~/リモートの関わり方で、「開発案件」や「企業のIT化や設計のアドバザリーなどの技術顧問案件」を受けてみませんか?副業をしたい、独立して個人で仕事を受けたエンジニア・デザイナー・PM・技術顧問の皆様のお仕事探し支援サービスがあります。

FLEXYでご案内できる業務委託案件

業務委託契約・開発案件(JavaScriptメイン)

テーマ FLEXY登録画面から案件詳細の確認と直接応募が可能です
勤務日数 2-3日/週
報酬 4万円/日
必要スキル JavaScript・React
勤務地 東京都内/リモート含む
リモート

外部CTO、技術顧問

テーマ 技術アドバイザリーとして知見と経験を生かす
勤務日数 1日/週
報酬 10万円/日
必要スキル エンジニア組織立ち上げや統括のご経験、コードレビュー経験、技術的なアドバイスが出来る方
勤務地 東京
リモート 相談可

業務委託契約・インフラエンジニア

テーマ FLEXY登録画面から案件詳細の確認と直接応募が可能です
勤務日数 2-3日/週
報酬 5万円/日
必要スキル それぞれの案件により異なります
勤務地 東京
リモート 相談可

業務委託・フロントエンドエンジニア

テーマ FLEXY登録画面から案件詳細の確認と直接応募が可能です
勤務日数 週1日〜
報酬 5万/日
必要スキル それぞれの案件により異なります
勤務地 東京
リモート リモートと常駐のMIXなど

人材紹介のCTO案件(非公開求人)

テーマ CTO、技術顧問案件はFLEXYに登録後、案件をコンサルタントからご紹介します
勤務日数 業務委託から人材紹介への移行
報酬 年収800万以上
必要スキル CTOとして活躍可能な方、エンジニア組織のマネージメント経験
勤務地 東京
リモート 最初は業務委託契約で週3日などご要望に合わせます

業務委託契約・サーバサイドエンジニア

テーマ FLEXY登録画面から案件詳細の確認と直接応募が可能です
勤務日数 週2-3日
報酬 案件により異なります
必要スキル 案件により異なります
勤務地 東京都内
リモート 相談可能
個人登録

お仕事をお探しの方(無料登録)
法人の方(IT課題の相談)