こんにちは、テクマトリックスの酒井です。

前回の記事ではローカル環境に Kubernetes クラスターと CI 環境を構築しました。そのときはビルド環境として Docker Hub で公開されている maven などのコンテナイメージを利用しましたが、実際の開発では自分たちでコンテナイメージを用意することが多いのではないかと思います。そこで今回は、前回の記事で構築した CI 環境をベースに、ビルドに使用するコンテナイメージをビルドする CI 環境を作ってみます。

なぜコンテナイメージをビルドするのか

この CI 環境では、すべてのビルド/テストをコンテナで実行したいと考えています。

ビルド/テストを実行するには、自分たちが使用するコンパイラ、ライブラリ、ビルド/テストツールなどがインストールされた環境(ここでは「ビルド/テスト環境」と呼びます。)が必要です。そしてビルド/テスト環境をコンテナで実行する際、ビルド/テストの実行の都度コンパイラなどをインストールして実行するのは無駄が多いと思います。反対にあらかじめコンパイラなどがインストール済みのコンテナイメージを作成しておくことで、インストールの時間を省き、ビルド/テストをより短時間で実行可能になります。

また CI 環境が社内に浸透して多数のビルドが実行されるようになれば、 Kubernetes クラスターを増強(マシンを追加)することにより、一つのコンテナイメージから多数のコンテナを並列で実行し、多数のビルドの要求をさばくことができます。社内で利用されるすべてのビルド/テスト環境のコンテナイメージを用意することができれば、一つの CI 環境で多数のビルド/テスト環境、多数のプロジェクトをサポートすることが可能になります。

なぜ Docker を使わないのか

Docker はコンテナイメージのビルドのためにもコンテナのランタイム(デーモン)を必要とします。しかしデーモンは通常特権を持つ必要があるため、 Kubernetes 上でデーモンをセキュアに実行することは簡単ではありません。

この課題を解決するアプローチとツールがいくつも存在するようですが、ここでは CloudBees CI という Jenkins をベースにした製品のドキュメントに記載のある Kaniko を利用してみます。ちなみに Kaniko のドキュメントには他のツールとの比較が記載されていますので、他のツールに興味がある方は参考にしていただければと思います。

構成

全体像

今回構築する CI 環境の構成図は次のとおりです。

今回構築する CI 環境の構成図

前回の記事で構築した CI 環境からの変更点は、 Kubernetes クラスター上にコンテナレジストリのポッドが追加されることです。コンテナレジストリの IP アドレスを使用して、 Kubernetes とクラスター内のコンテナの両方から同じ URL でコンテナイメージにアクセスできます。コンテナレジストリには VM の外からもアクセス可能ですが、URL が異なる点はご注意ください。もし Docker Hub のような外部のコンテナレジストリが利用できるなら、アクセス元の場所によって URL を変更する必要はもちろんありません。

CI 環境の利用方法は、コンテナイメージのソースコードを Gitea のリポジトリにプッシュします。すると Jenkins がソースコードをチェックアウトして Kaniko を実行します。 Kaniko はコンテナイメージをビルドしてコンテナレジストリにアップロードします。アップロードされたコンテナイメージは、 Kubernetes クラスターの内外でビルド/テスト環境として利用できます。

コンテナイメージに含まれるもの

コンテナイメージにはビルド/テストに必要なコンパイラやライブラリなどを含めます。今回は C/C++ 言語の Arm アーキテクチャー向けのクロスコンパイラである Arm GNU Toolchain を利用します。余談ですが Arm の CPU は以前から組込みシステムで良く採用されていますが、現在はそれに加えて Mac や AWS の EC2 インスタンスでも採用されていますので、 Arm の CPU を使う機会がより増えたように感じます。

閑話休題。コンパイラに加えて、 C/C++ 言語のビルドツールとして良く利用される CMakeMakeNinja もイメージに含めることにします。

Kubernetes クラスターの設定

前提として、前回の記事に沿って Kubernetes クラスターと CI 環境が構築されているものとします。 そして構成図に記載のとおりコンテナレジストリを追加します。

コンテナレジストリの追加

Kubernetes クラスターの構築には Minikube を利用しているため、コンテナレジストリの追加は Minikube の registry プラグインを有効にすることで簡単にできます。コンテナレジストリを追加したら、その IP アドレスをメモしておきます。これらを実行する Powershell スクリプトは次のとおりです。

