Self-hostedなGitlabでもdependabotを使いたい

## はじめに

Dependabot（正確にはDependabot security updates）はリポジトリで使用している外部のライブラリやパッケージのアップデートを検出して、外部パッケージの管理ファイル(*1)に対する修正リクエスト(*2)を自動で作成してくれるツールです。

[DependabotはGithubに買収されています](https://github.blog/jp/2019-05-24-building-an-interconnected-community-together/) ので、開発元はGithubとなります。  
そのため、GithubではDependabotがサッと使えるようになっているのですが、同じことをself-managedなGitlabでもやってみたいと思います。

Dependabotのことをより知りたい人は [Github Docs - Code security - Dependabot を使用してサプライ チェーンを安全に保つ](https://docs.github.com/ja/code-security/dependabot) を読んでみましょう。

(*1) Pythonで言うrequirements.txtや、Goで言うgo.modなどです
(*2) 言わずもがな、Gitlabではマージリクエストのことです

## 環境の前提

Gitlabの環境についてはお手元のものをお使いください。  
一から構築する場合は [GitlabとKanikoによるコンテナの自動ビルド&配布環境の構築](https://qiita.com/ymbk990/items/a526fa0ed87f2a999ebb) でも紹介しています。  
UIや操作面で若干違いがあるかもしれませんが、参考にしていただければ幸いです。

以降は以下の設定が終わっている前提で進行します。

- GitlabおよびGitlab runner（Docker）が設定済み
- 空っぽのプロジェクトが作成済み
  - 今回は `qiita/dependabot` を作りました
- ユーザーはGitlabに対してgit pull/pushするためのgit設定が完了している
- Githubのアカウントを持っている（dependabotで使用するトークン取得のため）


## サンプルプロジェクトの作成

まずは、Dependabotでパッケージ更新をチェックするためのサンプルコードを作成します。  
空のプロジェクトを取得してスタートです。

```
git clone http://qiita-gitlab.example.com/qiita/dependabot.git
cd dependabot
```

今回はPythonでやってみましょう。  
Dependabotに関する設定などは後で行うので、ここでは以下の2ファイルだけを作成します。  
`http://localhost:8080` にGETリクエストを送るだけの簡単なプログラムです。

:::note info
2023/10/13 時点ではrequestsの最新版は2.31.0です。この後の手順では、dependabotの設定をすることで、requestsパッケージのアップデートをするマージリクエストが作成されることを確認していきます。
:::

requirements.txt

```
requests==2.29.0
```

sample.py

```python
import requests


def get(url):
    resp = requests.get(url)
    print(f"GET {url} returned {resp.status_code}")


if __name__ == '__main__':
    get("http://localhost:8000")
```

もちろん通信相手がいなければ例外がドバーっと出力されるわけですが、本題では無いので気にしないでください。

あとは、これらのコードをリポジトリに格納します。

```
git switch --create main
git add requirements.txt 
git add sample.py 
git commit -m "Add sample code"
git push --set-upstream origin main
```

## Dependabotの導入にあたって

DependabotはGithubが開発していますが [dependabot-coreはOSSとして公開](https://github.com/dependabot/dependabot-core) してくれています。  
これを使って [Gitlabでdependabotを動かすプロジェクト](https://dependabot-gitlab.gitlab.io/dependabot/) がありますので、これを参考に導入していきます。

特に参考になるのは同プロジェクトの [standalone](https://dependabot-gitlab.gitlab.io/dependabot/guide/deploy.html#standalone) の部分の説明ですね。

> Application can be used in a stateless cli like mode. This mode is most useful to run dependency updates from Gitlab CI. dependabot-gitlab/dependabot-standalone describes the best way to run dependency updates from gitlab ci. Like docker compose, all configuration is done via environment variables.

というわけで https://gitlab.com/dependabot-gitlab/dependabot-standalone をに見に行くと、サンプルとなる .gitlab-ci.yml と README.md が置かれています。

必要なトークンは以下の2つです。

* `SETTINGS__GITLAB_ACCESS_TOKEN` - gitlab personal access token with `api` access scope and at least `Developer` role
* `SETTINGS__GITHUB_ACCESS_TOKEN` - github personal access token with repository read scope

更に、スケジューラ登録時に必要な変数は以下の3つです。

* `PROJECT_PATH` - project path, like `dependabot-gitlab/dependabot`
* `PACKAGE_MANAGER_SET` - comma separated package eco-systems, like `bundler,docker` (dependency file must be in the same directory for multiple package managers to work within same schedule job)
* `DIRECTORY` - update directory, usually `/` if not a larger monorepo

そこで、サンプルプロジェクトに手を入れる前に、まずは必要なトークンを取得します。

### Githubトークンの取得

GithubアカウントのSettings左下の "Developer settings" を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2613240/bf7c3850-bd0f-9a8b-e789-39e79cc07238.png)

Personao access tokensからclassicのトークンを選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2613240/401082f9-b527-754d-efb8-6fc673c33d95.png)

アクセスが必要なのは `public_repo` だけです。Expirationは各自の用途に合わせて設定してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2613240/0da5cfe3-02cf-b302-99ba-f0ce8f7966cc.png)

トークンが生成出来たらすぐ後で使うので保存しておきます。

### Gitlabトークンの取得

Gitlabに対しては、Dependabotがプロジェクトにマージリクエストを作成するための権限が必要です。  
今回はプロジェクトアクセストークンを発行して、プロジェクト固有で使えるようにします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2613240/9da7d8b1-7511-c39b-135a-4d8edfb48dff.png)

必要な権限は `api` スコープに対して `Developer` ロールが必要です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2613240/aafade4a-ce81-aca5-3c8e-7de609e08d4d.png)

Gitlabではトークンの有効期限は最長1年となっていますが、指定しないとデフォルトで1年になります。

### GitlabプロジェクトのCI/CD変数設定

取得したトークンをプロジェクトのCI/CD変数に設定しておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2613240/81c6a682-b312-a91d-f50e-0ad552301deb.png)

