Golangはじめて物語(第12話: セルフマネージドなGitLabで共通モジュール運用編)

# はじめに
Golangシリーズ第12回。
以前の記事ではCodeCommitを用いたモジュール管理の方法を書いたが、今回はGitLabを使い、より実践的にモジュールを管理・運用する方法をまとめてみる。

https://qiita.com/neruneruo/items/fdbb9bbe02861223c215

前提知識としては以下の通り。
- 基本的なGitの操作ができること
- 基本的なGitLabコンソール画面の操作ができること

また、Golangのバージョンは1.21.7を用いているため、今後のバージョンアップによりコマンド等が変更になる可能性があることは留意いただきたい。

# 共通モジュールの作成
今回は```module-group/submodule```という名前のモジュールを作っていく。
また、GitLabは```example.com```というドメインで運用する前提とする。

## コードを作成する
今回のサンプルでは中身はとりあえず何でもよいので、標準出力するだけの超簡単なモジュールにする。

```golang:submodule.go
package submodule

func HelloModule() {
    print("Hello World!!")
}
```

上記を作成したら、go.modを作成しよう。

```shell-session
$ go mod init example.com/module-group/submodule
```

生成されたgo.modは以下のような内容だ。

```go.mod
module example.com/module-group/submodule

go 1.21.7
```

## 作成したモジュールをPublishする
上記で作成したモジュールを以下の手順でGitLabに登録していこう。

```shell-session
$ git init
$ git remote add origin https://example.com/module-group/submodule.git
$ git add .
$ git commit -m "Initial Commit"
$ git push --set-upstream origin master
$ git tag v0.1.0
$ git push origin v0.1.0
```

GitLabのコンソールで以下のようにタグが打たれていればOKだ。
![キャプチャ1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/84abdd5e-c347-86c3-ffa8-d82a9b87cbc2.png)

## アクセストークンを払い出す
共通モジュールを運用することを考えた場合、以下の事を考慮した方が良いだろう。

- モジュール提供側がモジュール利用者側一人一人を意識しないようにする
- モジュール利用者側は共通モジュール単位にトークン管理なんてしたくない(管理煩雑になる)
- 万が一アクセスのための情報が流出した場合は速やかにクレデンシャル情報を置換可能にする

セキュリティ的に強固なのはモジュール単位×ユーザ単位でのトークン管理になるが、運用負荷が高いので、今回は、グループに対してアクセストークンを払い出すことを考える。

グループに対するアクセストークンの払い出しはコンソールの以下の部分から行える。

![キャプチャ2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/1d54f8b9-b696-f372-496d-6dd470d5c79c.png)

Token nameはテキトーなものを設定しておけば良いが、以下は誤るとリポジトリへのアクセスができないため、間違いなく設定しよう。

| 項目| 値 |
|---|---|
| Select a role | Reporter |
| Select scopes | read_repository |

なお、Terraformでもトークン払い出しが可能だ。

```HCL
resource "gitlab_group_access_token" "example" {
  group        = gitlab_group.example.id
  name         = "Example group access token"
  expires_at   = "2024-06-30"
  access_level = "reporter"

  scopes = ["read_repository"]
}

output "gitlab_group_access_token" {
  sensitive = true
  value = gitlab_group_access_token.example.token
}
```

払い出したトークンは以下のコマンドで参照ができる(Sensitive Valueであるため、単にOutputするだけでは参照ができない)。

```shell-session
$ terraform output -json | jq -r .gitlab_group_access_token.value
```

TerraformでGitLabを利用するための準備は、以下の記事を参考にしていただきたい。

https://qiita.com/neruneruo/items/55bfbbe0ab2fd5e9b785

# 共通モジュールを利用する
さて、次は共通モジュールを利用するアプリを作っていく。

## アプリケーションの作成
こちらも、まずは簡単に以下のように作成をする。

```golang:main.go
package main

import (
    submodule "example.com/module-group/submodule"
)

func main() {
    submodule.HelloModule()
}
```

## 共通モジュールのダウンロード
モジュールを取り込みたいが、ここで問題がある。
Golang側で共通モジュールを取り込む際に、```example.com```からモジュールを引っ張ってくるようにしたい。

GitLabの公式ドキュメントによると、

https://docs.gitlab.com/ee/user/project/use_project_as_go_package.html#authenticate-git-requests-to-private-subgroups