minikube addons enable registry
$RegistryAddress = & kubectl get service -n kube-system registry -o jsonpath='{.spec.clusterIP}'
$RegistryAddress

コンテナイメージのソースコードの作成

ビルド/テスト環境用のコンテナイメージのソースコードリポジトリを作成します。そしてその中に、コンテナイメージのビルド手順を記述する Dockerfile ファイル、および、コンテナイメージのビルドとアップロードを行うパイプラインを定義する Jenkinsfile ファイルを追加します。

ソースコードリポジトリの作成とクローン

gitea コマンドを使用してリポジトリを作成します。 Powershell にて以下のスクリプトを実行します。 ${GITEA_DOMAIN} は Gitea のドメイン名に置き換えます。

$orgName       = 'exampleorg'        # 組織名
$repoName      = 'arm-gnu-toolchain' # リポジトリ名
$giteaUsername = 'gitea_admin'       # Gitea の初期ユーザー名
$giteaPassword = 'r8sA8CPHD9!bt6d'   # Gitea の初期ユーザーのパスワード

# Gitea API にアクセスするための認証情報
$headers = @{ Authorization = "Basic "+ [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes("${giteaUsername}:${giteaPassword}")) }

# リポジトリ example-pipeline を作成。 README を自動生成
$repo = Invoke-RestMethod -Method 'POST' `
  -Uri "http://${GITEA_DOMAIN}/api/v1/orgs/${orgName}/repos" `
  -Headers ${headers} `
  -ContentType 'application/json' `
  -Body (ConvertTo-Json @{ name = $repoName; auto_init = $True })

リポジトリを作成したら、リポジトリをクローンして作業ディレクトリに移動します。

git clone $repo.clone_url
cd $repoName

Dockerfile の作成

コンテナのビルド手順を記載した Dockerfile ファイルを作成します。ファイルの内容は以下のとおりです。 Arm GNU Toolchain のインストール手順は、 Arm GNU Toolchain のリリースノートに従います。

FROM ubuntu:23.04 AS base_image

