使用GitHub动作构建GraalVM原生图像教程

337 阅读9分钟

作为一个开发者,让某样东西运转起来是你能拥有的最伟大的感觉之一。特别是当你花了几个小时、几天或几个月的时间来实现它。最后一英里可能是最痛苦和最有价值的经历之一,所有这些都被包裹在同一天或两天里。

我最近在为JHipster做的Spring Native项目中经历了这种情况。如果我回顾一下,花了一年的时间来实现它的愿望、研究和坚持不懈的努力。当我们最终实现了它的工作和自动化时,你可以想象我的兴奋之情。

在几天的兴奋之后,我认为使用GitHub Actions为每个操作系统(Linux、macOS和Windows)创建本地构建很容易。我错了。

如果你想跟着学习如何配置GitHub Actions来创建本地二进制文件,你需要一些先决条件。

先决条件

目录

你可以通过下面的推特阅读完整的对话(也就是这篇文章的浓缩版),或者继续阅读,了解我在GitHub Actions和GraalVM上经历的考验和磨难。希望我的经验能够为你节省几个小时的时间。

上周,当我得到这个工作时,我非常兴奋,我决定探索让@graalvm的构建与GitHub Actions一起工作。

这种体验是缓慢而痛苦的。

当前状态:它只在macOS上工作。

Windows说命令太长,Linux说内存用完了。https://t.co/sfLWQCVzaY

- Matt Raible (@mraible)2022年2月23日

配置一个JHipster应用来使用GitHub Actions

我将为你加快进度,只告诉你如何为现有的JHipster应用配置GitHub Actions。在这种情况下,它是一个全栈的React + Spring Boot应用。

使用npm安装其依赖项。

git clone -b jhipster-native-1.1.2 \
  https://github.com/oktadev/auth0-full-stack-java-example.git flickr2
cd flickr2
npm install

然后,在GitHub上创建一个新的 repo。例如,jhipster-flickr2

接下来,把这个例子项目推送给它。

USERNAME=<your-github-username>
git remote rm origin
git remote add origin git@github.com:${USERNAME}/jhipster-flickr2.git
git branch -M main
git push -u origin main

使用JHipster的CI/CD自动等待

用GraalVM构建原生镜像让我回到了21世纪初用Ant和XDoclet构建Java应用的日子。我们会开始构建,然后去做其他事情,因为要花几分钟时间来构建工件。

原生二进制文件的另一个经常被忽视的问题是,你必须为每个操作系统构建一个。这不像Java,你可以构建一个JAR(Java ARchive)并在任何地方运行它。

接下来,使用JHipster的CI/CD子生成器生成持续集成(CI)工作流程。

npx jhipster ci-cd

这个命令会提示你选择一个CI/CD管道。选择GitHub Actions

Welcome to the JHipster CI/CD Sub-Generator

当提示你这个快速例子的任务/集成(Sonar, Docker, Snyk, Heroku, 和Cypress Dashboard)时,不要选择任何任务。子生成器将创建三个文件。

  • .github/workflows/main.yml

  • .github/workflows/native.yml

  • .github/workflows/native-artifact.yml

我将在下面的章节中向你展示每个文件的内容。让我们首先检查一下main.yml

main.yml 工作流文件将配置 GitHub Actions 来检查你的项目,配置 Node 16,配置 Java 11,运行你项目的后端/前端单元测试,以及运行其端到端的测试。不仅如此,它还会在Docker中启动你依赖的容器(例如Keycloak)。你可以看到大部分的功能都隐藏在npm run 命令后面。

name: Application CI
on: [push, pull_request]
jobs:
  pipeline:
    name: flickr2 pipeline
    runs-on: ubuntu-latest
    if: "!contains(github.event.head_commit.message, '[ci skip]') && !contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.pull_request.title, '[skip ci]') && !contains(github.event.pull_request.title, '[ci skip]')"
    timeout-minutes: 40
    env:
      NODE_VERSION: 16.14.0
      SPRING_OUTPUT_ANSI_ENABLED: DETECT
      SPRING_JPA_SHOW_SQL: false
      JHI_DISABLE_WEBPACK_LOGS: true
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: 16.14.0
      - uses: actions/setup-java@v3
        with:
          distribution: 'temurin'
          java-version: 11
      - name: Install node.js packages
        run: npm install
      - name: Run backend test
        run: |
          chmod +x mvnw
          npm run ci:backend:test
      - name: Run frontend test
        run: npm run ci:frontend:test
      - name: Package application
        run: npm run java:jar:prod
      - name: 'E2E: Package'
        run: npm run ci:e2e:package
      - name: 'E2E: Prepare'
        run: npm run ci:e2e:prepare
      - name: 'E2E: Run'
        run: npm run ci:e2e:run
        env:
          CYPRESS_ENABLE_RECORD: false
          CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
          CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
      - name: 'E2E: Teardown'
        run: npm run ci:e2e:teardown

