---
# System prepended metadata

title: 第十五章 Kubernetes 和整合儲存解決方案

---

本章介紹在Kubernetes中將儲存整合到容器化微服務的各種方法。
1. 匯入外部服務
2. 在Kubernetes內部運行可靠的機器
3. StatefulSet

## 匯入外部服務
已經有正在運行的資料庫系統，但需要被Kubernetes中的程式所使用
可以使用內建的命名及服務探索功能，Kubernetes中所有程式都可以把資料庫當作在Kubernetes叢集中的服務來使用，可以很輕易地利用Kubernetes服務來更換資料庫。
ex:在開發及線上環境中保持高擬真度
```yaml
# 測試站yaml
kind: Service
metadata:
    name: my-database
    # note 'test' namespace here
    namespace: test
...
# 正式站yaml
kind: Service
metadata:
    name: my-database
    # note 'prod' namespace here
    namespace: prod
...

```
內部域名分別連到test/prod的DB
my-database.test.svc.cluster.internal
my-database.prod.svc.cluster.internal


### Services Without Selectors
因為外部DB沒有label，要將外部資料庫導入到Kubernetes中，必須先建立一個不指定Pod選擇器的service，但引用DNS的名稱
```yaml
# dns-service.yaml
kind: Service
apiVersion: v1
metadata:
    name: external-database
spec:
    type: ExternalName
    externalName: database.company.com
```
建立一般的Service時，也會建立IP位址，並且DNS服務會配置一個A紀錄指向這個IP
建立ExternalName類型的service時，Kubernetes DNS服務則是配置一個CNAME紀錄，這個紀錄會指向指定的外部名稱
```yaml
# 外部DB沒有DNS
kind: Endpoints
apiVersion: v1
metadata:
    name: external-ip-database
subsets:
    - addresses:
        - ip: 192.168.0.1
        ports:
        - port: 3306
```
如果有多組備援IP，可以在addresses陣列中設定。一但端點被配置，附載平衡器將可以開始把流量從service重新指向到IP位址的端點。
* 由於使用者或應用程式會假設端點的位置(ip)永遠是最新的，因此須確保ip不是浮動的，或使用自動化的流程更新端點的紀錄。
### 外部Service的限制
不會主動進行健康檢查，因此必須承擔提供正確設定給Kubernetes的端點的責任

## 運行可靠的單一個體
使用Kubernetes的基本資源且不要複製儲存空間，並且只運行單一Pod的資料庫或其他儲存方案。
唯一的犧牲就是升級或機器故障的潛在停機時間。
### 運行一個MySQL單個體
建立三個基本的物件：
* persistent volume：獨立於MySQL應用程式生命週期中的永久磁碟區，這個磁碟區在硬碟上擁有自己的生命週期
* Pod：一個運行MySQL應用程式的Pod
* service：該service會將這個Pod開放到叢集中的其他容器中

使用NFS來實現最大可移植性

persistent volume
```yaml
# nfs-volume.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
    name: database
    labels:
        volume: my-volume
spec:
    accessModes:
    - ReadWriteMany
    capacity:
        storage: 1Gi
    nfs:
        server: 192.168.0.1
        path: "/exports"
```
這裡定義了1GB儲存空間的NFS persistent volume
```
create this persistent volume as usual with:
$ kubectl apply -f nfs-volume.yaml
```
宣告這個persistent volume then apply this file
```yaml
# nfs-volume-claim.yaml
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
    name: database
spec:
    accessModes:
    - ReadWriteMany
    resources:
        requests:
            storage: 1Gi
    selector:
        matchLabels:
            volume: my-volume
```
在selector欄位中，填入之前定義的persistent volume的label
現在已經宣告了磁碟區，可以使用ReplicaSet來構建單個體的Pod
``` yaml
# mysql-replicaset.yaml
apiVersion: extensions/v1
kind: ReplicaSet
metadata:
    name: mysql
    # Labels so that we can bind a Service to this Pod
    labels:
        app: mysql
spec:
    replicas: 1
    selector:
        matchLabels:
            app: mysql
    template:
        metadata:
            labels:
                app: mysql
        spec:
            containers:
            - name: database
                image: mysql
                resources:
                    requests:
                    cpu: 1
                    memory: 2Gi
                env:
                - name: MYSQL_ROOT_PASSWORD
                  value: some-password-here
                livenessProbe:
                    tcpSocket:
                        port: 3306
                ports:
                - containerPort: 3306
                volumeMounts:
                    - name: database
                    # /var/lib/mysql is where MySQL stores its databases
                      mountPath: "/var/lib/mysql"
            volumes:
            - name: database
              persistentVolumeClaim:
                claimName: database
```
建立ReplicaSet後，他將建立一個運行MySQL的Pod並使用之前建立的永久儲存空間，最後一步，是將其作為service開放出來
```yaml
mysql-service.yaml
apiVersion: v1
kind: Service
metadata:
    name: mysql
spec:
    ports:
    - port: 3306
      protocol: TCP
    # Labels so that we can bind a Service to this Pod
    selector:
        app: mysql
```
現在在叢集中，我們有一個可靠的單一MySQL個體在運行，並且透過service將他以mysql的名字供人使用，可以透過完整的域名mysql.svc.default.cluster來存取他

