基于 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 即学即用》
什么是容器?
从操作系统的角度来看,容器代表一个(或一组)位于各自命名空间中的隔离进程,容器内部的进程看不到外部的进程,反之亦然。
kubectl run busybox --image busybox:1.28 --rm -it --restart=Never /bin/sh
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 容器清单
每个容器的规范中必须指定的字段只有 name 和 image :
spec:
containers:
- name: container1
image: example/container1
- name: container2
image: example/container2
镜像标识符
每个镜像标识符都有四个不同的部分:
- 镜像仓库的主机名
- 镜像仓库的命名空间
- 镜像仓库(镜像名称)
- 标签
除镜像名称外,其它都是可选项。
示例:docker.io/cloudnatived/demo:hello
- 镜像仓库主机名:docker.io
这是 Docker 镜像的默认值。如果镜像存在其它仓库,则必须指定主机名。 - 镜像仓库命名空间:cloudnatived
不指定时使用默认命名空间 library - 镜像仓库:demo
指定某个特定的容器镜像。 - 标签:hello
标识同一个镜像的不同版本。
可以向镜像添加任意数量的标签。
不指定时默认标签为 latest 。
常用标签:- 语义版本标签:v1.3.0
- Git SHA 标签:ed7e7dc4
- 环境标签:staging、production
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:永远不会更新镜像。不推荐使用。
- 如果节点上已有镜像则使用;
- 如果节点上没有镜像则容器启动失败;
containers:
- name: demo
imagePullPolicy: Always
环境变量
环境变量是一种在运行时将信息传递到容器的方法。
环境变量只能是字符串。
进程环境的总大小上限为 32KiB。
如果容器镜像本身指定了环境变量,则会被 Kubernetes env 的设置覆盖。
containers:
- name: demo
image: cloudnatived/demo:latest
env:
- name: GREETING
value: "Hello from the enviroment"
还有一种更灵活地将配置数据传输到容器地方法是使用 Kubernetes ConfigMap 或 Secret 对象,更多信息请参见第 10 章。
8.3 容器安全
不要以 root 身份运行容器。因为这违反了最小权限原则(Principle of least privilege)。该原则要求程序只能访问完成工作必需的信息和资源。
以非 root 用户身份运行容器
containers:
- name: demo
image: cloudnatived/demo:hello
securityContext:
runAsUser: 1000
runAsUser 的值是 UID(用户数字标识符)。在许多 Linux 系统上,UID 1000 会被分配给系统上创建的第一个非 root 用户,因此通常容器中的 UID 选择 1000 或者更高的值比较安全。即使是空白的容器也可以这样指定。
如果清单和镜像中均未指定任何用户,则该容器将以 root 身份运行。
为了获得最高安全性,应该为每个容器选择一个不同的 UID。如果希望两个或多个容器能够访问相同的数据(例如通过挂载卷),则应为它们分配相同的 UID。
阻止 root 容器
containers:
- name: demo
image: cloudnatived/demo:hello
securityContext:
runAsNonRoot: true
运行容器时会检查该容器是否以 root 用户身份启动,如果以 root 用户身份运行,则拒绝启动。
这样可以避免忘记设置以非 root 用户身份启动。
最佳实践
以非 root 用户运行容器,并通过 runAsNonRoot: true 设置禁止以 root 用户身份运行容器。
设置只读文件系统
这个设置可以防止容器写入自己的文件系统。除非容器确实需要写入文件,否则最好设置 readOnlyRootFilesystem 。
containers:
- name: demo
image: cloudnatived/demo:hello
securityContext:
readOnlyRootFilesystem: true
禁用权限提升
通常,Linux 可执行文件在执行时获得的权限就是执行它们的用户的权限。但是有一个例外:拥有 setuid 机制的可执行文件可以临时获得该可执行文件拥有者(通常是 root)权限。
为避免这种情况,可将 allowPrivilegeEscalation 设置为 false 。
containers:
- name: demo
image: cloudnatived/demo:hello
securityContext:
allowPrivilegeEscalation: false
现代 Linux 程序不需要 setuid ,它可以使用更灵活、更小粒度的特权机制来达到这个目的,即能力(capability)。
能力(capability)
UNIX 程序拥有两个级别的权限: 普通用户 和 超级用户 。
超级用户可以做任何事,可以绕过所有的内核安全检查。
Linux 的能力(capalibity)机制改进了权限控制,它定义了多种特定操作,比如加载内核模块、执行直接的网络 I/O 操作、访问系统设备等。凡是有需要的程序都可以获得这些特定的权限,但无法获得其它权限。
最小权限原则表明,容器不应拥有不必要的能力。
Kubernetes 的安全上下文(securityContext)允许删除默认设置中的能力,还可以根据需要添加能力。
containers:
- name: demo
image: cloudnatived/demo:hello
securityContext:
capabilities:
drop: ["CHOWN", "NET_RAW", "SETPCAP"]
add: ["NET_ADMIN"]
Docker 官方文档:Runtime privilege and Linux capabilities 列出了默认情况下容器上设置的所有能力,以及可以根据需要添加的能力。
最大安全性: 删除容器的所有能力,仅添加需要的能力。
containers:
- name: demo
image: cloudnatived/demo:hello
securityContext:
capabilities:
drop: ["all"]
add: ["NET_BIND_SERVICE"]
能力机制对容器内部的进程进行了硬性限制,即使它们以 root 身份运行也是如此。 一旦容器删除了某一项能力,就无法重新再获得,即使是拥有最大权限的恶意进程也没办法。
Pod 安全上下文
也可以在 Pod 级别上设置安全上下文。
容器级别上的设置会覆盖 Pod 级别上的设置。
apiVersion: v1
kind: Pod
spec:
securityContext:
runAsUser: 1000
runAsNonRoot: false
allowPrivilegeEscalation: false
最佳实践
在所有 Pod 和容器中均设置安全上下文。禁用权限提升,并禁止所有能力。只添加容器所需要的特定能力。
Pod 安全策略
通过 PodSecurityPolicy 资源在集群级别上指定安全设置。
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 标志的容器)。
- 创建策略
- 将策略的访问权限通过 RBAC 赋给相关服务账号
- 启用集群中的 PodSecurityPolicy 准入控制器
Pod 服务账号:
运行 Pod 需要使用命名空间默认的服务账号的权限,除非另外指定。
如果处于某种原因,你需要授予额外的权限,则请问该应用创建专用的服务账号,然后将其绑定到所需的角色,再通过配置让 Pod 使用新的服务账号。
可以通过 serviceAccountName 字段指定运行 Pod 的服务账号。
apiVersion: v1
kind: Pod
spec:
serviceAccountName: deploy-tool
8.4 卷
Kubernetes 的卷(Volume)可以实现在多个容器间共享数据,也可以在容器重启后保留数据。
可以将多个不同类型的卷挂载到 Pod。挂载到 Pod 上的卷可供 Pod 中的所有容器访问。
emptyDir 卷
一种临时存储,刚开始的时候为空。
数据存储在节点上。
只有 Pod 在该节点上运行时,它才能持久保存数据。
如果你想为容器配置一些额外的存储,可以考虑 emptyDir ,但是这种卷无法永久地保存数据,也无法随着容器一起调度到别的节点上。
适合 emptyDir 的例子:
- 缓存下载文件
- 生产内容
- 使用空白工作空间执行数据处理的作业
apiVersion: v1
kind: Pod
spec:
volumes:
- name: cache-volume
emptyDir: {}
containers:
- name: demo
image: cloudnatived/demo:hello
volumeMounts:
- mountPath: /cache
name: cache-volume
- 创建一个名为 cache-volume 的 emptyDir 卷。
- 在 demo 容器中挂在了这个卷,路径为 /cache 。
- 容器中凡是写入 /cache 路径的数据都会写入卷,而且挂载了同一个卷的其它容器也可以看到写入的数据。所有挂载了这个卷的容器均可对其进行读写。
- 注意: 多个容器同时写入一个卷时,需要实现自己的写入锁定机制,或者使用支持锁定的卷类型(nfs、glusterf)
持久卷(Persistent Volume)
创建持久卷的方法各个云服务商并不相同。
Pod 中将持久卷声明当作卷添加进来,以供服务器挂载和使用。
volumes:
- name: data-volume
persistentVolumeClaim:
claimName: data-pvc
8.5 重启策略
默认情况下,每当 Pod 退出时,Kubernetes 就会重启该 Pod。
默认的重启策略是 Always,可以改为 OnFailure 或 Never。
- OnFailure:仅当容器以非零状态退出时才重启
- Never:从不重启
apiVersion: v1
kind: Pod
spec:
restartPolicy: OnFailure
8.6 镜像拉取机密
使用私人仓库时,拉取镜像需要提供仓库凭证。
- 将仓库凭证存储在 Secret 对象中
- 通过 imagePullSecrets 字段指定这个 Secret
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: true
和allowPrivilegeEscalation: fasle
。 - Linux 能力提供了一种细粒度的特权控制机制,但是容器默认提供的能力过于宽泛。请首先删除容器的所有能力,然后仅授予容器需要的特定能力。
- 同一个 Pod 中的容器可以通过读写挂载的卷的方式共享数据。最简单的卷类型为 emptyDir ,这个卷刚开始为空,而且只能在 Pod 运行期间保存数据。
- 另一方面,持久卷可以永久地保存数据。Pod 可以使用持久卷声明动态设置新的持久卷。