> ### Authenticate Git requests to private subgroups
> If the Go module is located under a private subgroup like gitlab.com/namespace/subgroup/go-module, then the Git authentication doesn’t work. It happens, because go get makes an unauthenticated request to discover the repository path. Without an HTTP authentication via .netrc file, GitLab responds with gitlab.com/namespace/subgroup.git to prevent a security risk of exposing the project’s existence for unauthenticated users. As a result, the Go module cannot be downloaded.
> 
> Unfortunately, Go doesn’t provide any means of request authentication apart from .netrc. In a future version, Go may add support for arbitrary authentication headers. Follow golang/go#26232 for details.

とのことで、GolangとGitの設定に閉じて設定できれば良かったが、.netrcを使う必要がある。
以下の行を.netrcに追記しよう。

```.netrc
machine example.com login oauth password <事前準備したグループアクセストークン>
```

また、Golangの設定で以下のようにすることで共通モジュールのレジストリの向き先をセルフマネージドなGitLabに変更できる。

```shell-session
$ go env -w GOPRIVATE="example.com"
```

:::note info
```GOPRIVATE```を設定することで、```GONOPROXY```と```GONOSUMDB```も設定が行われる。```GOPRIVATE```の値をクリアすれば同様に残りもクリアされるので、「設定した覚えがない！」とならないよう留意しておこう。

```shell-session
# go env | grep GONO
GONOPROXY='example.com'
GONOSUMDB='example.com'
```
:::

この状態で、```go get```することでGitLabからサブモジュールをダウンロードできる。

```shell-session
$ go mod init main
go: creating new go.mod: module main
go: to add module requirements and sums:
        go mod tidy
$  go mod tidy
go: finding module for package example.com/module-group/submodule
go: found example.com/module-group/submodule in example.com/module-group/submodule v0.1.0
```

ダウンロード後、go.modは以下のような内容になる。

```go.mod
module main

go 1.21.7

require example.com/module-group/submodule v0.1.0
```

## コンパイルして実行する
ここまでできたら、あとはビルドして動かしてみるだけだ。

```shell-session
$ go build
$ ./main
Hello World!!
```

バッチリ動いた！
これで、共通モジュールの運用を開始することができる！

# 共通モジュールのバージョンアップをする
さて、ここで、元の共通モジュールは標準出力した文字列に改行が入っていないことに気付く。
これを、利用者みんなに修正してもらいたい。

そんな時は、タグをRetract(撤回)しよう。

Retractの詳細については、Golangのドキュメントを参照。
本記事では、ポイントを掻い摘んで説明をする。

https://go.dev/ref/mod#go-mod-file-retract

まずは、コードを以下のように修正する。

```diff_go:submodule.go
package submodule

func HelloModule() {
-   print("Hello World!!")
+   println("Hello World!!")
}
```

その上で、共通モジュールのgo.modに、以下の情報を追加しよう。
※Qiitaの仕様上シンタックスハイライトがうまくできていないため念のために補足するが、追加行の行頭の「+」は実際には記載しない

```diff:go.mod
module ec2-35-78-96-59.ap-northeast-1.compute.amazonaws.com/r-kubota-golang-smgitlab-module-group/submodule

+ retract v0.1.0 // Fixed HelloModule()

go 1.21.7
```

上記を実行後、以下の手順で新しいバージョンをpublishしよう。

```shell-session
$ git add .
$ git commit -m "Modify HelloModule()"
$ git push --set-upstream origin master
$ git tag v0.2.0
$ git push origin v0.2.0
```

この状態で、mainアプリケーション側で```go get```すると以下のようなWarningが出る。

```shell-session
$ go get
go: downloading example.com/module-group/submodule v0.1.0
go: warning:example.com/module-group/submodule@v0.1.0: retracted by module author: Fixed HelloModule()
go: to switch to the latest unretracted version, run:
        go get example.com/module-group/submodule@latest
```

これにより、共通モジュールを修正してもらいたい場合はビルド時に相手に気付いてもらえるようになる。

ただし、これはあくまでもwarningであり、```echo $?```しても結果は0になってしまう。
つまり、CI/CDで検知をしたい場合は、以下のようにして```go get```の結果を解析して分岐を作ろう。

```shell-session
$ go get > get.log 2>&1
$ RETRACT=`cat get.log | grep warning | grep retract | wc -l`
$ echo ${RETRACT}
1
```

これで、セルフマネージドなGitLabでも安全に共通モジュールを運用できるようになったはずだ。