ラズパイ上GitLab-CI/CD開発環境の構築を検証した
RaspberryPi上のmicrok8sクラスタ上に、Docker buildが可能なGitLab-runnerをいろいろな方法で構築してみました。
- 前提/目的
- GitLab-runnerについて
- GitLab-runnerの構築の下準備
- ホストマシン上でDockerコマンドを実施して構築
- DockerコマンドをDockerコンテナ内から実施して構築
- 結論
前提/目的
RaspberryPiがクラスタ化されていて、microk8sクラスタとGitLabノードが存在しています。
GitLabではGitLab-registry(insecure-registry)が有効になっており、GitLabで開発したコードをcommitをトリガーに逐次コンテナイメージ化&レジストリへpushを行いたいと考えています。
GitLab上でDocker buildを行う方法は複数あり、今回の環境(RaspberryPi on microk8s)上ではどれが最適解なのかを検証することが目的です。
- RaspberryPi 4B(8GB RAM)
- OS: Ubuntu 20.04.2 LTS
os-release
$ cat /etc/os-release NAME="Ubuntu" VERSION="20.04.2 LTS (Focal Fossa)" ID=ubuntu ID_LIKE=debian PRETTY_NAME="Ubuntu 20.04.2 LTS" VERSION_ID="20.04" HOME_URL="https://www.ubuntu.com/" SUPPORT_URL="https://help.ubuntu.com/" BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy" VERSION_CODENAME=focal UBUNTU_CODENAME=focal
GitLab-runnerについて
GitLab-runnerはGitLabのpipelineの状態を監視し、jobをポーリングするGitLabのCI/CDを実際に行う機関です。
GitLabとGitLab-runnerは疎結合の形をとっているため、GitLab-runnerはGitLabとネットワーク的に接続されていればどんな環境下でも実施することができます。
Executorの種別
GitLab-runnerはGitLabで実施するCI/CDの内容によって使い分けるために Executor 、実行方法の選択肢が存在します。
たくさん挙げられていますが、大まかなくくりとしては
- 直接ホストで実施するタイプ(Shell, SSH, VirtualBox, Parallels)
- 仮想環境上で実施するタイプ(Docker, Docker-Machine, Kubernetes)
となっています。仮想上で実施するタイプのメリットは一般にコンテナを使うメリットと同様です。
Dockerイメージのビルド方法の選定
今回はkube環境におけるデプロイを目的としているので、Docker buildを行えるコンテナ環境の構築を行います。
そもそもDocker buildを行う方法として次の3パターンが存在します。
- ホストマシン上でDockerコマンドを実施して構築(Shell executor)
- DockerコマンドをDockerコンテナ内から実施して構築(Docker executor)
- Dockerコンテナ内でDocker buildをする方法
- Docker daemonをホストと共有する方法
以下に特徴を記述します。強調部はメリット、強調斜体部はデメリットになります。
- ホストマシン上でDockerコマンドを実施して構築
- gitlab-runner導入済みのホストで
gitlab-runner
ユーザーがコマンドを実施 - 導入が簡単で実行結果が予想しやすい。
- gitlab-runnerに与える権限によっては何でもできてしまう。
- ホスト環境によるビルド結果の依存が発生する。
- gitlab-runner導入済みのホストで
- Dockerコンテナ内でDocker buildする方法
- DinD(Docker in Docker)と呼ばれる方法
- コンテナ内でDocker daemonを起動する。
- ホスト上で動いているコンテナから別離が可能。
- ただしprivilegedオプション(特権モード)が必要になる
- Docker daemonをホストと共有する方法
- DooD(Docker Out of Docker)と呼ばれる方法
- Unixソケットを通じてコンテナ内からホストのDockerデーモンにアクセスする。
- privileged不要。Dockerコマンド以外でホストに影響を与えない。
- コンテナ内からホストで動作中のコンテナにアクセス可能な状態になってしまう。
ホストマシン上でDockerを実施する方法については、今回の環境(Docker build on microk8s)に適合しないため、検証結果に含めないものとします。
GitLab-runnerの構築の下準備
tokenについて
Gitlab-runnerはGitLabにアクセスするためにtoken
を利用します。合鍵のようなもので、GitLabから払い出されるregistration token
をGitLab-runner上でregister
を行うことでrunner向けに払い出されます。
registration tokenはプロジェクト固有・所属グループ全体・GitLab全体の3種類の権限が存在します。
以下は所属グループ内で利用できるrunnerのregistration tokenの取得方法の例です。
必要な範囲に応じてregistration tokenを確認しておきます。
Dockerの導入
Docker buildを利用するためにはDockerが必要です。変な響きですが、例えばmicrok8sのcontainerdでは代用は効きません。
aptとsnapは構造に違いがあり、aptはパッケージの技術を、snapはコンテナの技術を用いられています。
このためsnapは同じアプリケーションを複数バージョン所持したり、設定などで環境が汚れにくい等メリットが高いですが、設定ファイルのパスがapt版やマニュアルなどの内容と異なったり、これらの情報がaptより少ない印象を受けます。
いずれかの方法でホストマシンにDockerをインストールしてください。
# aptでインストールする場合 sudo apt install docker.io # snapでインストールする場合 sudo snap install docker
さらにインストール後、insecure-registryを使用する場合はdaemon.json
に設定を行い、読み込ませる必要があります。
aptで導入した場合のinsecure-registry設定
/etc/docker/daemon.json
にGitLab・GitLab-registryが存在するアドレスを記述します。
cat /etc/docker/daemon.json { "insecure-registries" : ["XXXXXXXXXXXXXX:XXXX"] }
dockerデーモンに新しい設定を読み込ませたら完了です。
sudo systemctl daemon-reload
snapで導入した場合のinsecure-registry設定
snapのinsecure-registryの設定は/etc/docker/daemon.json
ではなく、/var/snap/docker/current/config/daemon.json
にあり、バージョンによらず参照するようになっています。
cat /var/snap/docker/current/config/daemon.json { "insecure-registries": ["XXXXXXXXXXXXXX:XXXX"], "log-level": "error", "storage-driver": "overlay2" }
設定の再読み込みを行わせます。
snap restart docker
ホストマシン上でDockerコマンドを実施して構築
今回の検証結果には含めませんが、別途で考える必要があったため記載しておきます。
Shell executorのトークンを発行・導入
Shell executorを採用する場合はホストマシンにgitlab-runnerを導入する必要があります。
curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-ci-multi-runner/script.deb.sh | sudo bash
sudo apt-get install gitlab-runner
最新状況は公式を確認してください。
docs.gitlab.com
上記で導入したらrunnerを設定します。--url
にはGitLabのURLを、--registration-token=
にはGitLab GUI上で取得したregistration tokenを入力します。
--tag-list
にはタグを設定します。.gitlab-ci.yaml内でrunnerを区別するために使用します。今回は他順にshell
としています。
タグを使用しない場合は--tag-list=shell
を削除し、--run-untagged
と記述します。
gitlab-runner register --non-interactive --url=http://XXXXXXXXXXXXXX/ --registration-token=XXXXXXXXXXXXXX --description=Shell --tag-list=shell --executor=shell
Shell executorの設定は以上で完了です。GitLabがjobを発行した際、Shell executorはgitlab-runnerユーザによってホストマシン上で命令を実行します。
Shell executorの場合.gitlab-ci.yml
ホストマシンで実行するのと同じように記述します。ここではdockerコマンドが入力できるようにあらかじめgitlab-runner
ユーザがdocker
グループに入っている、またはsudoer
などでdocker利用時にrootであることが必要です。
build-shell: tags: [shell] stage: build script: - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN $CI_REGISTRY - docker build . -t "${CI_REGISTRY_IMAGE}:${CI_COMMIT_REF_SLUG}" --build-arg GITLAB_URL=$CI_PROJECT_URL --build-arg REF_NAME=${CI_COMMIT_REF_SLUG} - docker push "${CI_REGISTRY_IMAGE}:${CI_COMMIT_REF_SLUG}"
DockerコマンドをDockerコンテナ内から実施して構築
Docker executorを採用する場合、ホストマシンにgitlab-runnerを導入したいことはまずないと思いますので、
ここではmicrok8sクラスタ上でgitlab-register
を実施することにしました。
Docker executorのトークンを発行
gitlab-register
さえ出来れば良いので単発のJobを書きます。以下はDocker executorのサンプルです。
apiVersion: batch/v1 kind: Job metadata: name: gitlab-runner-register namespace: gitlab-runner spec: backoffLimit: 0 template: spec: restartPolicy: Never containers: - image: gitlab/gitlab-runner:latest command: - gitlab-runner - register - --non-interactive - --url=http://XXXXXXXXXXXXXX/ - --registration-token= XXXXXXXXXXXXXX - --description=Docker-in-Docker - --tag-list=docker - --executor=docker - --docker-image=docker:latest - --docker-network-mode host # - --run-untagged #タグを使わない場合は指定 # - --docker-priviledged #priviledgedが必要な場合指定 name: gitlab-runner ports: - containerPort: 80 name: http - containerPort: 443 name: https volumeMounts: - name: config mountPath: /etc/gitlab-runner/ readOnly: false volumes: - name: config hostPath: path: /tmp/gitlab-runner/ type: DirectoryOrCreate
--url
にはGitLabのURLを、--registration-token=
にはGitLab GUI上で取得したregistration tokenを入力します。
--tag-list
にはタグを設定します。.gitlab-ci.yaml内でrunnerを区別するために使用します。今回はDocker buildを行うため、docker
を指定しました。
その他のオプションも必要に応じて設定します(--docker-network-mode host
はGitLab-runner内でDNS名前解決するために必要です。)
以上がk8sに適応され、コンテナが実行すると、Podが実行されたホストの/tmp/gitlab-runner/
にconfig.toml
が作成されるので、これをメモしておきます。
Docker executorを導入
Dockerコンテナ内でDocker buildする方法
Dockerコンテナの内部でDocker buildを行う方法です。
gitlab-runner本体は以下です。PrivilegedをTrueにしていますが、socketは別途マウントしてあげる必要があるようです。
apiVersion: apps/v1 kind: DaemonSet metadata: name: gitlab-runner namespace: gitlab-runner spec: selector: matchLabels: name: gitlab-runner template: metadata: labels: name: gitlab-runner spec: containers: - image: gitlab/gitlab-runner:latest name: gitlab-runner ports: - containerPort: 80 name: http - containerPort: 443 name: https volumeMounts: - name: config mountPath: /etc/gitlab-runner/config.toml readOnly: true subPath: config.toml - name: socket mountPath: /var/run/docker.sock readOnly: true securityContext: privileged: true volumes: - name: config configMap: name: gitlab-runner-config - name: socket hostPath: path: /var/run/docker.sock restartPolicy: Always
configmapは以下。ImageにはDinDを指定し、子のコンテナ起動のprivilegedもONにします。こちらではvolumesへのsocketの記述は不要です。
apiVersion: v1 kind: Namespace metadata: name: gitlab-runner --- apiVersion: v1 kind: ConfigMap metadata: name: gitlab-runner-config namespace: gitlab-runner data: config.toml: |- concurrent = 1 check_interval = 0 [[runners]] name = "Docker-in-Docker" url = "http://XXXXXXXXXXXXXX/" token = "XXXXXXXXXXXXXX" executor = "docker" [runners.docker] tls_verify = false image = "docker:dind" privileged = true disable_cache = false volumes = ["/cache"] shm_size = 0 [runners.cache]
以上で構築は完了です。
Docker daemonをホストと共有する方法
ホスト上のDocker daemonを共有してdocker buildを行う方法です。DinDの場合と異なり、Privilegedは必要ありません。
gitlab-runner本体
apiVersion: apps/v1 kind: DaemonSet metadata: name: gitlab-runner namespace: gitlab-runner spec: selector: matchLabels: name: gitlab-runner template: metadata: labels: name: gitlab-runner spec: containers: - image: gitlab/gitlab-runner:latest imagePullPolicy: Always name: gitlab-runner ports: - containerPort: 80 name: http - containerPort: 443 name: https volumeMounts: - name: config mountPath: /etc/gitlab-runner/config.toml readOnly: true subPath: config.toml - name: socket mountPath: /var/run/docker.sock readOnly: true volumes: - name: config configMap: name: gitlab-runner-config - name: socket hostPath: path: /var/run/docker.sock restartPolicy: Always
configmapの設定です。volumesの値にsocketをマウントするように記述します。
また、GitLabのURLで名前解決を行っている場合はnetwork_mode = "host"
を指定してネットワーク情報をコンテナに共有してもらいます。
apiVersion: v1 kind: Namespace metadata: name: gitlab-runner --- apiVersion: v1 kind: ConfigMap metadata: name: gitlab-runner-config namespace: gitlab-runner data: config.toml: |- concurrent = 1 check_interval = 0 [[runners]] name = "Docker-in-Docker" url = "http://XXXXXXXXXXXXXX/" token = "XXXXXXXXXXXXXX" executor = "docker" [runners.docker] tls_verify = false image = "docker:latest" privileged = false disable_cache = false # 以下の設定によってホストのdocker daemonと接続しています volumes = ["/cache","/var/run/docker.sock:/var/run/docker.sock"] network_mode = "host" shm_size = 0 [runners.cache]
以上で構築は完了です。
Docker executorの利用
DinDとDooDの場合で.gitlab-ci.yml
の内容が異なってきます。
DinDの場合のgitlab-ci.yml
DinDではservice
を利用してDocker daemon用のコンテナを起動して、client-daemonでtcp通信を行う方法もあります。
ただし、DinDのコンテナイメージにはclientも同封されているため、以下のように内部でデーモンを起動することで、1つのコンテナ内で実施することが可能です。
また、DinDではホストのDocker daemon設定が読み込まれないため、insecure-registryを利用する場合は引数で指定して設定を行う必要があります。
build-dind: variables: DOCKER_HOST: unix:///var/run/docker.sock tags: [docker] stage: build image: docker:dind script: - dockerd --insecure-registry= XXXXXXXXXXXXXX:XXXX & - sleep 5 - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN $CI_REGISTRY - docker build . -t "${CI_REGISTRY_IMAGE}:${CI_COMMIT_REF_SLUG}" --build-arg GITLAB_URL=$CI_PROJECT_URL --build-arg REF_NAME=${CI_COMMIT_REF_SLUG} - docker push "${CI_REGISTRY_IMAGE}:${CI_COMMIT_REF_SLUG}"
DooDの場合のgitlab-ci.yml
DooDはDinDのようにデーモンを起動させる必要がありません。
build-dood: variables: DOCKER_HOST: unix:///var/run/docker.sock tags: [docker] stage: build image: docker:dind script: - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN $CI_REGISTRY - docker build . -t "${CI_REGISTRY_IMAGE}:${CI_COMMIT_REF_SLUG}" --build-arg GITLAB_URL=$CI_PROJECT_URL --build-arg REF_NAME=${CI_COMMIT_REF_SLUG} - docker push "${CI_REGISTRY_IMAGE}:${CI_COMMIT_REF_SLUG}"
ホストのDocker daemonの設定を読み込むため、insecure-registyを利用する場合はホストのdaemon.json
を編集してください。
結論
本環境(GitLab-runner on microk8s)では以下の結論を得ました。
- Dockerコンテナ内でDocker buildする方法(DinD)
- k8sPodで利用する場合は結局docker.socketのマウントも必要
- ホストのDockerから切り離せる
- gitlab-runner-helperがCI実施時にDinDコンテナを構築するため、レイヤーキャッシュが行われない。
- 毎回レイヤーを構築するため、通信が多く発生する
- 毎回レイヤーを構築するため、キャッシュエラーなどとは無縁
- Docker daemonをホストと共有する方法
- privileged不要。docker.socketは必要
- ホストのDockerを共有できる
- gitlab-runner-helperがCI実施時のみDooD用コンテナを構築するため、常にホストへの脆弱性があるわけでもない
- ホストのキャッシュを使うことができ、高速
デメリットの少なさを考えると、GitLabでbuildを行う用途に限ってはDocker daemonをホストと共有する方法(DooD)が優勢なように感じました。
逆にDinDを利用しないといけないのは、gitlab-runner-helperを介さず、k8s上でDocker daemonを利用するコンテナを構築する場合、例えばk8sPodをshell環境としてユーザに貸し出すサービスなどが該当すると考えられます。
所感
GitLab-runnerを理解せずに構築しようとしていた時期があり、この頃はk8s上ならexecutorはkubernetesだろうなどと右往左往していた時期がありました。
多分できるのだろうけど固定でPodを立てておくより面倒くさそうなので、また構築を見直す際には構築してみようとおもいます。
参考
Docker in Docker のベタープラクティス - Qiita
PANIC: The docker-image needs to be entered (#2862) · Issues · GitLab.org / gitlab-runner · GitLab
Docker in Docker (dind) でプロジェクト毎にSandbox化すると便利だよ - Qiita
shell executorでdocker buildを実行する件
GitLab CI/CDによるDockerイメージのビルド | GitLab
dindについて
GitLab の CI/CD で Docker in Docker - Qiita
doodとdindの違い
Docker で立てた CI で Docker Build する -Docker in Docker と /var/run/docker.sock - 🤖
privilegedオプションの開発によりDocker in Dockerが可能になった -> Dockerはホストのデバイス利用しないと機能できない
Docker can now run within Docker - Docker Blog
変数内変数展開
【bash】変数の値に含まれる変数を展開させる方法 - Qiita
パイプラインをより柔軟に制御
GitLab CI/CDパイプライン設定リファレンス(日本語訳:GitLab CI/CD Pipeline Configuration Reference) - Qiita