应用存储和持久化数据卷

默认情况下容器的数据都是非持久化的,在容器消亡以后数据也跟着丢失,所以 Docker提供了Volume机制以便将数据持久化存储。类似的,Kubernetes提供了更强大的Volume机制和丰富的插件,解决了容器数据持久化和容器间共享数据的问题。

与Docker不同,Kubernetes Volume的生命周期与Pod绑定,容器挂掉后,Kubelet再次重启容器时,Volume的数据依然还在,而Pod删除时,Volume才会清理。数据是否丢失取决于具体的Volume类型,比如emptyDir的数据会丢失,而PV的数据则不会丢失。

PersistentVolume(PV)和PersistentVolumeClaim(PVC)是k8s提供的两种API资源,用于抽象存储细节。管理员关注于如何通过pv提供存储功能而无需关注用户如何使用,同样的用户只需要挂载PVC到容器中而不需要关注存储卷采用何种技术实现。

PVC和PV的关系与Pod和Node关系类似,前者消耗后者的资源。PVC可以向PV申请指定大小的存储资源并设置访问模式。

一. Volumes

容器中的文件在磁盘上是临时存放的,这给容器中运行的特殊应用程序带来一些问题。

  • 首先,当容器崩溃时,kubelet将重新启动容器,容器中的文件将会丢失——因为容器会以干净的状态重建。
  • 其次,当在一个Pod中同时运行多个容器时,常常需要在这些容器之间共享文件。Kubernetes抽象出Volume对象来解决这两个问题。

1.1 背景

Docker也有Volume的概念,但对它只有少量且松散的管理。在Docker中,Volume是磁盘上或者另外一个容器内的一个目录。直到最近,Docker才支持对基于本地磁盘的Volume的生存期进行管理。

Kubernetes卷具有明确的生命周期——与包裹它的Pod相同。因此,卷比Pod中运行的任何容器的存活期都长,在容器重新启动时数据也会得到保留。当然,当一个Pod不再存在时,卷也将不再存在。更重要的是,Kubernetes可以支持许多类型的卷,Pod也能同时使用任意数量的卷。

卷的核心是包含一些数据的目录,Pod中的容器可以访问该目录。特定的卷类型可以决定这个目录如何形成的,并能决定它支持何种介质,以及目录中存放什么内容。

使用卷时, Pod声明中需要提供卷的类型 (.spec.volumes 字段)和卷挂载的位置 (.spec.containers.volumeMounts 字段)。

容器中的进程能看到由它们的Docker镜像和卷组成的文件系统视图。Docker 镜像位于文件系统层次结构的根部,并且任何Volume都挂载在镜像内的指定路径上。卷不能挂载到其他卷,也不能与其他卷有硬链接。Pod中的每个容器必须独立地指定每个卷的挂载位置。

1.2 Volume 的类型

Kubernetes 支持下列类型的卷:

  • awsElasticBlockStore (亚马逊)
  • azureDisk(Microsoft Azure 数据盘(Data Disk))
  • azureFile(Microsoft Azure File Volume)
  • cephfs (CephFS volume)
  • cinder (OpenStack Cinder Volume)
  • configMap
  • csi
  • downwardAPI
  • emptyDir
  • fc (fibre channel) (光纤通道)
  • flexVolume
  • flocker
  • gcePersistentDisk (谷歌计算引擎 (GCE) 持久盘(PD) )
  • gitRepo (deprecated)
  • glusterfs
  • hostPath
  • iscsi
  • local
  • nfs
  • persistentVolumeClaim
  • projected
  • portworxVolume
  • quobyte
  • rbd
  • scaleIO
  • secret
  • storageos
  • vsphereVolume

configMap

configMap 提供了向Pod注入配置数据的方法。ConfigMap对象中存储的数据可以被configMap类型的卷引用,然后被应用到Pod中运行的容器应用。

当引用configMap对象时,你可以简单的在Volume中通过它名称来引用。还可以自定义ConfigMap中特定条目所要使用的路径。例如,要将名为log-config 的ConfigMap挂载到名为configmap-pod的Pod中,您可以使用下面的YAML:

apiVersion: v1
kind: Pod
metadata:
  name: configmap-pod
spec:
  containers:
    - name: test
      image: busybox
      volumeMounts:
        - name: config-vol
          mountPath: /etc/config
  volumes:
    - name: config-vol
      configMap:
        name: log-config
        items:
          - key: log_level
            path: log_level

log-configConfigMap是以卷的形式挂载的,存储在log_level 条目中的所有内容都被挂载到Pod的 “/etc/config/log_level” 路径下。请注意,这个路径来源于Volume的mountPathlog_level键对应的path

注意

  • 在使用ConfigMap之前您首先要创建它。
  • 容器以subPath卷挂载方式使用ConfigMap时,将无法接收ConfigMap的更新。

emptyDir

当Pod指定到某个节点上时,首先创建的是一个emptyDir卷,并且只要Pod在该节点上运行,卷就一直存在。就像它的名称表示的那样,卷最初是空的。尽管 Pod中的容器挂载emptyDir卷的路径可能相同也可能不同,但是这些容器都可以读写emptyDir卷中相同的文件。当Pod因为某些原因被从节点上删除时,emptyDir卷中的数据也会永久删除。

注意:容器崩溃并不会导致Pod被从节点上移除,因此容器崩溃时emptyDir卷中的数据是安全的。

emptyDir 的一些用途:

  • 缓存空间,例如基于磁盘的归并排序。
  • 为耗时较长的计算任务提供检查点,以便任务能方便地从崩溃前状态恢复执行。
  • 在Web服务器容器服务数据时,保存内容管理器容器获取的文件。

默认情况下,emptyDir 卷存储在支持该节点所使用的介质上;这里的介质可以是磁盘或SSD或网络存储,这取决于您的环境。但是,您可以将emptyDir.medium字段设置为 “Memory”,以告诉Kubernetes为您安装tmpfs(基于 RAM 的文件系统)。虽然tmpfs速度非常快,但是要注意它与磁盘不同。tmpfs在节点重启时会被清除,并且您所写入的所有文件都会计入容器的内存消耗,受容器内存限制约束。

Pod示例

apiVersion: v1
kind: Pod
metadata:
  name: test-pd
spec:
  containers:
  - image: k8s.gcr.io/test-webserver
    name: test-container
    volumeMounts:
    - mountPath: /cache
      name: cache-volume
  volumes:
  - name: cache-volume
    emptyDir: {}

再看一个例子:

apiVersion: v1
kind: Pod
metadata:
  name: pod-emptydir-demo
  namespace: default
  labels:
    app: myapp
    tier: frontend
spec:
  containers:
  - name: myapp
    image: nginx:1.7.9
    ports:
    - name: myapp
      containerPort: 80
    volumeMounts:
    - name: html
      mountPath: /usr/share/nginx/html
  - name: busybox
    image: busybox:latest
    volumeMounts:    
    - name: html     
      mountPath: /data  
    command:
    - "/bin/sh"
    - "-c"
    - "while true; do echo $(date) >> /data/index.html; sleep 2; done"
  volumes:
  - name: html
    emptyDir: {}

##pod中有两个container挂载同一个emptyDir,nginx提供web服务,busybox则循环向挂载目录下的index.html文件写入数据
##两个container都挂载了同一个volumes,只是容器中的路径不同而已。

执行这个yaml文件

##创建Pod
[root@k8s-master storage]# kubectl apply -f pod-emptydir-demo.yaml 
pod/pod-emptydir-demo created

