こんにちは。SREの徳田です。
今回は、「GitLabからGitHubに移行してみた(アプリケーションエンジニア編)」のSREチーム視点なるものを書いてみようと思います。
とはいえ、アプリケーションエンジニア編で素晴らしいほどにまとめてあるため、こちらでは実際に行った作業やGit LFSについて書いていきます。
Git LFS
Git LFSとは
Git LFSとはGit Large File Storageのことで、以下の機能を提供しています。
- 大きなファイルのバージョニング
- より多くのリポジトリ容量
- 高速なクローン・フェッチ
- Git workflowと同じ様に利用
- Gitと同様のアクセス制御
導入方法は簡単で、Windowsの場合はGit for Windowsをインストール、Macはbrewを使って導入することができます。
また、基本的にはインストール時に自動的にやってくれるはずですが、以下のコマンドでhookの登録を行います。
git lfs install
使い方
LFSで管理させるファイルを登録します。登録する際には「git lfs track」コマンドを使用します。
例えば、psdファイルを登録する際には以下のように実行します。
git lfs track "*.psd"
すると、「.gitattributes」というファイルが作成されます。このファイルにLFSで追跡するファイルが登録されます。
*.psd filter=lfs diff=lfs merge=lfs -text
また、追加で登録する際も同様のコマンドで追加することが出来ます。
これでpsdファイルをadd、commitするとLFSファイルとしてコミットされます。
Git LFSを使ったリポジトリの移行
既存のリポジトリをLFSに対応させるには、全ての履歴を変更する必要が有ります。一番最初のコミットから
- .gitattributeの追加(LFSで管理するファイルのリスト)
- LFSの管理対象であるファイルをLFSのオブジェクトとしてコミット
を全てのコミットに対して行います。こんな気の長い作業どうやって・・・、という気分でしたw
一応「git filter-branch」というコマンドがあり、以下のようなコマンドを実行することで変換を実行することが出来ます。
git filter-branch --tree-filter 'git lfs track "*.psd" > /dev/null' --index-filter "rm .gitattributes" --tag-name-filter cat -- --all
このコマンドは各コミットに対してチェックアウト・LFSの追跡登録・コミットを行います。ファイル・コミット数が多ければ多いほど時間がかかります。
このコマンドで対象のリポジトリを変換してみようと試みましたが、1日経っても終わらなかったリポジトリがあったため断念しました。(このリポジトリはファイル数が30万、容量が5GBほどのものでした)
ツライ〜〜と思っていた所、「git lfs migrate」なるコマンドを発見しました。このコマンドはオプションで指定されたファイルを対象として、既存のコミットを書き換えてくれます。
今回LFSの対象としたファイルは以下のようなものです。
- 画像ファイル(*.jpg, *.png, *.gif, *.bmp, *.ico, *.svg)
- フォントファイル(*.ttf, *.otf, *.woff, *woff2)
- その他バイナリファイル(*.lib, *.pack)
これらのファイルを対象に全てのコミットを変換するため以下のコマンドで変換を行いました。
git lfs migrate import \
--include="*.jpg,*.png,*.gif,*.bmp,*.ico,*.svg,*.ttf,*.otf,*.woff,*.woff2,*.lib,*.pack" \
--everything
「–include」オプションで対象ファイルの指定、「–everything」オプションで全てのReferenceを対象に変換を実行しています。
このコマンドのおかげで1日で変換が終わらなかったリポジトリが2時間程度で変換が出来ました。素晴らしい!!
おそらくファイルのチェックアウトをしないでindexの更新、コミットの変更を行っているので高速に処理することができたのかなと思っています。
Git LFSを掘り返してみる。
Git LFSで管理されるファイルについて
普段は「git lfs install」で行ったFilterの登録のおかげでLFSに対するファイルの操作は意識することはありません。
が、なにかあった時のためにもちょっとLFSの中を見てみましょう。
ここにLFSで管理しているリポジトリがあります。
$ git remote show origin -n
* remote origin
Fetch URL: git@github.com:cstoku/gitlfs-test.git
Push URL: git@github.com:cstoku/gitlfs-test.git
HEAD branch: (not queried)
Remote branch: (status not queried)
master
Local ref configured for 'git push' (status not queried):
(matching) pushes to (matching)
$ ls -a
. .. .git .gitattributes image.png
$ file image.png
gitlfs_test/image.png: PNG image data, 491 x 181, 8-bit/color RGBA, non-interlaced
$ cat .gitattributes
*.png filter=lfs diff=lfs merge=lfs -text
git lfsのhookを解除(「git lfs uninstall」で可能)このリポジトリを別名のフォルダにCloneしてみます。
$ git clone git@github.com:cstoku/gitlfs-test.git gitlfs_cloned
Cloning into 'gitlfs_cloned'...
done.
$ cd gitlfs_cloned/
$ ls
image.png
$ file image.png
image.png: ASCII text
なんとCloneしたリポジトリの中にあるimage.pngはtextみたいです。こちらの内容を見てみましょう。
$ cat image.png
version https://git-lfs.github.com/spec/v1
oid sha256:5e3f896398a0defa8ab6a9af864384c609072cdbc69c4f38168bb6918bad241c
size 25214
これはpointerと呼ばれており、LFSで管理しているファイルの情報を持っています。
このpointerを「git lfs smudge」コマンドのstdinに渡すことで実体のファイルを取ってきてstdoutに出力してくれます。
$ cat image.png | git lfs smudge > fetched_image.png
Downloading <unknown file> (25 KB)
$ file fetched_image.png
fetched_image.png: PNG image data, 491 x 181, 8-bit/color RGBA, non-interlaced
また、「git lfs pointer」コマンドでpointerと実体のファイルを比較し、対応したpointerかチェックすることが出来ます。
$ git lfs pointer --pointer image.png --file fetched_image.png
Git LFS pointer for fetched_image.png
version https://git-lfs.github.com/spec/v1
oid sha256:5e3f896398a0defa8ab6a9af864384c609072cdbc69c4f38168bb6918bad241c
size 25214
Git blob OID: 4acc6277d8a04cbe6cae85ae3ef8865a0eb2c439
Pointer from image.png
version https://git-lfs.github.com/spec/v1
oid sha256:5e3f896398a0defa8ab6a9af864384c609072cdbc69c4f38168bb6918bad241c
size 25214
Git blob OID: 4acc6277d8a04cbe6cae85ae3ef8865a0eb2c439
ちゃんと一致してますね。
LFSで管理されているファイルを一覧で出したいときは「git lfs ls-files」コマンドを実行します。
$ git lfs ls-files
5e3f896398 - image.png
これらのコマンドは直接使用することはほとんどありません。ですが、勉強がてら触ってみると理解が深まるかと思います。
Git LFSのファイルはどこに保存されるのか
Pushを行った際、LFSのファイルはどこに保存されるのでしょうか。なんとなくリモートのリポジトリと紐付いて保存されるんだろうなー、というのはわかりますが、実際どうなのでしょう。
Git LFSについての設定を表示してくれる「git lfs env」というコマンドがあります。これを実行してみるとちょっと分かってきます。
$ git lfs env
git-lfs/2.5.2 (GitHub; linux amd64; go 1.11)
git version 2.19.0
Endpoint=https://github.com/cstoku/gitlfs-test.git/info/lfs (auth=none)
SSH=git@github.com:cstoku/gitlfs-test.git
LocalWorkingDir=/home/cs_toku/tmp/git-lfs-test/gitlfs-cloned
LocalGitDir=/home/cs_toku/tmp/git-lfs-test/gitlfs-cloned/.git
LocalGitStorageDir=/home/cs_toku/tmp/git-lfs-test/gitlfs-cloned/.git
LocalMediaDir=/home/cs_toku/tmp/git-lfs-test/gitlfs-cloned/.git/lfs/objects
LocalReferenceDirs=
TempDir=/home/cs_toku/tmp/git-lfs-test/gitlfs-cloned/.git/lfs/tmp
ConcurrentTransfers=100
TusTransfers=false
BasicTransfersOnly=false
SkipDownloadErrors=false
FetchRecentAlways=false
FetchRecentRefsDays=7
FetchRecentCommitsDays=0
FetchRecentRefsIncludeRemotes=true
PruneOffsetDays=3
PruneVerifyRemoteAlways=false
PruneRemoteName=origin
LfsStorageDir=/home/cs_toku/tmp/git-lfs-test/gitlfs-cloned/.git/lfs
AccessDownload=none
AccessUpload=none
DownloadTransfers=basic
UploadTransfers=basic
GIT_EXEC_PATH=/usr/lib/git-core
git config filter.lfs.process = ""
git config filter.lfs.smudge = ""
git config filter.lfs.clean = ""
少々長いのと、色々表示されてますが気にせず・・・w
重要なのは 「Endpoint」という項目です。
Endpoint=https://github.com/cstoku/gitlfs-test.git/info/lfs (auth=none)
ここがLFSのオブジェクトを送信・取得する際のエンドポイントになります。要はHTTPSのリポジトリのURLに「/info/lfs」を追加したものです。
通常LFSはこのエンドポイントに対してHTTPSで通信を行い、GETやPUTの通信を発行しているだけです。そして、今回はリモートリポジトリがGitHubなのでGitHubでLFSオブジェクトをホスティングしてくれているわけです。
LFSオブジェクトを別のサーバーに保存
実はこのLFSを保存する先のエンドポイントは変更することが出来ます。「lfs.url」というパラメータにエンドポイントを指定することで実現できます。
git config lfs.url = "https://repo.example.com/test-user/test-repo/info/lfs"
しかし、このコマンドだとローカルの「.git/config」に追加されます。この設定をGitで管理したい場合は「.lfsconfig」というファイルに記述することで同様のことが行なえます。
git config -f .lfsconfig lfs.url = "https://repo.example.com/test-user/test-repo/info/lfs"
Git LFSについてのAPI仕様についても公開されています。
https://github.com/git-lfs/git-lfs/blob/master/docs/api/batch.md
この仕様に乗ってサーバーを実装すれば任意のバックエンドストレージにLFSオブジェクトを保存して運用する、なんてことも出来ます!
例として、S3に保存するアプリケーションを実装されている方がいましたので、そちらを紹介させていただきます。
- YDKK/LFS2S3Proxy (C#で実装されたLFSオブジェクトをS3へ保存するアプリケーション)
- Serverless Git LFS for Game Development (API Gateway / Lambda / S3 を使ったServerless構成)
また、HTTPSを使わないCustom Transferという手段も有り、これはLFSオブジェクトの送信・取得を別のCliに委譲して処理を行うことが出来ます。
詳細は長いので、気になる方は以下のドキュメントへどうぞw
https://github.com/git-lfs/git-lfs/blob/master/docs/custom-transfers.md
実装された方もいるみたいなので一応紹介させていただきます。
- sjansen/hoggle (Custom Transferを使ってS3へ保存を行う実装)
Git LFSへ移行をして思ったこと
移行しての利点はアプリケーションエンジニア編でだいたい上がっているので、どちらかと言うと気づきや欠点についてです。
小さいファイルをLFSで管理するべきではない
移行対象のリポジトリに数十から数百KBの画像ファイルが大量にあるものがあったのですが、そのリポジトリに対して、ほとんどの操作が遅くなってしまいました。
PullやCheckout、Statusなどもレスポンスが遅くなります。Git LFSは大きなファイルを保存するための機能であって、小さなファイルが大量にあるリポジトリを扱うためのものではないということです・・・。
Git LFSはsmudge filterという機能を使ってpointerファイルを実体のLFSオブジェクトと置き換える処理を行っています。なので通常のCheckoutなどの処理に比べてディスクのI/Oが増えます。
アプリチームの方でSSDを使用している方はその影響が少なかったですが、HDDを使用している方だとstatusの結果に数十秒かかる場合もあるそうです。
また、SourceTreeなどのGUIのクライアントを使用していると、バックグラウンドでのfetchやちょっとの操作でstatusなどを実行するので度々重くなったりする事象に見舞われたようです。😰
migrate後のpushで全オブジェクトがpushされない時がある
通常hookを登録しているのでgit pushでLFSオブジェクトがpushされますが、対象のコミットまでのLFSオブジェクトをpushするようだったので、 「git lfs push –all」コマンドで全てのLFSオブジェクトをpushするように実行したつもりでした。
が、別の方にpullしてもらったところ、見つからないLFSオブジェクトがいくつか出てきてしまいました。これについての原因は完全に謎で、念押しの念押しという感じで以下のコマンドを実行して全てのLFSオブジェクトをpushするようにしました。
git lfs ls-files -l | cut -d- -f1 | xargs git lfs push --object-id origin
まず全てのLFSオブジェクトのidを出して、それをpushするように書きました。これを実行したらpushされたLFSオブジェクトが数件出てきて無事pullすることが出来ました。
よくわからないですね・・・😅
まとめ
GitHubの移行に際して、Git LFSへの移行方法やGit LFSの内部、移行して思ったことなどを紹介しました。
GitHubを使う際にはリポジトリ容量が1GBに収める必要がある(超えるとメールが飛んでくる)ということでGit LFSを導入しました。
アプリチームにはGitHubの様々な便利機能を使い倒して頂いて、高速に・効率よく開発を進めていただければな、と思っています!