変数名は `SETTINGS__GITLAB_ACCESS_TOKEN` と `SETTINGS__GITHUB_ACCESS_TOKEN` が予約名なので、その通りにします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2613240/d11569be-d8ee-0e1d-6eb6-451990168cb4.png)

* `Protect variable` については、以降の手順で動作確認用のブランチを作成するため、一時的にOFFにしておきます
* `Mask variable` はトークンがログに出力されないようにONにしておきましょう
* `Expand variable reference` はデフォルトのONで良いです


## Dependabotのコード追加

Pythonだけなので https://gitlab.com/dependabot-gitlab/dependabot-standalone/-/blob/main/.gitlab-ci.yml の中からPythonに関係する部分だけを抜き出して、以下のような .gitlab-ci.yml を作成します。

:::note info
今回は例示のために必要部分だけを抜き出していますが、通常は上記のリポジトリからファイルをそのまま取得して、includeするのが簡単で良いでしょう。
:::

この記事では `3.5.0-alpha.1` を使用していますが、最新のリリース状況については https://gitlab.com/dependabot-gitlab/dependabot/-/releases を参照してください。

```yaml
default:
  image: python:3.11.6-slim
  tags:
    - docker

variables:
  DEPENDABOT_GITLAB_IMAGE: docker.io/andrcuns/dependabot-gitlab
  DEPENDABOT_GITLAB_VERSION: 3.5.0-alpha.1

.dependabot-gitlab:
  image:
    name: ${DEPENDABOT_GITLAB_IMAGE}-${CI_JOB_NAME}:${DEPENDABOT_GITLAB_VERSION}
    entrypoint: [""]
  variables:
    GIT_STRATEGY: none
    RAILS_ENV: production
    SECRET_KEY_BASE: key
    PACKAGE_MANAGER: $CI_JOB_NAME
    SETTINGS__GITLAB_URL: $CI_SERVER_URL
    SETTINGS__STANDALONE: "true"
    SETTINGS__LOG_COLOR: "true"
  script:
    - cd /home/dependabot/app
    - bundle exec rake "dependabot:update[${PROJECT_PATH?},${PACKAGE_MANAGER?},${DIRECTORY?}]"

pip:
  extends: .dependabot-gitlab
  rules:
    - if: $DEPENDENCY_UPDATES_DISABLED
      when: never
    - if: '$CI_PIPELINE_SOURCE == "schedule" && $PACKAGE_MANAGER_SET =~ /\bpip\b/'
```

更に、dependabotには https://gitlab.com/dependabot-gitlab/dependabot-standalone のREADMEに書かれているように `.gitlab/dependabot.yml` を作成する必要があります。

dependabot.ymlの形式はGithub公式の [dependabot.yml ファイルの構成オプション](https://docs.github.com/ja/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file) に従って書くことになります。  
上記の文書には `package-ecosystem` / `directory` / `schedule.interval` が必須となっていますが、スケジューラはGitlab側で設定することになるため `schedule.interval` は参照されません。  
従って、Gitlabにおける最小限構成は以下のようになります。

```yaml
version: 2
updates:
  - package-ecosystem: "pip"
    directory: "/"
```

それでは、新しくブランチを作成して、今作ったファイルを追加したらpushします。

```
git switch --create dependabot
git add .gitlab-ci.yml
git add .gitlab/dependabot.yml
git commit -m "Add dependabot"
git push --set-upstream origin dependabot
```

## Dependabotの試験動作

それでは、Gitlabプロジェクトでスケジューラを設定してDependabotを動かしてみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2613240/f2f57c4e-ead7-d8dc-75ea-98a0081c2ba4.png)

