Distroless与多阶段构建

2023年 7月 16日 73.3k 0

此前,使用centos以及ubuntu为容器基础镜像。后来为了缩减镜像的大小,使用alpine。这次,使用google的“Distroless”基本image做进一步的限制,这有助于我们构建更精简和更安全的镜像。20180904.png

介绍

从三个方面来考虑是否使用 Distroless:

  • 安全性

Distroless镜像仅包含应用程序及其运行时依赖项。它们不包含程序包管理器,shell或您希望在标准Linux发行版中找到的任何其他程序。从安全角度来看,您可以通过从镜像中删除不必要的组件来减少攻击面。容器中较少的代码/较少的程序意味着较少的攻击面。因此,基本OS可以更安全。

  • 时间和成本(alpine除外):

带宽和存储通常很昂贵。如果你是使用企业级的仓库,或者通过公网拉取镜像,如果镜像太大,这也不符合我们的预期。特别是但集群居多,容器上千时候,在分发到机器时就成了问题

  • 有效跟踪:

镜像中的所有内容都是另一个需要跟踪其来源并且需要针对已知漏洞进行扫描的内容!您在映像上安装的库和二进制文件越多,您的系统就越容易受到未发现的漏洞影响。只在容器中安装一个二进制文件可以降低总体风险。

Docker multi-stage在Docker CE 17.05(EE 17.06)中引入了构建,其中第一阶段构建产生可执行工件,在第二阶段构建中添加到运行时映像。您可能会惊讶地发现docker容器只能在其中使用二进制文件。这意味着当我们使用二进制文件时,多阶段构建工作非常完美,例如用nodejs

当然,我们可以使用Alpine,这是一个有效和推荐的建议!并且本人一直使用alpine。alpine非常小,但是兼容性差。Alpline基本上是一个Linux内核(带有grsecurity补丁的非官方端口),musl C库,BusyBox,LibreSSL和OpenRC!Alpine还使用自己的名为apk-tools的包管理器,可以用来安装你需要的运行时的依赖包!但问题是:这些差异是否满足你的需求,您是否愿意这么做!

如果你是在生产环境中运行容器,并且更关心安全性,那么可能 distroless 镜像更合适。 这就是谷歌distroless镜像的用武之地。“Distroless”图片仅包含您的应用程序及其运行时依赖项。它们不包含任何程序,例如:在Linux发行版中找到的shell和程序包管理器。

默认情况下,distroless镜象不包含shell。这意味着ENTRYPOINT必须在vector表单中指定Dockerfile 命令,以避免容器运行时前缀为shell。

nodejs测试

在Dockerfile中我们使用多阶段构建,使用distroless的nodejs,最终使用node index.js

FROM node:8.11.4-alpine
RUN mkdir /app
WORKDIR /app
COPY linuxea .

FROM gcr.io/distroless/nodejs
COPY --from=0 /app .
EXPOSE 3000
CMD ["index.js"]

index.js代码

[root@linuxea-Node_10_0_1_61 ~/nodjs]# cat linuxea/index.js 
const http = require('http');
const hostname = '0.0.0.0';
const port = 3000;
const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello Here is the nodejs test for linuxea.comn');
});
server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

目录结构

[root@linuxea-Node_10_0_1_61 ~/nodjs]# tree ./
./
|-- Dockerfile
`-- linuxea
    `-- index.js

1 directory, 2 files
[root@linuxea-Node_10_0_1_61 ~/nodjs]# 

多阶段构建docker build -t marksugar/node:v0.1 .--from=0 /app .使用参考官网

[root@linuxea-Node_10_0_1_61 ~/nodjs]# docker build -t marksugar/node:v0.1 .
Sending build context to Docker daemon  3.584kB
Step 1/8 : FROM node:8.11.4-alpine
 ---> e24de9a22ce3
Step 2/8 : RUN mkdir /app
 ---> Running in be94ab0d5aec
Removing intermediate container be94ab0d5aec
 ---> 09cd3b667fde
