システム運用の戦略について考える
はじめに
今回は、これまでのプロジェクトの経験を通して、システム運用の観点から学んだこと大切だと思ったことについて記事にしていきたいと思います。
初期段階で設計思想を統一する
プロジェクトの機能やサービスごとに設計思想が異なっている合、何らかの追加要望や修正が発生すると、サービスごとに変更する箇所を洗い出し方針を考える必要があります。設計の違いによって確認範囲が膨大になり、考慮漏れによる追加実装が発生するリスクも高まります。
- ある機能のデータ取得処理がAサービスではREST API、BサービスではGraphQLで実装されている
- AサービスとBサービスでStoreの処理や取得方法が異なる
- Aサービスは高度な料金計算処理を各コンポーネントで行なっているが、Bサービスではそもそも金額をAPIから取得している
このような場合、修正時に両方の仕組みを考慮して変更する必要があるため、機能が追加されるたびに、開発・テストの工数が増加していきます。プロジェクト立ち上げの初期段階からアーキテクチャ設計の共通ルールを策定し、各サービスが同じ設計思想を持つ必要があります。
サイロを減らす
サイロとは、組織やシステムが連携できず外部との情報共有が滞る状態を指します。サイロが進むことで、エンジニア間のナレッジ共有が不足したり、協力会社との連携が困難になったりする問題が発生します。特定の機能についてどちらが開発の責任を持つのかの線引きが曖昧なまま進んでしまい、一方の組織が独自に開発を進めた結果、後々問題になりやすいケースがあります。
例えば、本来バックエンドで実装する箇所をフロントが実装を行なっていたり、APIで持つ必要のある箇所をハードコーディングを行なっていたりしていた場合、その実装が前提で進んでしまうため、途中で変更することはほぼ不可能になる可能性が高くなります。
この課題を解決するために、エンジニア間や協力会社と、定期的な状況確認の機会を設けることで、コミュニケーションの習慣を定着させる必要があります。個人的にはサイロを減らすために、以下のようなやり方が効果的でした。
- 週に1回のミーティングを設定し、進捗や課題を共有する。
- 会議で決まったことや質問内容をSlack上で細かく記載し、参照可能な形にする。また口頭でも内容を伝える。
- 気軽に情報交換できる場を作る。
- ペアプログラミングを実施して、知識の属人化を防ぐ。
トイルを減らす試みを定期的に実行する
トイルとは、手作業であり、繰り返すことが可能であり、自動化可能な、長期的な価値がない作業のことを指します。トイルが増加するとチームの作業効率が低下し技術的な成長や新規機能開発の障害となります。
トイルの特徴
- 手作業であり、同じ作業を繰り返し行う必要のある作業。
- 短期的には解決可能であるが、長期的な価値がほとんどない。
- システムが成長するにつれて、トイルの量が比例して増加する。
- その性質上、自動化が可能である。
具体例
- 膨大なPDFの再発行処理を毎回手作業で実施している → スクリプトしてデータ整形を行いPostmanでループ処理を実行する。
- 毎回手順書を参照しながらデプロイを行なっている → CI/CDパイプラインを導入(Github Actionsであれば、scheduleを使用して定期的な実行またはスクリプトを作成してcronジョブを設定)することを検討する。
チームや組織の成長を妨げるトイルは削減することが望ましく、定期的にチーム内で業務の中のトイルを洗い出しどれだけ負荷がかかっているかを明確にして、トイルを削減する時間を作ることが必要です。
CI/CDパイプラインを初期段階で整備することの重要性
CI/CDパイプラインをプロジェクトの初期段階で構築することは、開発効率と品質向上の観点から非常に重要です。例えば、以下のようなツールや手法を導入することが考えられます。
-
Dependabot
を活用し、依存関係のバージョンアップを自動化 -
GitHub Releases
を利用したリリースの自動化 -
CodeQL
による静的解析の導入 - Lintやテストの自動実行によりコード品質を担保
CI/CDを早期に整備することで、継続的なデリバリーと安定した開発環境を実現し、プロジェクト全体のメンテナンス性を向上させることができます。
メンテナンス性を考慮する
プロジェクトが大きくなるにつれて、コード全体が複雑になり新規参入者にとって理解しにくいものになりがちです。モジュール間の密結合、一貫性のない命名や用語、何らかの問題を回避するための特別な対応。メンテナンスコストの増加により予算とスケジュールが増加していきます。コードが複雑になるにつれて、仕様変更の際に予想外のバグが混入してしまうリスクが大きくなります。そのため、可能な限り再利用可能な抽象化を意識し、変更が発生しやすい部分(例えば、値の追加やロジックの分岐)を考慮した設計を行うことが重要です。適切なモジュール分割、命名規則の統一、ドキュメントの充実化などを意識して、メンテナンスしやすいコードベースを維持していきます。
静的解析ツールを初期導入することの重要性
静的解析ツールをプロジェクト立ち上げの初期段階で導入することは重要です。開発の途中で静的解析ツールを導入しようとしても、すでに存在するコードで大量のエラーが発生し、導入が困難になってしまいます。JavaScript/TypeScriptであれば、ESLint
、StyleLint
、Prettier
を導入し、Pythonであればflake8
、isort
、mypy
、black
などで構築し、GitHub Actionsを組み合わせて、厳格に整備することで、コードレビュー前の自動検出や修正コストの削減、脆弱性を含むコードの検出を行います。
技術的負債への向き合い方と段階的な解消戦略を立てることの重要性
技術的負債とは開発者が一時的に早急な対応を選んで実装したり、修正せずに放置したりすることで、将来的に修正や改善が必要になる技術的な問題のことを指します。開発者はコードの品質とスピードのバランスをとる必要がありますが、実際は機能開発の締め切り制限や開発方法の選択ミスなどによって技術的負債が発生し、長年放置されることが多々あります。もちろんプロジェクトの締め切りを守り、予定通りリリースさせるために、一時的に技術的負債を許容することがあり事業の存続や発展に寄与することがありますが、開発者がこのトレードオフを選択をする場合は、短期的な利益と長期的なコストが発生することを認識する必要があります。
通常の開発業務でいっぱいになりがちにはなってしまいますが、この技術的負債と向き合うために、負債を整理する時間を確保しておき、「技術的負債」の見える化を行うことが重要です。技術的負債を一つ一つタスク化し、他の機能要件と同じように見ることができれば、技術的負債を解消している間は、開発のプロジェクトが止まるので、これだけのお金をかける必要が本当にあるのかどうかをよく検討することができ、「毎週金曜日に技術的負債に取り組む」ことや「通常の開発業務の終わりに計画を立てて開発を行い、テスト後にリリース」する作業をおこなうことが可能となります。
レガシーコード化による開発効率の低下を考慮する
初期開発時においては今まで積み上げてきたシステムもなく、単純な機能であることが多いため新機能の開発は比較的早く終わります。しかし、「早く」開発することを求めて構築されたシステムが、長年の蓄積により債務であるように徐々に開発スピードが鈍化してしまい、工数が肥大化してしまいます。同じような仕様であったとしても、考慮しなければいけない範囲が広がり、複雑性が増すからです。
また、長年のプロジェクトが継続した場合、人の出入りが起こりますが、中身を知らない開発者が取り組む際には予想に反して、時間のかかるものになってしまいます。この場合、開発初期のメンバーの作業時間感覚と中身を知らない開発者の時間感覚の差が生まれることになり、時々エンジニアの能力的な問題もしくは、努力の不足ではないかと言う考えに基づく軋轢を生み出してしまいます。
技術的負債であれば、整理をして画面上には影響がないが、性能や拡張性、運用性による要件としてタスク化を行い解消すると同時に、想定している工数の1.5倍〜を算出して見積もりを行い想定外の事態に対応する必要があります。
誰でもロールバック可能であること
リリース時にもし何か予期せぬ不具合が発生した際は、ロールバックを実施する必要があります。ですが、ロールバック作業は実行頻度が低いため、やり方を知らない人や慣れていない人がいる可能性があります。手順を明文化して、平時にテスト環境でロールバックの練習を行うことが大切です。Blue-Greenデプロイやカナリアリリースを活用したリリース管理することも場合によっては検討する必要があります。
心理的安定性を確保する
エンジニアの心理的安全性を確保することは、チームの生産性向上と健全な開発環境の維持に不可欠です。プロジェクトで何か失敗が発生し、報告が上がってきた場合、まずは報告してくれたことに対して「ありがとう」と伝えることを心がけ、失敗を個人の問題として責めるのではなく、チーム全体で再発防止策を考えることが重要です。定期的な1on1ミーティングを設け、細な問題でも気軽に共有できる環境を作り、問題が大きくなる前に対応できる仕組みを整えます。
システムの知識の保全を行う
メンバーの出入りがあった場合でも、システムに関する組織の知識が失われないようにすることが重要です。新メンバーへのスムーズな業務共有を実現するために、ドメイン知識を記載したドキュメントを定期的に整備し、説明の機会を設ける必要があります。
具体的な施策
- ドキュメントの維持・更新:設計仕様、アーキテクチャの概要、主要なビジネスルールなどを適切に記録し、常に最新の状態を保つ。
- オンボーディングプロセスの確立:新メンバーがスムーズにシステムを理解できるよう、体系的なオンボーディング資料を用意。
- PR後のレビュー: PR後のレビューを口頭で説明を行い、問題がなかったか、仕様面で抜け漏れがなかったかの説明を行う
また、実際の引き継ぎ時はペアプロ(2人1組でペアを組みながら開発を進める手法)を活用し、業務を通じて知識を伝えていきます。
ペアプロ時は特定のルールに沿って実施すると、理解度が増します。
ペアプロのルール
- ハドルを実施、1つのタスクで何回ペアプロしてもOK
- 初回以下のことを説明する
- チケットの概要(背景・目的、何を実装するのか)
- 実装のゴールの確認
- 期待される動作・仕様
- 影響する範囲を洗い出す
- 関連するファイルやコンポーネント、変更時のリスク
- 実装時
- 実装時は意図を説明しながら行う
Atomic Designでの管理の仕方を考える
Atomic Designを導入する意図は「コンポーネントの責務がより明確になる」、「実装の粒度やロジックの責務が明確になる。」、コンポーネントの再利用が可能であることがメリットとして挙げられます。
ただAtomic DesignにはAtoms・Molecules・Organisms
がありますが、チームで管理するメンバーが途中でいなくなった場合、そのナレッジが共有されず、それぞれの分割粒度がバラバラになりがちです。特に、Molecules
とOrganisms
の違いが分かりにくく、あっという間に大小さまざまコンポーネントが乱立する状況が生まれてしまいます。
原則として、Atomic・Molecules・Organisms
のいずれかのディレクトリにコンポーネントを作成する必要がありますが、機能によっては明確に区別することができず、個々の機能で独自のコンポーネントが作成され、ページが肥大化してしまうことになります。Atomic Designを維持していくのは管理メンバーがいなくなった際に難しく、管理コスト高いのではないかと思います。
現状、Scenario Based Design(シナリオベースデザイン)とAtomic Designの組み合わせが良いかと思われます。Scenario Based Designは整合性と再利用性に注目した、Atomic Designとは異なり、ユーザー毎の機能フローがコンポーネント分割の中心になります。
以下の記事では、ディレクトリ構成をcomponentsディレクトリとscenariosディレクトリに分割してます。
src/
├── components/ # Atoms, Molecules, Organismsをまとめる
│ ├── Atoms/
│ ├── Molecules/
│ └── Organisms/
└── scenarios/ # 特定の機能を持つComponentsのまとまり
├── ShoppingCart/
└── UserProfile/
Atomic DesignにおけるOrganisms
は、Molecules
を組み合わせて作られる、より大きなコンポーネントで構成されますが、個人的には、Molecules
, Organisms
の責務や違いを解消するため、Atoms
とOrganisms
のみで良いかと思います。Organismsも日本人に馴染みのある表現に変えてしまいます。
src/
├── components/ # Atoms, Containersをまとめる
│ ├── Atoms/
│ └── Containers/
└── scenarios/ # 特定の機能を持つComponentsのまとまり
├── ShoppingCart/
└── UserProfile/
クリーンなリポジトリを維持する
リポジトリのクリーンな状態を維持するためには、ブランチの運用を明確にし、指定したルールに基づいて適切に保護することが重要です。GitHubのBranch Protection Rulesを活用することきとで、特定のブランチに対するルールを設定し、安全なコード管理を実現できます。
- releaseブランチへのマージには必ずレビューを必要とする
- releaseブランチへのgit push --forceやCIがパスしていない変更を禁止する
- 承認後に新しい新しいコミットがプッシュされた場合に承認を取り消す
- マージ前のプルリクエストの作成を必須にする
適切なエラーログ設計を行う
問題が発生した時に調査するために、エラーログの出力が大きな助けになりますが、ログの設計が適切でなく、何のエラーか分からない場合、原因の特定に多くの時間がかかることがあります。ログのレベル、ログの出力場所、フォーマット(JSON形式や構造化ログを用い、解析しやすい形で出力)、出力内容(情報、処理結果、メッセージなど)を明確にして適切な表示を出力します。
- ログレベルの適切な分類:
WARN(軽微なエラーや想定外の動作)
、INFO(正常な処理の進捗状況)
、FATAL(致命的なエラーで、即時対応が必要なケース)
、ERROR(軽微なエラーや想定外の動作)
など - ログの出力場所を明確化:コンソール、ファイル、リモートログ管理ツール(Elasticsearch + Kibana、CloudWatchなど)を活用する。
- 統一されたログフォーマットを採用:JSON形式や構造化ログを用い、解析しやすい形で出力する。
- 重要な情報を記録:エラーメッセージ、発生時刻、影響範囲、リクエストID、スタックトレースなどを含める。
- コンテキスト情報を付与:どの機能・どの処理で発生したエラーかを明確にするため、関連するリクエスト情報やユーザー情報を含める。
- ノイズを減らす:不要なログの出力を抑え、実際に重要なログを見逃さないようにする。
また、定期的にログの運用状況を見直し、改善を行うことも重要です。
適切なキャッシュ戦略を行う
不要なAPI通信を削減するための適切なキャッシュ戦略は、フロントエンドのパフォーマンス向上において大きな役割を果たします。特に、大規模で長期間運用されているプロジェクトでは、重複したAPIリクエストが発生しやすく、サーバーやフロントエンドの負荷が増加するため、APIリクエストの管理が重要な課題となります。
キャッシュ戦略のポイント
- データの再利用を意識する:一度取得したデータをアプリケーションの Store(Pinia、Recoil、Jotai など)に格納し、不要なAPIリクエストを削減する。
- ブラウザキャッシュの活用:
Cache-Control
ヘッダーを適切に設定し、リクエストの頻度を制御する。 - メモリキャッシュの導入:コンポーネントのライフサイクルに応じてデータを保持し、頻繁なリロードを抑制する。
- リクエストの最適化:連続して同じAPIを呼び出すケースでは、lodashの
debounce
を使用してリクエストをまとめる。 - GraphQLのキャッシュ機能を活用:GraphQLを使用する場合は、Apollo Clientを利用し、適切なキャッシュ管理を行う。
共通処理のモジュール化・コードの関心を分離するタイミングを見極める
複数の箇所で使用される処理については、共通化を行うことで、コードの再利用性と保守性を高めることができますが、一方で、共通化できそうな箇所も内部の実装が複雑な場合、逆に保守性が悪化する可能性があります。「内部の実装が複雑なコード」、「変更頻度の高いコード」、「複雑な処理とページ固有の条件分岐が入り混じったコンポーネント」、「過度に汎用的なコンポーネント」については変更のたびに影響範囲が広がり、管理が困難になるため共通化を行わずに、コードを切り離したままの方が良い場合があります。
参考文献
Discussion