🦊

k8s上でdockerイメージをbuild&pushする

2021/10/10に公開

個人で開発用の環境を持つことを考えた時、有力な選択肢にgitlabの構築が挙げられます[1]

今回はk8s上で、gitlabのリポジトリ上で管理されているdockerfileをdocker build & pushし、コンテナレジストリにpushする機構 = k8s上にdocker build可能なgitlab-runner Podを、kubernetesの機能のみ用いてデプロイします。(helm等のマネジメントツールは不要です。)

gitlabについて

gitlabはgithubをプライベートクラウド/ローカルで構築することができるパッケージです。
https://about.gitlab.com
クラウド上でのコードの管理・チーム開発などの基本機能の他、コンテナレジストリの構築やCI/CDなど、多くの拡張機能によってカスタマイズが可能です。

gitlab-runnerについて

gitlab-runnerはgitlabで発行されたjobを受信し、CIを実施するための機構です。
gitlabリポジトリへのpushを検知し、あらかじめ定義されたコマンドを各executorを用いて実行することができます。

gitlab-runnerはexecutorを実行してコマンドの実行を実現します。
https://docs.gitlab.com/runner/executors/index.html

今回はgitlab-runner内でdockerコマンドを実行できる、dockerコンテナイメージを用いて、docker executorを実行するという流れになっています。
https://hub.docker.com/_/docker

アクセストークン登録の自動化

gitlabは、登録済みのgitlab-runnerに対してリクエストを許可する仕組みになっているため、runnerごとにアクセストークンを発行する必要があります。

アクセストークンの発行はrunner側で登録時に手動で実施する必要があるのですが、k8sの機能、initコンテナ・LifecycleEventsを用いることで、この部分を自動化することができます。

initコンテナとは

initコンテナはPodをメインで構成するコンテナが動作する前に実行されるコンテナです。
メインコンテナで利用するファイルをあらかじめ作成しておいたり、セットアップを実行させることができます。

https://kubernetes.io/ja/docs/concepts/workloads/pods/init-containers/

LifecycleEventsとは

LifecycleEventsは、Pod内のコンテナに対して特定のタイミングで、コマンドを実行することができる機能です。
コンテナのプロセスはルートプロセスとして実行されますが、LifecycleEventsは同コンテナ別プロセスとして実行されます。

https://kubernetes.io/ja/docs/tasks/configure-pod-container/attach-handler-lifecycle-event/

initコンテナとLifecycleEventsの違い

両者の特徴は以下のようになります。

  • initコンテナ

    • Podに対して設定、Podに設定されたコンテナの前にデプロイされる。
    • initコンテナが失敗した場合は、メインコンテナは実行されない
    • initコンテナは終了する必要がある。initコンテナ終了後はメインプロセス(コンテナ)からは隔離。
      • k8sのtemplate.spec.containersの要領でマニフェストを記述できるため、volumeマウントなどが可能
      • このため、主にメインコンテナで使用する必要のあるデータなどを作成するために使用される。
  • LifecycleEvents

    • コンテナに対して設定、postStartとpreStopが設定可能。
      • postStartはコンテナのルートプロセス実行前に実行。
      • preStopはコンテナ終了前に実行。
    • postStart/preStop失敗時はコンテナは強制終了される
    • LifecycleEventsはコンテナのサブプロセスとして実行される
      • postStartはルートプロセスより後に実行される可能性がある。
      • preStopはPod自体の終了でも稼働するが、処理の終了前にPodが削除される可能性もある。
        • template.spec.terminationGracePeriodSeconds(v1.20~)の設定により終了処理用の時間確保が可能
      • 実行分類によってはシェル/インタープリターを必要とする(exec)

gitlab-runnerでの利用

gitlab-runnerでは、初期トークンの作成をinitコンテナで実施し、configを作成します。本編のコンテナでここで作成されたconfigを使用。gitlab-runnerが不正停止された場合、preStopの実行によりgitlab上のgitlab-runnerのリストから除外することができます。

構築方法

動作の概要

本記事で構築するマニフェストは、次のような仕組みで動作します。

gitlabの構築

gitlabは以下の方法で構築できます。
https://www.gitlab.jp/install/?version=ce#ubuntu
今回はk8s on RaspberryPiでHAクラスタ構築にて、ansibleを用いて構築しています。

gitlabの初期トークンの固定化