##查看Pod地址
[root@k8s-master storage]# kubectl get pods -o wide
NAME                     READY   STATUS    RESTARTS   AGE     IP            NODE        NOMINATED NODE   READINESS GATES
curl-69c656fd45-26jv2    1/1     Running   6          21d     10.244.1.27   k8s-node1   <none>           <none>
my-service-gb45b         1/1     Running   1          40h     10.244.1.28   k8s-node1   <none>           <none>
my-service-sxz97         1/1     Running   1          40h     10.244.2.22   k8s-node2   <none>           <none>
nginx-86c57db685-hdwr8   1/1     Running   5          21d     10.244.2.23   k8s-node2   <none>           <none>
pod-emptydir-demo        2/2     Running   0          10s     10.244.2.29   k8s-node2   <none>           <none>
test-8656bc94b4-nsk77    1/1     Running   3          2d16h   10.244.1.29   k8s-node1   <none>           <none>

##访问Pod
[root@k8s-master storage]# while true; do curl 10.244.2.29; sleep 1 ;done
Sat Apr 4 07:53:39 UTC 2020
Sat Apr 4 07:53:41 UTC 2020
Sat Apr 4 07:53:43 UTC 2020
Sat Apr 4 07:53:45 UTC 2020
Sat Apr 4 07:53:47 UTC 2020
Sat Apr 4 07:53:49 UTC 2020

hostPath

hostPath 卷能将主机节点文件系统上的文件或目录挂载到您的Pod中。虽然这不是大多数Pod需要的,但是它为一些应用程序提供了强大的逃生舱。

hostPath 的一些用法有:

  • 运行一个需要访问Docker引擎内部机制的容器;请使用hostPath挂载/var/lib/docker路径。
  • 在容器中运行 cAdvisor 时,以hostPath方式挂载/sys。
  • 允许Pod指定给定的hostPath在运行Pod之前是否应该存在,是否应该创建以及应该以什么方式存在。

除了必需的path属性之外,用户可以选择性地为hostPath卷指定type。

当使用hostPath类型的卷时要注意:

  • 具有相同配置(例如从podTemplate创建)的多个Pod会由于节点上文件的不同而在不同节点上有不同的行为。
  • 当Kubernetes按照计划添加资源感知的调度时,这类调度机制将无法考虑由hostPath使用的资源。
  • 基础主机上创建的文件或目录只能由 root 用户写入。您需要在特权容器中以 root 身份运行进程,或者修改主机上的文件权限以便容器能够写入hostPath卷。

Pod示例:

apiVersion: v1
kind: Pod
metadata:
  name: test-pd
spec:
  containers:
  - image: k8s.gcr.io/test-webserver
    name: test-container
    volumeMounts:
    - mountPath: /test-pd
      name: test-volume
  volumes:
  - name: test-volume
    hostPath:
      # directory location on host
      path: /data
      # this field is optional
      type: Directory

看一个应用案例:

apiVersion: v1
kind: Pod
metadata:
  name: pod-hostpath-demo
spec:
  containers:
  - image: nginx:1.7.9
    name: test-container
    volumeMounts:
    - mountPath: /usr/share/nginx/html
      name: test-volume
  volumes:
  - name: test-volume
    hostPath:
      # directory location on host
      path: /data
      # this field is optional
      type: DirectoryOrCreate

执行这个yaml文件

[root@k8s-master storage]# kubectl apply -f pod-hostpath-demo.yaml 
pod/pod-hostpath-demo created

##查看其地址及所在主机
[root@k8s-master storage]# kubectl get pods -o wide
NAME                     READY   STATUS    RESTARTS   AGE     IP            NODE        NOMINATED NODE   READINESS GATES
curl-69c656fd45-26jv2    1/1     Running   6          21d     10.244.1.27   k8s-node1   <none>           <none>
my-service-gb45b         1/1     Running   1          41h     10.244.1.28   k8s-node1   <none>           <none>
my-service-sxz97         1/1     Running   1          41h     10.244.2.22   k8s-node2   <none>           <none>
nginx-86c57db685-hdwr8   1/1     Running   5          21d     10.244.2.23   k8s-node2   <none>           <none>
pod-hostpath-demo        1/1     Running   0          9s      10.244.1.33   k8s-node1   <none>           <none>
test-8656bc94b4-nsk77    1/1     Running   3          2d16h   10.244.1.29   k8s-node1   <none>           <none>

##到k8s-node1节点查看是否创建了/data目录
[root@k8s-node1 ~]# ls /data/

##添加首页文件,并访问
[root@k8s-node1 ~]# echo 'this is test' >> /data/index.html
[root@k8s-node1 ~]# curl 10.244.1.33
this is test

local

  • local 卷指的是所挂载的某个本地存储设备,例如磁盘、分区或者目录。
  • local 卷只能用作静态创建的持久卷。尚不支持动态配置。
  • 相比 hostPath 卷,local 卷可以以持久和可移植的方式使用,而无需手动将 Pod 调度到节点,因为系统通过查看 PersistentVolume 所属节点的亲和性配置,就能了解卷的节点约束。
  • 然而,local 卷仍然取决于底层节点的可用性,并不是适合所有应用程序。如果节点变得不健康,那么local 卷也将变得不可访问,并且使用它的 Pod 将不能运行。

下面是一个使用local卷和nodeAffinity的持久卷示例:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: example-pv
spec:
  capacity:
    storage: 100Gi
  # volumeMode field requires BlockVolume Alpha feature gate to be enabled.
  volumeMode: Filesystem
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Delete
  storageClassName: local-storage
  local:
    path: /mnt/disks/ssd1
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - example-node

使用 local 卷时,需要使用PersistentVolume对象的nodeAffinity字段。 它使 Kubernetes 调度器能够将使用local卷的Pod正确地调度到合适的节点。

persistentVolumeClaim

persistentVolumeClaim 卷用来将持久卷(PersistentVolume)挂载到 Pod 中。 持久卷是用户在不知道特定云环境细节的情况下”申领”持久存储(例如 GCE PersistentDisk 或者 iSCSI 卷)的一种方法。

secret

secret 卷用来给Pod传递敏感信息,例如密码。您可以将 secret 存储在 Kubernetes API 服务器上,然后以文件的形式挂在到 Pod 中,无需直接与 Kubernetes 耦合。 secret 卷由 tmpfs(基于 RAM 的文件系统)提供存储,因此它们永远不会被写入非易失性(持久化的)存储器。

注意:

  • 使用前您必须在 Kubernetes API 中创建secret
  • 容器以 subPath 卷的方式挂载Secret时,它将感知不到Secret的更新。

storageOS

storageos卷允许将现有的StorageOS卷挂载到您的Pod中。

StorageOS 在 Kubernetes 环境中以容器的形式运行,这使得应用能够从 Kubernetes 集群中的任何节点访问本地或关联的存储。为应对节点失效状况,可以复制数据。若需提高利用率和降低成本,可以考虑瘦配置(Thin Provisioning)和数据压缩。

作为其核心能力之一,StorageOS为容器提供了可以通过文件系统访问的块存储。

StorageOS 容器需要64位的Linux,并且没有其他的依赖关系。StorageOS提供免费的开发者授权许可。

注意:必须在每个希望访问StorageOS卷的或者将向存储资源池贡献存储容量的节点上运行StorageOS容器。

apiVersion: v1
kind: Pod
metadata:
  labels:
    name: redis
    role: master
  name: test-storageos-redis
spec:
  containers:
    - name: master
      image: kubernetes/redis:v1
      env:
        - name: MASTER
          value: "true"
      ports:
        - containerPort: 6379
      volumeMounts:
        - mountPath: /redis-master-data
          name: redis-data
  volumes:
    - name: redis-data
      storageos:
        # The `redis-vol01` volume must already exist within StorageOS in the `default` namespace.
        volumeName: redis-vol01
        fsType: ext4

二. ConfigMap

