我们使用容器进行软件打包已经有很长一段时间了。但让我们面对现实吧,Docker的用户体验其实并不是最好的。构建和测试通常需要有一堆的Makefile、繁琐的Dockerfiles、一点黑魔法和一捆Bash脚本。这是一条可能导致疯狂的道路。
这是一种耻辱,因为Docker可以更加友好。地球上的开发者当然相信这一点:他们已经创造了一个工具,填补了Docker和Make之间的空间。
在本教程的最后,你会知道Earthly是如何工作的。作为奖励,你将拥有一个可以构建、测试并将Docker镜像推送到远程仓库的管道。
Earthly是如何工作的
Earthly是一个基于Docker的构建工具。然而,它并没有取代Maven、Gradle或Webpack等特定语言的工具。相反,它利用并整合了这些工具--充当了胶水的角色。
使用Docker容器作为实现可重复性的核心机制,每个Earthly命令都在一个隔离的环境中运行,并尽可能毫不费力地进行并行化。
Earthly由CLI二进制文件和一个名为earthly/buildkit的Docker镜像组成,基于Docker的BuildKitd。
Earthly组件
Earthly的开发者希望你在任何地方都能获得相同的体验。BuildKit的作用是产生可移植的Docker兼容层。因此,无论是在我们的本地机器上还是在我们的CI系统中,我们都会得到同样的结果。
Earthly是否实现了它的承诺?
Earthly 承诺了更好的、可重复的构建。但是,它能实现吗?这里有一些最好的功能和一些在做试驾时发现的问题。
优点。
- Earthly,像Docker一样,是不分语言的。它的语法感觉很熟悉,因为它混合了Make和Dockerfiles。
- 他们的网站有很好的文档。
- 配置是低维护的,只有少数设置需要调整。
- 即使在你不需要Docker的情况下也很有用。容器镜像只是可能的输出之一。例如,你可以使用Earthly在一个干净的环境中测试和编译二进制文件。
- 导入系统在multirepo和monorepos中运行良好。
缺点:
- 它实际上并没有取代Makefile或Docker Compose。而且你仍然需要一些shell脚本。
- 它比本地Docker慢,因为它依赖于复制文件而不是挂载。当然,这也是为了确保可重复性而设计的。
- 多平台构建有时会使用仿真,所以对于某些用例来说,它可能太慢。
- 错误信息可能会变得非常混乱。
设置Earthly
Earthly的配置文件被称为,如软件工程中的典型,Earthfiles
。打开一个文件让我们想起了Dockerfiles和Makefiles的区别。
Earthly采用了Docker的语法,并将其扩展到一般的使用情况。虽然Dockerfiles只是为了生成容器镜像,但Earthly可以生成人工制品。
下面的Earthfile取自Earthly的官方指南。它描述了两个目标:build
和docker
FROM golang:1.15-alpine3.13
WORKDIR /go-example
build:
COPY main.go .
RUN go build -o build/go-example main.go
SAVE ARTIFACT build/go-example /go-example AS LOCAL build/go-example
docker:
COPY +build/go-example .
ENTRYPOINT ["/go-example/go-example"]
SAVE IMAGE go-example:latest
build
目标在容器内编译一个 Go 程序,而docker
则将其复制到一个生产就绪的镜像中,可以推送到任何容器注册表并在任何地方运行。
如果你熟悉Dockerfiles的话,FROM
关键字的作用与你预期的一样。同样,COPY
、ENTRYPOINT
、WORKDIR
、RUN
,也有类似的行为。有两个概念值得暂停一下,以进一步解释。
- 目标:我们想要构建的东西。Earthly使用一个基于目标的系统,其灵感来自Make。
- 引用:目标可以引用其他目标。就像在Make中一样,Earthly会对依赖关系进行分类并做正确的事情。
- 扩展命令:Earthly具有专门的命令,如SAVE IMAGE和SAVE ARTIFACT。
要运行这个Earthfile,我们需要运行:earthly +TARGET
。第一次,Earthly会拉出图像并创建一个缓存卷。缓存将在两次运行之间持续保存构建文件:
$ earthly +build
buildkitd | Found buildkit daemon as docker container (earthly-buildkitd)
golang:1.15-alpine3.13 | --> Load metadata linux/arm64
context | --> local context .
+base | --> FROM golang:1.15-alpine3.13
context | transferred 1 file(s) for context . (2.1 MB, 4 file/dir stats)
+base | *cached* --> WORKDIR /go-example
+build | *cached* --> COPY main.go .
+build | *cached* --> RUN go build -o build/go-example main.go
output | --> exporting outputs
================================ SUCCESS [main] ================================
+build | Artifact +build/go-example as local build/go-example
或者我们可以直接进入Docker生成目标用:
$ earthly +docker
+base | --> FROM golang:1.15-alpine3.13
+base | *cached* --> WORKDIR /go-example
context | transferred 3 file(s) for context . (2.1 MB, 4 file/dir stats)
+build | *cached* --> COPY main.go .
+build | *cached* --> RUN go build -o build/go-example main.go
+build | *cached* --> SAVE ARTIFACT build/go-example +build/go-example AS LOCAL build/go-example
+docker | --> COPY +build/go-example ./
output | [██████████] exporting layers ... 100%
output | [ ] exporting manifest
================================ SUCCESS [main] ================================
+docker | Image +docker as go-example:latest
+build | Artifact +build/go-example as local build/go-example
由于docker
目标依赖于build
,Earthly首先运行构建步骤,然后继续构建并导出镜像到本地Docker目录。
扩展Earthfiles
让我们在一个更复杂的例子中试试Earthly。请继续前进,分叉这个演示仓库:
TomFern / semaphore-demo-earthly
提供的Dockerfile构建了一个Node Alpine容器镜像:
FROM node:14.17-alpine3.12
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install
COPY src src
EXPOSE 3000
ENTRYPOINT ["npm", "start"]
你可以用:docker build . -t mydemo
构建它,然后以:docker run -it -p 3000:3000 mydemo
的方式运行它。浏览3000端口的localhost应该打印出 "hello, world "的信息。
我们将如何把它变成一个工作的Earthfile?让我们从头开始。
前两行保持不变,将用于每个目标:
FROM node:14.17-alpine3.12
WORKDIR /app
现在我们添加构建目标,它可以下载依赖项并复制源文件:
build:
COPY package.json package-lock.json ./
COPY --if-exists node_modules node_modules
RUN npm install
COPY src src
我们添加了一个COPY
语句,将node_modules文件夹复制到构建环境中。
由于npm在容器中运行,我们需要把它的输出文件复制到外面。我们用Earthly的特殊命令SAVE ARTIFACT ... AS LOCAL
来做这个:
SAVE ARTIFACT node_modules AS LOCAL ./node_modules
SAVE ARTIFACT package.json AS LOCAL ./package.json
SAVE ARTIFACT package-lock.json AS LOCAL ./package-lock.json
运行earthly +build
应该可以安装Node的依赖关系。这个过程的最终结果类似于我们直接运行npm install
,还有一个好处是我们在任何地方都能得到同样的结果。
接下来,我们应该创建Docker镜像。我们可以通过以下方式实现:
docker:
FROM +build
EXPOSE 3000
ENTRYPOINT ["npm", "start"]
SAVE IMAGE semaphore-demo-earthly:latest
新的目标从构建停止的地方接续。它添加了启动命令并将镜像导出到主机的本地Docker注册表。
用Earthly测试
Earthly带来的最强大的功能之一是能够用测试来扩展Dockerfiles。让我们通过扩展build
,添加单元测试,运行目标是:npm test
:
tests:
FROM +build
COPY spec spec
RUN npm test
现在我们可以运行earthly +tests
来获得结果。
添加一个提示目标也是微不足道的:
lint:
FROM +build
COPY .jshintrc ./
RUN npm run lint
为了运行更多的测试,如集成测试或端到端测试,我们可以重用现有的Docker Compose清单,以便在集成阶段调出其他容器:
integration-tests:
FROM +build
COPY docker-compose.yml ./
COPY integration-tests integration-tests
WITH DOCKER --compose docker-compose.yml --service db
RUN sleep 10 && npm run integration-tests
END
Earthly使用Docker-in-Docker方法来启动辅助容器:
- 在主容器(应用容器)中复制
docker-compose.yml
和集成测试。 - 在主容器内启动一个PostgreSQL容器。特殊的
WITH DOCKER
命令正好接受一个RUN
语句。 - 运行集成测试。
我们的Earthfile现在已经足够好了。让我们把它上传到资源库,这样我们就可以在下一节中配置CI/CD管道:
$ git add Earthfile
$ git commit -m "push Earthfile"
$ git push origin master
地球上的持续集成管道
我们现在已经准备好用Earthly和持续集成来测试应用程序了。我们的管道将从Docker Hub拉入和推送,这就需要对服务进行认证。
Semaphore的认证是通过秘密进行的。要创建一个加密的秘密,进入你的组织菜单,点击设置 > 秘密。然后,按创建秘密,输入变量DOCKER_USERNAME
和DOCKER_PASSWORD
,如下图所示:
Docker Hub秘密
接下来,初始化分叉的演示仓库。添加项目的程序在入门指南中有详细说明。如果这是第一次使用Semaphore,请去看看。
由于Earthly没有预装在Ubuntu CI镜像中,我们需要为每个作业安装它。实现这一目标的最简单方法是使用管道级的序言。全局序言中的命令会在管道中的每个作业之前执行。
Linux下的Earthly安装命令是:
sudo /bin/sh -c 'wget https://github.com/earthly/earthly/releases/latest/download/earthly-linux-amd64 -O /usr/local/bin/earthly && chmod +x /usr/local/bin/earthly && /usr/local/bin/earthly bootstrap --with-autocomplete'
点击流水线,在序言部分输入这一行:
构建块
第一个区块将开始一个构建阶段。在其中,我们将下载并缓存Node的依赖。
在作业中输入以下命令:
echo "${DOCKER_PASSWORD}" | docker login -u "${DOCKER_USERNAME}" --password-stdin
checkout
cache restore
earthly --ci +build
cache store
我们将在管道中的每个连续作业中运行这些命令或它们的变体:
- docker login:用秘密中定义的凭证登录Docker Hub。
- checkout:将版本库克隆到CI机器上。
- cache:通过cache命令,我们可以在Semaphore提供的项目级存储中存储
node_modules
。 - earthly ci:用一些CI优化的设置运行Earthly。
为了完成这个块,启用 "dockerhub "秘密,所以变量被解密并在作业中导出。
测试块
下一个块将运行快速测试,如单元测试和linting。在该块的序言中添加以下命令。
echo "${DOCKER_PASSWORD}" | docker login -u "${DOCKER_USERNAME}" --password-stdin
checkout
cache restore
接下来,创建两个作业:
earthly --ci +tests
和:
earthly --ci +lint
集成测试块
最后一个作业将运行集成测试。输入以下命令。-P
开关需要在Docker内部启用特权模式(Docker-in-Docker需要)。
echo "${DOCKER_PASSWORD}" | docker login -u "${DOCKER_USERNAME}" --password-stdin
checkout
cache restore
earthly --ci -P +integration-tests
不要忘记检查所有区块中的秘密是否被启用。准备好后,运行工作流以尝试CI管道。
CI管道
用Earthly持续交付
CI/CD流水线的最终结果是交付可以部署的东西。这里就是持续交付部分发挥作用的地方。我们将把图像推送到远程存储库,以便以后可以在生产中部署。
一个Semaphore工作流可以跨越多个管道,只要它们是通过促销活动连接的。回到工作流程编辑器中,点击添加推广。点击新的流水线,再次复制全局的Earthly安装命令。
sudo /bin/sh -c 'wget https://github.com/earthly/earthly/releases/latest/download/earthly-linux-amd64 -O /usr/local/bin/earthly && chmod +x /usr/local/bin/earthly && /usr/local/bin/earthly bootstrap --with-autocomplete'
管道中的区块将包含一个生成Docker镜像并将其推送到Docker Hub的单一作业。这些命令如下:
echo "${DOCKER_PASSWORD}" | docker login -u "${DOCKER_USERNAME}" --password-stdin
checkout
cache restore
earthly +docker
docker tag semaphore-demo-earthly $DOCKER_USERNAME/semaphore-demo-earthly
docker push $DOCKER_USERNAME/semaphore-demo-earthly
我们使用earthly +docker
,将镜像导出到CI机器中,然后用docker tag
和docker push
的组合,将其发送到远程注册中心。
剩下的就是启用Docker Hub的秘密了......我们就完成了!
重新运行工作流程,点击推广,尝试推送镜像。
最终构建
你觉得怎么样?
Earthly项目还很年轻,正在大力开发,许多功能仍处于实验阶段(在文章的过程中我们坚持使用稳定的功能)。它并不完美,但它感觉是在正确的方向上迈出了一大步。
用这些教程提高你的Docker知识:
- Kubernetes vs. Docker。了解2021年的容器
- 如何用Docker部署Go Web应用
- 如何用Docker部署Go网络应用程序?
- 如何将Ruby on Rails应用程序进行Docker化?
你尝试过Earthly吗?告诉我们你的经验。