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

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

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
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