こんにちは、sue445です。
先日社内で使ってるGitLabのリプレイスをしたのでその辺の話をしたいと思います。
リプレイスの内容
今回のGitLabリプレイスでは主に下記を行いました。
- サーバ移設に伴いURL以外全部変えた
- レガシーな環境で運用されていたGitLabを全てDockerコンテナに載せた
- MySQLからPostgreSQLに移行
- 以上を1時間弱のメンテでやりきった
構成
ざっくり書くと、SSL終端のフロントサーバのみ同じで、それ以外のバックエンドを全部変えました。
旧
- APサーバ
- Debian Wheezy
- CPU: Intel Xeon E5-2640v2 * 2
- Memory: 40GB
- Disk: 64G + 512G
- MySQL兼Redisサーバ
- Debian Wheezy
- CPU: Intel Xeon X3430
- Memory: 8GB
- Disk: 256G
- MySQL 5.5
- Redis 3.2
新
- APサーバ
- Debian Stretch
- CPU: Intel Xeon Gold 5218
- Memory: 64GB
- Disk: 500G + 1Tx3 (RAID 5)
- Docker SwarmでクラスタリングしたAP, DB, KVSなど全部入り環境(後述)
- PostgreSQL 10
- Redis 4.0
構成図
コンテナ外のポート番号は一部伏せていますが、それ以外は実際のピクシブのGitLabと全く同じ構成です。
今回は社内サービスということで冗長構成は考慮せずに構築しましたが、サービス用途だったらまた違う構成を選択したと思います。
余談ですが真面目にGitLabのHA構成をやりたい場合には下記を参照してください。
GitLab High Availability | GitLab
リプレイスの理由
今回のGitLabのリプレイスにはいくつかの理由があったので紹介したいと思います。
サーバの老朽化
GitLab Container Registry を使いたいというアプリチームからの要望があり導入を試みたことがあるのですが、APのkernelが古すぎてDockerが入れられないという問題にぶちあたりました。1
kernelのバージョンを上げるという手もあったのですが、元のGitLabのサーバが開発機からの転用でずっと使ってきてOSもだいぶ古くなっていたので、いっそOSをアップグレードしたいというモチベーションがあってリプレイスを選択しました。
MySQLからPostgreSQLへの移行
当初のリプレイスの予定ではDockerizeまでは考えておらず、DBとKVSはそのままでAPサーバのみ新設して構成自体は従来どおりサーバにベタにインストール方式を考えていました。
しかし本番サーバの調達が終わっていざ新しい本番環境構築しようとした矢先に下記ツイートが流れてきました。
2019年7月22日にリリースされるGitLab 12.1で、MySQLサポートの終了が予定されています。MySQLを利用中のユーザーはPostgreSQLへの移行が必要です。https://t.co/SG2hhUg1pW
— GitLab.JP (@GitLabJP) June 25, 2019
余談ですがこのツイートの1ヶ月前に僕が投げたMySQL 8対応のパッチがマージされたところでした(´;ω;`)
Fix. db:migrate is failed on MySQL 8 (!28351) · Merge Requests · GitLab.org / GitLab FOSS · GitLab
ともあれこのままMySQLを使い続けてるとGitLabをバージョンアップできなくなるリスクがありました。
MySQLからPostgreSQLに移行するにはこのタイミングしかないということで、それまで計画してたサーバ構成を全て捨てて 新しく構成を考え直すことにしました
また検証環境ではDockerize前の構成で何回かGitLabのバージョンアップをしていたのですが、前述の構成図にあるようにGitLabはRailsアプリ以外にもgitlab-shell, gitlab-workhorse, gitalyなどのミドルウェアの集合体で1回のバージョンアップで全てを更新する必要があり非常に大変でした。
しかしDockerイメージで配布されているGitLabを使えば各ミドルウェアのバージョンアップの手間が大幅に緩和されるという目論見がありました。
どうせ計画を白紙にしてMySQLからPostgreSQLに移行するんだから、全部Dockerizeしても大して変わらないだろうというノリでDBやRedisまで含めて全部Dockerコンテナで構築することにしました。
リプレイス時の苦労点など
今回のリプレイスにあたって技術選択やアーキテクチャの迷いがあったので書いておきます。
GitLab関連のソースコードをどこに置くか
ピクシブではGitLabとGitHubの両方を利用しています。
GitLab関連のソースコードをどっちに置くか一瞬迷ったのですが、 GitLabが止まっていたらデプロイのためのgit pullもできない ことに気付いたためデプロイに必要なソースはGitHubに置くことにしました。
gitlab/gitlab-ce vs sameersbn/gitlab
GitLabのDockerイメージは大きくわけて下記の2つあります
普通だったら公式イメージを使うべきです。しかし、ピクシブGitLabでは敢えて3rd party製のsameersbn/gitlabを採用しました。
その一番の理由は、一通り動く docker-compose.yml
が https://github.com/sameersbn/docker-gitlab で提供されていたためでした。
当初は公式のDockerイメージを利用した docker-compose.yml
を途中まで書いていたのですが、 https://github.com/sameersbn/docker-gitlab/blob/master/docker-compose.yml と全く同じものができそうな未来が見えたので途中で方向転換してsameersbn/gitlabを使う方に切り替えました。
sameersbn/gitlabもGitHubを見る感じGitLabの最新バージョンに追従して積極的にメンテされてる感があって信頼できると判断しました。
自分のdocker-compose力が高かったり、公式でsameersbn/gitlabと同等レベルの docker-compose.yml
が提供されていたらまた違った結果になったかもしれないです。
移行後に知ったのですが、sameersbn/docker-gitlab は最新のGitLabに追従してくれてはいてもPRはなかなか取り込んでくれないみたいなので、公式イメージを使うことが苦じゃなければ間違いなくそっちの方がいいです。(最後の付録にも書いてますが明らかな設定漏れを修正するPRも取り込んでくれないのは厳しい)
Docker Swarmによるクラスタリング
本番環境をDockerコンテナで作ることは決まってましたが本番運用経験がないのでどうやってクラスタリングするか迷っていました。
迷っていた時期に Single node docker swarm でお手軽 rolling update | | 1Q77 を見つけたのでDocker Swarmを採用しました。
上記ブログに載ってることから変えたことがあるとしたら、ピクシブGitLabではデプロイ時に docker service update
ではなく docker stack deploy
を叩いてるところです。
docker service update
だとイメージの入れ替えはできるのですが、イメージはそのままでコンテナに渡す環境変数だけ変更したい場合に対応する手段がないためです。
docker stack deploy
でもreplica数が2以上であればrolling updateされることは確認できたので毎回 docker stack deploy
を叩く方式に落ち着きました。
2019/9/24 11:40追記
deploy.update_config.order
が start-first
であれば新しいコンテナ起動して healthcheck
の結果が healthy
になるまでは古いコンテナが残るので、 deploy.replicas
は1のままでもrolling updateは実現できました。 2
services:
gitlab:
deploy:
replicas: 1
update_config:
order: start-first
capistrano + docker-compose.yml + Docker Swarmによるデプロイ
クラスタリングまで決まったところで今度はどうやってデプロイして本番環境や検証環境に反映するかという問題にぶち当たりました。
Docker daemonのソケット自体はTLS認証できるのですが 3 、検証環境がOpenStackのVM内のDockerにあって「deployサーバ -> OpenStackサーバ -> OpenStack内のVM -> OpenStack内のVM内のDockerのソケット」の経路でdaemonのソケットを通すのが一筋縄でいかない予感がしました。
1週間くらい悩んだところで、docker-compose.yml
をcapistranoで転送してcapistrano越しにdockerコマンドを実行することにしました。
capistranoといえばデプロイツールの印象が強いですが、ssh越しにシェルを実行するための便利ツールなのでシェルでできることは全部capistranoの execute で実行できます。
幸い秘伝の ~/.ssh/config
のおかげでdeployサーバからOpenStack内のVMまで透過的にsshできて普通に cap deploy
できたのも嬉しかったです。
リポジトリ構成
ピクシブはオープンな文化なので従業員であれば社内のリポジトリは誰でも見れます。
しかしパスワードなどの機微情報は全従業員に見せたくないという悩ましさもあります。
ピクシブでは
- app1:アプリ本体のコード
- app1-env:機微情報を含む環境変数を管理するリポジトリ
という感じにリポジトリを分けて運用していて後者のリポジトリの権限を絞るような運用をしていたので、それに則った構成にしました。
最終的に、2つのリポジトリで計5種類の docker-compose.yml
が完成しました。
- docker-gitlab : 全従業員が見れるリポジトリ
docker-compose.yml
- ローカルから本番まで全てに適用したい設定はここに記述。(例:タイムゾーンなど)
docker-compose.override.yml
: docker-gitlabのみで動作確認するための設定を記述。(主にローカル開発用途)
- docker-gitlab-env : 管理者のみが見れるリポジトリ
docker-compose.common.yml
- productionとprestage 4 の両方で使いたいがdocker-gitlabには置きたくない設定を記述。(例:LDAPのパスワードや
volumes
)
- productionとprestage 4 の両方で使いたいがdocker-gitlabには置きたくない設定を記述。(例:LDAPのパスワードや
docker-compose.prestage.yml
,docker-compose.production.yml
- prestageとproductionで差分が出る設定を記述
- 一番分かりやすいのがホスト名。prestageとproductionではスペックが違うのでunicornのworker数やunicorn-worker-killerの設定も変えている
- productionのみDatadogの監視を入れたかったので
docker-compose.production.yml
にDatadogのコンテナを立てる設定を記述
デプロイ時には docker stack deploy --compose-file docker-compose.yml --compose-file docker-compose.common.yml --compose-file docker-compose.(production|prestage).yml gitlab
のような感じで --compose-file
オプションを複数個つけることで各ファイルの設定がいい感じにマージされています 。(詳細は後述)
rolling updateでもデプロイが完全に終わった時を通知したい
docker stack deploy
コマンド自体はすぐに終わるのですが、実際にはバックグラウンドでコンテナがモリモリ起動中の状態です。
rolling updateでダウンタイム無しでデプロイできて嬉しい反面、デプロイがいつ完全に終わったかSlackで通知されたいという気持ちもあります。
いい感じのツールがないか探したところ https://github.com/sudo-bmitch/docker-stack-wait という便利なものを見つけたのでcapistranoに組み込んで docker stack deploy
したコンテナが全て最新になるまで待つようにしました。(その後続のタスクでSlack通知)
pgloaderで色々ハマった
MySQLからPostgreSQLに移行する手順は公式で提供されています。
Migrating from MySQL to PostgreSQL | GitLab
しかしPostgreSQL初心者すぎて思ったよりハマりどころが多かったので、今回の代表的なハマリポイントを遺しておきます。
pgloaderの特定のバージョンじゃないと動かなかった
相性の問題かもしれないですが、結論から書くと自分の環境(gitlab 10.4.0, PostgreSQL 10, Debian Stretch)だとpgloader v3.5.1じゃないと途中でエラーになってうまく移行できませんでした。
とりあえず下記バージョンはダメでした
- https://github.com/dimitri/pgloader にある最新版のv3.6.1
- aptで入るv3.5.2
最終的には GitLab 9.1.2 (MySQL) を 11.4.0 (PostgreSQL) にアップグレード – FLAMA技術Blog で書いてあったv3.5.1で解決したのですが、v3.5.1はaptで入らないためpgloaderを手元でビルドするためにCommon Lispの環境も作りました。
pgloader実行後にRailsのタイムゾーンが破滅する
pgloaderのデフォルトの設定を使うとdatetime系のカラムがRailsが意図しない形式に変換されてしまい Time.zone
でJSTを指定してもSELECT時にUTCになる事象がありました。5
詳しいことは僕が投げたパッチに全部書いてるのでそっちを読んでください。
Squash migrationsの罠
当初の予定だとリプレイス時に10.4から12.0まで一気に上げるつもりでした。
しかし一気にバージョンを上げすぎたせいで Squash old migrations に引っかかり、古いmigrationファイルが適用されないという事象に陥りました。
そのため、1回のメンテナンス内で
- 10.4 -> 11.0のバージョンアップPR
- 11.0 -> 11.11のバージョンアップPR
- 11.11 -> 12.0のバージョンアップPR
というようにPRを分割して3回デプロイすることで対応しました。
結果だけ見ると11.11を経由するのはあまり意味がなかったのですが、リプレイス作業開始時点だと12.0がまだ出ていなくてその時点の最新の11.11でPRを作っていた感じです。
メンテナンスにあたって意識したこと
冒頭にも書いたようにURL以外全部変わるレベルの大規模メンテです。
しかしピクシブ社内だとGitHubとGitLabがほぼ同じくらい使われていて社内にだいたいエンジニアは80人くらいいるため、丸1日GitLabを止めると40人日分の開発が全部ストップするため障害を出すことはできません。
ジョジョ5部のブチャラティ の言葉を借りると 「『GitLabもリプレイスする』、『障害も起こさない』、『両方』やらなくっちゃあならないのが『インフラ』のつらいところだな」 という心境でした。そういう覚悟を持ってメンテナンスに臨みました。
メンテナンスの段取り
メンテナンス手順書の作成
大規模メンテを失敗させないためにメンテナンス手順書は入念に作成しました。
手順書は esa で作成しました。
esaにはコードブロックを書くと右上にCopyボタンが出る機能があり、実際にメンテナンス時にコマンドを1行ずつコピー&ペーストしやすかったためです。
余談ですがGitLabメンテナンス当日は丁度 AWS東京リージョンの障害 が起こってメンテナンス作業中にesaが見れなくなることも十分想像できたので、esaが見れるうちにローカルにHTMLとmarkdownの両方で保存していました。(幸いメンテ作業中にesaが落ちることはなかったので中の人には本当に感謝です)
メンテナンス手順書にはだいたい下記のような作業をコマンド単位で書いてます。
- 動いてるジョブがいたら止める @管理画面
- Runnerを止める
- git操作を封じる @旧AP
- nginxとgitlabを止める @旧AP
- git repoをrsync @旧AP
- GitLab本体のバックアップを使ってもよかったのだが、日次backupだけで1時間以上かかっていたためメンテ時間の短縮のためにrsyncすることにした
- メンテ時間短縮のためにメンテ日以前にもちょいちょいrsyncしていた
- backupを作って新APにscp @旧AP
- gitリポジトリとDB以外のbackupを作成
- pgloaderでMySQLをPostgreSQLに変換して投入 @新AP
- pgloader実行時のみ
docker-compose.yml
のportsで5432番ポートを開放し、コンテナ外のpgloaderからコンテナ内のPostgreSQLに接続
- pgloader実行時のみ
- git repoをimport @新AP
- backupをrestore @新AP
- PRを順番にマージして新APにデプロイする
- Frontのnginxのupstreamを旧APから新APに変える @Front
- iptablesの向き先を変える @Front
- DNSの向き先を変える
- Runnerをアップグレード
- 動作確認
- 後始末
- バックアップの設定確認、旧APの監視無効化など
「3. git操作を封じる」では /home/git/gitlab-shell/bin/gitlab-shell
に下記のようなのを追加してssh経由でのgit操作を封じています。
#!/usr/bin/env ruby
# これを追加する
puts "maintenance!"
exit 1
unless ENV['SSH_CONNECTION']
puts "Only ssh allowed"
exit
end
gitリポジトリのrsync中にgit操作が行われると面倒だったのでわざわざこんなことをしていましたが、よく考えたらGitLabを止めた時点でGitalyも止まってリポジトリへの操作もできなくなりそうなので不要だったかもしれないです。
リハーサルを行う
手順書に書かかれたコマンドが本当に正しいかの確認はもちろんですが、データ移行周りでどれくらいの時間がかかるか計測するために本番のGitLabとほぼ同じ量のデータを検証環境に用意して本番にかなり近い状態でリハーサルを行いました。
重めの作業の所要時間を予め計測しておくことでメンテナンス全体の時間を見積もることができてよかったです。
余談ですが、僕は 石橋を壊れる寸前まで叩き続ける性格 なので、今回の1時間のメンテナンス作業のために3週間くらいの準備期間を設けています。
メンテナンス中の様子
長丁場のメンテなので作業内容を全てSlackのスレッドに下記のように逐一書いていました。
これには理由があって、
- メンテ中に想定外のことが起こった時に他のメンバーに助け舟を出しやすくする
- 各コマンドの所要時間を後から振り返ることができる
ためです。
実際メンテ中に予期せぬ挙動になって他のメンバーにすぐに助け舟を出せて本当に助かりました。
ちなみに参考までに一番重かった作業はこんな感じでした。
- 約68GBあるgitリポジトリのrsync:7分
- バックアップ作成:11分
- 5.7GBあるMySQLのデータをpgloederで変換しながら新APのPostgreSQLに転送:10分
GitLabリプレイスのメリット
体感的に速くなった
メンテナンス直後の喜びの声です
APIも速くなった
ClosedなGitLabのURLをSlackに貼ったら展開されるようにした - pixiv inside で紹介したgitpandaは中でGitLabのAPIを叩いているのですが、リプレイス効果によりだいたい倍くらい速くなっていました。
ノーメンテでGitLabをバージョンアップできるようになった
Docker Swarmのrolling updateにより新しいバージョンのGitLabコンテナのデプロイ中であっても古いバージョンのコンテナにしかアクセスがいかないため、ダウンタイム無しでバージョンアップができるようになり、GitLabのバージョンアップのためのメンテが不要になりました。
細かいけどノーメンテバージョンアップ時のいくつか注意点です。
- バージョンアップに失敗した時のために念の為DBのバックアップだけはとっておく
- ノーメンテでいけるのはGitLabのAPのみなので、RunnerをAP外に置いている場合には止めておく必要がある
- とはいえ、「バージョンアップ中はGitLabに全くアクセスできない」より「バージョンアップ中はCIだけ動かない(GitLabは普通に使える)」方が利用者へのインパクトは圧倒的に小さいと思います
- Runnerが止まっていてもqueueingはされているので、Runnerのバージョンアップが終わって起動したら止まってた時間帯のジョブが再開されます
- 新しいコンテナで
rake db:migrate
が実行されてDBのスキーマが更新されて、古いコンテナが止まるまでの一瞬の間にアクセスがあるとエラーになることがある- 例)GitLabのバージョンアップでテーブルのカラムが削除されたとか
- レアケースではあるけど一応利用の少ない時間帯を狙ってバージョンアップした方がよさそう
付録A. GitLabリプレイスにあたってupstreamに投げたパッチ一覧
gitlabとdocker-gitlabに5つほどパッチを投げているのでご査収ください
gitlab (旧gitlab-ee) 6
Docs: Disable default conversion to timestamptz
on pgloader (!16601)
https://gitlab.com/gitlab-org/gitlab/merge_requests/16601
- 上の方でも書いたけどMySQL -> PostgreSQL移行の公式ドキュメントどおりにやるとDBのカラムが変な感じになってRailsが変な挙動起こす件の修正
- ドキュメントだけの修正なので実際に使う場合には手順をいい感じに変えてください
Fixed. ActiveRecord::StatementInvalid: PG::UnableToSend: no connection to the server
when backup heavy repository (!16598)
https://gitlab.com/gitlab-org/gitlab/merge_requests/16598
- 23GBあるgitリポジトリのバックアップを作成したらPostgreSQLのコネクションが死んでバックアップ作成処理が失敗する件の修正
docker-gitlab
Fixed. GitLab: Disallowed command
when ssh test command by sue445 · Pull Request #1959
https://github.com/sameersbn/docker-gitlab/pull/1959
gitlab-shell.sh
(ホストマシンからコンテナにgitユーザのsshを通すスクリプト) でssh -T git@〜
なコマンドを実行するとエラーになる件の修正- サンプルコードの修正なので実際に使う場合にはいい感じに置き換えてください
Add Sentry configuration to gitlab.yml by sue445 · Pull Request #1993
https://github.com/sameersbn/docker-gitlab/pull/1993
gitlab.yml
にSentryの設定を渡すための修正
Add prepared_statements to database.yml by sue445 · Pull Request #1994
https://github.com/sameersbn/docker-gitlab/pull/1994
database.yml
でpgbouncerを使うための設定を入れるための修正
付録B. Dockerコンテナの中で動いているRailsアプリに対してコンテナの外からモンキーパッチをあてる黒魔術
今回GitLabに絶対モンキーパッチをあてないという強い気持ちを持ってDockerイメージで提供されているGitLabを使ったのですが、付録Aにあるように数々のパッチが必要になりパッチがマージされるまでの間モンキーパッチをあてる必要が出てきました。(つらい)
動いているDockerコンテナの中でファイルを編集するとデプロイ時に変更が巻き戻ってしまうため、コンテナの外からコンテナの中に対してモンキーパッチをあてる手法を取りました。(とてもつらい)
変更できないDockerイメージの外からモンキーパッチをあてる黒魔術を2つ紹介したいと思います。
config/initializers/ にファイルをmountする
Railsアプリは config/initializers/
の下にファイルを置けば起動時に自動的に読み込んでくれます。
docker-compose.yml
の volumes
を使えばコンテナの任意の場所にファイルを置けるので、下記のようにコンテナ内の config/initializers/
にファイルをmountすることでDockerイメージに手を加えずにモンキーパッチを適用できて便利です。
gitlab:
volumes:
- "/path/to/monkey_patch.rb:/home/git/gitlab/config/initializers/monkey_patch.rb"
lib/tasks/ にファイルをmountする
付録Aの「Fixed. ActiveRecord::StatementInvalid: PG::UnableToSend: no connection to the server
when backup heavy repository (!16598)」だとGitLabのバックアップ用のrakeタスクから呼ばれるメソッドに対してモンキーパッチを適用して挙動を変える必要がありました。
当初は前述の config/initializers/
にファイルを置く方式でモンキーパッチを作ってたのですが、モンキーパッチをミスってアプリ本体を落としてしまったため動いているアプリ本体に全く影響を与えないためにrakeタスク実行時のみにモンキーパッチをあてる方式に変えました。
それが下記になります
このモンキーパッチのポイントは Rake::Task#enhance
を使って rake gitlab:backup:repo:create
の実行直前に backup_repository_monkey_patch
を呼び出してtask内の prepend
で Backup::Repository
にモンキーパッチを適用しているところです。
このモンキーパッチも前述の volumes
を利用して lib/tasks/
にmountして読み込まれるようにしています。
付録C. ピクシブGitLabで実際に使っている設定(一部)
機微情報を含まない範囲で公開します。ご査収ください
docker-compose.yml
ここにない書いてない設定は別の docker-compose.yml
に書いています。
1つ1つ解説するのも大変なのでyml内のコメントから苦労を察してください。
config/deploy.rb
cap deploy時に docker stack deploy
している処理一式です。
sameersbn/gitlabではコンテナ内のcrontabにバックアップの設定が書かれているのですが、複数replica構成だとバックアップが全コンテナで作られてしまうので、コンテナ1つだけ残して他コンテナのcrontabを消しています。
上の方でも追記してますが、 replicas
が1のままでのroling updateできるため不要上記処理は不要になったので削除
設定を全部見たい人は下記よりご応募お願いします。
21年度新卒(エンジニア職)(東京本社)の採用情報 | ピクシブ株式会社
-
http://otiai10.hatenablog.com/entry/2015/10/19/223647 にもありますがkernelが古すぎてdocker-engineの依存のinit-system-helpersがインストールできない感じでした。 ↩
-
https://docs.docker.com/engine/security/https/ , http://docs.docker.jp/engine/security/https.html ↩
-
prestageとは社内の開発環境によくつけられる名前です ↩
-
データ変換当時のGitLabで使ってたRailsのバージョン(4.2.10)で確認しましたが6.0.0でも同じ挙動かどうかまでは確認していません ↩
-
9/13にgitlab-ceとgitlab-eeのコードベースが統合されて、gitlab-ceがRead onlyの https://gitlab.com/gitlab-org/gitlab-foss に、gitlab-eeが https://gitlab.com/gitlab-org/gitlab にリネームされています ↩