ちなみに

火曜日の空は僕を押しつぶした。

Dockerコンテナ内でプライベートリポジトリのGemをBundlerでインストールしたい

tl;dr

  • 認証情報をイメージに残さずにGitHubのプライベートリポジトリのGemをインストールしたい
  • ビルド時にインストールする場合は --mount=type=ssh がおすすめ
  • 実行時にインストールする場合には BUNDLE_GITHUB__COM を使うのがおすすめ

やりたいこと

Docker内で bundle install するときに社内で使っているプライベートなGitHubリポジトリのGemが含まれる場合、なんとかして認証情報を渡す必要があります。 イメージのビルド時にインストールするのか、それとも実行時にコンテナ内でインストールするのかでも方法が違うのでそれぞれ方法を探ってみました。

ビルド時にインストールする場合

何も考えずビルド時にSSH秘密鍵をマウントしてみたり、環境変数やビルド時変数としてGitHubの認証情報を渡したりすると、イメージ内に認証情報を残してしまうことになります。 ローカル開発だとまあいのではとも思うのですが、出来るだけリスクは軽減したいです。 この辺りのことが意識できてない人は去年のはてなインターンで出題された Docker Quiz を解いてみると良さそう。

ここで紹介したいのは BuildKitBuild Mounts を使う方法です。

--mount=type=secret

--mount=type=secret を使うとビルド時にだけ秘匿情報をマウントし、イメージレイヤには残さないということが出来ます。 これを使って秘密鍵をマウントすることでプライベートリポジトリにアクセスしようというのが今回の方法です。

# syntax=docker/dockerfile:experimental
FROM alpine
RUN apk add --no-cache openssh-client git
# GitHubの公開鍵を取得しておく必要がある
RUN mkdir -p -m 0600 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts
RUN --mount=type=secret,id=ssh,dst=/root/.ssh/id_rsa bundle install

bundle install するときに秘密鍵をマウントするようにした Dockerfile を書いておいて、ビルド時にGitHubに登録している公開鍵の対となる秘密鍵を指定します。

$ docker build --secret id=ssh,src=/path/to/private_key .

2点注意があって、

--mount=type=ssh

--mount=type=secretでも目的は達成出来るのですが、パスフレーズを設定していない秘密鍵を扱いたくないという課題が残ります。 これを解決するには --mount=type=ssh を使って ssh-agent 経由でホストの秘密鍵を利用する方法が使えます。

# syntax=docker/dockerfile:experimental
FROM alpine
RUN apk add --no-cache openssh-client git
RUN mkdir -p -m 0600 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts
RUN --mount=type=ssh bundle install

先ほどとほとんど同じですが --mount=type=ssh のところが違います。これによってイメージのビルド時に ssh-agent を経由してホストの秘密鍵にアクセス出来ます。もちろんパスフレーズはホストで入力出来るので安心。

$ ssh-add /path/to/private_key
$ docker build --ssh default .

このようにして ssh-add しておいた秘密鍵を利用してプライベートリポジトリにアクセスすることが出来ます。

また、以下のように秘密鍵を指定することも可能です。

$ docker build --ssh default=/path/to/another_private_key .

ここでも注意点があります。何もしていないと uid=0gid=0 でマウントされるのでroot以外のユーザーを使っている場合は意図した動作にならないという点です。 これは以下のようにマウント時に適切なuidとgidを指定することで解決します。

RUN --mount=type=ssh,uid=1000,gui=1000 bundle install

詳しい使い方は リファレンスマニュアル を参照してください。

実行時にインストールする場合

ビルド時にインストール出来ることは確認出来たのですが、普段の開発フローを考えると Gemfile を更新したときに毎回イメージをビルドしなおすのは手間です。 実行時にインストールしなおせるといいのですが、ビルド時に渡した認証情報はどこにも残っていません。ということで実行時に認証情報を渡す方法を探しました。

ちなみに実行時は標準入力にアクセス出来るので1個1個認証情報を入力すればいいという話はありますが、複数のプライベートリポジトリがある場合に毎回入力するのは手間ですよね。

ssh-agent を利用する方法

--mount=type=ssh と同様に ssh-agent を使うのはどうでしょうか。

r7kamura.com

この方法は確立されていて ssh-agent が用意した通信用のソケットをボリュームとしてマウントしつつ、マウントしたパスを SSH_AUTH_SOCK 環境変数に設定してあげるだけです。

ただしこの方法には問題があって macOS 上では動作しません。

https://github.com/docker/for-mac/issues/410 によると /run/host-services/ssh-auth.sock を指定することで回避出来るようなので以下のようにすることでやりたいことは実現できます。

$ docker run -v /run/host-services/ssh-auth.sock:/ssh-agent -e SSH_AUTH_SOCK=/ssh-agent bundle install

詳しい仕組みについてはざっくりと読み飛ばしてしまったので理解出来てなくて、詳しい人いたら教えて欲しいです。 macOS 上で SSH_AUTH_SOCK に指定されているソケットが特殊なもの?みたいな雑な理解です。

BUNDLE_GITHUB__COM を使う方法

ssh-agent を利用しても実現出来ることは分かったのですが、これだと環境依存が発生してしまいます。 もう少しうまい方法はないでしょうか。

Bundler ではGitHubの認証情報渡すのに BUNDLE_GITHUB__COM 環境変数を使うことが出来ます。 これを使うのはどうでしょうか。

この環境変数には user:pass 形式の認証情報も渡せますが、Personal Access Token (PAT) を渡す方が管理しやすいでしょう。 プライベートリポジトリにアクセスしたいので repo 権限をもったPATを生成して以下のようにして渡すことで無事にプライベートリポジトリのGemもインストール出来ました。

$ docker run --rm -e BUNDLE_GITHUB__COM=<YOUR PAT> container bundle install

これまではソースとして SSH プロトコルの形式でURLを指定していましたが、今回はGitHubの認証を使うため HTTP(S) プロトコルで指定する必要がある点が注意です。