GitHub Actionsでkanikoを使ってコンテナイメージをビルドする技術

2023年05月24日 水曜日


【この記事を書いた人】
李 瀚

締め切りドリブンな生活から脱却したい

「GitHub Actionsでkanikoを使ってコンテナイメージをビルドする技術」のイメージ

概要

kanikoはコンテナの中でコンテナイメージをビルドするソフトウェアです。これを使うことによって、例えばKubernetes上のPodの中でコンテナをビルドすることができるようになります。

一方で、GitHub ActionsのRunnerは揮発的であることが望ましいです。揮発的であることで実行環境を汚染しなくなり、また、セキュリティリスクを下げることもできます。

本稿ではGitHub ActionsのRunnerでkanikoを使うときに課題になること、また組み合わせて動作させるための実験的な試みを記します。

Runnerというもの

サーバ型のGitリポジトリシステムとしてGitHub(Enterprise)とGitLabがあります。(他にもありますが、ひとまず考えないことにします。)

CIは開発の高速道路ともいえるもので、あるのとないのとでは開発時の反復速度が大きく変わります。そのためモダンな開発においてCIが組まれていないことはほぼありません。テストやビルドの再現性や安定性はそのままソフトウェアの品質に直結し、また実行の速度がそのままリリースサイクルに影響します。

上記のような背景もありGitHubでもGitLabでもそれぞれCIを実行するRunnerなるものがあります。RunnerというのはGitHub/GitLab上のリポジトリへの変更をトリガーにしてJobというタスクを与えられて実際に実行するエージェントです。

ところが、GitHubとGitLabではこのRunnerの作りが異なっています。

GitLabではRunner自体はJobを実行するものというよりはJobを実行させるマネージャです。複数のJobがあるとRunnerは複数のコンテナを走らせてその結果を集約する役割を果たします。このような作りのおかげでGitLabはKubernetes上でkanikoの公式コンテナを動かしてコンテナイメージをビルドすることができます。大変考慮された作りとなっています。

GitHubではActionsという機能でRunnerを走らせます。RunnerはJobを直接実行します。結果、Runnerがコンテナを扱うにはDockerを操作する権限が必要です。この作りはKubernetes上では大変厄介であり、なぜならPod内では自由に新たなコンテナを立ち上げることが難しいからです。公式のやり方としてはdind(Docker in Docker)と呼ばれる方法で操作します。このためにRunnerには特権が要求され、またホスト上ではDockerが利用できなければなりません。

GitHub ActionsのRunnerはあまりKubernetesのようなものの上で走ることは考慮されておらず、どちらかというと仮想マシン上で実行されることを前提としています。ただし仮想マシンの場合揮発的にするには重いです。またプライベートクラウドではそもそも仮想マシンをAPIで生やして並べることは難しいです。

社内ではGitHub Enterpriseを利用しています(GitHub Actionsと仲良くなったよ | IIJ Engineers Blog)。また各自Self-hosted Runnerを走らせるコストは高いです。そこでKubernetes基盤上にあるEnterprise Runnerをデプロイしています(actions/actions-runner-controller: Kubernetes controller for GitHub Actions self-hosted runners)。更にこのご時世コンテナイメージをビルドしたい需要も高いです。そこでKubernetes上のGitHub Actions RunnerにおいてGitLabと同様にkanikoでコンテナをビルドできるような環境を整えるモチベーションがあります。

kanikoというもの

kanikoはなぞの技術によってコンテナ上でコンテナイメージをビルドするためのソフトウェアです(GoogleContainerTools/kaniko: Build Container Images In Kubernetes (github.com))。

では、実際どのようにしてkanikoはコンテナ内でコンテナイメージをビルドするのでしょうか。

それはコンテナイメージがtarのレイヤーによって構成されていることを利用しています。kanikoはコンテナの中で以下のステップを踏んでコンテナイメージをビルドします。

  • そのままDockerfileにかかれているコマンドを実行して
  • その後スナップショットと呼ばれる工程でコンテナ内の差分を検知
  • 差分をtarに包めてコンテナイメージを作成する

このような不思議なことができるとなると当然疑問に思うのはkaniko自体もコンテナ内にあるのにどうなってしまうのかということです。

この問題については/kanikoというディレクトリ内に閉じるような作りになっており、そこの差分だけを無視するようになっています。実際はもう少し個別のファイルがありますが、それらは有限個にとどまっており個別に処理されます。

