此前,使用centos以及ubuntu为容器基础镜像。后来为了缩减镜像的大小,使用alpine。这次,使用google的“Distroless”基本image做进一步的限制,这有助于我们构建更精简和更安全的镜像。
介绍
从三个方面来考虑是否使用 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