くりにっき

フルスタックキュアエンジニアです

Software Design 2020年1月号のGitLab特集に寄稿させて頂きました #gihyosd

商業誌デビューです!

gihyo.jp

f:id:sue445:20191207005442p:plain

きっかけ

GitLab.JP@hiroponz79 さんにお声がけいただいて参加することになりました。

僕の担当について

GitLab特集は全3章構成なのですが、僕は第3章の「自前でGitLabを管理するために知っておかなければならないこと」というタイトルで寄稿させて頂いています。

「自前でGitLabを管理するために知っておかなければならないこと」というタイトルの通り、僕の知ってるGitLab運用の知見を余すところなく書いています。

当初の予定だと GitLab令和最初のリプレイス。フルコンテナ化ポスグレ移行 - pixiv inside を深掘りした内容を書くつもりだったのですが、気づいたら全く別ベクトルのGitLab運用の話になって8〜9割くらい新規に書いていました。

GitLabが巨大なRailsアプリということもありどのRailsアプリでも汎用的に使えるunicorn-worker-killerの監視の話についても書いてるので、そっち方面での知見も得られると思ってます。

紙面の都合でいくらか内容を削ったので削った分は後日付録として別エントリで書こうと思ってます。*1

余談1

余談ですが、hiroponz79さんから声がかかったのと全く同時期に別方面でCI系の執筆依頼があって、てっきりSoftware DesignのCI特集で双方から僕にオファーがきたのかと思ったのですが、全く関係なかったというエピソードがあります。

ちなみに同時期に2本同時に執筆は体力的に厳しいのでもう片方のはお断りさせていただきました。

余談2

この記事の執筆中のBGA*2アイカツ! だったので、実質アイカツでできているといっても過言ではないです。*3

Amazonに自分の名前載ってるのすげーなw

f:id:sue445:20191207010829p:plain

*1:編集の人に問題ないことは確認済

*2:Back Ground Animation

*3:僕の好みはユリカ様です

HP Pavilion 23-q191jp ハイエンドモデルのメモリを増設した

3年前くらいに買った HP Pavilion 23-q191jp ハイエンドモデル のメモリを増設したのですが、分解に手間取った&ググっても詳しい分解手順が書いていなかったのでメモ

注意点

  • 自分で分解するとメーカー保証を受けられなくなるので自己責任で!

メモリ増設の経緯

今回8GBから16GBに増設したんですが、8GBだと下記のようにかなりストレスフルでした

  • PCの電源を入れてまともに使える(ChromeとSlackが操作できる)ようになるまで20分くらいかかる
  • Slackのワークスペースをたくさん開くとメモリを食いつぶす
  • とにかく重い
  • 全てが重い
  • メモリ8GBには人権がない

ここまでくると本当にPC買い直すかなって気持ちになってたのですが、8GBのメモリが数千円で買えたのでダメ元で増設しました

分解手順

1. モニタの下のキャップを2つ外してネジを緩める

ネジを外すのではなく緩めるのがポイント。ネジを緩めることでカバーを浮かせられるようになります

詳しい事 teru-maru.com

f:id:sue445:20191206225255j:plain

f:id:sue445:20191206225123j:plain

2. 背面のスタンドを外す

これが最難関ポイント

スタンドの根本部分

f:id:sue445:20191206221503j:plain

実はカバーになってて外せます。キッチリハマっているのでマイナスドライバーみたいに固くて平たいものを隙間に入れないと外せないです

f:id:sue445:20191206221448j:plain

カバーを外すとネジが4つあるので、こいつを外さないとカバーを外せません。

f:id:sue445:20191206214843j:plain

f:id:sue445:20191206214850j:plain

ちなみにノーヒントでここのネジの存在に気づくまで2日間くらいかかりました。。。

3. カバーを外す

ネジを外すことで前後に開けることができるのですが、これもキッチリハマっているのでマイナスドライバーなどでゆっくり外しましょう

4. 金属のカバーのネジを外してメモリを挿す

写真の右下部分のカバーがネジ4本で固定されているのでドライバーで外すとメモリが挿せるようになります

f:id:sue445:20191206215603j:plain

f:id:sue445:20191206220114j:plain

5. 組み立てる

逆順で組み立てる

