如何使用带有 Makefile 的容器构建我的个人网站

通过将构建、测试和部署项目的命令组合到 Makefile 中,简化容器管理。
73 位读者喜欢这篇文章。
Parts, modules, containers for software

Opensource.com

make 实用程序及其相关的 Makefile 已经使用了很长时间来构建软件。 Makefile 定义了一组要运行的命令,而 make 实用程序运行这些命令。 它类似于 Dockerfile 或 Containerfile - 一组用于构建容器镜像的命令。

Makefile 和 Containerfile 结合使用是管理基于容器的项目的绝佳方式。 Containerfile 描述了容器镜像的内容,而 Makefile 描述了如何管理项目本身:启动镜像构建、测试和部署,以及其他有用的命令。

Make 目标

Makefile 由“目标”组成:一个或多个命令组合在一个命令下。 您可以通过运行 make 命令,后跟您要运行的目标来运行每个目标

# Runs the "build_image" make target from the Makefile
$ make build_image

这就是 Makefile 的魅力所在。 您可以为每个需要手动执行的任务构建一组目标。 在基于容器的项目的上下文中,这包括构建镜像、将其推送到注册表、测试镜像,甚至部署镜像并更新运行它的服务。 我使用 Makefile 来使我的个人网站以一种简单、自动化的方式完成所有这些任务。

构建、测试、部署

我使用 Hugo 构建我的网站,Hugo 是一个静态网站生成器,可以从 YAML 文件构建静态 HTML。 我使用 Hugo 为我构建 HTML 文件,然后使用这些文件和 Caddy(一个快速而简单的 Web 服务器)构建一个容器镜像,并将该镜像作为容器运行。(Hugo 和 Caddy 都是开源的、采用 Apache 许可的项目。)我使用 Makefile 来使构建和将该镜像部署到生产环境更加容易。

Makefile 中的第一个目标恰当地是 image_build 命令

image_build:
  podman build --format docker -f Containerfile -t $(IMAGE_REF):$(HASH) .

此目标调用 Podman 以从项目中包含的 Containerfile 构建镜像。 上面的命令中有一些变量 - 它们是什么? 可以在 Makefile 中指定变量,类似于 Bash 或编程语言。 我在 Makefile 中将它们用于各种事情,但最有用的用途是构建要推送到远程容器镜像注册表的镜像引用

# Image values
REGISTRY := "us.gcr.io"
PROJECT := "my-project-name"
IMAGE := "some-image-name"
IMAGE_REF := $(REGISTRY)/$(PROJECT)/$(IMAGE)

# Git commit hash
HASH := $(shell git rev-parse --short HEAD)

使用这些变量,image_build 目标构建一个像 us.gcr.io/my-project-name/my-image-name:abc1234 这样的镜像引用,使用短 Git 修订版本哈希作为镜像标签,以便可以轻松地将其绑定到构建它的代码。

然后,Makefile 将该镜像标记为 :latest。 我通常不会在生产环境中使用 :latest,但在 Makefile 的后面,它将在清理时变得有用

image_tag:
  podman tag $(IMAGE_REF):$(HASH) $(IMAGE_REF):latest

所以,现在镜像已经构建完成,需要进行验证,以确保它满足一些最低要求。 对于我的个人网站来说,这实际上只是,“Web 服务器是否启动并返回某些内容?” 这可以通过 Makefile 中的 shell 命令来完成,但是对我来说,编写一个 Python 脚本更容易,该脚本使用 Podman 启动一个容器,向容器发出 HTTP 请求,验证它是否收到回复,然后清理容器。 Python 的“try, except, finally”异常处理非常适合这一点,并且比在 Makefile 中从 shell 命令复制相同的逻辑要容易得多

#!/usr/bin/env python3

import time
import argparse
from subprocess import check_call, CalledProcessError
from urllib.request import urlopen, Request


parser = argparse.ArgumentParser()
parser.add_argument('-i', '--image', action='store', required=True, help='image name')
args = parser.parse_args()

print(args.image)

try:
    check_call("podman rm smk".split())
except CalledProcessError as err:
    pass

check_call(
    "podman run --rm --name=smk -p 8080:8080 -d {}".format(args.image).split()
)

time.sleep(5)

r = Request("http://localhost:8080", headers={'Host': 'chris.collins.is'})
try:
    print(str(urlopen(r).read()))
finally:
    check_call("podman kill smk".split())

这可能是一个更彻底的测试。 例如,在构建过程中,可以将 Git 修订版本哈希构建到响应中,并且测试可以检查响应是否包含预期的哈希。 这样做的好处是可以验证至少存在一些预期的内容。

如果测试一切顺利,那么镜像就可以部署了。 我使用 Google 的 Cloud Run 服务来托管我的网站,并且像任何主要的云服务一样,我可以使用一个出色的命令行界面 (CLI) 工具与该服务进行交互。 由于 Cloud Run 是一项容器服务,因此部署包括将本地构建的镜像推送到远程容器注册表,然后使用 gcloud CLI 工具启动服务的部署。

您可以使用 Podman 或 Skopeo(或者 Docker,如果您正在使用它)进行推送。 我的推送目标推送 $(IMAGE_REF):$(HASH) 镜像以及 :latest 标签

