테라폼 기초 강좌 #5 state의 정체: tfstate 파일 구조와 plan이 변경을 계산하는 원리
#1에서 destroy가 만든 것을 정확히 되짚어 지우는 것을 보고 “테라폼은 자기가 만든 것을 어떻게 기억할까"라는 질문을 남겨 두었습니다. 답은 그때 디렉터리에 생긴 terraform.tfstate 파일입니다. state는 테라폼 동작 원리의 중심이면서, 실무에서 일어나는 테라폼 사고의 대부분이 이 파일을 둘러싸고 벌어집니다. 이번 글에서는 파일을 직접 열어 구조를 보고, plan이 무엇과 무엇을 비교하는지, 그리고 state를 다뤄야 할 때와 절대 건드리면 안 되는 것을 구분하겠습니다.
tfstate 파일 열어 보기 #
버킷 하나를 apply한 디렉터리에서 terraform.tfstate를 열면 JSON이 나옵니다. 중요한 부분만 남기면 이렇습니다.
{
"version": 4,
"terraform_version": "1.15.8",
"serial": 3,
"resources": [
{
"mode": "managed",
"type": "aws_s3_bucket",
"name": "hello",
"instances": [
{
"attributes": {
"bucket": "my-terraform-hello-20260822",
"arn": "arn:aws:s3:::my-terraform-hello-20260822",
"region": "ap-northeast-2"
}
}
]
}
]
}구조는 단순합니다. 테라폼이 관리 중인 리소스의 목록과, 마지막으로 확인한 각 리소스의 속성값입니다. type과 name이 #2에서 배운 리소스 주소이고, attributes가 참조 문법으로 가져오던 값들의 출처입니다. aws_s3_bucket.hello.arn을 참조하면 테라폼은 AWS에 물어보는 것이 아니라 우선 이 파일을 봅니다.
여기서 보안 문제가 하나 따라옵니다. attributes에는 모든 속성이 평문으로 저장됩니다. RDS를 만들며 넣은 마스터 비밀번호도, #3에서 sensitive로 가렸던 변수의 값도 state 안에는 그대로 있습니다. sensitive는 화면 출력을 가릴 뿐입니다. 그래서 state 파일은 절대 Git에 커밋하지 않으며, .gitignore에 다음을 넣는 것이 표준입니다.
.terraform/
*.tfstate
*.tfstate.*주의할 예외가 하나 있습니다. init이 만드는 .terraform.lock.hcl은 provider 버전을 고정하는 파일로, 이름에 lock이 들어가지만 state와 무관하며 커밋해야 하는 파일입니다.
plan은 삼자 비교입니다 #
plan이 코드와 state 둘만 비교한다고 생각하기 쉽지만, 실제로는 셋을 비교합니다.
코드(.tf) vs state(.tfstate) vs 실제 인프라(AWS API)plan을 실행하면 테라폼은 먼저 state에 있는 리소스들의 현재 모습을 AWS에 조회해 state를 갱신하고(refresh), 그다음 코드와 비교해 할 일을 계산합니다. 실험으로 확인할 수 있습니다. 테라폼으로 만든 버킷을 콘솔에서 손으로 지운 뒤 plan을 실행하면, 테라폼은 refresh 단계에서 버킷이 사라진 것을 알아차리고 “1 to add"를 제안합니다. 코드에는 있어야 한다고 적혀 있으니 다시 만들겠다는 것입니다.
반대로 콘솔에서 태그를 손으로 바꾸면 plan은 코드의 값으로 되돌리는 변경을 제안합니다. 이렇게 코드 밖에서 일어난 변경과 코드의 어긋남을 드리프트(drift)라고 부릅니다. 테라폼으로 관리하는 리소스는 테라폼으로만 바꾸는 것이 원칙인 이유이며, 팀 규모에서 드리프트를 다루는 방법은 운영 시리즈에서 다루겠습니다.
state 조작 명령: 파일을 직접 고치지 않습니다 #
state를 조정할 일이 생기면 텍스트 에디터가 아니라 전용 명령을 씁니다. 파일을 손으로 고치다 JSON을 깨뜨리거나 serial을 어긋나게 하는 것이 최악의 사고이기 때문입니다.
terraform state list # 관리 중인 리소스 주소 목록
terraform state show aws_s3_bucket.hello # 한 리소스의 속성 전체이 둘은 읽기 전용이라 안전하며, 디버깅의 첫 도구입니다. 다음 둘은 state를 바꾸는 명령이므로 신중해야 합니다.
terraform state mv: 리소스의 주소를 바꿉니다. 코드에서aws_s3_bucket.hello를aws_s3_bucket.logs로 이름만 바꾸면 테라폼은 hello를 지우고 logs를 새로 만들려고 합니다. 이름 변경이 파괴가 되는 것입니다.terraform state mv 'aws_s3_bucket.hello' 'aws_s3_bucket.logs'로 state 쪽 주소를 맞춰 주면 재생성 없이 이름이 바뀝니다. 최근 버전에서는 코드에moved블록을 적는 선언적 방법도 있으며, 팀 작업에서는 리뷰에 남는 이쪽을 권합니다.terraform state rm: 리소스를 state에서만 제거합니다. 실제 리소스는 남기고 테라폼의 관리 대상에서만 빼는 것으로, “이 리소스는 이제 다른 코드가 관리한다” 같은 이관 상황에서 씁니다. 실수로 rm 하면 다음 apply가 같은 리소스를 또 만들려다 이름 충돌로 실패하는 식의 혼란이 오므로, 쓰기 전에 반드시 목적을 분명히 해야 합니다.
state를 잃으면 일어나는 일 #
terraform.tfstate가 삭제되면 테라폼은 아무것도 관리하고 있지 않다고 믿습니다. 인프라는 AWS에 멀쩡히 살아 있는데 plan은 전부 새로 만들겠다고 제안하고, 그대로 apply 하면 이름 충돌 에러 또는 중복 리소스가 생깁니다. 복구 수단은 terraform import로 기존 리소스를 하나씩 state에 다시 연결하는 것인데, 리소스가 수십 개면 상당한 노동입니다. 자세한 절차는 운영 시리즈에서 다루겠지만, 기초 단계의 결론은 단순합니다. state는 잃어버리면 안 되는 파일이고, 노트북 한 대의 로컬 디스크는 그 보관처로 적합하지 않습니다. 이 문제의 답인 원격 state는 #9에서 다루겠습니다.
정리 #
이번 글에서 다룬 내용입니다.
- state는 테라폼이 관리 중인 리소스 목록과 마지막 확인 속성값을 담은 JSON이며, 참조 문법이 읽는 값의 출처입니다
- 모든 속성이 평문으로 저장되므로 state는 커밋하지 않습니다. 단,
.terraform.lock.hcl은 state가 아니라 버전 고정 파일이므로 커밋합니다 - plan은 코드, state, 실제 인프라의 삼자 비교입니다. 코드 밖 변경이 만드는 어긋남이 드리프트이고, 관리 리소스는 테라폼으로만 바꾸는 것이 원칙입니다
- state 조정은 파일 편집이 아니라 명령으로 합니다. 이름 변경은
state mv또는moved블록, 관리 제외는state rm입니다 - state를 잃으면 테라폼은 전부 새로 만들려고 합니다. 로컬 디스크는 state 보관처로 부적합하며, 답은 #9의 원격 state입니다
다음 글(#6 의존성과 반복)에서는 리소스가 여러 개로 늘어날 때의 문법을 다룹니다. 테라폼이 생성 순서를 결정하는 원리와 count, for_each로 리소스를 반복 생성하는 방법입니다.