使用 Distroless
镜像来保护 Kubernetes
上的容器。python
容器改变了咱们看待技术基础设施的方式。这是咱们运行应用程序方式的一次巨大飞跃。容器编排和云服务一块儿为咱们提供了一种近乎无限规模的无缝扩展能力。linux
根据定义,容器应该包含 应用程序 及其 运行时依赖项。然而,在现实中,它们包含的远不止这些。标准容器基础映像包含标准 Linux
发行版中能够找到的包管理器、shell
和其余程序。git
虽然这些都是构建容器镜像所必需的,但它们不该该成为最终镜像的一部分。例如,一旦你把包安装好了,就再也不须要在容器中使用 apt
等包管理工具了。github
这不只使你的容器里充满了没必要要的软件包和程序,并且还为网络罪犯提供了攻击特定程序漏洞的机会。docker
你应该始终了解容器运行时中存在什么,而且应该精确地限制其只包含应用程序所需的依赖项。shell
除了那些必要的,你不该该安装任何东西。一些领先的科技巨头,如谷歌,有多年在生产中运行容器的经验,已经采用了这种方法。编程
谷歌如今经过提供 Distroless
镜像向全世界开放这种能力。谷歌构建的这些镜像的目标是只包含你的应用程序及其依赖项,同时它们将没有常规 Linux
发行版的全部特性,包括 shell
。flask
这意味着虽然能够想之前同样运行应用程序的容器,但不能在容器运行的时候进入容器内。这是一个重大的安全改进,由于你如今已经为黑客经过 shell
进入你的容器关上了大门。api
谷歌为大多数流行的编程语言和平台提供了 Distroless
的基础镜像。安全
如下基础镜像是正式发布的版本:
Docker
来作一样的事情。关于使用 Distroless
镜像的一个有争议的问题是:当咱们有一个 Distroless
镜像时,咱们如何使用 Dockerfile
来构建咱们的应用程序呢?一般,Dockerfile
以一个标准的 OS 基础镜像开始,而后是建立适当的运行时构建所需执行的多个步骤。这包括包的安装,为此须要像 apt
或 yum
这样的包管理器。
有两种方法:
Docker
外部构建好你的应用程序,而后使用 Dockerfile
中的 ADD 或 COPY 指令将二进制包复制到容器中。Docker
构建。这是 Docker 17.05 及之后版本的一个新特性,它容许你将构建分为不一样的阶段。第一阶段能够从标准的 OS 基础镜像开始,能够帮助你构建应用程序;第二阶段能够简单地从第一阶段获取构建的文件并使用 Distroless
做为基础镜像。为了理解它是如何工做的,让咱们使用多阶段构建流程进行一个实际操做练习。
你须要具有如下内容:
Kubernetes
集群用于实践练习的第二部分。若是你想在 Docker
中运行你的容器,你可使用等价的 docker
命令。做为实践练习,将 此代码仓 Fork 到你的 GitHub 账号下,而后克隆 GitHub 代码仓并使用 cd
进入到项目目录下。
该代码仓包含一个 Python
的 Flask
应用程序,当你调用API时,该应用程序会响应 Hello World!
。
app.py
文件以下所示:
from flask import Flask app = Flask(__name__) @app.route("/") def hello(): return "Hello World!" if __name__ == '__main__': app.run(host='0.0.0.0', debug=True)
Dockerfile 包含两个阶段:
FROM python:2.7-slim AS build ADD . /app WORKDIR /app RUN pip install --upgrade pip RUN pip install -r ./requirements.txt FROM gcr.io/distroless/python2.7 COPY --from=build /app /app COPY --from=build /usr/local/lib/python2.7/site-packages /usr/local/lib/python2.7/site-packages WORKDIR /app ENV PYTHONPATH=/usr/local/lib/python2.7/site-packages EXPOSE 5000 CMD ["app.py"]
构建阶段:
pip
并安装依赖Distroless 阶段:
site-packages
从构建阶段复制到当前阶段的 site-packages
目录site-packages
目录,并暴露 5000 端口CMD
指令运行 app.py
因为 Disroless
镜像不包含 shell
,因此应该在最后使用 CMD
指令。若是不这样作,Docker 将认为它是一个 shell CMD,并试图这样执行它,但这是不工做的。
构建镜像:
$ docker build -t <your_docker_repo>/flask-hello-world-distroless . Sending build context to Docker daemon 95.74kB Step 1/12 : FROM python:2.7-slim AS build ---> eeb27ee6b893 Step 2/12 : ADD . /app ---> a01dc81df193 Step 3/12 : WORKDIR /app ---> Running in 48ccf6b990e4 Removing intermediate container 48ccf6b990e4 ---> 2e5e335be678 Step 4/12 : RUN pip install --upgrade pip ---> Running in 583be3d0b8cc Collecting pip Downloading pip-20.1.1-py2.py3-none-any.whl (1.5 MB) Installing collected packages: pip Attempting uninstall: pip Found existing installation: pip 20.0.2 Uninstalling pip-20.0.2: Successfully uninstalled pip-20.0.2 Successfully installed pip-20.1.1 Removing intermediate container 583be3d0b8cc ................................... Successfully installed Jinja2-2.11.2 MarkupSafe-0.23 click-7.1.2 flask-1.1.2 itsdangerous-0.24 werkzeug-1.0.1 Removing intermediate container c4d00b1abf4a ---> 01cbadcc531f Step 6/12 : FROM gcr.io/distroless/python2.7 ---> 796952c43cc4 Step 7/12 : COPY --from=build /app /app ---> 92657682cdcc Step 8/12 : COPY --from=build /usr/local/lib/python2.7/site-packages /usr/local/lib/python2.7/site-packages ---> faafd06edeac Step 9/12 : WORKDIR /app ---> Running in 0cf545aa0e62 Removing intermediate container 0cf545aa0e62 ---> 4c4af4333209 Step 10/12 : ENV PYTHONPATH=/usr/local/lib/python2.7/site-packages ---> Running in 681ae3cd51cc Removing intermediate container 681ae3cd51cc ---> 564f48eff90a Step 11/12 : EXPOSE 5000 ---> Running in 7ff5c073d568 Removing intermediate container 7ff5c073d568 ---> ccc3d211d295 Step 12/12 : CMD ["app.py"] ---> Running in 2b2c2f111423 Removing intermediate container 2b2c2f111423 ---> 76d13d2f61cd Successfully built 76d13d2f61cd Successfully tagged <your_docker_repo>/flask-hello-world-distroless:latest
登陆到 DockerHub 并推送镜像:
docker login docker push <your_docker_repo>/flask-hello-world-distroless:latest
登陆到 DockerHub(或者你的私有镜像仓),你应该会看到容器镜像可使用:
若是你看一下压缩后的大小,它只有 23.36 MB。若是你使用 slim
发行版做为基础镜像,它将占用 56 MB。
你已经减小了超过一半的容器占用空间。That’s amazing!
为了测试构建是否有效,让咱们在 Kubernetes 集群中运行容器。若是你没有 Kubernetes,你能够运行等价的 Docker 命令来作相同的活动,由于 Kubectl 和 Docker 命令是类似的。
我在代码仓中建立了一个 kubernetes.yaml 文件,该文件包含使用咱们构建的镜像的 Deployment
和 负载均衡的 Service
。
--- apiVersion: apps/v1 kind: Deployment metadata: name: flask-deployment spec: selector: matchLabels: app: flask replicas: 2 template: metadata: labels: app: flask spec: containers: - name: flask image: bharamicrosystems/flask-hello-world-distroless ports: - containerPort: 5000 --- apiVersion: v1 kind: Service metadata: name: flask-service spec: selector: app: flask ports: - port: 80 targetPort: 5000 type: LoadBalancer
这是一个很是简单的设置。负载均衡器监听端口 80 并映射到目标端口 5000。这些 Pods 在默认的 5000 端口上监听 Flask 应用程序。
应用:
$ kubectl apply -f kubernetes.yaml deployment.apps/flask-deployment created service/flask-service created
咱们查看一下全部的资源,看看咱们已经建立了什么:
$ kubectl get all NAME READY STATUS RESTARTS AGE pod/flask-deployment-576496558b-hnbxt 1/1 Running 0 47s pod/flask-deployment-576496558b-hszpq 1/1 Running 0 73s NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/flask-service LoadBalancer 10.8.9.163 35.184.113.120 80:31357/TCP 86s service/kubernetes ClusterIP 10.8.0.1 <none> 443/TCP 26m NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/flask-deployment 2/2 2 2 88s NAME DESIRED CURRENT READY AGE replicaset.apps/flask-deployment-576496558b 2 2 2 89s
咱们看到存在两个 Pods
、一个 Deployment
、一个带有外部 IP 的 LoadBalancer
服务和一个 ReplicaSet
。
让咱们访问应用程序:
$ curl http://35.184.113.120 Hello World!
咱们获得了 Hello World!
。这代表 Flask 应用程序在正常工做。
正如我在引言中所描述的,Disroless
容器中没有 shell
,所以不可能进入到容器内。然而,让咱们试着在容器中执行 exec:
$ kubectl exec -it flask-deployment-576496558b-hnbxt /bin/bash OCI runtime exec failed: exec failed: container_linux.go:349: starting container process caused "exec: \"/bin/bash\": stat /bin/bash: no such file or directory": unknown command terminated with exit code 126
咱们没法链接到容器上。
容器日志呢?若是拿不到容器日志,咱们就失去了调试应用程序的方法。
让咱们试着去拿日志:
$ kubectl logs flask-deployment-576496558b-hnbxt * Running on http://0.0.0.0:5000/ * Restarting with reloader 10.128.0.4 - - [31/May/2020 13:40:27] "GET / HTTP/1.1" 200 - 10.128.0.3 - - [31/May/2020 13:42:01] "GET / HTTP/1.1" 200 -
因此容器日志是能够被获取到的!
使用 Distroless
做为基础镜像是一种使人兴奋的保护容器安全的方式。因为镜像小而且仅包含应用程序和依赖项,所以它为应用程序提供了最小的攻击面。它在更大程度上提升了应用程序的安全性,因此它是保护容器安全的好方法。
谢谢阅读!我但愿你喜欢这篇文章。
本文翻译自 How to Harden Your Containers With Distroless Docker Images