TT Lab
시작하기
배우기 러닝패스 코스

Terraform 실전

상태 수술 — 실물을 건드리지 않고 기억만 고치기

TT Lab 에서 이어서 보기

한 줄 요약

state mv 와 state rm 은 인프라를 바꾸지 않는다. 도구의 기억만 바꾼다. 이 한 문장을 오해하면 사고가 나고, 이해하면 리팩터링이 안전해진다.

왜 이게 필요했나

인프라 코드는 반드시 리팩터링을 겪는다. 리소스 이름이 마음에 들지 않아서, 리소스 몇 개를 모듈로 묶고 싶어서, 상태 파일이 너무 커져서 나누고 싶어서. 그런데 코드에서 이름을 바꾸는 순간 도구는 옛 이름의 자원이 사라졌고 새 이름의 자원이 생겼다고 판단한다. 계획에는 파괴와 생성이 나란히 뜬다. 실제 운영 DB 였다면 그 계획을 승인하는 순간 끝이다.

반대 방향의 문제도 있다. 장애 대응 중에 누군가 콘솔에서 보안 그룹을 만들었다. 실물은 존재하는데 코드와 상태에는 없다. 다음 apply 는 그 자원을 모르므로 건드리지 않지만, 아무도 관리하지 않는 자원이 하나 늘어난 것이다. 이 자원을 코드 관리 아래로 데려오는 것이 import 다.

이 두 방향 — 주소를 옮기는 일과 바깥의 것을 데려오는 일 — 이 상태 수술의 전부다.

state rm 과 destroy 가 건드리는 곳

두 명령 모두 주소 하나를 겨누지만 건드리는 곳이 다르다. 본문은 state rm 을 삭제가 아니라 관리 포기 선언이라고 부른다. 주소 aws_instance.web 은 설명용 예시다.

  • state rm장부, 곧 상태 파일에서 항목을 지운다. 실물은 그대로 남고 도구만 그것을 잊는다. 실물에는 아무 영향이 없다.
  • destroy실물까지 지우고 싶을 때 쓰는 쪽이다. 장부만 고치는 state rm 과 달리 실제 자원을 건드린다.

여기서 구분할 것 다른 스택으로 옮길 때 state rm 을 쓰는데, 옮길 곳에 import 하기 전까지는 아무도 관리하지 않는 상태다. 그 사이에는 아무도 apply 하지 않게 한다.

잠깐, 예측해 보세요 state rm 을 했는데 코드의 리소스 블록은 지우지 않고 plan 을 돌렸다. 도구는 그 자원을 어떻게 읽을까?

설명 확인 · 채점 없는 자가 점검

장부에 없으니 아직 만들지 않은 자원으로 읽고 새로 만들자고 한다. 실물이 이미 있으면 이름이 겹쳐 실패하거나 최악에는 중복 자원이 생긴다. 관리를 포기한다는 뜻이라면 코드의 블록도 함께 정리해야 한다.

근거 문서

어떻게 동작하나

상태 파일은 "내가 만든 자원의 주소와 마지막으로 알던 속성"을 적은 장부다. 수술 도구는 네 가지다.

명령 하는 일 실물에 대한 영향
state list 주소 목록 출력 없음
state show <주소> 그 주소의 속성 전체 출력 없음
state mv A B 장부에서 A 항목을 B 이름으로 옮김 없음
state rm A 장부에서 A 항목을 지움 없음 (실물은 그대로 남는다)
import 실물을 장부에 등록 없음

가장 자주 오해되는 것이 state rm 이다. 이것은 삭제 명령이 아니라 관리 포기 선언이다. 실물은 그대로 남고 도구만 잊는다. 그래서 state rm 뒤에 코드에서도 해당 블록을 지우지 않으면, 다음 계획은 "그 자원이 없네, 새로 만들자"고 말한다. 실제 클라우드에서는 이름이 겹치는 자원을 또 만들려다 실패하거나, 최악의 경우 중복 자원이 생긴다. 실물까지 지우고 싶다면 destroy 를 써야 한다.

state mv 와 같은 일을 코드로 하는 것이 moved 블록이다. 차이는 기록에 있다. state mv 는 누군가의 터미널에서 한 번 실행되고 사라지지만, moved 는 코드에 남아 팀원이 plan 만 돌려도 같은 이주가 자동으로 일어난다. 그래서 혼자 쓰는 상태에는 state mv, 여럿이 쓰는 코드에는 moved 가 기본이다.

import 도 두 가지 형태가 있다. 명령줄 tofu import <주소> <ID> 는 즉시 실행되고 상태를 바꾼다. import {} 블록은 코드에 선언해 두고 계획으로 먼저 확인한 뒤 apply 로 반영한다. 후자는 리뷰가 가능하고 이력이 남기 때문에 협업에서는 블록 방식이 낫다. 둘 다 가져올 자리(리소스 블록)를 미리 만들어 둬야 한다는 점은 같다. 빈 껍데기가 없으면 도구는 그 ID 를 어디에 넣어야 할지 모른다.

현장에서 만나는 모습

첫째, 수술 전 사본은 협상 대상이 아니다. 상태를 만지는 모든 명령 전에 파일을 복사해 둔다. 원격 백엔드라면 state pull 로 받아 둔다. 잘못 옮긴 주소는 되돌리기 어렵지만, 사본이 있으면 그냥 되돌리면 된다. 사본이 진짜인지 확인하는 방법은 lineage 값 비교다 — 같은 상태의 계보를 가진 파일이어야 한다.

