Slide 1

Slide 1 text

チームメンバーが 迷わないIaC設計 読むときも、実行するときも、どんなときも 2026/02/28 SRE Kaigi 2026 延長戦 @hymaaa_k

Slide 2

Slide 2 text

hym(@hymaaa_k) 株式会社スリーシェイクでプラットフォーム基盤を構築 したり、SRE支援を行っています。 イベントに参加・運営したりするイベント驚き屋 IaCが難しいよねって世論に対抗したい 2

Slide 3

Slide 3 text

今日お話しすること IaCの課題を設計で解消する こんな経験ありませんか? diff追ってたはずが、変数→tfvars→module→……「あれ、何見てたんだっけ?」 plan 打ったら関係なさそうなリソースまで出てきて「これ関係ある…?」 「ここ見てね」って言ったのに翌日「どこから読めばいいですか?」って聞かれる 技術的な難しさではなく、設計の問題として IaC を捉え直す 3

Slide 4

Slide 4 text

なぜIaCは難しいのか 「処理」ではなく「状態」を書くから 通常のプログラミング = 処理を書く 「何をするか」を書く コードを一方向に読めば結果が予測できる IaC = 状態を書く 「どうあるべきか」を書く 「今どうなっているか」が別に存在する コードだけ読んでも答えがわからない 状態を扱う以上、コンテキストスイッチは構造的に多くなりがち 4

Slide 5

Slide 5 text

IaCで「迷う」とは? 状態を扱うから生まれる3つの迷い 読むだけではわからない 変数や分岐を自分で処理しない と、最終的な状態が組み立てら れない 差分が読めない 変更の結果が意図通りか、影響 範囲はどこまでか、判断できな い どこまで抽象化すべきかわから ない 何を隠して何を見せるか、その 線引きが判断できない 根っこにあるのはコンテキストスイッチだと思っています 5

Slide 6

Slide 6 text

心がけている5つの原則 まずはコードの構造の話から 6

Slide 7

Slide 7 text

原則 1: 条件分岐を減らす コードを読むだけで状態がわかるようにする 条件分岐があると、コードを読んだだけでは最終的な値がわからない。読み手が頭の中で実行する必要がある コンテキストスイッチが発生する ファイル間の移動: 変数定義を探す → tfvarsの値を確認 脳内処理: 条件を組み立てる、レビュー時はすべてのパターンを追い直す 7

Slide 8

Slide 8 text

原則 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

Slide 9

Slide 9 text

原則 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

Slide 10

Slide 10 text

原則 2: 構造で語る 管理単位を分けて認知のコンテキストスイッチを減らす 管理単位が大きいと、変更対象以外のリソースまで認知範囲に入ってくる コンテキストスイッチが発生する 差分出力のノイズ: 変えたのはECSだけなのに、VPC・ALB・RDSも表示される 安全確認の往復: 「これ触って大丈夫?」と確認して回る 10

Slide 11

Slide 11 text

原則 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

Slide 12

Slide 12 text

原則 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

Slide 13

Slide 13 text

原則 3: 分割粒度はライフサイクル 変更頻度で分けて実行時のコンテキストスイッチを減らす Terraform State、CDK Stack、Pulumi Stack など管理単位の呼び方は違っても、変更頻度が違うものが同居している と実行のたびに余計な確認が発生します コンテキストスイッチが発生する 無関係な差分の混入: アプリをデプロイしたいだけなのに、ネットワーク層の差分も表示される 毎回の安全確認: 「これ触って大丈夫?」と確認する往復が毎回発生 13

Slide 14

Slide 14 text

原則 3: 悪い例 platform/ ├── alb.tf # 月1回の変更 ├── ecs_cluster.tf # 月1回の変更 ├── ecs_service.tf # 日次でデプロイ ← ここだけ頻度が違う └── ecs_task_def.tf # 日次でデプロイ ← ここだけ頻度が違う 役割で分けた結果、変更頻度が全く違うリソースが同居。planのたびにクラスタやALBの差分も表示される 変更頻度の違うリソースが同居し、毎回の安全確認が避けられない 14

Slide 15

Slide 15 text

原則 3: 良い例 小さいプロダクト → プロダクトごと product-a/ # 変更頻度が近いリソースが自然にまとまる product-b/ shared/ # 共通リソース プロダクト内のリソースは変更タイミングが近いので、そ のまま管理単位になる 大きいプロダクト → レイヤーで分割 01-network/ # 月1回 02-compute/ # 週1回 03-apps/ # 日次デプロイ プロダクト内で変更頻度に差が出てきたら、レイヤーで分 ける 変更頻度が近いものだけが同居する。確認対象が減り集中できる 15

Slide 16

Slide 16 text

「どこまでやるか」の線引き 抽象化と管理範囲の話 16

Slide 17

Slide 17 text

原則 4: 目的を考えた抽象化 実装へのコンテキストスイッチをコントロールする IaCの抽象化(module、Construct等)は、利用者と実装の境界を決める設計判断です 手段は2つあり、リソースの扱い方が異なります テンプレート(共通化) リソースを固定し、繰り返しを減らす ←→ カプセル化(隠蔽) 関心事だけ受け取り、リソースは内部で決定する テンプレートはコンテキストスイッチのコストを下げ、カプセル化はさせない 17