6. タスクマネージャーで本当に16GB使えているか確認する

タスクマネージャーで確認したところ、メモリは16GB認識されているものの、「ハードウェア予約済み」が9GBあって16GB分フルで使えない感じでした

ググったら下記のように「ブート詳細オプション」の最大メモリにチェックが入っていてOSで使えるメモリの上限が8GBになっているようでした

blueskyzz.com

チェック外して再起動することで無事16GB使えるようになりました

f:id:sue445:20191206232716p:plain

メモリ増設の効果

PCの電源を入れて10分ちょいで使えるようになったので効果はあった模様

tanuki_reminderを作った

tanuki_reminderとは

マージされていないMRの一覧を指定した時間にチャットに通知するリマインダーで、 Pull Reminders のGitLabクローンです。

f:id:sue445:20191117012728p:plain

Pull Remindersが便利なのでGitLabでも使いたくて作りました。

gitlab.com

名前の由来

Pull Panda がパンダなので動物つながりでGitLabなのでタヌキにしました

余談ですがtanuki呼びは公式設定です

f:id:sue445:20191117084501p:plain

https://gitlab.com/gitlab-org/omnibus-gitlab/blob/12.3.4+ee.0/files/gitlab-ctl-commands/upgrade.rb#L178-197

技術的なこと

使い方

.gitlab-ci.yml に下記のようなジョブを書いてschedulerに登録するだけで使えます。

include:
  - remote: https://gitlab.com/sue445/tanuki_reminder/raw/master/ci-templates/slack_reminder.yml

slack_reminder:
  variables:
    # GITLAB_ACCESS_TOKEN: "" # [required]
    # INCLUDE_WIP: "false"    # [optional] put `true` or `false`
    # MERGE_REQUEST_COUNT: 10 # [optional]
    # SLACK_WEBHOOK_URL: ""   # [required]
    # SLACK_CHANNEL: ""       # [optional]
include:
  - remote: https://gitlab.com/sue445/tanuki_reminder/raw/master/ci-templates/chatwork_reminder.yml

chatwork_reminder:
  variables:
    # GITLAB_ACCESS_TOKEN: "" # [required]
    # INCLUDE_WIP: "false"    # [optional] put `true` or `false`
    # MERGE_REQUEST_COUNT: 10 # [optional]
    # CHATWORK_API_KEY: ""    # [required]
    # CHATWORK_ROOM_ID: ""    # [required]

通知対象のリポジトリにリマインダージョブを書いてもいいですが、通知専用のリポジトリを作ってschedulerに通知対象のリポジトリを複数登録するのが運用的に楽だと思います。

基本的にはGitLab CI上で動かすことを想定していますが、ビルド済のバイナリやDockerイメージでも配布しているのでGitLab CI以外でも使えると思います。

GitLab CIとCircleCIのキャッシュ戦略の違い

仕事でCI全般のお悩み相談されることが多くて .circleci/config.yml.gitlab-ci.ymlリファクタリングすることがよくあるのですが、その時に一番意識してるキャッシュ戦略について長年自分の中の暗黙知になっていて明文化できてなかったので書きます。

前置き

用語の定義

GitLab CIとCircleCIで用語が違っているので説明しやすくするために最初に定義します

  • キャッシュ:CIのビルドの実行を高速化するために保存しておくもの
  • ジョブ : CIで実行するスクリプト群の最小単位
  • ワークフロー:複数のジョブの集合で1コミットで実行される最小単位
    • GitLab CIだとpipeline、CirclrCIだとworkflowにあたる

その他

  • あくまで僕のポリシーなので異論は認めます

GitLab CIとCircleCIの両方に共通すること

キャッシュを過度に使いすぎない

複数のジョブ間でファイルを受け渡すためにキャッシュを利用することがよくあると思うのですが、個人的にこれはアンチパターンだとおもています。

理由はジョブ間が密に依存しているとCIの設定ファイル全体が複雑になり、リファクタリングの障壁になるためです。僕がCIの設定ファイルのリファクタリングをする時もまずはじめにこの手のキャッシュ依存をなくし、各ジョブをステートレスにしてワークフロー全体をシンプルにしています。

許容できるケース

CircleCIの persist_to_workspaceattach_workspace です。

