Docker container で private gem を利用する


2023年 07月 13日

こんにちは。SI部の r_maeda です。

私の所属するSI部では、普段 Ruby on Rails を利用して Webアプリケーションを開発しています。

そんな中で「複数のシステムをまたがって共有できる、共有したほうがよい」コードを書くこともあり、そういったコードについては gem として切り出し、独立したリポジトリで管理することがあります。

例えば、RuboCop の設定ファイルなどはその一つですね。

RuboCop の設定ファイルであれば秘匿情報などは含まれないため、公開リポジトリで管理しても問題はないのですが、中には非公開リポジトリで管理したほうがよい gem もあります。

私は普段の開発に Docker を利用しているのですが、非公開リポジトリで管理されている gem を Docker image / Docker container から利用するためには、いくつかの工夫が必要となります。

今回は Docker image / Docker container から、非公開の Git リポジトリに置かれた gem を利用するために必要な設定・手順について、紹介していきたいと思います。

事前準備

今回紹介する方法では、Docker ホストの SSH 鍵を利用して、Docker image / Docker container から非公開リポジトリへの接続を行います。

そのため、Docker ホスト側での事前準備が必要になります。

Docker ホストから非公開リポジトリに SSH アクセスできるようにしておく

Docker ホストから、非公開リポジトリへのアクセスができるようにしておいてください。具体的には以下の手順を行います。

  • SSH 秘密鍵/公開鍵のペアを生成する
  • SSH 公開鍵を Git ホスティングサービス (i.e. GitHub, GitLab, etc) に登録しておく

Docker ホストから非公開リポジトリの git clone ができれば、問題ありません。

Docker ホストの ssh-agent を起動しておく

ssh-agent とは、SSHの公開鍵認証が必要な場面において、秘密鍵を直接指定する代わりに利用できるアプリケーションです。

あらかじめ秘密鍵とそのパスフレーズを与えて ssh-agent を起動しておくことで、「公開鍵認証を行うたびにパスフレーズを要求される」ことがなくなります。

また、ssh-agent は「秘密鍵を保持しているホスト」以外にも共有できるため、多段 SSH などを行う際にも活躍します。踏み台となる SSH サーバに秘密鍵を置いておく必要がなくなるため、セキュリティの面でもより安全な通信を行うことが出来ます。

Linux, macOS それぞれでの ssh-agent の起動方法は、以下の通りです。

# Linux
ssh-add path/to/private_key # 鍵の追加 e.g. `ssh-add ~/.ssh/id_ed25519`
eval $(ssh-agent -s)        # ssh-agent が起動し、SSH_AUTH_SOCK 環境変数が設定される

# macOS
ssh-add path/to/private_key # ssh-add をきっかけに ssh-agent が起動するため、ssh-add のみを実行すればよい

Gemfile (Gemfile.lock) に private gem を含めておく

Docker image / Docker container から利用したい gem の情報を、Gemfile に書いておきます。

gem "some-private-gem", git: "git@github.com:timedia/some-private-gem.git"

Docker image に private gem を含める

本番環境で利用する Docker image などでは、アプリで利用する gem を Docker image に含めておく必要があります。この場合、Docker image のビルド中に、非公開リポジトリにアクセスするための設定が必要となります。

Dockerfile を作成する

まずは Dockerfile を作成しましょう。以下に簡単な例を出します。

# syntax=docker/dockerfile:1.2

FROM ruby:3.2-slim

RUN apt-get install -y build-essential git

RUN mkdir -p -m 0600 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts

COPY Gemfile Gemfile.lock .
RUN --mount=type=ssh bundle install

この Dockerfile で注目してほしいのは次の2行です。

  1. RUN mkdir -p -m 0600 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts
  2. RUN --mount=type=ssh bundle install

まず ssh-keyscan の行では、ssh-keyscan を実行することで github.com の SSH ホストキーを取得し、取得したホストキーを ~/.ssh/known_hosts に記録しています。
この行を省略した場合、「ホストキーの検証に失敗しました(=信用していいホストか分からなかったよ)」といった警告が出力され、Gitリポジトリからソースコードを取得できません。

#9 13.48 Git error: command `git clone ... has failed.
#9 13.48 Host key verification failed.
#9 13.48 fatal: Could not read from remote repository.

