↓ メインコンテンツへスキップ

仕事用と個人用のGitHub、Cloudflareを、ディレクトリだけで切り替える

·3658 文字·8 分
mota
著者
mota
AI開発、個人開発、ベトナムでの仕事と暮らしについて書いています。

一台のMacで、会社のGitHubアカウントと個人のGitHubアカウントを使っています。 Cloudflareも同じで、仕事のアカウントとこのブログを置いている個人のアカウントがあります。

最初は、必要になるたびに手で切り替えれば足りると思っていました。 ところが、個人のリポジトリを git clone しようとして、権限がないと言われることが何度かありました。 会社の鍵で個人のリポジトリへ接続しようとしていたのです。 コミットの作者が会社のメールアドレスになっていたこともあります。

切り替えを覚えておく運用は、どこかで漏れます。 そこで今は、作業するディレクトリを見れば、どのアカウントを使うかが決まるようにしています。

ディレクトリをアカウントの境界にする
#

まず、リポジトリを置く場所を分けました。

~/WORK_DEV/   会社のリポジトリ
~/MY_DEV/     個人のリポジトリ

あとはすべての道具に「このディレクトリの下ではこのアカウント」と教えます。 ただ、道具ごとに切り替えの仕組みが違うので、一つの方法では揃いません。 私の環境では、次の四つを組み合わせています。

対象切り替える仕組み
SSH鍵~/.ssh/config のホストエイリアス
gitの作者情報と接続先~/.gitconfig の includeIf と insteadOf
ghやgcloudなどのCLIdirenvで環境変数を切り替える
Cloudflare(cf CLI)cf CLIのプロファイルをディレクトリに結びつける

SSH鍵は、ホスト名で分岐させる
#

GitHubは、SSHの鍵でアカウントを見分けます。 会社と個人で別の鍵を作り、~/.ssh/config で接続先ごとに使う鍵を決めます。

# 会社アカウント(既定)
Host github.com
  HostName github.com
  User git
  IdentityFile ~/.ssh/work_github
  IdentitiesOnly yes

# 個人アカウント
Host github.com.private
  HostName ssh.github.com
  Port 443
  User git
  IdentityFile ~/.ssh/private_github
  IdentitiesOnly yes

Host に書いた github.com.private は、実在するドメインではありません。 SSHの中だけで通じる別名で、実際の接続先は HostName に書いたGitHubのサーバーです。 個人用は、GitHubが用意している443番ポートの入口へ向けています。ポート22がふさがれたネットワークでも接続できます。

IdentitiesOnly yes も外せません。 これがないと、SSHはエージェントに登録された鍵を順番に試します。 先に会社の鍵が通ると、GitHubはそれを会社アカウントとして受け付けてしまいます。

設定できたかは、別名ごとに接続して確かめます。

ssh -T git@github.com          # Hi <会社アカウント>!
ssh -T git@github.com.private  # Hi <個人アカウント>!

gitは、ディレクトリで設定を読み分ける
#

SSHの別名を作っただけでは、まだ足りませんでした。 GitHubの画面からコピーしたURLは git@github.com:... なので、そのまま使うと会社の鍵で接続します。 gh repo clone も同じURLを使います。 毎回URLを書き換えるのは、冒頭の失敗を繰り返すだけです。

ここで使うのが、gitの includeIf です。 リポジトリの場所に応じて、追加の設定ファイルを読み込めます。

# ~/.gitconfig
[user]
	name = 会社での名前
	email = 会社のメールアドレス

[includeIf "gitdir:~/MY_DEV/**"]
	path = ~/.gitconfig-private
# ~/.gitconfig-private(~/MY_DEV の下でだけ読まれる)
[user]
	name = 個人での名前
	email = 個人のメールアドレス

[url "git@github.com.private:"]
	insteadOf = git@github.com:
	insteadOf = https://github.com/

insteadOf は、URLの先頭が一致したときに書き換える設定です。 ~/MY_DEV の下では、git@github.com: で始まるURLが自動で git@github.com.private: に置き換わり、個人の鍵で接続します。 コピーしたURLも gh repo clone も、そのまま使えるようになりました。

既定を会社側にしているのは、会社のリポジトリの数が多く、置き場所も深いからです。 例外になる個人側だけを includeIf で上書きしています。

確認は、それぞれのディレクトリで次のように打ちます。

git config user.email                  # このリポジトリで使うメールアドレス
git ls-remote git@github.com:<個人アカウント>/<リポジトリ>.git

gitとSSHを、direnvに載せなかった理由
#

direnv は、ディレクトリに入ったときに .envrc を読み込み、出たときに元へ戻すツールです。 環境変数でアカウントを切り替えられるなら、gitもSSHもdirenvにまとめたくなります。 GIT_SSH_COMMAND や GIT_AUTHOR_EMAIL を .envrc に書けば、同じことはできます。

私も最初はそうするつもりでした。 ただ、direnvが働くのは、シェルがそのディレクトリに入ったときだけです。 エディタに組み込まれたgitや、GUIのgitクライアントからコミットすると、.envrc は読まれません。 その経路では、作者情報が会社のメールアドレスに戻ります。

~/.gitconfig と ~/.ssh/config は、どこからgitを呼んでも読まれます。 そのため、gitとSSHは設定ファイル側で分け、direnvは環境変数でしか切り替えられないCLIに限っています。

