当前位置: 首页 > 科技观察

Docker的七大好实践_0

时间:2023-03-11 22:52:28 科技观察

译者|陈军评论|孙书娟众所周知,Dockerfile作为一个文本文档,包含了用户创建镜像的所有命令和指令。Docker可以通过读取Dockerfile中的指令自动构建镜像。因此,人们往往认为写一个Dockerfile应该很简单,从网上随便选一个例子,根据实际需要定制即可。然而,事实并非如此。由于生产环境的严格要求,尤其是在安全方面,有很多例子可能适合开发环境,但不一定适合生产环境。另外,由于Docker也提供了一套编写Dockerfile的指导策略,这就导致Dockerfile像写代码一样。你可能知道相关的语法,但你可能无法用特定的编程语言编写出干净简洁的代码。下面,我将和大家一起探讨编写Dockerfile时比较实用的7个优秀策略和理论实践。1.简介首先,让我们看一个典型的Dockerfile示例:DockerfileFROMeclipse-temurin:17RUNmkdir/opt/appARGJAR_FILEADDtarget/${JAR_FILE}/opt/app/app.jarCMD["java","-jar","/opt/app/app.jar"]根据其内容,Dockerfile将执行以下操作:lFROM:使用JavaDocker镜像--eclipse-temurin:17作为基础镜像;lRUN:为jar文件创建一个目录;lARG:通过提供参数--JAR_FILE避免将jar文件名硬编码到Dockerfile中;lADD:将jar文件添加到Docker镜像中;lCMD:包含运行容器时必须执行的命令。可以看到,在Git仓库的Dockerfiles目录下,可以找到上面每段生成的Dockerfile。并且在每个段落的末尾,还会在适用的地方提到相应的Dockerfile名称。下面,我们将通过修改这个Dockerfile来实现七个良好实践。2.先决条件在继续阅读以下内容之前,您需要具备的先决条件是:基本的Linux知识基本的Java和SpringBoot知识基本的Docker知识3.应用示例为了演示各种优秀的实践,我创建了一个BasicSpringBoot应用程序SpringWeb依赖项。该应用程序可以通过在存储库的根目录中调用以下命令来运行:Shell$MVNspring-boot:run为了构建Docker映像,我将使用Spotify的dockerfile-maven-plugin的一个分支。为此,我会将以下代码片段添加到pom文件中。XMLcom.xenoamess.dockerdockerfile-maven-plugin1.4.25mydeveloperplanet/dockerbestpractices${project.version}${project.build.finalName}.jar使用这个插件的好处是您可以轻松地重用配置。同时,为了通过Maven命令创建Docker镜像,您可以调用以下命令构建jar文件:Shell$mvncleanverify然后,请调用以下命令构建Docker镜像:Shell$mvndockerfile:build下面的命令可以让你运行Docker镜像:Shell$dockerrun--namedockerbestpracticesmydeveloperplanet/dockerbestpractices:0.0.1-SNAPSHOT然后,请使用下面的代码找到运行容器的IP地址:Shell$dockerinspectdocker最佳实践|grepIPAddress"SecondaryIPAddresses":null,"IPAddress":"172.17.0.3","IPAddress":"172.17.0.3"本例中的IP地址为172.17.0.3。同时,应用程序还包含一个仅用于响应hello消息的HelloController。此外,可以按如下方式调用Hello端点:Shell$curlhttp://172.17.0.3:8080/helloHelloDocker!至此,一切就绪。四、各种好的实践1、使用哪个镜像在上一篇文章中,我们提到本例Dockerfile使用的镜像是eclipse-temurin:17。接下来,让我们看看这个镜像是如何构建的:访问DockerHub上的链接;搜索“eclipse-temurin”;导航到“标签”;搜索17;按A-Z排序;单击标签17。如果仔细查看页面上每一层的详细信息并将其与标签17-JRE进行比较,您会注意到标签17包含完整的JDK,而标签17-JRE仅包含JRE。当然,后者足以运行Java应用程序。毕竟在生产环境中运行各种应用并不需要整个JDK。而且,由于开发工具可能被滥用,使用JDK也存在一定的安全问题。另外,tag17压缩后的图片大小为235MB,而17-jre压缩后的大小仅为89MB。为了进一步减小图像的大小,我们可以使用“瘦身”图像:17-jre-alpine。镜像压缩后大小为59MB,比17-jre减少了30MB,分发更方便。值得注意的是,上面使用的标签是通用标签,指向最新版本。这对于开发环境可能没问题,但对于生产环境你需要提前知道你使用的是哪个版本。本例中使用的标签是17.0.5_8-jre-alpine。如果想进一步加强安全性,可以在镜像版本中加入SHA256哈希。SHA256哈希可以在涵盖这些层的页面上找到。当SHA256的哈希值与Dockerfile中定义的哈希值不对应时,构建Docker镜像的过程就会失败。在本例中,Dockerfile的第一行:DockerfileFROMeclipse-temurin:17有了以上知识,我们可以将这一行改成:DockerfileFROMeclipse-temurin:17.0.5_8-jre-alpine@sha256:02c04793fa49ad5cd193c961403223755f9209a67894302f如下Docker映像已完成构建,您会注意到(解压缩后)映像已从475MB缩小到188MB。Shell$dockerimagesREPOSITORYTAGIMAGEIDCREATEDSIZEmydeveloperplanet/dockerbestpractices0.0.1-SNAPSHOT0b8d896166023secondsago188MB生成的Dockerfile在Git存储库中被命名为1-Dockerfile-specific-image。2.不要以root用户身份运行默认情况下,应用程序将以root用户身份在容器中运行。这显然暴露了很多漏洞,没有必要。为此,您应该为您的应用程序定义一个系统级用户。如下代码所示,在容器启动时的第一行日志中,可以看到应用是由Root启动的。Shell2022-11-2609:03:41.210INFO1---[main]m.MyDockerBestPracticesPlanetApplication:在3b06feee6c65上使用Java17.0.5启动MyDockerBestPracticesPlanetApplicationv0.0.1-SNAPSHOT,PID1(/opt/app/app.jarrootin/)我们可以通过将组javauser和用户javauser添加到Dockerfile来创建系统级用户。然后,通过将以下指令添加到您的Dockerfile来执行此操作。其中javauser为系统级用户,没有登录权限。请注意,组和用户创建步骤与符号组合在一行中,以便仅创建一层。DockerfileRUNaddgroup--systemjavauser&&adduser-S-S/usr/sbin/nologin-Gjavauserjavauser下表列出了adduser可用的完整参数集:-h,即DIR主目录-g,即GECOS字段-s,即登录SHELL-G,即group-S,即创建系统级用户-D,即不设置密码-H,即不设置创建家目录-u,即UID,用户id-k,即Skeleton目录(/etc/SKEL)同时,也可以将/opt/apt目录的属主更改为新的javauser添加如下行,否则javauser将无法访问该目录:DockerfileRUNchown-Rjavauser:javauser/opt/app最后,您需要通过USER命令确保容器中确实使用了javauser。其对应的完整Dockerfile为:DockerfileFROMeclipse-temurin:17.0.5_8-jre-alpine@sha256:02c04793fa49ad5cd193c961403223755f9209a67894622e05438598b32f210eRUNmkdir/opt/appRUNaddgroup--systemjavauser&&adduser-S-s/usr/sbin/nologin-GjavauserjavauserARGJAR_FILEADDtarget/${JAR_FILE}/opt/app/app.jarRUNchown-Rjavauser:javauser/opt/appUSERjavauserCMD["java","-jar","/opt/app/app.jar"]来测试这个新的图像,您首先需要使用以下命令停止并删除正在运行的容器。shell$dockerstopdockerbestpractices$dockerrmdockerbestpractices重新构建并运行容器后,如下代码所示,在日志第一行可以看到应用是由javauser启动的。Shell2022-11-2609:06:45.227INFO1---[main]m.MyDockerBestPracticesPlanetApplication:在ab1bcd38dff7上使用Java17.0.5启动MyDockerBestPracticesPlanetApplicationv0.0.1-SNAPSHOT,PID1(/opt/app/app.jarjavauserin/)同样,生成的Dockerfile在Git仓库中的名称为2-Dockerfile-do-not-run-as-root。3.使用WORKDIR在你使用的Dockerfile中,目录/opt/app被创建一次,毕竟这是你的工作目录。即使它不存在,Docker也会默认为您创建它。所以你不必每次都重复这条路径。例如,你会看到Dockerfile的第二行包含以下RUN指令:DockerfileRUNmkdir/opt/app我们可以使用WORKDIR指令稍微更改它:DockerfileWORKDIR/opt/app因为WORKDIR指令已经确保你是inthisdirectory,soyoucandeleteeveryreferenceto/opt/app.因此,新的Dockerfile如下代码所示:DockerfileFROMeclipse-temurin:17.0.5_8-jre-alpine@sha256:02c04793fa49ad5cd193c961403223755f9209a67894622e05438598b32f210eWORKDIR/opt/appRUNaddgroup--systemjavauser&&adduser-S-s/usr/sbin/nologin-GjavauserjavauserARGJAR_FILEADDtarget/${JAR_FILE}app.jarRUNchown-Rjavauser:javauser.USERjavauserCMD["java","-jar","app.jar"]构建并重新运行容器后,可以在日志中看到,jar文件仍然在/opt/app目录下执行:Shell2022-11-2616:07:18.503INFO1---[main]m.MyDockerBestPracticesPlanetApplication:使用J启动MyDockerBestPracticesPlanetApplicationv0.0.1-SNAPSHOTfe5cf9223143上的ava17.0.5,PID1(/opt/app/app.jar由javauser在/opt/app中启动)同样,生成的Dockerfile在Git存储库中被命名为3-Dockerfile-use-workdir。4.使用ENTRYPOINTCMD指令和使用ENTRYPOINT指令是有区别的。简而言之,两者的使用场景是:ENTRYPOINT:当你总是需要执行各种命令来构建一个可执行的Docker镜像时,你可以在命令中附加任意长的参数。CMD:当你想提供一组默认参数并允许它们在容器运行时被命令行覆盖时。那么,在运行Java应用程序的情况下,最好使用ENTRYPOINT。比如原来Dockerfile的最后一行:DockerfileCMD["java","-jar","app.jar"]现在可以改为:DockerfileENTRYPOINT["java","-jar","app.jar"]完成构建并重新运行容器,您不会注意到任何具体差异,容器仍将照常运行。生成的Dockerfile在Git存储库中被命名为4-Dockerfile-use-entrypoint。5.使用COPY而不是ADDCOPY和ADD指令看起来很相似。但是,COPY优于ADD。毕竟,COPY只是将文件复制到镜像中,而ADD还有一些额外的功能,比如从远程资源添加文件。Dockerfile中的ADD命令是:DockerfileADDtarget/${JAR_FILE}app.jar如果你使用COPY命令代替,它是:DockerfileCOPYtarget/${JAR_FILE}app.jar重新构建并运行容器,你将看不到任何内容重大更改,除了它在构建日志中显示COPY命令而不是ADD命令。生成的Dockerfile在Git存储库中可用,名称为5-Dockerfile-use-copy-instead-of-add。6、使用.dockerignore为了防止Docker镜像不小心添加文件,可以使用.dockerignore文件来指定哪些文件可以发送给Docker守护进程或者在镜像中使用。一种推荐的方法是忽略所有文件并仅显式添加您允许的文件。通过在.dockerignore文件中添加星号,我们可以排除所有子目录和文件。当然为了把jar文件放到build的上下文中,也可以使用感叹号来避免忽略jar文件。如下面的dockerignore文件所示,我们可以将其添加到我们运行Docker命令的目录中。例如,在此示例中,我们将其添加到Git存储库的根目录中。在PlainText**/**!target/*.jar完成构建并重新运行容器后,它可能不会发生重大变化。但是当您使用npm进行开发时,创建Docker镜像的过程会明显缩短,因为不再将node_modules目录复制到Docker构建的上下文中。请注意,您可以直接在Git存储库的Dockerfiles目录下找到dockerignore文件。7.以非root模式运行Docker守护进程默认情况下,Docker守护进程以Root身份运行。通过前面的讨论,您必须意识到潜在的安全问题。幸运的是,从Dockerv20.10开始,我们可以以非root用户身份运行Docker守护进程。此外,您还可以利用无守护进程的容器引擎——Podman(https://podman.io/)。默认情况下以非root模式运行。虽然有些人认为Podman是Docker的直接替代品,但它们在容器中挂载卷的方式不同。五、小结上面我们介绍了7个编写Dockerfile和运行容器的最佳实践。虽然编写Dockerfile并不复杂,但要想正确规范地编写,还是需要花一些时间去研究和理解它的指令。原文链接:https://dzone.com/articles/docker-best-practices译者介绍JulianChen(朱利安陈),社区编辑,十余年IT项目实施经验,善于把控内部和外部资源和风险,重点传播网络和信息安全知识和经验。