go mod vendorについて調べてみる

インターン先でvendorを使うことがあり、go mod tidyとは何が違うのかわからなかったため、vendorとはどんな仕組みなのか調べてみました。

# vendorとは
vendorとは、goの依存パッケージをローカルのプロジェクト内に保存する仕組みです。
`go mod vendor`と打つことでプロジェクト内に`.vendor`ディレクトリが生成され、そこにプロジェクト内で依存しているパッケージをダウンロードしてくれます。

goにおけるパッケージのダウンロードは、`go get`や`go mod tidy`を使用して、`$GOPATH/pkg/mod`にダウんロードすることになります。
このパスはローカルキャッシュとして、他プロジェクトにも同じように使い回していくことができます。

vendorでは、他プロジェクトに使い回すことができず、`.vendor`ディレクトリがあるファイル配下のみでパッケージが機能します。

## vendorのメリット
vendorを行うことでプロジェクトが依存しているモジュールの依存関係固定化を行ってくれます。

ただ、go.sumでversion等は固定されているので、vendorを知らない方は依存関係固定化なんていらなくないか？と思うことがあると思います。

go mod tidy等でダウンロードしたモジュールは`$GOPATH/pkg/mod`にダウンロードされますが、全プロジェクトのパッケージをキャッシュしてしまっているので、プロジェクトに依存しているパッケージだけ抽出してみるということが大分難しいです。

vendorを使うと上のような問題が解決するのですが、大分想像しづらいと思うので、もう少し具体的な例を出してみようと思います。

### 1.オフライン環境下におけるビルド
オフライン環境でビルドしないといけず、ビルド環境に該当パッケージがダウンロードされていないとビルドを行うことができません。
また、モジュールのversionが上がると適宜ビルド環境で特定のモジュールを入れ替えるという作業が必要になります。

そのためvendorディレクトリを作成して、ビルド環境に持っていくことで、依存するパッケージのみが全てビルド環境に入っている状況にすることができます。

### 2.ビルド時間の短縮
上に大分関係していますが、vendorディレクトリにモジュールが入っていることによって、モジュールのダウンロードが不要になるため、結果ビルドする際にかかる時間が少なくなります。


### 3.privateなパッケージの再利用
GitHubのプライベートリポジトリで管理されているGoのパッケージをインストールするには、そのリポジトリへのアクセス権が必要です。ローカルでの開発では自身のGitアカウントを使用できますが、Gitアカウントが紐付いていない環境、例えばGitHub ActionsのようなCI/CD環境でGoモジュールをダウンロードする必要がある場合はどうでしょうか？

GitHub Actionsで自動ビルドやテストを行う際、ホステッドランナー上でGoモジュールをダウンロードする必要があります。このダウンロードには、プライベートリポジトリからクローンするためのPersonal Access Token (PAT) が必要となり、そのPATをGitHub Actionsで設定したり、DockerイメージでのビルドではDockerfileに渡したりする必要があります。

しかし、PATが外部に漏洩すると、そのプライベートリポジトリが悪用される重大なリスクがあります。 このセキュリティリスクを軽減するため、GitHub Actionsでは一時的にPATを発行する actions/create-github-app-token などのActionが提供されています。

このActionを通して発行されたPATを使用してプライベートなモジュールをクローンした後、vendorディレクトリを利用することで、Goモジュールの管理とデプロイをより効率的に行えます。
vendorディレクトリには依存関係にあるモジュールのコピーが保存されるため、Dockerイメージをビルドする際に、自身のストレージに入っているGoモジュールキャッシュ全体をコピーする代わりにvendorディレクトリのみをコピーするだけで済みます。これにより、Dockerfileの記述が簡単にされ、最終的なイメージサイズも小さくなります。 

# vendorを実際に使ってみる
今回は上の話を踏まえてvendorを実際に使ってみようと思います。

## vendorディレクトリを生成
vendorディレクトリを生成してみます。
```zsh
go mod vendor
```
こうすることでvendorディレクトリが生成されます。
```zsh
❯ ls
go.mod  go.sum  main.go vendor
```

vendorディレクトリの中を見てみると下のようになっていました。
```zsh
❯ tree
.
├── github.com
│   └── maooz4426
│       └── hello
│           └── hello.go
└── modules.txt
```

## actionsでvendorのメリットを実感してみる

vendorのメリットの方で書いた`privateなパッケージの再利用`を実感するためにactionsでvendorを使ったビルドを行ってみようと思います。


### まずgo mod vendorを実行してみる
privateなパッケージをgo.modに依存関係として追加するために下のように環境変数を追加します。
```zsh
❯ export GOPRIVATE="github.com/maooz4426/hello"
```
これを行うことでダウンロードできます。

```zsh
go mod tidy
```

go.modに依存関係を追加したのでpushします。

まず失敗する例をworkflowで構築してみます。

```.github.com/workflows/build.yml
name: build
on:
  pull_request:
    types: [opened, synchronize, reopened]
    branches:
      - main

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
      - name: Set up Go
        uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5
        with:
          go-version-file: "go.mod"
      - name: Install dependencies
        run: go mod vendor
      - name: go build
        run: go build -mod=vendor -v ./...
```
すると下のように失敗します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3766230/b13166b2-854c-49fa-bdbc-1c2632179d57.png)

Actionsにはprivateリポジトリの閲覧権限が存在しないため怒られました。