主要管理容器运行所需要的配置文件、环境变量、命令行参数等可变配置。用于解耦容器镜像和可变配置,从而保障工作负载(Pod)的可移植性。

ConfigMap API资源用来保存key-value pair配置数据,这个数据可以在pods里使用,或者被用来为像controller一样的系统组件存储配置数据。虽然ConfigMap跟Secrets类似,但是ConfigMap更方便的处理不含敏感信息的字符串。

注意:ConfigMaps不是属性配置文件的替代品。ConfigMaps只是作为多个properties文件的引用。你可以把它理解为Linux系统中的/etc目录,专门用来存储配置文件的目录。下面举个例子,使用ConfigMap配置来创建Kubernetes Volumes,ConfigMap中的每个data项都会成为一个新文件。

kind: ConfigMap
apiVersion: v1
metadata:
  name: example-config
  namespace: default
data:
  example.property.1: hello
  example.property.2: world
  example.property.file: |-
    property.1=value-1
    property.2=value-2
    property.3=value-3

data一栏包括了配置数据,ConfigMap可以被用来保存单个属性,也可以用来保存一个配置文件。配置数据可以通过很多种方式在Pods里被使用。ConfigMaps可以被用来:

  • 设置环境变量的值
  • 在容器里设置命令行参数
  • 在数据卷里面创建config文件

用户和系统组件两者都可以在ConfigMap里面存储配置数据。

2.1 创建ConfigMaps

可以使用kubectl命令进行创建,可以使用帮助命令:

[root@k8s-master storage]# kubectl create configmap -h
Examples:
  # Create a new configmap named my-config based on folder bar
  kubectl create configmap my-config --from-file=path/to/bar
  
  # Create a new configmap named my-config with specified keys instead of file basenames on disk
  kubectl create configmap my-config --from-file=key1=/path/to/bar/file1.txt --from-file=key2=/path/to/bar/file2.txt
  
  # Create a new configmap named my-config with key1=config1 and key2=config2
  kubectl create configmap my-config --from-literal=key1=config1 --from-literal=key2=config2
  
  # Create a new configmap named my-config from the key=value pairs in the file
  kubectl create configmap my-config --from-file=path/to/bar
  
  # Create a new configmap named my-config from an env file
  kubectl create configmap my-config --from-env-file=path/to/bar.env

使用目录创建

首选创建一个目录,目录中放入2个文件

[root@k8s-master demo]# ls configmap-test/
mysql.conf  redis.conf
[root@k8s-master demo]# cat configmap-test/mysql.conf 
host=127.0.0.1
port=3306
[root@k8s-master demo]# cat configmap-test/redis.conf 
host=127.0.0.1
port=6379

使用命令进行创建

[root@k8s-master demo]# kubectl create configmap cm-demo1 --from-file=configmap-test
configmap/cm-demo1 created

[root@k8s-master demo]# kubectl get configmap
NAME       DATA   AGE
cm-demo1   2      35s

##其中from-file参数指定在该目录下面的所有文件都会被用在ConfigMap里面创建一个键值对;
##键的名字就是文件名,值就是文件的内容。

##查看详细信息
[root@k8s-master demo]# kubectl describe configmap cm-demo1
Name:         cm-demo1
Namespace:    default
Labels:       <none>
Annotations:  <none>

Data
====
mysql.conf:     ## 键名,对应于目录下的文件名
----
host=127.0.0.1
port=3306

redis.conf:     ## 键名,对应于目录下的文件名
----
host=127.0.0.1
port=6379

Events:  <none>

##查看其yaml格式,也可以使用yaml格式进行创建的
[root@k8s-master demo]# kubectl get configmap cm-demo1 -o yaml
apiVersion: v1
data:
  mysql.conf: |
    host=127.0.0.1
    port=3306
  redis.conf: |
    host=127.0.0.1
    port=6379
kind: ConfigMap
metadata:
  creationTimestamp: "2020-04-04T11:50:05Z"
  name: cm-demo1
  namespace: default
  resourceVersion: "210155"
  selfLink: /api/v1/namespaces/default/configmaps/cm-demo1
  uid: 77a82826-0b7b-44ad-860e-a5f8ecb6f158

使用文件创建

刚才使用目录创建的时候我们—from-file指定的是一个目录,只要指定为一个文件就可以从单个文件中创建ConfigMap。

[root@k8s-master demo]# kubectl create configmap cm-demo2 --from-file=configmap-test/redis.conf
configmap/cm-demo2 created

##查看
[root@k8s-master demo]# kubectl get configmap cm-demo2 -o yaml
apiVersion: v1
data:
  redis.conf: |
    host=127.0.0.1
    port=6379
kind: ConfigMap
metadata:
  creationTimestamp: "2020-04-04T12:06:37Z"
  name: cm-demo2
  namespace: default
  resourceVersion: "212681"
  selfLink: /api/v1/namespaces/default/configmaps/cm-demo2
  uid: 10f03150-5512-4309-beb0-97c99f3efe82

–from-file这个参数可以使用多次,你可以使用两次分别指定上个实例中的那两个配置文件,效果就跟指定整个目录是一样的。

使用字面值创建

使用字符串直接创建,利用–from-literal参数传递配置信息,该参数可以使用多次。

[root@k8s-master demo]# kubectl create configmap cm-demo3 --from-literal=db.host=localhost --from-literal=db.port=3306
configmap/cm-demo3 created

##查看
[root@k8s-master demo]# kubectl get configmap cm-demo3 -o yaml
apiVersion: v1
data:
  db.host: localhost
  db.port: "3306"
kind: ConfigMap
metadata:
  creationTimestamp: "2020-04-04T12:14:51Z"
  name: cm-demo3
  namespace: default
  resourceVersion: "213937"
  selfLink: /api/v1/namespaces/default/configmaps/cm-demo3
  uid: a706de16-6a33-4f5a-b5ee-f79ca0b1d902

2.2 Pod中使用ConfigMap

ConfigMap创建成功了,那么我们应该怎么在Pod中来使用呢?

使用ConfigMap来替代环境变量

ConfigMap可以被用来填入环境变量。看下下面的ConfigMap。

apiVersion: v1
kind: ConfigMap
metadata:
  name: special-config
  namespace: default
data:
  special.how: very
  special.type: charm
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: env-config
  namespace: default
data:
  log_level: INFO

可以在Pod中这样使用ConfigMap:

apiVersion: v1
kind: Pod
metadata:
  name: dapi-test-pod
spec:
  containers:
    - name: test-container
      image: busybox
      command: [ "/bin/sh", "-c", "env" ]
      env:
        - name: SPECIAL_LEVEL_KEY
          valueFrom:
            configMapKeyRef:
              name: special-config
              key: special.how
        - name: SPECIAL_TYPE_KEY
          valueFrom:
            configMapKeyRef:
              name: special-config
              key: special.type
      envFrom:
        - configMapRef:
            name: env-config
  restartPolicy: Never

这个Pod运行后会输出以下信息:

SPECIAL_LEVEL_KEY=very
SPECIAL_TYPE_KEY=charm
log_level=INFO

用ConfigMap设置命令行参数

ConfigMap也可以被使用来设置容器中的命令或者参数值。它使用的是Kubernetes的$(VAR_NAME)替换语法。

apiVersion: v1
kind: Pod
metadata:
  name: dapi-test-pod2
spec:
  containers:
    - name: test-container
      image: busybox
      command: [ "/bin/sh", "-c", "echo $(SPECIAL_LEVEL_KEY) $(SPECIAL_TYPE_KEY)" ]
      env:
        - name: SPECIAL_LEVEL_KEY
          valueFrom:
            configMapKeyRef:
              name: special-config
              key: special.how
        - name: SPECIAL_TYPE_KEY
          valueFrom:
            configMapKeyRef:
              name: special-config
              key: special.type
  restartPolicy: Never