要在你的新仓库上测试这个功能,你需要创建一个分支和拉动请求(PR),其中包括你的改动。

git checkout -b actions
git add .
git commit -m "Add GitHub Actions"
git push origin actions

你应该在你的终端看到一个链接来创建一个拉动请求(PR)。

remote: Create a pull request for 'actions' on GitHub by visiting:
remote:      https://github.com/mraible/jhipster-flickr2/pull/new/actions

如果你观察PR中的测试运行,你会很高兴,直到它进入E2E:打包阶段。它可能会失败,出现以下错误。

Found orphan containers (docker_keycloak_1) for this project. If you removed or renamed
this service in your compose file, you can run this command with the --remove-orphans flag
to clean it up.

在JHipster中报告了这个问题,因为--remove-orphans 最近被从docker:db:downdocker:keycloak:down 命令中删除。该解释为我提供了足够的信息来关闭该问题。把它们重新添加到package.json ,作为一种变通方法。

"scripts": {
  ...
  "docker:db:down": "... --remove-orphans",
  ...
  "docker:keycloak:down": "... --remove-orphans",
  ...
}

提交并推送这些修改。现在一切都应该通过了。

First successful build in GitHub Actions

把这个PR合并到main 分支。

GraalVM构建的环境影响

这给我们带来了一个有趣的两难问题。如果你要创建本地镜像作为你的应用程序的发布工件,你是不是应该使用setup-graalvm动作来配置GraalVM和你的Java SDK?

我不这么认为。如果你这样做,每次你创建一个PR并提交给它,它就会运行一个本地构建。这个项目的GraalVM构建对我来说在本地需要3-4分钟。而使用GitHub Actions则需要30多分钟。

对我来说,这似乎和加密货币一样对环境不利。如果你使用的是私有回购,这也会让你后悔几年前买了加密货币。对于私人仓库,你只能获得2000分钟的免费GitHub操作。之后的任何分钟,你都会被收费。

是的,我知道加密货币的话题是有争议的。不过我确实喜欢拿它开玩笑。在我看来,每次提交的原生构建和挖掘比特币似乎是相似的。话说回来,单纯的上网对环境来说也是很糟糕的。

使用GitHub Actions的GraalVM的最佳实践

当我第一次开始研究GraalVM的GitHub动作时,JHipster Native蓝图修改了package.json 中的命令,以便总是构建本地镜像,并在运行端到端测试时使用它们。这意味着,当你第一次尝试添加GitHub Actions支持时,构建会失败,因为没有找到GRAALVM_HOME 。为了解决这个问题,你可以从actions/setup-java 切换到graalvm/setup-graalvm ,但这不是很环保的做法。

此后,我们修改了蓝图,生成了两个新的工作流程,反映了(在我看来)GitHub Actions和GraalVM的最佳实践。

  1. native.yml:在午夜使用GraalVM运行夜间测试

  2. native-artifact.yml: 构建并上传用于发布的本地二进制文件

main.yml ,保持与JHipster默认的一样,在JVM上持续测试。

使用GraalVM和GitHub Actions运行夜间测试

native.yml 工作流文件执行类似于main.yml 的操作,但使用GraalVM。它每天在UTC午夜按计划运行。目前不支持添加时区。

name: Native CI
on:
  workflow_dispatch:
  schedule:
    - cron: '0 0 * * *'
permissions:
  contents: read
