LabHub

Terraform/OpenTofu 기초 · 선언형과 상태 · 이론

선언형 인프라와 상태 파일이라는 장부

LabHub 에서 이어서 보기

한 줄 요약

Terraform 은 "이 명령을 실행하라"가 아니라 "끝났을 때 이런 인프라가 있어야 한다"를 적는 도구다. 그 약속을 지키려고 도구는 상태 파일이라는 장부를 든다.

왜 이게 필요했나

콘솔에서 클릭으로 서버를 만들면 세 가지가 동시에 사라진다. 같은 환경을 다시 만들 방법, 누가 언제 무엇을 바꿨는지에 대한 기록, 그리고 개발과 운영이 같다는 확신이다. 서버 열 대까지는 사람이 외워서 버티지만 백 대는 불가능하다. dev·stage·prod 가 미묘하게 어긋나는 구성 드리프트는 정확히 이 지점에서 시작된다.

그래서 셸 스크립트로 옮겨 보면 새로운 문제가 나온다. 처음엔 "만들어라" 한 줄이었다가, 두 번째 실행에서 "이미 있다" 오류가 나서 조건문을 붙이고, 속성이 달라져 있을 수 있으니 비교문을 붙인다. 결국 스크립트의 절반이 지금 상태를 확인하는 코드가 된다. 선언형 도구는 그 절반을 통째로 가져간다. 사람은 원하는 모습만 적고, 현재와 원하는 것의 차이를 계산하는 일은 도구가 한다.

여기서 진짜 질문이 나온다. 도구는 "지금 상태"를 어떻게 아는가? 매번 클라우드 전체를 훑는 것은 느리고, 무엇보다 그 많은 리소스 중에서 무엇이 내가 만든 것인지 구분할 방법이 없다. 이 문제의 답이 상태 파일이다.

어떻게 동작하나

plan 은 세 가지 값을 비교한다.

| 값 | 어디에 있나 | 의미 |
| --- | --- | --- |
| 원하는 상태 | .tf 코드 | 사람이 선언한 것 |
| 마지막으로 아는 상태 | terraform.tfstate | 지난 적용의 결과 |
| 실제 상태 | 프로바이더 API | 지금 진짜 존재하는 것 |

먼저 실제를 조회해(refresh) 상태를 갱신하고, 갱신된 상태와 코드를 비교해 계획을 만든다. 그래서 상태 파일이 없으면 도구는 "처음 보는 인프라"라고 판단해 모든 것을 새로 만들려 든다. 상태 파일이 인프라 자체보다 위험한 자산인 이유가 여기 있다.

상태 파일은 JSON 이고, 실무에서 실제로 읽게 되는 필드는 몇 개 되지 않는다.

{  "version": 4,  "serial": 42,  "lineage": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",  "resources": [    { "mode": "managed", "type": "local_file", "name": "hello",      "instances": [ { "attributes": { "filename": "/root/out/hello.txt" },                       "dependencies": ["random_pet.suffix"] } ] }  ]}

여기서 attributes마지막으로 적용된 결과의 기록이라는 점이 중요하다. 상태에 적힌 값과 실제 리소스의 값이 어긋나 있다면 그 자체가 드리프트의 정의다. 그래서 상태를 읽을 때 습관적으로 "이 값이 실제와 같은가"를 함께 확인하는 버릇이 필요하다.

version 은 상태 파일 포맷 버전으로 지금은 4다. serial 은 상태가 바뀔 때마다 오르는 번호이고 원격 백엔드에서 동시 수정 충돌을 잡는 데 쓰인다. lineage 는 이 상태 파일의 계보를 나타내는 고유 식별자로, 다른 프로젝트의 상태를 실수로 덮어쓰는 사고를 막는다. resources 안의 typename 을 이어 붙인 local_file.hello 가 우리가 다루는 주소다.

한편 init 은 코드의 required_providers 를 읽어 프로바이더를 내려받고 .terraform.lock.hcl 에 확정된 버전과 해시를 적어 둔다. 이 잠금 파일은 저장소에 커밋해야 한다. 그래야 내 노트북과 CI 가 같은 프로바이더를 쓴다.

현장에서 만나는 모습

첫째, 상태 파일을 Git 에 커밋하는 사고. 상태에는 데이터베이스 비밀번호나 토큰이 평문으로 들어간다. 게다가 두 사람이 각자 apply 하면 병합 충돌이 나고, 잘못 합친 상태는 리소스를 통째로 잃게 만든다. 그래서 실무에서는 S3 같은 원격 백엔드에 버저닝을 켜서 두고, 잠금으로 동시 apply 를 막는다. AWS 라면 오랫동안 S3 + DynamoDB 조합이 표준이었다.

둘째, 계보가 다른 상태를 덮어쓴 날. 팀이 백엔드 키를 잘못 적어 다른 스택의 상태 위에 자기 상태를 올려 버리면, 다음 plan 은 "수백 개를 삭제하겠다"고 말한다. 이때 살아나는 것은 S3 버저닝의 이전 버전이다. lineage 는 그런 사고를 사전에 알아채라고 존재하는 필드다.

셋째, 상태 안의 비밀. OpenTofu 는 상태와 계획 파일 암호화를 도구 자체에서 지원한다. Terraform 에서 같은 수준을 얻으려면 유료 서비스가 필요하다. 명령 이름과 하위 명령 구조는 거의 동일해서, 이 실습에서 tofu 로 배운 것은 terraform 에 그대로 옮겨 간다.

다음 실습에서 할 것

/root/tf/first 에서 프로바이더를 받아 초기화하고, 파일 하나를 선언해 적용한다. 이어서 무작위 이름을 만드는 리소스를 붙이고, 선언을 바꿔 다시 적용하며 serial 이 오르는 것을 눈으로 본다. 임시 리소스를 하나 만들었다가 그것만 골라 지우고, 상태에 남은 주소 목록을 뽑는다. 마지막에는 값을 베끼는 대신 참조해서 의존 관계가 상태에 기록되게 만든다.