問い

既存のAWSインフラをTerraformに移行するとき、すべてをコード管理にすべきでしょうか。

VPCからRoute 53まで23リソースを terraform import で取り込んだ結果、私の答えはNoになりました。

結論

IaC化の境界は、「機密性」「変更頻度」「安全装置」の3つの軸で引くのが良いと思います。

「技術的にTerraformで管理できるか」ではなくて、「運用としてどう管理するべきか」で判断するのが良いです。そして、この境界線はチーム間の責任分界点と一致します。

前提

今回の移行対象は、ステージング環境のAWSインフラ全体でした。VPC、Subnet、Security Groupといったネットワーク層から、ECS、RDS、CloudFront、Route 53まで、23種類のリソースを約3ヶ月ほどかけて terraform import で取り込みました。

最初は「全部Terraform管理にすればそれで安心でしょう」と思っていました。全てコードになっていれば差分が追えるし、レビューもできる。中途半端にコード管理外にすると二重管理になるので可能な限り全てコード化するのが善と思っていました。

しかし、実際に取り込んでいくなかでTerraform管理にすると逆にリスクが高まるものや、管轄が違うのにTerraformが口を出してしまうものもあることに気がつきました。

以下、軸に沿ってTerraformの具体の話をします。

軸1:機密性 — tfstateは平文である

Secrets Managerをimportしたとき、最初は aws_secretsmanager_secret(箱)と aws_secretsmanager_secret_version(中身)の両方を取り込もうとしました。

ここで気づいたのがtfstateファイルにはリソースの属性値が平文で記録されるということです。

DB接続情報やAPIキーをTerraformで管理すると、stateファイルにそれらが平文で残ってしまいます。terraform plan の出力にも表示されます。

ということは、S3バックエンドに保存されているstateファイルを読める人は、全ての機密値を読めることになってしまいます。

これは技術的な制約であり、Terraformの設計思想そのものと分かりました。stateはインフラの現在状態のスナップショットであり、暗号化する仕組みは持っていない。

したがって、Secrets Managerの設計判断は以下のようになっていることが理解できました。

  • 箱(aws_secretsmanager_secret) → Terraform管理。シークレットの存在自体はインフラ構成の一部
  • 中身(aws_secretsmanager_secret_version) → コンソールで手動管理。stateに値を残さない

判断基準はシンプルに、「その値がGitやstateに残って問題ないか」。問題があるなら、Terraform管理にしないのがベターです。

この判断は組織設計にも影響すると考えます。stateファイルへのアクセス権限は、事実上「すべてのインフラの設定値を閲覧できる権限」と同義となります。機密値をstateに入れると、閲覧可能な人を制限するか、stateの暗号化を別途設計する必要が出てくるので、それだったら最初から機密情報は入れないほうが権限設計をシンプルにできます。

なので機密情報は手動管理とします。

軸2:変更頻度 — ignore_changesは「管轄外宣言」

ECSのリソースをimportしたとき、lifecycle ブロックの ignore_changes をどこに入れるかで判断が分かれました。

ECSタスク定義:CI/CDとの境界

ECSタスク定義には container_definitions という属性があり、コンテナイメージのタグ、環境変数、リソース割り当てなどが含まれています。

この値はデプロイのたびにCI/CDパイプラインが更新されるため、イメージタグは v1.2.3 から v1.2.4 に変わり、環境変数も追加されることがあります。

もし container_definitions をTerraformの管理対象にすると、次に terraform plan を実行したとき、CI/CDが更新した最新の値を、Terraformがimport時点の古い値に戻そうとしてしまいます。

lifecycle {
  ignore_changes = [container_definitions]
}

この1行は、「container_definitions はTerraformの管轄ではない」という宣言です。

技術的にはただの設定。でも意味としては 「ここから先はCI/CDの責任範囲です」という境界線を引いたことになります。

ECSサービス:オートスケーラーとの境界

同じ考え方がECSサービスの desired_count にも当てはまると分かりました。

lifecycle {
  ignore_changes = [desired_count, task_definition]
}

desired_count(タスク数)はオートスケーリングが負荷に応じて動的に変更されます。Terraformがこの値を管理すると、スケールアウトした直後に terraform apply で元の数に戻されてしまいます。

そしてtask_definition も同様で、CI/CDが新しいリビジョンをデプロイするたびにARNが変わるのです。なので、「ここまではTerraform、ここからは管理外」という境界線の設計がここでも重要になってきます。

RDS:パスワード管理の境界

RDSの password にも、 ignore_changes を入れます。

具体的には以下です。

resource "random_password" "db" {
  length  = 24
  special = true
}

resource "aws_db_instance" "main" {
  password = random_password.db.result
  # ...
  lifecycle {
    ignore_changes = [password]
  }
}

random_password で初回のパスワードを生成して、ignore_changes でそれ以降はTerraformの管理外にします。パスワードのローテーションはAWS Secrets Managerのローテーション機能やDBAの手動操作に委ねることに。

random_password だけだと plan のたびに差分が出続け、ignore_changes だけだとパスワードの初期設定手段がないため、初回だけ設定して以降は触らないというパターンはこの組み合わせで実現できます。

ignore_changesの本質