### 動態詞碟區擴充
透過動態詞碟區擴充，讓叢集操作人員建立一到多個StorageClass物件
ex：在Azure上利用storage class自動配置硬碟的範例
```yaml
# storageclass.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
    name: default
    annotations:
        storageclass.beta.kubernetes.io/is-default-class: "true"
    labels:
        kubernetes.io/cluster-service: "true"
# azure提供的storage
provisioner: kubernetes.io/azure-disk
```
當叢集建立storage class後，可以在persistent volume claim中引用此儲存類別，而不是引用某個persistent volume
```yaml
# dynamic-volume-claim.yaml
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
    name: my-claim
    annotations:
        volume.beta.kubernetes.io/storage-class: default
spec:
    accessModes:
    - ReadWriteOnce
    resources:
        requests:
            storage: 10Gi
```
volume.beta.kubernetes.io/storage-class: default的annotation表示將此宣告連結到剛剛所建立的儲存類別上
* 自動建立的永久磁碟區的生命週期被磁碟區宣告的回收政策所控制，而預設的設定是綁定到建立他的pod的生命週期，如果剛好刪除了自動建立磁碟區的pod，磁碟區也會跟著被刪除

## 讓StatefulSet使用Kubernetes的儲存空間
### StatefulSet的特性
StatefulSet的Pod副本群組類似ReplicaSet
* 每個Replica具有永久主機名加上唯一的索引值
* 每個Replica依照低到高的索引建立，並且會等到前一個的Pod正常且可運作，才會建立下一個，而這也是用於擴展Replica
* 而刪除Pod時，每個Replica也會按照從高到低的索引依序刪除，這個特性也同樣適用於縮小Replica
### 使用StatefulSet手動複製MongoDB
利用StatefulSet物件建立三個包含MongoDB的Pod副本集合
```ymal
mongo-simple.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
    name: mongo
spec:
    serviceName: "mongo"
    replicas: 3
    selector:
        matchLabels:
            app: mongo
    template:
        metadata:
            labels:
                app: mongo
        spec:
            containers:
            - name: mongodb
              image: mongo:3.4.24
              command:
                - mongod
                - --replSet
                - rs0
              ports:
              - containerPort: 27017
                name: peer
```
與前面介紹的ReplicaSet類似，唯一不同的是apiVersion和kind欄位
建立StatefulSet
```
$ kubectl apply -f mongo-simple.yaml
```
建立後ReplicaSet和StatefulSet就有明顯差別
```
$ kubectl get pods

NAME READY STATUS RESTARTS AGE
mongo-0 1/1 Running 0 1m
mongo-1 0/1 ContainerCreating 0 10s
```
第二個是pod按順序慢慢建立，而不像ReplicaSet一次全部建立
StatefulSet被建立後，還需要建立headless的service來管理StatefulSet的DNS項目
在Kubernetes中，如果service沒有cluster IP位址，就稱為headless service
可以在service規格中，利用cluster IP: None建立headless的service
```yaml
mongo-service.yaml
apiVersion: v1
kind: Service
metadata:
    name: mongo
spec:
    ports:
    - port: 27017
      name: peer
    clusterIP: None
    selector:
        app: mongo
```
建立這個service通常會配置四個DNS項目
mongo.default.svc.cluster.local
mongo-0.mongo.default.svc.cluster.local
mongo-1.mongo and mongo-2.mongo.

但與標準的service不同的是，在這主機名上執行DNS會得到StatefulSet中的所有位址

