Kubernetesで学ぶ分散システムデザインパターン

コンテナを使ったシステムのアーキテクチャパターンが掲載されている、[分散システムデザインパターン](https://www.oreilly.co.jp/books/9784873118758/)という良書に出会ったので、Kubernetesの勉強を兼ねて各デザインパターンを実装してみましたので、ご紹介です。

この本は執筆者の論文が元になっており、そちらの翻訳記事もあるので、一読することをおすすめします。
[Qiita | コンテナ・デザイン・パターンの論文要約](https://qiita.com/MahoTakara/items/03fc0afe29379026c1f3)


# 本記事で扱うデザインパターンとその例
分散システムデザインパターン中では、大別して2種のデザインパターンが紹介されています。

- 1台のノード上で複数のコンテナが動作する**シングルノードパターン**
- 複数のノード上で実現される**マルチノードパターン**

本記事では、シングルノードパターンから以下のデザインパターンとその例を、Kubernetesでの実装例とともに解説します。

- サイドカーパターン
  - ex1. git-sync
  - ex2. topz
- アンバサダパターン
  - ex1. twemproxy
- アダプタパターン
  - ex1. redis exporter
  - ex2. mysql-healthcheck

# リポジトリ
本記事の内容の英語版は以下のリポジトリで公開しております。
<a href="https://github.com/reireias/kubernetes-design-pattern"><img src="https://github-link-card.s3.ap-northeast-1.amazonaws.com/reireias/kubernetes-design-pattern.png" width="460px"></a>

# サイドカーパターン

サイドカーパターンはポッド(ノード)の中で、主となるコンテナを補助する様なコンテナを持つ構成です。
![sidecar.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214526/8f8757c3-a28f-ccb8-6aef-38809672d96e.png)



## ex1. git-sync
[git-sync](https://github.com/kubernetes/git-sync)というツールを利用したサイドカーパターンの例です。
git-syncは定期的にリポジトリへアクセスし、常に最新のコードにローカルのコードを保つツールです。

この例での構成は下記のようになります。

```
     +--------+           +--------+
     | client |           | GitHub | html file
     +--------+           +--------+
          |                   |
          | access            | git pull
          |                   |
+---------------------------------------+
|         |                   |         |
|  +-------------+     +-------------+  |
|  |    Nginx    |     |   git-sync  |  |
|  | <Container> |     | <Container> |  |
|  +-------------+     +-------------+  |
|         |                   |         |
|         | mount             | mount   |
|         |                   |         |
|         |  +-------------+  |         |
|         +--| source code |--+         |
|            |  <Voluem>   |            |
|            +-------------+            |
|                 <Pod>                 |
+---------------------------------------+
```

- [git-sync](https://github.com/kubernetes/git-sync)が定期的にgitリポジトリへアクセスし、最新のコードを取得します。
- 取得されたコードはノード上のVolumeへ保存され、git-syncコンテナとメインとなるコンテナ(ここではNginx)からアクセスできるように構築されています。
- この構成により、Nginxのコンテナに手を入れなくとも、常に最新のコードをNginxのコンテナから参照できるようになります。

※コードの更新後、再起動が必要なケースでは、git-syncにもう少し複雑な設定を追加する必要があります。

### マニフェスト
マニフェストファイルの実装は下記のようになります。

```yaml:git-sync.yml
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: git-sync
spec:
  replicas: 1
  selector:
    matchLabels:
      app: git-sync
  template:
    metadata:
      labels:
        app: git-sync
    spec:
      containers:
        - name: nginx
          image: nginx:alpine
          ports:
            - containerPort: 80
          volumeMounts:
            - name: git-sync-volume
              mountPath: /usr/share/nginx
        - name: git-sync
          image: gcr.io/google_containers/git-sync:v3.1.1
          volumeMounts:
            - name: git-sync-volume
              mountPath: /sync
          env:
            - name: GIT_SYNC_REPO
              value: https://github.com/reireias/git-sync-example.git
            - name: GIT_SYNC_BRANCH
              value: master
            - name: GIT_SYNC_ROOT
              value: /sync
            - name: GIT_SYNC_DEST
              value: html
      volumes:
        - name: git-sync-volume
          emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
  name: git-sync
  labels:
    app: git-sync
spec:
  selector:
    app: git-sync
  type: NodePort
  ports:
    - port: 80
      targetPort: 80
```
解説

- git-syncコンテナとNginxコンテナを持つPodを1台構築するDeploymentが定義されています
- Deployment中のPodの設定では、コードをコンテナ間で共有するためのvolumeを定義しています
- git-syncで最新のコードを取得するリポジトリは、git-syncコンテナの環境変数で指定しています
  - もし、自分で起動して確認する場合は、自分でpush可能なリポジトリを指定するために、上記リポジトリをforkし、そのリポジトリを利用してください
- Nginxのコンテナを外部へ公開するための、Serviceを定義しています

### minikube上での実行
[miniube](https://kubernetes.io/ja/docs/tutorials/hello-minikube/)での実行例です。

```sh
# デプロイする
kubectl apply -f git-sync.yml

# ブラウザでgit-syncサービス(Nginx)へアクセスする
minikube service git-sync

# git-syncで参照しているリポジトリのコードを更新する

# サイドブラウザでアクセスし、htmlファイル等が更新されていることを確認する
```

## ex2. topz
サイドカーパターンの2つめの例は[topz](https://github.com/brendandburns/topz)を用いたリソース監視です。

構成は下記のようになります。

```
+---------------------------------------------+
|  +---------------------------------------+  |
|  |  +-------------+     +-------------+  |  |
|  |  |    Nginx    |     |     topz    |  |  |
|  |  | <Container> |     | <Container> |  |  |
|  |  +-------------+     +-------------+  |  |
|  |            <PID namespace>            |  |
|  +---------------------------------------+  |
|                    <Pod>                    |
+---------------------------------------------+
```

- topzコンテナとNginxコンテナ間でPID namespaceを共有させています
- topzからNginxのプロセス情報が参照できるようになっています
- これをwebアクセスで参照できるのがtopzの機能です

### マニフェスト
マニフェストファイルの実装は下記のようになります。

```yaml:topz.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: topz
spec:
  replicas: 1
  selector:
    matchLabels:
      app: topz
  template:
    metadata:
      labels:
        app: topz
    spec:
      shareProcessNamespace: true
      containers:
        - name: nginx
          image: nginx:alpine
          ports:
            - containerPort: 80
        - name: topz
          image: brendanburns/topz:db0fa58
          ports:
            - containerPort: 8080
          command:
            - /server
          args:
            - -addr
            - 0.0.0.0:8080
---
apiVersion: v1
kind: Service
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  selector:
    app: topz
  type: NodePort
  ports:
    - port: 80
      targetPort: 80
      name: nginx
---
apiVersion: v1
kind: Service
metadata:
  name: topz
  labels:
    app: topz
spec:
  selector:
    app: topz
  type: NodePort
  ports:
    - port: 8080
      targetPort: 8080
      name: topz
```

解説

- DeploymentでNginxとtopzコンテナを持つPodを定義しています
- 外部からアクセスできるように、topzとNginxのそれぞれのServeceを定義しています

### minikube上での実行

```sh
# デプロイします
kubectl apply -f topz.yml

# サービス一覧からエンドポイントを確認できます
minikube service list
|-------------|----------------------|-----------------------------|
|  NAMESPACE  |         NAME         |             URL             |
|-------------|----------------------|-----------------------------|
| default     | nginx                | http://192.168.39.196:31831 |
| default     | topz                 | http://192.168.39.196:30532 |
|-------------|----------------------|-----------------------------|

# 下記コマンドでNginxサービスへアクセスできます
minikube service nginx

# topz側のエンドポイントは下記コマンドで表示できるので、ブラウザでアクセスします
echo "$(minikube service topz --url)/topz"
# リソース使用状況が表示されるはずです
```

# アンバサダパターン
アンバサダパターンはメインのコンテナから外部へ接続する際に仲介する仕組みです。
キャッシュを利用したり、リクエストに応じて接続先を切り替えたりする用途で利用します。
![ambassador.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214526/5b5cfc5b-ffe7-79c0-431e-ce859a66a426.png)


## ex1. twemproxy
Redis用のproxyサーバーである[twemproxy](https://github.com/twitter/twemproxy)を利用する例です。

構成は下記のようになっています。

```
                     +-----------------+
                     | +-------------+ |
                     | |    Nginx    | |
                     | | <Container> | |
                     | +-------------+ |
                     |        |        |
                     | +-------------+ |
                     | |  twemproxy  | |
                     | | <Container> | |
                     | +-------------+ |
                     |        |  <Pod> |
                     +-----------------+
                              |
            +-----------------+-----------------+ 
            |                 |                 |
  +---------+-----------------+-----------------+---------+
  |         |                 |                 |         |
  | +---------------+ +---------------+ +---------------+ |
  | | Redis Shard 0 | | Redis Shard 1 | | Redis Shard 2 | |
  | |     <Pod>     | |     <Pod>     | |     <Pod>     | |
  | +---------------+ +---------------+ +---------------+ |
  |                     <StatefulSet>                     |
  +-------------------------------------------------------+
```

- twemproxyは高速かつ軽量なmemcached/Redis用プロキシです
- リクエスト中のキーに応じて接続先のRedisサーバーを自動的に選択してくれます
- ハッシュを利用しているので、同じキーの保存/取得リクエストはすべて同じRedisサーバーへ送られます
- この仕組みにより、Redisサーバーの負荷の分散が実現できます。

### マニフェスト
マニュフェストは下記のようになります。

```yaml:
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: sharded-redis
spec:
  selector:
    matchLabels:
      app: redis
  serviceName: "redis"
  replicas: 3
  template:
    metadata:
      labels:
        app: redis
    spec:
      terminationGracePeriodSeconds: 10
      containers:
        - name: redis
          image: redis
          ports:
            - containerPort: 6379
              name: redis
---
apiVersion: v1
kind: Service
metadata:
  name: redis
  labels:
    app: redis
spec:
  ports:
    - port: 6379
      name: redis
  clusterIP: None
  selector:
    app: redis
---
apiVersion: v1
kind: Pod
metadata:
  name: ambassador-example
spec:
  containers:
    - name: nginx
      image: nginx
    - name: twemproxy
      image: ganomede/twemproxy
      command:
        - "nutcracker"
        - "-c"
        - "/etc/config/nutcracker.yml"
        - "-v"
        - "7"
        - "-s"
        - "6222"
      volumeMounts:
        - name: config-volume
          mountPath: /etc/config
  volumes:
    - name: config-volume
      configMap:
        name: twem-config
```

解説

- StatefulSetにて、Redisコンテナを3台定義しています
- DeploymentにてNginxとtwemproxyコンテナをもつPodを定義しています
- 下記に記載してあるtwemproxy用の設定ファイルを参照できるようにconfigMapとそのマウントを定義しています
- nginxコンテナからは同一Pod上にtwemproxyコンテナが存在するので、`127.0.0.1:6379`でtwemproxyに接続ができます

事前にconfigMapに登録しておく、twemproxyの設定ファイルは下記のようになります。

```yaml:nutcracker.yml
redis:
  listen: 127.0.0.1:6379
  hash: fnv1a_64
  distribution: ketama
  auto_eject_hosts: true
  redis: true
  timeout: 400
  server_retry_timeout: 2000
  server_failure_limit: 1
  servers:
   - sharded-redis-0.redis:6379:1
   - sharded-redis-1.redis:6379:1
   - sharded-redis-2.redis:6379:1
```


### minikube上での実行

```sh
# 最初にconfigMapにtwemproxy用の設定ファイルを登録します
kubectl create configmap twem-config --from-file=./nutcracker.yml

# デプロイします
kubectl apply -f twemproxy.yml

# redisクライアントでの動作確認
# nginxコンテナでbashを起動します
kubectl exec -it ambassador-example --container nginx bash 
# redis-cliをインストールします
apt update
apt install -y redis-tools

# twemproxy経由で値をRedisに格納します
redis-cli
127.0.0.1:6379> set hoge 1
127.0.0.1:6379> set fuga 2

# twemproxy経由で値を取得します
127.0.0.1:6379> get hoge

# ローカルマシンに戻り、下記の各コマンドで、各Redisの登録されたキーを確認できます
kubectl exec -it sharded-redis-0 --container redis redis-cli
127.0.0.1:6379> keys *
kubectl exec -it sharded-redis-1 --container redis redis-cli
127.0.0.1:6379> keys *
kubectl exec -it sharded-redis-2 --container redis redis-cli
127.0.0.1:6379> keys *
```

# アダプタパターン
アダプタパターンはメインのコンテナの不均一なインターフェースを吸収し、統一されたインターフェースを外部に公開するのに利用するパターンです。
監視用のインターフェースを提供するのによく利用します。
![adapter.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214526/70b9db8d-1030-41a3-6f80-62d8fa570363.png)

## ex1. redis-exporter
[redis_exporter](https://github.com/oliver006/redis_exporter)を使い、監視ツール[Prometheus](https://prometheus.io/)用のインターフェースを提供するパターンです。

構成は下記のようになります。

```
    +------------+        +-------+
    | Prometheus |        |  app  |
    +------------+        +-------+
          | get metrics       |
+---------+----------------------------+
|         |                   |        |
| +----------------+   +-------------+ |
| | redis_exporter |---|    redis    | |
| |   <Container>  |   | <Container> | |
| +----------------+   +-------------+ |
|                <Pod>                 |
+--------------------------------------+
```

- RedisはPrometheus用リソース監視I/Fは持っていませんが、この構成で提供できます

### マニフェスト
マニュフェストは以下のようになります。

```yaml:redis-with-exporter.yml
---
apiVersion: v1
kind: Pod
metadata:
  name: adapter-example
  namespace: default
  labels:
    app: exporter
spec:
  containers:
    - name: redis
      image: redis
    - name: exporter
      image: oliver006/redis_exporter
      ports:
        - containerPort: 9121
---
apiVersion: v1
kind: Service
metadata:
  name: exporter
  labels:
    app: exporter
spec:
  selector:
    app: exporter
  type: NodePort
  ports:
    - port: 9121
      targetPort: 9121
```

解説

- Pod内にRedisコンテナとredis_exporterコンテナを定義しています
- Serviceでredis_exporterを外部に公開しています

### minikube上での実行
```sh
# デプロイします
kubectl apply -f redis-with-exporter.yml

# 下記コマンドで、Prometheus用I/Fにアクセスし、メトリクスが取得できることを確認します
curl $(minikube service exporter --url)/metrics
```


## ex2. mysql rich healthcheck

golang製のリッチなMySQL用ヘルスチェックツールによるアダプタパターンです。

構成は下記のようになります。

```
                             +--------+
                             | Client |
                             +--------+
                                  |
                                  | access to "/"
                                  |
+---------------------------------------------+
|                                 |           |
| +-------------+       +-------------------+ |
| |    MySQL    | query | mysql-healthcheck | |
| | <Container> |-------|    <Container>    | |
| +-------------+       +-------------------+ |
|                    <Pod>                    |
+---------------------------------------------+
```

- [reireias/mysql-healthcheck](https://github.com/reireias/mysql-healthcheck)を利用して、MySQLに特定のクエリを発行してヘルスチェックを行います
- mysql-healthcheckコンテナへアクセスがあった際にクエリは発行されます

### マニフェスト
マニュフェストは下記のようになります。

```yaml:mysql-with-rich-healthcheck.yml
---
apiVersion: v1
kind: Service
metadata:
  name: healthcheck
spec:
  type: NodePort
  ports:
    - name: healthcheck
      port: 8080
      targetPort: 8080
  selector:
    app: mysql
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mysql
spec:
  selector:
    matchLabels:
      app: mysql
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
        - name: mysql
          image: mysql:5.6
          env:
            - name: MYSQL_ROOT_PASSWORD
              value: password
            - name: MYSQL_USER
              value: user
            - name: MYSQL_PASSWORD
              value: password
            - name: MYSQL_DATABASE
              value: test
          ports:
            - name: mysql
              containerPort: 3306
          volumeMounts:
            - name: mysql-persistent-storage
              mountPath: /var/lib/mysql
        - name: healthcheck
          image: reireias/mysql-healthcheck
          env:
            - name: MYSQL_USER
              value: user
            - name: MYSQL_PASSWORD
              value: password
            - name: MYSQL_DATABASE
              value: test
            - name: MYSQL_QUERY
              value: 'show databases;'
            - name: MYSQL_HOST
              value: mysql
            - name: MYSQL_PORT
              value: '3306'
            - name: ADDRESS
              value: 0.0.0.0:8080
          ports:
            - name: healthcheck
              containerPort: 8080
      volumes:
        - name: mysql-persistent-storage
          persistentVolumeClaim:
            claimName: mysql-pv-claim
```

```yaml:mysql-pv.yml
---
kind: PersistentVolume
apiVersion: v1
metadata:
  name: mysql-pv-volume
  labels:
    type: local
spec:
  storageClassName: manual
  capacity:
    storage: 20Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: "/mnt/data"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mysql-pv-claim
spec:
  storageClassName: manual
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 20Gi
```

解説

- MySLQコンテナとmysql-healthcheckコンテナをDeploymentで定義しています
- 個々のコンテナの環境変数で設定を指定しています
- mysql-healthcheckをServiceで公開するように定義しています
- PersistentVolumeは依存関係で事前に作成するため、別のマニュフェストファイルとして作成しています

### minikube上での実行
```sh
# PersistentVolumeをデプロイします
kubectl apply -f mysql-pv.yml

# PodとServiceをデプロイします
kubectl apply -f mysql-with-ritch-healthcheck.yml

# ヘルスチェックが実行されるか確認します
curl $(minikube service healthcheck --url)
# OKと返るはずです
```

# まとめ
分散システムで利用できるデザインパターンのうち、シングルノードパターンを実例とともにいくつか紹介しました。
意外と既に実践していたパターンがいくつかあったのではないでしょうか？
デザインパターンは知っておくと便利ですし、それに適したコンテナがDockerHubで公開されていたりするので、活用していきましょう。