立て直しが必要な Web システムを引き継いだ最初の半年で何をやったか

引き受けの背景

Web システム開発の引き継ぎ後、切り替えリリースから 1 年弱が経ちました。初回リリース後も大きなトラブルなくサービスできているので、記録がてらチームメンバに振り返ってもらいこの記事をまとめてみました。

ある夏の終わりに、「開発が立て込んでいて、開発ベンダーの変更を検討している」という相談を受けました。相談の段階で挙がっていた困りごとは、概ね以下のようなものでした。

  • 延べ 20 人規模のエンジニアが投入されているが、スケジュールが守れていない
  • バッチ処理がうまく動かず、運用側が手で SQL を叩いて修正することが常態化していて辛い(夜も眠れない)
  • 日付関連のバグが頻発している。時差に起因した不具合が常に出ている
  • 次のリリースまでは現ベンダーが続投する。ただし、その次のリリース(約 2 ヶ月後)から glucose で巻き取ってほしい

身につまされる話です。話を聞いて、引き受けることにしました。

蓋を開けてみたら何があったか

ソースコードを受領して中を見たところ、構成は以下のようなものでした。

  • アプリ用モノレポ + インフラリポジトリの 2 リポジトリ構成
  • フロントエンド(Next.js)3 種 → サーバ(Fastify)4 種 → PostgreSQL(Prisma 経由)
  • ディレクトリ構成は Clean Architecture 風の切り方

コードベースを読み込んでいくと、歴史的経緯によるものと思われる以下のような状態が見えてきました。

  • TimeZone が暗黙の前提で書かれていた:コードはローカルタイム(+9 時間)前提なのに、サーバや DB は UTC で動いており、9 時間ずれが頻発。対応として「突然 9 時間足したり引いたり」するコードがあちこちにあって、全体的に挙動の予測が難しい状態でした。AWS のデフォルト TZ は UTC なので、ローカルだけで開発した日本のサービスをそのままデプロイすると起きやすい不具合です。
  • コード統制の仕組みが弱かった:命名規則、linter / formatter が無く、似たロジックのコピペが各所に散っていたり、識別子の typo / スペルミスが目立ったり。.ts とビルド後の .js が両方コミットされていたり、TypeScript なのに any が広く使われていたり。
  • テストコードが無い
  • バッチが過剰に分割されていた:中身がほぼ同じで一部だけ違う Docker イメージが 20 種強あり、SSM Parameter Store のフラグ管理で起動制御をかける仕組みになっていて、運用上の管理コストが高そうな状態でした。
  • インフラリポジトリ側もなかなか読みにくい状態:5万行以上あるTerraformで、不要なコードや、内容のちょっと違うコピーが多く、調査に時間をかけました。

加えて、契約上の事情として「次のリリース開発が走っている間は、解約の意向を前任には伝えない」という方針がクライアント側で決まっていました。つまり、前任主管のリリース直前までホットな開発が続くのに、その先の自社担当リリースの開発も並行して進めなければならない、という状況です。

ここで考えるべきは「いつから、どう手を入れていくか」でした。

引き継ぎ”前”にやったこと: Weekly Rebase + 自前のコード書き換えスクリプト

リリース時期から逆算すると、正式な引き継ぎを待たずに先行で開発を進める必要がありました。一方でこちらが先行作業を進めている間も、前任側では次のリリースに向けた開発が並行して走り続けています。自分たちが先に着手し、前任の変更は後追いで取り込み続けるという状況です。

加えて、前述の通り元のコードベースには最低限の統制が無く、そのまま開発を上に積むと自分たちまで巻き込まれます。

そこで以下の方針で先行作業を進めました。

  • 不要コード削除と formatter / linter の適用を行う「自前のコード書き換えスクリプト」を作成)
  • 毎週月曜朝に、前任側の最新ブランチに対してこのスクリプトを適用したものを作り、そこに自分たちの開発ブランチを rebase

スクリプトはだらだらと多重 sed で書いてしまい、毎週の処理がやたら遅かったのは反省点です(その後 AI でやり直したい気持ちが芽生えました)。

最終的に元ブランチとの差分は 650 コミットを超えました。毎週半日以上かかる Rebase 作業をやり切ったチームメンバーには本当に頭が下がります。

引き継ぎ”後”にやった土台作り

引き継ぎが正式に始まってからは、機能開発に走る前に「後戻りが効きにくい領域」から土台を整えに行きました。具体的には、日付処理、型・バリデーション、インフラ、開発環境の 4 つです。引き継ぎ前の調査・リファクタ段階でリリース目標の実装のためには、このあたりを触らないと辿り着けないことは明白でした。作り直しを加味した工数で顧客合意をとり、直しつつ実装をすすめるような進行を目指しました。