RUN export DEBIAN_FRONTEND=noninteractive \
    && apt-get update \
    && apt-get upgrade -y \
    && apt-get install -y --no-install-recommends \
        ca-certificates \
        curl \
        gnupg \
        locales \
        xz-utils \
        cmake \
        make \
        ninja-build \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/* \
    && locale-gen ja_JP.UTF-8
ENV LANG=ja_JP.UTF-8 TZ=JST-9

ARG INSTALL_DIR=/opt/arm
RUN cd /tmp \
    && curl --fail --location --remote-name \
        "https://developer.arm.com/-/media/Files/downloads/gnu/12.2.mpacbti-rel1/binrel/arm-gnu-toolchain-12.2.mpacbti-rel1-x86_64-arm-none-eabi.tar.xz?rev=71e595a1f2b6457bab9242bc4a40db90&hash=41B9EE53CF6E77F7F0647767EC481951" \
    && curl --fail --location \
        "https://developer.arm.com/-/media/Files/downloads/gnu/12.2.mpacbti-rel1/binrel/arm-gnu-toolchain-12.2.mpacbti-rel1-x86_64-arm-none-eabi.tar.xz.sha256asc?rev=9e9d90ff234a44aaafb1c27f0639ea56&hash=0C655496C8164E8FF86D900897681345" \
        | sha256sum --check \
    && mkdir -p "${INSTALL_DIR}" \
    && tar xJf arm-gnu-toolchain-12.2.mpacbti-rel1-x86_64-arm-none-eabi.tar.xz -C "${INSTALL_DIR}" \
    && rm -f arm-gnu-toolchain-12.2.mpacbti-rel1-x86_64-arm-none-eabi.tar.xz
ENV PATH="${PATH}:${INSTALL_DIR}/arm-gnu-toolchain-12.2.mpacbti-rel1-x86_64-arm-none-eabi/bin"

Jenkinsfile の作成

続いて CI パイプラインを定義する Jenkinsfile ファイルを作成します。ファイルの内容は以下のとおりです。ファイル内の ${RegistryAddress} はコンテナレジストリの IP アドレスに置き換えます。

pipeline {
  agent {
    kubernetes {
      yaml """
kind: Pod
spec:
  containers:
  - name: kaniko
    image: gcr.io/kaniko-project/executor:debug # Kaniko のコンテナイメージ
    imagePullPolicy: Always
    command:
    - sleep
    args:
    - 9999999
"""
    }
  }
  stages {
    stage('Build with Kaniko') {
      steps {
        container(name: 'kaniko', shell: '/busybox/sh') {
          sh '''
            /kaniko/executor --context `pwd` --destination ${RegistryAddress}/exampleorg/arm-gnu-toolchain:12.2.mpacbti-rel1-x86_64-arm-none-eabi --insecure
          '''
        }
      }
    }
  }
}

最後に、作成した 2 つのファイルをコミットおよびリポジトリにプッシュします。

git add .
git commit -m 'ファイルを追加'
git push

コンテナイメージのビルド

Jenkins を利用してパイプラインを実行します。まず Jenkins にログインし、 exampleorg フォルダの行の右端にある右向きの三角アイコン(マウスカーソルを当てると Schedule a scan for exampleorg と表示されます)をクリックします。数秒後にページ左側のビルドキュー に exampleorg » arm-gnu-toolchain » main と表示されるので、これをクリックして main ジョブに移動します。このページでコンテナイメージのビルドの様子を確認できます。

exampleorg » arm-gnu-toolchain » main ジョブ

Build with Kaniko ステージのログを見ると、最終的にコンテナイメージ ${RegistryAddress}/exampleorg/arm-gnu-toolchain:12.2.mpacbti-rel1-x86_64-arm-none-eabi がプッシュされたことが分かります。

ビルド #1 の Build with Kaniko ステージのログ

これでビルド/テスト環境のコンテナイメージが用意できました!

新しいコンテナイメージでビルドしてみる

最後に、前回作成した example-pipeline リポジトリの Jenkins パイプラインのコードを変更して、作成したコンテナイメージがきちんと利用できるかを確認してみます。まず example-pipeline リポジトリをクローンしたディレクトリに移動します。

cd ../example-pipeline

このディレクトリに存在する Jenkinsfile ファイルの内容を以下の内容に置き換えます。パイプラインの内容は、作成したコンテナイメージからポッドを作成して、その中でコンパイラのバージョンを出力します。ファイル内の ${RegistryAddress} はコンテナレジストリの IP アドレスに置き換えます。

pipeline {
  agent {
    kubernetes {
      yaml '''
apiVersion: v1
kind: Pod
spec:
  containers:
  - name: arm-gnu-toolchain
    image: ${RegistryAddress}/exampleorg/arm-gnu-toolchain:12.2.mpacbti-rel1-x86_64-arm-none-eabi # 作成したコンテナイメージ
    command:
    - cat
    tty: true
'''
    }
  }
  stages {
    stage('Run gcc') {
      steps {
        container('arm-gnu-toolchain') {
          sh 'arm-none-eabi-gcc --version'
        }
      }
    }
  }
}

変更した Jenkinsfile ファイルをコミットおよびリポジトリにプッシュします。

git add Jenkinsfile
git commit -m 'Arm GNU Toolchain に変更'
git push

変更をリポジトリにプッシュすると、 Jenkins の exampleorg » example-pipeline » main パイプラインジョブの実行が開始されます。ジョブの完了後に Run gcc ステージのログを見ると、 Arm GNU Toolchain のバージョンが出力されたことが確認できます。

ビルド #2 の Run gcc ステージのログ

まとめ

Kubernetes クラスター上の CI 環境で Kaniko を使用してコンテナイメージをビルドする方法をご紹介しました。これによって、ビルド/テスト環境を作成し、その環境でソフトウェアをビルド/テストする、という一連の流れをサポートする 1 つの CI 環境ができました。

なおコンテナレジストリについて、アクセスする際は認証が必要なことが多いと思います。Kubernetes上で Kaniko を使用してコンテナイメージをプッシュする際の認証情報を設定する方法については、 CloudBees CI のドキュメントを参照するのが分かりやすいと思います。

参考

By tsakai

Jenkins関連のサービスやCloudBees製品を主に担当しています。 Certified CloudBees Jenkins Engineer (CCJE) および CloudBees CI DevOps Associate です。