Step 3/8 : WORKDIR /app
Removing intermediate container 1263bd6627b2
 ---> a478edd21a7b
Step 4/8 : COPY linuxea .
 ---> 572c427a005b
Step 5/8 : FROM gcr.io/distroless/nodejs
 ---> b9e7cdd009c6
Step 6/8 : COPY --from=0 /app .
 ---> 75430ca17849
Step 7/8 : EXPOSE 3000
 ---> Running in e39bd57fc8b4
Removing intermediate container e39bd57fc8b4
 ---> 322d07c1ed78
Step 8/8 : CMD ["index.js"]
 ---> Running in 39ee293ca1f9
Removing intermediate container 39ee293ca1f9
 ---> 4852778c4115
Successfully built 4852778c4115
Successfully tagged marksugar/node:v0.1

上面这个构建的过程,相信你已经很明白了,从创建目录,到COPY,而后--from(COPY --from=0行仅将前一阶段的构建工件复制到此新阶段),在COPY到目录下启动启动

[root@linuxea-Node_10_0_1_61 ~/nodjs]# docker run --net=host -d marksugar/node:v0.1
dd234eba4a83535c460b91c63729d2d0213bbffd8ca47d2d3af454f35aa440e9

尝试sh

[root@linuxea-Node_10_0_1_61 ~/java]# docker exec -it dd234eba4a83 sh
OCI runtime exec failed: exec failed: container_linux.go:348: starting container process caused "exec: "sh": executable file not found in $PATH": unknown
[root@linuxea-Node_10_0_1_61 ~/java]# docker exec -it dd234eba4a83 bash
OCI runtime exec failed: exec failed: container_linux.go:348: starting container process caused "exec: "bash": executable file not found in $PATH": unknown

访问

[root@linuxea-Node_10_0_1_61 ~/nodjs]# curl 10.0.1.61:3000
Hello Here is the nodejs test for linuxea.com

java测试

简单测试java的镜象使用,来看Dockerfile将测试代码(hello word)copy到镜象内,而后mvn打包,随后在COP到gcr.io/distroless/java中,进行启动即可

FROM maven:3.5-jdk-8 AS build
COPY linuxea /linuxea
RUN mvn -f /linuxea/pom.xml clean package

FROM gcr.io/distroless/java
COPY --from=build /linuxea /linuxea
EXPOSE 8086
CMD ["/linuxea/target/hello-world-0.0.6.jar"]

直接build,build后直接Up

[root@linuxea-Node_10_0_1_61 ~/java]# docker build -t heloo-java:v0.1 . && docker run --net=host -d heloo-java:v0.1

已经运行

[root@linuxea-Node_10_0_1_61 ~/java]# docker ps -a
CONTAINER ID        IMAGE                 COMMAND                  CREATED             STATUS              PORTS               NAMES
c0c35b070403        heloo-java:v0.1       "/usr/bin/java -jar …"   24 seconds ago      Up 24 seconds                           hopeful_elbakyan

尝试sh

[root@linuxea-Node_10_0_1_61 ~/java]# docker exec -it hopeful_elbakyan sh
OCI runtime exec failed: exec failed: container_linux.go:348: starting container process caused "exec: "sh": executable file not found in $PATH": unknown
[root@linuxea-Node_10_0_1_61 ~/java]# docker exec -it hopeful_elbakyan bash
OCI runtime exec failed: exec failed: container_linux.go:348: starting container process caused "exec: "bash": executable file not found in $PATH": unknown

观察日志

[root@linuxea-Node_10_0_1_61 ~/java]# docker logs hopeful_elbakyan 

  .   ____          _            __ _ _
 /\ / ___'_ __ _ _(_)_ __  __ _    
