Article

レガシーシステム刷新のススメ:古い基幹系をアップデートする方法

高田晃太郎
レガシーシステム刷新のススメ:古い基幹系をアップデートする方法

COBOLは依然として世界で約8千億行が稼働していると報告され[1]、多くが金融や公共のトランザクションを支えています。公開調査では、レガシーシステムのモダナイゼーション(既存の基幹システムを現代のアーキテクチャやクラウドへ段階的に適合させる取り組み)は上位の投資優先事項に位置づけられ[2,3]、ダウンタイムの平均損失は1分あたり数千ドル規模とされます[7,8]。各種公開データを読み解くと、価値が高いのに更新できず、技術的負債(将来の変更コストとして利息のように増える負担)の利息だけが膨らむ領域が偏在している構図が見えてきます。止められない基幹システムをどう動かしながら変えるか。この矛盾に向き合うには、段階移行・契約の固定化・データの整流化という3点を同時に進める必要があります。難度は高いものの、ビジネスの連続性を維持しつつリスクを可視化し、段階ごとにROIを回収する設計に切り替えれば、現実的な道筋は描けます。なお、グローバル大企業全体ではダウンタイム損失が年間4,000億ドル規模に達するとの分析もあります[9]。

なぜ今、レガシー刷新か:価値とリスクのマッピングから始める

刷新の議論はしばしば技術軸で先行しますが、先に決めるべきは価値の地図です。売上や粗利への寄与、規制リスク、オペレーション負荷、採用難易度といったファクターを横並びにし、現行機能を価値×リスクでマッピングします。高価値・高リスクの象限にある機能は、丸ごとの置き換えではなく、インターフェース(外部に見せるAPIや画面上の動き)の固定化から始めるのが合理的です。逆に低価値領域は存続コストが利息化していることが多く、廃止またはSaaSリプレースの検討で即効の費用対効果を出せます。ここで有効なのがドメイン単位での分解です。イベントストーミング(業務イベントの流れをホワイトボードで可視化する手法)やビジネスプロセスの観測から境界づけられたコンテキスト(バウンデッドコンテキスト:用語とデータが自律する業務の境界)を見極め、基幹系の巨大な一枚岩を自然な流路に沿って割っていきます。たとえば「顧客管理」と「請求・入金」を分け、まずは顧客の読み取りAPIだけを独立させる、といった具合です。分け方の正しさは、インターフェースの明快さとデータの独立性で検証できるため、まずは読み取り専用のサービス抽出から小さく確かめるのが定石です。

モダナイゼーションの最初の成果は「止めずに分ける」ことです。既存のバッチやオンライン処理に手を入れず、周縁にアダプタ層を築いて新旧の接続点を設計します。呼び出し元や下流にとっては契約が安定し、内部の実装は段階的に置き換えられます。これにより変更の影響範囲を絞り、短いサイクルで価値を見せることができます。ここでの「契約」はOpenAPIなどで定義された外向け仕様を指し、契約を固定することで上位のフロントエンドやBFF層を揺らさずに内側を差し替えられます。

サービスポートの固定化:アンチコラプション層の最小実装

古いホストやRDBのスキーマを直接晒すと新生サービス側が汚染されます。アンチコラプション層(ACL:レガシーのデータ形を翻訳し、外部ドメインモデルを守る防波堤)で契約を固定化し、内外でモデルを分けます[4]。Java/Spring Bootで実装する最小の読み取りアダプタは次のように書けます。

package com.example.legacy.adapter;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.dao.DataAccessException;
import javax.sql.DataSource;
import java.sql.*;

@SpringBootApplication
@RestController
@RequestMapping("/api/v1/customers")
public class LegacyReadAdapterApplication {
  private final DataSource dataSource;
  public LegacyReadAdapterApplication(DataSource dataSource) { this.dataSource = dataSource; }

