世界を動かす技術を、日本語で。

「dry-run」の賛美

概要

  • 新しいレポート作成アプリケーション の開発体験
  • --dry-runオプション の導入経緯と活用事例
  • テスト・検証時の利便性 と具体的な使い方
  • コードへの影響 やデメリットの考察
  • --dry-runの有用性 と導入タイミングの重要性

--dry-runオプション導入の背景と経緯

  • 新しい レポート作成アプリケーション の開発プロジェクト
  • アプリケーションは 平日ごとに自動でレポートを生成 し、複数の工程を実施
    • データベースからのデータ読込
    • レポート生成ロジックの適用
    • レポートのzip圧縮・SFTPサーバーへのアップロード
    • SFTPサーバーからのエラーレスポンス確認・解析
    • 通知メールの送信
    • ファイルの工程別ディレクトリ移動
  • 開発初期に SubversionやLinuxコマンドの--dry-runオプション を思い出し、同様の機能を組み込むことを決断
  • --dry-run指定時は 各フェーズの処理内容を出力 し、実際には変更を加えない
    • 生成するレポートや処理対象ファイルの一覧表示
    • SFTPサーバーへのアップロード・ダウンロード予定ファイルの確認

--dry-runオプションの具体的な効果と活用例

  • 日常的に--dry-runを活用 し、作業前の安全確認や動作チェックを実施
  • --dry-runは 安全に実行可能 なため、意図せぬ変更を防止
  • 各種設定やシステム状態の 即時確認・サニティチェック 用途
  • システム全体のテスト時にも 素早いフィードバック を得る手段として有効
    • 例:レポート状態ファイルの日付変更後、即座に生成対象かを出力で確認
    • 実際のレポート生成を伴わないため、 結果確認が高速

--dry-run導入によるデメリットと実装上の注意点

  • dryRunフラグの存在がコードを多少複雑化
    • 各主要フェーズで dryRunフラグの判定 が必要
    • ただし、実際のレポート生成など深い処理内部では判定不要
    • 処理呼び出し前の判定 のみで十分

--dry-runオプション導入の総括

  • 今回のような コマンド実行型・バッチ処理アプリケーション には--dry-runが非常に適合
  • リアクティブ型アプリケーション (イベント駆動型)にはあまり適さない
  • 開発初期から--dry-runを導入 したことで、機能追加時にも継続的な恩恵
  • --dry-runは 全てのプロジェクトに適する機能ではない が、適用できる場面では極めて有用

Hackerたちの意見

普段は逆のことをして、CLIユーティリティに--reallyフラグを追加するんだ。デフォルトは読み取り専用にして、何かを間違えるにはちょっと手間がかかるようにしてる。

同じこと言いに来た。

"--i-meant-that"をコミットしたことがあるよ(リモートマシンを壊すコマンド用で、通常はメッセージを表示して10秒間^Cを押すかどうか考える時間を与えるんだ)。特にせっかちな同僚のためにね。結局、不適切に使われることはなかったからラッキーだったけど(でも、どれだけ運が良かったかは数えなかったよ)。

コードベースを汚さずに動かすためには、永続性を注入可能な戦略に移さなきゃいけないって分かった。それが逆に良い結果を生むんだよね。if dry_run:をあちこちに渡してたら、マジでやばいことになるよ。正直言うと、テスト実行のために人に--dry-runを実行させるより、プロダクション実行には--wet-runを使った方がずっといい。実際のものを誤って起動する可能性が低くなるからね。

デザインパターンが役立つ場面だね、みんながそれに対して目を roll するけど。

もしできるなら、アプリケーションが取るアクションを明示的にモデル化して、それを実際に処理する中央のものに渡すのがいい方法だよ。そうすれば、ドライランかどうかを理解する必要があるのはコードの一箇所だけになる。理想的には、コアロジックからそれを返すだけで済む、"機能的コア、命令的シェル"スタイルがいいね。

「--wet-run」って音があんまり好きじゃないけど、何度か「dry-run」がデフォルトで、実際に変更を加えるには「--no-dry-run」が必要なツール(たまにサービスも)を作ったことがある。サービスに関しては、どこで動いてるかを自動で検知してほしいな。つまり、開発環境で動いてるなら、デフォルトで開発用のDBを使うって感じ。

毎回「rm --wet-run tempfile.tmp」とか「mkdir -p --yes-really-do-it /usr/local/bin」って打ちたくないんだよね。プログラムは、頼んだことをちゃんとやるのがデフォルトであるべきだと思う。一方で、すべてのツールに「--undo」引数があって、最後にやったことを元に戻せるといいな。