( ( )___ | '_ | '_| | '_ / _` |    
 \/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |___, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::       (v1.5.10.RELEASE)

2018-09-04 08:58:48.071  INFO 1 --- [           main] com.dt.info.InfoSiteServiceApplication   : Starting InfoSiteServiceApplication v0.0.6 with PID 1 (/linuxea/target/hello-world-0.0.6.jar started by root in )
2018-09-04 08:58:48.078  INFO 1 --- [           main] com.dt.info.InfoSiteServiceApplication   : No active profile set, falling back to default profiles: default
2018-09-04 08:58:48.194  INFO 1 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@ba8a1dc: stary
2018-09-04 08:58:50.424  INFO 1 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8086 (http)
2018-09-04 08:58:50.445  INFO 1 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2018-09-04 08:58:50.446  INFO 1 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/8.5.27
2018-09-04 08:58:50.577  INFO 1 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2018-09-04 08:58:50.577  INFO 1 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 2390 ms
2018-09-04 08:58:50.743  INFO 1 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean  : Mapping servlet: 'dispatcherServlet' to [/]
2018-09-04 08:58:50.749  INFO 1 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'characterEncodingFilter' to: [/*]
2018-09-04 08:58:50.750  INFO 1 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2018-09-04 08:58:50.751  INFO 1 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'httpPutFormContentFilter' to: [/*]
2018-09-04 08:58:50.751  INFO 1 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'requestContextFilter' to: [/*]
2018-09-04 08:58:50.848  INFO 1 --- [           main] o.s.s.c.ThreadPoolTaskScheduler          : Initializing ExecutorService 
2018-09-04 08:58:50.859  INFO 1 --- [           main] o.s.s.c.ThreadPoolTaskScheduler          : Initializing ExecutorService  'getThreadPoolTaskScheduler'
2018-09-04 08:58:51.258  INFO 1 --- [           main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationCy
2018-09-04 08:58:51.370  INFO 1 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/index]}" onto public java.lang.String com.dt.info.controller.HelloController.hello()
2018-09-04 08:58:51.375  INFO 1 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springfram)
2018-09-04 08:58:51.375  INFO 1 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lan)
2018-09-04 08:58:51.417  INFO 1 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpR]
2018-09-04 08:58:51.417  INFO 1 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHa]
2018-09-04 08:58:51.466  INFO 1 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceH]
2018-09-04 08:58:51.494  INFO 1 --- [           main] oConfiguration$WelcomePageHandlerMapping : Adding welcome page: class path resource [static/index.html]
2018-09-04 08:58:51.684  INFO 1 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2018-09-04 08:58:51.828  INFO 1 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8086 (http)
2018-09-04 08:58:51.834  INFO 1 --- [           main] com.dt.info.InfoSiteServiceApplication   : Started InfoSiteServiceApplication in 4.621 seconds (JVM running for 5.805)

端口已经启动

[root@linuxea-Node_10_0_1_61 ~/java]# ss -tlnp
State      Recv-Q Send-Q                                                            Local Address:Port                                                                           Peer Address:Port              
LISTEN     0      128                                                                           *:22992                                                                                     *:*                  )
LISTEN     0      100                                                                           *:8086                                                                                      *:* 

curl访问试试。

[root@linuxea-Node_10_0_1_61 ~/java]# curl 10.0.1.61:8086
hello world !!!

测试没有问题,最后我们观察这两个镜象的大小,分别是136MB和75.1MB,坦白讲这个大小并不大

[root@linuxea-Node_10_0_1_61 ~/java]# docker images
REPOSITORY                    TAG                 IMAGE ID            CREATED             SIZE
heloo-java                    v0.1                7ebd053ab697        10 minutes ago      136MB
<none>                        <none>              fd6c1a602638        10 minutes ago      710MB
marksugar/node                v0.1                4852778c4115        25 hours ago        75.1MB
node-helloword                latest              3a82747d14ec        25 hours ago        75.1MB

相关文章

LeaferJS 1.0 重磅发布:强悍的前端 Canvas 渲染引擎
10分钟搞定支持通配符的永久有效免费HTTPS证书
300 多个 Microsoft Excel 快捷方式
一步步配置基于kubeadmin的kubevip高可用
istio全链路传递cookie和header灰度
REST Web 服务版本控制

发布评论