jobs:
  pipeline:
    name: flickr2 native pipeline
    runs-on: ${{ matrix.os }}
    if: "!contains(github.event.head_commit.message, '[ci skip]') && !contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.pull_request.title, '[skip ci]') && !contains(github.event.pull_request.title, '[ci skip]')"
    timeout-minutes: 90
    env:
      SPRING_OUTPUT_ANSI_ENABLED: DETECT
      SPRING_JPA_SHOW_SQL: false
      JHI_DISABLE_WEBPACK_LOGS: true
    defaults:
      run:
        shell: bash
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-latest, windows-2019]
        graalvm-version: ['22.0.0.2']
        java-version: ['11']
        include:
          - os: ubuntu-latest
            executable-suffix: ''
            native-build-args: --verbose -J-Xmx10g
          - os: macos-latest
            executable-suffix: ''
            native-build-args: --verbose -J-Xmx13g
          - os: windows-2019
            executable-suffix: '.exe'
            # e2e is disabled due to unstable docker step
            e2e: false
            native-build-args: --verbose -J-Xmx10g
    steps:
      # OS customizations that allow the builds to succeed on Linux and Windows.
      # Using hash for better security due to third party actions.
      - name: Set up swap space
        if: runner.os == 'Linux'
        # v1.0 (49819abfb41bd9b44fb781159c033dba90353a7c)
        uses: pierotofy/set-swap-space@49819abfb41bd9b44fb781159c033dba90353a7c
        with:
          swap-size-gb: 10
      - name:
          Configure pagefile
          # v1.2 (7e234852c937eea04d6ee627c599fb24a5bfffee)
        uses: al-cheb/configure-pagefile-action@7e234852c937eea04d6ee627c599fb24a5bfffee
        if: runner.os == 'Windows'
        with:
          minimum-size: 10GB
          maximum-size: 12GB
      - name: Set up pagefile
        if: runner.os == 'Windows'
        run: |
          (Get-CimInstance Win32_PageFileUsage).AllocatedBaseSize
        shell: pwsh
      - name: 'SETUP: docker'
        run: |
          HOMEBREW_NO_AUTO_UPDATE=1 brew install --cask docker
          sudo /Applications/Docker.app/Contents/MacOS/Docker --unattended --install-privileged-components
          open -a /Applications/Docker.app --args --unattended --accept-license
          #echo "We are waiting for Docker to be up and running. It can take over 2 minutes..."
          #while ! /Applications/Docker.app/Contents/Resources/bin/docker info &>/dev/null; do sleep 1; done
        if: runner.os == 'macOS'

      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: 16.14.0
      - name: Set up GraalVM (Java ${{ matrix.java-version }})
        uses: graalvm/setup-graalvm@v1
        with:
          version: '${{ matrix.graalvm-version }}'
          java-version: '${{ matrix.java-version }}'
          components: 'native-image'
          github-token: ${{ secrets.GITHUB_TOKEN }}
      - name: Cache Maven dependencies
        uses: actions/cache@v3
        with:
          path: ~/.m2/repository
          key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
          restore-keys: ${{ runner.os }}-maven
      - name: Cache npm dependencies
        uses: actions/cache@v3
        with:
          path: ~/.npm
          key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
      - name: Install node.js packages
        run: npm install
      - name: 'E2E: Package'
        run: npm run native-package -- -B -ntp "-Dnative-build-args=${{ matrix.native-build-args }}"
      - name: 'E2E: Prepare'
        if: matrix.e2e != false
        run: npm run ci:e2e:prepare
      - name: 'E2E: Run'
        if: matrix.e2e != false
        run: npm run native-e2e

如果你将native.ymlmain.yml 进行比较,你会发现它并不运行单元测试(因为Spring Native还不支持Mockito)。它确实构建了一个本地可执行文件,并针对它运行端到端的测试。

如果你等到UTC午夜之后,你可以在Repo的Actions标签中查看这个工作流的结果。它也有一个workflow_dispatch 事件触发器,所以你可以从你的浏览器手动触发它。

Run native workflow

由于Docker镜像无法启动,目前Windows系统的端到端测试被禁用。

在GitHub上发布时如何构建和上传本地二进制文件

native-artifact.yml 工作流文件在发布时为 macOS、Linux 和 Windows 创建了二进制文件。这个工作流将Linux和Windows配置成有足够的内存,将工件上传到action job,并将本地二进制文件上传到GitHub上的release。它只在你创建一个版本(又称标签)时执行。

