Skip to content

基于 Kubernetes 的云原生 DevOps 第 8 章 运行容器

🏷️ Kubernetes 《基于 Kubernetes 的云原生 DevOps》


If you have a tough question that you can't answer, start by tackling a simpler question that you cant't answer.

-- Max Tegmark


8.1 容器与 Pod

Pod 是 Kubernetes 的调度单位。Pod 对象代表一个容器或一组容器,Kubernetes 中运行的所有操作都是通过 Pod 来实现的。

Pod 代表在同一个执行环境中运行的应用程序的容器和卷集合。Pod(不是容器)是 Kubernetes 集群的最小可部署单位。这意味着一个 Pod 中的所有容器始终位于同一台机器上。
-- Kelsey Hightower 等,《Kubernets 即学即用》

什么是容器?

从操作系统的角度来看,容器代表一个(或一组)位于各自命名空间中的隔离进程,容器内部的进程看不到外部的进程,反之亦然。

powershell
kubectl run busybox --image busybox:1.28 --rm -it --restart=Never /bin/sh
powershell
PS C:\k8s> kubectl run busybox --image=busybox:1.28 --rm -it --restart=Never /bin/sh
If you don't see a command prompt, try pressing enter.
/ # ps ax
PID   USER     TIME  COMMAND
    1 root      0:00 /bin/sh
    8 root      0:00 ps ax
/ # hostname
busybox

通常,ps ax 会列出计算机上运行的所有进程,而且一般会有很多。这里仅显示了两个进程。因此,容器内部仅能看到实际正在容器中运行的进程。

容器中有什么?

一个容器可以运行任意多个进程,但最佳使用方法是: 一个容器只做一件事
容器有一个入口点,即在容器启动时运行的命令。如果想在容器中启动多个单独的进程,需要编写一个包装脚本作为入口,并由脚本来启动你想要的进程。

Pod 中有什么?

Pod 代表一组需要互相通信和共享数据的容器。

  • 一起调度
  • 一起启动和停止
  • 运行在同一台物理机上

Pod 中的容器应该共同完成一项工作。

8.2 容器清单

每个容器的规范中必须指定的字段只有 nameimage

yaml
spec:
  containers:
    - name: container1
      image: example/container1
    - name: container2
      image: example/container2

镜像标识符

每个镜像标识符都有四个不同的部分:

  1. 镜像仓库的主机名
  2. 镜像仓库的命名空间
  3. 镜像仓库(镜像名称)
  4. 标签
    除镜像名称外,其它都是可选项。

示例:docker.io/cloudnatived/demo:hello

  1. 镜像仓库主机名:docker.io
    这是 Docker 镜像的默认值。如果镜像存在其它仓库,则必须指定主机名。
  2. 镜像仓库命名空间:cloudnatived
    不指定时使用默认命名空间 library
  3. 镜像仓库:demo
    指定某个特定的容器镜像。
  4. 标签:hello
    标识同一个镜像的不同版本。
    可以向镜像添加任意数量的标签。
    不指定时默认标签为 latest
    常用标签:
    • 语义版本标签:v1.3.0
    • Git SHA 标签:ed7e7dc4
    • 环境标签:stagingproduction

latest 标签

如果在构建或推送镜像时未指定标签,则 latest 标签就会作为默认的标签加到镜像上。 latest 标签指向的镜像未必就是最新的镜像,只不过是最新的没有明确标记的镜像。因此 latest 并不适合用作标识符。

在生产环境部署容器时,应避免使用 latest 标签,因为我们很难通过该标签跟踪正在运行的镜像版本,而且也很难回滚。

容器摘要

摘要是一个镜像内容的加密哈希值,可以永久不变的标志该镜像。
镜像可以有多个标签,但只能有一个摘要。
可使用 命名空间/镜像仓库@sha256:摘要 的方式指定容器。

基础镜像标签

Dockerfile 中引用基础镜像时,如果不指定标签,则会使用 latest 。因此,建议指定某个特定的基础镜像标签。

为了保证构建的可复制性,请使用特定的标签或摘要。

端口

ports 字段指定了应用程序监听的端口。
它的作用仅仅是提示信息,对 Kubernetes 没有意义,但指定该字段是一个好习惯。

资源请求和约束

  • resources.requests.cpu
  • resources.requests.memory
  • resources.limits.cpu
  • resources.limits.memory

镜像拉取策略

  • Always:每次启动容器时都会拉取镜像。
  • IfNotPresent:默认值,如果节点上没有镜像,则下载镜像。
  • Never:永远不会更新镜像。不推荐使用。
    • 如果节点上已有镜像则使用;
    • 如果节点上没有镜像则容器启动失败;
yaml
containers:
- name: demo
  imagePullPolicy: Always

环境变量