通常、registerトークンについてはgitlabのWebUIを見なければregisterトークンがわかりません。
ただし、gitlab初期構築時の場合は、configに記載することでアクセストークンを固定化することができます。

/etc/gitlab/gitlab.rbの任意の場所に以下を追加します。

/etc/gitlab/gitlab.rb
gitlab_rails['initial_shared_runners_registration_token'] = 'gitlabtoken'

設定変更したらgitlab-ctl reconfigureします。

dockerのインストール

今回はDooD(docker out of docker)での構築を行うため、gitlab-runnerを実行するノードにdockerをインストールが必須になります。[2]

https://docs.docker.com/engine/install/ubuntu/

マニフェストのデプロイ

initコンテナで設定ファイルを作成し、メインコンテナから設定ファイルの存在するディレクトリをマウントするという流れをマニフェストに記述します。

container-builder.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: container-builder
spec:
  replicas: 2 # レプリカを用意すると、それぞれのPodがregisterされるため、並列実行できます。
  selector:
    matchLabels:
      name: container-builder
  template:
    metadata:
      labels:
        name: container-builder
    spec:
      nodeSelector:
        interface: ethernet
      initContainers:
        - name: init-runner
          image: gitlab/gitlab-runner:latest
          command:
            - gitlab-runner
            - register
            - --non-interactive
            - --url=http://<gitlabのURLを入力>/
            - --registration-token=<gitlabから確認できるregister-tokenを入力>
            - --description=container-builder
            - --tag-list=docker
            - --executor=docker
            - --docker-image=docker:latest
            - --docker-network-mode=host
            - --docker-volumes=/var/run/docker.sock:/var/run/docker.sock
            # - --docker-extra-hosts=<urlが名前解決できない場合はIPアドレスでURLを設定>
          volumeMounts:
            - name: config
              readOnly: false
              mountPath: /etc/gitlab-runner/
      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/
              readOnly: false
            - name: socket
              mountPath: /var/run/docker.sock
              readOnly: true
          lifecycle:
            preStop:
              exec:
                command: ["gitlab-runner", "unregister", "--all-runners"]
      volumes:
        - name: config
          emptyDir: {}
        - name: socket
          hostPath:
            path: /var/run/docker.sock
      restartPolicy: Always

上記をkubectl apply -fすれば、gitlabにrunnerが登録され、CIの実行準備が完了です。

.gitlab-ci.ymlの作成

gitlabでCIを実行するためには、gitリポジトリのルートディレクトリに次のようなファイルを設置します。

.gitlab-ci.yml
stages:
  - build

sample-app:
  tags: [docker]
  stage: build
  script:
    - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN $CI_REGISTRY
    - docker build ./${CI_JOB_NAME} -t "${CI_REGISTRY_IMAGE}/${CI_JOB_NAME}:${CI_COMMIT_TAG:=latest}"
    - docker push "${CI_REGISTRY_IMAGE}/${CI_JOB_NAME}:${CI_COMMIT_TAG:=latest}"

上記は、pushされたgitlabのコンテナレジストリに対して<registryURL>/<gitプロジェクト名>/sample-app:latest(tagがpushされた場合はtag)で登録処理を行うサンプルになります。

${CI_***}となっている環境変数はgitlabの組み込み変数で、環境に応じた値が自動的に代入されます。
https://qiita.com/ynott/items/4c5085b4cd6221bb71c5

以上で設定は完了です。

今後の課題

今回はコンテナのビルドを実行可能な機構の構築方法について考えた結果、kubernetesのinitコンテナ・およびLifecycleEventsの利用を行い、gitlab-runnerを用いて実現することができました。

gitlab CI/CDによる工程の自動化はビルドに限らず、テストやデプロイも存在するため、今後はこちらをどのように実装するかを考えていくことになりそうです。

参考

https://www.skyarch.net/blog/?p=16552
https://kubernetes.io/ja/docs/concepts/containers/container-lifecycle-hooks/

脚注
  1. プライベートクラウド/ローカル上の制約がない場合はgithubも選択肢となります。CI/CDについてはGitHub Actionsで実現可能です。 ↩︎

  2. DinD(docker in docker)も手法としては存在しますが、docker-daemonプロセスを別途実行する必要があり、kubernetesのCRIとの二重実行でリソースを無駄に消費してしまうため、今回は選択肢になりませんでした。 ↩︎

Discussion