こんにちは。
メディアサービス開発部 Webアプリケーション開発課のフサギコ(髙﨑)です。
Ruby on Railsによるバックエンドの実装運用と、AWSによるサービスインフラの設計構築を中心とした、いわゆるテックリードのような立ち位置で働いています。
2月11日から12日にかけて、アイドルマスターのM@STERS OF IDOL WORLD!!!!! 2023で東京ドームにいたのですが、その余韻が未だに抜けきりません。毎年となるとさすがに大変そうですが、2~3年に1度くらいの頻度ではぜひ、M@STERS OF IDOL WORLD、開催してほしいですね。次は2025年でしょうか。
さて、本記事では社内向けのgemをGitHub Packagesのプライベートgemとしてpublishし、ローカルでの開発時やCI/CD時にインストールする方法についてお話します。
共通なコードをgem化したい
皆さんの会社には、複数のプロダクトから利用されているコードはあるでしょうか。
社内の別部署が運用している基盤サービスや、SDKが用意されていない外部サービスのAPIへリクエストするクライアントライブラリなどのことです。
同じプロダクトに属する複数のサービス1であればモノレポ2でのコード管理としてファイルパスでの相対参照で利用する形にしてしまうという手もありますが、異なるユーザに対して異なる価値を提供する別プロダクトではそうも行きません3。
そういったコードは、社内向けのプライベートgem化してしまうとよいかもしれません。
もしコード管理にGitHubを使っているなら、社内向けのプライベートgemの提供にはGitHub Packagesが使えます。
gemを作る
GitHub Packagesへプライベートgemをpuhlishする方法を説明する前に、gemそのものを作りましょう。
gemは下記のようにbundlerのgemサブコマンドを使えば、テンプレートから生成できます。
$ bundle gem test_private_gem
途中いくつか聞かれる質問でGitHubを選べば、GtiHub ActionsでCIするためのワークフローファイルまで生成してくれるので非常に便利です。
さて、それらの質問に答え終わると、下記のようなgemspecファイルが生成されていると思います。
# frozen_string_literal: true require_relative "lib/test_private_gem/version" Gem::Specification.new do |spec| spec.name = "test_private_gem" spec.version = TestPrivateGem::VERSION spec.authors = ["fusagiko/takayamaki"] spec.email = ["takayamaki@users.noreply.github.com"] spec.summary = "TODO: Write a short summary, because RubyGems requires one." spec.description = "TODO: Write a longer description or delete this line." spec.homepage = "TODO: Put your gem's website or public repo URL here." spec.license = "MIT" spec.required_ruby_version = ">= 2.6.0" spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'" spec.metadata["homepage_uri"] = spec.homepage spec.metadata["source_code_uri"] = "TODO: Put your gem's public repo URL here." spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here." # 以降略
あくまで社内向けのgemなのでsummaryとdescriptionさえ適切に書いておけば、homepageやmetadataのhomepage_uri、 source_code_uri、 changelog_uriはそこまでこだわらずリポジトリのURLにしてもよいのではないでしょうか。
重要なのはmetadataのallowed_push_hostで、GitHub Packagesからgemを提供するにはこれを https://rubygems.pkg.github.com/{{org_name}}
にしておく必要があります。
{{org_name}}
はそのgemのリポジトリが属するGitHub Organizationの名前に置換してください。
gemspecが設定できたら、gemそのものを実装しましょう。
依存するgemをGemfileではなくgemspecファイルにspec.add_dependency
を使って書くことを除けばごく普通にRubyを書くだけなので本記事では割愛します。
gemをpublishする
gemを実装できたら、GitHub Packagesへgemをpublishしましょう。
GitHub Packagesへgemをpublishするには、GitHubのマーケットプレイスで公開されているjstastny/publish-gem-to-githubアクションが便利です。
このアクションを使えば下記のように、ごく簡単なワークフローでGitHub Packagesへgemをpublishできます。
name: Publish gem on: push: tags: - 'v*' permissions: contents: read packages: write jobs: publish_gem: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: jstastny/publish-gem-to-github@v2 with: token: ${{ secrets.GITHUB_TOKEN }} owner: ${{ github.repository_owner }}
gemをbundlerでインストールする
gemをpublishできたら、利用側のGemfileに記述しましょう。 下記のように記述すれば、GitHub Packagesで提供されているgemをbundlerを使ってインストールできます。
source 'https://rubygems.pkg.github.com/{{org_name}}' do gem 'test_private_gem' end
しかし、そのままbundle installを実行すると、Authentication is required for rubygems.pkg.github.com.
といったようなエラーで失敗してしまいます。
これはbundlerにGitHub Packagesの認証トークンを設定していないためです。
GitHub Packagesでは、GitHubのPersonal Access Token(以降PATと略します)を認証トークンとして使います4。
PATはSettings
→Developer settings
→Personal access tokens
→Tokens (classic)
から発行できます。権限はread:packagesのみで十分です。
発行したPATは下記のコマンドでbundlerに設定できます。
bundle config –global rubygems.pkg.github.com {{PAT}}
CI/CDではBUNDLE_RUBYGEMS__PKG__GITHUB__COM
環境変数にPATを格納すればbundle configで設定した際と同様にインストールできるようになります。
また、本番環境へのデプロイにDockerを使っている場合、PATをDockerイメージのレイヤ情報に残さずにbuildをするには
RUN --mount=type=secret
を使う- docker build前にホストマシン側でbundle installを実行し、docker buildではそれらgemのCOPYだけ行う
- マルチステージビルドなDockerfileとして、gemをインストールするステージにPATをARGで渡し、最終的なイメージを作るステージではgemをインストールするステージでインストールしたgemのCOPYだけを行う
- この方法を採る場合、gemをインストールするステージをdocker buildのtargetとして指定してdockerリポジトリにpushしてしまわないように気を付ける必要がある
などの方法があります。
ここまで整えればあとはもう、通常のgemと同様にプロダクトのコードで利用できるようになります。
まとめ
本記事では、社内向けのgemをGitHub Packagesのプライベートgemとしてpublishし、ローカルでの開発時やCI/CD時にインストールする方法についてお話ししました。
GitHub ActionsとGitHub Packagesを使うことで外部には公開したくない社内向けのgemを簡単に配布できるようになり非常に便利になりました。
しかし、個人に属するPATをダウンロード時の認証トークンとして用いるため、マシンユーザを用いなければビルドプロセスに属人性が混入してしまったり、また対象のOrgを制限できないために必要のないgemまでダウンロードできてしまう可能性がある点については、改善の余地があるとも思います。
それについてはGitHub社の今後の改修に期待したいところです。
互いに独立した複数のプロダクトから利用されるようなコードを提供する必要が生じた際には参考にしていただければ幸いです。 私が担当しているプロダクトのなかにも実際にGitHub Packagesで社内向けgemをpublish、利用しているものがあります。
ブックウォーカーでは物理・電子・Web連載問わず漫画や本が好き、あるいはRubyが好きなWebアプリケーションエンジニアを募集しています。
興味がありましたらぜひ、下記採用情報ページからご応募ください。 中途採用のページではカジュアル面談のご応募もお待ちしています。
- エンドユーザー向けのWebサイトを提供するWebアプリケーションサーバや、社内向け管理画面、スマートフォンアプリ向けAPIサーバなど↩
- 複数のサービスを一つのリポジトリでコード管理すること 参考:Monorepo開発のメリット vs デメリット | CircleCI↩
- googleのように全く異なるプロダクトすらも全てひっくるめて一つのリポジトリでコード管理している例もありますが…それはさすがに極端な例でしょう↩
- PATを使う、という点でお気付きのかたもいるかもしれませんが、その人が退職や異動でorgから抜けるとそのgemはダウンロードできなくなりますし、逆に言えばその人が属しているOrgのprivate gemは全てダウンロードできてしまいます。 最近、アクセスできるOrgの範囲を限定したFine-grained PATというものがbetaで提供され始めましたが、2023/03/08現在、GitHub PackagesはFine-grained PATには対応していないようです↩