自分がメンテしてる(内部の)CLIでは、REST APIを呼び出すコードの中にif not dry_run:を入れてる。HTTPコールをCURLコマンドとしてログに記録する設定があるから、ドライランモードでは実際に呼び出さずにどんなHTTPコールをするかを確認できるんだ。これがうまくいくのは、CLIコマンドが単一の操作を実行する場合、例えばこのREST APIを呼び出す時だけど、少しでも複雑なことをし始めると、例えばAPI1を呼び出してその結果をAPI2に送るみたいなことになると、かなり難しくなる。もちろん、API1が返す可能性のあるものをシミュレーションすることはできるけど、急にif not dry_run:よりもずっと複雑でエラーが起こりやすいものになるんだよね。

1つの場所(もしくは一般的にそれを制限すること)がその処理をすることで、dry_runチェックがコード全体を汚染するのを防げる。自動化パイプラインでヘッドレスVMが動かすCLIツールをたくさん管理してるけど、ほぼすべてのツールでこれをやってるよ。

コンソールツールにRESTやHTTPにこだわるの、なんでなの?!RESTの肥大化はマジでヤバい。最近の子供たちは、すべてをIP/TCP/httpsで動かしたがるんだよね。なんで?!まずはローカルツールを書くことを学ぼうよ。

逆のやり方も好きだな。-commitや-executeみたいに、デフォルトで実行するのが不変だと仮定することで、バリデーションの複雑さを簡素化して、ライブ実行を明示的にするんだ。

この考え方には、ここ8年くらいずっと偏ってる。誰かが「--commit」を通さなきゃいけないときに間違って何かを変更したってことはまだないけど、「--dry-run」を忘れて何度も間違えて変更しちゃった人は何人も見てきた。

俺は「--wet-run」や「-w」のファンなんだけど、逆のやつがどれだけ深刻か、うざいかによるかな。

アプローチをランダムに混ぜたりしない方がいいよ、そうすると大変なことになるから。

以前使ってたツールの中には、実際に動かすってことを認識するために単語やフレーズを入力しなきゃいけないものがあった。メリットとデメリットはあるけど、間違ってパラメータを使うのが難しくなるから、あれは好きだったな。

パラレルディレクトリの重複排除ツールを使ってて、ハードリンクを利用したこのパターンをそのまま採用してるんだ。デフォルトでは、2つのパラレルディレクトリ構造の間で同じファイルがどれか教えてくれるだけなんだよ。実際にハードリンクでファイルを置き換えたい場合は、--executeフラグを使わないといけない。

ここで学んだことなんだけど、最近作ったスクリプトがあって、ダウンロードしたSharepointの内容を(ローカルだけで)全部削除して、関連するMS365アカウントもコンピュータから削除するんだ。デフォルトでは読み取り専用モードで動くようになってる。変更を許可するには、明示的なフラグを付けて実行しないといけない。さらに、アカウントを実際に削除する前に、DELETE-ACCOUNTと入力して、これが本当に自分の意図だって確認する必要があるんだ。今のところ、誰もクライアントの現場での熱い状況でもミスを犯してないよ。

PowerShellのすごい機能の一つは、関数に「[CmdletBinding(SupportsShouldProcess)]」を追加するだけで「-whatIf」ドライランが使えること。めっちゃ便利だよ。

さらに良いのは、-WhatIfと-Confirmの両方を有効にして、ユーザーの影響閾値の好みと相互作用するShouldProcess関数を提供することだね。めっちゃクールだよ。

このパターン、めっちゃ好きなんだけど、ドライパスのコードが代表的であることが重要だね。print("would have updated ID: 123")みたいなドライコードに何度もやられたことがあるから、ホットパスのほとんどのコードが実行されてないんだよね。実際に実行すると、書き込み操作の準備にバグやエラーがあって、ドライランではあまり意味がなかったりする。言い換えれば、ドライコードはデータベースの書き込みやAPIコールが実際に行われる直前までのことを全部やるべきだよ。早く逃げちゃダメだね。

これってドライランと統合テストを混同してない?私の知る限り、ドライランの目的は何が起こるかを理解することであって、何が起こるかをテストすることじゃないよね。後者はテストがあるし。

