こんにちは。AI・IoTサービス開発部の青木です。
前回はチームリーダとしてのMTG運用方法などを投稿しました。
OPTiM IoTではIoTデバイス関連のデバイスエージェントの開発も行っていまして、 デバイスのアーキテクチャやディストリビューションの違いからビルド&パッケージングのパターンが多く困っていたこともあり、CIで簡単にビルド出来るようにしました。
AgentはGo言語で書かれているので、各OSとアーキテクチャ向けのクロスコンパイルの様子とOS毎のバージョン管理に利用するためのパッケージングについて記事にします。
目次
各アーキテクチャ毎に go build
Golangにはクロスコンパイル機能が搭載されています。 Golangの公式にクロスコンパイルができるOSとアーキテクチャの組み合わせ表があります。
今回はAgent自体をLinux向けに開発し、Raspberry PiやNVIDIA Jetson 用の arm
,arm64
。OPTiM Edge や amd64,i386 の計4種類のクロスコンパイルを行います。
以下が設定する $GOOS
と$GOARCH
です
$GOOS | $GOARCH |
---|---|
linux | 386 |
linux | amd64 |
linux | arm |
linux | arm64 |
クロスコンパイルを行う際の注意点
クロスコンパイルは上記の環境変数を以下のように指定すれば簡単にビルドが可能です。
GOOS=linux GOARCH=arm GOARM=7 go build -o main main.go # use fish shell env GOOS=linux GOARCH=arm GOARM=7 go build -o main main.go
簡単なプログラムであれば大抵は動作するでしょう。
しかし、C,C++のライブラリを利用している場合などは.so
ライブラリに対して動的リンクになってしまっているケースがあります。
file dist/linux_armv7 dist/linux_armv7: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-armhf.so.3,
dynamically
となっているのがその部分です。
file main main: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, not stripped
statically
は静的リンクとなります。
この場合、リンクされているライブラリがプログラムを実行する端末にインストールされている必要があります。 対処法は以下となります。
- 静的リンクでビルドする
- 注意: LGPLライセンスなどの制約でライセンス違反になる可能性があります。
- 端末に必要なライブラリをインストールする
開発を行う際は必ず確認することではありますが、しっかりOSSのライセンスやそれぞれの利用規約などを確認するようにしましょう。 しらずしらずのうちに違反してしまっているかも知れません。。
アーキテクチャ毎にDockerfileを準備
ビルドを簡単にかつ環境依存が無いようにDocker上でビルドを行おうと思います。 今回はDocker Buildを行いアーキテクチャ毎にビルドしバイナリファイルをイメージごと保存します。
- arm の例
FROM golang:1.16.11-bullseye AS ubuntu ARG BINARY_NAME=sample ARG VERSION=testing COPY . /app WORKDIR /app # 32bit arm RUN apt-get update && apt-get install -y gcc-arm-linux-gnueabihf RUN CC=arm-linux-gnueabihf-gcc CGO_ENABLED=1 GOPROXY=direct GOSUMDB=off GOARCH=arm GOOS=linux GOARM=5 go build -o /dist/${BINARY_NAME}_linux_armv5 main/main.go RUN CC=arm-linux-gnueabihf-gcc CGO_ENABLED=1 GOPROXY=direct GOSUMDB=off GOARCH=arm GOOS=linux GOARM=6 go build -o /dist/${BINARY_NAME}_linux_armv6 main/main.go RUN CC=arm-linux-gnueabihf-gcc CGO_ENABLED=1 GOPROXY=direct GOSUMDB=off GOARCH=arm GOOS=linux GOARM=7 go build -o /dist/${BINARY_NAME}_linux_armv7 main/main.go FROM scratch COPY --from=ubuntu /dist /dist
これらを4種類用意します。(ビルド部分のみ抜粋)
- amd64
RUN CGO_ENABLED=1 GOPROXY=direct GOSUMDB=off GOARCH=amd64 GOOS=linux go build -o /dist/${BINARY_NAME}_linux_amd64 main/main.go
- i386
RUN apt-get update && apt-get install -y libc6-dev-i386 RUN CGO_ENABLED=1 GOPROXY=direct GOSUMDB=off GOARCH=386 GOOS=linux go build -o /dist/${BINARY_NAME}_linux_i386 main/main.go
- arm64
RUN apt-get update && apt-get install -y gcc-aarch64-linux-gnu RUN CC=aarch64-linux-gnu-gcc CGO_ENABLED=1 GOPROXY=direct GOSUMDB=off GOARCH=arm64 GOOS=linux go build -o /dist/${BINARY_NAME}_linux_arm64 main/main.go
GitLab CI 構築
ビルドを毎回手動で行うのは大変面倒なので、CIにおまかせしようと思います。
構築後はこのようにパイプラインでビルドされ、弊社のDocker Image Registryに格納されます。
ここはArtifactなどでもいいですね。今回は他のリポジトリのDocker Multi Stage Build を利用して COPY --from=
でファイルを引っ張って来たいのでイメージ状態にしておきます。
.gitlab-ci.yml
stages: - 1st - 2nd include: - local: '.gitlab/ci/amd64.gitlab-ci.yml' - local: '.gitlab/ci/arm.gitlab-ci.yml' - local: '.gitlab/ci/i386.gitlab-ci.yml' - local: '.gitlab/ci/arm64.gitlab-ci.yml'
yamlファイル内がごちゃつかないように分けておきます。 タグを切った時にビルドが走り、Registryに格納されるようにしておきます。
.gitlab/ci/amd64.gitlab-ci.yml
build:amd64: image: docker stage: 2nd script: - docker login -u ${CI_REGISTRY_USER} -p ${CI_REGISTRY_PASSWORD} ${CI_REGISTRY} - docker build --build-arg VERSION=${CI_COMMIT_TAG} -f docker/amd64.docker -t $CI_REGISTRY_IMAGE/amd64:${CI_COMMIT_TAG} . - docker push $CI_REGISTRY_IMAGE/amd64:${CI_COMMIT_TAG} - docker rmi $CI_REGISTRY_IMAGE/amd64:${CI_COMMIT_TAG} rules: - if: $CI_COMMIT_TAG
ビルドしたバイナリをOSごとにパッケージング
今度はOSごとに提供されているパッケージマネージャでインストールやアップデートが出来るようにパッケージ化を行いたいと思います。 パッケージ化する際は、OSのバージョンもそうですがCPUのアーキテクチャも合わせないとだめなケースがあるためそれぞれのパターン毎に用意します。
- CentOS
- Ubuntu/Debian
- Raspberry Pi OS(arm用)
Ubuntu Linux の場合
Ubuntuは18.04LTS
と20.04LTS
を用意します。
どちらもほとんど同じパッケージング内容の為、同じDockerfileを利用してパッケージング出来るように作ります。
ARG MONITORING_DAEMON_IMAGE ARG MONITORING_DAEMON_VERSION ARG UBUNTU_IMAGE=ubuntu ARG UBUNTU_TAG="18.04" # Load Daemon Binary Image FROM ${MONITORING_DAEMON_IMAGE}:${MONITORING_DAEMON_VERSION} AS monitoring_daemon # Main Build Stage FROM $UBUNTU_IMAGE:$UBUNTU_TAG AS build_stage RUN apt-get update && apt-get install -y tzdata ENV TZ=Asia/Tokyo ARG MONITORING_DAEMON_VERSION ARG MONITORING_PACKAGE_NAME=cios-monitor ARG MONITORING_BINARY_NAME=monitord ARG BINARY_ARCH=amd64 COPY packaging/package_settings /packaging/package_settings COPY packaging/docker/package_scripts/package-deb.sh /package-deb.sh COPY --from=monitoring_daemon /dist/${MONITORING_BINARY_NAME}_linux_${BINARY_ARCH} /dist/${MONITORING_BINARY_NAME}_linux_${BINARY_ARCH} RUN apt-get install -y build-essential devscripts cdbs dh-make quilt debhelper devscripts apt-utils # /debへのパッケージングスクリプトを実行 RUN chmod +x /package-deb.sh RUN /package-deb.sh ${MONITORING_PACKAGE_NAME} ${MONITORING_DAEMON_VERSION} ${MONITORING_BINARY_NAME} /dist/${MONITORING_BINARY_NAME}_linux_${BINARY_ARCH} /packaging/package_settings FROM alpine COPY --from=build_stage /*.deb /
DockerのBuild Argを利用し、様々なイメージに対応しました。(と言ってもUbuntuに最適化しているのでUbuntuのTagバージョンを変更する程度ですが...)
パッケージングをCIで構築
GitLab CIでパッケージングします。 ここでポイントなのが、CIのRunnerで実行されているCPUアーキテクチャと異なったアーキテクチャでDockerContainerを立ち上げることが出来る multiarch/qemu-user-static を実行しています。 クロスコンパイルの場合は、コンパイルに必要なライブラリ等のインストールで行えましたが、パッケージングに関しては異なるアーキテクチャの環境を再現しなければいけません。
それらをふまえてまず .gitlab-ci.yml
を作成します。
.gitlab-ci.yml
image: docker:latest stages: - build - packaging before_script: - docker run --rm --privileged multiarch/qemu-user-static:register || true include: - local: '.gitlab/ci/packaging/ubuntu_amd64.gitlab-ci.yml' - local: '.gitlab/ci/packaging/centos_amd64.gitlab-ci.yml' - local: '.gitlab/ci/packaging/ubuntu_i386.gitlab-ci.yml' - local: '.gitlab/ci/packaging/raspberrypios_arm.gitlab-ci.yml' - local: '.gitlab/ci/packaging/debian_arm64v8.gitlab-ci.yml'
前回と同じように include
を利用して別の定義ファイルにしておきます。
.gitlab/ci/packaging/ubuntu_amd64.gitlab-ci.yml
p:ubuntu:18.04_amd64: stage: packaging variables: ARCH: amd64 script: - docker login -u ${CI_REGISTRY_USER} -p ${CI_REGISTRY_PASSWORD} ${CI_REGISTRY} - docker build -t $CI_REGISTRY_IMAGE/ubuntu_18.04_${ARCH}:${CI_BUILD_REF_SLUG} --build-arg MONITORING_DAEMON_IMAGE=${MONITORING_IMAGE_PATH}/${ARCH} --build-arg MONITORING_DAEMON_VERSION=${MONITORING_DAEMON_VERSION} --build-arg UBUNTU_IMAGE=ubuntu --build-arg UBUNTU_TAG=18.04 -f packaging/docker/ubuntu.docker . - docker push $CI_REGISTRY_IMAGE/ubuntu_18.04_${ARCH}:${CI_BUILD_REF_SLUG} - docker rmi $CI_REGISTRY_IMAGE/ubuntu_18.04_${ARCH}:${CI_BUILD_REF_SLUG} rules: - if: '$CI_PIPELINE_SOURCE == "push"' when: manual allow_failure: true # - if: $CI_COMMIT_TAG p:ubuntu:20.04_amd64: stage: packaging variables: ARCH: amd64 script: - docker login -u ${CI_REGISTRY_USER} -p ${CI_REGISTRY_PASSWORD} ${CI_REGISTRY} - docker build -t $CI_REGISTRY_IMAGE/ubuntu_20.04_${ARCH}:${CI_BUILD_REF_SLUG} --build-arg MONITORING_DAEMON_IMAGE=${MONITORING_IMAGE_PATH}/${ARCH} --build-arg MONITORING_DAEMON_VERSION=${MONITORING_DAEMON_VERSION} --build-arg UBUNTU_IMAGE=ubuntu --build-arg UBUNTU_TAG=20.04 -f packaging/docker/ubuntu.docker . - docker push $CI_REGISTRY_IMAGE/ubuntu_20.04_${ARCH}:${CI_BUILD_REF_SLUG} - docker rmi $CI_REGISTRY_IMAGE/ubuntu_20.04_${ARCH}:${CI_BUILD_REF_SLUG} rules: - if: '$CI_PIPELINE_SOURCE == "push"' when: manual allow_failure: true # - if: $CI_COMMIT_TAG
前回はTAGにしていましたが、デバッグや動作確認も含めてどのブランチで任意のタイミングで実行したいので rules
にその記述を行っています。
無事設定が完了すると以下のようになります。
Ubuntu20.04
の対応が未完了の為CIがコケていますが、それ以外はクリアしています。
完了
実際は2つのプログラムがインストールされるような仕組みになっているためもう少し複雑になっていますが、手作業で行っていたパッケージング作業をほぼ自動化することに成功しました。 また、今まではクロスコンパイルではなく実機でコンパイルしていたりとエンジニアとしても辛い部分であったため、かなり効率化できたと思います。
ちなみにですが、Golangだとクロスコンパイル時でも30秒程でビルドが終わるため全くストレスがありません。Runnerとして稼働しているマシンスペックもそこまで高くなくても良いので、便利だなと感じています。
さいごに
私個人ではGitHub Actionsをよく利用していたのですが、GitLab CIもかなり優秀でGitHub Actionsより自由度があるような気がします。 GitLabにPackageRegistryやContainerRegistryが導入されているので、CIで提供一歩手間まで簡単になりました。(特に認証周り)
CIは最初に構築しておく派で、これまで様々なCIによる効率化と最適化を行ってきましたので、その一部を紹介できて個人的に満足しています😄
CIでゴニョゴニョする話を連載にしてもいいかもしれませんね。
また紹介できるものがあれば紹介しようかと思います。
それでは、また今度。
オプティムでは効率化が大好きなエンジニアを募集しています。