可以透過在其中一個Mongo的replica中執行指令來查看這些DNS項目
```
$ kubectl run -it --rm --image busybox busybox ping mongo-1.mongo
```
接下來將使用這些不重複的pod主機名稱，手動設定Mongo副本配置
選擇mongo-0.mongo作為主要節點，在這個pod中執行mongo指令：
```
$ kubectl exec -it mongo-0 mongo
> rs.initiate( {
 _id: "rs0",
 members:[ { _id: 0, host: "mongo-0.mongo:27017" } ]
 });
 OK
```
這指令指定mongodb使用mongo-0.mongo作為主要的副本來初始化ReplicaSet的rs0
* rs0的名稱是可以自行決定的，但需要在mongo.yaml的StatefulSet定義中做出相同的改變
初始化Mongo的ReplicaSet之後，可以透過在mongo-0.mongo Pod的mongo工具中執行以下指令來新增其他的副本：
```
> rs.add("mongo-1.mongo:27017");
> rs.add("mongo-2.mongo:27017");
```
使用了replica特定的DNS名稱來將他們新增為Mongo叢集中的replica
### 自動化建立MongoDB叢集
為了自動部署我們會新增額外的容器以執行初始化
在不需額外構建新的Docker映像檔的前提下要配置這個Pod，可以使用ConfigMap將script新增到現有的MongoDB映像檔中，以下可看見新增的容器配置：
```yaml
    ...
     initContainers:
         - name: init-mongo
           image: mongo:3.4.24
           command:
               - bash
               - /config/init.sh
           volumeMounts:
               - name: config
                 mountPath: /config
    ...
      volumes:
         - name: config
           configMap:
               name: "mongo-init"

```
這個容器正在掛載名為mongo-init的ConfigMap磁碟區
這個ConfigMap包含執行初始化的script，這個script的第一步會確定他是否在mongo-0上執行
如果是的話，他會使用與我們之前執行的同樣指令來建立ReplicaSet，如果是在其他的mongo副本上，他將等到ReplicaSet存在，才會將自己註冊為這個ReplicaSet的成員
ex:有完整的ConfigMap物件定義
```yaml
mongo-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
    name: mongo-init
data:
    init.sh: |
     #!/bin/bash
     # Need to wait for the readiness health check to pass so that the
     # Mongo names resolve. This is kind of wonky.
     until ping -c 1 ${HOSTNAME}.mongo; do
     echo "waiting for DNS (${HOSTNAME}.mongo)..."
     sleep 2
     done
     until /usr/bin/mongo --eval 'printjson(db.serverStatus())'; do
     echo "connecting to local mongo..."
     sleep 2
     done
     echo "connected to local."
     HOST=mongo-0.mongo:27017
     until /usr/bin/mongo --host=${HOST} --eval 'printjson(db.serverStatus())'; do
     echo "connecting to remote mongo..."
     sleep 2
     done
     echo "connected to remote."
     if [[ "${HOSTNAME}" != 'mongo-0' ]]; then
     until /usr/bin/mongo --host=${HOST} --eval="printjson(rs.status())" \
     | grep -v "no replset config has been received"; do
     echo "waiting for replication set initialization"
     sleep 2
     done
     echo "adding self to mongo-0"
     /usr/bin/mongo --host=${HOST} \
     --eval="printjson(rs.add('${HOSTNAME}.mongo'))"
     fi
     if [[ "${HOSTNAME}" == 'mongo-0' ]]; then
     echo "initializing replica set"
     /usr/bin/mongo --eval="printjson(rs.initiate(\
     {'_id': 'rs0', 'members': [{'_id': 0, \
     'host': 'mongo-0.mongo:27017'}]}))"
     fi
     echo "initialized"
```
* 這個script在初始化叢集後，會永遠地休眠
* pod裡的每個容器必須具有一致的ResrartPolicy
* 由於我們希望主節點的mongo容器不要被重新啟動，因此我們也需要讓這個初始化用的容器保持持續進行，否則Kubernetes可能會認為這個Pod不健康
ex:使用ConfigMap的完整StatefulSet
```yaml
. mongo.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
    name: mongo
spec:
    serviceName: "mongo"
    replicas: 3
    selector:
        matchLabels:
            app: mongo
    template:
        metadata:
            labels:
                app: mongo
        spec:
            containers:
            - name: mongodb
              image: mongo:3.4.24
              command:
              - mongod
              - --replSet
              - rs0
              ports:
              - containerPort: 27017
                name: web
    # This container initializes the MongoDB server, then sleeps.
            - name: init-mongo
              image: mongo:3.4.24
              command:
              - bash
              - /config/init.sh
              volumeMounts:
              - name: config
                mountPath: /config
            volumes:
            - name: config
              configMap:
                name: "mongo-init"
```
可以使用下列指令建立一個Mongo叢集
```
$ kubectl apply -f mongo-config-map.yaml
$ kubectl apply -f mongo-service.yaml
$ kubectl apply -f mongo.yaml
```
### persistent volume 和 StatefulSet
對於持久性儲存空間，你需要將persistent volume掛載到/data/db目錄中
在pod模板中，你需要對其進行更新，並將persistent volume宣告掛載到該目錄中：
```yaml
...
     volumeMounts:
     - name: database
       mountPath: /data/db
```
雖然這種作法與可靠的單個體類似，但由於StatefulSet有多個Pod，因此不能單純地引用persistent volume claim。你需要新增persistent volume claim模板，將以下內容新增到StatefulSet定義的底下：
```yaml
volumeClaimTemplates:
    - metadata:
        name: database
        annotations:
            volume.alpha.kubernetes.io/storage-class: anything
    spec:
        accessModes: [ "ReadWriteOnce" ]
        resources:
            requests:
                storage: 100Gi
```
當volume claim模板新增至StatefulSet定義後，在每一次建立pod時，StatefulSet控制器會根據此模板建立一個persistent volume claim
* 為了讓這些副本式persistent volume正常運作，persistent volume需要設定自動配置或是預先配置persistent volume的集合，以供StatefulSet控制器使用。如果沒有可以被建立的claim，則StatefulSet控制器無法建立對應的Pod
### Readiness探測器
建構MongoDB叢集的最後階段是為Mongo容器新增liveness檢查，用於確認容器是否正常運行
對於liveness檢查，可以利用以下mongo工具新增至StatefulSet物件的Pod模板中：
```yaml
...
 livenessProbe:
     exec:
         command:
         - /usr/bin/mongo
         - --eval
         - db.serverStatus()
     initialDelaySeconds: 10
     timeoutSeconds: 10
 ...

```