状態を持つシステムとやり取りしてると(この手のコマンドではだいたいそうだけど)、--dry-runでもレースコンディションが発生することがあるよ。ツールは現在の状況で何をするか教えてくれるから、それを見て確認するんだ。そして、--dry-runなしで再実行するけど、その時は状況が変わってるかもしれない。だから、Terraformの「プラン」モードのアプローチが好きなんだ。何をするかだけじゃなくて、後でプログラム的に実行できるプランの形で教えてくれるからね。計画中に立てた仮定が変わったら、中止してロールバックできるし。おまけに、このパターンは「if dry_run:」があちこちに散らばる問題に対する良い答えを提供してくれる。計画と実行をコードで分ける必要があるから、単に「すぐに適用」モードを実行するだけで済むんだ。

そのアイデア、いいね!TerraformやAnsibleみたいなアプリケーションには理想的だと思う。でも、この記事のようなものにはプランモードはオーバーキルだと思う。プランモードは、何らかのドメイン特化型言語やデータ構造を作る必要があるから、実行モードがそれを解釈して実行することになるんだよね。データが1日1回しか収集されないレポートツールには、かなりの複雑さが追加されると思う。

そうそう!今、いくつかの敏感なファイルを修正するスクリプトを作ってるんだけど、重要なデータをうっかり失わないようにこのアプローチを取ってるんだ。プロセスを3つのパートに分けたよ:1. ファイルシステムを歩いて、ファイルの現在の状態をキャプチャして、プランをディスクに書き出す。2. ステップ1のファイルの状態が変わってないことを確認してから、プランを実行する。新しいファイルの状態をキャプチャする。その上で、すべての操作をジャーナルにディスクに記録する。3. ステップ1と2でキャプチャしたファイルの状態を使って、データが失われたり予期せず変更されていないかを検証する。操作ログを手動で見たり(またはLLMにダンプしたり)して、何もおかしいところがないか確認する。これらの3つのステップは、3つの別々のスクリプトにすることも、同じスクリプトに3つのフラグを付けることもできるよ。

そんな感じで、コンパイラ(仕様から計画へ)と仮想マシン(計画からアクションへ)を実装している自分に気づくよね!

だから、Terraformの「plan」モードのアプローチが好きなんだ。何をするかだけじゃなくて、後でプログラム的に実行できる計画の形で教えてくれる。計画中に立てた仮定が変わったら、中止してロールバックできるんだ。で、「rm」コマンドでそれをどうやってやると思う?

面白いことに、「molly-guard」っていうツールを思い出したんだ。これは、間違ったUnixサーバーを再起動しようとしたときに問題を解決してくれるやつ。サーバー名を入力するように聞いてくるんだ。間違ったサーバーを再起動したことがある人なら、このツールが素晴らしいって言えるよね。まるで「--dry-run」みたいだけど、「reboot」のための。

こういう障害は、ユーザーが繰り返し行うアクションには効果がないんだ。ポップアップで「本当に削除していいですか?」って聞いてくるダイアログ -> ユーザーは自動的に「はい」をクリックしちゃう。だって、過去10回もそうしたし、仕事を続けたいだけだから。サーバー「alpha」にログインしたのに「delta」だと思ってた。ツールがサーバー名を入力するように聞いてくる。自分がalphaにいるって知ってるから「alpha」って入力する。間違ったサーバーを再起動しちゃう。Githubは、削除する前にリポジトリ名を入力して確認するように求めてくる。ユーザーはリポジトリ名を見て、考えずに入力する。あるいは、怠け者の僕みたいに、表示された名前を選択してドラッグしてフィールドに入れるから、入力すらしない。要するに、ユーザーはアクションを始めた時点で既にそれを決めてるんだ。彼らを一貫して立ち止まらせて再評価させるのはほぼ不可能で、すごく手間がかかって面倒なんだよね。彼らはその摩擦をできるだけ効率的に回避する方法をすぐに学ぶ(つまり、考えずに)。より良い解決策は、ただやっちゃうことだけど、間違ってたらユーザーが元に戻せるようにすること(もちろん、いつも可能とは限らないけど)。

僕は「--really-do」が好きだな。ツールのデフォルトの動作は何もしないってこと。これだと、「--dry-run」を追加するのを忘れたときに、より耐障害性があるから。

欠点は、dryRunフラグがコードを少し汚してしまうことだね。主要なフェーズごとに、フラグが設定されているか確認しなきゃいけなくて、実際に行動を起こすんじゃなくて、取る予定のアクションだけを表示することになる。状態遷移パターンのケースみたいだね。