前置き
ソフトウェアアーキテクチャのコンポーネントの6大原則は、再利用可能で保守性の高いInfrastructure as Code(IaC)を設計する上で非常に重要な指針となります。
以下に、それぞれの6つの原則をIaCに適用した場合の具体的な事例を複数提示します。
IaCの環境では、是非とも常識にしてみてください。
その理由は、以下の記事にもあるように、インフラの制約がビジネス上のアジリティを阻害する要因になり得るからです。
1. 閉鎖性共通の原則 (CCP: The Common Closure Principle) 📦
同じ理由で、同じタイミングで変更されるコンポーネントは、同じ場所にまとめるべきである。
概説
これは変更の単位を基準にコンポーネントを分割する原則です。
IaCでは「同じライフサイクルを持つリソース群」や「同じチームが同じ目的で変更するリソース群」を一つのモジュールとしてまとめます。
以下に過去に書いた記事があるので参考にしてください。
事例
マイクロサービス単位のモジュール
あるマイクロサービスが「ECSサービス」「RDSデータベース」「専用IAMロール」「固有のセキュリティグループルール」で構成されているとします。
このサービスの機能追加や仕様変更(例: 必要なメモリ量の変更、DBへのアクセス権限の追加)が発生した場合、これらのリソースは常にセットで変更されます。
したがって、これら全てを単一のTerraformモジュール module "order-service" としてカプセル化するのは、CCPの優れた適用例です。
環境一式をプロビジョニングするモジュール
開発者が新しい機能開発を始める際に、サンドボックスとして「小規模なVPC」「小規模なKubernetesクラスタ」「コンテナレジストリ」「開発者用IAMユーザー」を一括で作成したいとします。
これらのリソースは「開発環境の払い出し」という単一の理由で同時に作成され、開発終了時には同時に破棄されます。
これらを module "developer-sandbox" としてまとめれば、ライフサイクル管理が非常に容易になります。
2. 再利用・リリース等価の原則 (REP: The Reuse/Release Equivalence Principle) 🔖
再利用の単位とリリースの単位は等価になるべきである。
概説
これは、再利用されるコンポーネントはバージョン管理され、リリースプロセスを経て提供されるべきという原則です。
これにより、利用者は予期せぬ変更から保護され、安定したバージョンを意図的に選択できます。
以下に過去に書いた記事があるので参考にしてください。
事例
バージョン管理された共有Terraformモジュール
プラットフォームチームが、社内の標準的なセキュリティ設定を組み込んだVPCモジュールを作成したとします。
このモジュールは専用のGitリポジトリで管理され、v1.0.0、v1.1.0 のようにGitのタグを使ってバージョンがリリースされます。
各アプリケーションチームは、自身のTerraformコードで
source = "git::our-repo.git//vpc?ref=v1.1.0" のようにバージョンを明示的に指定して利用します。
これにより、プラットフォームチームがVPCモジュールを更新してしまったとしても、利用側のインフラが意図せず変更されることはありません。
バージョン付きの共有Ansible Collection
セキュリティチームが、サーバーのセキュリティ基準(CISベンチマークなど)を適用するためのAnsible Role群を作成し、company.security_baseline というAnsible Collectionとしてパッケージ化したとします。
このCollectionはバージョン 2.5.0 として社内のAutomation Hubに公開されます。
運用チームは、Playbookで
ansible-galaxy collection install company.security_baseline:2.5.0 のようにバージョンを指定してインストールし、一貫性のあるセキュリティ設定を保証します。
3. 全再利用の原則 (CRP: The Common Reuse Principle) 🧩
コンポーネントの利用者は、自身が利用しないコンポーネントに依存すべきではない。
概説
これは利用者の視点でコンポーネントを分割する原則です。
一緒に使われることが多いリソースはまとめるべきですが、一緒に使われないリソースを同じコンポーネントに含めるべきではありません。
以下に過去に書いた記事があるので参考にしてください。
事例
巨大な「共通モジュール」のアンチパターン
よくある失敗例として、module "common-utils" のような巨大なモジュールを作り、その中に「S3バケット作成」「IAMユーザー作成」「ランダムな文字列生成」「セキュリティグループルール」など、関連性の低い機能を詰め込んでしまうケースがあります。
すると、あるチームがS3バケットを作りたいだけなのに、この巨大なモジュール全体に依存することになります。
IAMユーザー作成機能の変更が、S3バケットしか使っていないチームの terraform plan に影響を与えてしまうのは、この原則に反します。
解決策は、s3-module、iam-user-module のように、機能ごとに小さなモジュールに分割することです。
ただし、闇雲に小さくしすぎてはだめです!
データベースモジュールとキャッシュモジュールの分離
あるアプリケーションがPostgreSQLデータベースとRedisキャッシュを必要としているとします。ここで「データストアモジュール」という一つのモジュールを作り、db_type という変数でPostgreSQLとRedisを切り替えるように設計したとします。
しかし、PostgreSQLしか必要としない別の利用者は、Redisに関連する変数やリソース定義に不必要に依存してしまいます。
CRPに素直に従うならば、postgres-module と redis-module を別々に作成するのが適切な設計です。
4. 非循環依存関係の原則 (ADP: Acyclic Dependencies Principle) 🚫🔗
コンポーネントの依存関係グラフに循環(サイクル)があってはならない。
さて、ここからは結合に関する原則です。
概説
ADPは最も基本的かつ厳格な原則です。
循環依存はデッドロックを引き起こし、システムの変更や理解を不可能にします。
Terraformなど多くのIaCツールは、循環依存を検知するとエラー返して実行を停止します。
以下に過去に書いた記事があるので参考にしてください。
事例①. セキュリティグループとリソースの循環依存(典型的なアンチパターン)
状況
aws_security_group(SG-A)を作成し、そのルールで別のリソース(例: aws_lb ロードバランサー)からのアクセスを許可したい。
一方で、そのロードバランサーにはSG-Aをアタッチしたい。
問題
LBはSG-Aを必要とし、SG-AはLBのIDやIPをルール内で必要とするため、LB -> SG-A -> LB という循環が発生し、TerraformはCycle errorを出力します。
解決策
依存関係を一方通行にします。
まずSG-Aをルールなしで作成し、LBにアタッチします。
その後、aws_security_group_ruleリソースを使って、LBからのアクセス許可ルールを後からSG-Aに追加します。
これにより、LB -> SG-A と SG-Rule -> LB という2つの依存関係はあっても、サイクルは解消されます。
事例②. モジュール間の循環参照
状況
module "application" は、module "network" が作成したサブネットIDを必要とします。
ここまでは正常です。
しかし、ネットワークチームが誤って、module "network" の中で、特定のアプリケーションのIPアドレスをNACL(ネットワークACL)で許可するためにmodule "application"の出力値(IPアドレス)を参照しようとしました。
問題
これにより、application -> network -> applicationというモジュールレベルでの循環が発生します。これはインフラ全体の計画と適用を不可能にします。
解決策
依存の方向を一方通行に保ちます。
ネットワークモジュールは、アプリケーションの具体的な存在を知るべきではありません。
アプリケーション固有のネットワークルールは、アプリケーションモジュール側でaws_network_acl_ruleのようなリソースを使って、ネットワークモジュールが作成したNACLに対して追加すべきです。
5. 安定依存の原則 (SDP: Stable Dependencies Principle) 🏛️➡️🏠
依存の方向は、より安定したコンポーネントの方向に向かうべきである。
概説
IaCにおける「安定」とは「変更頻度が低い」ことを意味します。
一度構築したらめったに変更されないコアなインフラ(VPCなど)は安定しており、機能追加のたびに変更されるようなアプリケーションのデプロイ設定は不安定です。
この原則は、不安定なコンポーネントが、安定したコンポーネントに依存すべきであり、
その逆は許されないことを示しています。
以下に過去に書いた記事があるので参考にしてください。
事例①. アプリケーションからネットワークへの依存
正しい依存
アプリケーションのECSサービスを定義するモジュール(不安定)は、社内の標準VPCを定義するモジュール(非常に安定)に依存し、そのサブネットIDやセキュリティグループIDを利用します。
誤った依存(違反)
VPCモジュール(安定)の中に、特定のアプリケーションAのコンテナIPを許可するNACLルールがハードコードされている状態。
これでは、アプリケーションAのIPが変わるたびに、社内全体が依存する安定したVPCモジュールを変更する必要があり、多大なリスクと影響を生みます。
事例②. サービスから共有基盤への依存
正しい依存
新機能のキャンペーンサイトのインフラコード(不安定、キャンペーン終了後には破棄される)は、全社で共有されている監視基盤(Prometheus)やログ収集基盤(Fluentd)のモジュール(安定)に依存し、メトリクスやログの送信先エンドポイントを取得します。
誤った依存(違反)
共有監視基盤のモジュール(安定)が、「もしキャンペーンサイトAが存在するなら、このアラート設定を有効にする」といった、不安定なコンポーネントの存在を意識したロジックを持ってしまうこと。
6. 安定度・抽象度等価の原則 (SAP: Stable Abstractions Principle) 🏛️=📝
コンポーネントの抽象度は、その安定度と比例すべきである。
概説
この原則は、SDPを補完します。
安定したコンポーネントは抽象的であるべきで、不安定なコンポーネントは具体的であるべきです。
IaCにおける「抽象的」とは
多くの設定がハードコードされておらず、入力変数によって振る舞いが決まる。
特定のアプリケーション名などを知らず、汎用的に使える。
IaCにおける「具体的」とは
特定のアプリケーションのコンテナイメージ名や環境変数など、具体的な値が多くハードコードされている。
以下に過去に書いた記事があるので参考にしてください。
事例
紹介する事例集の前に、以下の2軸マトリクスを見てください。
横軸Iがモジュールの不安程度、縦軸Aがモジュールの抽象度を表します。
VPCモジュール(安定・抽象的)
これは正しい状態です。
社内の標準VPCモジュールは、非常に安定しています。
したがって、非常に抽象的であるべきです。
CIDRブロック、サブネット構成、AZの数、タグといった全ての要素が入力変数で定義され、特定のアプリケーション名や環境名が一切ハードコードされていません。
これにより、あらゆるプロジェクトで再利用可能になります。
アプリケーションデプロイモジュール(不安定・具体的)
これは正しい状態です。
あるマイクロサービスのECSデプロイを定義するモジュールは、機能追加のたびに変更されるため不安定な状態です。
したがって、具体的であっても問題ありません。
そのサービスのコンテナイメージ名("nginx:1.21.6")、必要な環境変数(DATABASE_URL)、CPU/メモリの割り当てといった具体的な値がコード内に記述されています。
このモジュールは、より抽象的なVPCモジュールに依存します。
「苦痛のゾーン」に陥ったモジュール(安定だが具体的)
これはアンチパターンです。
全社で利用される共有IAMロールのモジュール(安定)の中に、
「もしサービス名が"payment-service"なら、このS3バケットへのアクセス権限を追加する」という具体的なロジックがif文で記述されている状態。
このモジュールは安定しているのに具体的すぎるため、新しいサービスが追加されるたびに、この基盤モジュールを修正する必要があり、非常に保守が「苦痛」になります。
なので、変動からの保護をしてあげるべきです。
「無駄のゾーン」に陥ったモジュール(不安定だが抽象的)
これはYAGNIに反した、アンチパターンです。
ある一つのアプリケーションのためだけのデプロイモジュール(不安定)なのに、将来あらゆるアプリで使えるようにと過剰に汎用化され、大量の入力変数を持つ複雑なモジュールになっている状態。
このモジュールは頻繁に変わるのに抽象的すぎるため、誰も使い方を理解できず、結局「無用」なものになります。