日付の作り直し(dayjs + Prisma $extends

時差ズレの根を抜くには、コードの中で「タイムゾーンが不明な日付(tz-naive)」を流通させない状態にしないといけません。JS 標準の Date ではタイムゾーン情報を持たせるのが厳しいので、dayjs を導入しました。

導入手順は次の通りです。

  1. 既存の Date 周りのユーティリティ関xxx数を XXXX() legacyXXXXX() にリネーム。これで「これからの開発で間違えて使わない」ようにし、リファクタの進捗も grep で追えるようにする。
  2. 日付がコードに入ってくる入り口を 3 つに整理し、それぞれ書き換え方針を決める。
    • 現在時刻: new Date() / Date.now() を、tz-aware な dayjs を返す getNow() に置換
    • API リクエスト: zod のレイヤーで dayjs に変換
    • DB(Prisma): PrismaClient の $extends を使って、Prisma の返す Date を dayjs に変換するレイヤーを実装

これらは一気に全部書き換えるのは現実的でないので、API ハンドラやバッチ処理ごとに少しずつ書き換えていきました。

型・バリデーション統一(Zod + fastify-type-provider-zod)

引き継ぎ時点での型・バリデーション周りで気になっていたのは、主に 2 点でした。

(1) 共通の値に対するスキーマがバラバラ

たとえば「登録番号」のような頻出する値が、ある場所では z.string()、別の場所では z.string().length(10)、さらに別の場所では正規表現付きの厳密定義、といった具合に統一されていませんでした。これは共通スキーマを 1 か所に切り出して全箇所からインポートする形に整えました。アプリ全体で同じバリデーションが効くようになり、メンテナンス性も上がります。

(2) 同じリクエストボディに対して Zod スキーマが二重に存在

OpenAPI ドキュメント生成用の z.any() ベースのスキーマと、実バリデーション用の厳密なスキーマが別物として両方メンテされていたので、後者に一本化しました。

そのうえで、API バリデーションそのものを fastify-type-provider-zod に寄せました。それまではエンドポイントごとにバリデーションとエラーハンドリングを手書きしていて、似たコードが散らばっていたのですが、ルート定義にスキーマを書くだけで Fastify 側が自動でバリデーションしてくれる形に置き換え、ハンドラ側のコードを大幅に減らせました。

インフラ全部書き直し(Terraform → CDK)

インフラは、引き継いだ Terraform 構成をそのまま運用するのではなく、自分たちでメンテできる形にフルリプレイスしました。これが今回の作業の中でもっとも大きい塊です。

構成を調査する中で見えたこと

最初は旧 Terraform を自社 AWS に適用する試行から入りましたが、完璧には動かせませんでした。理由は次のようなものです。

  • モジュール間の参照がリモートステート(S3 上の State ファイル)経由で動的に解決される構造で、terraform graph のような静的ツールが効かない。State の分割粒度も細かく、デプロイにも時間がかかる
  • 案件で実際には使っていないリソース定義が大量にある。

そのうえで、5 つの構成上の改善ポイントが見えてきました(要点だけ)。

#旧構成の問題新構成での対応
1フロントエンド系サーバ → NAT GW → CloudFront → API 系サーバ という遠回り経路内部 ALB を新設して フロントエンド系サーバAPI 系サーバ を内部通信に。当初気になっていた「謎の遅延」の主原因
2API 系サーバ 経路が ALB → API Gateway → NLB と冗長API Gateway / NLB を廃止し、サービスごとの ALB 直下に配置
3ECS タスクが 40 台超で過剰Proxy 廃止と合わせて見直し、オートスケーリング導入
4Docker Image が 20 種類前後1 種類に統合(このシステム規模ではメリットが上回る判断)
5Terraform の記述が冗長で見通しが悪い(count = 0 \| 1 で環境差分を表現するなど)CDK(TypeScript)でフルスクラッチ

なぜ Terraform でなく CDK だったか

Terragrunt 等で緩和できる部分はありますが、Terraform(HCL)は宣言的言語であり、モジュール化やリモートステート経由の依存解決を多用すると静的解析が効きにくくなります。CDK(TypeScript)であれば、依存関係はコード上の import / 参照で自然に書け、型システムによる静的検査もそのまま乗ります。旧 Terraform を実際に触って感じていたフラストレーションを、CDK が解決してくれる手応えがありました。

規模感

ファイル数行数
旧構成(Terraform)1,35253,846
新構成(CDK)201,737

同じインフラ規模を扱うコードを 1/30 程度の行数に圧縮できました。

IaC で管理しなかったもの

すべてを IaC に寄せるのが理想ですが、今回は以下は手動管理にしました。

  • Route 53 の DNS レコード: ルート AWS アカウントが DNS ゾーンを一元管理しており、サブアカウント側からの依存が複雑になるため ARN 参照のみ
  • SSL 証明書: ACM の DNS 検証時のプロパゲーション待ちで CloudFormation がタイムアウトし、デプロイ全体が落ちる事故を避けるため、事前手動作成 → ARN 参照

「IaC の責務をどこで切るか」の解像度が上がったのも収穫でした。

プロセスとしての学び

IaC のフルスクラッチは、一人が一気通貫で担当したほうが効率的でした。デプロイ → 検証 → 修正のサイクルは本質的にインタラクティブで、検証環境が一つしか無い場合は並列作業が困難になるためです。今回も実際、構築を一人で完結させたことで全体把握とトラブルシューティングが速く回りました。

開発体験と運用の整備

開発環境自体の整備も早めに着手しました。とくにDevContainerの整備はCIに。Slack Bot やGithubActionsでのワークフローの整備は、デプロイ作業の抜け漏れや、クライアントコミュニケーションにとても便利で、先んじて細かいタスクを自動化しておくことでコード改善に向き合える時間がつくれました。

  • Dev Container 導入で、新しく入ったメンバーの立ち上がりの摩擦を下げる
  • 運用手順のドキュメント化で、QA や本番調査の速度を上げる
  • リリース手順 を Slack Bot/GithubActionsでワークフロー化して、運用・連携の「抜け」を減らす

やってみての学び(次の引き継ぎ案件に活かしたいこと)

コードフリーズできない期間の工数は多めに見積もる

引き継ぎは「前任の開発が止まってから巻き取る」、つまりコードフリーズ後に引き継ぐのを目指すのが大前提として一番楽です。とはいえ、ビジネス要件上どうしてもそれが難しいケースもあります。今回も、前任の最終リリース直前まで先方の開発が走り続けるという制約があり、こちらが先行作業を進めている間、前任側の変更は後追いで取り込み続けるしかありませんでした。

その結果として、Weekly Rebase で 650 コミットを取り込み続けることになり、機能開発・品質改善といった本質的な作業ではない「取り込み作業」にかなりの工数をとられました。やむを得ずコードフリーズできない期間が発生する案件では、ここに見えにくい工数が必ず乗ります。引き継ぎ全体の見積もりにこのバッファを多めに含めておかないと、土台整備にかけられる時間が削られていきます。

そのうえで繰り返しになりますが、最初に詰められるのであれば、前任のコードフリーズ時期を契約・体制・リリース計画とセットで先に握るのが最善です。

外部要因の遅延を前提にしたバッファ設計

外部 ID 認証システムの調達や AWS 環境準備など、こちらでコントロールできない遅延は必ず発生します。「止まる前提」で、止まっている期間に並行で進められる改善タスクを計画に組み込んでおくと、スケジュール上も精神衛生上も効きました。

セキュリティと運用の境界は最初に合意しておく

ログ通知のような「便利だから入れた」ものが、後から監査・運用ルールとの兼ね合いで問題化することがあります。データの取り扱いに触れる範囲は、最初に確認・合意するのが結局一番速いです。

おわりに

初回リリースまでの半年は、技術的にも調整的にもなかなかハードでした。ただ、引き継ぎ案件で起きがちな「理解不足による手戻り」や「運用不能」を避けるために、機能開発の前にまず土台に投資する判断を取れたことは、結果として効いたと思います。

引き継ぎを検討している会社の方へ。受領したコードベースの状態がどうあれ、最初の数ヶ月で何に投資するかの優先順位は、その後の開発速度を大きく左右します。glucose 開発部としては、こうした「立て直しと開発を並走させる」仕事に向き合ってきた経験が増えてきています。似たような状況で悩んでいる方がいたら、気軽に相談いただければと思います。

それから、書きながら正直に思ったことをひとつ。引き継ぎ案件は大変は大変なのですが、他社が作ったプロダクション規模のインフラやコードを読み解き、自分たちの納得いく形に作り直していくプロセス自体は、なかなか個人開発では得られない種類の楽しさがあります。受託でしか味わえないこの感じを、これからも大事にしていきたいと思っています。