mybook

アーキテクチャレビュー — 顧客のシステムを読み解く力

最初のアーキテクチャレビュー

金曜の午後、ソウタのSlackにカイからDMが届いた。

「来週火曜、MediFlowのアーキテクチャレビューに入ってもらう。ソウタの初リードだ」

MediFlow——医療従事者向けのSaaSで、電子カルテ連携と診療予約管理を提供している。Arclight AIのプラットフォームを導入して、診療データのAI分析機能を追加したいという案件だ。しかし既存システムのアーキテクチャが複雑で、統合前にレビューが必要だった。

ソウタは緊張した。TechNovaでRailsエンジニアとして働いていた頃、他社のシステムを評価する経験はなかった。翌朝、オフィスでカイに声をかけた。

「カイさん、アーキテクチャレビューって何を準備すればいいですか」

カイはコーヒーを啜りながら笑った。

「ソウタ、一番大事なことを最初に伝えておく。アーキテクチャレビューは監査じゃない。会話だ」

「会話、ですか」

「そう。顧客のエンジニアは、自分たちのシステムに誇りを持っている。それを外部の人間が来て『ここがダメ、あそこもダメ』と指摘したら defensive になる。FDEのレビューは、顧客と一緒にシステムを理解して、一緒に改善策を見つける共同作業だ。武器はいくつかあるが、根底にあるのは常にリスペクトだよ」

INFO

アーキテクチャレビューの本質は「監査」ではなく「対話」である。顧客のエンジニアが築いてきたシステムへの敬意を前提に、共に改善の道筋を探る姿勢が信頼関係を構築する。


AWS Well-Architected Framework

カイはホワイトボードに6つの柱を書き出した。

「AWSのWell-Architected Frameworkは知ってるか? FDEがアーキテクチャレビューを行うとき、最も使いやすいフレームワークの一つだ」

Loading diagram...

6つの柱と質問例

「フレームワークの公式ドキュメントには何百もの質問がある。でもFDEが現場で使うときは、各柱から2問に絞る。全部聞いたら日が暮れるからな」

1. 運用の優秀性 — デプロイ頻度はどのくらいですか? 障害発生時の検知から復旧までの流れを教えてください。

2. セキュリティ — 認証・認可はどのような仕組みですか? 機密データの暗号化は静止時・転送時それぞれどうしていますか?

3. 信頼性 — SLO/SLAはどのように設定していますか? 単一障害点はどこにありますか?

4. パフォーマンス効率 — ボトルネックの特定と解消をどのように行っていますか? キャッシュ戦略を教えてください。

5. コスト最適化 — 現在のインフラコストの内訳を把握していますか? Reserved Instance やSavings Planは活用していますか?

6. 持続可能性 — リソースの利用効率を最適化する取り組みはありますか? データのライフサイクル管理はしていますか?

「答えが『やってない』でも責めない。事実を一緒に確認して、優先順位をつけて、一つずつ改善する。それがFDEのやり方だ」

WARNING

Well-Architected Frameworkの質問を「テスト」のように投げかけないこと。「教えてください」「一緒に確認しましょう」という姿勢で、顧客が安心して現状を共有できる場をつくる。


TOGAF とエンタープライズ顧客

「エンタープライズ顧客の場合はもう一つ、TOGAF(The Open Group Architecture Framework)を知っておくべきだ。Architecture Compliance Reviewという正式なレビュープロセスが定義されている」

TOGAFのフルプロセスは、レビュー対象の特定→基準の策定→エビデンス収集→ギャップ分析→是正計画の5ステップだが、数週間かかる。FDEは軽量版にアレンジする。

「ステップ2と4だけ意識すればいい。何を基準にするか現状とのギャップ。この2つが明確なら、提案に根拠が生まれる。MediFlowならHIPAA準拠が基準になるし、SOC2も求められるだろう」


軽量RFCアプローチ

「レビューの結果、改善提案をまとめるときはRFC形式で書く」

# RFC: [タイトル]
## ステータス: Draft / Under Review / Accepted / Rejected
## 概要
1〜2文で提案を要約する
## 背景・課題
## 提案する解決策
## 代替案(各代替案を不採用にした理由)
## リスクと軽減策
## 影響範囲

