Diary of a Perpetual Student

Perpetual Student: A person who remains at university far beyond the normal period

docker build 時に private リポジトリから go get する

要件

Go 言語で作られた、GitHub の private repository の go module に依存しているアプリケーションについて考えましょう。例えばオープンソースではない社内ライブラリに依存している、という状態です。

手元の開発マシン上では GOPRIVATE 環境変数を利用して、go mod download できている状態です。このアプリケーションはコンテナ上で動かすことを想定しており、Dockerfile 内で RUN go build することでバイナリを生成し container image に格納しようとしています。

しかし、public な module だけに依存しているときと同じように Dockerfile を書いても docker build に失敗してしまいます。go mod download 時に private repository から fetch できないのです。

この問題、あなたなら数ある選択肢からどう取捨選択してこの問題を解決しますか?自分は以下の要件を立てて解決策を模索しました:

  • (must) 開発マシン上で docker build した時に、private repository から go get できる
  • (must) GitHub Actions 上で docker build した時に、private repository から go getできる
  • (should) docker build 時にコンテナ環境内で go mod download -> go build できる
  • (should) 生成された docker image 内に不用意に認証情報が埋め込まれていない
  • (may) GitHub の Private Access Token の運用はしたくない
  • (may) 開発マシン上と GitHub Actions 上で同じ Dockerfile を使いまわせる

GitHub Actions 環境で private repostitory から go get する

zenn.dev

概ねこの記事通りにやれば良いです。GitHub App を作る→GitHub Actions上で一時トークンを生成する→そのトークンを使ってgo get する という流れです。

少し古めの記事を見ると、Private Access Token を用いた方法が紹介されています。PAT に依存する運用では組織ではなく個人に紐づいてしまう、アクセス権限の制御をきめ細やかにできない、シークレットのローテーションを定期的に行う必要があるなどのデメリットがあります。

multi stage build を利用することで解決できる問題ではあるのですが、認証情報を ARG で受け渡しているところは少し気になります。multi stage build を用いない場合、RUN --mount=type=secret を用いて一時トークンを受け渡すとより安全になるでしょう。

docs.docker.jp

開発マシン上で docker build した時に、private repository から go get する

こちらの説明も他の記事に譲ります。

qiita.com

手元の秘密鍵を COPY するパワフルなやり方もありますがまあ怖いですよね。今時のBuildKit には --mount=type=ssh と言うオプションがあり、これを用いて ssh-agent に登録している鍵を docker build 時に一時利用することができます。

合わせ技

上の2つを両立するために以下のような Dockerfile を用意しました:

# syntax=docker/dockerfile:experimental

# ** build stage **
FROM golang:1.20-bullseye AS build

COPY ./ /go/src
WORKDIR /go/src

ENV GOPRIVATE=github.com/Arthur1/something-private-repo

# ここがすごい!
RUN --mount=type=secret,id=github_secret_token --mount=type=ssh \
    if [ -z "$(cat /run/secrets/github_secret_token)" ]; then \
    mkdir -p ~/.ssh && chmod 0600 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts; \
    git config --global url."git@github.com:".insteadOf https://github.com/; \
    else \
    git config --global url."https://x-access-token:$(cat /run/secrets/github_secret_token)@github.com/".insteadOf https://github.com/; \
    fi

RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=ssh \
    go build -o hoge ./cmd/hoge/

# ** main stage **
FROM debian:bullseye-slim

COPY --from=build /go/src/hoge  /hoge

ポイントはシェルコマンドの中で if 文を使っている箇所で、

  • github_secret_token secret がセットされているなら、その secret を用いて https で go get するための git config を用意
  • github_secret_token secret がセットされていないなら、mount した ssh 鍵を使って go get するための git config を用意

と言う挙動を実現しています。

つまり、GitHub Actions 上では DOCKER_BUILDKIT=1 docker build --secret id=github_secret_token,env=GH_SECRET_TOKEN .、手元では DOCKER_BUILDKIT=1 docker build --ssh default . すればコンテナイメージの build 時に private repository から go get してコンテナ上でバイナリビルドができるようになります。

めでたしめでたし。


 
 
 
 
 

END ... ?

これでいいのか?

この Dockerfile を見てなにか腑に落ちない人もいるかなと思います。高々 private repository から go get したいだけなのに、何でこんな大層な workaround を用意しているのだろう、と言う気持ちになりました。

要件を疑え

そもそも、要件にある「 docker build 時にコンテナ環境内で go mod download -> go build できる」という条件は本当に必要でしょうか?

例えば、go module の download は GOMODCACHE 変数を指定した上で Docker の外でやり、mod cache ディレクトリを Dockerfile で COPY してコンテナ内で build はやる、という解決策もあるでしょう。

他にも、クロスコンパイルが得意な Go で docker container 内で build する必要もなくて、docker の外で go build して生成した実行ファイルをコンテナに格納すると言う手法もあります。docker を使ってホスト環境依存をなくしたつもりでも、結局 Linux カーネル大親友である glibc への依存は避けられないですからね。もちろん、Go のバージョンを各自で固定しづらいといったデメリットは生じます。

まとめ

docker build 時に private リポジトリから go get する手法については、要件を定義して、各自でベストと思える解決策を採用しましょう。また、そもそも docker build 時にやる必要ありますか?ということについてもぜひ考え直してみてください。