环境变量是一种在运行时将信息传递到容器的方法。
环境变量只能是字符串。
进程环境的总大小上限为 32KiB
如果容器镜像本身指定了环境变量,则会被 Kubernetes env 的设置覆盖。

yaml
containers:
- name: demo
  image: cloudnatived/demo:latest
  env:
    - name: GREETING
      value: "Hello from the enviroment"

还有一种更灵活地将配置数据传输到容器地方法是使用 Kubernetes ConfigMapSecret 对象,更多信息请参见第 10 章。

8.3 容器安全

不要以 root 身份运行容器。因为这违反了最小权限原则(Principle of least privilege)。该原则要求程序只能访问完成工作必需的信息和资源。

以非 root 用户身份运行容器

yaml
containers:
- name: demo
  image: cloudnatived/demo:hello
  securityContext:
    runAsUser: 1000

runAsUser 的值是 UID(用户数字标识符)。在许多 Linux 系统上,UID 1000 会被分配给系统上创建的第一个非 root 用户,因此通常容器中的 UID 选择 1000 或者更高的值比较安全。即使是空白的容器也可以这样指定。

如果清单和镜像中均未指定任何用户,则该容器将以 root 身份运行。

为了获得最高安全性,应该为每个容器选择一个不同的 UID。如果希望两个或多个容器能够访问相同的数据(例如通过挂载卷),则应为它们分配相同的 UID。

阻止 root 容器

yaml
containers:
- name: demo
  image: cloudnatived/demo:hello
  securityContext:
    runAsNonRoot: true

运行容器时会检查该容器是否以 root 用户身份启动,如果以 root 用户身份运行,则拒绝启动。
这样可以避免忘记设置以非 root 用户身份启动。

最佳实践
以非 root 用户运行容器,并通过 runAsNonRoot: true 设置禁止以 root 用户身份运行容器。

设置只读文件系统

这个设置可以防止容器写入自己的文件系统。除非容器确实需要写入文件,否则最好设置 readOnlyRootFilesystem

yaml
containers:
- name: demo
  image: cloudnatived/demo:hello
  securityContext:
    readOnlyRootFilesystem: true

禁用权限提升

通常,Linux 可执行文件在执行时获得的权限就是执行它们的用户的权限。但是有一个例外:拥有 setuid 机制的可执行文件可以临时获得该可执行文件拥有者(通常是 root)权限。

为避免这种情况,可将 allowPrivilegeEscalation 设置为 false

yaml
containers:
- name: demo
  image: cloudnatived/demo:hello
  securityContext:
    allowPrivilegeEscalation: false

现代 Linux 程序不需要 setuid ,它可以使用更灵活、更小粒度的特权机制来达到这个目的,即能力capability)。

能力(capability)

UNIX 程序拥有两个级别的权限: 普通用户超级用户
超级用户可以做任何事,可以绕过所有的内核安全检查。

Linux 的能力(capalibity)机制改进了权限控制,它定义了多种特定操作,比如加载内核模块、执行直接的网络 I/O 操作、访问系统设备等。凡是有需要的程序都可以获得这些特定的权限,但无法获得其它权限。

最小权限原则表明,容器不应拥有不必要的能力。
Kubernetes 的安全上下文(securityContext)允许删除默认设置中的能力,还可以根据需要添加能力。

yaml
containers:
- name: demo
  image: cloudnatived/demo:hello
  securityContext:
    capabilities:
      drop: ["CHOWN", "NET_RAW", "SETPCAP"]
      add: ["NET_ADMIN"]

Docker 官方文档:Runtime privilege and Linux capabilities 列出了默认情况下容器上设置的所有能力,以及可以根据需要添加的能力。

最大安全性: 删除容器的所有能力,仅添加需要的能力。

yaml
containers:
- name: demo
  image: cloudnatived/demo:hello
  securityContext:
    capabilities:
      drop: ["all"]
      add: ["NET_BIND_SERVICE"]

能力机制对容器内部的进程进行了硬性限制,即使它们以 root 身份运行也是如此。 一旦容器删除了某一项能力,就无法重新再获得,即使是拥有最大权限的恶意进程也没办法。

Pod 安全上下文

也可以在 Pod 级别上设置安全上下文。
容器级别上的设置会覆盖 Pod 级别上的设置。

yaml
apiVersion: v1
kind: Pod
spec:
  securityContext:
    runAsUser: 1000
    runAsNonRoot: false
    allowPrivilegeEscalation: false

最佳实践
在所有 Pod 和容器中均设置安全上下文。禁用权限提升,并禁止所有能力。只添加容器所需要的特定能力。

Pod 安全策略

通过 PodSecurityPolicy 资源在集群级别上指定安全设置。

yaml
apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
  name: example
spec:
  priviledged: false
  seLinux:
    rule: RunAsAny
  supplementalGroups:
    rule: RunAsAny
  runAsUser:
    rule: RunAsAny
  fsGroup:
    rule: RunAsAny
  volumes:
  - *