```zsh
Run go mod download
  go mod download
  shell: /usr/bin/bash -e {0}
go: github.com/maooz4426/hello@v1.0.2: reading github.com/maooz4426/hello/go.mod at revision v1.0.2: git ls-remote -q origin in /home/runner/go/pkg/mod/cache/vcs/18842ccf53f7f60310496984429b6bd1a1db7a42f058f8dba917eb73175a7732: exit status 128:
	fatal: could not read Username for 'https://github.com': terminal prompts disabled
Confirm the import path was entered correctly.
If this is a private repository, see https://golang.org/doc/faq#git_https for additional information.
```


### Github Appの準備
上のビルドを成功させるためにGithubAppを準備します。
`Developers settings`からGithubAppを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3766230/feb50892-a8a6-4631-91b2-2a583ee22f2b.png)

下のように該当Appを作ってみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3766230/2d2ce775-7a2b-417c-9e6e-8aafad140c75.png)

Webhookはオンになっていますが、最初はいらないのでチェックは外します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3766230/c3620edb-ca1c-4430-97b4-3e80b4de4802.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3766230/27aa6c40-75b2-42d6-8e0c-493d3876160c.png)

権限はcontentsをread-onlyにします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3766230/1b174bf1-144c-4305-bfca-3d34e02304dd.png)

上でAppを作成した後下のように作成したAppを該当リポジトリにインストールします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3766230/14dd4805-1573-4972-b402-df721b950433.png)

>ダウンロードするモジュールが存在するリポジトリも追加しておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3766230/6540f2e6-4e78-4648-8796-a1dc8e13d522.png)


### repositoryの設定
Github AppのIDをvariablesとして登録します。

IDは該当GithubAppの下の部分に書かれています。
![スクリーンショット 2025-06-15 22.43.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3766230/f4c063f1-5303-47d1-a7d2-02fda1621420.png)

これをvariablesに設定します。
![スクリーンショット 2025-06-15 22.44.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3766230/e7be754a-afa4-434c-883e-a659620cbd96.png)

次に秘密鍵を生成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3766230/2cd27d27-1119-4cdd-9ac5-f508d8f82427.png)

catコマンドで秘密鍵が下の形式で表示されるのでそれをコピーします。

```
-----BEGIN RSA PRIVATE KEY-----

-----END RSA PRIVATE KEY-----
```

これをrepositoryのsecretsに保存します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3766230/2e34be5a-099f-4f16-8ba2-2b8251f90769.png)

### ビルドが成功するようにworkflowを組む

下のようにworkflowsを組んで見ました。
privateリポジトリを見るためtokenを発行するactionを使うことでパッケージのインストールを可能にしています。
```.github.com/workflows/build.yml
name: build
on:
  pull_request:
    types: [opened, synchronize, reopened]
    branches:
      - main

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
      - uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e #v2.0.6
        id: generate-token
        with:
          app-id: ${{ vars.APP_ID }}
          private-key: ${{ secrets.PRIVATE_KEY }}
      - name: Configure Git for private modules
        run: |
          git config --global url."https://x-access-token:${{ steps.generate-token.outputs.token }}@github.com".insteadOf "https://github.com"
        env:
          GOPRIVATE: github.com/maooz4426/*
      - name: Set up Go
        uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5
        with:
          go-version-file: "go.mod"
      - name: Install dependencies
        run: go mod vendor
      - name: go build
        run: go build -mod=vendor -v ./...
```

上のように組んでみるとビルドが成功します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3766230/ba12f6dd-9595-4bb8-96a0-33369f234ea6.png)

### vendorをDockerfileに渡してみる
実際にDockerfileにvendorをコピーさせてみます。
```Dockerfile
FROM public.ecr.aws/docker/library/golang:1.24.2

WORKDIR /app

COPY go.mod go.sum ./
COPY vendor/ ./vendor/
COPY *.go ./

RUN go build -mod=vendor -o main .
```

workflowsは下のように修正してみました。

```.github/workflows/build.yml
name: build
on:
  pull_request:
    types: [opened, synchronize, reopened]
    branches:
      - main

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
      - uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e #v2.0.6
        id: generate-token
        with:
          app-id: ${{ vars.APP_ID }}
          private-key: ${{ secrets.PRIVATE_KEY }}
      - name: Configure Git for private modules
        run: |
          git config --global url."https://x-access-token:${{ steps.generate-token.outputs.token }}@github.com".insteadOf "https://github.com"
        env:
          GOPRIVATE: github.com/maooz4426/*
      - name: Set up Go
        uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5
        with:
          go-version-file: "go.mod"
      - name: Install dependencies
        run: go mod vendor
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb

      - name: Build and push
        uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83
        with:
          context: .
          push: false 
          tags: |
            myapp:latest
            myapp:pr-${{ github.event.number }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
```

先述しましたが、もしvendorを使っていなかった場合は、Dockerfileで`go mod tidy`をする必要があ離ますが、その場合は閲覧権限のあるPATを渡さなければなりません。またGOPATH配下のモジュールキャッシュを使うこともできますが、GOPATHを指定するとイメージサイズが大きくなりますし、特定のパッケージだけコピーするように記述するのも大分めんどくさいため制限があります。

このような問題をvendorを使うことで解決できます。

# まとめ
vendorを使用することで安全にprivateリポジトリからのgoパッケージのクローン、そこからactions等でのビルドが行えるようになります。

個人開発ではあまり出会うことは少ないと思いますが、vendorを使う機会があったら一度調べてみると面白いと思います。