ElIauk

CI/CD:GitHub Actions 构建镜像

从“本地打镜像”到流水线 本地 docker build 打镜像、手动推到仓库、再 kubectl set image,在只有一两个服务时还能忍;服务一多,问题就变成:谁推的、用的哪个 commit、测试跑没跑、回滚到哪个版本。CI/CD 的价值不是“自动化炫技”,而是让每一次发布都可追溯、可重复。

评论
CI/CD:GitHub Actions 构建镜像

从“本地打镜像”到流水线

本地 docker build 打镜像、手动推到仓库、再 kubectl set image,在只有一两个服务时还能忍;服务一多,问题就变成:谁推的、用的哪个 commit、测试跑没跑、回滚到哪个版本。CI/CD 的价值不是“自动化炫技”,而是让每一次发布都可追溯、可重复。

GitHub Actions 对开源和小团队最友好:托管 runner、按仓库计费、和 PR 天然集成。下面这条流水线覆盖构建、测试、打镜像、推 GHCR,改一改就能用于私有仓库。

一条工作流的骨架

name: build-and-push

on:
  push:
    branches: [main]
    tags: ['v*']
  pull_request:
    branches: [main]

permissions:
  contents: read
  packages: write

concurrency:
  group: build-${{ github.ref }}
  cancel-in-progress: true

jobs:
  build:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: '21'
          cache: maven

      - name: Build and test
        run: mvn -B -ntp verify

      - uses: docker/setup-buildx-action@v3

      - uses: docker/login-action@v3
        if: github.event_name != 'pull_request'
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - id: meta
        uses: docker/metadata-action@v5
        with:
          images: ghcr.io/${{ github.repository }}
          tags: |
            type=ref,event=branch
            type=semver,pattern={{version}}
            type=sha

      - uses: docker/build-push-action@v6
        with:
          context: .
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

几个设计点值得解释:

  • permissions 只给最小集合。推 GHCR 需要 packages: write,读代码需要 contents: read,不给 write-all。
  • concurrency 在同一个分支上取消旧构建,避免连续 push 时多个镜像同时推。
  • push 在 PR 时为 false,只验证构建;合并到 main 或打 tag 才推送。
  • metadata-action 自动生成 main、sha-xxxx、v1.2.3 多组标签,回滚时按 sha 拉取最准确。
  • cache-from/to: type=gha 复用层缓存,Java 镜像的依赖层能省下大量时间。

多阶段 Dockerfile

CI 里跑完 mvn verify,镜像构建就应该跳过测试,只负责打包:

FROM maven:3.9-eclipse-temurin-21 AS build
WORKDIR /build
COPY pom.xml .
RUN mvn -B -ntp dependency:go-offline
COPY src ./src
RUN mvn -B -ntp package -DskipTests

FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=build /build/target/*.jar app.jar
ENTRYPOINT ["java", "-XX:MaxRAMPercentage=75", "-jar", "/app/app.jar"]

先拷 pom.xml 再拷源码,是为了让依赖层在源码变化时仍能命中缓存。运行镜像只保留 JRE,体积能小一半以上。alpine 用的是 musl,个别 native 库可能不兼容,遇到 UnsatisfiedLinkError 就换 ubuntu 或 distroless 基础镜像。

安全:权限、密钥和供应链

第一,能用 GITHUB_TOKEN 就不要建长期 PAT。需要访问云厂商时优先用 OIDC 联合身份,把 id-token: write 加上,让云平台临时颁发凭证,避免把 AK/SK 塞进 Secrets。

第二,第三方 action 尽量锁到 commit SHA,而不是 @v4 这种浮动标签。标签可以被移动,SHA 不会。配合 Dependabot 定期升级,安全性和可维护性都能兼顾。

第三,日志里不要打印环境变量和命令行参数。GitHub 会对已知 secret 做掩码,但拼接后的字符串、base64 之后的内容不一定能识别。set -x 在调试完一定要去掉。

第四,镜像仓库的可见性要确认。GHCR 的包默认继承仓库权限,公开仓库的包可能被匿名拉取,私有镜像记得检查 package 设置。

第五,生产发布建议再套一层 GitHub Environment,配置 required reviewers 和分支保护,把“合并即上线”改成“合并后人工确认”。个人项目可以省,团队项目这一步能挡住大部分误操作。

常见坑与排查

坑一:403 推不上去。 90% 是 permissions 没给 packages: write,或者 workflow 用的是只读的 fork PR 上下文。PR 里不推镜像,合并后再推,能避开一大半问题。

坑二:缓存不生效。 cache-from: type=gha 要求 buildx 已初始化,且缓存 key 和构建上下文匹配。用 mode=max 才能缓存中间层,默认的 min 只缓存最终层。

坑三:架构不匹配。 在 arm64 的 runner 上构建,推到 amd64 节点会报 exec format error。要么固定 platforms: linux/amd64,要么用 QEMU 做多架构,但构建时间会明显变长。

坑四:Docker 上下文太大。 把 target/、.git/、node_modules/ 都打进上下文,构建几分钟都卡在传输。写 .dockerignore,这是收益最高的一步。

坑五:tag 覆盖导致回滚困难。 只用 latest 时,回滚只能靠猜。至少保留 sha 标签,生产环境用不可变 tag,配合 Deployment 的 image 字段做精确回滚。

排查步骤

  1. 先看失败在哪一步,是编译、测试、构建还是推送。
  2. 编译失败本地复现 mvn -B -ntp verify,确认不是 CI 环境差异(JDK 版本、编码、时区)。
  3. 推送失败查 permissions、登录步骤和包可见性,用 docker manifest inspect 确认镜像是否真的推上去了。
  4. 构建慢看缓存命中和 .dockerignore,不要一上来就加机器。
  5. 回滚时用 kubectl set image 指到旧的 sha 标签,确认滚动更新完成再观察指标。
  6. Actions 本身出问题时,用 act 在本地跑一遍关键 job,能快速区分是 workflow 写错还是 runner 环境问题。

参考

评论

推荐阅读

订阅邮件

订阅更新,及时收到最新文章。

作者