Terraform の状態: S3/GCS を使用したリモート バックエンド、ロックとインポート
本番環境で最も多くの事故を引き起こす Terraform の側面が 1 つあるとすれば、それは
そして状態管理。チームが腐敗した状態を修正するために時間を無駄にしているのを見てきましたが、
2 人の開発者が行ったため、構成が異なる apply 並行して、
クラウドには存在するが状態には存在しない「ゴースト」リソース。これらすべての問題
これらには共通の原因があります。それは、ロックせずにローカル状態にあるということです。
このガイドは技術的かつ実践的なものです。AWS 用に DynamoDB ロックを使用して S3 バックエンドを構成します。
Google Cloud の GCS バックエンドについては、環境を分離するためのワークスペースについて説明します。
そしてどうなるか見てみましょう 既存のリソースをインポートする ダウンタイムなしで使用
ブロック import Terraform 1.5 で導入されました (最終的に宣言型で安全になりました)。
何を学ぶか
- チーム内でローカル状態が危険な理由とリモート バックエンドに移行する方法
- AWS で DynamoDB ロックを使用して S3 バックエンドをセットアップする (実稼働対応)
- Google Cloud Platform で GCS バックエンドを構成する
- Workspace Terraform: 環境を分離するためのパターン
- ロックを使用して既存のアセットをインポートする
import(Terraform 1.5 以降) - 緊急操作:状態操作、強制ロック解除、バックアップ
- 部分的なバックエンド構成: ハードコーディングせずに認証情報を管理
リモート バックエンドが基本的な理由
ファイル terraform.tfstate ローカルのチームには 4 つの根本的な問題があります。
-
ロックなし: 2 人の開発者が実行した場合
terraform apply同時に、両方とも同じ状態を読み取り、部分的な変更を書き込みます。 破損した状態とリソースの重複につながります。 - 共有されていない: 各開発者は独自のローカル コピーを持っています。最新のものを持っている人は バージョン?最後に申請したのは誰ですか?知ることは不可能です。
- 秘密が含まれています: 状態には機密性の高い値 (RDS パスワード、 API キー)。 Git には決して入りません。
- 履歴なし: 誰がいつ何をしたかについての監査証跡はありません。
リモート バックエンドは、集中状態、アトミック ロック、 保存時の暗号化とロールバックのためのバージョン管理。
ブートストラップ: Terraform を使用したバックエンド リソースの作成
Terraform バックエンドの矛盾は、AWS リソース (S3 バケット + DynamoDB テーブル) を作成する必要があることです。 前に バックエンドを設定できるようになります。解決策はミニプロジェクトでブートストラップすることです ローカル バックエンドを使用する別のもの。
# bootstrap/main.tf — crea le risorse per il backend remoto
# Questo progetto usa il backend locale (terraform.tfstate nella directory)
# Committalo nel repo come "infra/bootstrap/"
terraform {
required_version = ">= 1.8.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
# NOTA: qui non c'e backend block — usa il file locale
}
provider "aws" {
region = "eu-west-1"
}
locals {
name = "acme"
environment = "global"
}
# S3 Bucket per lo state
resource "aws_s3_bucket" "terraform_state" {
bucket = "${local.name}-terraform-state"
# Protezione contro cancellazione accidentale
lifecycle {
prevent_destroy = true
}
tags = {
Name = "${local.name}-terraform-state"
ManagedBy = "Terraform"
Environment = local.environment
}
}
# Versioning: ogni apply crea una nuova versione del file di state
resource "aws_s3_bucket_versioning" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
versioning_configuration {
status = "Enabled"
}
}
# Encryption at rest: obbligatorio per state con segreti
resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
}
bucket_key_enabled = true # Riduce i costi KMS del 99%
}
}
# Blocca accesso pubblico: lo state NON deve essere pubblico
resource "aws_s3_bucket_public_access_block" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
# Policy del bucket: nega qualsiasi accesso non-TLS
resource "aws_s3_bucket_policy" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "DenyNonTLS"
Effect = "Deny"
Principal = "*"
Action = "s3:*"
Resource = [
aws_s3_bucket.terraform_state.arn,
"${aws_s3_bucket.terraform_state.arn}/*"
]
Condition = {
Bool = {
"aws:SecureTransport" = "false"
}
}
}
]
})
}
# DynamoDB table per il locking
resource "aws_dynamodb_table" "terraform_locks" {
name = "${local.name}-terraform-locks"
billing_mode = "PAY_PER_REQUEST" # Nessun costo fisso
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
# Protezione contro cancellazione
lifecycle {
prevent_destroy = true
}
tags = {
Name = "${local.name}-terraform-locks"
ManagedBy = "Terraform"
Environment = local.environment
}
}
# Output: valori da copiare nel backend block dei progetti
output "state_bucket_name" {
value = aws_s3_bucket.terraform_state.id
description = "Nome del bucket S3 per lo state Terraform"
}
output "dynamodb_table_name" {
value = aws_dynamodb_table.terraform_locks.name
description = "Nome della tabella DynamoDB per il locking"
}
output "aws_region" {
value = "eu-west-1"
description = "Region AWS dove sono create le risorse del backend"
}
S3 バックエンドを構成する
ブートストラップ リソースを作成したら、プロジェクトでバックエンドをセットアップします。
ブロック backend Terraform 変数 (および既知の制限) はサポートされていません。
だから、を使用してください 部分的なバックエンド構成 資格情報を分離します。
# versions.tf — configurazione backend S3
terraform {
required_version = ">= 1.8.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
# Backend con partial configuration (valori statici non-sensibili)
backend "s3" {
bucket = "acme-terraform-state"
key = "environments/prod/networking/terraform.tfstate"
region = "eu-west-1"
dynamodb_table = "acme-terraform-locks"
encrypt = true
# NON mettere access_key e secret_key qui!
# Usa variabili di ambiente o IAM Role
}
}
# La struttura raccomandata per il "key" (path del file):
# {account}/{environment}/{stack}/terraform.tfstate
#
# Esempi:
# "123456789/dev/networking/terraform.tfstate"
# "123456789/prod/eks-cluster/terraform.tfstate"
# "123456789/shared/monitoring/terraform.tfstate"
# Inizializzazione con partial backend config
# (passa i valori mancanti come -backend-config)
terraform init \
-backend-config="bucket=acme-terraform-state" \
-backend-config="key=environments/dev/networking/terraform.tfstate" \
-backend-config="region=eu-west-1" \
-backend-config="dynamodb_table=acme-terraform-locks"
# Oppure con file di backend config
# backend.hcl (NON committare se contiene credenziali)
# bucket = "acme-terraform-state"
# key = "environments/dev/networking/terraform.tfstate"
# region = "eu-west-1"
# dynamodb_table = "acme-terraform-locks"
terraform init -backend-config=backend.hcl
# Migra da backend locale a remoto
terraform init -migrate-state
# Terraform chiede conferma prima di copiare il state locale su S3
DynamoDB ロック
いつ terraform apply 実行すると、DynamoDB に行が作成されます
と LockID = {bucket}/{key}。プロセスが突然停止した場合
(キル、クラッシュ)、ロックが解除されない可能性があります。ロックを強制的に解除するには:
terraform force-unlock {LOCK_ID}
# Il LOCK_ID e visibile nel messaggio di errore quando Terraform trova il lock
アメリカ合衆国 force-unlock 確かな場合のみ 他に誰もいないということ
申請中です。適用の進行中にブロックを解除すると、状態が破損する可能性があります。
Google Cloud Platform の GCS バックエンド
# versions.tf — backend GCS
terraform {
required_version = ">= 1.8.0"
required_providers {
google = {
source = "hashicorp/google"
version = "~> 5.0"
}
}
backend "gcs" {
bucket = "acme-terraform-state-eu"
prefix = "environments/prod/networking"
# prefix = "environments/{environment}/{stack}"
# Il file effettivo sara: {prefix}/default.tfstate
}
}
# Crea il bucket GCS per lo state (bootstrap)
resource "google_storage_bucket" "terraform_state" {
name = "acme-terraform-state-eu"
location = "EU"
force_destroy = false
# Versioning per rollback
versioning {
enabled = true
}
# Encryption con CMEK (opzionale ma consigliato)
# encryption {
# default_kms_key_name = google_kms_crypto_key.terraform_state.id
# }
# Policy di retention: mantieni le versioni per 30 giorni
lifecycle_rule {
condition {
num_newer_versions = 5
with_state = "ARCHIVED"
}
action {
type = "Delete"
}
}
}
# Blocca l'accesso pubblico al bucket
resource "google_storage_bucket_iam_binding" "terraform_state_private" {
bucket = google_storage_bucket.terraform_state.name
role = "roles/storage.admin"
members = [
"serviceAccount:terraform@acme-project.iam.gserviceaccount.com"
]
}
ワークスペース Terraform: 環境の分離
Terraform ワークスペースを使用すると、別の状態ファイルを管理できます。
同じ HCL 構成。各ワークスペースにはバックエンドに独自の状態ファイルがあります。
env:/{workspace_name}/terraform.tfstate.
# Gestione dei workspace
terraform workspace list
# * default
terraform workspace new dev
terraform workspace new staging
terraform workspace new prod
terraform workspace list
# default
# * dev
# staging
# prod
terraform workspace select prod
terraform workspace show # stampa il nome del workspace corrente
# Cancella un workspace (deve essere vuoto)
terraform workspace delete staging
# Uso del workspace nella configurazione HCL
locals {
# Recupera il nome del workspace corrente
workspace = terraform.workspace
# Configurazioni specifiche per workspace
workspace_config = {
dev = {
instance_type = "t3.micro"
min_instances = 1
max_instances = 2
enable_monitoring = false
rds_class = "db.t3.micro"
multi_az = false
}
staging = {
instance_type = "t3.small"
min_instances = 1
max_instances = 3
enable_monitoring = true
rds_class = "db.t3.small"
multi_az = false
}
prod = {
instance_type = "t3.medium"
min_instances = 2
max_instances = 6
enable_monitoring = true
rds_class = "db.t3.medium"
multi_az = true
}
}
# Accede alla configurazione del workspace corrente
# con fallback a "dev" se il workspace non e nella mappa
current_config = lookup(local.workspace_config, local.workspace, local.workspace_config["dev"])
# Naming con workspace nel prefisso
name_prefix = "${local.workspace}-${var.project_name}"
}
# Usa i valori dalla configurazione del workspace
resource "aws_autoscaling_group" "web" {
min_size = local.current_config.min_instances
max_size = local.current_config.max_instances
desired_capacity = local.current_config.min_instances
# ...
}
resource "aws_db_instance" "main" {
instance_class = local.current_config.rds_class
multi_az = local.current_config.multi_az
# ...
}
ワークスペースと個別のディレクトリ: いつ何を使用するか
ワークスペースは環境に適しています 構造的に同一 それは違う スケーリング専用 (dev/staging/prod)。環境のアーキテクチャが異なる場合 (例: prod には VPN がありますが、dev にはありません)、使用します。 別々のディレクトリ モジュールを再利用する人。 複雑なインフラストラクチャを持つ多くのチームは常に別のディレクトリを好みます 最大限の明瞭さのために。
インポートブロックを使用して既存のリソースをインポートする
既存の組織で Terraform を導入する場合の最も一般的なケースの 1 つは、次のことを行う必要があることです。
手動または他のツールで作成されたリソースを管理します。 Terraform 1.5 より前では、インポートは
必須のみ (terraform import CLI)、リスクがあり、検証できません。
の ブロック import (1.5 で GA、1.7 で改善されました
import generate)そして宣言的で自信に満ちています。
# main.tf — import dichiarativo (Terraform 1.5+)
# Step 1: definisci il blocco import
import {
# ID della risorsa nel cloud provider
id = "vpc-0123456789abcdef0"
# Punta alla risorsa HCL che vuoi associare
to = aws_vpc.main
}
# Step 2: scrivi la configurazione HCL della risorsa
# (puoi usare "terraform plan -generate-config-out=generated.tf" per auto-generarla)
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "prod-main-vpc"
Environment = "prod"
ManagedBy = "Terraform" # Aggiunta da noi
}
}
# Step 3: terraform plan verifica che lo state da importare
# corrisponda alla configurazione HCL.
# Se ci sono differenze, Terraform le evidenzia come "~ to change"
# Import multiplo: importa un intero gruppo di risorse
import {
id = "subnet-0abc123"
to = aws_subnet.public[0]
}
import {
id = "subnet-0def456"
to = aws_subnet.public[1]
}
# Import con for_each (Terraform 1.7+)
locals {
existing_subnets = {
"public-1" = "subnet-0abc123"
"public-2" = "subnet-0def456"
}
}
import {
for_each = local.existing_subnets
id = each.value
to = aws_subnet.public[each.key]
}
# Workflow completo di import
# 1. Genera automaticamente la configurazione HCL dalla risorsa esistente
# (Terraform 1.5+ con -generate-config-out)
terraform plan -generate-config-out=generated_imports.tf
# 2. Rivedi il file generato: contiene la configurazione della risorsa
# come Terraform la vede nel cloud provider
cat generated_imports.tf
# 3. Copia e adatta la configurazione nel tuo main.tf
# (il file generated non e perfetto, va revisionato)
# 4. Esegui plan per verificare che l'import sia corretto
terraform plan
# Se il plan mostra "Plan: 0 to add, 0 to change, 0 to destroy"
# con note "Will import", l'import e perfetto
# 5. Apply: esegue l'import effettivo e aggiorna lo state
terraform apply
# 6. Rimuovi i blocchi import dal codice dopo l'apply
# (non sono piu necessari una volta che le risorse sono nello state)
状態操作: 緊急操作
状態を直接操作する必要がある状況があります。 これらの操作は強力かつ危険です。常に予防バックアップを使用して実行してください。
# SEMPRE fare backup prima di operazioni sullo state
terraform state pull > backup-$(date +%Y%m%d-%H%M%S).tfstate
# Rimuove una risorsa dallo state (la risorsa rimane nel cloud)
# Uso: quando vuoi che Terraform "dimentichi" una risorsa
# senza cancellarla (es. passi a gestirla con un altro tool)
terraform state rm aws_instance.legacy_server
# Sposta una risorsa da un indirizzo a un altro nello state
# Uso: refactoring del codice senza distruggere le risorse
terraform state mv aws_instance.web aws_instance.web_new
terraform state mv 'aws_subnet.public[0]' 'aws_subnet.public["eu-west-1a"]'
# Mostra i dettagli JSON di una risorsa nello state
terraform state show aws_vpc.main
# Pull dello state remoto in locale (utile per ispezione)
terraform state pull > current.tfstate
# Push di uno state locale sul backend remoto
# PERICOLOSO: sovrascrive lo state remoto senza conferma
# Usare solo per recovery da state corrotto
terraform state push recovered.tfstate
# Refresh: aggiorna lo state leggendo lo stato reale del cloud
# (ora deprecato a favore di terraform apply -refresh-only)
terraform apply -refresh-only
状態分割: 大規模チーム向けの戦略
大規模なインフラストラクチャを備えた組織では、単一のモノリシックな状態を持つことが重要になります。
問題: すべて plan 何百ものリソースを制御する必要があるため、ロックがブロックされます
チーム全体が影響を受けるため、1 つのモジュールのエラーがデプロイメント全体をブロックします。解決策は分けること
あなたはそこに留まります 独立したスタック.
# Struttura raccomandata per infrastrutture large-scale
# Ogni directory e un progetto Terraform indipendente con il suo state
environments/
├── prod/
│ ├── networking/ # State: prod/networking/terraform.tfstate
│ │ ├── main.tf
│ │ └── versions.tf
│ ├── eks-cluster/ # State: prod/eks-cluster/terraform.tfstate
│ │ ├── main.tf
│ │ └── versions.tf
│ ├── databases/ # State: prod/databases/terraform.tfstate
│ │ ├── main.tf
│ │ └── versions.tf
│ └── monitoring/ # State: prod/monitoring/terraform.tfstate
│ ├── main.tf
│ └── versions.tf
└── dev/
└── ...
# Per passare informazioni tra stack: terraform_remote_state data source
# stack eks-cluster recupera gli output da networking
data "terraform_remote_state" "networking" {
backend = "s3"
config = {
bucket = "acme-terraform-state"
key = "environments/prod/networking/terraform.tfstate"
region = "eu-west-1"
}
}
resource "aws_eks_cluster" "main" {
name = "prod-cluster"
role_arn = aws_iam_role.eks.arn
vpc_config {
# Usa gli output dallo stack networking
subnet_ids = data.terraform_remote_state.networking.outputs.private_subnet_ids
endpoint_private_access = true
endpoint_public_access = false
}
}
# Alternativa moderna: usare variabili con file .tfvars invece di
# terraform_remote_state. Piu sicuro (evita dipendenze tra state),
# meno conveniente (richiede aggiornamento manuale dei valori)
S3 バックエンドの IAM ポリシー
運用環境では、Terraform (開発者または CI/CD) によって使用される IAM ロールには、 バックエンドに必要な最小限の権限。完全なポリシーは次のとおりです。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "TerraformStateBucketAccess",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject",
"s3:ListBucket",
"s3:GetBucketVersioning",
"s3:GetEncryptionConfiguration"
],
"Resource": [
"arn:aws:s3:::acme-terraform-state",
"arn:aws:s3:::acme-terraform-state/*"
]
},
{
"Sid": "TerraformStateLocking",
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:DeleteItem",
"dynamodb:DescribeTable"
],
"Resource": "arn:aws:dynamodb:eu-west-1:*:table/acme-terraform-locks"
},
{
"Sid": "TerraformKMSDecryptState",
"Effect": "Allow",
"Action": [
"kms:GenerateDataKey",
"kms:Decrypt"
],
"Resource": "arn:aws:kms:eu-west-1:*:key/*"
}
]
}
結論と次のステップ
状態管理は、セットアップ内の他のすべてを構築する基盤です。 テラフォーム。適切なロックを備えたリモート バックエンドにより、クラス全体からユーザーを保護します 操業上の事故のこと。時間をかけて最初から正しく設定してください。 既存の実行可能だが手間のかかるプロジェクトで、州のローカルからリモートに移行する。
次のステップは、Terraform をプロフェッショナルな CI/CD パイプラインに統合することです。
ただ実行しないでください plan e apply 自動的に管理されますが、
プル リクエストでの計画レビュー ワークフロー、未承認のアプリケーションのブロック
検出されたドリフトの通知。
完全なシリーズ: Terraform と IaC
- 第01条 — ゼロからの Terraform: HCL、プロバイダー、およびプラン、適用、破棄
- 第02条 — 再利用可能な Terraform モジュールの設計: 構造、I/O、およびレジストリ
- 第03条(本) — Terraform 状態: S3/GCS を使用したリモート バックエンド、ロックおよびインポート
- 記事 04 — CI/CD の Terraform: GitHub アクション、Atlantis、およびプル リクエストのワークフロー
- 第 05 条 — IaC テスト: Terratest、Terraform ネイティブ テスト、および契約テスト
- 第 06 条 — IaC セキュリティ: Checkov、Trivy、OPA のポリシー・アズ・コード
- 記事 07 — Terraform マルチクラウド: 共有モジュールを使用した AWS + Azure + GCP
- 記事 08 — Terraform 用 GitOps: Flux TF コントローラー、スペースリフトおよびドリフト検出
- 記事 09 — Terraform vs Pulumi vs OpenTofu: 最終比較 2026
- 第 10 条 — Terraform エンタープライズ パターン: ワークスペース、センチネル、チーム スケーリング







