先日、GitHub Actionsからマネージドランナー扱いでAWS CodeBuildを呼び出せるようになった。
CodeBuildからはLambda環境を呼び出すこともできるので、簡単な処理ならLambdaを使い、ビルドなどの大掛かりでファイルシステムを触るような処理ならEC2ランナーを使うという使い分けができる。しかも、Lambdaを利用する場合の課金体系は秒単位なので、分単位で課金されるGitHub Actionsのホステッドランナー(プライベートリポジトリの場合)と比べると大幅に優位だ。
加えて、CodeBuildは柔軟にx86_64環境とARM(aarch64)環境とを切り替えられるため、クロスビルドもできる。
そこで、今回はこの機能の練習と紹介も兼ねて、Scalaプロジェクトをx86_64環境とaarch64環境とでクロスビルドし、シングルバイナリを出力する構成を作成する。
リポジトリは以下(CodeBuildの設定も必要なので単独では動作しない)。
公式ドキュメントは以下。
前提条件
- AWSのアカウントを持っていてCodeBuildを利用できること
- GitHub Actionsでワークフローを実行できること
Scala Native
ScalaをシングルバイナリにビルドするためにScala Nativeを利用する。
Scala Nativeについてはこのへんの記事を参照。
build.sbt
は以下の通り:
val scala3Version = "3.4.1" enablePlugins(ScalaNativePlugin) // set to Debug for compilation details (Info is default) logLevel := Level.Info // import to add Scala Native options import scala.scalanative.build._ lazy val root = project .in(file(".")) .settings( name := "cross-build-exercise", version := "0.1.0-SNAPSHOT", scalaVersion := scala3Version, // defaults set with common options shown nativeConfig ~= { c => c.withLTO(LTO.none) // thin .withMode(Mode.releaseSize) .withGC(GC.immix) // commix .withLinkingOptions(Seq("-static")) }, )
project/plugins.sbt
は以下の通り:
addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.1")
src/main/scala/Main.scala
には以下のコードを用意した。実行しているアーキテクチャが表示されるようにPOSIX APIを呼び出すことにした。
@main def hello(): Unit = println("Hello world!") println(msg) println(s"Running on $arch") def msg = "I was compiled by Scala 3. :)" def arch: String = import scala.scalanative.unsafe._ Zone: // We need Zone to allocate memory import scalanative.posix.sys.utsname import scalanative.posix.sys.utsnameOps.given val ptr = stackalloc[utsname.utsname]() scala.scalanative.posix.sys.utsname.uname(ptr) fromCString(ptr.machine.at(0))
まぁたいしたことはしていない。これだけでシングルバイナリを出力できる。バックエンドにLLVMを利用しているので、そのセットアップは必要だ*1。
この時点でsbt nativeLink
を実行するとシングルバイナリが出てくる。
% sbt nativeLink [info] welcome to sbt 1.9.9 (Amazon.com Inc. Java 21.0.2) [info] loading global plugins from /home/windymelt/.sbt/1.0/plugins [info] loading settings for project codebuild-runner-cross-build-exercise-build from plugins.sbt ... [info] loading project definition from /home/windymelt/src/github.com/windymelt/codebuild-runner-cross-build-exercise/project [info] loading settings for project root from build.sbt ... [info] set current project to cross-build-exercise (in build file:/home/windymelt/src/github.com/windymelt/codebuild-runner-cross-build-exercise/) [info] Linking (multithreadingEnabled=true, disable if not used) (1297 ms) [info] Discovered 885 classes and 5383 methods after classloading [info] Checking intermediate code (quick) (36 ms) [info] Multithreading was not explicitly enabled - initial class loading has not detected any usage of system threads. Multithreading support will be disabled to improve performance. [info] Linking (multithreadingEnabled=false) (472 ms) [info] Discovered 512 classes and 2618 methods after classloading [info] Checking intermediate code (quick) (7 ms) [info] Discovered 411 classes and 1263 methods after optimization [info] Optimizing (release-size mode) (1327 ms) [info] Produced 42 LLVM IR files [info] Generating intermediate code (1629 ms) [info] Compiling to native code (1489 ms) [info] Linking with [pthread, dl] [error] clang++: warning: argument unused during compilation: '-rdynamic' [-Wunused-command-line-argument] [error] /usr/bin/ld: warning: /home/windymelt/src/github.com/windymelt/codebuild-runner-cross-build-exercise/target/scala-3.4.1/native/dependencies/nativelib_native0.5_3-0.5.1-0/scala-native/delimcc/setjmp_amd32.S.o: missing .note.GNU-stack section implies executable stack [error] /usr/bin/ld: NOTE: This behaviour is deprecated and will be removed in a future version of the linker [info] Linking native code (immix gc, none lto) (205 ms) [info] Postprocessing (0 ms) [info] Total (5301 ms) [success] Total time: 6 s, completed 2024/04/30 13:20:47
実行するとマシンのアーキテクチャが表示される。
% ./target/scala-3.4.1/cross-build-exercise Hello world! I was compiled by Scala 3. :) Running on x86_64
CodeBuildのセットアップ
AWSコンソールに移動し、CodeBuildのプロジェクトを作成しよう。
プロジェクト名はなんでもいい。ここはGitHub Actionsのワークフローから呼び出す名前になるので、わかりやすいものがいいだろう。
ソースとしてGitHubアカウントを指定する。認証方法としてOAuth2を利用してもいいし、PATを利用してもいい。今回はPATを利用し、classicなほうを選んでread:user, repo
を指定した(fine grained PATだとどうもうまくいかなった)。
ウェブフックイベントとして、プッシュされるたびに再構築するチェックボックスを入れよう。そしてイベントタイプにWORKFLOW_JOB_QUEUED
を指定する。これにより、ワークフロー起動時にCodeBuildがフックされるようになる。
実行環境として今回はEC2を選んだ。というのも、Lambda環境ではファイルシステムがRead-onlyになってしまい、ビルド用のファイルを展開できないためだ。もちろん、ちょっとしたタスクを実行したいならLambdaがおすすめだ。Lambdaを使うと秒課金になるし、aarch64環境は安いのでたくさん使える。
実行アーキテクチャやインスタンスサイズはワークフローから動的に設定可能なので、1つプロジェクトを作ればクロスコンパイル可能だ。
最後にbuildspecはファイルから得る設定にする。ちなみにこの設定はダミーで、実際は勝手にGitHub Actionsのワークフローをもとに実行される。
ワークフローファイル
最後にワークフローからランナーとしてこのプロジェクトを設定すればCodeBuild上でランナーが動くようになる。
今回は、「リリースを作成するジョブ」「各アーキテクチャでバイナリを作成し、リリースに登録するジョブ × 2」の構成にしてある。タグを打つと起動する仕組みだ。
runs-on: codebuild-codebuild-runner-cross-build-exer-${{ github.run_id }}-${{ github.run_attempt }}-al2-5.0-large
と書いてある場所に注目してほしい。ここでcodebuild-codebuild-runner-cross-build-exer-${{ github.run_id }}-${{ github.run_attempt }}-arm-3.0-large
を指定するとaarch64環境で動かすことができる。
書式はruns-on: codebuild-<project-name>-${{ github.run_id }}-${{ github.run_attempt }}-<image>-<image-version>-<instance-size>
だ。
name: Build and release on: push: tags: - "v*.*.*" jobs: release: runs-on: ubuntu-latest permissions: contents: write outputs: release-id: ${{ steps.release.outputs.id }} steps: - name: Checkout uses: actions/checkout@v4 - name: Release id: release uses: softprops/action-gh-release@v2 with: token: ${{ secrets.GITHUB_TOKEN }} body: | This is a release build-x86_64: permissions: contents: write needs: - release runs-on: codebuild-codebuild-runner-cross-build-exer-${{ github.run_id }}-${{ github.run_attempt }}-al2-5.0-large steps: - uses: actions/checkout@v4 - run: | sudo yum install -y clang llvm clang-libs glibc-static libstdc++ libstdc++-devel libstdc++-static ln -s /lib64/libstdc++.so.6 /lib64/libstdc++.so curl -fLO https://github.com/sbt/sbt/releases/download/v1.9.9/sbt-1.9.9.zip unzip sbt-1.9.9.zip ./sbt/bin/sbt nativeLink mv ./target/scala-3.4.1/cross-build-exercise ./target/scala-3.4.1/cross-build-exercise-linux-x86_64 - uses: AButler/upload-release-assets@v3.0 with: release-id: ${{ needs.release.outputs.release-id }} files: "./target/scala-3.4.1/cross-build-exercise-linux-x86_64" repo-token: ${{ secrets.GITHUB_TOKEN }} build-aarch64: permissions: contents: write needs: - release runs-on: codebuild-codebuild-runner-cross-build-exer-${{ github.run_id }}-${{ github.run_attempt }}-arm-3.0-large steps: - uses: actions/checkout@v4 - run: | sudo yum install -y clang llvm clang-libs glibc-static libstdc++ libstdc++-devel libstdc++-static ln -s /lib64/libstdc++.so.6 /lib64/libstdc++.so curl -fLO https://github.com/sbt/sbt/releases/download/v1.9.9/sbt-1.9.9.zip unzip sbt-1.9.9.zip ./sbt/bin/sbt nativeLink mv ./target/scala-3.4.1/cross-build-exercise ./target/scala-3.4.1/cross-build-exercise-linux-aarch64 - uses: AButler/upload-release-assets@v3.0 with: release-id: ${{ needs.release.outputs.release-id }} files: "./target/scala-3.4.1/cross-build-exercise-linux-aarch64" repo-token: ${{ secrets.GITHUB_TOKEN }}
これを実行するとバイナリがリリースされる:
まとめ
ちょっとした処理だが、CodeBuildを利用してGitHub Actionsのワークフローを実行する方法を紹介した。
- CodeBuildをランナーとしてGitHub Actionsから呼び出せるようになった。
- CodeBuildの課金体系は通常のランナーと比べて有利な側面がある。
- CodeBuildでクロスビルドもできる。
*1:そもそもLLVMだと最初からクロスコンパイルできる気もするけど、練習ということでいったん考えないでおく