name: Generate Executables
on:
  workflow_dispatch:
  release:
    types: [published]
permissions:
  contents: write
jobs:
  build:
    name: Generate executable - ${{ matrix.os }}
    runs-on: ${{ matrix.os }}
    timeout-minutes: 90
    defaults:
      run:
        shell: bash
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-latest, windows-2019]
        graalvm-version: ['22.0.0.2']
        java-version: ['11']
        include:
          - os: ubuntu-latest
            executable-suffix: ''
            native-build-args: --verbose -J-Xmx10g
          - os: macos-latest
            executable-suffix: ''
            native-build-args: --verbose -J-Xmx13g
          - os: windows-2019
            executable-suffix: '.exe'
            native-build-args: --verbose -J-Xmx10g
    steps:
      # OS customizations that allow the builds to succeed on Linux and Windows.
      # Using hash for better security due to third party actions.
      - name: Set up swap space
        if: runner.os == 'Linux'
        # v1.0 (49819abfb41bd9b44fb781159c033dba90353a7c)
        uses: pierotofy/set-swap-space@49819abfb41bd9b44fb781159c033dba90353a7c
        with:
          swap-size-gb: 10
      - name:
          Configure pagefile
          # v1.2 (7e234852c937eea04d6ee627c599fb24a5bfffee)
        uses: al-cheb/configure-pagefile-action@7e234852c937eea04d6ee627c599fb24a5bfffee
        if: runner.os == 'Windows'
        with:
          minimum-size: 10GB
          maximum-size: 12GB
      - name: Set up pagefile
        if: runner.os == 'Windows'
        run: |
          (Get-CimInstance Win32_PageFileUsage).AllocatedBaseSize
        shell: pwsh

      - uses: actions/checkout@v3
      - id: executable
        run: echo "::set-output name=name::flickr2-${{ runner.os }}-${{ github.event.release.tag_name || 'snapshot' }}-x86_64"
      - uses: actions/setup-node@v3
        with:
          node-version: 16.14.0
      - name: Set up GraalVM (Java ${{ matrix.java-version }})
        uses: graalvm/setup-graalvm@v1
        with:
          version: '${{ matrix.graalvm-version }}'
          java-version: '${{ matrix.java-version }}'
          components: 'native-image'
          github-token: ${{ secrets.GITHUB_TOKEN }}
      - name: Cache Maven dependencies
        uses: actions/cache@v3
        with:
          path: ~/.m2/repository
          key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
          restore-keys: ${{ runner.os }}-maven
      - name: Cache npm dependencies
        uses: actions/cache@v3
        with:
          path: ~/.npm
          key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
      - run: npm install
      - name: Build ${{ steps.executable.outputs.name }} native image
        run: npm run native-package -- -B -ntp "-Dnative-image-name=${{ steps.executable.outputs.name }}" "-Dnative-build-args=${{ matrix.native-build-args }}"
      - name: Archive binary
        uses: actions/upload-artifact@v3
        with:
          name: ${{ steps.executable.outputs.name }}
          path: target/${{ steps.executable.outputs.name }}${{ matrix.executable-suffix }}
      - name: Upload release
        if: github.event.release.tag_name
        run: gh release upload ${{ github.event.release.tag_name }} target/${{ steps.executable.outputs.name }}${{ matrix.executable-suffix }}
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Linux和Windows的问题和解决方案

当我第一次开始尝试用GraalVM构建本地二进制文件时,我很快在Linux和Windows上遇到了问题

  • Linux上:java.lang.OutOfMemoryError: GC overhead limit exceeded

  • Windows:The command line is too long.

我很高兴地说,通过在native-maven-plugin 插件的构建参数中指定-J-Xmx10g ,我能够修复Linux上的OOM错误。JHipster Native现在默认配置了这个设置,并在构建本地工件时针对你的操作系统进行优化。

<native-image-name>native-executable</native-image-name>
<native-build-args>--verbose -J-Xmx10g</native-build-args>
...
<plugin>
    <groupId>org.graalvm.buildtools</groupId>
    <artifactId>native-maven-plugin</artifactId>
    ..
    <configuration>
        <imageName>${native-image-name}</imageName>
        <buildArgs>
            <buildArg>--no-fallback ${native-build-args}</buildArg>
        </buildArgs>
    </configuration>
</plugin>