dive(wagoodman/dive: A tool for exploring each layer in a docker image (github.com)を使うとコンテナイメージの内容を見ることができます。kanikoの公式イメージをのぞいてみると中身はすっからかんです。

kanikoのコンテナイメージの中身

上記のようなコンテナイメージをビルドする仕組みにより、kanikoは公式コンテナイメージ以外は動作保証外です(Better support for building custom Kaniko images with Kaniko · Issue #1881 · GoogleContainerTools/kaniko (github.com))。

kanikoとGitHub Actions Runnerの相性の悪さ

さて、なんとなく察する頃かと思われますが、コンテナのGitHub Actions Runner(以後Runner)の中でkanikoを動かすと想定外のことが起きます。

Runner自体は広い依存関係を持っています(runner/envlinux.md at main · actions/runner (github.com))。

Runnerは.NETで書かれており、そもそもシングルバイナリではありません。また動かすのにgitなど多くのコマンドを必要としています。

Runnerは特定のディレクトリ下にすべての作業内容を収める設計になっており、そのディレクトリの外はJobの実行からは隔離されているとみなします。ゆえに外ではどのようなコードや構造を持っていても許容するという設計です。

そしてRunner内にkanikoを動かすと、kanikoは/を手中に収めて暴れ出します。

結果として

  • Dockerfileで入れたものがビルドしたコンテナイメージに含まれない
  • Runnerに必要なファイルが全消しされる

といったことが起きます。

入っているべきものが入っていないのは、ビルドをする前にすでにRunnerのコンテナの中にあるものはkanikoのスナップショット時には検出できないせいです。典型的なパターンとしてファットなRunnerで各種ツールなどがRunnerイメージの中にある場合、ビルド後のコンテナイメージにはRunnerイメージに含まれるファイルの一部が同梱されません。

kanikoを動かすとRunnerがおかしくなるのは重要なファイルが消されるためです。kanikoはコンテナ内をビルドする際に汚すため、multi stage buildをするためには次のステージをビルドする前に差分を消す必要があります。その際にRunnerを動かすのに必要なファイルが全て消されます。

解消するためのトリック: chroot

ところで、/にすべてを展開して動かしても正常に複数の仮想マシンようのプロセスを同一ホスト上で動かす技術があります。それがまさにコンテナという技術です。コンテナ内でもapt updateなどをすることはでき、その際にはホスト側を破壊しません。

その際に使われるものがchrootです。

ということで、kanikoはデフォルトでは/にすべてを展開するため、kanikoを動かすコンテナはクリーンでなければなりませんでしたが、kanikoを例えば/workdir内にchrootして動かすと、kanikoを動かしている間は/workdir/とみなすことが出来て/workdir以外を壊さずに済みます。

kanikoでも当然議論されているはずで、調べてみると出てきます。

実はこのことについてIssueがあるものの、それはできないよとだけ回答されてクローズされています。

また、別のIssueでそれっぽいことができた、という報告があるため、これを真似てみます。

chrootを使ってkanikoをRunner内で走らせるコンテナの作成

まずRunnerとしてkanikoのファイルを持ち込みます。kanikoはすべて/kanikoの中に収まっていることがわかっているため、multi stage buildでバイナリを拝借します。

FROM gcr.io/kaniko-project/executor:v1.9.2 AS base           # <-----公式コンテナイメージからもろもろを拝借するためにASとして取り入れる
 
FROM summerwind/actions-runner:v2.302.1-ubuntu-22.04-7f3eef8
USER root
RUN sed 's/sudo //g' /usr/bin/*.sh -i'.bak'
RUN ln -fs /usr/bin/python3 /usr/bin/python
RUN curl -sS https://bootstrap.pypa.io/get-pip.py | python
RUN apt-get update && apt-get install -y \
    make \
    build-essential \
    && rm -rf /var/lib/apt/lists/*
 
COPY --from=base /kaniko /kaniko           # <-----ここでコピー
 
COPY kaniko-build /usr/bin/kaniko-build
RUN chmod +x /usr/bin/kaniko-build
ENTRYPOINT ["/bin/bash", "-c"]
CMD ["entrypoint.sh"]

そしてkanikoを実行させるのにchrootで包むスクリプトを用意します。Runnerの作りに合わせていくつかパスを変更したりしています。

#!/bin/bash
set -x
 
CURRENT=$PWD
 
dockerfile="$1"
destination="$2"
 
context="$(dirname ${dockerfile})"
 
mkdir /kaniko-workdir
cp -r /kaniko /kaniko-workdir/kaniko
export DOCKER_CONFIG=/kaniko/.docker/
mkdir -p /kaniko-workdir/kaniko/workspace
 
cd /kaniko-workdir
mkdir dev
mknod -m 666 dev/null c 1 3
mknod -m 666 dev/zero c 1 5
 
mkdir -p proc/self
cp /proc/self/mountinfo proc/self/
 
mkdir etc
cp /etc/resolv.conf etc/
cp /etc/nsswitch.conf etc
 
mkdir -p etc/ssl/certs
cat /etc/ssl/certs/* > etc/ssl/certs/ca-certificates.crt
 
cp -r "${CURRENT}/${context}" kaniko/workspace
 
chroot . ./kaniko/executor -f /kaniko-kaniko/workspace/${dockerfile} --context=/kaniko/workspace/ --force --destination="$destination" --cleanup
 
cd $CURRENT

後はkanikoをRunnerで使う際にこのスクリプトを利用させれば破壊を引き起こさずにビルドができるはずです。

利用方法

実際にやることは上記の特殊なコンテナ内で、以下のコマンドを実行します。

kaniko-build ${{inputs.dockerfile}} ${{inputs.image}}:${{inputs.tag}}

使いやすいようにcomposite Actionsで包みます。

name: "Kaniko build"
description: "Build dockerfile with kaniko"
branding:
  icon: anchor
  color: orange
inputs:
  dockerfile:
    description: path to Dockerfile
    default: Dockerfile
  image:
    description: target destination
    required: true
  tag:
    description: target image tag
    default: latest
runs:
  using: "composite"
  steps:
  - name: build
    run: |
      kaniko-build ${{inputs.dockerfile}} ${{inputs.image}}:${{inputs.tag}}
    shell: bash

これを利用するには以下のようなActionsのWorkflowを書きます。

name: build
 
on:
  push:
    branches: [ "actions-build" ]
  workflow_dispatch: 
 
jobs:
  build:
    runs-on:
    - self-hosted
    - enterprise
    - kaniko         # <----------ここで指定して前節のコンテナイメージを使ったRunnerを利用させる
    steps:
    - uses: actions/checkout@v3
     
    - uses: customized-actions/kaniko-build@v2        # <---------上記のcomposite Actionsのリポジトリを指定する
      with:
        image: hogehoge                               # <----コンテナイメージ名
        tag: test                                     # <----コンテナイメージタグ
        dockerfile: Dockerfile                        # <----Dockerファイルの場所

上記の内容だけでmulti stage buildも安定して実施することができるようになりました。

注意点としてchrootをするため、どこをビルドのコンテキストに持ち込むのかという問題があります。上記のスクリプトではDockerfileを指定してあるフォルダをまるごと持ち込んでいます。つまり、注意点として./dockerfiles/Dockerfile.prdのようにDockerfileを別フォルダにしていると、親レベルの情報はビルドコンテキストに持ち込まれず正常にビルドできなくなります。

例外

上記のことをしても正常にビルドできないものがあります。それは/kanikoを展開しないといけないDockerfileのビルドです。

/kaniko内にすべての必要なファイルを入れて保護するが、/kanikoというフォルダを持つコンテナイメージをビルドをすると/kanikoをビルド時に触らなければなりません。

kanikoでkanikoを取り入れると/kanikoを展開した際にkaniko自体が壊れてビルドが失敗します。kanikoをビルドしたい人がそれほど多くはないと信じてビルドがうまくいかないknown bugとして考えることをやめます。

まとめ

Kubernetes上で動かしているGitHub ActionsのRunnerをいじってkanikoを使っても比較的安定してビルドさせるためにchrootを利用した方法を試しました。一般的な振る舞いをするコンテナイメージのビルドについては動作確認を行いました。chrootを利用しない場合と比較して格段にビルドの安定性は向上しました。一方でkanikoのプロジェクト自体でも隔離する方法について検討しているとのことでしたが、うまくマージされてないということはなにかの落とし穴があるのかも知れません。もし本稿の内容を参照する際にはその点にご注意ください。

李 瀚

2023年05月24日 水曜日

締め切りドリブンな生活から脱却したい

Related
関連記事