运行这个Pod后会输出:

[root@k8s-master configmap]# kubectl logs dapi-test-pod
very charm

通过数据卷插件使用ConfigMap

在数据卷里面使用这个ConfigMap,有不同的选项。最基本的就是将文件填入数据卷,在这个文件中,键就是文件名,键值就是文件内容:

apiVersion: v1
kind: Pod
metadata:
  name: dapi-test-pod3
spec:
  containers:
    - name: test-container
      image: busybox
      command: [ "/bin/sh", "-c", "cat /etc/config/special.how" ]
      volumeMounts:
      - name: config-volume
        mountPath: /etc/config
  volumes:
    - name: config-volume
      configMap:
        name: special-config
  restartPolicy: Never

运行这个Pod的输出是very。

也可以在ConfigMap值被映射的数据卷里控制路径。

apiVersion: v1
kind: Pod
metadata:
  name: testcm4-pod
spec:
  containers:
    - name: testcm4
      image: busybox
      command: [ "/bin/sh","-c","cat /etc/config/path/to/msyql.conf" ]
      volumeMounts:
      - name: config-volume
        mountPath: /etc/config
  volumes:
    - name: config-volume
      configMap:
        name: cm-demo1
        items:
        - key: mysql.conf
          path: path/to/msyql.conf

输出结果是:

[root@k8s-master configmap]# kubectl logs testcm4-pod
host=127.0.0.1
port=3306

注意:当ConfigMap以数据卷的形式挂载进Pod的时,这时更新ConfigMap(或删掉重建ConfigMap),Pod内挂载的配置信息会热更新。这时可以增加一些监测配置文件变更的脚本,然后reload对应服务。

2.3 ConfigMap 注意要点

对 ConfigMap 的使用做一个总结,以及它的一些注意点,注意点一共列了以下五条:

  • ConfigMap 文件的大小。虽然说 ConfigMap 文件没有大小限制,但是在 ETCD 里面,数据的写入是有大小限制的,现在是限制在 1MB 以内;
  • pod 引入 ConfigMap 的时候,必须是相同的 Namespace 中的 ConfigMap,ConfigMap.metadata 里面是有 namespace 字段的;
  • pod 引用的 ConfigMap。假如这个 ConfigMap 不存在,那么这个 pod 是无法创建成功的,其实这也表示在创建 pod 前,必须先把要引用的 ConfigMap 创建好;
  • 使用 envFrom 的方式。把 ConfigMap 里面所有的信息导入成环境变量时,如果 ConfigMap 里有些 key 是无效的,比如 key 的名字里面带有数字,那么这个环境变量其实是不会注入容器的,它会被忽略。但是这个 pod 本身是可以创建的。这个和第三点是不一样的方式,是 ConfigMap 文件存在基础上,整体导入成环境变量的一种形式;
  • 最后一点是:什么样的 pod 才能使用 ConfigMap?只有通过 K8s api 创建的 pod 才能使用 ConfigMap,比如说通过用命令行 kubectl 来创建的 pod,肯定是可以使用 ConfigMap 的,但其他方式创建的 pod,比如说 kubelet 通过 manifest 创建的 static pod,它是不能使用 ConfigMap 的。

三. Secret

Secret解决了密码、token、密钥等敏感数据的配置问题,而不需要把这些敏感数据暴露到镜像或者Pod Spec中。Secret可以以Volume或者环境变量的方式使用。

Secret有三种类型:

  • Opaque:base64 编码格式的 Secret,用来存储密码、密钥等;但数据也可以通过base64 –decode解码得到原始数据,所以加密性很弱。
  • kubernetes.io/dockerconfigjson: 用来存储私有docker registry的认证信息。
  • kubernetes.io/service-account-token: 用于被serviceaccount引用,serviceaccout 创建时Kubernetes会默认创建对应的secret。Pod如果使用了serviceaccount,对应的secret会自动挂载到Pod目录/run/secrets/kubernetes.io/serviceaccount中。

3.1 Opaque Secret

Opaque 类型的数据是一个 map 类型,要求value是base64编码格式,比如我们来创建一个用户名为 admin,密码为 admin321 的 Secret 对象,首先我们先把这用户名和密码做 base64 编码。

[root@k8s-master secret]# echo -n "admin" | base64
YWRtaW4=
[root@k8s-master secret]# echo -n "admin321" | base64
YWRtaW4zMjE=
[root@k8s-master secret]#

然后我们就可以利用上面编码过后的数据来编写一个YAML文件:

apiVersion: v1
kind: Secret
metadata:
  name: mysecret
type: Opaque
data:
  username: YWRtaW4=
  password: YWRtaW4zMjE=

执行创建并查看

[root@k8s-master secret]# kubectl apply -f demo1.yaml 
secret/mysecret created
[root@k8s-master secret]# kubectl get secret 
NAME                  TYPE                                  DATA   AGE
default-token-4bqzd   kubernetes.io/service-account-token   3      22d
mysecret              Opaque                                2      11s

##其中default-token-4bqzd为创建集群时默认创建的 secret,被serviceacount/default 引用

[root@k8s-master secret]# kubectl describe secret mysecret
Name:         mysecret
Namespace:    default
Labels:       <none>
Annotations:  
Type:         Opaque

Data
====
password:  8 bytes
username:  5 bytes

## Data 并没有直接显示出来,如何想看,可以使用 yaml 格式查看

创建好Secret对象后,有两种方式来使用它:

  • 以环境变量的形式
  • 以Volume的形式挂载

将Secret导出到环境变量中

apiVersion: v1
kind: Pod
metadata:
  name: secret1-pod
spec:
  containers:
  - name: secret1
    image: busybox
    command: [ "/bin/sh", "-c", "env" ]
    env:
    - name: USERNAME
      valueFrom:
        secretKeyRef:
          name: mysecret
          key: username
    - name: PASSWORD
      valueFrom:
        secretKeyRef:
          name: mysecret
          key: password

上面环境变量中定义的secretKeyRef关键字,和configMapKeyRef比较类似,一个是从Secret对象中获取,一个是从ConfigMap对象中获取。

将Secret挂载到Volume中

apiVersion: v1
kind: Pod
metadata:
  name: secret2-pod
spec:
  containers:
  - name: secret2
    image: busybox
    command: ["/bin/sh", "-c", "ls /etc/secrets"]
    volumeMounts:
    - name: secrets
      mountPath: /etc/secrets
  volumes:
  - name: secrets
    secret:
     secretName: mysecret

创建Pod然后查看日志

[root@k8s-master secret]# kubectl apply -f secret-pod2.yaml 
pod/secret2-pod created

[root@k8s-master secret]# kubectl logs secret2-pod 
password
username

3.2 kubernetes.io/dockerconfigjson

除了上面的Opaque这种类型外,我们还可以来创建用户docker registry认证的Secret,直接使用kubectl create命令创建即可,如下:

[root@k8s-master ~]# kubectl create secret docker-registry myregistry \
--docker-server=DOCKER_SERVER --docker-username=DOCKER_USER \
--docker-password=DOCKER_PASSWORD --docker-email=DOCKER_EMAIL
secret/myregistry created

然后查看Secret列表:

[root@k8s-master ~]# kubectl get secret
NAME                  TYPE                                  DATA   AGE
default-token-4bqzd   kubernetes.io/service-account-token   3      22d
myregistry            kubernetes.io/dockerconfigjson        1      3m39s
mysecret              Opaque                                2      18h


##查看详细信息
[root@k8s-master ~]# kubectl describe secret myregistry
Name:         myregistry
Namespace:    default
Labels:       <none>
Annotations:  <none>