둘째, 상태 손상은 대개 중단된 apply 에서 온다. 네트워크가 끊긴 채 apply 가 죽으면 파일이 깨진다. 원격 백엔드의 버전 관리로 이전 버전을 복원하고 apply -refresh-only 로 실물과 다시 맞추는 것이 표준 복구 절차다. 로컬 파일 하나로 운영하는 구성이 위험한 진짜 이유가 이것이다.

셋째, import 후 첫 계획은 반드시 읽는다. 가져온 자원의 실제 속성이 코드와 다르면 계획은 그 차이를 없애려 든다. 콘솔에서 만든 자원의 세부 설정을 코드에 옮겨 적기 전에 apply 하면, 데려오자마자 설정이 날아간다. import 직후에는 prevent_destroy 를 임시로 걸어 두는 것도 흔한 방어책이다.

넷째, 상태를 옮기는 작업은 잠금과 함께 생각한다. 여럿이 쓰는 원격 상태에서 수술 중에 남이 apply 를 돌리면 결과를 예측할 수 없다. 작업 시간을 공지하고, 가능하면 파이프라인을 잠깐 멈춘다.

상태를 손대기 전에 반드시 하는 것

state rm, state mv, import 는 실제 자원을 건드리지 않지만, 테라폼이 세상을 보는 눈을 바꾼다. 잘못하면 살아 있는 자원이 관리 밖으로 나가거나, 다음 apply 가 그것을 지운다.

먼저 상태를 받아 둔다. 원격 백엔드라도 로컬에 사본을 남긴다.

terraform state pull > state.backup.json
terraform state list | tee resources.txt

되돌릴 방법이 있는지가 손댈 수 있는지를 정한다.

import 는 상태에만 넣는다. 코드가 없으면 다음 계획에서 "지우겠다" 가 나온다. 그래서 순서는 코드를 먼저 쓰고, import 하고, 계획이 비는지 확인하는 것이다. 계획이 비지 않으면 코드가 실제 자원과 다르다는 뜻이므로 코드를 맞춘다.

terraform plan -generate-config-out=generated.tf   # 코드 초안을 만들어 준다
terraform import aws_s3_bucket.logs my-logs-bucket
terraform plan     # 여기서 "No changes" 가 나와야 끝난 것이다

import 블록을 코드에 적는 방식이면 계획에서 미리 볼 수 있어 더 안전하다.

import {
  to = aws_s3_bucket.logs
  id = "my-logs-bucket"
}

state rm 은 자원을 버리는 것이 아니라 놓아주는 것이다. 실제 자원은 그대로 남고 테라폼만 잊는다. 다른 스택으로 옮길 때 쓰는데, 옮길 곳에 import 하기 전에는 아무도 관리하지 않는 상태가 되므로 그 사이에 아무도 apply 하지 않게 한다.

모듈을 옮길 때는 주소가 통째로 바뀐다. 자원 하나씩 state mv 하는 대신 moved 블록을 쓰면 계획에 드러나고 검토를 받을 수 있다. 상태 조작은 기록이 남지 않지만 코드에 적은 이동은 커밋에 남는다.

손대기 전에 잠금이 있는지 확인한다. CI 가 도는 중에 상태를 고치면 두 쪽이 서로 다른 상태를 올린다.

바깥에서 만든 자원을 코드 아래로 데려오는 순서

장애 대응 중 콘솔에서 만든 보안 그룹처럼 실물은 있는데 코드와 상태에는 없는 자원을 관리 아래로 데려오는 순서다. 본문은 코드를 먼저 쓰고, import 하고, 계획이 비는지 본다고 말한다. 자원 이름 logs 는 설명용 예시다.

  1. 상태 사본을 뜬다state pull 로 받아 두고 state list 결과도 파일로 남긴다. 되돌릴 방법이 있는지가 손댈 수 있는지를 정한다.
  2. 코드를 먼저 쓰고 import 한다가져올 자리인 리소스 블록이 먼저 있어야 한다. 빈 껍데기가 없으면 도구는 그 ID 를 어디에 넣어야 할지 모른다. 계획으로 미리 볼 수 있는 import 블록이 협업에는 더 안전하다.
  3. 계획이 비는지 본다변경 없음이 나와야 끝난 것이다. 비지 않으면 코드가 실제 자원과 다르다는 뜻이므로 코드를 실제에 맞춘다.

여기서 구분할 것 import 는 상태에만 넣고 실물은 바꾸지 않는다. 그런데 코드가 실물과 다르면 다음 계획이 그 차이를 없애려 든다. 콘솔에서 만든 세부 설정을 코드에 옮겨 적기 전에 apply 하면 데려오자마자 설정이 날아간다.

잠깐, 예측해 보세요 팀 여럿이 쓰는 코드에서 import 를 명령줄로 즉시 실행하는 방식과 import 블록으로 적는 방식 중 무엇이 나을까?

설명 확인 · 채점 없는 자가 점검

블록 방식이다. 코드에 선언해 두고 계획으로 먼저 확인한 뒤 apply 로 반영하므로 리뷰가 가능하고 이력이 남는다. 명령줄은 즉시 실행되어 상태를 바꾸고 그 사실이 코드에 남지 않는다. 어느 쪽이든 가져올 리소스 블록은 미리 있어야 한다.

근거 문서

다음 실습에서 할 것

먼저 사본을 뜨고 상태를 들여다본 다음, state mv 로 리소스 이름을 바꾸고 계획이 비는 것을 확인한다. 같은 명령으로 리소스를 모듈 안으로 옮기고, state rm 이 실물을 지우지 않는다는 사실을 계획으로 증명한다. 그다음 import {} 블록과 tofu import 명령 두 가지 방법으로 바깥의 자원을 상태에 편입시키고, 마지막에 -detailed-exitcode 로 코드와 상태가 완전히 일치함을 종료 코드로 증명한다.