  @GetMapping("/{id}")
  public ResponseEntity<CustomerDto> findById(@PathVariable String id) {
    try (Connection c = dataSource.getConnection();
         PreparedStatement ps = c.prepareStatement(
           "SELECT CUST_ID, NAME, RANK FROM LEGACY_CUSTOMER WHERE CUST_ID = ?")) {
      ps.setString(1, id);
      try (ResultSet rs = ps.executeQuery()) {
        if (!rs.next()) return ResponseEntity.notFound().build();
        CustomerDto dto = new CustomerDto(rs.getString(1), rs.getString(2), rs.getString(3));
        return ResponseEntity.ok(dto);
      }
    } catch (SQLException | DataAccessException ex) {
      return ResponseEntity.status(502).build(); // Bad gateway to legacy
    }
  }

  public static void main(String[] args) { SpringApplication.run(LegacyReadAdapterApplication.class, args); }
}

class CustomerDto {
  public String id; public String name; public String rank;
  public CustomerDto(String id, String name, String rank) { this.id=id; this.name=name; this.rank=rank; }
}

新生ドメインモデルと外部のDTOは分け、外向けの契約だけを安定化するのが狙いです。まずはGETに限定し、書き込みは後段で扱います。読み取りの分離は低リスクで、パフォーマンスの現実測定にも向くため、P95/P99のレイテンシ基準を早期に決め、SLO(Service Level Objective:合意した品質目標)として可視化します。クラウド上のAPIゲートウェイやキャッシュの組み合わせで、既存の基幹システムに負荷をかけずに段階移行を進められます。

統合のボトルネックをBFFで吸収する:タイムアウトとフォールバック

画面やAPIの統合レイヤはBFF(Backend for Frontend:UIごとに最適化された中間層)が有効です。旧来のSOAPやJMSと新APIを束ね、短いタイムアウトとフォールバックで体験を守ります。Node.jsの例では非機能要件をコードで表明します。

import express from 'express';
import fetch from 'node-fetch';
import soap from 'soap';

const app = express();
const PORT = process.env.PORT || 8080;
const TIMEOUT_MS = 800;

function timeout(promise, ms) {
  return Promise.race([
    promise,
    new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), ms))
  ]);
}

app.get('/bff/v1/summary/:id', async (req, res) => {
  const id = req.params.id;
  try {
    const rest = timeout(fetch(`https://new-api/v1/customers/${id}`, { timeout: TIMEOUT_MS }), TIMEOUT_MS)
      .then(r => r.ok ? r.json() : Promise.reject(new Error('bad status')));
    const legacy = new Promise((resolve, reject) => {
      soap.createClient('https://legacy/Customer.wsdl', (err, client) => {
        if (err) return reject(err);
        client.getCustomer({ id }, (e, result) => e ? reject(e) : resolve(result));
      });
    });
    const [a, b] = await Promise.allSettled([rest, legacy]);
    if (a.status === 'fulfilled' || b.status === 'fulfilled') {
      return res.json({ modern: a.status==='fulfilled' ? a.value : null, legacy: b.status==='fulfilled' ? b.value : null });
    }
    return res.status(502).json({ message: 'both sources failed' });
  } catch (e) {
    return res.status(504).json({ message: 'gateway timeout' });
  }
});

app.listen(PORT, () => console.log(`BFF on ${PORT}`));

タイムアウトやフォールバックを明示することで、遅延があっても体験が破綻しない構造を最初から組み込みます。短時間のタイムアウトはレガシー側の長い待ちに引きずられないための防波堤になり、外形的なSLOを守ります。基幹システムのモダナイゼーションでは、このBFFが「古いを隠し、新しいをつなぐ」要として機能します。

止めずに置き換える:ストラングラーパターンの運用

ストラングラーフィグパターンは、新旧を並走させながら徐々に新を太らせるアプローチです[5]。基本線はルーティングの制御、機能単位の切り出し、影響範囲の測定からなります。まずはリードパスの新経路を立てて観測することが多く、成功指標をレイテンシ、エラーレート、キャッシュヒット率などに設定します。切り替えは一気ではなく、対象ユーザーやテナントを限定したカナリアで段階的に進めるのが安全です[3]。これはクラウドのトラフィック分割(例:APIゲートウェイやリバースプロキシの重みづけ)とも親和性が高く、マイクロサービス化の入口としても有効です。

