Lambdaカクテル

京都在住Webエンジニアの日記です

Invite link for Scalaわいわいランド

GitHub ActionsからCodeBuildを呼び出せるようになったのでx86_64とaarch64とでクロスビルドしてみた

先日、GitHub Actionsからマネージドランナー扱いでAWS CodeBuildを呼び出せるようになった。

aws.amazon.com

CodeBuildからはLambda環境を呼び出すこともできるので、簡単な処理ならLambdaを使い、ビルドなどの大掛かりでファイルシステムを触るような処理ならEC2ランナーを使うという使い分けができる。しかも、Lambdaを利用する場合の課金体系は秒単位なので、分単位で課金されるGitHub Actionsのホステッドランナー(プライベートリポジトリの場合)と比べると大幅に優位だ。

加えて、CodeBuildは柔軟にx86_64環境とARM(aarch64)環境とを切り替えられるため、クロスビルドもできる。

そこで、今回はこの機能の練習と紹介も兼ねて、Scalaプロジェクトをx86_64環境とaarch64環境とでクロスビルドし、シングルバイナリを出力する構成を作成する。

リポジトリは以下(CodeBuildの設定も必要なので単独では動作しない)。

github.com

公式ドキュメントは以下。

docs.aws.amazon.com

前提条件

  • AWSのアカウントを持っていてCodeBuildを利用できること
  • GitHub Actionsでワークフローを実行できること

Scala Native

ScalaをシングルバイナリにビルドするためにScala Nativeを利用する。

Scala Nativeについてはこのへんの記事を参照。

blog.3qe.us

blog.3qe.us

blog.3qe.us

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だと最初からクロスコンパイルできる気もするけど、練習ということでいったん考えないでおく

★記事をRTしてもらえると喜びます
Webアプリケーション開発関連の記事を投稿しています.読者になってみませんか?