モヒカンメモ

髪色が定期的に変わることに定評のある(比較的)若者Webエンジニアの備忘録

docker build時に署名検証失敗エラーが出たが、真の原因はファイルサイズ上限だった件

Docker for Macで docker build していたところ apt-get で署名検証失敗エラーが出たが、真の原因はDockerイメージの保存用ファイルがfullだったからだった。

ざっくりまとめ

  • Docker for Mac全体でのDockerイメージを保持するためのファイルのサイズ上限を決める設定がある
  • docker build 時に上記の上限を超えると、原因とは別のエラーが出る (今回でいうと At least one invalid signature was encountered. )
  • そうなったらイメージファイルのサイズ上限を引き上げるか、 docker system prune などを叩いてお掃除する必要がある

↓ Docker for Mac全体でのDockerイメージを保持するためのファイルのサイズ上限を決める設定

Docker for Mac - Settings - Resources - Virtual disk limit

調べたこと

Docker for Mac (Docker Desktop) で docker build をしたところ、下記のようなエラーが出た。

$ docker-compose up -d --build
...
 => ERROR [app stage-0 2/8] RUN apt-get update  0.9s
------
 > [app stage-0 2/8] RUN apt-get update:
0.143 Get:1 http://deb.debian.org/debian bookworm InRelease [151 kB]
0.170 Get:2 http://deb.debian.org/debian bookworm-updates InRelease [55.4 kB]
0.181 Get:3 http://deb.debian.org/debian-security bookworm-security InRelease [48.0 kB]
0.183 Err:1 http://deb.debian.org/debian bookworm InRelease
0.183   At least one invalid signature was encountered.
0.195 Err:2 http://deb.debian.org/debian bookworm-updates InRelease
0.195   At least one invalid signature was encountered.
0.207 Err:3 http://deb.debian.org/debian-security bookworm-security InRelease
0.207   At least one invalid signature was encountered.
0.208 Reading package lists...
0.211 W: GPG error: http://deb.debian.org/debian bookworm InRelease: At least one invalid signature was encountered.
0.211 E: The repository 'http://deb.debian.org/debian bookworm InRelease' is not signed.
0.211 W: GPG error: http://deb.debian.org/debian bookworm-updates InRelease: At least one invalid signature was encountered.
0.211 E: The repository 'http://deb.debian.org/debian bookworm-updates InRelease' is not signed.
0.211 W: GPG error: http://deb.debian.org/debian-security bookworm-security InRelease: At least one invalid signature was encountered.
0.211 E: The repository 'http://deb.debian.org/debian-security bookworm-security InRelease' is not signed.
------
failed to solve: process "/bin/sh -c apt-get update" did not complete successfully: exit code: 100

エラーを素直に読むとビルド失敗の原因は下記で、署名自体または署名の検証に何らかの問題がありそうと読み取れる。

0.183 Err:1 http://deb.debian.org/debian bookworm InRelease 0.183 At least one invalid signature was encountered.

が、実際には署名には問題がなく、真の原因はそこではなかった。

Docker "At least one invalid signature was encountered." というキーワードでググったところ、下記の記事がヒットした。

qiita.com

Docker for Macは使い続けるとコンテナデータやDockerイメージが溜まることを知っていた (以前にも苦しめられたとも言う) ので、Docker for Macの設定より Virtual disk limit128GB から 184GB へ引き上げたところビルドが通るようになったことを確認した。

Docker for Mac - Resources

Docker for Macの設定よりDockerイメージファイルがある場所を突き止め、下記記事を参考にコマンドを叩いたところ (117973668 / 1024/ 1024) = 112.5GBを実際に使っていたことを確認した。このとき、 ls -lh などでパッとでるサイズは割り当て済みサイズで、実際に使用済みの容量ではないことに注意。

docs.docker.jp

Docker for Macさん、実際は112.5GB使ってた

原因がわかってスッキリ〜。

ざっくりまとめ (再掲)

  • Docker for Mac全体でのDockerイメージを保持するためのファイルのサイズ上限を決める設定がある
  • docker build 時に上記の上限を超えると、原因とは別のエラーが出る (今回でいうと At least one invalid signature was encountered. )
  • そうなったらイメージファイルのサイズ上限を引き上げるか、 docker system prune などを叩いてお掃除する必要がある

