GitHub RubyGems

GitHub Enterprise ServerのGitHub Packagesで社内用gemをホストする

GitHub RubyGems

カラーミーショップ DXチームのkymmtです。この記事では、GitHub Enterprise Server 3.0以降で利用可能になったGitHub Packagesで社内用のRubyのライブラリ(gem)をホストした事例について紹介します。

概要

GitHub Enterprise Server(以降GHESと呼びます)3.0からGitHub Packagesが利用可能になりました。

GitHub Enterprise Server 3.0を正式リリース - GitHubブログ(太字は筆者による)

また、GitHub.comで最も人気のCIツールであるGitHub Actions、GitHub Packagesやソースコードの強力なシークレットスキャンツールを、GitHub Enterprise Serverで使用できるようになりました

GitHub Packagesとは、GitHubでRubyのgemやnpmのpackageのような言語ライブラリやDockerイメージをホストできる機能です。つまり、Rubyであれば、一般に使われるrubygems.orgではなく、GitHubにgemをホストし、そこからgemを取得できるようになります。

rubygems.orgやwww.npmjs.comのようなライブラリをホストするサービスをレジストリと呼びます。この記事では、RubyのライブラリであるgemをホストするサービスのことをRubyGemsレジストリと呼びます。

ペパボでは、主に自社サービスの内部APIのクライアントや決済サービスのクライアントとして、社内向けのRubyのライブラリを数多く作成しています。これらのgemを認証が必要なGHESのGitHub Packagesでホストすることで、プライベートなRubyGemsレジストリを実現していっています。この方式のメリットとして、

  • 社内のgemリポジトリのコードを本当のgemとして扱える
  • レジストリがGHESと統合されているので運用面の負荷を下げられる

という点があります。

GitHub Packagesを使う場合のGemfile

ghes.example.comにGHESをホストしているとします。このとき、GHESのsubdomain isolationが有効化されているなら、GitHub PackagesのRubyGemsレジストリのドメインはrubygems.ghes.example.comになります。次のようにGemfileを書くと、このレジストリからgemを取得できます。

# GHESのRubyGemsレジストリのURLを指定する。ホストしている組織やリポジトリに応じて、オーナー名を指定する
source 'https://rubygems.ghes.example.com/awesome' do
  gem 'internal_gem', '~> 1.2.3'
end

source 'https://rubygems.ghes.example.com/great' do
  gem 'another_internal_gem', group: :development
end

# ここから下はrubygems.orgから取得するgem
source 'https://rubygems.org'

gem 'rails'

# ...

このように、普通のGemfileとほぼ同じ形式で扱えます。

sourceの引数はレジストリにおけるgemのオーナーのURLです。ここで、オーナーとは組織(organization)かユーザーのどちらかです。ペパボでは、ほとんどの場合で事業部ごとにGitHubの組織を分離しているので、該当のgemをホストしているリポジトリが属する組織の名前をオーナーとして指定することが多いです。

なお、Gemfileに複数のsourceを宣言する場合は、1つのsourceを除き、sourceに渡したブロックの中でgemを宣言する方法が推奨されています。なぜなら、トップレベルに複数のsourceを宣言すると、その順番によっては意図しないgemをダウンロードする可能性があり、セキュリティリスクとなりえるからです。トップレベルにブロックを渡さないsourceを複数宣言すると、Bundler 2では非推奨メッセージが表示され、Bundler 3ではエラーになります1

GitHub Packagesによる社内RubyGemsレジストリの実現

どのように社内RubyGemsレジストリにgemをリリースして、各アプリケーションからダウンロードしているかについて詳しく説明します。

この記事では、各ソフトウェアのバージョンは次のものとします。

  • GHES: 3.1.x
  • RubyGems(gem): 3.2.x
  • Bundler: 2.2.x

レジストリへのgemのリリース

RubyGems(gemコマンド)の設定

GitHub PackagesのRubyGemsレジストリへgemをリリースするとき、認証情報としてパーソナルアクセストークン(PAT)を使います。このPATは該当のgemのリポジトリにアクセスできるユーザーのものを使います。また、このPATはwrite:packageスコープを持つ必要があります。詳しくは"Creating a personal access token - GitHub Docs"を参照してください。

gemコマンドは~/.gem/credentialsに認証情報を設定できます。GHESのPATをそのファイルに書き込みます。

$ cat ~/.gem/credentials
---
:ghes: Bearer <PAT>

gemspecの設定

RubyGemsレジストリにgemをリリースするには、gem pushコマンドを使います。レジストリにgemをpushするには、gemspecでallowed_push_hostを設定する必要があります。今回はRubyGems.orgではなくGHESのレジストリにpushするので、そのレジストリのURLを指定します。

# gemspecの設定例
Gem::Specification.new do |spec|
  # ...

  spec.metadata["allowed_push_host"] = "https://rubygems.ghes.example.com"

  # ...
end

この設定によってhttps://rubygems.ghes.example.comにgemをpushできるようになります。また、他のレジストリにgemをpushしようとするとエラーが発生します。この挙動の便利な点は、誤ってプライベートなgemをパブリックなレジストリにリリースする事故を防げるというところです。

GitHub Actionsを利用したリリース

今回はGitHub Actionsを使ってgemをリリースできるようにしました。

まず、gemをRubyGemsレジストリにリリースするためのrelease-gemという社内用actionを作成しました。actionの作成方式として、シェルスクリプトだけの単純なものとしたかったので、Dockerコンテナを使ったactionを採用しました2。actionでは、入力としてGitHubの組織名(ORGANIZATION)とPAT(PERSONAL_ACCESS_TOKEN)を受け取り、シェルスクリプトで愚直に認証情報の準備をしてから、gemコマンドでgemをpushしています3

