Terraform 상태: S3/GCS, 잠금 및 가져오기를 사용한 원격 백엔드
생산 과정에서 가장 많은 사고를 일으키는 Terraform의 한 측면이 있다면,
그리고 상태 관리. 저는 팀이 부패한 상태를 고치는 데 시간을 낭비하는 것을 보았습니다.
두 명의 개발자가 수행했기 때문에 다양한 구성 apply 병렬로,
클라우드에 존재하지만 상태에는 존재하지 않는 "유령" 리소스입니다. 이 모든 문제
공통된 원인이 있습니다. 즉, 잠금 없이 로컬 상태를 유지하는 것입니다.
이 가이드는 기술적이고 실용적입니다. AWS용 DynamoDB 잠금을 사용하여 S3 백엔드를 구성합니다.
Google Cloud용 GCS 백엔드, 환경 분리를 위한 작업공간에 대해 이야기하겠습니다.
그리고 우리는 어떻게 볼 것인가 기존 리소스 가져오기 다운타임 없이
블록 import Terraform 1.5에 도입되었습니다(최종적으로 선언적이며 안전함).
무엇을 배울 것인가
- 팀에서 로컬 상태가 위험한 이유와 원격 백엔드로 마이그레이션하는 방법
- AWS에서 DynamoDB 잠금을 사용하여 S3 백엔드 설정(프로덕션 준비 완료)
- Google Cloud Platform에서 GCS 백엔드 구성
- Workspace Terraform: 환경 분리를 위한 패턴
- 잠금을 사용하여 기존 자산 가져오기
import(테라폼 1.5+) - 비상작동: 상태조작, 강제잠금해제, 백업
- 부분 백엔드 구성: 하드코딩 없이 자격 증명 관리
원격 백엔드가 기본인 이유
파일 terraform.tfstate 로컬 팀에는 네 가지 근본적인 문제가 있습니다.
-
잠금 없음: 두 명의 개발자가 실행하는 경우
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}. 프로세스가 갑자기 중단된 경우
(kill, crash) 잠금이 해제되지 않을 수 있습니다. 강제로 잠금 해제하려면:
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"
]
}
Workspace 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에는 없습니다), 사용 별도의 디렉토리 모듈을 재사용하는 사람. 복잡한 인프라를 갖춘 많은 팀은 항상 별도의 디렉터리를 선호합니다. 최대한의 명확성을 위해.
Import 블록을 사용하여 기존 리소스 가져오기
기존 조직에서 Terraform을 채택할 때 가장 일반적인 경우 중 하나는
수동으로 또는 다른 도구를 사용하여 생성된 리소스를 관리합니다. 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 수백 개의 리소스를 제어해야 하며 잠금이 차단됩니다.
한 모듈의 오류로 인해 전체 배포가 차단됩니다. 해결책은 나누는 것이다.
넌 그 안에 있어 독립 스택.
# 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 자동으로 관리하지만
Pull Requests의 계획 검토 워크플로우, 승인되지 않은 애플리케이션 차단
감지된 드리프트에 대한 알림.
전체 시리즈: Terraform 및 IaC
- 제01조 — 처음부터 새로 만드는 Terraform: HCL, 공급자 및 계획-적용-파괴
- 제02조 — 재사용 가능한 Terraform 모듈 설계: 구조, I/O 및 레지스트리
- 제03조(본) — Terraform 상태: S3/GCS, 잠금 및 가져오기를 포함한 원격 백엔드
- 기사 04 — CI/CD의 Terraform: GitHub Actions, Atlantis 및 Pull Request Workflow
- 조항 05 — IaC 테스트: Terratest, Terraform 기본 테스트 및 계약 테스트
- 06조 — IaC 보안: Checkov, Trivy 및 OPA Policy-as-Code
- 기사 07 — Terraform 다중 클라우드: AWS + Azure + 공유 모듈이 있는 GCP
- 기사 08 — Terraform용 GitOps: Flux TF 컨트롤러, Spacelift 및 드리프트 감지
- 기사 09 — Terraform vs Pulumi vs OpenTofu: 2026년 최종 비교
- 기사 10 — Terraform 엔터프라이즈 패턴: 작업 공간, Sentinel 및 팀 확장







