プロジェクト間共通ライブラリを GitLab Package Registry に登録して使う

そこそこ以上の規模のプロジェクトを作る際、どうしても複数プロジェクト間で共有する共通ライブラリ(`*.jar`など)を作りたいことがあると思うのですが、GitLabには [GitLab Package Registry](https://docs.gitlab.com/ee/user/packages/package_registry/) という **共通ライブラリをGitLabに登録できる便利な機能** があります。

通常であれば、 [Sonatype Nexus](https://www.sonatype.com/products/repository-oss) や [JFrog](https://jfrog.com/ja/) といったアーティファクトマネージャを別個で立てなければならないのですが、これを使えばGitLab一つで済むので非常に楽です。

従来は有料版のみの機能だったのですが、なんと 13.3 から全エディションで利用可能になっています。

そういうことで、実際に仕事で使ってみたのですが、ややドキュメントが分かりづらかったので、概要を適当にまとめておきます。


## 結局 GitLab Package Registry　とはどういう機能なのか？

公式ドキュメント: https://docs.gitlab.com/ee/user/packages/package_registry/

GitLab上に `*.jar` (Maven) や `*.dll` (.NET) などを登録できるプライベートなパッケージレジストリを持つことができます。自前でGitLabを運用している場合は他機能と同様、パッケージファイルそのものはS3上にオフロードすることが可能です。

ここでいうパッケージというのは、下記を指します。

- Maven (Java)
- NPM (JavaScript)
- NuGet (.NET)
- PyPi (Python)
- 汎用ファイル置き場
- 以下はまだ開発中 (2022/01/11時点)
    - Ruby Gems (Ruby)
    - Composer (PHP)
    - Go (プロキシ)
    - Debian packages
    - Conan (C/C++)
    - Helm chart

**この記事ではMaven (Gradle)で構成していきます。**

パッケージ種類ごとにかなり設定値や方法が異なるので、他のパッケージを登録する際は[ドキュメント](https://docs.gitlab.com/ee/user/packages/package_registry/)をチェックしてください。

一般的な利用方法としては、リポジトリ上に共有ライブラリプロジェクトを作成して、そのプロジェクトのCIがパスしたらビルドパイプラインの最後にそのプロジェクトのパッケージレジストリに、バージョン付きでライブラリバイナリをpushして利用するのがほとんどだと思います。

弊社でも画面とAPIの共通仕様を共有ライブラリプロジェクトとして作成して、両者のコード生成に利用しています。


## Maven レジストリの場所（エンドポイント）・認証方法

プライベートなパッケージレジストリなので、利用にはレジストリのURLおよび、レジストリにログインするためのユーザIDとパスワードによる認証が必要です。

### レジストリのURL (エンドポイントURL)

#### リポジトリレベルのレジストリ （登録・参照用）

パッケージはすべてプロジェクト内のレジストリに登録していきます。

ドキュメントを見ると、グループやインスタンス(GitLab)レベルでレジストリが生えてそうな気がしますが、そいつらは参照専用のレジストリであって、結局のところパッケージはプロジェクト以外には登録できません。ハマりポイントです。

具体的には下記のようなURLがプロジェクトの（登録用の）MavenレジストリのURLになります。

```
https://gitlabのURL/api/v4/projects/プロジェクトID/packages/maven

# e.g. プロジェクトID=100であれば
#   https://gitlabのURL/api/v4/projects/100/packages/maven
```

#### グループ・インスタンス(GitLab)レベルのレジストリ （参照用）

各プロジェクトに登録されたパッケージを参照する際はプロジェクト単位・グループ単位・GitLab全体(インスタンスレベル)といった複数の粒度でパッケージを参照することができます。

これは、Mavenがたまたまそうなっているだけで、他のパッケージの種類だと使えない粒度があります。たとえば、npmだとグループレベルのレジストリはありません。

具体的には下記のようなURLで参照することが可能です。

```
(プロジェクト単位のURL形式はさっきと同じ)

グループ単位
https://gitlabのURL/api/v4/groups/グループIDもしくはグループ名/-/packages/maven

# e.g. グループID=64であれば
#   https://gitlabのURL/api/v4/groups/64/-/packages/maven
# e.g. グループ名=foo/barであれば
#   https://gitlabのURL/api/v4/groups/foo%2Fbar/-/packages/maven

GitLab全体(インスタンスレベル)
https://gitlabのURL/api/v4/packages/maven

※ Mavenの場合は "パッケージ名" = "グループ名/プロジェクト名" という一致をしているパッケージ名じゃないとインスタンスレベルのURLは利用不可
  (要らぬ制約のように見えますが、gitlab.comをホスティングする運用を考えれば当然の制約ではあります)
```

**Mavenのパッケージを登録する時はプロジェクト単位のURL、参照する時はグループ単位のURLを使うのが一般的** かなと思います。別にプロジェクトのリポジトリをグループ内で使いまわしてもいいですが……


### レジストリの認証方法

Mavenパッケージレジストリにおいて、ログインのための認証情報は下記の3パターンのいずれかを利用します。

- プライベートトークン
    - ユーザID: `Private-Token`
    - パスワード: GitLabのユーザ設定より`api`スコープをつけたプライベートトークンを発行したもの
- デプロイトークン
    - ユーザID: `Deploy-Token`
    - パスワード: プロジェクトの設定より発行した`read_package_registry` / `write_package_registry` スコープをつけたデプロイキー
- CIトークン
    - ユーザID: `Job-Token`
    - パスワード: CI/CDパイプライン中のジョブ実行中、 `CI_JOB_TOKEN` という環境変数に入っている値
    - （※ トークンの権限範囲はリポジトリにpushしたユーザに準じます）

ローカルの開発環境ではプライベートトークン、GitLab CI/CD実行中はCIトークンを使えば問題ありません。

デプロイトークンは外部コントリビューターとのパッケージ読込・登録を行う際に利用すればいいでしょう。

## GradleでGitLab Package Registry (Maven)を使ってみよう

Gradle (Groovy) でGitLab Package Registryを使ってみます。

### 手動でパッケージ登録してみよう

下記を `build.gradle` なりに記述して `./gradlew publish` を実行するだけです。

```gradle:build.gradle
publishing {
    publications {
        mavenJava(MavenPublication) {
            from components.java
        }
    }
    repositories {
        maven {
            url "https://GitLabのURL/projects/プロジェクトID/packages/maven"
            credentials(HttpHeaderCredentials) {
                name = "Private-Token"
                value = "プライベートトークン"
            }
            authentication {
                header(HttpHeaderAuthentication)
            }
        }
    }
}
```

なお、実際プライベートトークンをそのまま `build.gradle` に記述するのはセキュリティ上大いに問題があるので、 `~/.gradle/gradle.properties` などの外部ファイルに記述を移すことをおすすめします。（Gradleから読み込めれば記述場所はなんでもいいです）

```gradle:build.gradle
            credentials(HttpHeaderCredentials) {
                name = "Private-Token"
                value = myPrivateToken // <= "プライベートトークン"
            }
```

```propeties:~/.gradle/gradle.properties
myPrivateToken=プライベートトークン
```

## Gradle からパッケージを参照してみよう

グループレベルでパッケージを参照してみます。 リポジトリの部分に下記を追加すればOKです。

```gradle:build.gradle
maven {
    name "GitLab"
    url "https://GitLabのURL/api/v4/groups/グループID/-/packages/maven"

    credentials(HttpHeaderCredentials) {
        name = "Private-Token"
        value = "プライベートトークン"
     }
     authentication {
        header(HttpHeaderAuthentication)
    }
}
```

登録同様、 `"プライベートトークン"` 部分は `build.gradle` に書かないほうがいいでしょう。

### GitLab CI/CD させてみよう

上記までの例では、プライベートトークンのみ利用していましたが、`build.gradle`にプライベートトークンベタ書きでもしない限り、この設定をGitLab CI/CDにはそのまま使えません。

GitLab CI/CDでは `CI_JOB_TOKEN` という環境変数に入っている値を `Job-Token` というユーザー名とともに使用する必要があります。

なので、やや冗長ですが、 `build.gradle` 上では下記のような記述で `Private-Token` と `Job-Token` 両者を共存させる必要が出てきます。

```gradle:build.gradle(例としてリポジトリ参照)
maven {
    name "GitLab"
    url "https://GitLabのURL/api/v4/groups/グループID/-/packages/maven"

    credentials(HttpHeaderCredentials) {
        def ciToken = System.getenv("CI_JOB_TOKEN")

        // CI_JOB_TOKEN が指定されている場合とされていない場合で指定を分ける
        if (ciToken) {
            name = "Job-Token"
            value = ciToken
        } else {
            name = "Private-Token"

            // myPrivateToken をそのまま参照すると
            // ~/.gradle/gradle.properties に指定がない場合に落ちるので間接的に参照する
            value = project.properties["myPrivateToken"]
        }
    }
    authentication {
        header(HttpHeaderAuthentication)
    }
}
```

もしくは、事前に `~/.gradle/gradle.properties` にユーザ名とパスワードを用意しておく構成にしてもいいです。CIではビルド前にこのファイルを用意するステップを追加すればいいでしょう。

（こちらの手法であれば、さらにDockerでコンテナをビルドしたいといった場合に `RUN --mount=type=secret` を使ってシークレットを一時的にマウントする手法にも応用できます）

```~/.gradle/gradle.properties
gitlabRepositoryUsername=foo
gitlabRepositoryPassword=プライベートトークン (or ジョブトークン)
```

```gradle:build.gradle(例としてリポジトリ参照)
maven {
    name "GitLab"
    url "https://GitLabのURL/api/v4/groups/グループID/-/packages/maven"

    credentials(HttpHeaderCredentials) {
        name = gitlabRepositoryUsername
        value = gitlabRepositoryPassword
    }
    authentication {
        header(HttpHeaderAuthentication)
    }
}
```

ちなみにCI/CDで登録したパッケージは契機になったブランチ・コミットハッシュが表示されるようになります。便利ですね！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1577592/4b3a62ea-5b98-e85d-02fd-a376644b0dd7.png)

（上段が手動、下段がCI/CDで登録）


## まとめ

そういうことでGitLab Package Registryの簡単な使い方でした。

これのほかにも、GitLabには

- Dockerイメージを登録できる [GitLab Container Registry](https://docs.gitlab.com/ee/user/packages/container_registry/)
- `docker pull` のキャッシュを取ってくれる [Dependency Proxy](https://docs.gitlab.com/ee/user/packages/dependency_proxy/)
    - 将来的にいろんなパッケージに対応してくれるのを期待
- Terraformのtfstateの管理をしてくれる [GitLab managed Terraform State](https://docs.gitlab.com/ee/user/infrastructure/iac/terraform_state.html)

といったソフトウェア開発を支援する機能があり、GitLab一本でソフトウェアプロジェクト周りの管理が完結できるようになってきています。

規模の小さいプロジェクトであればGitLab一つあれば完結するのではと思います。GitLabの今後にますます期待です。