カラーミーショップ 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にリリースしたパッケージを確認できます。
レジストリからの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を使ったライブラリ管理の役に立てば幸いです。
-
rubygems/dsl.rb at 96e5cff3df491c4d943186804c6d03b57fcb459b · rubygems/rubygems ↩
-
gemspecの
allowed_push_host
が適切に設定されていればbundle exec rake release
でもリリースできます cf. rubygems/gem_helper.rb at cc8f9b576f2b2a5f62fec067b2d8a8f8fb7197db · rubygems/rubygems ↩