ども!先日、久しぶりに背筋が伸びる出来事があった龍ちゃんです。
昔いたチームからくる確認のSlackって、なんかこう怖いんですよね。実際に怒られているわけじゃなくても、文面に若干の圧を感じるというか。今回まさにそれが飛んできて、詳しく聞いてみたら「org全体で使っているGitHub ActionsのArtifactストレージが大量に、僕がいま関わっているリポジトリ1つで使われている」という話でした。
え、そんなに?と思って自分でも調べてみたら、驚異の98%!一つのリポジトリで使用していました。しかも厄介なことに、この手の超過アラートって、僕みたいに実際にartifactを積み上げている本人には届かないんです。誰かが教えてくれない限り、自分では一生気づかないままでした。
「ストレージを使いすぎ」と言われたら、まずどこを見るか
同じことを言われたら、たぶん多くの人がまず思い浮かべるのは「actions/cacheのキャッシュが膨らんでるんじゃないか」だと思います。でも、cacheとartifactは別枠です。cacheはリポジトリごとに独立した枠を持っているので、そこがいくら膨らんでも、artifactの枠には効かないんですよね。
じゃあ何を見ればいいのかというと、軸は2つでした。
- どこを見るか: artifactとGitHub Packagesはセットで見ます。同じ課金プールを使っているので、片方だけ見ても切り分けになりません
- どう見るか: 件数ではなく容量で並べます。しかも画面に出るのは合計だけで、リポジトリごと・artifactごとの内訳はAPIを叩かないと出てきません
で、蓋を開けてみると、容量を食っていたのはGitHub Packagesではなくartifactの方でした。しかも、件数がいちばん多かったartifactは、容量で見るとほぼゼロだったんですよね。数が多いものが重いとは限らない、というやつでした。
本当の原因は、PRを出すたびに積み上がる「誰も取りに来ないartifact」でした
じゃあ何が積み上がっていたのかというと、CI自体はPRを出したときもマージしたときも同じように走っていて、ビルドしてテストして、その成果物をartifactとしてアップロードする、という流れになっています。ここまでは普通ですよね。
問題は出口でした。マージされたときはCDがそのartifactを拾ってデプロイに使ってくれるんですが、PRの段階だとそのartifactを取りに来る人が誰もいないんですよね。デプロイされるわけでもないし、レビューでダウンロードして確認するわけでもない。ただ保持期間が過ぎるまで、ストレージの上に律儀に残り続けるだけの存在でした。

実際に数えてみたら、このリポジトリに溜まっていたartifactのうち69%が、この誰も取りに来ないやつだったんですよね。PRの数が増えれば増えるほど、この「誰も取りに来ないartifact」も比例して積み上がっていく構造だったわけです。
悪かったのは書き方じゃなくて、運用が変わってもCI/CDを直さなかったことでした
このワークフロー、デプロイを回す道具としては十分に仕事をしていたんですよね。実際、書いてからしばらくは何も起きませんでした。
穴が表に出たのは、バグ改修フェーズに入ってからでした。PRが同時に何本も飛ぶようになって、その回数ぶんCIが走るようになったんですよね。
問題は、条件がデプロイする側にしかなかったことでした。デプロイする側はちゃんと条件で絞ってあったのに、artifactを作る側は特に絞っていなくて、retention-daysも指定していなかったんですよね。PRの数がそこまで多くなかった頃は、これで何の問題もなかったんです。
つまり、動くという目的では役割を果たしていたんですが、大量に発行される前提には耐えられなかったということですね。運用が変わったなら、CI/CDの設計もそれに合わせて変えるべきだったんだと思います。こういうのはぶつかって初めて覚えるんだな、と反省をしています。
依頼から3時間後には、消せるものは消し終えていました
管理チームから連絡をもらってすぐに調査を始めて、応急対応まで含めて3時間くらいで一通り片付けました。焦りながらの対応でしたね。
応急では、溜まった分を片付けて、保持期間を決めました
応急でやったことは2つです。
- 溜まっていたartifactを削除した。5日より古いものが対象で、画面からはまとめて消せなかったのでAPIを叩きました
retention-daysを5日に指定した。これから作られるartifactが、5日で勝手に消えるようにするためです
retention-daysというのは、artifactをアップロードするときに、そのartifact 1件だけの有効期限を指定できる設定です。workflowのupload側に書くもので、ログの保持期間には効きません。
何も指定しなければ、GitHubの既定で90日残ります。ただしこの値はリポジトリ・organization・enterpriseの各階層でも設定できて、下の階層は上の階層が決めた上限を超えられません。今回のリポジトリは設定側が30日だったので、指定漏れのartifactは30日きっちり残っていたわけです。

