Herokuでプライベートリポジトリのgemをビルドする
こんにちは。株式会社ペライチ CTO の瀬川です。
会社のサービスが増えてくると共通処理をパッケージにまとめたくなってきますよね。
ペライチでは最近 Rails のサービスが10個を超えてきており、共通処理をまとめた gem をプライベートリポジトリに作ることにしました。
プライベートな gem の CI/CD、特に Heroku での取り扱いに少し苦労したのでまとめておこうと思います。
GitHub リポジトリを gem として指定する
Gemfileに github:
オプションをつけることで github リポジトリを gem として直接指定することができます。
# Gemfile
gem "the_private_gem", github: "hogeorg/the_private_gem", tag: "v1.0.0"
gem にプライベートリポジトリを指定した場合、ローカルでは以下のように bundle install 時に GitHub へのログインを求められます。
$ bundle install
Bundler 2.4.18 is running, but your lockfile was generated with 2.3.26. Installing Bundler 2.3.26 and restarting using that version.
Fetching gem metadata from https://rubygems.org/.
Fetching bundler 2.3.26
Installing bundler 2.3.26
Fetching gem metadata from https://rubygems.org/.........
Fetching https://github.com/PagerTree/attr_encrypted.git
NOTE: Gem::Specification#has_rdoc= is deprecated with no replacement. It will be removed in Rubygems 4
Gem::Specification#has_rdoc= called from /Users/segawa/.rbenv/versions/3.1.1/lib/ruby/gems/3.1.0/bundler/gems/attr_encrypted-eafd77a11f69/attr_encrypted.gemspec:22.
Fetching https://github.com/hogeorg/the_private_gem.git
Username for 'https://github.com': tektoh
Password for 'https://tektoh@github.com':
ここでパスワードには Personal Access Token (PAT) を指定する必要があります。 PAT は Developer Settings から作成します。
CI 上でのビルド
Personal access tokens are intended to access GitHub resources on behalf of yourself. To access resources on behalf of an organization, or for long-lived integrations, you should use a GitHub App.
PAT のドキュメントでは上記のように、組織としてリソースにアクセスする場合は GitHub App を使うことが推奨されています。
オーガニゼーションで GitHub App を作成しておくと離職者があってもアクセストークンが無効にならないのでトークンが管理しやすくなります。
オーガニゼーションの GitHub App は、オーガニゼーションの Developer settings > GitHub Apps から作成できます。GitHub Appsの細かい設定は今回は割愛しますが、パーミッションは Contents
のリード権限が必要です。
GitHub App のアクセストークンを発行する
GitHub App のアクセストークンは以下の API で発行できます。
POST /app/installations/{installation_id}/access_tokens
つまり installation_id
が必要です。
installation_id
取得は以下の API です。
GET /repos/{owner}/{repo}/installation
上記2つの API は JWT で認証をします。
JWT は iss に GitHub App の App ID を設定し、 RS256 アルゴリズムで GitHub App の Private key を使って署名する必要があります。
まとめると、以下の手順でようやくアクセストークンが手に入ります。
- GitHub App を作成
- GitHub App の Private key を発行
- GitHub App の App ID と Private key を使って JWT を作成
- API でリポジトリを指定して installation_id を取得
- API でアクセストークンを発行
CI の bundle install にアクセストークンを渡す
ローカル環境で bundle install
した時は PAT をプロンプトで入力しましたが、CIでは以下のように環境変数でアクセストークンを渡す必要があります。
BUNDLE_GITHUB__COM=x-access-token:${GITHUB_ACCESS_TOKEN}
Heroku でのビルド
Heroku では buildpack-ruby を使うことでビルドフェーズで bundle install が実行されます。
プライベートリポジトリの gem をインストールする場合、 buildpack-ruby より先に GitHub App のアクセストークンを取得して環境変数に設定するビルドパックが必要になってきます。
ビルドパックを作る
ビルドバックは以下の3つのスクリプトで構成されます。
- bin/detect: この buildpack がアプリに適用されるかどうかを判定します。
- bin/compile: アプリで変換手順を実行するために使用されます。
- bin/release: メタデータをランタイムに戻します。
bin/detect
はとりあえず exit 0
を実行すれば良く、 bin/release
は作成しなくても問題ありません。
bin/compile
にアクセストークンを発行するスクリプトを書いていきます。
#!/usr/bin/env bash
# bin/compile
set -e
# Utility functions
indent() { sed 's/^/ /'; }
arrow() { sed 's/^/-----> /'; }
indent-err() { sed "s/^/$(printf '\033')[31m /;s/$/$(printf '\033')[0m/"; }
arrow-err() { sed "s/^/$(printf '\033')[31m-----> /;s/$/$(printf '\033')[0m/"; }
# Base64 URL エンコードする関数
function base64url_encode {
declare input=$(cat -)
echo -n "${input}" | base64 | url_safe
}
# Base64 エンコードをURLセーフにする関数
function url_safe {
declare input=$(cat -)
echo -n "${input}" | tr -d '=\r\n' | tr '+' '-' | tr '/' '_'
}
BUILD_DIR=${1:-}
CACHE_DIR=${2:-}
ENV_DIR=${3:-}
BIN_DIR=$(cd $(dirname $0); pwd)
BP_DIR=$(cd $BIN_DIR; cd ..; pwd)
cd "$BUILD_DIR"
# ビルドフェーズでは Config Vars は環境変数には展開されておらず、 ENV_DIR 内にファイルで配置されている。
GITHUB_USER=$(cat $ENV_DIR/GITHUB_USER)
GITHUB_ORG=$(cat $ENV_DIR/GITHUB_ORG)
GITHUB_REPO=$(cat $ENV_DIR/GITHUB_REPO)
GITHUB_APP_ID=$(cat $ENV_DIR/GITHUB_APP_ID)
# Private key は改行を含んでいるため、 一度 Base64 エンコードして Config Vars に入れ、デコードして利用する
GITHUB_APP_PRIVATE_KEY_BASE64=$(cat $ENV_DIR/GITHUB_APP_PRIVATE_KEY_BASE64)
GITHUB_APP_PRIVATE_KEY=$(echo $GITHUB_APP_PRIVATE_KEY_BASE64 | base64 -d)
# GitHub App の App ID と Private key を使って JWT を作成
JWT_IAT=$(date +%s)
JWT_EXP=$(($JWT_IAT + 60))
JWT_ISS=$GITHUB_APP_ID
JWT_HEADER=$(echo -n '{"alg":"RS256","type":"JWT"}' | base64url_encode)
JWT_PAYLOAD=$(echo -n "{\"iat\":$JWT_IAT,\"exp\":$JWT_EXP,\"iss\":\"$JWT_ISS\"}" | base64url_encode)
# openssl dgst のバイナリをそのままパイプで渡そうとすると null 文字が削られてしまうため、テンポラリファイルに書き出してからファイルに Base64 エンコードをかけている
echo -n "$JWT_HEADER.$JWT_PAYLOAD" | openssl dgst -sha256 -sign <(echo "$GITHUB_APP_PRIVATE_KEY") -binary > /tmp/jwt_signature
JWT_SIGNATURE=$(base64 -i /tmp/jwt_signature | url_safe)
JWT=$JWT_HEADER.$JWT_PAYLOAD.$JWT_SIGNATURE
# リポジトリを指定して installation_id を取得
echo "Getting installation id for $GITHUB_ORG/$GITHUB_REPO" | arrow
INSTALLATION=$(curl -sS -L \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $JWT" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"https://api.github.com/repos/$GITHUB_ORG/$GITHUB_REPO/installation")
INSTALLATION_ID=$(echo $INSTALLATION | jq -r '.id')
if [ $INSTALLATION_ID = "null" ]; then
echo "Could not find installation id for $GITHUB_ORG/$GITHUB_REPO" | arrow-err
exit 1
fi
# アクセストークンを発行
echo "Getting access token for $GITHUB_ORG/$GITHUB_REPO" | arrow
ACCESS_TOKEN=$(curl -sS -L \
-X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $JWT" \
-H "X-GitHub-Api-Version: 2022-11-28" \
https://api.github.com/app/installations/$INSTALLATION_ID/access_tokens \
-d '{}')
TOKEN=$(echo $ACCESS_TOKEN | jq -r '.token')
if [ $TOKEN = "null" ]; then
echo "Could not create access token for $GITHUB_ORG/$GITHUB_REPO" | arrow-err
exit 1
fi
# BP_DIR/export に環境変数を書き込んでおくと、次に実行されるビルドパックに環境変数を渡すことができる
echo "export GITHUB_ACCESS_TOKEN=$TOKEN" > $BP_DIR/export
echo "export BUNDLE_GITHUB__COM=x-access-token:${TOKEN}" >> $BP_DIR/export
ビルドパックを設定する
ビルドパックのスクリプトをパブリックリポジトリに push し、 heroku/ruby
より先に実行されるようにセットします。
$ heroku buildpacks -a hogeapp
=== hogeapp Buildpack URLs
1. https://github.com/hogeorg/buildpack-gh-access-token
2. heroku/ruby
Config Vars を設定する
上記のビルドパックで利用する環境変数を Config Vars に設定します。
- GITHUB_ORG: ビルドするリポジトリのオーガニゼーション名
- GITHUB_REPO: ビルドするリポジトリのリポジトリ名
- GITHUB_APP_ID: GitHub App の App ID
- GITHUB_APP_PRIVATE_KEY_BASE64: GitHub App の Private key を Base64 エンコードしたもの。
Private key は base64
コマンドでエンコードします。
$ base64 -i hogeapp.2023-08-01.private-key.pem
デプロイ
以上の設定でデプロイすると、プライベートリポジトリの gem を Heroku でビルドできるようになりました。
Build log
-----> Building on the Heroku-20 stack
-----> Deleting 4 files matching .slugignore patterns.
-----> Using buildpacks:
1. https://github.com/hogeorg/buildpack-gh-access-token
2. heroku/ruby
-----> GitHub Access Token app detected
-----> Getting installation id for hogeorg/the_private_gem
-----> Getting access token for hogeorg/the_private_gem
-----> Ruby app detected
-----> Installing bundler 2.3.25
-----> Removing BUNDLED WITH version in the Gemfile.lock
-----> Compiling Ruby/Rails
-----> Using Ruby version: ruby-3.1.1
-----> Installing dependencies using bundler 2.3.25
Running: BUNDLE_WITHOUT='development:test' BUNDLE_PATH=vendor/bundle BUNDLE_BIN=vendor/bundle/bin BUNDLE_DEPLOYMENT=1 bundle install -j4
Fetching gem metadata from https://rubygems.org/.........
Fetching https://github.com/hogeorg/the_private_gem.git
:
Using the_private_gem 1.0.0 from https://github.com/hogeorg/the_private_gem.git (at v1.0.0@1111111)
:
Bundle complete! 41 Gemfile dependencies, 99 gems now installed.
Gems in the groups 'development' and 'test' were not installed.
Bundled gems are installed into `./vendor/bundle`
Bundle completed (2.85s)
Well done!
採用情報
現在エンジニア募集しています!
▼ 採用ページ
▼ 選考をご希望の方はこちら(募集職種一覧)
▼ まずはカジュアル面談をご希望の方はこちら
募集中の職種についてご興味がある方は、お気軽にお申し込みください(CTO がお会いします)
Discussion