push:
  podman push --remove-signatures $(IMAGE_REF):$(HASH)
  podman push --remove-signatures $(IMAGE_REF):latest

推送镜像后,使用 gcloud run deploy 命令将最新的镜像部署到项目并使新镜像生效。 同样,Makefile 在这里派上用场。 我可以在 Makefile 中将 --platform--region 参数指定为变量,这样我就不必每次都记住它们。 老实说:我很少为我的个人博客写作,如果我每次部署新镜像都必须从记忆中输入这些变量,那么我根本不可能记住它们

rollout:
  gcloud run deploy $(PROJECT) --image $(IMAGE_REF):$(HASH) --platform $(PLATFORM) --region $(REGION)

更多目标

还有其他有用的 make 目标。 在编写新内容或测试 CSS 或代码更改时,我希望在本地查看我正在处理的内容,而无需将其部署到远程服务器。 为此,我的 Makefile 有一个 run_local 命令,该命令使用我当前提交的内容启动一个容器,并将我的浏览器打开到由本地运行的 Web 服务器托管的页面的 URL。

.PHONY: run_local
run_local:
  podman stop mansmk ; podman rm mansmk ; podman run --name=mansmk --rm -p $(HOST_ADDR):$(HOST_PORT):$(TARGET_PORT) -d $(IMAGE_REF):$(HASH) && $(BROWSER) $(HOST_URL):$(HOST_PORT)

我还为浏览器名称使用了一个变量,所以我可以根据需要使用多个浏览器进行测试。 默认情况下,当我运行 make run_local 时,它将在 Firefox 中打开。 如果我想在 Google 中测试相同的东西,我运行 make run_local BROWSER="google-chrome"

在使用容器和容器镜像时,清理旧的容器和镜像是一项令人讨厌的琐事,尤其是在您频繁迭代时。 我还在 Makefile 中包含用于处理这些任务的目标。 在清理容器时,如果容器不存在,Podman 或 Docker 将返回退出代码 125。 不幸的是,make 期望每个命令都返回 0,否则它将停止处理,因此我使用一个包装器脚本来处理这种情况

#!/usr/bin/env bash

ID="${@}"

podman stop ${ID} 2>/dev/null

if [[ $?  == 125 ]]
then
  # No such container
  exit 0
elif [[ $? == 0 ]]
then
  podman rm ${ID} 2>/dev/null
else
  exit $?
fi

清理镜像需要更多的逻辑,但所有这些都可以在 Makefile 中完成。 为了轻松地做到这一点,我在构建镜像时(通过 Containerfile)向镜像添加一个标签。 这使得查找所有带有这些标签的镜像变得容易。 可以通过查找 :latest 标签来识别这些镜像中最新的镜像。 最后,可以删除所有镜像,除了指向标记为 :latest 的镜像的镜像之外。

clean_images:
  $(eval LATEST_IMAGES := $(shell podman images --filter "label=my-project.purpose=app-image" --no-trunc | awk '/latest/ {print $$3}'))
  podman images --filter "label=my-project.purpose=app-image" --no-trunc --quiet | grep -v $(LATEST_IMAGES) | xargs --no-run-if-empty --max-lines=1 podman image rm

这是使用 Makefile 管理容器项目真正融合在一起形成一些很酷的东西的点。 到目前为止,Makefile 包含用于构建和标记镜像、测试、推送镜像、部署新版本、清理容器、清理镜像和运行本地版本的命令。 使用 make image_build && make image_tag && make test… 等等运行每一个都比运行原始命令容易得多,但它可以进一步简化。

Makefile 可以将命令分组到一个目标中,允许使用单个命令运行多个目标。 例如,我的 Makefile 将 image_buildimage_tag 目标分组在 build 目标下,因此我可以通过简单地使用 make build 来运行这两个目标。 更好的是,这些目标可以进一步分组到默认的 make 目标 all 中,允许我通过执行 make all 或更简单地 make 来按顺序运行所有这些目标。

对于我的项目,我希望默认的 make 操作包括从构建镜像到测试、部署和清理的所有内容,因此我包含以下目标

.PHONY: all
all: build test deploy clean

.PHONY: build image_build image_tag
build: image_build image_tag

.PHONY: deploy push rollout
deploy: push rollout

.PHONY: clean clean_containers clean_images
clean: clean_containers clean_images

这完成了我在本文中谈到的所有内容,除了 make run_local 目标,仅需一个命令:make

结论

Makefile 是管理基于容器的项目的绝佳方法。 通过将构建、测试和部署项目所需的所有命令组合到 Makefile 中的 make 目标中,所有“元”工作——除了编写代码之外的所有工作——都可以简化和自动化。 Makefile 甚至可以用于与代码相关的任务:运行单元测试、维护模块、编译二进制文件和校验和。 虽然它还不能为您编写代码,但是使用 Makefile 结合容器化、基于云的服务的优势可以make(眨眼,眨眼)使管理项目的许多方面变得更加容易。

下一步阅读
标签
Chris Collins
Chris Collins 是红帽公司的 SRE,也是 OpenSource.com 的记者,热衷于自动化、容器编排及其生态系统,并且喜欢在家中娱乐性地重现企业级技术。

评论已关闭。

© . All rights reserved.