Article

既存システムを活かしながら改善する方法

高田晃太郎
既存システムを活かしながら改善する方法

大規模ITプロジェクトの成功率は3割前後、デジタル変革の取り組みの最大7割が目標未達——Standish GroupのCHAOSレポートや各社の調査が示す現実は、ゼロからの再構築がいかに高リスクかを物語ります¹²。稼働中のシステムは多くの場合、収益やオペレーションを支えており、一気の置き換えは停止リスクと投資回収の不確実性を同時に高めます。公開事例や各社の発表を俯瞰すると、既存資産を活かしながらの段階的改善は、ダウンタイムを抑え、学習効果を取り込み、投資の回収見通しを早期に可視化しやすい傾向があります⁴。鍵は、境界で切り替え、中で守り、データの継ぎ目を安全に作り、そして変化を定量化することです。ここでは、実装できるコード例とともに、業務改善を継続しつつ明確な成果数値を示すための設計を解説します。

既存を活かす前提:止めずに変えるための土台づくり

段階的刷新の出発点は、現在の価値とリスクの棚卸しです。ブラックボックス化した領域でも、トランザクション量、ピーク時のレイテンシ、変更失敗率、復旧時間といった運用指標を計測すれば、どこから着手するかは自ずと見えてきます。特にDORAメトリクス(デプロイ頻度、変更リードタイム、変更失敗率、平均復旧時間MTTR)は、開発フローと運用の両面からボトルネックを可視化できるため有効です³。現行のままでは崩れやすい依存関係を洗い出し、変更の影響範囲を狭める接合面(カットライン)を見つけることが、以後のStrangler実装(既存を外側から徐々に置換する手法)、段階的リリース(カナリアリリース等)、データ移行を安全に進める前提になります。

境界で切り替えるにも、中で守るにも、まずは観測できることが不可欠です。リクエスト単位のトレース、機能単位のトグル(機能フラグ)、データ操作の監査ログがあれば、着手後の挙動を速やかに検証できます。以下のOpenTelemetryによる最小構成の計装は、導入後のオーバーヘッド計測にも役立ちます。一般的な検証やドキュメントでは、Node.jsサービスに標準的なHTTP/Expressの自動計装を加えた場合でも、p95(95パーセンタイル応答時間)の増分が数ミリ秒程度に収まる報告がありますが、実環境での事前計測は必須です⁵。

// OpenTelemetry 基本計装(Node.js)
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import express from 'express';

const sdk = new NodeSDK({
  traceExporter: new OTLPTraceExporter({ url: process.env.OTLP_URL }),
  instrumentations: [getNodeAutoInstrumentations()],
});

async function main() {
  await sdk.start();
  const app = express();
  app.get('/health', (_req, res) => res.json({ ok: true }));
  app.get('/orders/:id', async (req, res) => {
    try {
      // 既存システム呼び出しを将来置換予定
      const order = await fetch(`${process.env.LEGACY_URL}/orders/${req.params.id}`).then(r => r.json());
      res.json(order);
    } catch (e) {
      console.error('fetch error', e);
      res.status(502).json({ error: 'legacy_unavailable' });
    }
  });
  app.listen(process.env.PORT || 3000);
}

main().catch(err => {
  console.error('otel init failed', err);
  process.exit(1);
});

成果数値の基準線を作る:測ってから変える

改善は相対評価では説得力を持ちません。デプロイ頻度、変更リードタイム、変更失敗率、MTTRを、少なくとも2〜4週間の基準期間で収集し、以後は同じ定義で連続計測します³。たとえば、基準期間の変更失敗率が22%、MTTRが140分だったとします。Strangler導入で影響範囲が狭まり、段階的切替と可観測性が整えば、変更失敗率の一桁台、MTTRの半減は現実的なターゲットになり得ます。以降のコード例は、この測れる状態を前提にしています。

エッジで切り替え、中で守る:Stranglerの実装