設定値は以下の通り。

* Description: なんでも良いです。とりあえずdependabotと書きました。
* Interval Pattern: 状況に合わせて設定してください。今回はデフォルトのEvery day。
* Cron timezone: 状況に合わせて設定してください。とりあえず日本にいるのでTokyo。
* Select target branch or tag: 試験動作なので、先ほどpushした `dependabot` ブランチを選択。
* Variables
    * `PROJECT_PATH` : 今回は `qiita/dependabot` になります
    * `PACKAGE_MANAGER_SET` : 今回は `pip` になります
    * `DIRECTORY` : 今回は `/` になります
        * モノレポなどで、プロジェクトファイルが下の階層にある場合は、構成ファイルが置かれているディレクトリを指定します。
    * `SETTINGS__CONFIG_BRANCH` : 今だけ `dependabot` を設定します。これは `.gitlab/dependabot.yml` を取得するブランチを指しており、mainブランチにマージするまでは設定しておきます。
* Activated: 試験動作が終わるまではOFFにしておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2613240/ed5c3db1-3bf5-89a1-b869-d51a705f0284.png)

それでは、作成したスケジューラを実行してみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2613240/5b7493cf-2696-dbb6-85bf-aabbef659f09.png)

Pipelineが成功したら、Merge requestsが1件増えたようです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2613240/ffab045d-bbcf-7355-54da-14b9a4483a93.png)

古いパッケージが検出されて、マージリクエストが作成されました！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2613240/bd05f32d-178d-54b7-a068-32b2f279fd5e.png)

マージリクエストにはRelease NoteやVulnerabilities fixedが書かれており、いちいち調べに行かなくてもサマリーがまとまっています。  
このVulnerabilities fixedについては、情報源がGitHub Advisory Databaseなので [CVE-2023-32681](https://github.com/advisories/GHSA-j8r2-6x86-q33q) のページに書かれている内容と同じ文章が使われているはずです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2613240/b791707e-4092-a630-1282-97fb4e672ce2.png)

:::note info
Dependabotが作成するマージリクエストの文章粒度はパッケージの作成者によるため、必ずこれだけの情報量があるわけではありません。  
とはいえ、大抵リポジトリにタグが打ってあるので、コミット一覧くらいは最低限取得できると思ってよいでしょう。
:::

そして、requestsパッケージを2.31.0にアップデートする変更がちゃんと加えられています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2613240/9d7423b8-fac2-fe18-e163-853a09fe6f6d.png)

変更に問題がなければ、このマージリクエストはそのままマージしてしまいましょう。


## CIコードのマージと後始末

最後に、動作確認が出来てmainブランチ（Protectedなブランチ）にマージしたら、試験動作用の設定を削除して、定期実行を有効にします。

* CI/CD -> Schedules -> dependabot
    * Select target branch or tag を `main` に変更（※各自のブランチ戦略に合わせてください）
    * Variables `SETTINGS__CONFIG_BRANCH` を削除
    * Activatedにチェックを入れて、定期実行を有効化
* Settings -> CI/CD -> Variables
    * `SETTINGS__GITHUB_ACCESS_TOKEN` のFlagsで `Protect variable` を有効化
    * `SETTINGS__GITLAB_ACCESS_TOKEN` のFlagsで `Protect variable` を有効化
    * これで、Protectedなmainブランチ以外ではdependabot用のトークンが参照できなくなります。

設定変更後に再度手動でdependabotのパイプラインを実行し、念のため完走することを確認して完了です。  
パッケージの更新が無ければ重複したマージリクエストは作られませんので、何度実行しても大丈夫です。


## おしまい

最初は勢いでソフトウェアを作ってみたものの、ふと目を離すと知らない間に依存ライブラリがアップデートされている...というのは良くある話ですよね。  
自分の書いたコードには更新が無いけど、依存ライブラリの更新が無いか定期的にチェックできると、とても便利です。  
脆弱性が見つかったパッケージにすぐ対応できるので、セキュリティリスクを減らすことが出来るでしょう。

それだけでなく、Dependabotの作るプルリクエスト（マージリクエスト）には、Release NoteやChangelogが書かれているのがとても親切です。  
あると便利なのになと思っていた機能がある日追加されているのに気付いたり、知らない機能に対応したRelease Noteを見て新しい知識を取り入れたり、自身のアップデートにも活用できます。

是非色々なプロジェクトに導入してみてください。