「RFCの良いところは、意思決定のプロセスが文書として残ることだ。半年後に別のエンジニアが読んでも経緯が追える。特に『代替案』セクションを必ず含めること。なぜ他のアプローチを選ばなかったかを明記すると、提案の説得力が大幅に増す」

INFO

RFCの「代替案」セクションは必須。顧客の社内稟議でも「他の選択肢を検討した上での結論」として使ってもらえる。


FDEのエンドツーエンドオーナーシップ

火曜日の朝、ソウタはカイと一緒にMediFlowのオフィスに向かった。道中、カイがFDEのレビュープロセスを説明した。

「FDEのアーキテクチャレビューは5つのフェーズで進む。従来のコンサルとの違いは、レポートを出して終わりじゃないことだ」

Loading diagram...

Phase 1: Discovery — 顧客のシステム全体像をヒアリングし、既存のドキュメント・コード・インフラ構成を確認。チームの体制と課題感を把握する。

Phase 2: Technical Scoping — レビュー対象のシステム境界を明確化し、制約条件(予算、期間、規制)を洗い出す。成功基準を合意する。

Phase 3: System Design — 改善案をRFCとして文書化。トレードオフの明示と段階的な移行計画の策定。

Phase 4: Build — 提案した改善を自ら実装し、顧客チームとペアプログラミングで技術移転を並行して行う。

Phase 5: Production Rollout — カナリアリリースやブルー/グリーンデプロイで本番展開。モニタリング設定とランブック作成、1〜2週間のハイパーケア期間。

「従来のコンサルは Phase 3 で終わる。FDEは Phase 5 まで責任を持つ。これがエンドツーエンドオーナーシップだ


MediFlowのレビュー — Discovery フェーズ

MediFlowのオフィスに到着すると、CTOの田中さんとリードエンジニアの佐藤さんが待っていた。

「よろしくお願いします。正直、外部の方にコードを見せるのは初めてで少し緊張しています」佐藤さんが苦笑した。

ソウタはカイの教えを思い出し、笑顔で応えた。

「コードの良し悪しを評価しに来たわけではありません。Arclight AIのプラットフォームをMediFlowさんのシステムにうまく統合するために、一緒にシステムを理解させてください

佐藤さんの表情が少し和らいだ。

ヒアリングを進めると、MediFlowのシステム全体像が見えてきた。Rails 6のモノリスアプリケーションで、5年間の開発で約15万行のコードベースに成長していた。

レガシーコードの読み解き方

ソウタは佐藤さんにコードベースのツアーをお願いし、並行していくつかの分析テクニックを使った。依存関係マッピングでActiveRecordのアソシエーションを追跡し、ホットスポット分析でgitログから変更頻度の高いファイルを特定し、結合度メトリクスでモジュール間の呼び出し関係を定量化した。

Loading diagram...

ソウタは分析を自動化するためのツールを書いた。