Slide 18

Slide 18 text

原則 4: 2つの手段 誰が作り、誰が使うかで設計が変わる 目的と手段がブレると、コンテキストスイッチが制御できなくなる インフラ・ SREチーム → 抽象化 中身も 把握する → インフラ・ SREチーム 作る人 = 使う人。繰り返しを減らして整理する 抽象化 = DRY原則。同じリソース定義を書かせない 共通化 Platform チーム → 抽象化 中身は 見せない → App チーム 作る人 ≠ 使う人。複雑さを吸収して迷わせない 抽象化 = インターフェース。チーム間の境界を定義する 隠蔽 18

Slide 19

Slide 19 text

原則 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

Slide 20

Slide 20 text

原則 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

Slide 21

Slide 21 text

原則 5: IaCだけで管理しない 不要なコンテキストスイッチを避ける どのIaCツールでも、全てをコード化しようとすると条件分岐が増え、コンテキストスイッチが増えます コンテキストスイッチが増える 初回セットアップをTerraformで管理 → is_first_run 変数を確認 → 「今は初回?」と考える → enable_locking 変数 も確認 → 「ロックは有効?」と考える... 本来1回きりの作業に、永続的なコンテキストスイッチが残る 21

Slide 22

Slide 22 text

原則 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

Slide 23

Slide 23 text

原則 5: 良い例 # docs/setup.md ## 初回セットアップ手順 1. S3バケットを手動で作成 aws s3 mb s3://my-terraform-state 2. backend設定を追加 3. terraform initを実行 シンプルなドキュメントとして記述 手順書を見るだけで完結。コード内の条件分岐を確認する往復が不要 23

Slide 24

Slide 24 text

まとめ 5つの原則 1. 分岐を減らす — 読むだけで結果がわかる 2. 構造で語る — 見る範囲を限定する 3. ライフサイクルで分割 — 変更対象だけに集中する 4. 目的を考えた抽象化 — 実装へのコンテキストスイッ チをコントロールする 5. IaCだけで管理しない — コード化しない判断をする 明日からできること まずは自分のIaCで ? や if を検索して、条件分岐 の数を数えてみる 1つのStateやStackが管理するリソース数を確認す る。多すぎたら分割を検討 余裕があれば、moduleの目的が「共通化」か「隠 蔽」かチームで話してみる そのIaC、何回コンテキストスイッチが発生しますか? 24

Slide 25

Slide 25 text

about 3-shake 25

Slide 26

Slide 26 text

We are Hiring 3-shakeは一緒にSRE界隈を盛り上げてくれる仲間を大募集中です! Mobility、FinTech、通信など大規模SREを存分に経験できます 是非、カジュアル面談しましょう! 26

Slide 27

Slide 27 text

ありがとうございました ご質問・ご相談はお気軽にお問い合わせください @hymaaa_k | https://3-shake.com

Slide 28

Slide 28 text

Appendix 抽象化の2つの手段 — 共通化と隠蔽 28

Slide 29

Slide 29 text

抽象化の2つの手段 誰が作り、誰が使うかで設計が変わる 同じmoduleでも、目的が違えばインターフェースが変わる インフラ・ SREチーム → module 中身も 把握する → インフラ・ SREチーム 作る人 = 使う人。繰り返しを減らして整理する 共通化 Platform チーム → module 中身は 見せない → App チーム 作る人 ≠ 使う人。複雑さを吸収して迷わせない 隠蔽 29

Slide 30

Slide 30 text

共通化: インフラ・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

Slide 31

Slide 31 text

隠蔽: 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

Slide 32

Slide 32 text

共通化と隠蔽の比較 共通化 隠蔽 誰が作る インフラ・SREチーム Platformチーム 誰が使う 作った本人たち Appチーム インターフェース インフラの関心事を全て露出 アプリの関心事だけ module内部 見る前提 見なくていい 例外対応 moduleを使わない判断をする module内部で吸収 パラメータ数 多い 少ない 目指すもの 繰り返させない 迷わせない どちらが正しいかではない。目的が違えばインターフェースが変わる 共通化なのに隠蔽のインターフェース → SREチームが迷う(中で何が起きてるかわからない) 隠蔽なのに共通化のインターフェース → Appチームが迷う(パラメータ多すぎ) 32

Slide 33

Slide 33 text

テンプレートにリソースの増減が入ったら リソースの変化パターンごとの対処 数が増える(例: レプリカ、サブ ネット) count や for_each で動的なテ ンプレートにする。リソースの種 類は固定のまま 0→1で生える(例: ListenerRule) それはこのmoduleが管理すべきも のではない。module外で管理す る 条件で消える(例: ServiceDiscovery) それはこのmoduleが管理すべきも のではない。最初からmoduleに 含めない テンプレートはリソース宣言を固定するもの。増減が必要ならmoduleの境界を見直す 33