カナリア切替とサーキットブレーカの併用

JavaでResilience4jを用いたサーキットブレーカの最少実装は次の通りです。旧来APIの不安定性に引きずられないための防御線を張ります。

import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import io.github.resilience4j.decorators.Decorators;
import java.time.Duration;
import java.util.function.Supplier;

public class LegacyClient {
  private final CircuitBreaker cb;
  public LegacyClient() {
    cb = CircuitBreaker.of("legacy", CircuitBreakerConfig.custom()
      .failureRateThreshold(50f)
      .waitDurationInOpenState(Duration.ofSeconds(10))
      .slidingWindowSize(20)
      .permittedNumberOfCallsInHalfOpenState(3)
      .build());
  }
  public String getCustomer(String id) {
    Supplier<String> call = () -> doSoapCall(id); // 実装は省略
    Supplier<String> guarded = Decorators.ofSupplier(call)
      .withCircuitBreaker(cb)
      .withFallback(ex -> "{}")
      .decorate();
    return guarded.get();
  }
}

これをリバースプロキシやAPIゲートウェイの重みづけと合わせ、トラフィックの一部を新生サービスに流し、エラーレートやP99レイテンシがSLOを超えたら自動で元に戻す運用が現実的です。小さく流して測り、悪ければ即座に戻せることが心理的安全性にもつながります。基幹システムの移行では、請求書PDFの生成や会員ランク算出のような独立性が高い機能からカナリアを始めると、影響半径を最小化できます。

契約を壊さない置き換え:OpenAPI契約テスト

新旧のエンドポイントが同じ契約を満たすことを自動で確かめます。PythonのSchemathesisを使うと、OpenAPIからプロパティベーステストを生成できます。

import schemathesis as st

schema = st.from_uri("https://api.example.com/openapi.yaml")

@schema.parametrize()
def test_api(case):
    response = case.call_asgi()  # or call_wsgi / call
    case.validate_response(response)

契約テストは新サービスのデプロイごとに回し、互換性の破壊を早期に検知します。UIや業務は変えず、契約だけを固定化するという姿勢が、段階的移行の成功率を押し上げます。ここでいう契約は、HTTPステータス、スキーマ、エラーフォーマット等を含む「約束」で、フロントエンドやBFF、外部連携の変更コストを抑える要となります。

データは一度に動かさない:CDCと二重書きでゼロダウンタイム

データ移行の難所は、切替瞬間の整合性と欠損への恐れです。全量一括移行はスケジュールが滑りやすく、やり直しコストも高い。そこで整流のためのCDC(Change Data Capture:DBの変更をトランザクションログから検出してイベントとして配信する仕組み)と二重書きを併用し、読み取りは新、確定の系は旧といった役割分担で揺れ幅を抑えます。例えば顧客住所変更の事例では、初期スナップショットで顧客マスタを複製し、その後は住所更新をCDCでイベント化して新・旧双方に反映します。まずスナップショットで初期同期し、以降はトランザクションログからの増分を流します。Debezium + Kafka Connectの構成は、RDBの変更をイベントとして流し、新旧双方に伝播させるのに適しています[6]。

Kafka ConnectでCDCストリームを立てる

最小のPostgreSQLコネクタの設定は次の通りです。運用ではパーティションキーや再送戦略を明示的に決め、遅延時の回復時間を計測します。

{
  "name": "pg-customer-source",
  "config": {
    "connector.class": "io.debezium.connector.postgresql.PostgresConnector",
    "database.hostname": "pg",
    "database.port": "5432",
    "database.user": "debezium",
    "database.password": "secret",
    "database.dbname": "legacy",
    "plugin.name": "pgoutput",
    "table.include.list": "public.customer",
    "slot.name": "debezium_slot",
    "tombstones.on.delete": "false",
    "transforms": "unwrap",
    "transforms.unwrap.type": "io.debezium.transforms.ExtractNewRecordState"
  }
}

