段階的自動化の鍵、Do-nothingスクリプティング(2019)
(blog.danslimmon.com)- 運用チームには、インフラ変更やアカウントプロビジョニングのように完全にはなくしにくい**手作業の手順(toil)**が残り、会社が成長するほど工程や例外が増えていく
- 各工程は自動化できそうに見えても、一部だけをスクリプト化すると単一用途のツールばかりが増え、利用者は依然として長い手順書に従わなければならない
- Do-nothingスクリプトは実際の作業を自動実行せず、手順の各工程を関数で包んだうえで、利用者に一段階ずつ指示する
- 利用者は現在位置を見失ったり工程を飛ばしたりする可能性が減り、開発者は後から特定工程の案内文を実際の自動化コードに置き換えやすい
- 当面の手作業は減らないが、自動化の初期コストを下げることで、時間をかけてtoilを段階的に取り除ける
手作業の手順が苦役になる瞬間
- どの運用チームにも、まだ自動化できていない手作業の手順があり、toilを完全になくすのは難しい
- 成長する会社では、インフラ変更やユーザーアカウントのプロビジョニングのような手順が大きなtoilの中心になりやすい
- ユーザーアカウントのプロビジョニング手順は、たとえば次のような複数の工程をたどることがある
- ユーザーのSSHキーペアを生成
- 公開鍵をGitにコミットしてmasterへプッシュ
- ビルドジョブの完了を待機
- 社員ディレクトリでユーザーのメールアドレスを確認
- 1Passwordでユーザーに秘密鍵を渡す
- 実際の環境では手順が20工程まで増えたり、進行中に分岐や特殊ケースを追い続けなければならなかったりする
- こうした作業は高い集中力を要するが、面白い問題解決というよりチェックボックスをもう1つ埋めることに近く、単調な苦役になってしまう
部分的な自動化が残す隙間
- 単調な苦役は、自動化に向いた対象に見える
- 各工程を自動化する方法は簡単に思いつく
- コンピュータは人間より速く正確に指示を実行できる
- 実務ドリフト(practical drift)が起こる可能性も低い
- 問題は、こうした苦役の自動化がしばしば全部かゼロかのように感じられる点にある
- 2工程目や5工程目だけを処理するスクリプトは作れても、手順全体の煩雑さはあまり減らない
- 単一用途のスクリプトが増えると、各ツールごとに慣習や期待される動作が異なり、利用者は依然として複数工程の文書を追わなければならない
Do-nothingスクリプトの仕組み
- ほとんどすべての単調な苦役は、Do-nothingスクリプトに変えられる
- 核心は、苦役の指示をコードに落とし込み、各工程を1つの関数としてカプセル化することにある
- 例示されたスクリプトの流れは次のとおり
CreateSSHKeypairStepはssh-keygenコマンドを表示し、利用者がEnterを押すまで待つGitCommitStepは公開鍵をGitリポジトリにコピーしたうえで、git commit、git pushを実行するよう案内するWaitForBuildStepはビルドジョブのURLで完了を待つよう案内するRetrieveUserEmailStepはディレクトリからメールアドレスを探して入力させ、context["email"]に保存するSendPrivateKeyStepは1Passwordで秘密鍵のドキュメントを作成し、そのメールアドレスの利用者と共有するよう案内する
- このスクリプトは手順のどの工程も実際には実行せず、利用者に一段階ずつ指示したあと、手動での完了を待つ
自動化へつながる道筋
- Do-nothingスクリプトは一見すると文書を読むのを難しくしただけのように見えるかもしれないが、実際には作業フローをより安全に支えてくれる
- 利用者が進行位置を見失ったり工程を飛ばしたりする可能性が低くなる
- 集中を保ったまま苦役を最後まで進めやすくなる
- 各工程が関数として分離されるため、特定工程のテキストを実際の動作コードに置き換えられる
- 時間がたつにつれて再利用可能な工程ライブラリができ、その後の自動化作業がより効率的になる
- Do-nothingスクリプト自体は、チームの手作業の量を減らさない
- 効果は、自動化を始めやすい状態を作ることにあり、チームはそれを足がかりに時間をかけてtoilを取り除いていける
1件のコメント
Hacker News の意見
このアプローチは本当に好き
全体として見ると、プロセスの周辺にインターフェースを定義する別の方法でもある。そのプロセスは手作業かもしれないし自動化されているかもしれないが、インターフェースはそのまま維持できる。だから手順を自動化するときにかなり強力
ほかのシステムに対してやるのと同じように適用すればいい
以前、Google Sheets を手で埋めていたものをスクリプトで自動化したことがあり、Jira チケットを作ると自動で取り込んで処理するようにしたこともある。より早く始められ、いちばん面倒な部分だけを自動化でき、必ずしも全体を自動化する必要もない
何もしないスクリプトの副次的な効果は、ドキュメントより実際に使われる可能性が高いため、より頻繁に最新状態に保たれうる点にある
よくある作業を自動化するスクリプト一覧そのものには賛成。ただ、そのスクリプト群が運用サービスから呼び出され始めた瞬間、そのスパゲッティ状の網をほどかなければならない不運な人に巨大な技術的負債が生まれる
さらに言えば、人間中心のプロセスがシステムをコード化してしまったため、ソフトウェアシステムに有利な変更、たとえばシステムをコンポーネントに分割することが不可能になる。結局、段階的な拡張はプロセスへさらに多くのものを押し込む形になり、それがスクリプトのモノリスに継ぎ足され続ける自己増殖の循環になる
3 と 4 を進めている間は、プロセスを反復的に、できるだけ多くの人に使ってもらい、実際の現場で見える誤用に合わせてプロセスを調整する
4) プロセスを自動化する
誰もが 1 と 2 には同意し、たいていは 4 にも最終的についてくるが、3 をやると 4 をより強く求め、理解する人が増える
ただしこのアプローチの限界は、プロセスの始まりや終わりを自動化できるなら問題ないが、途中に島のように孤立した自動化ポイントが 2 つあると、プロンプト付きのコマンドラインツールと大差ない点にある
やるべきことを説明し、ボイラープレートや例、パラメータ化された実行ファイルを提供すればいい
Notebooks を事実上のドキュメントシステムとして使うのはよさそう。ただ、Confluence のようなクラウド SaaS の上にレイヤーとして載せるには重すぎるし、今の形では権限昇格のような問題が起きる余地も多すぎる
このアプローチは Confluence の案内ページを半分インタラクティブなウォークスルーに変える。これは意識的な側だ
もう一方はテスト自動化。最初は過剰適合した XPath の塊と、ドキュメント化されていない手順から始まる。アプリケーションが変わるたびに、その手順を再発見することになる。これは無意識的な側だ
願わくば頂上に到達して高速に実行しつつ、どうやってそこまで来たのか、なぜそうなったのかをなお理解していられるといい
ユーザーには
Execute command (y/N)?のように確認すればいいあるターミナルから別のターミナルへコピー&ペーストするのは本当に嫌いだ。それも集中力をかなり奪う
まだコマンド化されていない手作業だけをユーザーに尋ねればいい:
Look up the e-mail address for foo. Paste it here:またはPut that shit in 1Password: Are you done (y/N)?興味深いアプローチ
ただし例に挙げられている作業は、その会社の SSH キー配布方法がどれほど安全でなかったかを示しているだけ。実際には、ユーザーが自分で秘密鍵を生成し、公開鍵だけをシステム管理者に渡してアクセス権を付与してもらうように教えるべきだ。どの時点でもシステム管理者が秘密鍵のコピーを持つべきではない。一時的であってもだ。だから 1Password の手順自体が不要であるべき
ちなみに私は、GitHub アクセス専用の SSH キー生成と、そのコンテキストの SSH 設定を自動化するツール github-keygen の作者
https://github.com/dolmen/github-keygen
ユーザーが自分で秘密鍵を生成し、公開鍵だけをシステム管理者に渡すべきだという手順は、いつも実装するのが面倒だった私たちは SSH で証明書ベース認証へ移行し、もう公開鍵をあちこちに移さなくなった。プロセス全体が本当に単純になった
この記事が最初に公開されてから1年ほど経って、結局このやり方を試してみた。
私たちのツールチェーンのバグのせいで、ホットフィックス用のランブックは通常のリリースプロセスの2倍ほど複雑だった。
それほど評価はされなかったが、以前は Sev 1 の問題や終盤のエピック作業で10週に1回くらいしか使われなかったものが、平均で週1回、多い週には3回まで使われるようになった。すべてをフィーチャートグルにする必要がなくなり、技術的負債にずっと深く踏み込めるようになった。
小さな会社で、本番データをプリプロダクション環境へ複製しやすいなら、こういう結果は見られないかもしれない。だが、私たちが通信していたエンドポイントは150以上あり、サービスあたり平均3つほどだったと思う。データセットは非常に多く、その一部は Kafka が存在する前の Kafka のような方式で収集されていた。
本番データを複製しようとしていた人は1人だけで、その人も時間とエネルギーが足りず、年に1、2回しかできなかった。そのペースは顧客や機能が変化する速度よりずっと遅かった。結局、ブルーグリーンデプロイのプロセスと jmeter をいじりながら、どれだけ近づけたのか、本番前に成功/失敗をどう測るのかを見極めるしかなかった。
結局、人々の前に立ちはだかっていたのは細かくてエラーが起きやすいビルドプロセスで、私がそれを半分自動化してようやく解消された。
その後、利用が増えるにつれて手動ステップ内のすべての URL を探し出してツール内のルックアップテーブルに入れ、リリース承認用の通常の検証プロセスでも表示するようにした。おかげでコーディネーターは少し速く、ストレスも少なくなった。そのプロセスはあまりに面倒だったので、負担を分けるために3チームが交代で担当していた。
「作業自動化の活性化エネルギーを下げる」というのは、結局何もしないスクリプトが後で実際に何かを自動化するステップを持つようになる、という意味なのか?
将来の自動化のためのプレースホルダーと見れば、自動化と効率の間の適切なバランスのように感じる。投資しすぎずに最初の試みができ、後で努力の価値がより明確になったときに片付ける、手の届きやすい成果を残しておける。
手順の各ステップは今や関数としてカプセル化されているので、特定のステップのテキストを、実際の動作を自動で行うコードに置き換えられる。
class Foo(object): def run(self, context): ...実行メソッドを1つだけ持つオブジェクトは、すでに Python に組み込まれている。それは関数だ。
def foo(context): ...関数のアプローチでは、新しい関数が必要になるとグローバルレベルに追加することになる。1つや2つならまったく問題ないが、もっと増えると同じ階層に関数がずらっと並び、それらの依存関係がもはや明確でなくなる。
そのうちの1つでは、不必要に抽象化されたコードを単純化する例を扱っている。
素晴らしいが、中断できない。
あらかじめすべてのステップを見せて、進行しながら各項目にチェックを付けてくれるとよい。時には、より広い視点から実際に準備する方がよいこともある。
要約をファイルにログとして残すこともできる。
改善できることがあまりに多いので、むしろ最も単純な解決策が最善なのかもしれない。
ただ、何もしないシェルスクリプトは始めるのがあまりにも簡単なので、最後まで作らずに終えるのが難しい。その労力は、むしろステップを1つ自動化することに使った方がよいかもしれない。どの TUI ライブラリを使うか、構造をどう組むかといった、面白いがあまり生産的ではない沼にはまる可能性がある。
各ステップは
*.doneという名前のルールで、完了すると.doneファイルを生成する。いつでも中断し、何かを直すためにスクリプトを修正してからmakeで再開できる。ただ、その Makefile を書くのは本当に苦痛だ。もっと良い解決策はあるだろうか?
./do-the-thing.sh 2025のように実行し、2025 ディレクトリを作ってどこまで進んだかの状態を保存する。最初のステップを承認したら、
2025/first-stepファイルをtouchできる。スクリプトが落ちたり中断されたりした後に再実行されると、そのファイルを確認して最初のステップをスキップする。何かが変わって自動化が動かないとき、状態を失わずに抜け出し、スクリプトを直してから再実行できるとよい。
たいていは、スクリプトが次の手動ステップを1つだけ知らせて終了するようにしている。そうすればターミナルを別の作業に使える。コマンド履歴からスクリプトを簡単に再実行できる。
以前の議論もある。コメントが多い。
https://news.ycombinator.com/item?id=29083367 - 3年前、コメント230件
https://news.ycombinator.com/item?id=20495739 - 6年前、コメント124件
このアプローチをどれほど気に入っているかは、いくら強調してもしすぎることはない。
複数のプロジェクトでうまく適用できた。いちばん気に入っている例は、「人的要因」のせいでラボテストに失敗していた3,000万ドルの手術ロボットだ。
私は別分野の法律実務にいるが、このアプローチを自分の会社にどう適用できるか考えてみたい。
このアプローチはよい。一定以上の複雑さを持つプログラミング言語ベースのシステムでも、すでに似たようなやり方をするのが好きだ。関数型プログラミングの分野では、これを穴(holes)と呼ぶようだ
インターフェースの
not implementedエラーも似た論理に従っているが、相互に依存する複数の部品それぞれについて、意味はないが有効な出力を返す些細なものを書く価値はかなり大きいと思う。そうすれば、それらの部品を作る過程が速くなり、一度に1つずつテストして作る可能性がずっと高くなる。テストを始める前に複数の部分をまとめて書かなければならない必要が減る記事で扱われているユースケースのように、スクリプトの文脈ではこうした型レベルの妥当性が重要になりにくい場合もある。コマンドラインの効果は、関数型の観点ではかなりの部分が副作用だろうからだ。だが、逐次的に進むという点から影響ははるかに小さく、待機プロンプトのおかげで、いずれにせよスクリプトなしでも行う必要がある手動ステップを少しのコストで維持しつつ、作業順序を保てる
足場は不完全だが、根本的に有用だ
理論的にはよいが、実際には難しそうだ
運用チームが同じ作業を繰り返していて、何もしないスクリプトが本当に何もしないのを見れば、手順を覚えたと思ったり、手動でやるほうが速い、あるいは面白いと感じた瞬間に、すぐ使わなくなるだろう
運用チーム向けの自動化や文書を多く書いてきたが、人々にそれを使わせ、使い続けてもらうことは常に課題だった。文書変更にも告知が必要だった。人は何かのやり方を覚えると、すぐ文書を読まなくなる
完璧な世界なら、このアプローチは非常に妥当で、個人的な作業には自分も使えそうだ。だが現実はほとんど完璧ではない。自分なら、90%が自動化されていて、まだ解決できていないステップが1つ残っている場合にだけこの方式を使うと思う。その場合でさえ、運用チームの一部はその手動ステップを飛ばし、全体が自動化された魔法だと仮定してしまうかもしれない