# lib/architecture/monolith_analyzer.rb
module Architecture
  class MonolithAnalyzer
    attr_reader :app_root, :results
 
    def initialize(app_root)
      @app_root = Pathname.new(app_root)
      @results = { models: [], coupling_scores: {}, hotspots: [], readiness_score: nil }
    end
 
    def analyze
      scan_models
      calculate_coupling
      detect_hotspots
      @results[:readiness_score] = ModularizationReadinessScore.new(@results).calculate
      self
    end
 
    private
 
    def scan_models
      Dir.glob(app_root.join("app", "models", "**", "*.rb")).each do |file|
        content = File.read(file)
        associations = %w[belongs_to has_many has_one has_and_belongs_to_many].flat_map do |type|
          content.scan(/#{type}\s+:(\w+)/).map { |m| { type: type, target: m[0] } }
        end
 
        @results[:models] << {
          name: File.basename(file, ".rb").camelize,
          file: file,
          lines: content.lines.count,
          associations: associations,
          callback_count: content.scan(/\b(before|after|around)_(save|create|update|destroy|commit)\b/).size,
          concerns: content.scan(/include\s+(\w+)/).flatten
        }
      end
    end
 
    def calculate_coupling
      @results[:models].each do |model|
        score = model[:associations].size + model[:callback_count] + model[:concerns].size
        @results[:coupling_scores][model[:name]] = {
          association_count: model[:associations].size,
          callback_count: model[:callback_count],
          concern_count: model[:concerns].size,
          total_score: score,
          risk_level: score <= 5 ? :low : score <= 12 ? :medium : score <= 20 ? :high : :critical
        }
      end
    end
 
    def detect_hotspots
      git_log = `cd #{app_root} && git log --format=format: --name-only --since='6 months ago' 2>/dev/null`
      file_counts = Hash.new(0)
      git_log.lines.map(&:strip).reject(&:empty?).each { |f| file_counts[f] += 1 }
      @results[:hotspots] = file_counts.sort_by { |_, c| -c }.first(20)
        .map { |file, count| { file: file, change_count: count } }
    end
  end
end

依存関係マッパー

モデル間の依存関係をトレースし、循環依存や境界づけられたコンテキストを検出するクラスも用意した。

# lib/architecture/dependency_mapper.rb
module Architecture
  class DependencyMapper
    def initialize(app_root)
      @app_root = Pathname.new(app_root)
      @graph = Hash.new { |h, k| h[k] = Set.new }
    end
 
    def build_graph
      scan_active_record_associations
      scan_service_dependencies
      self
    end
 
    def circular_dependencies
      visited, in_stack, cycles = Set.new, Set.new, []
      @graph.each_key do |node|
        dfs_detect_cycles(node, visited, in_stack, [], cycles) unless visited.include?(node)
      end
      cycles.uniq
    end
 
    def dependency_depth(node)
      visited = Set.new
      queue = [[node, 0]]
      max_depth = 0
      while (current, depth = queue.shift)
        break unless current
        next if visited.include?(current)
        visited.add(current)
        max_depth = [max_depth, depth].max
        @graph[current].each { |dep| queue << [dep, depth + 1] }
      end
      max_depth
    end
 
    private
 
    def scan_active_record_associations
      Dir.glob(@app_root.join("app", "models", "**", "*.rb")).each do |file|
        content = File.read(file)
        model = File.basename(file, ".rb").camelize
        content.scan(/(belongs_to|has_many|has_one)\s+:(\w+)/).each do |_, target|
          @graph[model].add(target.to_s.singularize.camelize)
        end
      end
    end
 
    def scan_service_dependencies
      Dir.glob(@app_root.join("app", "services", "**", "*.rb")).each do |file|
        content = File.read(file)
        service = File.basename(file, ".rb").camelize
        content.scan(/([A-Z][a-zA-Z]+)\.(find|where|create|new|all)/).each do |ref, _|
          @graph[service].add(ref)
        end
      end
    end
 
    def dfs_detect_cycles(node, visited, in_stack, path, cycles)
      visited.add(node)
      in_stack.add(node)
      path.push(node)
      @graph[node].each do |neighbor|
        if in_stack.include?(neighbor)
          cycle_start = path.index(neighbor)
          cycles << path[cycle_start..] + [neighbor]
        elsif !visited.include?(neighbor)
          dfs_detect_cycles(neighbor, visited, in_stack, path, cycles)
        end
      end
      path.pop
      in_stack.delete(node)
    end
  end
end

モジュラリゼーション準備度スコア

# lib/architecture/modularization_readiness_score.rb
module Architecture
  class ModularizationReadinessScore
    WEIGHTS = { coupling: 0.30, cohesion: 0.25, test_coverage: 0.20,
                api_boundaries: 0.15, data_isolation: 0.10 }.freeze
 
    def initialize(analysis_results)
      @results = analysis_results
    end
 
    def calculate
      scores = {
        coupling: evaluate_coupling,
        cohesion: evaluate_cohesion,
        test_coverage: 65.0, # 実際のプロジェクトではSimpleCovの結果を使用
        api_boundaries: 50.0,
        data_isolation: evaluate_data_isolation
      }
      weighted_total = scores.sum { |key, score| score * WEIGHTS[key] }
 
      {
        overall: weighted_total.round(1),
        breakdown: scores,
        recommendation: recommendation_for(weighted_total)
      }
    end
 
    private
 
    def evaluate_coupling
      return 100.0 if @results[:coupling_scores].empty?
      critical = @results[:coupling_scores].count { |_, v| v[:risk_level] == :critical }
      high = @results[:coupling_scores].count { |_, v| v[:risk_level] == :high }
      total = @results[:coupling_scores].size.to_f
      [100.0 - ((critical * 20 + high * 10) / total * 100), 0].max
    end
 
    def evaluate_cohesion
      return 50.0 if @results[:models].empty?
      ratio = @results[:models].count { |m| m[:concerns].size > 3 }.to_f / @results[:models].size
      ((1 - ratio) * 100).round(1)
    end
 
    def evaluate_data_isolation
      return 50.0 if @results[:models].empty?
      ratio = @results[:models].count { |m|
        m[:associations].any? { |a| a[:type] == "has_and_belongs_to_many" }
      }.to_f / @results[:models].size
      ((1 - ratio) * 100).round(1)
    end
 
    def recommendation_for(score)
      case score
      when 80..100 then "モジュラリゼーションの準備が整っています。段階的な分離を開始できます。"
      when 60..79  then "一部のリファクタリングが必要です。まず結合度の高いモデルを整理してください。"
      when 40..59  then "大幅なリファクタリングが必要です。Strangler Figパターンでの段階的移行を推奨します。"
      else "現状のまま分離するのはリスクが高いです。まずテストカバレッジの向上と依存関係の整理を行ってください。"
      end
    end
  end
end

これらのツールに加え、ArchitectureReportクラスで分析結果をMarkdownレポートにまとめ、循環依存の解消や高結合モデルのリファクタリングなどの推奨事項を自動生成する仕組みも用意した。

ソウタは分析ツールを実行し、結果を佐藤さんに見せた。

「MediFlowのモジュラリゼーション準備度スコアは58点です。特に Patient モデルが22個のアソシエーションを持っていて、結合度がcriticalになっています」

佐藤さんは画面を見て頷いた。

「やっぱり……Patient モデルは創業時からずっと機能を追加してきたので、何でもPatientに紐づいてるんです。数字で見ると改めて厳しいですね」

「一気に分離するのは難しいので、Strangler Figパターンで段階的に進めることを提案します。まずAI分析に必要な診療データ周辺だけを新しいモジュールに切り出して、既存機能には影響を与えないようにします」

INFO

Strangler Fig(絞め殺しの木)パターンは、レガシーシステムの周囲に新しいシステムを段階的に構築し、徐々に古い部分を置き換える手法。一括移行のリスクを大幅に低減できる。


セキュリティポスチャー評価

レビューの午後、セキュリティの観点から確認を行った。MediFlowは医療データを扱うため、セキュリティは特に重要だ。ソウタは OWASP Top 10 をベースに確認を進めた。

Deviseベースの認証は適切だったが、APIエンドポイントの認可チェックに一貫性がなく、RBACが一部未実装だった。転送時暗号化はTLS 1.2(1.3への更新を推奨)、RDSのストレージ暗号化は有効だが、アプリケーションレベルの暗号鍵管理がENV変数のみだった。監査ログはpaper_trailを一部で使用しているものの全体的なカバレッジが不十分で、データ保持ポリシーも明文化されていなかった。

WARNING

医療データを扱うシステムでは、暗号鍵の管理にENV変数だけを使うのはリスクが高い。AWS KMS やHashiCorp Vault などの専用の鍵管理サービスを導入し、鍵のローテーションを自動化すべき。

「セキュリティについては、暗号鍵管理のAWS KMS移行と監査ログの網羅性向上を優先的に提案します。HIPAA準拠のためには避けて通れないポイントです」

田中CTOが真剣な表情で聞いていた。「セキュリティは正直、開発速度を優先して後回しにしてきた部分です。AI機能の導入と合わせて、ここもしっかり改善したい」


AWS Well-Architected Tool による自動評価

レビューの一環として、ソウタはAWSのWell-Architected Toolを使ってプログラマティックに評価を行った。

# lib/architecture/well_architected_assessment.rb
require "aws-sdk-wellarchitected"
 
module Architecture
  class WellArchitectedAssessment
    PILLARS = {
      operationalExcellence: "運用の優秀性", security: "セキュリティ",
      reliability: "信頼性", performanceEfficiency: "パフォーマンス効率",
      costOptimization: "コスト最適化", sustainability: "持続可能性"
    }.freeze
 
    def initialize(region: "ap-northeast-1")
      @client = Aws::WellArchitected::Client.new(region: region)
    end
 
    def create_workload(name:, description:, environment:)
      response = @client.create_workload(
        workload_name: name, description: description,
        environment: environment, lenses: ["wellarchitected"],
        review_owner: "fde-team@arclight.ai",
        industry_type: "Healthcare"
      )
      response.workload_id
    end
 
    def generate_report(workload_id:)
      workload = @client.get_workload(workload_id: workload_id).workload
      lens_review = @client.get_lens_review(
        workload_id: workload_id, lens_alias: "wellarchitected"
      ).lens_review
 
      pillar_reviews = lens_review.pillar_review_summaries.map do |summary|
        { pillar: PILLARS[summary.pillar_id.to_sym], risk_count: summary.risk_counts }
      end
 
      improvements = @client.list_lens_review_improvements(
        workload_id: workload_id, lens_alias: "wellarchitected", pillar_id: "security"
      ).improvement_summaries.map do |item|
        { question_title: item.question_title, risk: item.risk }
      end
 
      { workload_name: workload.workload_name, risk_summary: workload.risk_counts,
        pillar_reviews: pillar_reviews, improvement_plan: improvements }
    end
  end
end

「Well-Architected ToolのAPIを使うと、評価結果をプログラマティックに取得してレポートに組み込める。定期的に再評価するときのベースラインにもなる」

ソウタはカイに報告した。「MediFlowのWell-Architected評価では、セキュリティの柱でHIGHリスクが3件、信頼性でMEDIUMリスクが5件出ました。暗号鍵管理、監査ログ、単一障害点が主な指摘事項です」

「いい分析だ。それをRFCにまとめて、優先順位をつけて提案しよう」


レビューの完了とフィードバック

3日間のレビューを終え、ソウタはRFCを3本作成した。

  1. RFC-001: 暗号鍵管理のAWS KMS移行 — 優先度: 高
  2. RFC-002: Patient モデルの段階的モジュラリゼーション — 優先度: 中
  3. RFC-003: 監査ログ基盤の構築 — 優先度: 高

最終日のプレゼンテーションで、田中CTOからこう言われた。

「今回のレビューは、今まで受けたどのコンサルの報告より実践的でした。特に、問題点を指摘するだけでなく、具体的な実装レベルのロードマップを示してくれたのが助かります」

帰りの電車で、カイがソウタに言った。

「良い初回レビューだった。特に良かったのは3つある」

「まず、佐藤さんの警戒心を解く入り方。『一緒に理解させてください』という言葉で、レビューが対話になった。次に、数字で語ったこと。モジュラリゼーション準備度スコアという定量的な指標を出したから、田中CTOも意思決定しやすかった。最後に、RFCで提案を文書化したこと。口頭の提案は忘れられるが、文書は残る」

「ありがとうございます。正直、初日は緊張しましたが、Well-Architected Frameworkの質問に沿って進めたら自然に会話が広がりました」

「それがフレームワークの力だ。フレームワークは杖みたいなものだ——歩くのは自分だが、支えがあると遠くまで行ける」

カイは窓の外を見ながら付け加えた。

「次は Phase 4——Build だ。自分がRFCで書いたことを、自分の手で実装する。FDEの真価が問われるのはそこからだぞ

ソウタは頷いた。評価して、提案して、そして自ら実装する。レポートを出して終わりのコンサルタントではなく、顧客と一緒にシステムを作り上げるエンジニア。それがForward Deployed Engineerだ。

INFO

アーキテクチャレビューの成功は「何を見つけたか」ではなく「顧客が次のアクションに自信を持てたか」で測る。定量データ、文書化された提案、そして実装コミットメントの三位一体が、FDEの価値を最大化する。