#!/bin/sh -l

# Dockerコンテナを使ったactionにおけるentrypoint.shの擬似コード
# rubylang-ruby(https://hub.docker.com/r/rubylang/ruby)のDockerイメージを使って実行しています

# gemコマンドの認証情報を準備
mkdir ~/.gem
echo ":ghes: Bearer ${INPUT_PERSONAL_ACCESS_TOKEN}" > ~/.gem/credentials
chmod 600 ~/.gem/credentials

# gemをpkgディレクトリ配下にビルド
bundle install --jobs 4 --retry 3 --quiet
bundle exec rake build

# pkgディレクトリに置かれたgemをGHESのRubyGemsレジストリへpush
PACKAGE=$(find pkg -type f | sort | tail -n 1)
gem push --key ghes --host "https://rubygems.ghes.example.com/${INPUT_ORGANIZATION}" ${PACKAGE}

そして、"v*"にマッチするGitのタグをリポジトリにpushすると上述したrelease-gem actionを実行するワークフローを社内のgemのリポジトリに導入しています。

# 擬似的なワークフロー
name: Release my gem

on:
  push:
    tags:
      - v*

jobs:
  release:
    runs-on: self-hosted
    container: ubuntu:latest
    steps:
      - uses: actions/checkout@v2
        with:
          ref: ${{ github.ref }}
      - uses: foobar/release-gem@v1 # 組織名とPATを渡すとgemをリリースするaction
        with:
          organization: 'awesome'
          personal_access_token: ${{ secrets.GITHUB_TOKEN }}

これで、新しいバージョンタグを打つたびに、自動でGHESのRubyGemsレジストリにgemがリリースできるようになりました。

https://<ドメイン>/<オーナー名>/<リポジトリ名>/packagesにアクセスすることで、GitHub Packagesにリリースしたパッケージを確認できます。

GitHub Packagesにリリースしたパッケージの例

レジストリからのgemのダウンロード

Bundlerはレジストリからgemをダウンロードするときに使う認証情報を設定する機能を持っています4

開発環境やCI環境では、次のようなコマンドを実行することで、GHESのRubyGemsレジストリの認証情報を設定できます。

$ bundle config set https://rubygems.ghes.example.com <GHESのユーザーID>:<PAT>

この設定のもとで、さきほど紹介したようなGemfileを書くと、GHESのRubyGemsレジストリからgemをダウンロードできます。

# bundle install時のイメージ。GitHub PackagesのRubyGemsレジストリからもメタデータを取得している
$ bundle install
Fetching gem metadata from https://rubygems.ghes.example.com/awesome/...
Fetching gem metadata from https://rubygems.ghes.example.com/great/...
Fetching gem metadata from https://rubygems.org/........
Installing rake 13.0.6
...

本番環境などでBundlerへ認証情報を設定するために環境変数を使うこともできます。たとえばrubygems.ghes.example.comからのダウンロードであれば、BUNDLE_RUBYGEMS__GHES__EXAMPLE__COMのようにドメインのドットがアンダースコア2つに置き換わるような命名規則の環境変数を使います。この環境変数を次のように設定します。

$ export BUNDLE_RUBYGEMS__GHES__EXAMPLE__COM=<GHESのユーザーID>:<PAT>

GitHubのドキュメントには、環境変数の名前はオーナー名まで指定するようにという記述があります5。たとえば、BUNDLE_RUBYGEMS__GHES__EXAMPLE__COM/AWESOMEのような形式です。しかし、実際はドメイン名だけの指定でも動くようです。Herokuなど一部のプラットフォームではスラッシュの入った環境変数が使えないという事情もあり、現在は上述したようなドメイン名だけを含む環境変数を使っています。

他には、Dockerイメージのビルドでbundle installするときは、イメージの中にPATが残らないように気をつける必要があります。マルチステージビルドを利用して、最初のステージのビルド中にPATを利用してbundle installし、次のステージでインストールしたgemだけをコピーすることで、最終的なイメージにPATを残さないようにできます。

FROM rubylang/ruby:2.7-bionic

ARG PERSONAL_ACCESS_TOKEN

# ...

COPY Gemfile Gemfile.lock /usr/src/app/

# 入力として受け取ったPATを使ってレジストリの認証を通過しgemをダウンロード
RUN bundle config rubygems.ghes.example.com <GHESのユーザーID>:${PERSONAL_ACCESS_TOKEN} \
  && bundle install --without=production -j8

FROM rubylang/ruby:2.7-bionic

# ...

# 前段のイメージからgemだけをコピー
COPY Gemfile Gemfile.lock /usr/src/app/
COPY --from=0 /usr/local/bundle /usr/local/bundle

まとめ

この記事では、GitHub Enterprise Server 3.0以降で利用可能なGitHub Packagesの活用方法について紹介しました。GitHub Packagesを社内向けのRubyGemsレジストリとして使うメリットとして、社内のgemリポジトリのコードを本当のgemとして扱えたり、レジストリがGHESと統合されているので運用面の負荷を下げられるという点があります。また、GitHub Actionでリリースを半自動化することで、気軽にgemをリリースできるようになります。これには、社内向けのgemのメンテナンスを楽にする効果もありそうだと考えています。

2021年9月現在、GitHub Packagesは言語ライブラリのレジストリとしてRubyGemsの他にnpm、Maven、Gradle、NuGetもサポートしています。この記事がGitHub Packagesを使ったライブラリ管理の役に立てば幸いです。