で、ここで一回ズルをしようとしたんですよね。設定側の保持期間を1日にしてしまえば、すでに溜まっている古いartifactも巻き込んで勝手に消えてくれるんじゃないかと思ったんです。でも消えませんでした。すでに作られているartifactは、作られた時点で決まった保持期間をそのまま持ち続けるので、あとから設定を変えても知らん顔で残り続けるんですよね。設定の変更は既存のものには遡及しないと公式ドキュメントにもはっきり書いてあって、先に読んでおけよという話でした。
結局、溜まっていた分はAPIで個別に消すしかありませんでした。削除とretention-daysの指定を両方やらないといけなかったのはこれが理由で、片方だけだと、消しても積み戻るか、積み戻りは止まっても溜まった分が残るかのどちらかになるんですよね。
消し方そのものは公式にまとまっているので、そちらに任せようと思います。画面から消す手順はRemoving workflow artifactsに、まとめて消すためのAPIはREST APIのartifactsにあります(DELETE /repos/{owner}/{repo}/actions/artifacts/{artifact_id}ですね)。
ここで書きたかったのは「何を不要と判断したか」の方で、答えはシンプルに「5日で切って消した」でした。過去のartifactを参照する経路は実際に無くて、必要になったらソースから再ビルドすればいいので、そこまで重い判断ではなかったです。
ただ!消したらコード側からしか戻せないので!きちんと不必要なものを見極めて消しましょうね。
恒久の方は、artifactをそもそも作らせない方向で直します
応急でやったのは、あくまで止血です。アラートが飛ぶほど溜まることはもう無くなりましたが、誰も取りに来ないartifactが作られ続けている状態そのものは、何も変わっていないんですよね。作って、置いて、5日後に消す。リソースがもったいないですし、直すべきなのはそこじゃないよなと思っています。
やるべきなのは、そもそも作らせないことです。今回の根っこは、デプロイする側には条件があったのに、artifactを作る側には条件が無かったことでした。PRのrunでは、成果物を渡す先がそもそも無いんですよね。だったらupload自体に条件をつけて、要らないところでは作らせないようにするのが筋です。さっきの図でいうと、上に伸びていた「誰も取りに来ない」側の経路を、根元で塞ぐイメージですね。

あわせて、寿命を用途で分けることも考えています。デプロイに受け渡すだけのものは短くていいし、後から参照する可能性があるものだけ長く残せばいい。ただこっちは補足で、本命は「そもそも作らない」の方です。
方向はもう決まっていて、いま実際に当てているところです。
それで、artifactの話はどうなったかというと
応急も恒久も、いまはもう手が動いている状態です。
あの日のSlackが無ければ、僕はいまも気づかないままだったと思います。というのも、超過アラートの既定の宛先はaccount ownerとbilling managerで、開発者本人は入っていないんですよね。ただ、受信者は後から足せます。
いや~でも会社のリソースの98%を食らいつぶすという驚異的な数字を叩きだしたときは大笑いしましたね。笑っている場合じゃないですけど。
というわけで、今回は僕がGitHub ActionsのArtifactでやらかした話でした。同じような運用をしているところがあれば、管理者に頼んでアラートの宛先に自分も入れてもらうといいと思います。気づける人を増やしておくのが一番手っ取り早いはずです。GitHub ActionsのArtifactの使い方自体は前に一度記事にしているので、そちらもよければどうぞ。
いや~管理チームがきちんと仕事をしていて素晴らしいですね!ちなみに僕は迷惑をかけるタイプの管理者だったので本当に頭が上がらないです。
ではまた!