https://circleci.com/docs/ja/2.0/configuration-reference/#persist_to_workspace

これは同一ワークフロー内でジョブ間でファイルを受け渡すための機能なので無問題。

特に persist_to_workspace を複数回読んだ後に attach_workspace を呼び出すとそれまでの persist_to_workspace で保存した結果全てが取り出せて便利です。

GitLab CIだとキャッシュのKeyに $CI_PIPELINE_ID *1を入れることで persist_to_workspaceattach_workspace に近いことはできますが、GitLab CIだと1つのジョブで1種類のキャッシュしか使えない関係で前述のように複数のキャッシュをマージして受け渡せないという問題があり、キャッシュに依存しすぎてると詰むことになります。

GitLab CIだとArtifactsでファイルの受け渡しをするのであれば可。

11/6追記

GitLab CIでもArtifactsでジョブ間でファイルのやり取りできたのを失念してたので修正。

ないと困るものはキャッシュにしない

キャッシュはあくまでもビルドを速くするために存在するので、キャッシュがあればラッキー、キャッシュがなくてもビルドが多少遅くなるだけでビルド自体は成功するって形の方がハマりが少ないです。

10/30 12:00追記

ここで言っているキャッシュというのは、例えばジョブの前段でライブラリをインストールして後続のジョブでそのライブラリがインストールされていることを前提にしているようなケースで引き回しているキャッシュのことを言っていて、ビルドの最終成果物であるArtifactsとは異なります。

ライブラリのインストールに時間にもよるんだけど基本的にはジョブわけない方がシンプルでメンテしやすいです。

persist_to_workspace 使うかどうかは好みによります。(個人的にはこの程度なら使わなくてもいいのでは派)

キャッシュをバージョニングする

キャッシュを互換性のないレベルで仕様変更するために古いキャッシュが使われないようにしたいことがあると思います。

GitLab CIだとリポジトリ単位でCIのキャッシュの全クリアができるのですが、CircleCIだとそういう機能がありません。

僕はよく v1-xxxx のような感じに先頭にバージョン情報を付与していて、必要に応じてインクリメントしています。

複数のブランチでキャッシュを共有できるようにする

キャッシュのkeyにブランチ名が入っているとその異なるブランチ間でキャッシュの共有ができません。

そうすると初めてブランチをpushした時に必ずキャッシュが存在しない状態でフルで bundle installnpm install されるような状態になるので、CI全体の時間で見ると遅くなります。

複数ブランチでキャッシュが共有されるとライブラリのインストールやバージョンアップが複数ブランチで同時進行しているとインストール合戦になってキャッシュがうまく使われなくなるデメリットがありますが、そういうのはレアケースなので複数ブランチでキャッシュが共有されて普段のビルドが速くなる方がメリットの方が多いと思ってます。

GitLab CI固有の話

GitLab CIのキャッシュの仕様

  • 同一のkeyに対して上書きができる
  • キャッシュのkeyは完全一致
    • keyに一致したキャッシュが存在しなければ取得されない
    • GitLab CIのキャッシュの実態はzipファイルなので、キャッシュのkey名でzipファイルが作られていると考えればイメージしやすいでしょう

CircleCI固有の話

CircleCIのキャッシュの仕様

CircleCIのキャッシュはGitLab CIに比べてトリッキーなので注意が必要です。

同一のkeyで一度キャッシュが作られたら上書きができない

save_cache で同名keyでキャッシュが保存できなくても「そのkey名で既にキャッシュを作成してるから作らなかったよ」ってログが出るだけでエラーにはならないため気づきづらいです。

https://circleci.com/docs/ja/2.0/configuration-reference/#save_cache

そのためCircleCIでは myapp-{{ checksum "package-lock.json" }} のようにキャッシュ全体のdigestをキャッシュのkeyに付与する必要があります。

キャッシュのkeyは前方一致

割と勘違いされがちなのですが、CircleCIのキャッシュのkeyは完全一致ではなく前方一致です。(完全一致するkeyがなければ前方一致するkeyのうち最新のものが返る)

またCircleCIの restore_cache では下記のようにキャッシュのkeyを複数設定でき、上から順番に評価されます。

