猫の手なら貸せる

いろいろ共有できたらいいなとおもってます

ラズパイ上GitLab-CI/CD開発環境の構築を検証した

RaspberryPi上のmicrok8sクラスタ上に、Docker buildが可能なGitLab-runnerをいろいろな方法で構築してみました。

前提/目的

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 、実行方法の選択肢が存在します。

docs.gitlab.com

たくさん挙げられていますが、大まかなくくりとしては

  • 直接ホストで実施するタイプ(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に与える権限によっては何でもできてしまう
    • ホスト環境によるビルド結果の依存が発生する
  • 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の取得方法の例です。 f:id:nkhnd:20210418201326j:plain

必要な範囲に応じて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-daemontcp通信を行う方法もあります。

ただし、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