CLIは、direnvで環境変数を切り替える
#

direnvはHomebrewで入れ、シェルにフックを足します。

brew install direnv
echo 'eval "$(direnv hook zsh)"' >> ~/.zshrc

.envrc は、リポジトリごとではなく、境界のディレクトリに一つだけ置いています。 direnvは親ディレクトリをさかのぼって .envrc を探すので、~/MY_DEV/.envrc は ~/MY_DEV の下のすべてのリポジトリに効きます。

# ~/MY_DEV/.envrc
export GH_CONFIG_DIR="$HOME/.config/gh-private"
export CLOUDSDK_ACTIVE_CONFIG_NAME="personal"
use_config_home "$HOME/.config-my"

.envrc を新しく置いたり書き換えたりしたら、direnv allow で許可します。 知らないリポジトリの .envrc が勝手に実行されないための確認です。

ツールごとに、切り替えに使える環境変数が違います。

  • gh:GH_CONFIG_DIR で設定ディレクトリごと分けます。個人用のディレクトリで gh auth login を一度実行しておきます。
  • gcloud:認証情報は一か所にまとめたまま、CLOUDSDK_ACTIVE_CONFIG_NAME で使う構成だけを切り替えます。
  • wrangler:専用の環境変数がありませんでした。

環境変数がないツールをどう分けたか
#

wranglerは、ログイン情報を $XDG_CONFIG_HOME/.wrangler/ に保存します。 切り替えるには XDG_CONFIG_HOME そのものを変えるしかありません。

ところが、XDG_CONFIG_HOME を見ているのはwranglerだけではありません。 gitやghも、この変数を手がかりに設定を探します。 単純に別のディレクトリへ向けると、そうした設定まで見えなくなります。

そこで、direnvの共通ライブラリ(~/.direnvrc)に関数を一つ書きました。 切り替え先のディレクトリに ~/.config の中身をすべてシンボリックリンクで並べ、.wrangler だけを実体として残します。

# ~/.direnvrc
use_config_home() {
	local target="$1"
	local shared="$HOME/.config"
	local entry name

	mkdir -p "$target"

	# ~/.config 側で消えたもののリンク切れを掃除する
	for entry in "$target"/*; do
		[ -L "$entry" ] && [ ! -e "$entry" ] && rm "$entry"
	done

	# .wrangler 以外は ~/.config を共有する
	for entry in "$shared"/*; do
		[ -e "$entry" ] || continue
		name="$(basename "$entry")"
		[ "$name" = ".wrangler" ] && continue
		ln -sfn "$shared/$name" "$target/$name"
	done

	export XDG_CONFIG_HOME="$target"
}

ディレクトリに入るたびにリンクを張り直すので、~/.config にツールが増えても追従します。 この関数を ~/.config/direnv/ ではなく ~/.direnvrc に置いているのは、direnvが共通ライブラリを探す場所も XDG_CONFIG_HOME によって変わるからです。 切り替えの影響を受けない場所に置けば、どちらのディレクトリでも同じ関数が読まれます。

最初の版には不具合がありました。 リンク切れが残っていると、[ -e ] はリンク先を見て「存在しない」と判定します。 そのまま ln -s しようとして、すでにあるリンクとぶつかって失敗していました。 今は先にリンク切れを消し、ln -sfn で上書きしています。

Cloudflareは、cf CLIの機能で済んだ
#

このブログのデプロイは、wranglerから新しい cf CLI に移しました。 cf CLIには、名前付きのプロファイルをディレクトリに結びつける機能があります。

cf auth create my-dev              # 個人アカウントでログイン
cf auth activate my-dev ~/MY_DEV   # ~/MY_DEV の下ではこのプロファイル
cf auth whoami                     # どのプロファイルで認証しているか

結びつけた内容は、cf CLI自身の設定ファイルに保存されます。 direnvもリンクの張り替えも要りません。

wranglerのために書いた仕組みは、ここでは使わずに済みました。 道具の側に切り替えの機能があるなら、それを使うのがいちばん壊れにくいと感じています。

ただし、ディレクトリで決まるのはログインの情報だけです。 誤ったアカウントへデプロイしないように、リポジトリの設定ファイルにもアカウントIDを書いています。 ログインしているアカウントがそのIDに権限を持っていなければ、デプロイは失敗して止まります。

新しい道具が増えたときに確かめていること
#

この構成にしてから、アカウントを取り違えた失敗はなくなりました。 それでも、新しいCLIを使い始めるたびに、同じ問いに戻ります。 その道具は、どこに認証情報を保存しているのか、という問いです。

私は次の順で確かめています。

  1. 道具自身に、プロファイルやディレクトリごとの切り替え機能があるか
  2. なければ、設定ディレクトリや構成名を指定する環境変数があるか
  3. それもなければ、XDG_CONFIG_HOME のような共通の変数で分けられるか

gitとSSHだけは、エディタやGUIからも呼ばれるので、環境変数ではなく設定ファイルで分けます。

ディレクトリに入るだけで切り替わる状態は、一度作ると存在を忘れます。 新しいPCに移るときに困らないように、各ファイルの先頭には、なぜこの仕組みにしたのかをコメントで残しています。

関連記事