新生系ではこのイベントを消費して読み取りストアを最新化し、段階が進めば書き込みを新に寄せてから、最終的に旧へのミラーを切る流れに移ります。二重書き期間は可用性が下がるため、整合性監査のバッチと可観測性のダッシュボードで常時監視します。特に重複書き込みの冪等性(同じリクエストを複数回処理しても結果が変わらない性質)を担保し、リトライ時に重複レコードが生まれないようキー設計とバージョン管理(例:楽観ロック)を徹底します。

移行の安全網:トリガと監査の一時的活用

双方差分を検出するための一時的な監査テーブルを設けます。PostgreSQLのトリガで更新ハッシュを保存し、乖離が検知されたらアラートを上げます。

CREATE TABLE audit_customer(
  cust_id text PRIMARY KEY,
  legacy_hash text,
  modern_hash text,
  updated_at timestamptz default now()
);

CREATE OR REPLACE FUNCTION audit_customer_fn() RETURNS trigger AS $$
BEGIN
  INSERT INTO audit_customer(cust_id, legacy_hash)
  VALUES(NEW.cust_id, md5(NEW.name || '|' || NEW.rank))
  ON CONFLICT (cust_id) DO UPDATE SET legacy_hash = EXCLUDED.legacy_hash, updated_at = now();
  RETURN NEW;
END; $$ LANGUAGE plpgsql;

CREATE TRIGGER trg_audit_customer AFTER UPDATE OR INSERT ON legacy_customer
FOR EACH ROW EXECUTE FUNCTION audit_customer_fn();

監査の仕組みは移行完了後に撤去します。目的は永続化ではなく、移行段階の可視化です。これにより、夜間バッチの遅延や日中ピーク時のイベント滞留も早期に検知できます。ゼロダウンタイムを目指す場合でも、「観測−検知−復旧」の3点セットをCDCパイプラインに組み込むのが業界標準です。

SLO・コスト・人の計画:刷新を経営課題に翻訳する

技術計画はSLOとコストモデルに翻訳されて初めて継続予算になります。レイテンシ、エラーレート、可用性のSLOを設定し、稼働データでトレンドを出します。クラウド移行と密接である場合、リザーブド・キャパシティやストレージ層のティア設計と合わせ、3年のTCOで比較すると話が進みやすくなります。人の計画では、既存技術に詳しいメンバーをアンカーに、新技術側の実装者とペアを組ませます。知識の橋渡し役がチーム間の摩擦を減らし、移行速度を左右するからです。育成と採用の両輪で、モダナイゼーション、マイクロサービス、データ移行(CDC)の実務経験を持つ人材の厚みを作ることが、継続的刷新の基盤になります。

測定の一例として、読み取りアダプタのP99レイテンシが300ms、エラーレートが0.2%という初期SLOを置き、その上でカナリア時の新経路のP99を450ms以内に収めるといった目標を置きます。負荷試験は小さく頻繁に行い、ピーク時の同時接続数やGCポーズ等の指標を蓄積します。以下は簡易のwrk測定の実例です。

wrk -t4 -c128 -d60s https://adapter.example.com/api/v1/customers/123
# 例: Requests/sec: 2100, P50: 42ms, P95: 160ms, P99: 290ms

インフラの構成管理はコード化します。最小のALB + ECS/Fargate構成で始め、観測のダッシュボードとアラートをセットで用意します。Terraformの例を示します。

provider "aws" { region = var.region }

module "vpc" { source = "terraform-aws-modules/vpc/aws" version = "5.0.0" name = "legacy-modern" cidr = "10.0.0.0/16" }

resource "aws_lb" "public" { name = "legacy-modern-alb" internal = false load_balancer_type = "application" subnets = module.vpc.public_subnets }