这个简单的策略会阻止所有特权容器(securityContext 设置了 priviledged 标志的容器)。

  1. 创建策略
  2. 将策略的访问权限通过 RBAC 赋给相关服务账号
  3. 启用集群中的 PodSecurityPolicy 准入控制器

Pod 服务账号:

运行 Pod 需要使用命名空间默认的服务账号的权限,除非另外指定。

如果处于某种原因,你需要授予额外的权限,则请问该应用创建专用的服务账号,然后将其绑定到所需的角色,再通过配置让 Pod 使用新的服务账号。

可以通过 serviceAccountName 字段指定运行 Pod 的服务账号。

yaml
apiVersion: v1
kind: Pod
spec:
  serviceAccountName: deploy-tool

8.4 卷

Kubernetes 的卷(Volume)可以实现在多个容器间共享数据,也可以在容器重启后保留数据。

可以将多个不同类型的卷挂载到 Pod。挂载到 Pod 上的卷可供 Pod 中的所有容器访问。

emptyDir 卷

一种临时存储,刚开始的时候为空。
数据存储在节点上。
只有 Pod 在该节点上运行时,它才能持久保存数据。

如果你想为容器配置一些额外的存储,可以考虑 emptyDir ,但是这种卷无法永久地保存数据,也无法随着容器一起调度到别的节点上。

适合 emptyDir 的例子:

  • 缓存下载文件
  • 生产内容
  • 使用空白工作空间执行数据处理的作业
yaml
apiVersion: v1
kind: Pod
spec:
  volumes:
  - name: cache-volume
    emptyDir: {}
  containers:
  - name: demo
    image: cloudnatived/demo:hello
    volumeMounts:
    - mountPath: /cache
      name: cache-volume
  • 创建一个名为 cache-volumeemptyDir 卷。
  • demo 容器中挂在了这个卷,路径为 /cache
  • 容器中凡是写入 /cache 路径的数据都会写入卷,而且挂载了同一个卷的其它容器也可以看到写入的数据。所有挂载了这个卷的容器均可对其进行读写。
  • 注意: 多个容器同时写入一个卷时,需要实现自己的写入锁定机制,或者使用支持锁定的卷类型(nfsglusterf

持久卷(Persistent Volume)

创建持久卷的方法各个云服务商并不相同。

Pod 中将持久卷声明当作卷添加进来,以供服务器挂载和使用。

yaml
volumes:
- name: data-volume
  persistentVolumeClaim:
    claimName: data-pvc

8.5 重启策略

默认情况下,每当 Pod 退出时,Kubernetes 就会重启该 Pod。
默认的重启策略是 Always,可以改为 OnFailureNever

  • OnFailure:仅当容器以非零状态退出时才重启
  • Never:从不重启
yaml
apiVersion: v1
kind: Pod
spec:
  restartPolicy: OnFailure

8.6 镜像拉取机密

使用私人仓库时,拉取镜像需要提供仓库凭证。

  1. 将仓库凭证存储在 Secret 对象中
  2. 通过 imagePullSecrets 字段指定这个 Secret
yaml
apiVersion: v1
kind: Pod
spec:
  imagePullSecrets:
  - name: registry-creds

8.7 小结

  • 从内核级别上看,Linux 容器是一组隔离的进程,拥有隔离的资源。从容器内部看,容器就像是一台 Linux 机器。
  • 容器不是虚拟机。每个容器应该只运行一个主要进程。
  • 通常,Pod 包含一个运行主应用程序的容器,以及支持主应用程序的可选辅助容器。
  • 容器镜像规范可以包含镜像仓库主机名、镜像仓库命名空间、镜像仓库和标签,例如 docker.io/cloudnatived/demo:hello 。只有镜像名是必须的。
  • 为了实现可以重现的部署,请务必为容器镜像指定标签。否则,你会收到 latest 版本的影响。
  • 不要以 root 用户身份运行容器中的程序,请给它们分配一个普通用户。
  • 通过设置容器上的 runAsNonRoot: true 字段,可以阻止以 root 用户身份运行的任何容器。
  • 其它有关容器安全的设置包括 readOnlyRootFilesystem: trueallowPrivilegeEscalation: fasle
  • Linux 能力提供了一种细粒度的特权控制机制,但是容器默认提供的能力过于宽泛。请首先删除容器的所有能力,然后仅授予容器需要的特定能力。
  • 同一个 Pod 中的容器可以通过读写挂载的卷的方式共享数据。最简单的卷类型为 emptyDir ,这个卷刚开始为空,而且只能在 Pod 运行期间保存数据。
  • 另一方面,持久卷可以永久地保存数据。Pod 可以使用持久卷声明动态设置新的持久卷。