次に RUN --mount=type=ssh の行では、bundle install を実行し gem を取得するにあたって、「ホストの ssh-agent を利用する」ように指示しています。
この --mount=type=ssh は「ホストの ssh-agent を利用したい RUN ステップ」には毎回記載する必要があります。

補足: Dockerfile 先頭のコメント文について

1行目の # syntax=docker/dockerfile:1.2 は、Dockerfile の構文バージョンを指定するための特別なコメントです。
今回利用した RUN --mount=type=ssh は、Dockerfile 1.2 から導入された構文なので、それ以降のバージョンを指定する必要がありました。

この記事を編集している現在(2023/06/26)、最新の構文バージョンは 1.5.2 です。そのため、十分に新しい Docker engine を使っていれば、このコメント行は省略しても動きます。しかし、Dockerfile をチームで共有するのであれば、全員の engine のバージョンが十分新しいとは限らないため、書いておくのが無難かと思います。

Docker image をビルドする

先の Dockerfile で定義したイメージをビルドするためには、 docker build の実行時に --ssh オプションを指定する必要があります。docker build の実行時にこのオプションが指定されていないと、DockerfileRUN --mount=type=ssh と記述されていたとしても、「ホストの ssh-agent を利用する」ことはできません。

docker build --ssh default .

docker compose build を利用する場合でも、同様に --ssh オプションを指定することで、イメージのビルド中にホストの ssh-agent を利用できるようになります。

docker compose build --ssh default

Docker container から private gem を取得する

開発環境で利用する Docker image / Docker container の場合、コンテナの実行中にアプリが利用している gem を取得、更新したい場面があります。

先ほど紹介した方法は「Docker image のビルド中に非公開リポジトリにアクセスする」ためのものでしたが、次は「実行中の Docker container から非公開リポジトリにアクセスする」ために必要な設定を見てみましょう。

必要な設定は以下の2つです。

  • Docker container に、ホストの ssh-agent のソケットファイルをマウントする
  • Docker container に SSH_AUTH_SOCK 環境変数を設定し、マウントしたソケットファイルを参照させる

ホストの ssh-agent ソケットファイルは、OS によって「どこに存在するか」が異なるため、Linux と macOS では異なる path をコンテナにマウントしてあげる必要があります。

# ホストが Linux の場合
# ssh-agent のソケットファイルはホストの環境変数 $SSH_AUTH_SOCK に格納されている
docker run --rm -it \
  -e "SSH_AUTH_SOCK=/tmp/ssh-auth.sock" \
  -v "${SSH_AUTH_SOCK}:/tmp/ssh-auth.sock" \
  app-image bundle update

# ホストが macOS の場合
# ssh-agent のソケットファイルは固定パス `/run/host-services/ssh-auth.sock` に存在する
docker run --rm -it \
  -e "SSH_AUTH_SOCK=/tmp/ssh-auth.sock" \
  -v "/run/host-services/ssh-auth.sock:/tmp/ssh-auth.sock" \
  app-image bundle update

これは docker compose 環境からでも利用できます。

version: '3.8'
services:
  app:
    build: .
    environment:
      SSH_AUTH_SOCK: /tmp/ssh-auth.sock
    volumes:
      - "${SSH_AUTH_SOCK:-/dev/null}:/tmp/ssh-auth.sock"
# ホストが Linux の場合
# シェルセッションに設定された `$SSH_AUTH_SOCK` に任せる
docker compose run --rm app bundle update

# ホストが macOS の場合
# `docker compose up` `docker compose run` に合わせて `SSH_AUTH_SOCK=/run/host-services/ssh-auth.sock` を指定すればよい
SSH_AUTH_SOCK=/run/host-services/ssh-auth.sock docker compose run --rm app bundle update

終わりに

今回は Docker image のビルド中に、または実行中の Docker container 内から、非公開 Git リポジトリに対して SSH 接続を行う方法を紹介しました。

ssh-agent は少しクセのあるアプリケーションでもありますが、「Docker コンテナにホストの秘密鍵をマウントする」などセキュリティ的に今一つな方法を採らなくても、コンテナ内から SSH 接続ができるようになる点は非常に便利だと思います。

この記事が、ここまで読んで頂いた皆様のお役に立ちますと幸いです。