Type:  kubernetes.io/dockerconfigjson

Data
====
.dockerconfigjson:  152 bytes

##通过yaml格式查看
[root@k8s-master ~]# kubectl get secret myregistry -o yaml
apiVersion: v1
data:
  .dockerconfigjson: eyJhdXRocyI6eyJET0NLRVJfU0VSVkVSIjp7InVzZXJuYW1lIjoiRE9DS0VSX1VTRVIiLCJwYXNzd29yZCI6IkRPQ0tFUl9QQVNTV09SRCIsImVtYWlsIjoiRE9DS0VSX0VNQUlMIiwiYXV0aCI6IlJFOURTMFZTWDFWVFJWSTZSRTlEUzBWU1gxQkJVMU5YVDFKRSJ9fX0=
kind: Secret
metadata:
  creationTimestamp: "2020-04-05T08:30:10Z"
  name: myregistry
  namespace: default
  resourceVersion: "264179"
  selfLink: /api/v1/namespaces/default/secrets/myregistry
  uid: aa84c54f-1ce6-46ff-8019-c4f2b0f8d71b
type: kubernetes.io/dockerconfigjson

可以使用base64 -d解码查看一下内容:

[root@k8s-master ~]# echo "eyJhdXRocyI6eyJET0NLRVJfU0VSVkVSIjp7InVzZXJuYW1lIjoiRE9DS0VSX1VTRVIiLCJwYXNzd29yZCI6IkRPQ0tFUl9QQVNTV09SRCIsImVtYWlsIjoiRE9DS0VSX0VNQUlMIiwiYXV0aCI6IlJFOURTMFZTWDFWVFJWSTZSRTlEUzBWU1gxQkJVMU5YVDFKRSJ9fX0=" | base64 -d
{"auths":{"DOCKER_SERVER":{"username":"DOCKER_USER","password":"DOCKER_PASSWORD","email":"DOCKER_EMAIL","auth":"RE9DS0VSX1VTRVI6RE9DS0VSX1BBU1NXT1JE"}}}

也可以直接读取~/.docker/config.json的内容来创建:

$ cat ~/.docker/config.json | base64
$ cat > myregistrykey.yaml <<EOF
apiVersion: v1
kind: Secret
metadata:
  name: myregistrykey
data:
  .dockerconfigjson: UmVhbGx5IHJlYWxseSByZWVlZWVlZWVlZWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGx5eXl5eXl5eXl5eXl5eXl5eXl5eSBsbGxsbGxsbGxsbGxsbG9vb29vb29vb29vb29vb29vb29vb29vb29vb25ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubmdnZ2dnZ2dnZ2dnZ2dnZ2dnZ2cgYXV0aCBrZXlzCg==
type: kubernetes.io/dockerconfigjson
EOF
$ kubectl create -f myregistrykey.yaml

在创建Pod的时候,如果使用的是私有仓库拉取镜像,通过imagePullSecrets来引用刚创建的myregistrykey:

apiVersion: v1
kind: Pod
metadata:
  name: foo
spec:
  containers:
  - name: foo
    image: 192.168.1.100:5000/test:v1
  imagePullSecrets:
  - name: myregistrykey

3.3 kubernetes.io/service-account-token

另外一种Secret类型就是 kubernetes.io/service-account-token ,用于被serviceaccount引用。serviceaccout 创建时 Kubernetes 会默认创建对应的 secret。Pod 如果使用了 serviceaccount,对应的secret会自动挂载到Pod的/run/secrets/kubernetes.io/serviceaccount目录中。

验证:

##创建一个Pod
[root@k8s-master ~]# kubectl run secret-pod3 --image nginx:1.7.9 --generator=run-pod/v1

##查看是否成功
[root@k8s-master ~]# kubectl get pods secret-pod3
NAME          READY   STATUS    RESTARTS   AGE
secret-pod3   1/1     Running   0          2m31s

##验证使用的secret
[root@k8s-master ~]# kubectl exec secret-pod3 ls /run/secrets/kubernetes.io/serviceaccount
ca.crt
namespace
token

四. PV和PVC

对于管理计算资源来说,管理存储资源明显是另一个问题。PersistentVolume 子系统为用户和管理员提供了一个 API,该 API 将如何提供存储的细节抽象了出来。为了屏蔽底层的技术实现细节,让用户更加方便的使用,为此,我们引入两个新的 API 资源:PersistentVolume 和 PersistentVolumeClaim。

PV 的全称是:PersistentVolume(持久化卷),是对底层的共享存储的一种抽象,PV 由管理员进行创建和配置,它和具体的底层的共享存储技术的实现方式有关,比如 Ceph、GlusterFS、NFS 等,都是通过插件机制完成与共享存储的对接。

PVC 的全称是:PersistentVolumeClaim(持久化卷声明),PVC 是用户存储的一种声明,PVC 和 Pod 比较类似,Pod 消耗的是节点,PVC 消耗的是 PV 资源,Pod 可以请求 CPU 和内存,而 PVC 可以请求特定的存储空间和访问模式。对于真正使用存储的用户不需要关心底层的存储实现细节,只需要直接使用 PVC 即可。

但是通过 PVC 请求到一定的存储空间也很有可能不足以满足应用对于存储设备的各种需求,而且不同的应用程序对于存储性能的要求可能也不尽相同,比如读写速度、并发性能等,为了解决这一问题,Kubernetes 又为我们引入了一个新的资源对象:StorageClass,通过 StorageClass 的定义,管理员可以将存储资源定义为某种类型的资源,比如快速存储、慢速存储等,用户根据 StorageClass 的描述就可以非常直观的知道各种存储资源的具体特性了,这样就可以根据应用的特性去申请合适的存储资源了。

4.1 NFS

为了演示方便,决定使用相对简单的 NFS 这种存储资源,这里使用node2来作为 NFS 服务器。防火墙已经关闭。

## 安装nfs-utils软件包
[root@k8s-node2 ~]# yum install nfs-utils -y

##创建共享目录
[root@k8s-node2 ~]# mkdir /data/k8s/v{1..3} -p
[root@k8s-node2 ~]# tree -C /data/
/data/
└── k8s
    ├── v1
    ├── v2
    └── v3
    
##配置NFS共享
[root@k8s-node2 ~]# vim /etc/exports
/data/k8s/v1 192.168.154.0/24(rw,sync,no_root_squash)
/data/k8s/v2 192.168.154.0/24(rw,sync,no_root_squash)
/data/k8s/v3 192.168.154.0/24(rw,sync,no_root_squash)

##启动nfs-server服务
[root@k8s-node2 ~]# systemctl start nfs-server.service 
[root@k8s-node2 ~]# systemctl enable nfs-server.service 
Created symlink from /etc/systemd/system/multi-user.target.wants/nfs-server.service to /usr/lib/systemd/system/nfs-server.service.
[root@k8s-node2 ~]# systemctl status nfs-server.service 
● nfs-server.service - NFS server and services
   Loaded: loaded (/usr/lib/systemd/system/nfs-server.service; enabled; vendor preset: disabled)
  Drop-In: /run/systemd/generator/nfs-server.service.d
           └─order-with-mounts.conf
   Active: active (exited) since 日 2020-04-05 22:05:04 CST; 11s ago
 Main PID: 74852 (code=exited, status=0/SUCCESS)
   CGroup: /system.slice/nfs-server.service

4月 05 22:05:04 k8s-node2 systemd[1]: Starting NFS server and services...
4月 05 22:05:04 k8s-node2 systemd[1]: Started NFS server and services.

4.2 PV

