TL;DR
本文内容:
- 介绍 distroless 镜像、作用以及简略的应用
- 如何针对 distroless 容器的进行调试
- 长期容器 (v.1.18+) 的应用
Distroless 镜像
Distroless 容器,顾名思义应用 Distroless 镜像作为根底镜像运行的容器。
“Distroless” 镜像只蕴含了你的应用程序以及其运行时所须要的依赖。不蕴含你能在规范 Linxu 发行版里的能够找到的包管理器、shells 或者其余程序。
GoogleContainerTools/distroless 针对不同语言提供了 distroless 镜像:
- gcr.io/distroless/static-debian11
- gcr.io/distroless/base-debian11
- gcr.io/distroless/java-debian11
- gcr.io/distroless/cc-debian11
- gcr.io/distroless/nodejs-debian11
- gcr.io/distroless/python3-debian11
Distroless 镜像有什么用?
那些可能是构建镜像时须要的,但大部分并不是运行时须要的。这也是为什么上篇文章介绍 Buildpacks 时说的一个 builder 的 stack 镜像蕴含构建时根底镜像和运行时根底镜像,这样能够做到镜像的最小化。
其实管制体积并不是 distroless 镜像的次要作用。将运行时容器中的内容限度为应用程序所需的依赖,此外不应该装置任何货色。这种形式可能极大的晋升容器的安全性,也是 distroless 镜像的最重要作用。
这里并不会再深刻探索 distroless 镜像,而是如何调试 distroless 容器
没有了包管理器,镜像构建实现后就不能再应用相似 apt
、yum
的包管理工具;没有了 shell
,容器运行后无奈再进入容器。
“就像一个没有任何门的房间,也无奈装置门。” Distroless 镜像在晋升容器安全性的同时,也为调试减少了难度。
应用 distroless 镜像
写个很简略的 golang 利用:
package main
import (
"fmt"
"net/http"
)
func defaultHandler(w http.ResponseWriter, r *http.Request) {fmt.Fprintf(w, "Hello world!")
}
func main() {http.HandleFunc("/", defaultHandler)
http.ListenAndServe(":8080", nil)
}
比方应用 gcr.io/distroless/base-debian11
作为 golang 利用的根底镜像:
FROM golang:1.12 as build-env
WORKDIR /go/src/app
COPY . /go/src/app
RUN go get -d -v ./...
RUN go build -o /go/bin/app
FROM gcr.io/distroless/base-debian11
COPY --from=build-env /go/bin/app /
CMD ["/app"]
应用镜像创立 deployment
$ kubectl create deploy golang-distroless --image addozhang/golang-distroless-example:latest
$ kubectl get po
NAME READY STATUS RESTARTS AGE
golang-distroless-784bb4875-srmmr 1/1 Running 0 3m2s
尝试进入容器:
$ kubectl exec -it golang-distroless-784bb4875-srmmr -- sh
error: Internal error occurred: error executing command in container: failed to exec in container: failed to start exec "b76e800eafa85d39f909f39fcee4a4ba9fc2f37d5f674aa6620690b8e2939203": OCI runtime exec failed: exec failed: container_linux.go:380: starting container process caused: exec: "sh": executable file not found in $PATH: unknown
如何调试 Distroless 容器
1. 应用 distroless debug 镜像
GoogleContainerTools 为每个 distroless 镜像都提供了 debug
tag,适宜在开发阶段进行调试。如何应用?替换容器的 base 镜像:
FROM golang:1.12 as build-env
WORKDIR /go/src/app
COPY . /go/src/app
RUN go get -d -v ./...
RUN go build -o /go/bin/app
FROM gcr.io/distroless/base-debian11:debug # use debug tag here
COPY --from=build-env /go/bin/app /
CMD ["/app"]
从新构建镜像并部署,得益于 debug
镜像中提供了 busybox shell 让咱们能够 exec 到容器中。
2. debug 容器与共享过程命名空间
同一个 pod 中能够运行多个容器,通过设置 pod.spec.shareProcessNamespace
为 true
,来让同一个 Pod 中的多容器共享同一个过程命名空间。
Share a single process namespace between all of the containers in a pod.
When this is set containers will be able to view and signal processes from
other containers in the same pod, and the first process in each container
will not be assigned PID 1. HostPID and ShareProcessNamespace cannot both
be set. Optional: Default to false.
增加一个应用 ubuntu
镜像的 debug
容器,这里为了测试(前面解释)咱们为原容器增加 securityContext.runAsUser: 1000
,模仿两个容器应用不同的 UID 运行:
apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
app: golang-distroless
name: golang-distroless
spec:
replicas: 1
selector:
matchLabels:
app: golang-distroless
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
app: golang-distroless
spec:
shareProcessNamespace: true
containers:
- image: addozhang/golang-distroless-example:latest
name: golang-distroless-example
securityContext:
runAsUser: 1000
resources: {}
- image: ubuntu
name: debug
args: ['sleep', '1d']
securityContext:
capabilities:
add:
- SYS_PTRACE
resources: {}
status: {}
更新 deployment 之后:
$ kubectl get po
NAME READY STATUS RESTARTS AGE
golang-distroless-85c4896c45-rkjwn 2/2 Running 0 3m12s
$ kubectl get po -o json | jq -r '.items[].spec.containers[].name'
golang-distroless-example
debug
而后通过 debug 容器来进入到 pod 中:
$ kubectl exec -it golang-distroless-85c4896c45-rkjwn -c debug -- sh
而后在容器中执行:
$ ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 14:54 ? 00:00:00 /pause # infra 容器
1000 7 0 0 14:54 ? 00:00:00 /app # 原容器,UID 为 1000
root 19 0 0 14:55 ? 00:00:00 sleep 1d # debug 容器
root 25 0 0 14:55 pts/0 00:00:00 sh
root 32 25 0 14:55 pts/0 00:00:00 ps -ef
尝试拜访 过程 7
的过程空间:
$ cat /proc/7/environ
$ cat: /proc/7/environ: Permission denied
咱们须要为 debug
容器加上:
securityContext:
capabilities:
add:
- SYS_PTRACE
之后再拜访就失常了:
$ cat /proc/7/environ
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binHOSTNAME=golang-distroless-58b6c5f455-v9zkvSSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crtKUBERNETES_PORT_443_TCP=tcp://10.43.0.1:443KUBERNETES_PORT_443_TCP_PROTO=tcpKUBERNETES_PORT_443_TCP_PORT=443KUBERNETES_PORT_443_TCP_ADDR=10.43.0.1KUBERNETES_SERVICE_HOST=10.43.0.1KUBERNETES_SERVICE_PORT=443KUBERNETES_SERVICE_PORT_HTTPS=443KUBERNETES_PORT=tcp://10.43.0.1:443HOME=/root
同样咱们也能够拜访过程的文件系统:
$ cd /proc/7/root
$ ls
app bin boot dev etc home lib lib64 proc root run sbin sys tmp usr var
无需批改容器的根底镜像,应用 pod.spec.shareProcessNamespace: true
配合平安配置中减少 SYS_PTRACE
个性,为 debug 容器赋予残缺的 shell 拜访来调试利用。然而批改 YAML 和平安配置只适宜在测试环境应用,到了生产环境这些都是不容许的。
咱们就须要用到 kubectl debug
了。
3. Kubectl debug
针对不同的资源 kubectl debug
能够进行不同操作:
- 负载:创立一个正在运行的 Pod 的拷贝,并能够批改局部属性。比方在拷贝中应用新版本的 tag。
- 负载:为运行中的 Pod 减少一个长期容器(上面介绍),应用长期容器中的工具调试,无需重启 Pod。
- 节点:在节点上创立一个 Pod 运行在 节点的 host 命名空间,能够拜访节点的文件系统。
3.1 长期容器
从 Kubernetes 1.18 之后开始,能够应用 kubectl
为运行的 pod 增加一个长期容器。这个命令还处于 alpha
阶段,因而须要在“feature gate”中关上。
在应用 k3d 创立 k3s 集群时,关上 EphemeralContainers
feature:
$ k3d cluster create test --k3s-arg "--kube-apiserver-arg=feature-gates=EphemeralContainers=true"@
而后创立长期容器,创立实现后会间接进入容器:
$ kubectl debug golang-distroless-85c4896c45-rkjwn -it --image=ubuntu --image-pull-policy=IfNotPresent
#长期容器 shell
$ apt update && apt install -y curl
$ curl localhost:8080
Hello world!
值得注意的是,长期容器无奈与原容器共享过程命名空间:
$ ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 02:59 pts/0 00:00:00 bash
root 3042 1 0 03:02 pts/0 00:00:00 ps -ef
能够通过增加参数 --target=[container]
来将长期容器挂接到指标容器。这里与 pod.spec.shareProcessNamespace
并不同,过程号为 1 的过程是指标容器的过程,而后者的过程是 infra 容器的过程 /pause
:
$ kubectl debug golang-distroless-85c4896c45-rkjwn -it --image=ubuntu --image-pull-policy=IfNotPresent --target=golang-distroless-example
留神:目前的版本还不反对删除长期容器,参考 issue,反对的版本:
3.2 拷贝 Pod 并增加容器
除了增加长期容器以外,另一种形式就是创立一个 Pod 的拷贝,并增加一个容器。留神这里的是一般容器,不是长期容器。 留神这里加上了 --share-processes
$ kubectl debug golang-distroless-85c4896c45-rkjwn -it --image=ubuntu --image-pull-policy=IfNotPresent --share-processes --copy-to=golang-distroless-debug
留神这里加上了 --share-processes
,会主动加上 pod.spec.shareProcessNamespace=true
:
$ kubectl get po golang-distroless-debug -o jsonpath='{.spec.shareProcessNamespace}'
true
留神:应用 kubectl debug
调试,并不能为 pod 主动加上 SYS_PTRACE
平安个性,这就意味着如果容器应用的 UID 不统一,就无法访问过程空间。 截止发文,打算在 1.23
中反对。
总结
目前下面所有的都不适宜在生产环境应用,无奈在不批改 Pod 定义的状况下进行调试。
冀望 Kubernetes 1.23 版本之后 debug
性能增加 SYS_PTRACE
的反对。到时候,再尝试一下。
文章对立公布在公众号
云原生指北