DockerHub MySQLイメージのベースOSがOracleLinuxへ変わった件

タイミング的には2017年ぐらいのイベントだけど、歴史のあるプロジェクトを保守する時たまに踏むので備忘録がてらまとめておく。

DockerHub MySQLイメージのベースOSはOracleLinuxへ変わった

github.com

これまでのベースOSは debian だったが、2017年ぐらいに OracleLinux へ変わった。

これに伴って mysql:5.7 のようにバージョン指定していた場合、変更以前はdebianベースのイメージが落ちてきていたけど今はOracleLinuxベースのイメージが落ちてくるようになる。そのため、Dockerfileとかで apt ( apt-get ) などのOracleLinuxに入っていないコマンドを叩くとエラーになってしまう。

エラーになる例

$ docker build --platform linux/amd64 --no-cache .
[+] Building 0.8s (5/5) FINISHED                                               docker:desktop-linux
 => [internal] load .dockerignore                                                              0.0s
 => => transferring context: 2B                                                                0.0s
 => [internal] load build definition from Dockerfile                                           0.0s
 => => transferring dockerfile: 115B                                                           0.0s
 => [internal] load metadata for docker.io/library/mysql:5.7                                   0.6s
 => CACHED [1/2] FROM docker.io/library/mysql:5.7@sha256:f566819f2eee3a60cf5ea6c8b7d1bfc9de62  0.0s
 => ERROR [2/2] RUN apt-get update && apt-get install -y vim && apt-get clean                  0.1s
------
 > [2/2] RUN apt-get update && apt-get install -y vim && apt-get clean:
0.127 /bin/sh: apt-get: command not found
------
Dockerfile:3
--------------------
   1 |     FROM mysql:5.7
   2 |
   3 | >>> RUN apt-get update && apt-get install -y vim && apt-get clean
   4 |
--------------------
ERROR: failed to solve: process "/bin/sh -c apt-get update && apt-get install -y vim && apt-get clean" did not complete successfully: exit code: 127

OracleLinuxのパッケージマネージャは apt ではなく yum なので、 apt 系のコマンドを叩くと command not found となってしまう。

対策

(1) OracleLinuxで動くようにDockerfileを変更する

自分の知る限りでは大きな相違点でいうとパッケージマネージャがaptかyum (RHELベースだからdnfも使えるのかな?) かぐらいなので、debianからOracleLinuxへの移行はそこまで大変ではないように見える。

実際にやってみたわけではないので隠れめんどいポイントがあるかもしれない。

(2) debianベースのイメージを使うようにイメージ名を変更する

ベースOSを変えたらユーザが混乱することは容易に想像がつくので、debian版のイメージも用意されている。

DockerHub - MySQL 5.7:

hub.docker.com

mysql:5.7 としていたものを mysql:5.7-debian のようにお尻に -debian とつければdebianをベースOSとしたイメージが落ちてくる。

Dockerfileの修正例:

--- a/Dockerfile
+++ b/Dockerfile
@@ -1,3 +1,3 @@
-FROM mysql:5.7
+FROM mysql:5.7-debian

 RUN apt-get update && apt-get install -y vim && apt-get clean

Dockerビルド例

$ docker build --platform linux/amd64 --no-cache .
[+] Building 8.5s (6/6) FINISHED                                               docker:desktop-linux
 => [internal] load build definition from Dockerfile                                           0.0s
 => => transferring dockerfile: 122B                                                           0.0s
 => [internal] load .dockerignore                                                              0.0s
 => => transferring context: 2B                                                                0.0s
 => [internal] load metadata for docker.io/library/mysql:5.7-debian                            1.5s
 => CACHED [1/2] FROM docker.io/library/mysql:5.7-debian@sha256:0821f3a5b3ecda79885d8bd40ec35  0.0s
 => [2/2] RUN apt-get update && apt-get install -y vim && apt-get clean                        6.8s
 => exporting to image                                                                         0.1s
 => => exporting layers                                                                        0.1s
 => => writing image sha256:8b578dbed59f832ec67ff9d07c118038b1a814a93cb330e81fcba55478a21218   0.0s

Redashのログが無限に貯まり続けたせいでdisk fullで死んだ

セルフホストしているRedashのログが貯まり続けたせいでdisk fullで死んだ。