有了上面的 NFS 共享存储,下面我们就可以来使用 PV 和 PVC 了。PV 作为存储资源,主要包括存储能力、访问模式、存储类型、回收策略等关键信息,下面我们来新建一个 PV 对象,使用 nfs 类型的后端存储,1G 的存储空间,访问模式为 ReadWriteOnce,回收策略为 Recycle,对应的 YAML 文件如下:(pv1-demo.yaml)

apiVersion: v1
kind: PersistentVolume
metadata:
  name:  pv1
spec:
  capacity: 
    storage: 1Gi
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Recycle
  nfs:
    path: /data/k8s/v1
    server: 192.168.154.220

Kubernetes 支持的 PV 类型有很多,比如常见的 Ceph、GlusterFs、NFS,甚至 HostPath也可以,不过 HostPath 我们之前也说过仅仅可以用于单机测试,更多的支持类型可以前往 Kubernetes PV 官方文档进行查看,因为每种存储类型都有各自的特点,所以我们在使用的时候可以去查看相应的文档来设置对应的参数。

直接使用 kubectl 创建即可:

[root@k8s-master pv]# kubectl create -f pv1-demo.yaml 
persistentvolume/pv1 created
[root@k8s-master pv]# kubectl get pv
NAME   CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM   STORAGECLASS   REASON   AGE
pv1    1Gi        RWO            Recycle          Available                                   10s

可以看到 pv1 已经创建成功了,状态是 Available,表示 pv1 就绪,可以被 PVC 申请。我们来分别对上面的属性进行一些解读。

配置(Provisioning)

静态

集群管理员创建一些PV。它们带有可供群集用户使用的实际存储的细节。它们存在于 Kubernetes API 中,可用于消费。

动态

根据 StorageClasses,当管理员创建的静态 PV 都不匹配用户的 PersistentVolumeClaim 时,集群可能会尝试动态地为 PVC 创建卷。此设置基于StorageClasses:PVC必须请求storage class,并且管理员必须已经创建并配置了该类,才能进行动态设置。

容量(Capacity)

一般来说,一个 PV 对象都要指定一个存储能力,通过 PV 的 capacity属性来设置的,目前只支持存储空间的设置,就是我们这里的 storage=1Gi,不过未来可能会加入 IOPS、吞吐量等指标的配置。

单位:Ei、Pi、Ti、Gi、Mi、Ki(或者E、P、T、G、M、K、M)

卷模式(Volume Mode)

  • Kubernetes支持两个volume Modes:FilesystemBlock
  • volumeMode是可选的API参数。
  • Filesystem是volumeMode省略参数时使用的默认模式。

访问模式(Access Modes)

AccessModes 是用来对 PV 进行访问模式的设置,用于描述用户应用对存储资源的访问权限,访问权限包括下面几种方式:

  • ReadWriteOnce(RWO):读写权限,但是只能被单个节点挂载
  • ReadOnlyMany(ROX):只读权限,可以被多个节点挂载
  • ReadWriteMany(RWX):读写权限,可以被多个节点挂载

注意:一些 PV 可能支持多种访问模式,但是在挂载的时候只能使用一种访问模式,多种访问模式是不会生效的。

下图是一些常用的 Volume 插件支持的访问模式:

回收策略(Reclaim Policy)

