LabHub

ブログ

アップデート可能なソリューションの作り方:インストールウィザードから安全な自動更新まで

한국어English日本語中文

はじめに — インストールは始まりにすぎない

ソフトウェアをユーザーに届けた瞬間から、本当の問題が始まります。最初のインストールは一度きりですが、アップデートは製品が生きているあいだずっと続きます。バグを直し、機能を足し、セキュリティの穴を塞ぐには、すでに配布された何千・何万台ものインストール済みを安全に新バージョンへ入れ替えなければなりません。

この記事は二つのことを一緒に扱います。ひとつはユーザーが最初に出会うインストールウィザード(wizard)のUXで、もうひとつはその裏で静かに動く自動更新のメカニズムです。デスクトップアプリでもモバイルアプリでもWeb PWAでも、「アップデート可能なソリューション」を作る原理は驚くほど似ています。ダウンロードし、検証し、アトミックに入れ替え、失敗したら元に戻す。この四つのステップがすべてです。

パート1. ウィザードのUX

マルチステップの流れとステップごとの検証

ウィザードは複雑な設定を複数のステップに分けたUIです。ひとつの画面にすべてのオプションを詰め込む代わりに、ユーザーを一歩ずつ案内します。よいウィザードには共通の骨格があります。

プレビュー、ドライラン、失敗時のロールバック

インストールや更新はシステムを変える作業です。ユーザーが「適用」を押す前に何が変わるかを見せると、信頼が高まります。

再開可能性

大きなファイルをダウンロードする途中でネットワークが切れたり、ユーザーがうっかりウィンドウを閉じたりします。そのたびに最初からやり直させるとユーザーは疲弊します。再開可能性(resumability)は、中断した地点の状態を保存しておき、再び開いたときにその地点から続ける能力です。ダウンロードはHTTP Rangeリクエストで再開し、ウィザードの入力値はローカルに一時保存します。

パート2. 更新のメカニズム

セマンティックバージョニングと更新チャネル

更新を管理するには、まずバージョンを体系的に付ける必要があります。セマンティックバージョニング(SemVer)はバージョンをMAJOR.MINOR.PATCHの三つに分けます。互換性が壊れればMAJOR、機能が追加されればMINOR、バグだけ直せばPATCHを上げます。クライアントはこのルールから「この更新が安全な小さな修正か、注意すべき大きな変更か」を判断できます。

そこに更新チャネル(update channel)を加えます。同じ製品でも stable(安定)、beta(ベータ)、nightly(毎日ビルド)のように複数の流れを用意し、リスクを取れるユーザーにだけ先に新バージョンを流します。ほとんどのユーザーは stable に置き、アーリーアダプターは beta に移します。

フル更新 vs デルタ/差分更新

もっとも単純なのは、新バージョン全体をまるごとダウンロードして入れ替えるフル更新(full update)です。実装は簡単ですが、アプリが数百MBなら一行直しても毎回数百MBをダウンロードすることになります。

デルタ/差分更新(delta/differential update)はこの無駄をなくします。旧バージョンと新バージョンの差分(diff)だけをダウンロードしてローカルで合成します。代表的なツールがあります。

デルタ更新は帯域を大きく節約しますが、パッチは「旧バージョンが正確にその版であるとき」だけ当たります。そのため通常は複数のバージョンから最新へ向かう複数のパッチを用意するか、デルタが失敗したらフル更新へ自動フォールバックするようにします。

アトミックな入れ替えとロールバック

更新でもっとも危険な瞬間は、ファイルを実際に入れ替えるときです。古いファイルを消して新しいファイルを書く途中で停電したら?アトミックな入れ替え(atomic swap)がこの問題を防ぎます。

要は「完全に準備してから最後に一度だけ切り替える」ことです。新バージョンを横にまるごとインストールしておき(例:v2フォルダ)、検証が終わったら「現在のバージョン」を指すポインタ(シンボリックリンクや設定)をv1からv2へ一度に切り替えます。ファイルシステムのアトミックなrenameを使えば、どの瞬間でもユーザーは完全なv1か完全なv2を見ます。半分混ざった状態は存在しません。問題が起きたら、ポインタをv1に戻すだけでロールバック(rollback)が終わります。

  /app/current  ->  /app/versions/1.4.0   (アトミックな rename で切り替え)
                    /app/versions/1.5.0   <- 事前インストール + 検証済み

  切り替え後:
  /app/current  ->  /app/versions/1.5.0
  失敗時は current ポインタを 1.4.0 に戻せばロールバック完了

段階的/カナリアロールアウト

新バージョンをすべてのユーザーに同時に押し出すと、隠れていたバグが全体を覆います。段階的/カナリアロールアウト(staged/canary rollout)はこれを防ぎます。まず1%だけに配布し、エラー率と指標を観察します。問題がなければ5%、25%、100%へ徐々に広げます。炭鉱のカナリアのように、少数のユーザーが先に危険を察知してくれるわけです。異常が見えたら即座にロールアウトを止めるか戻します。

バックグラウンドダウンロード、再起動時に適用

ユーザーを止めてプログレスバーを見つめさせるのは悪い体験です。成熟したアップデーターはバックグラウンドで静かにダウンロードし、準備が終わったら「次に再起動したときに適用されます」とだけ知らせます。そしてユーザーがアプリを再び開くとき、すでに取得済みの新バージョンへなめらかに切り替えます。Chrome、VS Code、macOSの多くのアプリがこの方式を使います。

パート3. 完全性とセキュリティ

更新チャネルは攻撃者にとって魅力的な標的です。もし偽の更新を押し込めれば、攻撃者はユーザーのすべての端末でコードを実行できます。だから完全性とセキュリティは選択肢ではなく必須です。

コード署名と署名・ハッシュの検証