ignore_changes に並ぶ属性名の一つひとつは、「この値の管理権限はTerraform外にある」という宣言にもなります。

container_definitions → CI/CDチームの管轄
desired_count         → オートスケーラーの管轄
task_definition       → CI/CDチームの管轄
password              → DBA / Secrets Managerの管轄

逆に言えば、ignore_changes に入っていない属性は「Terraformが責任を持つ」と宣言しています。この暗黙の宣言を意識すると、コードをチーム間の責任境界のドキュメントとして機能させることができます。

軸3:安全装置 — 「デフォルトだから消す」は危険

terraform import で取り込んだリソースには、自動生成された大量の属性があります。デフォルト値を削除してコードをスリムにするのは定石ですが、消してはいけない値もあります。

例えば以下です。

CloudFront:WAFの紐付け

CloudFrontのリファクタリング中、web_acl_id という属性を「デフォルト値っぽいからいらない」と判断しました。

しかし実際にはこれはCloudFrontとWAFの紐付けであり、値を null にするとWAFの防御が完全に外れ、SQLインジェクションやDDoSに対して無防備になってしまいます。

この削除がAIによるコード生成に含まれていたのも見逃せないポイントだな、と感じています。

リファクタリングの一環としてAIにコードを整理させたところで、web_acl_id の削除と cache_policy_id の変更が出力に含まれていました。

cache_policy_id の変更も見た目はUUIDが変わるだけですが、実態は CachingOptimized から CachingDisabled への切り替えであり、キャッシュが無効化されてオリジンサーバーへの負荷が急増する変更となっていました、、。

ここから得た教訓は2つあります。

1. 安全装置は「明示的に残す」がデフォルト

null / false / 0 がデフォルト値と一致する属性でも、「null にすることで設定を解除する意味を持つ」フィールドは残すようにする。判断に迷ったら消さないこと。

2. AIにインフラコードの判断を委ねない

AIはお手本に合わせたコード生成は得意でも、「現環境との差異が安全か」の判断は構造的に苦手だなと感じました。コード生成はAIに任せてもplan 差分の安全性判断は人間が握ったほうが良いです。plan の各行が何を制御しているかを理解して判断できるようにする。

RDS:ガードレールとしてのフラグ

RDSの allow_major_version_upgrade にも同様のことが当てはまります。

importすると true で取り込まれるけれど、そのままにすると「engine_version を変えるだけで誰でもメジャーバージョンアップできる」状態になってしまいます、、、!!

allow_major_version_upgrade = false  # trueは怖すぎる!

false に明示的に設定し、バージョンアップが必要なときだけ一時的に true にするのが良いと分かりました。この辺り分かる人がいないとTerraformの運用は回らないと考えます。

IaC化の境界はチーム設計の合意と言えそう

ここまでの3つの軸を整理すると、以下です。

判断基準Terraform管理管轄
機密性stateに残って問題ないか箱だけ管理、中身は管理しないセキュリティ / DBA
変更頻度Terraform以外が変更するかignore_changesで委譲CI/CD / オートスケーラー
安全装置削除すると防御が外れるか明示的に残すインフラチーム

この3つの判断は技術的な判断であると同時に、「誰が何に責任を持つか」というチーム間の合意でもあります。

ignore_changes の行は、インフラチームとアプリチームの責任境界を定義しています。

Secrets Managerの「箱だけ管理」は、stateファイルへのアクセス権限設計を簡素化しているといえます。

安全装置を明示的に残すことは、インフラチームが「この防御は私たちが守る」と宣言していることにもなります。

Terraformのコードは、インフラの構成定義であると同時に、組織の責任分界点のドキュメントとしても見ることができる、と考えると、コードから運用の課題点も推察できる部分が数多くありそうに思います。

ただ、この判断は組織の規模や体制によって変わります。

専任のSREチームがいて、stateファイルのアクセス管理が厳格に運用されている組織であれば、Secrets Managerの中身もTerraform管理にするほうが合理的な場合もあるかもしれないです。

また、ignore_changes の範囲が広すぎると、Terraformが実態と乖離していることに気づけなくなるリスクもあります。委譲した場合は、委譲先が責任を持って運用していることが前提です。CI/CDパイプラインが壊れていてデプロイされていないのに ignore_changes で差分を無視し続けていた、というのはまずい、、、!

境界を引くことは重要であり、それぞれが信頼できる状態を保つのが重要です。

まとめ

IaC化の境界設計で私が学んだことは、以下の3つです。

  1. 機密情報はstateに入れない。Terraform管理ができることと、“すべき”であることは違う
  2. 変更頻度が高い値はignore_changesで委譲する。各行がチーム間の責任境界になる
  3. 安全装置は消さない。「デフォルトだから不要」と「明示的に設定された値」を区別する

23リソースを取り込んだ結果、「何をTerraform管理にするか」ではなく、「何をTerraform管理にしないか」を決めることが最も重要だと考えました。要するに「何をするかの反対は何をしないか」で、Terraformにも当てはまるよ、そして境界線をしっかり引こうという話でした。


この記事で扱った内容は、オンラインスクール「Cloud Pratica」のTerraform Importコースで学んだ知見がベースです。

体系的にTerraformのimport手順を学びたい方にはぜひおすすめです!