当前的回收策略有三种:

  • Retain(保留):管理员手动回收
  • Recycle(回收):基本擦除 (rm -rf /thevolume/*)
  • Delete(删除):与 PV 相连的后端存储完成 volume 的删除操作,如 AWS EBS, GCE PD, Azure Disk, 或者 OpenStack Cinder volume。

当前仅NFS 和 HostPath 支持 recycling,AWS EBS, GCE PD, Azure Disk, 和 Cinder volumes 支持 deletion。

状态阶段(Phase)

一个 PV 的生命周期中,可能会处于4中不同的阶段:

  • Available(可用):表示可用状态,还未被任何 PVC 绑定
  • Bound(已绑定):表示 PV 已经被 PVC 绑定
  • Released(已释放):PVC 被删除,但是资源还未被集群重新声明
  • Failed(失败): 表示该 PV 的自动回收失败

4.3 PVC

Pod如果要想使用PV,需要创建PVC。

准备工作

在使用PVC之前,还得把集群节点上的 nfs 客户端给安装上,比如这里:

[root@k8s-master pv]# kubectl get nodes
NAME         STATUS   ROLES    AGE   VERSION
k8s-master   Ready    master   23d   v1.17.3
k8s-node1    Ready    <none>   23d   v1.17.3
k8s-node2    Ready    <none>   23d   v1.17.3

安装nfs客户端

[root@k8s-node1 ~]# yum install nfs-utils -y
[root@k8s-node1 ~]# systemctl start nfs.service 
[root@k8s-node1 ~]# systemctl enable nfs.service

创建PVC

同样的,我们来新建一个数据卷声明,我们来请求 1Gi 的存储容量,访问模式也是 ReadWriteOnce,YAML 文件如下:(pvc-nfs.yaml)

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: pvc-nfs
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi

创建并查看

[root@k8s-master pv]# kubectl create -f pvc-nfs.yaml 
persistentvolumeclaim/pvc-nfs created

##查看PVC已经绑定状态
[root@k8s-master pv]# kubectl get pvc
NAME      STATUS   VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE
pvc-nfs   Bound    pv1      1Gi        RWO                           9s

##查看PV也已经绑定,且和PVC:default/pvc-nfs绑定
[root@k8s-master pv]# kubectl get pv
NAME     CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM             STORAGECLASS   REASON   AGE
pv1      1Gi        RWO            Recycle          Bound       default/pvc-nfs                           53m

集群系统自动会去匹配PV和PVC,他会根据我们的声明要求去查找处于 Available 状态的 PV,如果没有找到的话,PVC 就会一直处于 Pending 状态,找到了的话就会把当前的 PVC 和目标 PV 进行绑定,这个时候状态就会变成 Bound 状态了。

例如:创建一个PVC2,空间是2G

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: pvc2-nfs
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 2Gi
  selector:
    matchLabels:
      app: nfs

执行命令进行创建

[root@k8s-master pv]# kubectl create -f pvc2-nfs.yaml 
persistentvolumeclaim/pvc2-nfs created

##发现其处于Pending状态
[root@k8s-master pv]# kubectl get pvc 
NAME       STATUS    VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE
pvc-nfs    Bound     pv1      1Gi        RWO                           9m19s
pvc2-nfs   Pending                                                     7s

可以创建一个和PVC:pvc2-nfs比较合适的PV,只是空间为1G大小。

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv2-nfs
  labels:
    app: nfs
spec:
  capacity:
    storage: 1Gi
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Recycle
  nfs:
    server: 192.168.154.220
    path: /data/k8s/v2

执行创建:

[root@k8s-master pv]# kubectl create -f pv2-nfs.yaml 
persistentvolume/pv2-nfs created

## 发现PVC依然是Pending状态
[root@k8s-master pv]# kubectl get pvc
NAME       STATUS    VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE
pvc-nfs    Bound     pv1      1Gi        RWO                           14m
pvc2-nfs   Pending                                                     5m15s

## 而PV2是可用的,说明是没有匹配上的
[root@k8s-master pv]# kubectl get pv
NAME      CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM             STORAGECLASS   REASON   AGE
pv1       1Gi        RWO            Recycle          Bound       default/pvc-nfs                           67m
pv2-nfs   1Gi        RWO            Recycle          Available                                             11s
pvtest    512M       RWO            Recycle          Available

将PV:pv2-nfs删除掉,将其大小改成3G。

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv2-nfs
  labels:
    app: nfs
spec:
  capacity:
    storage: 3Gi
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Recycle
  nfs:
    server: 192.168.154.220
    path: /data/k8s/v2

执行命令进行创建:

[root@k8s-master pv]# kubectl create -f pv2-nfs.yaml 
persistentvolume/pv2-nfs created

##发现刚创建的PV以及绑定状态了
[root@k8s-master pv]# kubectl get pv
NAME      CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM              STORAGECLASS   REASON   AGE
pv1       1Gi        RWO            Recycle          Bound    default/pvc-nfs                            73m
pv2-nfs   3Gi        RWO            Recycle          Bound    default/pvc2-nfs                           5s

##PVC2也已经绑定状态了,只是大小确是3G。
[root@k8s-master pv]# kubectl get pvc
NAME       STATUS   VOLUME    CAPACITY   ACCESS MODES   STORAGECLASS   AGE
pvc-nfs    Bound    pv1       1Gi        RWO                           20m
pvc2-nfs   Bound    pv2-nfs   3Gi        RWO                           10m

## PV是3Gi,PVC这里声明2Gi是不行的,你必须得使用3Gi。

4.4 使用PVC

创建一个Service

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nfs-pvc
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nfs-pvc
  template:
    metadata:
      labels:
        app: nfs-pvc
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 80
          name: web
        volumeMounts:
        - name: www
          mountPath: /usr/share/nginx/html
      volumes:
      - name: www
        persistentVolumeClaim:
          claimName: pvc2-nfs

---

apiVersion: v1
kind: Service
metadata:
  name: nfs-pvc
  labels:
    app: nfs-pvc
spec:
  type: NodePort
  ports:
  - port: 80
    targetPort: 80
  selector:
    app: nfs-pvc

执行命令进行创建并查看

[root@k8s-master pv]# kubectl apply -f nfs-pvc-depoly.yaml 
deployment.apps/nfs-pvc created
service/nfs-pvc created

##查看service
[root@k8s-master pv]# kubectl get svc -o wide
NAME         TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE    SELECTOR
kubernetes   ClusterIP   10.1.0.1       <none>        443/TCP        23d    <none>
my-service   ClusterIP   10.1.251.21    <none>        80/TCP         3d1h   app=my-service
nfs-pvc      NodePort    10.1.152.182   <none>        80:30871/TCP   20s    app=nfs-pvc

在集群外面使用Nodeport进行访问

因为我们把容器目录/user/share/nginx/html和挂载到了pvc2-nfs这个 PVC 上面,这个 PVC 就是对应着NFS服务器的共享数据目录的,该目录下面还没有任何数据,所以访问就出现了403。

[root@k8s-node2 ~]# cd /data/k8s/v2/
[root@k8s-node2 v2]# echo "<h1>pvc test</h1>" >index.html

然后再访问,即可成功看到内容:

现在已经正常,但是容器中的数据是直接放到共享数据目录根目录下面的,如果以后又有一个新的 nginx 容器也做了数据目录的挂载,就会有冲突,可以在 Pod 中使用一个新的属性:subPath,该属性可以来解决这个问题,只需要更改上面的 Pod 的 YAML 文件即可:

...
volumeMounts:
- name: www
  subPath: nginx001
  mountPath: /usr/share/nginx/html
...

删除以前的Service,重新创建

[root@k8s-master pv]# kubectl delete -f nfs-pvc-depoly.yaml 
deployment.apps "nfs-pvc" deleted
service "nfs-pvc" deleted

##删除deployment后,以前的文件依然是存在的
[root@k8s-node2 v2]# cd /data/k8s/v2/
[root@k8s-node2 v2]# ls
index.html

##重新部署修改后的yaml文件
[root@k8s-master pv]# kubectl apply -f nfs-pvc-depoly.yaml 
deployment.apps/nfs-pvc created
service/nfs-pvc created

##可以看到生成了新的子目录
[root@k8s-node2 v2]# cd /data/k8s/v2/
[root@k8s-node2 v2]# ls
index.html  nginx001

##在新的子目录中添加内容,重新访问
[root@k8s-node2 v2]# echo "<h1>this is subPath</h1>" > nginx001/index.html

##重新访问新的service
[root@k8s-master pv]# kubectl get svc
NAME         TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)        AGE
kubernetes   ClusterIP   10.1.0.1      <none>        443/TCP        23d
my-service   ClusterIP   10.1.251.21   <none>        80/TCP         3d9h
nfs-pvc      NodePort    10.1.1.188    <none>        80:30869/TCP   5m3s
[root@k8s-master pv]# curl 10.1.1.188
<h1>this is subPath</h1>

4.5 StorageClass

前面的 PV 都是静态的,要使用一个 PVC 的话就必须手动去创建一个 PV,这种方式在很大程度上并不能满足需求,比如我们有一个应用需要对存储的并发度要求比较高,而另外一个应用对读写速度又要求比较高,特别是对于 StatefulSet 类型的应用,简单的来使用静态的 PV 就很不合适了,这种情况下我们就需要用到动态 PV,也就 StorageClass。

创建

要使用 StorageClass,就得安装对应的自动配置程序,比如现在存储后端使用的是NFS,那么就需要使用到一个 nfs-client 的自动配置程序,也称它为 Provisioner,这个程序使用我们已经配置好的 NFS 服务器,来自动创建持久卷,也就是自动创建 PV。

  • 自动创建的 PV 以{namespace}-_namespace_−{pvcName}-${pvName}这样的命名格式创建在 NFS 服务器上的共享数据目录中;
  • 当这个 PV 被回收后会以archieved-{namespace}-_namespace_−{pvcName}-${pvName}这样的命名格式存在 NFS 服务器上。

参考:https://github.com/kubernetes-incubator/external-storage/tree/master/nfs-client

第一步:得到NFS服务器的连接信息
第二步:得到 NFS-Client Provisioner 文件
[root@k8s-master ~]# wget -c https://github.com/kubernetes-incubator/external-storage/archive/master.zip
[root@k8s-master ~]# unzip master.zip

##所有的部署文件均在deploy目录中
[root@k8s-master deploy]# cd /root/external-storage-master/nfs-client/deploy/
第三步:设置授权

如果是要安装在非默认的default命名空间,则需要修改deploy/rbac.yaml文件。

# Set the subject of the RBAC objects to the current namespace where the provisioner is being deployed
$ NS=$(kubectl config get-contexts|grep -e "^\*" |awk '{print $5}')
$ NAMESPACE=${NS:-default}
$ sed -i'' "s/namespace:.*/namespace: $NAMESPACE/g" ./deploy/rbac.yaml ./deploy/deployment.yaml
$ kubectl create -f deploy/rbac.yaml

授权yaml文件

apiVersion: v1
kind: ServiceAccount
metadata:
  name: nfs-client-provisioner
  # replace with namespace where provisioner is deployed
  namespace: default
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: nfs-client-provisioner-runner
rules:
  - apiGroups: [""]
    resources: ["persistentvolumes"]
    verbs: ["get", "list", "watch", "create", "delete"]
  - apiGroups: [""]
    resources: ["persistentvolumeclaims"]
    verbs: ["get", "list", "watch", "update"]
  - apiGroups: ["storage.k8s.io"]
    resources: ["storageclasses"]
    verbs: ["get", "list", "watch"]
  - apiGroups: [""]
    resources: ["events"]
    verbs: ["create", "update", "patch"]
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: run-nfs-client-provisioner
subjects:
  - kind: ServiceAccount
    name: nfs-client-provisioner
    # replace with namespace where provisioner is deployed
    namespace: default
roleRef:
  kind: ClusterRole
  name: nfs-client-provisioner-runner
  apiGroup: rbac.authorization.k8s.io
---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: leader-locking-nfs-client-provisioner
  # replace with namespace where provisioner is deployed
  namespace: default
rules:
  - apiGroups: [""]
    resources: ["endpoints"]
    verbs: ["get", "list", "watch", "create", "update", "patch"]
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: leader-locking-nfs-client-provisioner
  # replace with namespace where provisioner is deployed
  namespace: default
subjects:
  - kind: ServiceAccount
    name: nfs-client-provisioner
    # replace with namespace where provisioner is deployed
    namespace: default
roleRef:
  kind: Role
  name: leader-locking-nfs-client-provisioner
  apiGroup: rbac.authorization.k8s.io

执行并创建

[root@k8s-master deploy]# kubectl apply -f rbac.yaml 
serviceaccount/nfs-client-provisioner created
clusterrole.rbac.authorization.k8s.io/nfs-client-provisioner-runner created
clusterrolebinding.rbac.authorization.k8s.io/run-nfs-client-provisioner created
role.rbac.authorization.k8s.io/leader-locking-nfs-client-provisioner created
rolebinding.rbac.authorization.k8s.io/leader-locking-nfs-client-provisioner created

第四步:配置 NFS-Client provisioner

需要修改文件中的NFS服务器信息。主要是修改NFS_SERVER和NFS_PATH,如果想修改PROVISIONER_NAME不是fuseim.pri/ifs,例如:nfs-storage,这也是可以的,但你要也记得在storage class定义中修改PROVISIONER_NAME。

kind: Deployment
apiVersion: apps/v1
metadata:
  name: nfs-client-provisioner
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nfs-client-provisioner
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: nfs-client-provisioner
    spec:
      serviceAccountName: nfs-client-provisioner
      containers:
        - name: nfs-client-provisioner
          image: quay.io/external_storage/nfs-client-provisioner:latest
          volumeMounts:
            - name: nfs-client-root
              mountPath: /persistentvolumes
          env:
            - name: PROVISIONER_NAME
              value: fuseim.pri/ifs
            - name: NFS_SERVER
              value: 192.168.154.220
            - name: NFS_PATH
              value: /data/k8s/v3
      volumes:
        - name: nfs-client-root
          nfs:
            server: 192.168.154.220
            path: /data/k8s/v3

执行命令进行创建

[root@k8s-master deploy]# kubectl apply -f deployment.yaml 
deployment.apps/nfs-client-provisioner created

##查看deployment
[root@k8s-master deploy]# kubectl get deployment 
NAME                     READY   UP-TO-DATE   AVAILABLE   AGE
curl                     1/1     1            1           23d
nfs-client-provisioner   1/1     1            1           20s
nfs-pvc                  3/3     3            3           170m
nginx                    1/1     1            1           23d
test                     1/1     1            1           4d17h

##查看Pod
[root@k8s-master deploy]# kubectl get pods
NAME                                     READY   STATUS    RESTARTS   AGE
curl-69c656fd45-26jv2                    1/1     Running   6          23d
my-service-gb45b                         1/1     Running   1          3d12h
my-service-sxz97                         1/1     Running   1          3d12h
nfs-client-provisioner-c7b986bf7-hgs7h   1/1     Running   0          3m9s
nginx-86c57db685-hdwr8                   1/1     Running   5          23d
secret-pod3                              1/1     Running   0          18h
test-8656bc94b4-nsk77                    1/1     Running   9          4d11h

deploy/class.yaml 文件定义 NFS-Client 的 Kubernetes Storage Class:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: managed-nfs-storage
provisioner: fuseim.pri/ifs # or choose another name, must match deployment's env PROVISIONER_NAME'
parameters:
  archiveOnDelete: "false" # When set to "false" your PVs will not be archived
                           # by the provisioner upon deletion of the PVC.

创建并查看

[root@k8s-master deploy]# kubectl apply -f class.yaml
storageclass.storage.k8s.io/managed-nfs-storage created

##查看创建的storageclass
[root@k8s-master deploy]# kubectl get storageclass
NAME                  PROVISIONER      RECLAIMPOLICY   VOLUMEBINDINGMODE   ALLOWVOLUMEEXPANSION   AGE
managed-nfs-storage   fuseim.pri/ifs   Delete          Immediate           false                  19s

第五步:测试刚部署的环境

PVC样例文件,通过annotations属性和 StorageClass 相关联,这里都是:managed-nfs-storage。

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: test-claim
  annotations:
    volume.beta.kubernetes.io/storage-class: "managed-nfs-storage"
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 1Mi

Pod样例文件

kind: Pod
apiVersion: v1
metadata:
  name: test-pod
spec:
  containers:
  - name: test-pod
    image: busybox
    command:
      - "/bin/sh"
    args:
      - "-c"
      - "touch /mnt/SUCCESS && exit 0 || exit 1"
    volumeMounts:
      - name: nfs-pvc
        mountPath: "/mnt"
  restartPolicy: "Never"
  volumes:
    - name: nfs-pvc
      persistentVolumeClaim:
        claimName: test-claim

执行命令进行部署

[root@k8s-master deploy]# kubectl create -f test-claim.yaml -f test-pod.yaml 
persistentvolumeclaim/test-claim created
pod/test-pod created

##查看创建的PVC
[root@k8s-master deploy]# kubectl get pvc
NAME         STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS          AGE
pvc-nfs      Bound    pv1                                        1Gi        RWO                                  11h
pvc2-nfs     Bound    pv2-nfs                                    3Gi        RWO                                  11h
test-claim   Bound    pvc-1c6a812d-ad83-4408-984a-a5658601e51b   1Mi        RWX            managed-nfs-storage   104s

##查看自动创建的PV
[root@k8s-master deploy]# kubectl get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                STORAGECLASS          REASON   AGE
pv1                                        1Gi        RWO            Recycle          Bound    default/pvc-nfs                                     12h
pv2-nfs                                    3Gi        RWO            Recycle          Bound    default/pvc2-nfs                                    11h
pvc-1c6a812d-ad83-4408-984a-a5658601e51b   1Mi        RWX            Delete           Bound    default/test-claim   managed-nfs-storage            2m6s

到NFS服务器上查看

[root@k8s-node2 ~]# tree -C /data/k8s/v3/
/data/k8s/v3/
└── default-test-claim-pvc-1c6a812d-ad83-4408-984a-a5658601e51b 
    └── SUCCESS 

1 directory, 1 file

##自动生成的目录:default-test-claim-pvc-1c6a812d-ad83-4408-984a-a5658601e51b
##Pod生成的文件:SUCCESS

删除Pod及PVC,NFS服务器上自动创建的文件夹会被删除

[root@k8s-master deploy]# kubectl delete -f test-claim.yaml -f test-pod.yaml 
persistentvolumeclaim "test-claim" deleted
pod "test-pod" deleted

##检查NFS
[root@k8s-node2 ~]# tree -C /data/k8s/v3/
/data/k8s/v3/

0 directories, 0 files

第六步:部署你自己的 PersistentVolumeClaims

要部署自己的PVC,请确保拥有deploy/class.yaml文件所指示的正确存储类(storage-class)。

在实际工作中,使用 StorageClass 更多的是 StatefulSet 类型的服务,StatefulSet 类型的服务可以通过一个 volumeClaimTemplates 属性来直接使用 StorageClass。

apiVersion: v1
kind: Service
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  ports:
  - port: 80
    name: web
  clusterIP: None
  selector:
    app: nginx
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  selector:
    matchLabels:
      app: nginx # has to match .spec.template.metadata.labels
  serviceName: "nginx"
  replicas: 3 # by default is 1
  template:
    metadata:
      labels:
        app: nginx # has to match .spec.selector.matchLabels
    spec:
      terminationGracePeriodSeconds: 10
      containers:
      - name: nginx
        image: k8s.gcr.io/nginx-slim:0.8
        ports:
        - containerPort: 80
          name: web
        volumeMounts:
        - name: www
          mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
  - metadata:
      name: www
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: "my-storage-class"
      resources:
        requests:
          storage: 1Gi