- restore_cache:
    keys:
      - v1-{{ .Environment.CIRCLE_JOB }}-{{ checksum "Gemfile.lock" }}-{{ .Branch }}
      - v1-{{ .Environment.CIRCLE_JOB }}-{{ checksum "Gemfile.lock" }}
      - v1-{{ .Environment.CIRCLE_JOB }}
      - v1

この場合のキャッシュがヒットするルールはこんな感じです

  1. 同一ジョブ・同一Gemfile.lock・同一ブランチでキャッシュが存在すれば復元される
  2. 1でキャッシュが復元できなかった場合、同一ジョブ・同一Gemfile.lock・別ブランチの一番新しいキャッシュが復元される
    • 大抵の場合masterブランチで保存したキャッシュが復元される
  3. 2でキャッシュが復元できなかった場合、同一ジョブ・異なるGemfile.lock・別ブランチの最新のキャッシュが復元される
  4. 3でキャッシュが復元できなかった場合、同一バージョン(v1)のキャッシュが復元される
  5. 4でキャッシュが復元できなかった場合、キャッシュは復元されない

https://circleci.com/docs/ja/2.0/configuration-reference/#restore_cache

どうしても最新の状態でキャッシュを保存し続けたい時

同一key名だとキャッシュを上書きすることができないのですが、下記のように save_cacheepoch (エポックタイム)を含んだkeyのキャッシュを作ることで、 restore_cache で最新のキャッシュ復元できるようになります。

- restore_cache:
    keys:
      - v1-

- run: # キャッシュを作成する処理

- save_cache:
    key: v1-{{ epoch }}
    paths:
      - /caches/image.tar

せやかて

CircleCIのキャッシュの仕様を完全理解した状態で設定を書くのは無理ゲーだし設定ファイルも複雑になりがちなので他の人が作ったorbを使うのがいいでしょう。

Rubyリポジトリであれば下記orbに僕のCircleCIキャッシュの知見が全て詰まっているので何も考えなくてもいい感じにキャッシュを活用することができます。

https://circleci.com/orbs/registry/orb/sue445/ruby-orbs

詳しくは下記を参照

sue445.hatenablog.com

vagantでgem名とrequireする名前が異なるgemを使う時には注意が必要

タイトルが全て

経緯

https://hub.docker.com/r/sue445/vagrant-aws/ っていうDockerイメージをメンテしてて*1Vagrant 2.2.6が出たタイミングでDockerイメージのビルドがコケるようになりました

https://app.circleci.com/jobs/github/sue445/dockerfile-vagrant-aws/494

Installing the 'vagrant-serverspec' plugin. This can take a few minutes...
Vagrant failed to properly resolve required dependencies. These
errors can commonly be caused by misconfigured plugin installations
or transient network issues. The reported error is:

activesupport requires Ruby version >= 2.5.0.
The command '/bin/sh -c vagrant plugin install vagrant-aws  && vagrant plugin install vagrant-serverspec' returned a non-zero code: 1
Exited with code 1

ビルドコケてる原因はいたってシンプル*2でそれ自体は一瞬で直せました。

github.com

+# FIXME: Remove following when vagrant embed ruby is updated to 2.5+
+#        (vagrant embed ruby is 2.4.9, but activesupport 6.0+ requires ruby 2.5+)
+RUN vagrant plugin install activesupport --plugin-version 5.2.3
+
 RUN vagrant plugin install vagrant-aws \
  && vagrant plugin install vagrant-serverspec

誰だってそーする、俺もそーする。

docker buildも成功してvagrantのコマンドもCI上で一通り叩けることも確認してたんですが*3、実際に使おうとすると下記のようなエラーになってちょっとハマってました

root@b9657e1e1d4a:/# vagrant up
Vagrant failed to initialize at a very early stage:

The plugins failed to load properly. The error message given is
shown below.

cannot load such file -- activesupport

原因

vagrantのソース内の実装までは見つけられてないですが、vagrant plugin install でインストールしたgemは vagrant up などのコマンド実行時に自動的にgem名でrequireされているのが原因でした。(activesupportrequire する時は active_support になるのでエラーになってた)

requireする名前を明示的に指定する方法がないかなと思って探したら vagrant plugin install--entry-point があって、それを使うことで解決できました。