Windows的问题已经被本地构建工具0.9.10所修复。

我们使用windows-2019 ,而不是windows-latest ,因为我在尝试时已经没有磁盘空间了

在GitHub上发布一个版本

在你喜欢的浏览器中打开你的仓库页面,点击创建一个新版本。创建一个新的v0.0.1 标签,将该版本命名为v0.0.1 ,并在描述中添加一些有趣的文字。点击发布版本

Restore v0.0.1 - Giddyup!

点击Actions标签,看你的发布是否执行。我想提醒你,这需要一些时间。我的第一次成功发布只用了不到一个小时。

  • macOS:31m 30s

  • Linux系统:33m 50s

  • 窗口系统:59m 45s

我想你会对结果感到满意的。🤠

Released with native binaries attached

如果你的构建失败了,你可以通过运行git push origin :v0.0.1 删除发布的标签。然后你的发布版就会变成一个草案,你可以使用GitHub的用户界面轻松地再次创建发布版。

在本地运行你发布的二进制文件

如果你从GitHub下载这些二进制文件并试图在本地运行它们,你会得到失败的结果,因为它们无法连接到Keycloak或PostgreSQL的实例。

为了启动一个PostgreSQL数据库让应用程序与之对话,你可以从你的flickr2 目录中运行以下命令。

docker-compose -f src/main/docker/postgresql.yml up -d

你可以为Keycloak做同样的事情。

docker-compose -f src/main/docker/keycloak.yml up -d

或者,将应用程序配置为使用OktaAuth0!

Okta CLI使它变得如此简单,你可以在几分钟内完成。

在你开始之前,你需要一个免费的Okta开发者账户。安装Okta CLI并运行okta register ,以注册一个新账户。如果您已经有一个账户,运行okta login 。然后,运行okta apps create jhipster 。选择默认的应用程序名称,或根据您的需要进行更改。 接受为您提供的默认重定向URI值。

Okta CLI是做什么的?

Okta CLI简化了对JHipster应用程序的配置,并为您做了几件事。

  1. 创建一个具有正确重定向URI的OIDC应用程序。
    • 登录:http://localhost:8080/login/oauth2/code/oidchttp://localhost:8761/login/oauth2/code/oidc
    • 注销:http://localhost:8080http://localhost:8761
  2. 创建JHipster期望的ROLE_ADMINROLE_USER
  3. 将你的当前用户添加到ROLE_ADMINROLE_USER 组中。
  4. 在你的默认授权服务器中创建一个groups ,并将用户的组添加到其中。

注意http://localhost:8761* 重定向URI是为JHipster注册处准备的,在用JHipster创建微服务时经常使用。Okta CLI默认会添加这些。

完成后,你会看到如下的输出。

Okta application configuration has been written to: /path/to/app/.okta.env

运行cat .okta.env (或Windows上的type .okta.env ),查看你的应用程序的发行者和凭证。它将看起来像这样(除了占位符的值将被填充)。

export SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_OIDC_ISSUER_URI="/oauth2/default"
export SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_CLIENT_ID="{clientId}"
export SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_CLIENT_SECRET="{clientSecret}"

注意:您也可以使用Okta管理控制台来创建您的应用程序。请参阅在Okta上创建JHipster应用程序以了解更多信息。

然后,通过设置环境变量(.okta.env )和执行二进制文件来启动该应用程序。比如说。

source .okta.env
chmod +x flickr2-macOS-v0.0.1-x86_64
./flickr2-macOS-v0.0.1-x86_64
# verify in System Preferences > Security & Privacy and run again
如果你是在Windows上,你可能需要安装Windows Subsystem for Linux来使这些命令成功。或者,你可以在文件中把.okta.env 重命名为okta.bat ,把export 改为set 。然后,从你的终端运行它来设置变量。

一切都应该按预期工作。很狡猾,你不觉得吗?

你可以在auth0-full-stack-java-example的发布页面上看到该工件的发布版本。

了解更多关于CI、JHipster和GraalVM的信息

我希望你喜欢这个关于如何配置GitHub Actions以创建Java应用程序的GeralVM二进制文件的旅游。原生二进制文件的启动速度比JARs快一些,但它们的构建时间要长很多。这就是为什么把这些过程外包给持续集成服务器是个好主意。