OSS版Redash をEC2でセルフホストしてそのまま運用していると踏むトラップだと思うので各位気をつけられたし。

ざっくり要点まとめ

  • Redashは結構ログを吐く
  • Docker Daemonはデフォルト設定では無限にログを貯め続ける
  • ↑の仕様が重なって、ログのローテーション設定をしておかないとディスクが食いつぶされる

調査と一次対応

「Redashが使えなくなった」というアラートを受けて調査したところ、dockerがdiskを食いつぶしてハングしていた。

root@xxx:/var/lib/docker# cat . | sort -r
9.7G    containers
9.6G    overlay2
...

下記の記事を参考に各コンテナのdisk使用量を調べるも微々たる大きさ。要因は別にありそう。

suzuken.hatenablog.jp

地道に du コマンドを叩きながら深ぼっていくと、diskをたくさん食っているのはコンテナのログファイルということが判明。

root@xxx:/var/lib/docker# docker inspect c347f677f78a | less
...
        "LogPath": "/var/lib/docker/containers/c347f677f78a9aac644c1c818e119e6aced19b8506d683b9012a50e964382e54/c347f677f78a9aac644c1c818e119e6aced19b8506d683b9012a50e964382e54-json.log",
...
root@xxx:/var/lib/docker/containers/c347f677f78a9aac644c1c818e119e6aced19b8506d683b9012a50e964382e54# du -sh *
2.9G    c347f677f78a9aac644c1c818e119e6aced19b8506d683b9012a50e964382e54-json.log
4.0K    checkpoints
8.0K    config.v2.json
4.0K    hostconfig.json
4.0K    hostname
4.0K    hosts
4.0K    mounts
4.0K    resolv.conf
4.0K    resolv.conf.hash

Redashをとめて find /var/lib/docker/containers -type f -name '*-json.log' | xargs truncate --size 0 でログファイルを切り詰めたあとRedashを起動させると無事起動して復旧したことを確認。

root@xxx:/opt/redash# docker-compose stop
...

root@xxx:/opt/redash# find /var/lib/docker/containers -type f -name '*-json.log' | xargs sudo truncate --size 0
...

root@xxx:/opt/redash# docker-compose up -d
...

恒久対応を考える

一次対応として truncate コマンドを叩いてひとまず復旧はしたものの、このままでは時間が経つとまた起きてしまうので恒久対応について考える。

最初はtrucateコマンドをcrontabで定期実行しようかと思ったけど、ログ消した直後にRedashでトラブルが起きてしまったら調査するとき困りそうだと思ってやめた。「redash ログローテーション」や「docker ログローテーション」などのキーワードでヒットしたブログ記事を読んで、docker engineのconfigを書いておけば自動でログローテーションするように設定出来ることが分かったのでこの方法で行ことに。

it-web-life.com

qiita.com

Docker Daemonにログローテーションの設定をする

Docker DaemonをLinuxで動かす場合、設定ファイルは /etc/docker/daemon.json に置けば良いらしい。

https://docs.docker.jp/config/daemon/daemon.html

docs.docker.com

10MB分ぐらいログ残しとけば大抵のトラブルには対応できるだろうという雑な読みで、下記のように設定。

/etc/docker/daemon.json

{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "5m",
    "max-file": "2"
  }
}

設定を反映するにはdockerdの再起動とdocker composeスタック作り直しが必要だった。

ubuntu@xxx:/opt/redash$ sudo docker-compose down
...

ubuntu@xxx:/opt/redash$ sudo systemctl restart docker
...

ubuntu@xxx:/opt/redash$ sudo docker-compose up -d
...
ubuntu@xxx:/opt/redash$ sudo docker inspect redash_worker_1 > inspect.log
$ cat inspect.log | jq '.[0].HostConfig.LogConfig'
{
  "Type": "json-file",
  "Config": {
    "max-size": "5m",
    "max-file": "2"
  }
}

これで良いはず!

ECS上のPHPアプリからAWS SDKを使うとmetadata endpointへアクセスできない問題の備忘録

ECS (Fargate) 上で動くPHPアプリからAWS SDKを使ったとき、metadata endpointへアクセスできない問題を踏んだのでそのことを残しておく。

