<!-- 《基于 Kubernetes 的云原生 DevOps》第8章 运行容器 --> <!-- cloud-native-devops-with-kubernetes-ch-08 --> --- > If you have a tough question that you can't answer, start by tackling a simpler question that you cant't answer. > > -- Max Tegmark --- [TOC] ## 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 容器清单 每个容器的规范中必须指定的字段只有 *name* 和 *image* : ```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* - 环境标签:*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**:永远不会更新镜像。不推荐使用。 - 如果节点上已有镜像则使用; - 如果节点上没有镜像则容器启动失败; ```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 *ConfigMap* 或 *Secret* 对象,更多信息请参见第 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](https://docs.docker.com/engine/reference/run/#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-volume* 的 *emptyDir* 卷。 - 在 *demo* 容器中挂在了这个卷,路径为 */cache* 。 - 容器中凡是写入 */cache* 路径的数据都会写入卷,而且挂载了同一个卷的其它容器也可以看到写入的数据。所有挂载了这个卷的容器均可对其进行读写。 - **注意:**多个容器同时写入一个卷时,需要实现自己的写入锁定机制,或者使用支持锁定的卷类型(*nfs*、*glusterf*) **持久卷(Persistent Volume)** 创建持久卷的方法各个云服务商并不相同。 Pod 中将持久卷声明当作卷添加进来,以供服务器挂载和使用。 ```yaml volumes: - name: data-volume persistentVolumeClaim: claimName: data-pvc ``` ## 8.5 重启策略 默认情况下,每当 Pod 退出时, Kubernetes 就会重启该 Pod。 默认的重启策略是 *Always*,可以改为 *OnFailure* 或 *Never*。 - **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: true` 和 `allowPrivilegeEscalation: fasle` 。 - Linux 能力提供了一种细粒度的特权控制机制,但是容器默认提供的能力过于宽泛。请首先删除容器的所有能力,然后仅授予容器需要的特定能力。 - 同一个 Pod 中的容器可以通过读写挂载的卷的方式共享数据。最简单的卷类型为 *emptyDir* ,这个卷刚开始为空,而且只能在 Pod 运行期间保存数据。 - 另一方面,持久卷可以永久地保存数据。 Pod 可以使用持久卷声明动态设置新的持久卷。 Loading... 版权声明:本文为博主「佳佳」的原创文章,遵循 CC 4.0 BY-NC-SA 版权协议,转载请附上原文出处链接及本声明。 原文链接:https://www.liujiajia.me/2022/5/15/cloud-native-devops-with-kubernetes-ch-08 提交