github.com

 # FIXME: Remove following when vagrant embed ruby is updated to 2.5+
 #        (vagrant embed ruby is 2.4.9, but activesupport 6.0+ requires ruby 2.5+)
-RUN vagrant plugin install activesupport --plugin-version 5.2.3
+RUN vagrant plugin install activesupport --plugin-version 5.2.3 --entry-point active_support

*1:vagrant-awsのインストールに数分かかるのでCI用にDockerイメージを作ってる

*2:Dockerイメージにvagrant-awsと一緒についでにvagrant-serverspecをインストールしていて、vagrant-serverspecの依存にactivesupportがいて、activesupport v6がリリースされたタイミングで最新版がRuby 2.5以降必須(vagrantに添付されてるRubyのバージョンが2.4.9)になったのが原因

*3:https://github.com/sue445/dockerfile-vagrant-aws/blob/2.2.6/.circleci/config.yml#L58-L61

Slackからのリクエストを処理するwebhookを1回だけ実行したい

最近そういうことを社内で何回か書いたのでメモ

前提

Slackには特定のイベントが発生した時に任意のwebhookにリクエストを飛ばすことができます。

f:id:sue445:20191017143638p:plain

具体的には下記のような使い方ができます

sue445.hatenablog.com

しかし普通にやると1回しかemojiを登録してないのにSlackに何回も投稿されて困ることがありました

なぜ重複投稿されるか?

公式ドキュメントを見つけきれてないので推測ですが、そういう仕様な気がしてます。

(Herokuだとdyno起動に時間がかかる時に重複リクエストになりやすい傾向があったけど、GASやLambdaでも同じことになる)

重複実行対策

Heroku + sinatra + puma構成の場合下記を行いました

1. 実行内容を一定時間キャッシュする

前述のemoy_webhookだとSlackの投稿内容を5分間redisにキャッシュしてたけど、Slackからのリクエストに含まれているevent_idを使った方がよかったかもしれないです

参考:

teppay.hatenablog.com

2. 排他ロックをする

redis-objectsにredisで排他ロックする機能があるのでそれを使ってました。(ruby以外にも同じようなライブラリがあるかは不明)

https://github.com/nateware/redis-objects#locks

3. サーバ側での並列処理を捨てる

1と2だけでは不完全で、完全同時にリクエストが飛んでくるとどうしようもなかったので多重実行されてしまいました。。。

そのためHerokuのdynoを1にしつつ、pumaのスレッド数を1にしてサーバ側で絶対に並列でリクエストを処理できない状態にしたら多重実行問題は解決しました。(普通のウェブアプリだと完全にアンチパターンなんだけどwebhookが叩かれる頻度は少ないので許容できる範囲内)

【CVE 2019-16892】rubyzip v1.3.0にアプデする時は絶対validate_entry_sizesを設定してください

tl;dr;

タイトルが全てです

前置き

GitHubのSecurity Advisoryで下記のようなPRがきていると思います

f:id:sue445:20191001205324p:plain

このPRをマージすればリポジトリのアラートも消えて問題なさそうに見えるのですが、実際にはgemのアップデートだけでは不十分です

CVE 2019-16892について

zipのヘッダを改ざんして展開時にクソデカメモリを喰わせる、いわゆるzip bombです(雑説明)

なんでgemのupdateだけでは不十分なのか?

zip bomb対策には Zip.validate_entry_sizes = true が必要なのですが、 v1.3.0の時点ではデフォルト値が false になっていて有効になっていないためです

詳しくはこのPRを見てください

github.com

ではどうすればいいか?

明示的に Zip.validate_entry_sizes = true してください。

Railsであれば config/initializers/CVE-2019-16892-rubyzip-patch.rb のようなファイルを置いておけばいいでしょう

v2.0.0ではデフォルトでONになっていてこのパッチは不要になるので、v2.0.0で使おうとしたらエラーになるようにしています。(モンキーパッチの賞味期限)

FAQ

Q. だったら最初から2.0.0にアプデすればいいのでは?

依存gemのdependency~> 1.3 と書かれていて2系に素直に上げられないケースも多々あるので、rubyzipを直接使ってない限りいきなり2.0.0に上げるのは難しいのではと思います。