1 - 镜像
容器镜像(Image)所承载的是封装了应用程序及其所有软件依赖的二进制数据。
容器镜像是可执行的软件包,可以单独运行;该软件包对所处的运行时环境具有
良定(Well Defined)的假定。
你通常会创建应用的容器镜像并将其推送到某仓库(Registry),然后在
Pod 中引用它。
本页概要介绍容器镜像的概念。
镜像名称
容器镜像通常会被赋予 pause
、example/mycontainer
或者 kube-apiserver
这类的名称。
镜像名称也可以包含所在仓库的主机名。例如:fictional.registry.example/imagename
。
还可以包含仓库的端口号,例如:fictional.registry.example:10443/imagename
。
如果你不指定仓库的主机名,Kubernetes 认为你在使用 Docker 公共仓库。
在镜像名称之后,你可以添加一个标签(Tag)(与使用 docker
或 podman
等命令时的方式相同)。
使用标签能让你辨识同一镜像序列中的不同版本。
镜像标签可以包含小写字母、大写字母、数字、下划线(_
)、句点(.
)和连字符(-
)。
关于在镜像标签中何处可以使用分隔字符(_
、-
和 .
)还有一些额外的规则。
如果你不指定标签,Kubernetes 认为你想使用标签 latest
。
更新镜像
当你最初创建一个 Deployment、
StatefulSet、Pod
或者其他包含 Pod 模板的对象时,如果没有显式设定的话,Pod 中所有容器的默认镜像
拉取策略是 IfNotPresent
。这一策略会使得
kubelet
在镜像已经存在的情况下直接略过拉取镜像的操作。
镜像拉取策略
容器的 imagePullPolicy
和镜像的标签会影响 kubelet 尝试拉取(下载)指定的镜像。
以下列表包含了 imagePullPolicy
可以设置的值,以及这些值的效果:
IfNotPresent
- 只有当镜像在本地不存在时才会拉取。
Always
- 每当 kubelet 启动一个容器时,kubelet 会查询容器的镜像仓库,
将名称解析为一个镜像摘要。
如果 kubelet 有一个容器镜像,并且对应的摘要已在本地缓存,kubelet 就会使用其缓存的镜像;
否则,kubelet 就会使用解析后的摘要拉取镜像,并使用该镜像来启动容器。
Never
- Kubelet 不会尝试获取镜像。如果镜像已经以某种方式存在本地,
kubelet 会尝试启动容器;否则,会启动失败。
更多细节见提前拉取镜像。
只要能够可靠地访问镜像仓库,底层镜像提供者的缓存语义甚至可以使 imagePullPolicy: Always
高效。
你的容器运行时可以注意到节点上已经存在的镜像层,这样就不需要再次下载。
Note: 在生产环境中部署容器时,你应该避免使用 :latest
标签,因为这使得正在运行的镜像的版本难以追踪,并且难以正确地回滚。
相反,应指定一个有意义的标签,如 v1.42.0
。
为了确保 Pod 总是使用相同版本的容器镜像,你可以指定镜像的摘要;
将 <image-name>:<tag>
替换为 <image-name>@<digest>
,例如 image@sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2
。
当使用镜像标签时,如果镜像仓库修改了代码所对应的镜像标签,可能会出现新旧代码混杂在 Pod 中运行的情况。
镜像摘要唯一标识了镜像的特定版本,因此 Kubernetes 每次启动具有指定镜像名称和摘要的容器时,都会运行相同的代码。
通过摘要指定镜像可固定你运行的代码,这样镜像仓库的变化就不会导致版本的混杂。
有一些第三方的准入控制器
在创建 Pod(和 Pod 模板)时产生变更,这样运行的工作负载就是根据镜像摘要,而不是标签来定义的。
无论镜像仓库上的标签发生什么变化,你都想确保你所有的工作负载都运行相同的代码,那么指定镜像摘要会很有用。
默认镜像拉取策略
当你(或控制器)向 API 服务器提交一个新的 Pod 时,你的集群会在满足特定条件时设置 imagePullPolicy
字段:
- 如果你省略了
imagePullPolicy
字段,并且容器镜像的标签是 :latest
,
imagePullPolicy
会自动设置为 Always
。
- 如果你省略了
imagePullPolicy
字段,并且没有指定容器镜像的标签,
imagePullPolicy
会自动设置为 Always
。
- 如果你省略了
imagePullPolicy
字段,并且为容器镜像指定了非 :latest
的标签,
imagePullPolicy
就会自动设置为 IfNotPresent
。
Note:
容器的 imagePullPolicy
的值总是在对象初次 创建 时设置的,如果后来镜像的标签发生变化,则不会更新。
例如,如果你用一个 非 :latest
的镜像标签创建一个 Deployment,
并在随后更新该 Deployment 的镜像标签为 :latest
,则 imagePullPolicy
字段 不会 变成 Always
。
你必须手动更改已经创建的资源的拉取策略。
必要的镜像拉取
如果你想总是强制执行拉取,你可以使用下述的一中方式:
- 设置容器的
imagePullPolicy
为 Always
。
- 省略
imagePullPolicy
,并使用 :latest
作为镜像标签;
当你提交 Pod 时,Kubernetes 会将策略设置为 Always
。
- 省略
imagePullPolicy
和镜像的标签;
当你提交 Pod 时,Kubernetes 会将策略设置为 Always
。
- 启用准入控制器 AlwaysPullImages。
ImagePullBackOff
当 kubelet 使用容器运行时创建 Pod 时,容器可能因为 ImagePullBackOff
导致状态为
Waiting。
ImagePullBackOff
状态意味着容器无法启动,
因为 Kubernetes 无法拉取容器镜像(原因包括无效的镜像名称,或从私有仓库拉取而没有 imagePullSecret
)。
BackOff
部分表示 Kubernetes 将继续尝试拉取镜像,并增加回退延迟。
Kubernetes 会增加每次尝试之间的延迟,直到达到编译限制,即 300 秒(5 分钟)。
带镜像索引的多架构镜像
除了提供二进制的镜像之外,容器仓库也可以提供
容器镜像索引。
镜像索引可以根据特定于体系结构版本的容器指向镜像的多个
镜像清单。
这背后的理念是让你可以为镜像命名(例如:pause
、example/mycontainer
、kube-apiserver
)
的同时,允许不同的系统基于它们所使用的机器体系结构取回正确的二进制镜像。
Kubernetes 自身通常在命名容器镜像时添加后缀 -$(ARCH)
。
为了向前兼容,请在生成较老的镜像时也提供后缀。
这里的理念是为某镜像(如 pause
)生成针对所有平台都适用的清单时,
生成 pause-amd64
这类镜像,以便较老的配置文件或者将镜像后缀影编码到其中的
YAML 文件也能兼容。
使用私有仓库
从私有仓库读取镜像时可能需要密钥。
凭证可以用以下方式提供:
- 配置节点向私有仓库进行身份验证
- 所有 Pod 均可读取任何已配置的私有仓库
- 需要集群管理员配置节点
- 预拉镜像
- 所有 Pod 都可以使用节点上缓存的所有镜像
- 需要所有节点的 root 访问权限才能进行设置
- 在 Pod 中设置 ImagePullSecrets
- 特定于厂商的扩展或者本地扩展
- 如果你在使用定制的节点配置,你(或者云平台提供商)可以实现让节点
向容器仓库认证的机制
下面将详细描述每一项。
配置 Node 对私有仓库认证
设置凭据的具体说明取决于你选择使用的容器运行时和仓库。
你应该参考解决方案的文档来获取最准确的信息。
Note: Kubernetes 默认仅支持 Docker 配置中的 auths
和 HttpHeaders
部分,
不支持 Docker 凭据辅助程序(credHelpers
或 credsStore
)。
有关配置私有容器镜像仓库的示例,请参阅任务
从私有镜像库中提取图像。
该示例使用 Docker Hub 中的私有注册表。
config.json 说明
对于 config.json
的解释在原始 Docker 实现和 Kubernetes 的解释之间有所不同。
在 Docker 中,auths
键只能指定根 URL ,而 Kubernetes 允许 glob URLs 以及
前缀匹配的路径。这意味着,像这样的 config.json
是有效的:
{
"auths": {
"*my-registry.io/images": {
"auth": "…"
}
}
}
使用以下语法匹配根 URL (*my-registry.io
):
pattern:
{ term }
term:
'*' 匹配任何无分隔符字符序列
'?' 匹配任意单个非分隔符
'[' [ '^' ] 字符范围
字符集(必须非空)
c 匹配字符 c (c 不为 '*','?','\\','[')
'\\' c 匹配字符 c
字符范围:
c 匹配字符 c (c 不为 '\\','?','-',']')
'\\' c 匹配字符 c
lo '-' hi 匹配字符范围在 lo 到 hi 之间字符
现在镜像拉取操作会将每种有效模式的凭据都传递给 CRI 容器运行时。例如下面的容器镜像名称会匹配成功:
my-registry.io/images
my-registry.io/images/my-image
my-registry.io/images/another-image
sub.my-registry.io/images/my-image
a.sub.my-registry.io/images/my-image
kubelet 为每个找到的凭证的镜像按顺序拉取。 这意味着在 config.json
中可能有多项:
{
"auths": {
"my-registry.io/images": {
"auth": "…"
},
"my-registry.io/images/subpath": {
"auth": "…"
}
}
}
如果一个容器指定了要拉取的镜像 my-registry.io/images/subpath/my-image
,
并且其中一个失败,kubelet 将尝试从另一个身份验证源下载镜像。
提前拉取镜像
Note: 该方法适用于你能够控制节点配置的场合。
如果你的云供应商负责管理节点并自动置换节点,这一方案无法可靠地工作。
默认情况下,kubelet
会尝试从指定的仓库拉取每个镜像。
但是,如果容器属性 imagePullPolicy
设置为 IfNotPresent
或者 Never
,
则会优先使用(对应 IfNotPresent
)或者一定使用(对应 Never
)本地镜像。
如果你希望使用提前拉取镜像的方法代替仓库认证,就必须保证集群中所有节点提前拉取的镜像是相同的。
这一方案可以用来提前载入指定的镜像以提高速度,或者作为向私有仓库执行身份认证的一种替代方案。
所有的 Pod 都可以使用节点上提前拉取的镜像。
在 Pod 上指定 ImagePullSecrets
Note: 运行使用私有仓库中镜像的容器时,建议使用这种方法。
Kubernetes 支持在 Pod 中设置容器镜像仓库的密钥。
使用 Docker Config 创建 Secret
你需要知道用于向仓库进行身份验证的用户名、密码和客户端电子邮件地址,以及它的主机名。
运行以下命令,注意替换适当的大写值:
kubectl create secret docker-registry <name> --docker-server=DOCKER_REGISTRY_SERVER --docker-username=DOCKER_USER --docker-password=DOCKER_PASSWORD --docker-email=DOCKER_EMAIL
如果你已经有 Docker 凭据文件,则可以将凭据文件导入为 Kubernetes
Secret,
而不是执行上面的命令。
基于已有的 Docker 凭据创建 Secret
解释了如何完成这一操作。
如果你在使用多个私有容器仓库,这种技术将特别有用。
原因是 kubectl create secret docker-registry
创建的是仅适用于某个私有仓库的 Secret。
Note: Pod 只能引用位于自身所在名字空间中的 Secret,因此需要针对每个名字空间
重复执行上述过程。
在 Pod 中引用 ImagePullSecrets
现在,在创建 Pod 时,可以在 Pod 定义中增加 imagePullSecrets
部分来引用该 Secret。
例如:
cat <<EOF > pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: foo
namespace: awesomeapps
spec:
containers:
- name: foo
image: janedoe/awesomeapp:v1
imagePullSecrets:
- name: myregistrykey
EOF
cat <<EOF >> ./kustomization.yaml
resources:
- pod.yaml
EOF
你需要对使用私有仓库的每个 Pod 执行以上操作。
不过,设置该字段的过程也可以通过为
服务账号
资源设置 imagePullSecrets
来自动完成。
有关详细指令可参见
将 ImagePullSecrets 添加到服务账号。
你也可以将此方法与节点级别的 .docker/config.json
配置结合使用。
来自不同来源的凭据会被合并。
使用案例
配置私有仓库有多种方案,以下是一些常用场景和建议的解决方案。
- 集群运行非专有镜像(例如,开源镜像)。镜像不需要隐藏。
- 使用 Docker hub 上的公开镜像
- 无需配置
- 某些云厂商会自动为公开镜像提供高速缓存,以便提升可用性并缩短拉取镜像所需时间
- 集群运行一些专有镜像,这些镜像需要对公司外部隐藏,对所有集群用户可见
- 使用托管的私有 Docker 仓库。
- 可以托管在 Docker Hub 或者其他地方
- 按照上面的描述,在每个节点上手动配置
.docker/config.json
文件
- 或者,在防火墙内运行一个组织内部的私有仓库,并开放读取权限
- 使用控制镜像访问的托管容器镜像仓库服务
- 与手动配置节点相比,这种方案能更好地处理集群自动扩缩容
- 或者,在不方便更改节点配置的集群中,使用
imagePullSecrets
- 集群使用专有镜像,且有些镜像需要更严格的访问控制
- 集群是多租户的并且每个租户需要自己的私有仓库
- 确保 AlwaysPullImages 准入控制器。否则,所有租户的所有的 Pod 都可以使用所有镜像。
- 为私有仓库启用鉴权
- 为每个租户生成访问仓库的凭据,放置在 Secret 中,并将 Secrert 发布到各租户的命名空间下。
- 租户将 Secret 添加到每个名字空间中的 imagePullSecrets
如果你需要访问多个仓库,可以为每个仓库创建一个 Secret。
kubelet
将所有 imagePullSecrets
合并为一个虚拟的 .docker/config.json
文件。
What's next
2 - 容器环境
本页描述了在容器环境里容器可用的资源。
容器环境
Kubernetes 的容器环境给容器提供了几个重要的资源:
- 文件系统,其中包含一个镜像
和一个或多个的卷
- 容器自身的信息
- 集群中其他对象的信息
容器信息
容器的 hostname 是它所运行在的 pod 的名称。它可以通过 hostname
命令或者调用 libc 中的
gethostname
函数来获取。
Pod 名称和命名空间可以通过
下行 API
转换为环境变量。
Pod 定义中的用户所定义的环境变量也可在容器中使用,就像在 container 镜像中静态指定的任何环境变量一样。
集群信息
创建容器时正在运行的所有服务都可用作该容器的环境变量。
这里的服务仅限于新容器的 Pod 所在的名字空间中的服务,以及 Kubernetes 控制面的服务。
对于名为 foo 的服务,当映射到名为 bar 的容器时,以下变量是被定义了的:
FOO_SERVICE_HOST=<the host the service is running on>
FOO_SERVICE_PORT=<the port the service is running on>
服务具有专用的 IP 地址。如果启用了
DNS 插件,
可以在容器中通过 DNS 来访问服务。
What's next
3 - 容器运行时类(Runtime Class)
FEATURE STATE: Kubernetes v1.20 [stable]
本页面描述了 RuntimeClass 资源和运行时的选择机制。
RuntimeClass 是一个用于选择容器运行时配置的特性,容器运行时配置用于运行 Pod 中的容器。
动机
你可以在不同的 Pod 设置不同的 RuntimeClass,以提供性能与安全性之间的平衡。
例如,如果你的部分工作负载需要高级别的信息安全保证,你可以决定在调度这些 Pod
时尽量使它们在使用硬件虚拟化的容器运行时中运行。
这样,你将从这些不同运行时所提供的额外隔离中获益,代价是一些额外的开销。
你还可以使用 RuntimeClass 运行具有相同容器运行时但具有不同设置的 Pod。
设置
- 在节点上配置 CRI 的实现(取决于所选用的运行时)
- 创建相应的 RuntimeClass 资源
1. 在节点上配置 CRI 实现
RuntimeClass 的配置依赖于 运行时接口(CRI)的实现。
根据你使用的 CRI 实现,查阅相关的文档(下方)来了解如何配置。
Note: RuntimeClass 假设集群中的节点配置是同构的(换言之,所有的节点在容器运行时方面的配置是相同的)。
如果需要支持异构节点,配置方法请参阅下面的
调度。
所有这些配置都具有相应的 handler
名,并被 RuntimeClass 引用。
handler 必须是有效的 DNS 标签名。
2. 创建相应的 RuntimeClass 资源
在上面步骤 1 中,每个配置都需要有一个用于标识配置的 handler
。
针对每个 handler 需要创建一个 RuntimeClass 对象。
RuntimeClass 资源当前只有两个重要的字段:RuntimeClass 名 (metadata.name
) 和 handler (handler
)。
对象定义如下所示:
apiVersion: node.k8s.io/v1 # RuntimeClass 定义于 node.k8s.io API 组
kind: RuntimeClass
metadata:
name: myclass # 用来引用 RuntimeClass 的名字
# RuntimeClass 是一个集群层面的资源
handler: myconfiguration # 对应的 CRI 配置的名称
Note: 建议将 RuntimeClass 写操作(create、update、patch 和 delete)限定于集群管理员使用。
通常这是默认配置。参阅
授权概述了解更多信息。
使用说明
一旦完成集群中 RuntimeClasses 的配置,使用起来非常方便。
在 Pod spec 中指定 runtimeClassName
即可。例如:
apiVersion: v1
kind: Pod
metadata:
name: mypod
spec:
runtimeClassName: myclass
# ...
这一设置会告诉 kubelet 使用所指的 RuntimeClass 来运行该 pod。
如果所指的 RuntimeClass 不存在或者 CRI 无法运行相应的 handler,
那么 pod 将会进入 Failed
终止阶段。
你可以查看相应的事件,
获取执行过程中的错误信息。
如果未指定 runtimeClassName
,则将使用默认的 RuntimeHandler,相当于禁用 RuntimeClass 功能特性。
CRI 配置
关于如何安装 CRI 运行时,请查阅
CRI 安装。
dockershim
Dockershim 自 Kubernetes v1.20 起已弃用,并将在 v1.24 中删除。
有关弃用的更多信息查看 dockershim 弃用。
为 dockershim 设置 RuntimeClass 时,必须将运行时处理程序设置为 docker
。
Dockershim 不支持自定义的可配置的运行时处理程序。
通过 containerd 的 /etc/containerd/config.toml
配置文件来配置运行时 handler。
handler 需要配置在 runtimes 块中:
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.${HANDLER_NAME}]
更详细信息,请查阅 containerd
CRI 插件配置指南
通过 cri-o 的 /etc/crio/crio.conf
配置文件来配置运行时 handler。
handler 需要配置在
crio.runtime 表
下面:
[crio.runtime.runtimes.${HANDLER_NAME}]
runtime_path = "${PATH_TO_BINARY}"
更详细信息,请查阅 CRI-O 配置文档。
调度
FEATURE STATE: Kubernetes v1.16 [beta]
通过为 RuntimeClass 指定 scheduling
字段,
你可以通过设置约束,确保运行该 RuntimeClass 的 Pod 被调度到支持该 RuntimeClass 的节点上。
如果未设置 scheduling
,则假定所有节点均支持此 RuntimeClass 。
为了确保 pod 会被调度到支持指定运行时的 node 上,每个 node 需要设置一个通用的 label 用于被
runtimeclass.scheduling.nodeSelector
挑选。在 admission 阶段,RuntimeClass 的 nodeSelector 将会与
pod 的 nodeSelector 合并,取二者的交集。如果有冲突,pod 将会被拒绝。
如果 node 需要阻止某些需要特定 RuntimeClass 的 pod,可以在 tolerations
中指定。
与 nodeSelector
一样,tolerations 也在 admission 阶段与 pod 的 tolerations 合并,取二者的并集。
更多有关 node selector 和 tolerations 的配置信息,请查阅
将 Pod 分派到节点。
Pod 开销
FEATURE STATE: Kubernetes v1.18 [beta]
你可以指定与运行 Pod 相关的 开销 资源。声明开销即允许集群(包括调度器)在决策 Pod 和资源时将其考虑在内。
若要使用 Pod 开销特性,你必须确保 PodOverhead
特性门控
处于启用状态(默认为启用状态)。
Pod 开销通过 RuntimeClass 的 overhead
字段定义。
通过使用这些字段,你可以指定使用该 RuntimeClass 运行 Pod 时的开销并确保 Kubernetes 将这些开销计算在内。
What's next
4 - 容器生命周期回调
这个页面描述了 kubelet 管理的容器如何使用容器生命周期回调框架,
藉由其管理生命周期中的事件触发,运行指定代码。
概述
类似于许多具有生命周期回调组件的编程语言框架,例如 Angular、Kubernetes 为容器提供了生命周期回调。
回调使容器能够了解其管理生命周期中的事件,并在执行相应的生命周期回调时运行在处理程序中实现的代码。
容器回调
有两个回调暴露给容器:
PostStart
这个回调在容器被创建之后立即被执行。
但是,不能保证回调会在容器入口点(ENTRYPOINT)之前执行。
没有参数传递给处理程序。
PreStop
在容器因 API 请求或者管理事件(诸如存活态探针、启动探针失败、资源抢占、资源竞争等)
而被终止之前,此回调会被调用。
如果容器已经处于已终止或者已完成状态,则对 preStop 回调的调用将失败。
在用来停止容器的 TERM 信号被发出之前,回调必须执行结束。
Pod 的终止宽限周期在 PreStop
回调被执行之前即开始计数,所以无论
回调函数的执行结果如何,容器最终都会在 Pod 的终止宽限期内被终止。
没有参数会被传递给处理程序。
有关终止行为的更详细描述,请参见
终止 Pod。
回调处理程序的实现
容器可以通过实现和注册该回调的处理程序来访问该回调。
针对容器,有两种类型的回调处理程序可供实现:
- Exec - 在容器的 cgroups 和名称空间中执行特定的命令(例如
pre-stop.sh
)。
命令所消耗的资源计入容器的资源消耗。
- HTTP - 对容器上的特定端点执行 HTTP 请求。
回调处理程序执行
当调用容器生命周期管理回调时,Kubernetes 管理系统根据回调动作执行其处理程序,
httpGet
和 tcpSocket
在kubelet 进程执行,而 exec
则由容器内执行 。
回调处理程序调用在包含容器的 Pod 上下文中是同步的。
这意味着对于 PostStart
回调,容器入口点和回调异步触发。
但是,如果回调运行或挂起的时间太长,则容器无法达到 running
状态。
PreStop
回调并不会与停止容器的信号处理程序异步执行;回调必须在
可以发送信号之前完成执行。
如果 PreStop
回调在执行期间停滞不前,Pod 的阶段会变成 Terminating
并且一直处于该状态,直到其 terminationGracePeriodSeconds
耗尽为止,
这时 Pod 会被杀死。
这一宽限期是针对 PreStop
回调的执行时间及容器正常停止时间的总和而言的。
例如,如果 terminationGracePeriodSeconds
是 60,回调函数花了 55 秒钟
完成执行,而容器在收到信号之后花了 10 秒钟来正常结束,那么容器会在其
能够正常结束之前即被杀死,因为 terminationGracePeriodSeconds
的值
小于后面两件事情所花费的总时间(55+10)。
如果 PostStart
或 PreStop
回调失败,它会杀死容器。
用户应该使他们的回调处理程序尽可能的轻量级。
但也需要考虑长时间运行的命令也很有用的情况,比如在停止容器之前保存状态。
回调递送保证
回调的递送应该是 至少一次,这意味着对于任何给定的事件,
例如 PostStart
或 PreStop
,回调可以被调用多次。
如何正确处理被多次调用的情况,是回调实现所要考虑的问题。
通常情况下,只会进行单次递送。
例如,如果 HTTP 回调接收器宕机,无法接收流量,则不会尝试重新发送。
然而,偶尔也会发生重复递送的可能。
例如,如果 kubelet 在发送回调的过程中重新启动,回调可能会在 kubelet 恢复后重新发送。
调试回调处理程序
回调处理程序的日志不会在 Pod 事件中公开。
如果处理程序由于某种原因失败,它将播放一个事件。
对于 PostStart
,这是 FailedPostStartHook
事件,对于 PreStop
,这是 FailedPreStopHook
事件。
要自己生成失败的 FailedPreStopHook
事件,请修改
lifecycle-events.yaml
文件将 postStart 命令更改为 ”badcommand“ 并应用它。
以下是通过运行 kubectl describe pod lifecycle-demo
后你看到的一些结果事件的示例输出:
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 7s default-scheduler Successfully assigned default/lifecycle-demo to ip-XXX-XXX-XX-XX.us-east-2...
Normal Pulled 6s kubelet Successfully pulled image "nginx" in 229.604315ms
Normal Pulling 4s (x2 over 6s) kubelet Pulling image "nginx"
Normal Created 4s (x2 over 5s) kubelet Created container lifecycle-demo-container
Normal Started 4s (x2 over 5s) kubelet Started container lifecycle-demo-container
Warning FailedPostStartHook 4s (x2 over 5s) kubelet Exec lifecycle hook ([badcommand]) for Container "lifecycle-demo-container" in Pod "lifecycle-demo_default(30229739-9651-4e5a-9a32-a8f1688862db)" failed - error: command 'badcommand' exited with 126: , message: "OCI runtime exec failed: exec failed: container_linux.go:380: starting container process caused: exec: \"badcommand\": executable file not found in $PATH: unknown\r\n"
Normal Killing 4s (x2 over 5s) kubelet FailedPostStartHook
Normal Pulled 4s kubelet Successfully pulled image "nginx" in 215.66395ms
Warning BackOff 2s (x2 over 3s) kubelet Back-off restarting failed container
What's next