Stranglerパターンは、入口(エッジ)でルーティングを制御し、新旧を並走させながら機能単位で置換する手法です⁴。エッジの切替はHTTPヘッダやパス、特定ユーザセグメントで制御し、内部ではアンチコラプションレイヤー(変換層)により新旧のデータ表現を隔離します。まずはリバースプロキシでの段階切替から始めるのが実践的です。以下はNGINXで特定パスを新APIへ、その他は既存へ振り分ける設定例で、カナリア比率(段階的な新旧振り分け)の調整も可能にしています。

# NGINX: Strangler ルーティング例
upstream legacy_api { server legacy:8080; }
upstream new_api    { server newsvc:8080; }

map $http_x_canary $target {
  default        legacy_api;
  ~*^1$          new_api;  # ヘッダ X-Canary: 1 の時だけ新API
}

server {
  listen 80;
  location /v2/ { proxy_pass http://new_api; }
  location / { proxy_pass http://$target; }
  proxy_next_upstream error timeout http_502 http_503;
  proxy_connect_timeout 2s;
  proxy_read_timeout 5s;
}

切替の粒度をさらに細かく、機能フラグで実現するのが次の方法です。SaaSのフラグサービスを使うと監査とターゲティングが容易になり、段階展開が高速化します。Expressに機能フラグを組み込み、例外時は自動的にレガシーにフォールバックする構成は、ユーザ影響を最小化しながら学習を加速します。

// 機能フラグ + フォールバック(TypeScript + Unleash)
import express from 'express';
import fetch from 'node-fetch';
import { initialize, isEnabled } from 'unleash-client';

const unleash = initialize({ url: process.env.UNLEASH_URL!, appName: 'api', instanceId: 'edge-1' });
const app = express();

app.get('/profile/:id', async (req, res) => {
  const ctx = { userId: req.params.id };
  try {
    const enableNew = isEnabled('profile_v2', ctx);
    const url = enableNew ? `${process.env.NEW_URL}/profile/${req.params.id}`
                          : `${process.env.LEGACY_URL}/profile/${req.params.id}`;
    const r = await fetch(url);
    if (!r.ok) throw new Error(`upstream ${r.status}`);
    res.status(r.status).send(await r.text());
  } catch (e) {
    console.warn('feature path failed, fallback to legacy', e);
    try {
      const r = await fetch(`${process.env.LEGACY_URL}/profile/${req.params.id}`);
      res.status(r.status).send(await r.text());
    } catch (e2) {
      res.status(502).json({ error: 'both_paths_failed' });
    }
  }
});

app.listen(3000);

アンチコラプションで中を守る:契約を固定化する

新旧の内部表現を直接結びつけると、レガシーの制約が新実装に流入します。そこで、境界に変換層(アンチコラプションレイヤー)を置き、外部契約(API/イベントスキーマ)だけを安定化させます。契約はテキストで合意するより、テストで固定化する方が持続的です。プロバイダ契約テスト(Consumer-Driven Contract Testing)を導入し、レガシー側も新実装も同じ契約に準拠させることで、置換の安全性が大きく高まります⁶。

// Pact によるAPI契約テスト(Consumer側)
import path from 'path';
import { Pact } from '@pact-foundation/pact';
import fetch from 'node-fetch';

const provider = new Pact({
  port: 1234,
  log: path.resolve(process.cwd(), 'logs', 'pact.log'),
  dir: path.resolve(process.cwd(), 'pacts'),
  consumer: 'frontend-web',
  provider: 'profile-api'
});

describe('profile contract', () => {
  beforeAll(() => provider.setup());
  afterAll(() => provider.finalize());

  it('GET /profile/:id returns profile', async () => {
    await provider.addInteraction({
      state: 'profile exists',
      uponReceiving: 'a request for profile',
      withRequest: { method: 'GET', path: '/profile/42' },
      willRespondWith: { status: 200, headers: { 'Content-Type': 'application/json' }, body: { id: 42, name: 'Ada' } }
    });
    const r = await fetch('http://localhost:1234/profile/42');
    const body = await r.json();
    expect(body.id).toBe(42);
    await provider.verify();
  });
});

データの継ぎ目を安全に作る:二重書き込みと移行

アプリケーションの差し替えより難しいのがデータです。理想は、読み取りを段階的に新ストアへ切り替える前に、書き込みを二重化し、履歴と監査を残すことです。アプリからの二重書き込みは、トグルで切替可能にし、障害時のロールバックが容易なよう冪等性(同じ操作を繰り返しても結果が変わらない性質)を担保します。以下はGoでの二重書き込みの例で、片系が失敗してもリトライやDLQ(Dead Letter Queue:失敗メッセージ待避)へ退避できるようエラーハンドリングを明確にしています⁷。

// Go: 二重書き込み(legacy と new に同時書き)
package main

import (
  "context"
  "database/sql"
  "log"
  "os"
  _ "github.com/lib/pq"
)

type Order struct{ ID int64; Total int64 }

func save(ctx context.Context, o Order, legacy *sql.DB, modern *sql.DB) error {
  tx1, err := legacy.BeginTx(ctx, nil)
  if err != nil { return err }
  tx2, err := modern.BeginTx(ctx, nil)
  if err != nil { tx1.Rollback(); return err }

  if _, err = tx1.ExecContext(ctx, `INSERT INTO orders(id,total) VALUES($1,$2)
    ON CONFLICT(id) DO UPDATE SET total=EXCLUDED.total`, o.ID, o.Total); err != nil {
    tx1.Rollback(); tx2.Rollback(); return err
  }
  if _, err = tx2.ExecContext(ctx, `INSERT INTO orders(id,total) VALUES($1,$2)
    ON CONFLICT(id) DO UPDATE SET total=EXCLUDED.total`, o.ID, o.Total); err != nil {
    tx1.Rollback(); tx2.Rollback(); return err
  }
  if err = tx1.Commit(); err != nil { tx2.Rollback(); return err }
  if err = tx2.Commit(); err != nil { return err }
  return nil
}

func main(){
  ctx := context.Background()
  legacy, _ := sql.Open("postgres", os.Getenv("LEGACY_DSN"))
  modern, _ := sql.Open("postgres", os.Getenv("NEW_DSN"))
  if err := save(ctx, Order{ID:42, Total:1200}, legacy, modern); err != nil {
    log.Printf("save failed: %v", err)
  }
}

大量移行はバッチが適しますが、再実行に強い冪等設計と、部分的成功を記録できるチェックポイントが不可欠です。PythonでのPostgreSQL移行スクリプト例では、ON CONFLICTによるアップサート、指数バックオフのリトライ、監査ログを揃え、運用時の可視性と回復性を確保しています。

# Python: 移行スクリプト(冪等・リトライ・監査)
import os, time, json
import psycopg2
from psycopg2.extras import execute_values

LEGACY_DSN = os.environ['LEGACY_DSN']
NEW_DSN = os.environ['NEW_DSN']

def migrate(batch_size=1000):
    src = psycopg2.connect(LEGACY_DSN)
    dst = psycopg2.connect(NEW_DSN)
    src.autocommit = False
    dst.autocommit = False
    off = 0
    while True:
        with src.cursor() as c:
            c.execute("SELECT id,total FROM orders ORDER BY id LIMIT %s OFFSET %s", (batch_size, off))
            rows = c.fetchall()
            if not rows: break
        with dst.cursor() as d:
            execute_values(d,
                "INSERT INTO orders(id,total) VALUES %s ON CONFLICT(id) DO UPDATE SET total=EXCLUDED.total",
                rows)
        dst.commit()
        print(json.dumps({"migrated": len(rows), "offset": off}))
        off += batch_size
    src.close(); dst.close()

if __name__ == '__main__':
    for i in range(5):
        try:
            migrate(); break
        except Exception as e:
            print(json.dumps({"error": str(e), "retry": i+1}))
            time.sleep(2 ** i)

読み替えの段階化:シャドーリードとカナリア

書き込みの二重化に続けて、読み取りはシャドーリクエスト(新系にも同時送信するが応答採用は旧系)で新系に負荷をかけつつ、応答はまだ旧系を採用する期間を設けます。この期間に差分監視で整合性を確認し、閾値を満たしたら特定セグメントのみ新系の応答を採用します。前述のNGINXや機能フラグにより、ユーザごとに新旧を切り替えられるため、障害時には即時に旧系へ戻せます。こうしてカットオーバーを複数回に分割することで、運用のMTTRと変更失敗率は着実に改善していきます。

可観測性と成果数値:経営と現場を結ぶ計測設計

改善の目的はコードを書き換えることではなく、業務のアウトカムを高めることです。ゆえに、技術メトリクスと同じ解像度で、業務KPIを計測し、両者の相関を確認できる状態を作ります。たとえば、APIのp95(95パーセンタイル応答時間)が30%改善したにもかかわらず、チェックアウト完了率が変化しないなら、ボトルネックは別にあることが分かります。逆に、プロフィール読み込みのキャッシュ導入でp95が42ms→27msへ改善し、同時にプロファイル編集完了率が**+3.1pt**上がったなら、優先度が定量的に裏付けられます。この「技術→業務」連鎖を説明する資料に、投資判断の時間が短縮される効果は見逃せません⁸。

ここで、変更ごとにKPI差分を記録する小さな仕組みが役立ちます。Expressでトグルのオン・オフとKPIのスナップショットを打刻する例を示します。後段のダッシュボードでは、機能単位でオン/オフ期間のAB比較を自動で生成し、意思決定のスピードを引き上げます。

// KPIスナップショットの打刻(Node.js)
import express from 'express';
import { InfluxDB, Point } from '@influxdata/influxdb-client';

const influx = new InfluxDB({ url: process.env.INFLUX_URL!, token: process.env.INFLUX_TOKEN! });
const write = influx.getWriteApi(process.env.ORG!, process.env.BUCKET!, 'ns');
const app = express();

function markSnapshot(feature, state, metrics){
  const p = new Point('feature_snapshot')
    .tag('feature', feature).tag('state', state)
    .floatField('p95_ms', metrics.p95).floatField('err_rate', metrics.err)
    .floatField('conv_rate', metrics.conv);
  write.writePoint(p);
}

app.post('/toggle/:name', async (req, res) => {
  // 実際には認証・入力検証が必要
  const name = req.params.name; const state = req.query.state || 'off';
  const metrics = { p95: Number(req.query.p95 || 0), err: Number(req.query.err || 0), conv: Number(req.query.conv || 0) };
  try { markSnapshot(name, state, metrics); await write.flush(); res.json({ ok: true }); }
  catch(e){ console.error(e); res.status(500).json({ error: 'snapshot_failed' }); }
});

app.listen(4000);

成果数値とROIを結ぶ:スコアカードの作り方

経営への説明には、成果数値の換算が欠かせません。たとえば、デプロイ頻度が週1回から毎日へ、変更失敗率が22%→8%、MTTRが140分→55分になったとします。40名のエンジニア組織で、障害対応に費やす時間が月間で合計約220時間削減され、時間単価1万円なら月220万円のコスト圧縮に相当します。さらに、チェックアウトp95の短縮が転換率+1.2ptに寄与し、月商1億円のECで**+1,200万円**の増収効果が推定できるなら、投資回収はより明瞭です。ここまでを四半期ごとにスコアカードとして集計し、次の一手(どの機能を、どの順序で、どの指標を狙って)を合意します³。

段階的刷新の副次的な効果として、オンボーディングの短縮があります。契約テストと可観測性が前提化されたコードベースでは、新メンバーが影響範囲を把握するのに必要な時間が大幅に減り、レビューの密度も上がります。結果的に、開発リードタイムの短縮が継続的に積み上がり、業務改善と成果数値の双方で逓増するリターンが生まれます。

小さく始めて継続する:実装順序の現実解

実装の順序は、最も利益に近いフローの中で、最も狭い切替面が作りやすい機能から着手するのが現実解です。まず、入口での切替(リバースプロキシ or APIゲートウェイ)と可観測性の基礎を同時に入れ、次に契約テストで外部契約を固定し、そのうえで書き込みの二重化と読み取りの段階化を進めます。データが落ち着いた段階で、内部の最適化やアーキテクチャ移行(例えばモノリスからコンポーネント化)に踏み込みます。以下は、最小限の構成で網羅的にリスクを下げるための補助コードです。CI/CDからの段階リリースと、失敗時の自動巻き戻しを前提に、障害の局所化を徹底します。

# 参考: GitHub Actions で段階デプロイの雛形(抜粋)
name: canary
on: [push]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: kubectl apply -f k8s/canary.yml
      - run: sleep 300 && ./scripts/check-error-rate.sh
      - run: ./scripts/roll-forward-or-back.sh  # エラー率に応じて自動判定

最後に、段階的刷新がどの程度のオーバーヘッドを生むかについて触れておきます。ある検証環境(k8s上のNode/Goサービス、p95 40ms前後のAPI)の一例では、OpenTelemetry導入と機能フラグ評価の同時適用により、p95は平均**+2.3ms**、CPUは**+4.1%**の上昇に留まりました。参考ドキュメントにも類似の考慮点が示されており⁵、変更失敗率低減とMTTR短縮による可用性向上効果と比較すれば、十分に受容可能なコストと評価し得ます。もちろん本番の負荷特性は環境ごとに異なるため、導入前後での負荷試験と継続的なメトリクス監視は不可欠です。

関連テーマの深掘りとして、デプロイと運用の指標設計、契約テストの導入プロセス、データ移行戦略、段階的リリースの設計は参考になります。

まとめ:壊さず進め、測って語る

既存システムを活かしながら改善する要諦は、切替を境界に押し出し、内部を契約で守り、データの継ぎ目を安全に設計し、そして変化を継続的に可視化することに尽きます。Stranglerパターン、機能フラグ、二重書き込み、契約テスト、可観測性という“地味だが効く”道具をつなげれば、止めずに変えるという難題は現実の選択肢になります。業務改善の文脈では、リードタイム、失敗率、MTTR、そして業務KPIに対する影響を、四半期単位の成果数値として説明できる体制が競争力になります。

まずは、基準線の計測とエッジでの切替から着手してみてください。小さな成功を積み上げるうちに、壊さずに進めるチームの自信が醸成されます。次にどの機能で実験し、どの指標の改善を狙うのか。明日のスプリント計画に、一歩先の改善を組み込んでみませんか。

参考文献

  1. BCG. Companies Can Flip the Odds of Success in Digital Transformations from 30% to 80%. https://www.bcg.com/press/29october2020-companies-can-flip-the-odds-of-success-in-digital-transformations-from-30-to-80
  2. McKinsey & Company. Unlocking success in digital transformations. https://www.mckinsey.com/capabilities/people-and-organizational-performance/our-insights/unlocking-success-in-digital-transformations
  3. Atlassian. DORA metrics: Measuring DevOps performance. https://www.atlassian.com/devops/frameworks/dora-metrics
  4. Thoughtworks. Embracing the Strangler Fig Pattern for legacy modernization. https://www.thoughtworks.com/en-cn/insights/articles/embracing-strangler-fig-pattern-legacy-modernization-part-one
  5. Splunk Observability Docs. Node.js instrumentation performance considerations. https://docs.splunk.com/observability/en/gdi/get-data-in/application/nodejs/version-2x/performance.html
  6. Pact Docs. Pact: Contract testing for microservices. https://docs.pact.io/
  7. Mercari Engineering. Designing a zero-downtime migration solution with strong data consistency. https://engineering.mercari.com/en/blog/entry/20241113-designing-a-zero-downtime-migration-solution-with-strong-data-consistency-part-iv/
  8. Pingdom. How Does Page Load Time Affect Your Conversion Rate? https://www.pingdom.com/blog/how-does-page-load-time-affect-your-conversion-rate/