Upgrade to Pro — share decks privately, control downloads, hide ads and more …

チームメンバー迷わないIaC設計

Avatar for hayama hayama
February 28, 2026

 チームメンバー迷わないIaC設計

Avatar for hayama

hayama

February 28, 2026
Tweet

More Decks by hayama

Other Decks in Technology

Transcript

  1. なぜIaCは難しいのか 「処理」ではなく「状態」を書くから 通常のプログラミング = 処理を書く 「何をするか」を書く コードを一方向に読めば結果が予測できる IaC = 状態を書く

    「どうあるべきか」を書く 「今どうなっているか」が別に存在する コードだけ読んでも答えがわからない 状態を扱う以上、コンテキストスイッチは構造的に多くなりがち 4
  2. 原則 1: 悪い例 resource "aws_instance" "app" { instance_type = var.env

    == "prod" ? ( var.workspace == "shared" ? "t3.xlarge" : "t3.large" ) : "t3.micro" monitoring = var.env == "prod" && var.workspace != "batch" root_block_device { volume_size = var.env == "prod" ? ( var.workspace == "batch" ? 200 : 100 ) : 20 } } 2つの変数の組み合わせで分岐(CDKの if 文やPulumiの条件式でも同じ問題が起きる) 3属性 × 2変数 = 読み手が処理する分岐が6箇所 8
  3. 原則 1: 良い例 構造で解決する # prod/shared/main.tf resource "aws_instance" "app" {

    instance_type = "t3.xlarge" monitoring = true root_block_device { volume_size = 100 } } prod/shared/main.tf を開けば答えがわかる。分岐ゼロ データで解決する locals { config = { prod-shared = { type = "t3.xlarge" vol = 100, mon = true } prod-batch = { type = "t3.large" vol = 200, mon = false } dev-default = { type = "t3.micro" vol = 20, mon = false } } }["${var.env}-${var.workspace}"] 組み合わせを表にする。計算不要で「表を見るだけ」 どちらもコンテキストスイッチを不要にする。読むだけで答えがわかる 9
  4. 原則 2: 悪い例 Terraform terraform/ ├── main.tf # VPC、ALB、ECS、RDS、 │

    # S3、CloudFront...全部入り ├── variables.tf └── outputs.tf 1つのStateに全リソースが同居。 terraform plan で100個 のリソースが表示される CDK export class MyStack extends Stack { constructor(scope: Construct, id: string) { // 全部ここに3000行... const vpc = new ec2.Vpc(...); const alb = new elbv2.ApplicationLoadBalancer(...); const cluster = new ecs.Cluster(...); const db = new rds.DatabaseInstance(...); } } 1つのStackに全リソースが同居。 cdk synth で全リソース が一気に出力される 変更の影響範囲を絞れない。見たい差分だけを見ることができない 11
  5. 原則 2: 良い例 Terraform - Stateを分割 terraform/ ├── network/ #

    State1: VPCだけ ├── platform/ # State2: ALB、ECSだけ └── data/ # State3: RDS、S3だけ terraform plan の出力がState単位に限定される。 「今は ネットワークだけ」と集中できる CDK - Stackを分割 // NetworkStackはVPCだけ class NetworkStack extends Stack { ... } // PlatformStackはALB、ECSだけ class PlatformStack extends Stack { ... } cdk synth NetworkStack で対象を限定できる 分割すれば見る範囲が限定される。変更対象だけに集中できる 12
  6. 原則 3: 分割粒度はライフサイクル 変更頻度で分けて実行時のコンテキストスイッチを減らす Terraform State、CDK Stack、Pulumi Stack など管理単位の呼び方は違っても、変更頻度が違うものが同居している と実行のたびに余計な確認が発生します

    コンテキストスイッチが発生する 無関係な差分の混入: アプリをデプロイしたいだけなのに、ネットワーク層の差分も表示される 毎回の安全確認: 「これ触って大丈夫?」と確認する往復が毎回発生 13
  7. 原則 3: 悪い例 platform/ ├── alb.tf # 月1回の変更 ├── ecs_cluster.tf

    # 月1回の変更 ├── ecs_service.tf # 日次でデプロイ ← ここだけ頻度が違う └── ecs_task_def.tf # 日次でデプロイ ← ここだけ頻度が違う 役割で分けた結果、変更頻度が全く違うリソースが同居。planのたびにクラスタやALBの差分も表示される 変更頻度の違うリソースが同居し、毎回の安全確認が避けられない 14
  8. 原則 3: 良い例 小さいプロダクト → プロダクトごと product-a/ # 変更頻度が近いリソースが自然にまとまる product-b/

    shared/ # 共通リソース プロダクト内のリソースは変更タイミングが近いので、そ のまま管理単位になる 大きいプロダクト → レイヤーで分割 01-network/ # 月1回 02-compute/ # 週1回 03-apps/ # 日次デプロイ プロダクト内で変更頻度に差が出てきたら、レイヤーで分 ける 変更頻度が近いものだけが同居する。確認対象が減り集中できる 15
  9. 原則 4: 2つの手段 誰が作り、誰が使うかで設計が変わる 目的と手段がブレると、コンテキストスイッチが制御できなくなる インフラ・ SREチーム → 抽象化 中身も

    把握する → インフラ・ SREチーム 作る人 = 使う人。繰り返しを減らして整理する 抽象化 = DRY原則。同じリソース定義を書かせない 共通化 Platform チーム → 抽象化 中身は 見せない → App チーム 作る人 ≠ 使う人。複雑さを吸収して迷わせない 抽象化 = インターフェース。チーム間の境界を定義する 隠蔽 18
  10. 原則 4: 悪い例 module "service" { source = "./modules/service" family

    = "user-api" image = "ecr.../user-api:v1.2.3" cpu = 256 memory = 512 container_port = 8080 subnet_ids = module.network.private_ids vpc_id = module.network.vpc_id # 特定のサービスのために追加されたパラメータ legacy_port_mapping = { 8080 = 80 } # → ListenerRuleが追加される skip_service_discovery = true # → ServiceDiscoveryが消える } リソースの増減が入り込み、状態だけでなくリソースまで管理・確認が必要になっている 例外対応のために、実装を確認するコンテキストスイッチが増える 19
  11. 原則 4: 良い例 テンプレート(繰り返しを避けたい) module "service" { source = "./modules/service"

    family = "user-api" image = "ecr.../user-api:v1.2.3" cpu = 256 memory = 512 container_port = 8080 subnet_ids = module.network.private_ids vpc_id = module.network.vpc_id } # 例外が必要なら module を使わず直接書く DRY原則。インフラの関心事を変数として全て露出する カプセル化(複雑さを隠したい) module "service" { source = "./modules/service" app_name = "user-api" image = "ecr.../user-api:v1.2.3" container_port = 8080 legacy_port_mapping? → 対応 skip_service_discovery? → 対応 } インターフェース。Appチームの関心事(app名・image・ port)だけを渡す 目的が明確なら、不要なコンテキストスイッチが発生しない 20
  12. 原則 5: 悪い例 # 初回セットアップのために複雑な条件分岐 resource "aws_s3_bucket" "state" { count

    = var.is_first_run ? 1 : 0 # ... } resource "aws_dynamodb_table" "lock" { count = var.is_first_run && var.enable_locking ? 1 : 0 # ... } 1回きりの作業をコード化し、複雑な条件分岐を追加 is_first_run、enable_locking など、複数の変数を確認して理解する往復が毎回発生する 22
  13. 原則 5: 良い例 # docs/setup.md ## 初回セットアップ手順 1. S3バケットを手動で作成 aws

    s3 mb s3://my-terraform-state 2. backend設定を追加 3. terraform initを実行 シンプルなドキュメントとして記述 手順書を見るだけで完結。コード内の条件分岐を確認する往復が不要 23
  14. まとめ 5つの原則 1. 分岐を減らす — 読むだけで結果がわかる 2. 構造で語る — 見る範囲を限定する

    3. ライフサイクルで分割 — 変更対象だけに集中する 4. 目的を考えた抽象化 — 実装へのコンテキストスイッ チをコントロールする 5. IaCだけで管理しない — コード化しない判断をする 明日からできること まずは自分のIaCで ? や if を検索して、条件分岐 の数を数えてみる 1つのStateやStackが管理するリソース数を確認す る。多すぎたら分割を検討 余裕があれば、moduleの目的が「共通化」か「隠 蔽」かチームで話してみる そのIaC、何回コンテキストスイッチが発生しますか? 24
  15. 抽象化の2つの手段 誰が作り、誰が使うかで設計が変わる 同じmoduleでも、目的が違えばインターフェースが変わる インフラ・ SREチーム → module 中身も 把握する →

    インフラ・ SREチーム 作る人 = 使う人。繰り返しを減らして整理する 共通化 Platform チーム → module 中身は 見せない → App チーム 作る人 ≠ 使う人。複雑さを吸収して迷わせない 隠蔽 29
  16. 共通化: インフラ・SREチームの内部整理 同じパターンの繰り返しを減らす 呼び出し側(SREチーム自身が書く) module "service" { source = "./modules/service"

    family = "user-api" image = "ecr.../user-api:v1.2.3" cpu = 256 memory = 512 container_port = 8080 subnet_ids = module.network.private_ids vpc_id = module.network.vpc_id alarm_actions = [aws_sns_topic.alert.arn] } module内部(チーム全員が読む前提) # ECS TaskDefinition → パラメータで構成 # ECS Service → パラメータで構成 # TargetGroup → パラメータで構成 # CloudWatch Alarm → パラメータで構成 # # 「何が作られるか」は呼び出し側から # 全て把握できる。 # moduleはボイラープレートの削減が目的 特徴: インターフェースは「インフラの関心事」を全て露出する。チーム内の全員がmoduleの中身を理解している前提。 中身へのコンテキストスイッチがしやすい設計 30
  17. 隠蔽: Platformチーム → Appチームへの提供 アプリチームにインフラの詳細を見せない Appチームが書くコード module "service" { source

    = "git::https://.../modules/service" app_name = "user-api" image = "ecr.../user-api:v1.2.3" container_port = 8080 replicas = 3 # これだけ。あとはPlatformチームが面倒を見る } module内部(Appチームは触らない) # VPC, Subnet → 自動選択 # ALB, TargetGroup → 自動作成 # SecurityGroup → ベストプラクティス適用 # CloudWatch Alarm → 標準メトリクス設定 # IAM Role → 最小権限で自動生成 特徴: インターフェースは「アプリの関心事」だけ。ネットワーク・監視・セキュリティはmodule内で決定する。Appチ ームが中身を見る必要がない=コンテキストスイッチが発生しない 31
  18. 共通化と隠蔽の比較 共通化 隠蔽 誰が作る インフラ・SREチーム Platformチーム 誰が使う 作った本人たち Appチーム インターフェース

    インフラの関心事を全て露出 アプリの関心事だけ module内部 見る前提 見なくていい 例外対応 moduleを使わない判断をする module内部で吸収 パラメータ数 多い 少ない 目指すもの 繰り返させない 迷わせない どちらが正しいかではない。目的が違えばインターフェースが変わる 共通化なのに隠蔽のインターフェース → SREチームが迷う(中で何が起きてるかわからない) 隠蔽なのに共通化のインターフェース → Appチームが迷う(パラメータ多すぎ) 32
  19. テンプレートにリソースの増減が入ったら リソースの変化パターンごとの対処 数が増える(例: レプリカ、サブ ネット) count や for_each で動的なテ ンプレートにする。リソースの種

    類は固定のまま 0→1で生える(例: ListenerRule) それはこのmoduleが管理すべきも のではない。module外で管理す る 条件で消える(例: ServiceDiscovery) それはこのmoduleが管理すべきも のではない。最初からmoduleに 含めない テンプレートはリソース宣言を固定するもの。増減が必要ならmoduleの境界を見直す 33