ガバナンスでは変更承認を軽くし、ロールアウトとロールバックを自動化します。Pull Requestのマージでステージングに自動デプロイし、契約テストと負荷のスモークを通過したら小さく本番に流すパイプラインを用意します。GitHub Actionsの最小構成は次の通りです。

name: canary-deploy
on: [push]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with: { distribution: 'temurin', java-version: '21' }
      - run: ./gradlew build
      - run: ./scripts/deploy_canary.sh
      - run: ./scripts/run_contract_tests.sh

この一連の仕掛けが、段階移行の安全性と速度の双方を担保します。数値でリスクを可視化しながら、小さな成功を積み上げることが、経営判断の継続的な後押しになります。基幹システム刷新のROIは、停止回避による機会損失の低減、クラウドのスケール効率、変更リードタイムの短縮といった観点で定量化できます。

現場の負担を減らす設計原則

モダナイゼーションは長距離走です。日々の保守を続けながら新旧の二重運用を回すには、観測可能性、デフォルトタイムアウト、冪等性、再送の安全といった非機能の原則を最初からコードに織り込みます。たとえばリトライは指数バックオフとジッタ付きで、必ず冪等な操作に限定するという約束をチームの共通言語にします。こうした原則の共有はレビューの基準を明確にし、属人性を下げます。ログの相関ID、分散トレーシング、メトリクスのSLO紐づけといった可観測性の実装は、移行の意思決定スピードを上げる「見える化」の要です。

まとめ:止めずに変える、そのための設計と習慣

レガシー刷新は、正解を一度に当てるプロジェクトではありません。価値とリスクの地図を描き、契約を固定して外周から読み取りを剥がし、CDCでデータの流れを整え、サーキットブレーカとカナリアで段階的に切り替える。これらをSLOとコストモデルに落とし込み、観測と自動化のレールに乗せることで、基幹システムを止めずにアップデートできます。今のあなたの組織にとって最小の一歩は何か。まずは読み取り専用のアダプタを立て、P99レイテンシを測るところから始めてみてください。小さな測定は、次の意思決定を具体に変える力を持っています。ロードマップはそこから自然に延びていきます。今日、どの機能の契約を固定しますか。

参考文献

  1. DBTA. COBOL Market Shown to be Three Times Larger than Previously Estimated. https://www.dbta.com/Editorial/News-Flashes/COBOL-Market-Shown-to-be-Three-Times-Larger-than-Previously-Estimated-151446.aspx
  2. NASCIO. 2023 State CIO Survey. https://www.nascio.org/resource-center/2023-state-cio-survey/
  3. Gartner. Modernization initiatives are a top priority; must shift to continuous modernization. https://www.gartner.com/en/documents/5400863
  4. microservices.io. Pattern: Anti-corruption Layer. http://microservices.io/patterns/refactoring/anti-corruption-layer.html
  5. microservices.io. Strangler Fig Application Pattern: Incremental Modernization to Services. https://microservices.io/post/refactoring/2023/06/21/strangler-fig-application-pattern-incremental-modernization-to-services.md.html
  6. Red Hat Developers. Application modernization patterns: Apache Kafka, Debezium, and Kubernetes. https://developers.redhat.com/articles/2021/06/14/application-modernization-patterns-apache-kafka-debezium-and-kubernetes
  7. Pingdom. The Average Cost of Downtime per Industry. https://www.pingdom.com/outages/average-cost-of-downtime-per-industry/
  8. Mission Critical Magazine. Report: Unplanned Data Center Outages Cost Companies Nearly $9,000 Per Minute. https://www.missioncriticalmagazine.com/articles/88082-report-unplanned-data-center-outages-cost-companies-nearly-9000-per-minute
  9. Splunk. Splunk report shows downtime costs Global 2000 companies $400 billion annually (2024). https://www.splunk.com/en_us/newsroom/press-releases/2024/conf24-splunk-report-shows-downtime-costs-global-2000-companies-400-billion-annually.html