月曜日を台無しにしたSlackのメッセージ
「前のプラットフォームチームが去りました。リポジトリはこちらです。頑張ってください 🫡」
私はGitリポジトリを見つめていました。
47,000行のTerraformコード。x
temp2一つの状態ファイル。ゼロのモジュール。変数名はDO_NOT_TOUCH_ask_raj、__JHSNS_SEG_d25198f1_8__、そして私の大好きなもの——__JHSNS_SEG_d25198f1_9__。ラージは2年前に会社を去りました。
一年以上のシニアデボップエンジニアであれば、こんなものを引き継いでいます。47K行とはいかなくても、main.tfを開いて、キャリア選択を疑問に思ったことはあります。
これは「Terraformのベストプラクティス」に関する記事ではありません。それらは、運用する必要がなかった人によって書かれています。terraform plan は午前2時、3,000リソースの状態ファイルで、エンジニアリングのVPが見守っている間に起こりました。
これは生存ガイドです。
反パターン #1: モノリス状態ファイル(通称「キャリアの単一ポイント失敗」)
私が発見したこと
# main.tf — 8,400 lines
# "Managed" networking, compute, databases, DNS, IAM, monitoring,
# and somehow... a CloudFront distribution for a marketing site
# that was decommissioned in 2023.
resource "aws_vpc" "main" { ... }
resource "aws_instance" "api_server_1" { ... }
resource "aws_instance" "api_server_2" { ... }
# ... 200 more instances ...
resource "aws_rds_instance" "prod_db" { ... }
resource "aws_iam_role" "god_mode" { ... } # yes, really
単一の terraform apply が すべてを触りました ネットワーキング、データベース、コンピュート、DNS — すべてが1月のクリスマスライトのように絡み合っている。セキュリティグループルールの1つのタイプミス?おめでとう、あなたの plan が847のリソースを評価する必要があることを示し、TerraformはあなたのRDSインスタンスを交換する必要があると決定しました。
本当の危険
これはただの乱雑さではなく — これは 運用上の破滅です。以下が起こることです:
-
terraform planは 14 分 かかります。開発者はそれを停止しました。 - 状態ファイルロックは一度に一人しか作業できないことを意味します。
- どのミスもその影響範囲はインフラ全体です。
- 新しいチームメンバーは何かを触るのを恐れています(適切にです)。
どうやって解決したか(ダウンタイムなしで)
ステップ 1: 状態手術でterraform state mv
# First, I mapped resource dependencies visually
terraform graph | dot -Tsvg > infra-dependency-map.svg
# Then, split by domain boundaries
terraform state mv 'aws_vpc.main' -state-out=networking/terraform.tfstate
terraform state mv 'aws_subnet.public[0]' -state-out=networking/terraform.tfstate
terraform state mv 'aws_subnet.public[1]' -state-out=networking/terraform.tfstate
ステップ 2: バースト半径で状態の境界を紹介
私は変化頻度とバースト半径に基づいて五つの状態ファイルに分けました
| レイヤー | コンテンツ | 変化頻度 | 爆発半径 |
|---|---|---|---|
foundation |
VPC、サブネット、ルートテーブル | 月次 | 重大 |
security |
IAM、KMS、セキュリティグループ | 週次 | 重大 |
data |
RDS、ElastiCache、S3 | 稀少 | 災害的 |
compute |
ECS/EKS、ASGs、ALBs | 日次 | 高い |
edge |
CloudFront、Route53、WAF | 週間 | 中級 |
ステップ3: リモート状態データソースでそれらを接続します
# In compute/main.tf
data "terraform_remote_state" "networking" {
backend = "s3"
config = {
bucket = "company-terraform-state"
key = "foundation/terraform.tfstate"
region = "us-east-1"
}
}
resource "aws_ecs_service" "api" {
# Reference networking outputs safely
network_configuration {
subnets = data.terraform_remote_state.networking.outputs.private_subnet_ids
}
}
結果: terraform plan 14分から45秒に短縮されました。チームのスピードが3倍になりました。深夜2時の状態ロックに関するページの通知を受け取るのをやめました。
反パターン#2: コピペ帝国(別名「家にモジュールがある」)
私が発見したこと
environments/
├── dev/
│ └── main.tf # 1,200 lines
├── staging/
│ └── main.tf # 1,200 lines (95% identical to dev)
├── prod/
│ └── main.tf # 1,200 lines (90% identical... with 47 "hotfixes")
└── dr/
└── main.tf # 1,200 lines (copied from prod 8 months ago, never updated)
同じインフラストラクチャのコピーが4つあり、微妙なずれがあった。ステージングにはセキュリティグループルールがあり、プロダクションにはなかった。DRは3つのサービスが完全に欠けていた。誰も意図的にどの違いがあるか知らなかった。
なぜこれがシニアエンジニアを殺しているのか
あなたはできませんdiffこの問題から抜け出す方法です。ファイルは意図的(prodにはより大きなインスタンスがある)および偶然(誰かがdevでバグを修正したが、それを広めるのを忘れた)の両方の方法で分岐しています。あなたは真実の源がない.
実際に機能するリファクタリング戦略
一度にすべてを統合しようとしないこと.失敗した「ビッグバン」リファクタリングで3つのスプリントかけてステージングを1週間壊したから学んだ.
代わりに、ストラングラー・フィグパターンを使え.
# modules/api-platform/main.tf
variable "environment" {
type = string
validation {
condition = contains(["dev", "staging", "prod", "dr"], var.environment)
error_message = "Environment must be dev, staging, prod, or dr."
}
}
variable "config" {
type = object({
instance_type = string
min_capacity = number
max_capacity = number
enable_waf = bool
multi_az = bool
backup_retention = number
})
}
locals {
# Environment-specific defaults that document WHY they differ
env_config = {
dev = {
instance_type = "t3.medium"
min_capacity = 1
max_capacity = 2
enable_waf = false
multi_az = false
backup_retention = 1
}
prod = {
instance_type = "m5.xlarge"
min_capacity = 3
max_capacity = 20
enable_waf = true
multi_az = true
backup_retention = 35
}
}
}
鍵となる洞察は:すべての環境の違いはコードで意図的に文書化され、1200行のファイルに無意識の逸脱として隠されていません
。
反パターン#3: terraform apply -auto-approve YOLO パイプライン
.gitlab-ci.yml
deploy_prod:
stage: deploy
script:
- terraform init
- terraform apply -auto-approve # 🚨 WHAT
only:
- main
計画のアーティファクトがない。承認のゲートがない。差分レビューがない。mainにプッシュ → プロダクションでのインフラストラクチャの変更。コミット履歴は恐怖のストーリーを語っていた:
fix: revert the revert of the fix
fix: actually fix prod this time
fix: ok THIS one fixes it
revert: revert everything from today
実際にシニアエンジニアが必要とするもの
# .github/workflows/terraform.yml
name: "Terraform"
on:
pull_request:
paths: ['infrastructure/**']
push:
branches: [main]
paths: ['infrastructure/**']
jobs:
plan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Terraform Plan
id: plan
run: |
terraform init
terraform plan -no-color -out=tfplan \
-detailed-exitcode 2>&1 | tee plan_output.txt
continue-on-error: true
- name: Comment Plan on PR
uses: actions/github-script@v7
if: github.event_name == 'pull_request'
with:
script: |
const fs = require('fs');
const plan = fs.readFileSync('plan_output.txt', 'utf8');
const truncated = plan.length > 60000
? plan.substring(0, 60000) + '\n\n... truncated ...'
: plan;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `## Terraform Plan Output\n\`\`\`\n${truncated}\n\`\`\``
});
- name: Upload Plan Artifact
uses: actions/upload-artifact@v4
with:
name: tfplan
path: tfplan
apply:
needs: plan
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
environment: production # Requires manual approval
steps:
- uses: actions/checkout@v4
- name: Download Plan
uses: actions/download-artifact@v4
with:
name: tfplan
- name: Terraform Apply
run: terraform apply tfplan # Apply ONLY the reviewed plan
交渉不可能なルール:
- プランはPRで生成され、アーティファクトとして添付されます。
- 人間がプロダクション適用前に差分をレビューします。
- 適用は、レビューされた正確なプランを使用します(新しいプランではありません)。
production環境には、上級エンジニアからの手動承認が必要です。
アンチパターン #4: セキュリティの秘密 (時限爆弾的なコンプライアンス)
私が発見したこと
resource "aws_db_instance" "prod" {
engine = "postgres"
instance_class = "db.r5.2xlarge"
username = "admin"
password = "Pr0d_P@ssw0rd_2022!" # I wish I was joking
publicly_accessible = true # I really wish I was joking
}
パスワードは.tfファイル、状態ファイル、計画の出力、とGitの履歴にありました。漏洩する可能性のある場所が4つありました。そしてpublicly_accessible = trueは、このクズのアイスクリームサンデーの最後のトッピングでした。
The Fix (That Also Passes Audit)
# Use a data source to pull secrets at plan/apply time
data "aws_secretsmanager_secret_version" "db_password" {
secret_id = "prod/rds/master-password"
}
resource "aws_db_instance" "prod" {
engine = "postgres"
instance_class = "db.r5.2xlarge"
username = "admin"
password = data.aws_secretsmanager_secret_version.db_password.secret_string
publicly_accessible = false
# Prevent Terraform from detecting password "drift"
lifecycle {
ignore_changes = [password]
}
}
しかし、それだけでは不十分です。
ステートファイルまだ機密情報を含んでいます。完全な解決策:
# backend.tf
terraform {
backend "s3" {
bucket = "company-terraform-state"
key = "prod/data/terraform.tfstate"
region = "us-east-1"
encrypt = true # SSE-KMS encryption
kms_key_id = "arn:aws:kms:us-east-1:xxx:key/yyy"
dynamodb_table = "terraform-state-lock"
}
}
より厳格なS3バケットポリシー、アクセスログ、そして決して開発者に直接状態ファイルへのアクセスを与えない。terraform outputを使用する代わりに.
反パターン#5: 200行のネストされたブロックを持つ「神のリソース」
私が発見したこと
resource "aws_ecs_task_definition" "api" {
family = "api"
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
cpu = 1024
memory = 2048
execution_role_arn = aws_iam_role.ecs_execution.arn
task_role_arn = aws_iam_role.ecs_task.arn
container_definitions = jsonencode([
{
name = "api"
image = "company/api:latest" # 🚨 LATEST TAG IN PROD
portMappings = [{ containerPort = 8080 }]
environment = [
{ name = "DB_HOST", value = "prod-db.cluster-xxx.us-east-1.rds.amazonaws.com" },
{ name = "DB_NAME", value = "production" },
{ name = "REDIS_URL", value = "prod-redis.xxx.cache.amazonaws.com:6379" },
# ... 45 more environment variables hardcoded here ...
]
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = "/ecs/api"
"awslogs-region" = "us-east-1"
"awslogs-stream-prefix" = "api"
}
}
# ... 80 more lines of health checks, mount points, ulimits ...
}
])
}
問題が複雑化する:
- 環境変数がハードコードされている(SSM/Secrets Managerから取得していない)。
-
latestタグはデプロイが再現不能であることを意味する。 jsonencodeバルクはPRレビューでテスト不可能で差分比較もできない。- 環境変数の変更一つで完全なタスク定義の置き換えがトリガーされる。
リファクタリングされたバージョン
# Use templatefile for complex JSON — it's testable and readable
resource "aws_ecs_task_definition" "api" {
family = "api-${var.environment}"
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
cpu = var.task_cpu
memory = var.task_memory
execution_role_arn = aws_iam_role.ecs_execution.arn
task_role_arn = aws_iam_role.ecs_task.arn
container_definitions = templatefile("${path.module}/templates/api-container.json.tpl", {
image_tag = var.image_tag # Pinned, passed from CI/CD
environment = var.environment
db_host = data.aws_ssm_parameter.db_host.value
redis_url = data.aws_ssm_parameter.redis_url.value
log_group = aws_cloudwatch_log_group.api.name
aws_region = data.aws_region.current.name
})
}
リファクタリング・プレイブック(月曜日にこれをする)
この三年間の混乱を解きほぐした後、ここに有効な手順があります
第1週:分類と保護
# 1. Enable state file encryption and locking NOW
# 2. Add branch protection — no direct pushes to main
# 3. Run terraform plan and SAVE the output as your baseline
terraform plan -no-color > baseline_plan_$(date +%Y%m%d).txt
# 4. Enable detailed audit logging on your state bucket
第2週~第4週:モノリシスを分割
# Use terraform state list to inventory everything
terraform state list > all_resources.txt
wc -l all_resources.txt # Mine had 2,847 resources
# Group by service domain
grep "aws_vpc\|aws_subnet\|aws_route" all_resources.txt > networking.txt
grep "aws_iam\|aws_kms" all_resources.txt > security.txt
grep "aws_rds\|aws_elasticache\|aws_s3" all_resources.txt > data.txt
grep "aws_ecs\|aws_alb\|aws_autoscaling" all_resources.txt > compute.txt
第5週~第8週:段階的にモジュール化する
一つずつサービスをモジュールに移動させる。それぞれの移動後:
terraform planを実行する——それには変更がゼロであると表示されるべきです。- 計画に変更が示されている場合、バグがあります。進める前に修正してください。
- 他のシニアエンジニアからPRのレビューを取得してください。
- 適用し、24時間監視してください。
第9週~第12週:パイプラインを強化
- CIに
terraform validateとtflintを追加します。 - セキュリティスキャンのため、
checkovまたはtfsecを追加します。 - ずれ検知の実装(差異を通知するスケジュールされた計画).
- コスト見積もりを
infracostで追加.
Drift Detection Cron が私たちを救った
これは誰も話さないことです。完璧なリファクタリングの後でも、ずれが起こりますコンソールでクリックされました。自動修復ツールが変更を行います。Lambdaがセキュリティグループを修正します.
# .github/workflows/drift-detection.yml
name: "Drift Detection"
on:
schedule:
- cron: '0 6 * * 1-5' # Every weekday at 6 AM
jobs:
detect-drift:
runs-on: ubuntu-latest
strategy:
matrix:
layer: [foundation, security, data, compute, edge]
steps:
- uses: actions/checkout@v4
- name: Terraform Plan (Drift Check)
id: plan
working-directory: infrastructure/${{ matrix.layer }}
run: |
terraform init
terraform plan -detailed-exitcode -no-color > plan.txt 2>&1
echo "exitcode=$?" >> $GITHUB_OUTPUT
continue-on-error: true
- name: Alert on Drift
if: steps.plan.outputs.exitcode == '2'
run: |
# Exit code 2 = changes detected (drift!)
curl -X POST "${{ secrets.SLACK_WEBHOOK }}" \
-H 'Content-type: application/json' \
-d "{\"text\":\"🚨 Drift detected in *${{ matrix.layer }}* layer. Check the plan output.\"}"
最初の週だけで3件の不正なコンソール変更を検知しました.
混乱に直面した若手エンジニアへのアドバイス
すべてを一度にリファクタリングしないでください。 物事を壊して信用を失うことになる。
修正する前に、見つけたことを文書化する。 悪いことのスクリーンショットを取る。後日検証と評価のため必要になる。
開始する前に、リーダーシップの承認を得る。 "技術的負債のために3つのスプリントが必要です"は難しい販売です。"現在の設定では、インフラストラクチャの変更がインシデントを引き起こす可能性が40%あります"は予算を承認されます.
すべての
terraform state mvは別々の、レビューされたPRであるべきです. それは技術的に必要だからではなく、50のステップのうち37番目で何かが壊れたときに、クリーンなgit履歴でバイセクションしたいからです。目標は完璧なTerraformではない。目標は、チームが午後2時(2 AM)に安全に運用できるTerraformだ。若若ジュニアエンジニアが実行できません
terraform plan恐れず、リファクタリングは完了していない。
スクローラー用のTL;DR
| 反パターン | 直す | 優先順位 |
|---|---|---|
| モノリス状態ファイル | 爆発半径で分割し、頻度を変更 | P0 |
| 環境のコピー&ペースト | モジュール+環境設定 | P1 |
-auto-approveCI内 |
アーティファクトの計画+手動承認ゲート | P0 |
| 状態/コード内のシークレット | シークレットマネージャー+暗号化された状態+ignore_changes
|
P0 |
| ゴッドリソースとインラインJSON |
templatefile + SSMパラメータ |
P2 |
| ドリフト検知なし | スケジュールplanアラートあり |
P1 |
Terraformのコードベースを見て「誰がこれをしたんだろう?!」と空虚に囁いたことがあるなら、あなたは一人じゃない。私たちは皆そこにいた。良いニュースは?直せる。一つずつ状態を移動して。
この情報が役に立ったですか?もっと本番で試されたDevOpsのコンテンツを見るためにフォローしてください。私はドキュメントのハッピー・パスではなく、実際に本番で起こることについて書きます。










