「本番環境でAWSコンソールを決して触らない」というのは極端なルールに聞こえる。そうではない。これはクラウドネイティブチームにおいて最も重要な運用規律であり、これに違反したコストは静かに蓄積され、やがて大きなインシデントを引き起こす。

この記事ではその理由と、実際のチームでIaCファースト開発を強制する方法を説明する。

状態ドリフト問題

Terraform(およびOpenTofu)は、どのようなインフラストラクチャが存在するかを表す状態ファイルを維持する。applyを実行すると、Terraformは状態ファイルと設定を比較し、現実を設定に合わせるための最小限の変更セットを作成する。

AWSコンソールでクリックしてリソースを作成・変更すると、状態ファイルを変更せずに現実を変更していることになる。

すると: 次のplanでドリフトが表示される。Terraformは「存在すべきでない」リソース(設定にないため)を破棄または再作成しようとする。

あるいはさらに悪いことに: Terraformの状態にはリソースが設定Aで存在するとあるが、コンソールで設定Bに変更された。設定ミスがあるにもかかわらずplanは「変更なし」と表示する。

または: 誰かがコンソールでリソースを追加し、それが動作し、他のリソースから依存されるようになり、その後applyが実行されて状態にないため削除される。

コンソールは真実のソースではない。あなたのIaCがそうだ。コンソールは嘘だ。

インポートワークフロー: 「すでに存在する」への正しい対応

リソースがAWSに存在するがTerraform設定にない場合(誰かがコンソールで作成、CLIで手動作成、または別のツールから移行されたもの)、正しい対応はインポートすることだ:

既存のリソースをTerraform状態にインポートする

./deploy.sh --infra --import aws_s3_bucket.service existing-bucket-name
./deploy.sh --infra --import aws_dynamodb_table.service_files serviceFiles
./deploy.sh --infra --import aws_lambda_function.service_api myapp-service-api

インポート後、planを実行する:
./deploy.sh --infra

planはAWSに存在するものと設定が言っていることの差分を表示する。予期しない変更が表示されなくなるまで設定を修正する。それからapplyして状態ファイルを同期させる。

コンソールからリソースを削除してTerraformで再作成してはいけない。これはデータを破壊し、依存関係を壊す。インポート → 調整 → apply。

唯一の正当なコンソール使用

コンソールが適切なのは:

  • 読み取り専用の探索。CloudWatchログ、CloudWatchメトリクス、Lambda呼び出し履歴、DynamoDB項目の検査。
  • 即時インポートを伴う緊急操作。本番がダウンしていて修正にコンソールの変更が必要な場合、変更を行う。ただし文書化し、同じ作業セッション内でTerraformにインポートする。

以上だ。リソースを作成、変更、削除するすべてのものはIaCに属する。

デプロイスクリプトを唯一のエントリポイントに

ホスト上で直接実行される生のterraform applytofu applyコマンドは、チームの標準デプロイワークフロー(バリデーション、lint、planレビュー)をバイパスするため危険だ。すべてのTerraform操作をラップするデプロイスクリプトを作成する:

#!/bin/bash
# deploy.sh - the only way to interact with infrastructure

case “$1” in –infra) case “$2” in –apply) run_apply ;; –plan) run_plan ;; –validate) run_validate ;; –tflint) run_tflint ;; –import) run_import “$3” “$4” ;; *) run_plan ;; # default: plan only (safe) esac ;; esac

優れたデプロイスクリプトの主要な特性:

  • デフォルトはplan。2番目の引数なしで./deploy.sh --infraを実行するとplanを表示し、決してapplyしない。applyには明示的な意図(--apply)が必要。
  • applyの前に常にバリデーション。applyの前にterraform validateとtflintを実行する。どちらかが失敗したらデプロイを拒否する。
  • apply前に常にplanを表示。--applyでもplan出力を表示し、対話モードで確認を要求する。
  • 一貫したvar-file。常に同じvar-file(vars/production.tfvars)を含める。これなしではapplyしない。

Planレビューをゲートに