ざっくり要点まとめ

  • AWS SDKなどがしれっと見に行く metadata endpoint にはEC2用とECS用で分かれており、 AWS_CONTAINER_CREDENTIALS_RELATIVE_URI という環境変数がAWS SDKから見えなければAWS SDKはEC2用metadata endpointに繋ぎに行く
  • php-fpmはデフォルト設定ではサーバ環境変数を引き継がない (exportしない)
  • 上記の仕様が重なって、PHPアプリをECS上で動かすときは明示的にその環境変数をexportしてあげないとEC2用metadata endpointに繋ぎに行って死ぬ

背景

AWSをセキュアに使うために、権限管理は出来るだけアクセストークンを発行せずにTask Role (EC2でいうInstance Profile) を使いたい。

これまでアクセストークン ( AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY ) を使って権限管理を行っていたWebシステムを、Task Roleによる権限管理に移行しようとしたところPHPアプリからAWS SDKを叩いてるところでmetadata endpointへのアクセスができなくなって死んだ。

この原因で死んだときにAWS SDKが吐くエラー:

Error retrieving credentials from the instance profile metadata service. (cURL error 7:  (see https://curl.haxx.se/libcurl/c/libcurl-errors.html) for http://169.254.169.254/latest/meta-data/iam/security-credentials/)

調べたことメモ

FargateにおけるIAM Role

  • Task Execution Role … コンテナを起動する人に与えられるロール
  • Task Role … コンテナそのものに与えられるロール

AWS SDKがどうやってTask Roleを使ってAWS APIを叩くか

AWS SDKがAWS APIを叩くとき、アクセストークン ( AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY ) が必要。それが環境変数などに直接設定されている場合はそれを使うし、されていなければmetadata serviceへ内部的に問い合わせを行ってそこで払い出されたtemporaryなアクセストークンを使ってAPIを叩く振る舞いをする。

docs.aws.amazon.com

AWS SDKが使うアクセストークンを解決する実装は下記のあたり

https://github.com/aws/aws-sdk-php/blob/aefc78c0af15995944ff61c3099e0233d0914e87/src/Credentials/CredentialProvider.php#L114

読み進めると、環境変数に AWS_CONTAINER_CREDENTIALS_RELATIVE_URIAWS_CONTAINER_CREDENTIALS_FULL_URI が設定されている場合はECS用のmetadata endpointに問い合わせるっぽいことが読み取れる

https://github.com/aws/aws-sdk-php/blob/aefc78c0af15995944ff61c3099e0233d0914e87/src/Credentials/CredentialProvider.php#L845

github.com

php-fpmはデフォルト設定では環境変数を引き継がない

php-fpmのデフォルト設定では clear_env = true となっているのでサーバ環境変数を引き継がない。そのため、phpアプリから AWS_CONTAINER_CREDENTIALS_RELATIVE_URI 環境変数を読めない。

PHP: 設定 - Manual

clear_env bool FPM ワーカー内の環境をクリアする。 任意の環境変数が FPM ワーカープロセスに到達してしまうことを防ぐために、 ワーカー内の環境をいったんクリアしてから、このプールの設定で指定された環境変数を追加します。 デフォルト値: Yes

上記から、AWS SDKは自分がECS上で実行されていることを認識できず、ECS用metadata endpointではなくEC2用metadata endpointにつなぎに行ってしまって failed to connect となって死ぬことが分かった。

Aws\Exception\CredentialsException: Error retrieving credentials from the instance profile metadata service. (cURL error 7: (see https://curl.haxx.se/libcurl/c/libcurl-errors.html) for http://169.254.169.254/latest/meta-data/iam/security-credentials/)

対処方法

  1. 設定を clear_env = false とする方法
  2. 設定に ENV パラメータを書いて明示的に引き継がせる方法

1. 設定を clear_env = false とする方法

デフォルト true となっている設定を false へ変更すると、サーバに設定されている環境変数が引き継がれるのでAWS SDK (PHPアプリ) からAWS_CONTAINER_CREDENTIALS_RELATIVE_URI 環境変数が見えるようになる。

PHP: 設定 - Manual

www.conf:

- ;clear_env = no
+ clear_env = no

2. 設定に ENV パラメータを書いて明示的に引き継がせる方法

clear_env = true のままでも下記のような設定を記載すると AWS_CONTAINER_CREDENTIALS_RELATIVE_URI 環境変数だけを引き継がせる (exportする) ことができる。

www.conf:

env[AWS_CONTAINER_CREDENTIALS_RELATIVE_URI] = $AWS_CONTAINER_CREDENTIALS_RELATIVE_URI`

参考になった記事

sodane.hokkaido.jp

パフォーマンスチューニングコンテストISUCON13に参加しました

毎年参加しているLINEヤフー社 主催のパフォーマンスチューニングコンテスト「ISUCON」に今年も参戦しました。

isucon.net

去年のブログ

blog.pinkumohikan.com

チーム

ここ4年ほど同じメンバーでISUCONに参戦していますが、チーム名は毎年変更しています。意味は特にありません。 今年は「ONAKA NO OWARI」というチーム名で出場しました。略称は「ハラオワ」です。

cureseven

  • 歌う隊長

nekodaisuki

  • SQLの魔術師

pinkumohikan

  • インフラを統べるもの

去年からのアップデートとしてはcuresevenがリーダーになり、自分とnekodaisuki氏の担当をスイッチして自分がインフラ担当となりました。

リザルト

694チーム中45位

isucon.net

目標30位以内だったので未達です...。

リポジトリ & Issue: github.com

チームでの振り返りはまだなので個人としての反省でいうと、DNS水攻め攻撃対策に時間を使いすぎました。

所感

今年も唸らされる問題でした。 普段DNSはRoute53などのフルマネージドサービスしか使わないので、その裏側で繰り広げられているバトルが垣間見える面白い問題でした。

正直、1年間ほぼ毎月練習してきてもこの順位か…と意気消沈気味ではありますが、超えるべき壁がまだあるということで腐らずにまた来年頑張ります。

LINEヤフー社をはじめ問題作成して下さった皆さま、スポンサーの皆さま、感情を揺さぶられるイベントをありがとうございました。来年もまた宜しくお願いします!

PHPカンファレンス2023で登壇しました

2023/10/8 (日) に 大田区産業プラザPiO で開催された PHPカンファレンス 2023 にて、「安全にPHPでWebアプリ開発するために実践していること」というテーマでお話してきました。

トーク

PHPは言語仕様が年々堅牢になりつつありますが、より安全に仕様変更やリファクタリングを行うためにどういう工夫が出来るか?について、テテマーチ株式会社 SINIS for Twitter 開発チームでの事例を元にお話しました。

fortee.jp

speakerdeck.com

プロダクト開発の現場でPHPを使っていて、PHPerKaigi懇親会で「かなりしっかりと開発基盤整えられていますね」という感想を頂いたのが本トークのきっかけです。

参考になったことや分からなかったことなどありましたら、SNSでもforteeのFB機能からでも良いのでフィードバック頂けるとありがたいです。実際やってみたいけど「ここが分からん」みたいなところがあればSNSなどでご連絡いただければベストエフォートでサポートします。

感想

LT登壇は良くやっているんですが、PHPカンファレンスのような大きなイベントでの登壇は初めてだったので少し緊張しました!!!!

トークテーマがちょっと渋めなので「ガラガラだったらどうしよう」と少し不安もありましたが、結果的にたくさんの人が来てくださったので安心してお話出来ました。所々挟んだ冗談にも反応して下さってありがとうございました。

トーク後に「振り返りで当事者に責任を感じさせすぎないためにどういう工夫が出来るか?」「ツールの使い分けについての説明が欲しい」「PHPStan Level Maxつらすぎ問題」など、派生する話題や発表改善のヒントを頂きました。その場ではCoolな回答ができなかったので、その辺で話せそうなことを見つけたらまた登壇応募しようと思います。

イベントスタッフやスポンサーの皆さま、素敵なイベントをありがとうございました!

PRテンプレートの「レビュイーだけに伝えたいこと」をHTMLコメントで書いてレビュー負荷を下げるテクニック

GitHubでPull Requestを作る際、「必ずPRに書いて欲しいこと」をPRテンプレートに書くプラクティスが普及している。 PRテンプレートを用意する際に「レビュイー (PR作成者) だけに伝えたいこと」をHTMLコメント ( <-- aaaa --> ) として書くと余計な情報をレビュアーに見せずに済むテクニックがあるので紹介する。

Pull Requestテンプレート

GitHubでホストするgitリポジトリ内に pull_request_template.md という名前のMarkdownファイルを用意しておくと、開発者がGitHub上でPRを作成しようとしたときにそのファイルの内容がDescriptionのデフォルト値として入力される仕組みがある。

docs.github.com

PR Descriptionに何を書くかはチーム開発においてとても重要で、何を書くべきで何を書かないべきかについて議論したり、ルール化しているチームは多いと思う。 この機能を使うことでうっかりPRに記載し忘れていて「チケット番号を必ず記載するルールになっているので明記して下さい。」というような非生産的やり取りを無くすことが出来る。

PRテンプレートが成長すると起きる課題

PRには「作る人(レビュイー)」と「見る人(レビュアー)」が居る。 PRテンプレートには二種類の人間それぞれに読んでもらうべきメッセージを含める必要があるが、愚直に (工夫せずに) 記載するとどうしても情報量が多くなってしまう。

PRのレビューでは動作の正しさはもちろんコードの読みやすさや設計の良し悪し、パフォーマンス、セキュリティなど様々なことを考慮する必要があり、レビュイーの負担は大きい。そのままでは長大なPR Descriptionをレビュイーが「このメッセージは自分に関係があるのか?」を考えながら読まねばならない。

レビュイーだけが気にしていれば良い情報はレビュアーにとってノイズであり、そのままにすると生産性が下がる。

レビュイーだけへのメッセージはHTMLコメントで書く

GitHubのPull RequestにはHTMLコメントを記載できる。HTMLコメントとして書かれている内容は当たり前だが、PR作成 (編集) ページ以外では表示されない。

docs.github.com

この仕様を利用してレビュイーにだけ伝えたいメッセージ (例えば各項目に何を書いて欲しいかの説明) をHTMLコメントで記載することで、 レビュイーに必要なメッセージを伝えつつ、レビュアーが受け取る情報量を減らすことが出来る。

pull_request_template.md例:

# 概要
<!-- このPRは「何のために」「どういう変更をする」ものか、簡潔に説明して下さい -->

## 関連チケット
<!-- 関連するチケットがある場合はリンクを貼って下さい -->
* 

# 特筆事項
<!-- 参考にした資料、補足説明しておきたい背景などがあれば記載して下さい -->

----

※ :bulb: [コードレビューの目的と観点](https://github.com/pinkumohikan) についてもご確認下さい

PRページでの表示例 (レビュアー視点):

PRページでの表示例

想定質問

Q: そんなに頑張らなくてもレビュアーが頑張って読み飛ばせば良くない?

A: Perl開発者のLarry Wallが提唱した「プログラマ三大美徳」の「怠惰」を愛しなさい。少しの工夫で仕組みによって解決できるのに、わざわざそれをやらない理由は無いだろう。

Q: PRテンプレートにはそのまま書いて、レビュアーが気しなくていいことはレビュイーがPR作るときに消す運用にすれば良いんじゃない?

A: Perl開発者のLarry Wallが提唱した「プログラマ三大美徳」の「怠惰」を愛しなさい。そういう運用をしていたところをいくつか知っているが、期待通りの運用がされていたのは導入直後だけだったので個人的にはそれでは回らないと思っている。かなり強い気持ちで警察業 (ルールを守らない人を指導しまくる役) をするならばあるいは。

DBマイグレーションツールのロールバック機能は使うな

データベースマイグレーションツールのロールバック機能は安全に使えないので使うべきではないと思う。

ロールバック機能

RDBMSのデータベーススキーマを管理するためのツールとして flyway や、ウェブアプリケーションフレームワーク組み込みのマイグレーションツール (例: Laravel Migration ) がある。

DBマイグレーションツールにはマイグレーションを進める (up) 機能のほかに、進めた変更をロールバックする (down) 機能がついている。

マイグレーションを進める例:

CREATE TABLE customers (
  id INT PRIMARY KEY,
  name VARCHAR(50),
  email VARCHAR(100)
);

マイグレーションをロールバックする例:

DROP TABLE customers;

この記事では、ロールバックする (down) 機能に焦点を当てて話す。

DBマイグレーションは失敗し得る

ご存知の通り、DBマイグレーションは失敗し得る。

  • マイグレーションに複数のDDLが含まれていて、一部だけが失敗した
  • 各種制約 (ユニーク制約、外部キー制約、チェック制約など) に反するデータを一時的にでも作成、変更、または削除しようとした
  • テーブルに変更を加えるのに必要なメタデータロックを獲得できず、タイムアウトした
  • etc...

さて、あなたがこれまで書いてきたロールバック処理はこのうちのどれを想定したものだろうか?

マイグレーション失敗の原因に応じてロールバック方法が異なる

言いたいことはこの一言に尽きる。

当たり前の話として、予想される失敗に応じてrollbackを書くべきで、失敗が予想されるなら事前にテスト環境や検証環境などで検証を行うことで失敗する確率は減らせる。 自分はリスクが高いと感じるスキーマ変更を行うときは本番DBのスナップショットから検証用DBを立ち上げ、マイグレーションを適用し、正常に完了することや変更にかかる時間を計測している。

そこまでしても失敗するときは失敗する。 サービス規模によって本番と同等の検証用DBを用意することが現実的でないこともあるし、負荷がかかってないと発現しないトラブルもある (読み書き頻度が高する or long txが居てメタデータロックが取れないとか)。

失敗するまでテーブル構造やデータがどういう状態か確定しないので、どのようなロールバックを行うのが適切かは失敗するその時まで分からない。 なので、マイグレーションを書く時点では安全な (= 絶対に成功する) ロールバック処理を書くことはできない。故にロールバック機能を使うべきではない。

ロールバックも前進 (up) させればよい

ロールバック機能を使わないとしたらどうすれば良いのか?

知っている人には当たり前の話だが、あえて丁寧に説明しておくとデータベースのマイグレーションが失敗するとサービスが止まる。可能な限り早急にロールバックしてサービスを復旧させたいだろうが、なぜ失敗したのかをちゃんと確認しよう。必要に応じて関係者にメンションを飛ばす。事件事故の報告は早ければ早いほど良い。一人では冷静に対処出来ないなら誰かに助けを求めてペアオペするのも有効。

失敗の原因が分かったら安全にロールバックする、あるいは理想状態に持っていくための新しいマイグレーションを用意する。DDL/DSLを書いてcommitしてPull Requestを上げてレビューなどの正規のリリースプロセスを経て再度マイグレーションを行う。

緊急性が高い場合は本番データベースで直接オペレーションすることもあるだろうが、そのときは絶対に一人でやらない方がいい。第三者にレビューしてもらったりペアオペにすることで的はずれな対応をしている場合に気づける確率が上がる。的はずれな対応をして二次被害を生むことが最悪なので、逸る気持ちを押さえながらそれを回避することを一番に考える。

【GitHub】 マージ済みのremote branchを一括削除するコマンド

GitHubへマージ済みブランチが貯まると git clone するのにかかる時間が増えてしまったり、ブランチ一覧からお目当てのブランチを探すのが面倒になるなど、地味に生産性を下げるのでこまめにお掃除するのがオススメ。

コマンド

$ git branch --remotes --merged | grep -v "origin/main" | sed -E 's/  origin\/(.*)/\1/' | xargs -I{} git push origin :{}

解説

(1) マージ済みのブランチ一覧を取ってくる

$ git branch --help
...
OPTIONS
...
       -r, --remotes
           List or delete (if used with -d) the remote-tracking branches. Combine with
           --list to match the optional pattern(s).
...
       --merged [<commit>]
           Only list branches whose tips are reachable from the specified commit (HEAD if
           not specified). Implies --list.
$ git branch --remotes --merged
  origin/some-merged-branch-1
  origin/some-merged-branch-2
  origin/some-merged-branch-3
  origin/main

(2) 消しちゃダメなブランチを取り除く

例えば main ブランチを残しておきたいなら

$ git branch --remotes --merged
  origin/some-merged-branch-1
  origin/some-merged-branch-2
  origin/some-merged-branch-3
  origin/main

$ git branch --remotes --merged | grep -v "origin/main"
  origin/some-merged-branch-1
  origin/some-merged-branch-2
  origin/some-merged-branch-3

(3) "origin/" 部分をtrimする

sedで origin/ 以降だけを正規表現の参照で取り出す

$ git branch --remotes --merged | grep -v "origin/main"
  origin/some-merged-branch-1
  origin/some-merged-branch-2
  origin/some-merged-branch-3

$ git branch --remotes --merged | grep -v "origin/main" | sed -E 's/  origin\/(.*)/\1/'
some-merged-branch-1
some-merged-branch-2
some-merged-branch-3

(4) remote branchを削除する

単体のブランチ削除コマンドとxargsを組み合わせて全部消していく

$ git push --help
...
       git push origin :experimental
           Find a ref that matches experimental in the origin repository (e.g.
           refs/heads/experimental), and delete it.
$ git branch --remotes --merged | grep -v "origin/main" | sed -E 's/  origin\/(.*)/\1/'
some-merged-branch-1
some-merged-branch-2
some-merged-branch-3

$ git branch --remotes --merged | grep -v "origin/main" | sed -E 's/  origin\/(.*)/\1/' | xargs -I{} git push origin :{}
remote:
To github.com:aaa/bbb.git
 - [deleted]         some-merged-branch-1
remote:
To github.com:aaa/bbb.git
 - [deleted]         some-merged-branch-2
remote:
To github.com:aaa/bbb.git
 - [deleted]         some-merged-branch-3

削除対象がたくさんあるときは、xargsに -P n を指定して並列度上げると短時間で終えられる。

Docker ComposeでDBコンテナの起動を待ってからAPIコンテナを起動させる

ざっくりまとめ

Docker Composeの depends_onhealthcheck を組み合わせると、DBコンテナが完全に起動してからAPIコンテナを起動させられる。

課題

DBとAPIからなるWebアプリケーションの実行環境をDocker Composeで用意しているとき、DBが完全に起動し終わる前にAPIが起動してリクエストを処理すると 500 (Internal Server Error) を返してしまう問題がある。

ローカル環境などでの開発時であれば大きな問題はないが、CI時にコンテナスタックの起動後即座にAPIテストが走る前提だとこの問題を踏んで「なぜか稀にAPIテストが500で落ちる」という現象に悩まされることになる。

毎回起こるわけではないため対応の優先度付けが難しく、またAPIテストが落ちるとなったら普通はAPIに不具合があることをを疑ってしまい本当の原因に気づきづらい。

解決方法

Docker Composeの depends_onhealthcheck を組み合わせて、DBコンテナの完全起動を待ってからAPIコンテナを起動させる。

(1) depends_on

depends_on は、あるコンテナが別のコンテナに依存していることを表明する設定。 docker compose up を叩いた際のコンテナ起動順に影響があり、この設定をしておくと依存先から順に起動してくれる。

例えばAPIコンテナがDBコンテナに依存しているとき、下記のように depends_ondb と記載しておくと db コンテナを起動後に api コンテナが起動されるようになる。

...
  services:
  
    api:
      ...
+      depends_on:
+        - db
  
    db:
      ...

ただし、ここでいう "起動" とは「CMDで指定されているコマンドを実行して即座に異常終了しなかった」という状態を指しているので、この設定だけではDBのウォームアップ中にAPIコンテナが起動して500を返し得てしまうのでもう少し工夫が必要。

(2) healthcheck

healthcheck は、正常状態かどうかの検証方法をDockerに検証させるための設定。コンテナのウォームアップが終わってリクエストを正常に受け入れられる状態かどうかの検証方法をここで設定する。

例えばDBコンテナが完全起動したら mysqladmin ping -u root とか mysql -u root -e "SELECT 1;" コマンドが通るはずなので、下記のように test に検証コマンドを記載する。

  services:
  
    db:
      ...
+      healthcheck:
+        test: ["CMD", "mysql", "-u", "root", "-e", "SELECT 1"]
+        interval: 6s
+        timeout: 1s
+        retries: 5        

(3) depends_onとhealthcheckを組み合わせる

依存関係を表明する depends_on と、完全起動状態を検証する healthcheck を組み合わせると、DBコンテナが完全起動してからAPIコンテナを起動させられる。

...
  services:
  
    api:
      ...
+     depends_on:
+       db:
+         condition: service_healthy

    db:
      ...
+     healthcheck:
+       test: ["CMD", "mysql", "-u", "root", "-e", "SELECT 1"]
+       interval: 6s
+       timeout: 1s
+       retries: 5

depends_on を書くときに condition: service_healthy と指定しないといけないのがややトラップ。