コード署名(code signing)は、開発者の秘密鍵で配布物にデジタル署名を付けることです。クライアントは公開鍵でその署名を検証し、このファイルが本当にその開発者から来て、途中で改ざんされていないことを確認します。

鉄則は適用する前に検証(verify before applying)することです。ダウンロードしたパッチのハッシュと署名をまず確認し、通った場合にのみインストールを進めます。順序が逆になると(先に適用して後で検証)もう手遅れです。

TUFのアイデア

TUF(The Update Framework)は、更新システムを狙った特殊な攻撃にまで対処するよう設計されたフレームワークです。単一の署名鍵ひとつではなく、役割を分けた複数の鍵(root、targets、snapshot、timestamp)を持ち、鍵ひとつが漏れても全体が崩れないようにします。核心となるアイデアをいくつか取り入れるだけでも大いに役立ちます。

HTTPS、ピンニング、ダウングレード防止

パート4. エコシステムのツール

車輪を再発明する必要はありません。プラットフォームごとに成熟した更新ツールがあります。

パート5. 更新時のデータマイグレーション

新バージョンに変わるのはコードだけではありません。ローカルのデータベースや設定ファイルの形式(schema)も一緒に進化します。ここでよく事故が起きます。

例:小さな更新マニフェスト

サーバーがクライアントに「最新バージョンは何で、どこから受け取り、ハッシュは何か」を伝えるマニフェストは、たいていこのような形です。

{
  "channel": "stable",
  "latest": "1.5.0",
  "minimumSupported": "1.2.0",
  "releasedAt": "2026-07-03T09:00:00Z",
  "artifacts": [
    {
      "platform": "darwin-arm64",
      "url": "https://updates.example.com/app/1.5.0/app-arm64.zip",
      "sha256": "9f2c1b7e0a4d6f8b3c5e7a9d1f2b4c6e8a0d2f4b6c8e0a2d4f6b8c0e2a4d6f8b",
      "size": 41231884,
      "signature": "MEUCIQD...base64-signature..."
    },
    {
      "platform": "win32-x64",
      "url": "https://updates.example.com/app/1.5.0/app-x64.nupkg",
      "sha256": "1a3c5e7b9d0f2a4c6e8b0d2f4a6c8e0b2d4f6a8c0e2b4d6f8a0c2e4b6d8f0a2c",
      "size": 39882140,
      "signature": "MEQCIF...base64-signature..."
    }
  ],
  "delta": {
    "from": "1.4.0",
    "url": "https://updates.example.com/app/1.4.0-to-1.5.0.patch",
    "sha256": "7b9d1f3a5c7e9b0d2f4a6c8e0b2d4f6a8c0e2b4d6f8a0c2e4b6d8f0a2c4e6b8d"
  }
}

クライアントはこのマニフェストをHTTPSで受け取り、minimumSupportedでダウングレードを防ぎ、アーティファクトをダウンロードしてからsha256signatureを検証し、通ればアトミックに入れ替えます。

例:サービスワーカーの更新

PWAではサービスワーカーが「インストールされたアプリ」のようにキャッシュされるため、新バージョンをユーザーに知らせてなめらかに切り替える流れが必要です。以下は、新しいサービスワーカーが待機状態になったらユーザーに知らせ、同意すれば即座に有効化する最小の例です。

// ページ側のコード
navigator.serviceWorker.register("/sw.js").then((reg) => {
  reg.addEventListener("updatefound", () => {
    const newWorker = reg.installing;
    newWorker.addEventListener("statechange", () => {
      // 新しいワーカーがインストールを終え、既存のワーカーがまだページを制御中なら
      if (newWorker.state === "installed" && navigator.serviceWorker.controller) {
        // ユーザーに「新バージョンが準備できました」バナーを見せるタイミング
        showUpdateBanner(() => {
          // ユーザーが「今すぐ更新」を押したら待機中のワーカーを即座に有効化
          newWorker.postMessage({ type: "SKIP_WAITING" });
        });
      }
    });
  });
});

// コントローラーが変わったら(=新しいワーカーが有効化されたら)一度だけリロード
let refreshing = false;
navigator.serviceWorker.addEventListener("controllerchange", () => {
  if (refreshing) return;
  refreshing = true;
  window.location.reload();
});
// sw.js — サービスワーカー側のコード
self.addEventListener("message", (event) => {
  if (event.data && event.data.type === "SKIP_WAITING") {
    // 待機状態を飛ばして、このワーカーを今すぐアクティブなワーカーに昇格させる
    self.skipWaiting();
  }
});

self.addEventListener("activate", (event) => {
  // 有効化されたらすぐに、開いているすべてのタブの制御権を取る
  event.waitUntil(self.clients.claim());
});

意思決定チェックリスト

新しいプロジェクトで更新戦略を決めるとき、自分に問いかけるとよい質問です。

おわりに

アップデート可能なソリューションの本質は、実のところ単純です。ダウンロードし、検証し、アトミックに入れ替え、失敗したら元に戻す。 そこに、ユーザーを気づかうウィザードのUX(ステップごとの検証、プレビュー、再開)、安全に広げるロールアウト(カナリア、チャネル)、攻撃を防ぐ完全性(コード署名、TUF、ダウングレード防止)、そしてデータの進化を扱うマイグレーション(冪等・順次・互換性)を重ねればよいのです。

プラットフォームごとに良いツールがすでにあります。Sparkle、Squirrel、electron-updater、Tauri、サービスワーカー、OSパッケージマネージャ。大切なのは、これらの原理を理解し、自分の製品のリスクと規模に合う組み合わせを選ぶことです。インストールは始まりにすぎず、良い更新体験こそが製品を長く生かします。

参考資料

コメント

まだコメントはありません。

ログインするとコメントできます