インフラストラクチャPRがマージされる前に、plan出力がPRに表示されるべきだ:
## Terraform Plan

Terraform will perform the following actions:
module.service_api.aws_lambda_function.service_api will be updated in-place
~ resource "aws_lambda_function" "service_api" {
~ timeout = 30 -> 60
}

Plan: 0 to add, 1 to change, 0 to destroy.

これにより2つのことが達成される:

  1. レビュアーは承認前に本番で何が変更されるかを正確に確認できる。
  2. plan出力はマージ時に意図されていたことの履歴記録となる。

予期しない破棄(N to destroy)を表示するplanは、著者が理由を説明するまでPRをブロックすべきだ。

No-Excludeルール

applyが失敗したときのよくある間違い: 失敗したリソースを除外してapplyする。

# WRONG tofu apply -exclude='aws_bedrockagent_knowledge_base.kb'

これは部分的なapplyを作り出す: 一部のリソースは更新され、一部はされなかった。状態ファイルは部分的に適用された設定を反映する。除外されたリソースは、それに依存するすべてのものと同期が取れていない。

失敗したリソースへの正しい対応:

  1. applyを完了させる。中断しない。
  2. リソースが失敗した理由を理解する。
  3. 設定を修正するか、既存のリソースをインポートする。
  4. エラーがゼロになるまで再度applyを実行する。

エラーが「リソースが既に存在する」の場合: インポートする。
エラーが設定の不一致の場合: 既存のリソースに合わせて設定を修正する。
エラーが依存関係の順序問題の場合: depends_onを修正する。

決して: リソースを除外、コメントアウト、またはその他の方法でスキップしてはいけない。IaC設定は常に現実を反映しなければならない。

強制ロック解除

Terraformは同時applyを防ぐためにロックテーブル(DynamoDB)を使用する。ロックがスタックした場合(Lambdaがapply中にタイムアウト、プロセスがkillされた)、次のように表示される:

Error: Error locking state: Error acquiring the state lock

魅力的な修正: terraform force-unlock。これは別のapplyが実際に実行中の場合危険だ。実行中のapplyを強制ロック解除すると破損を引き起こす。

正しい対応:

  1. 実際にapplyが実行中かどうかを確認する(デプロイLambdaのCloudWatchをチェック、チームメイトに確認)。
  2. applyが実行中でなく、ロックが古いと確信できる場合のみ、force-unlockする。
  3. ロックを強制したことを文書化する。これは前回のapplyが適切にクリーンアップしなかった理由を調査するシグナルだ。

最初の対応としてforce-unlockを決して使わない。常に最初に実行中のapplyがないことを確認する。

ドリフト検出

厳格なIaC規律があっても、ドリフトは蓄積される。AWSがリソースを自動変更したり(例: CognitoがLambdaトリガー関連付けを更新)、チームメンバーが緊急のコンソール変更を行いインポートを忘れたりする。

applyなしの定期的なplan実行をスケジュールする(毎週または重要な活動期間の後):

# In CI/CD: weekly scheduled plan ./deploy.sh --infra --plan 2>&1 | tee plan-output.txt # Alert if plan shows unexpected changes

planが存在すべきでない変更を表示した場合、次のapplyがそれらを破棄する前に調査する。

主要なポイント

  • AWSコンソールは読み取り用であり、書き込み用ではない。すべてのインフラストラクチャ変更はIaCに属する。
  • リソースがAWSに存在するがTerraformにない場合: インポートし、設定を調整し、それからapplyする。
  • 失敗したリソースを回避するために-excludeを決して使わない。根本原因を修正する。
  • すべてのTerraform操作をデプロイスクリプトでラップする。デフォルトはplan。applyには明示的な意図を要求する。
  • すべてのインフラストラクチャPRにplan出力を含める。予期しない破棄はPRをブロックすべきだ。
  • 実行中のapplyがないことを確認せずにforce-unlockを決して行わない。
  • ドリフトを早期に検出するためにスケジュールされたplanチェックを実行する。