ZIP 배포와 Terraform import 기록
배포 묶음과 state가 이미 존재하던 Google Cloud 리소스를 이어받은 순서
2025년 저장소의 세 작업 디렉터리에는 모두 import 스크립트가 있었다. 이미 존재하던 Google Cloud 리소스를 HCL의 resource address와 state에 연결한 흔적이다.
terraform import google_compute_address.minecraft_static_ip ^
projects/<PROJECT_ID>/global/addresses/<STATIC_IP_NAME>
terraform import google_storage_bucket.backup_bucket <BACKUP_BUCKET>
terraform import google_compute_instance.minecraft_server ^
projects/<PROJECT_ID>/zones/<ZONE>/instances/<INSTANCE_NAME>
import는 이미 존재하는 리소스를 state에 연결하는 명령이다. import 대상과 보관된
state를 함께 보면 고정 IP, 버킷과 VM이 먼저 존재했고 Terraform이 나중에 관리하기
시작한 순서를 확인할 수 있다.
배포 묶음과 배치 파일, import 스크립트를 함께 읽어 당시의 배포 절차를 복원했다. 아래 인용은 모두 저장소에 남은 코드이고, 실제 식별자는 자리 표시자로 바꿨다. 어떤 명령이 언제 성공했는지는 실행 로그가 없어 확인할 수 없다.
ZIP을 만드는 줄과 적용하는 줄
21편에서
본 것처럼 서버와 선점 복구 디렉터리에는 배치 파일이 하나씩 있었고, 소스를 ZIP으로
묶은 뒤 terraform apply -auto-approve를 실행하는 형태였다. 백업 정리 디렉터리에는
같은 형태의 apply 배치 파일이 남아 있지 않다.
filebase64는 배치 파일이 방금 만든 ZIP을 읽었다. ZIP이 바뀌면 metadata 값과 VM
속성이 함께 바뀌었다. 22편에서
확인한 것처럼 애플리케이션을 빌드하는
명령과 인프라를 적용하는 명령이 한 파일 안에서 이어지고, 그 결과가 같은 계획에
섞인다.
여기에 -auto-approve가 붙어 계획 출력과 실제 변경 사이의 승인 입력 단계도
사라졌다.
세 개 남은 함수 ZIP
선점 복구 디렉터리에는 함수 배포 묶음이 세 개 남아 있다.
function.zip 2,301 바이트 6월 27일
function_<타임스탬프>.zip 2,119 바이트 6월 20일
function_<타임스탬프>.zip 2,119 바이트 6월 20일
이름에 타임스탬프가 붙은 두 개는 25초 차이로 만들어졌고 크기가 같다. 그리고
일주일 뒤 이름이 없는 function.zip이 더 큰 크기로 남았다. Terraform이 참조하는
것은 마지막 이름 하나뿐이다.
resource "google_storage_bucket_object" "function_zip" {
name = "function-${filesha256("function.zip")}.zip"
...
source = "function.zip"
}
타임스탬프가 붙은 두 파일은 어떤 명령도 읽지 않는다. 배포 중에 만들어진 사본으로 보이지만, 어느 것이 실제로 올라갔는지는 파일만으로 알 수 없다. 소스 디렉터리와 배포 묶음은 서로 다른 파일이고, 둘이 같은 내용인지 확인하는 장치는 없었다.
객체 이름의 filesha256 때문에 ZIP 내용이 바뀌면 객체 이름도 바뀌고 함수가 새
객체를 참조했다. 이 해시는 ZIP 파일의 해시이며 소스
디렉터리의 해시가 아니다. ZIP을 다시 만들지 않고 apply하면 예전 묶음이 그대로
올라간다.
import 스크립트가 남긴 순서
import 스크립트 세 개는 각 리소스가 코드보다 먼저 존재했다는 기록이다. 서버 디렉터리의 파일은 일곱 줄이었다. 고정 IP 하나, 버킷 하나, 방화벽 네 개, 인스턴스 하나다.
import 목록에는 리소스 하나가 빠져 있었다. 당시 main.tf가 관리한 대상은 여덟
개였고 그중 방화벽이 다섯 개였다. 지도 플러그인용 방화벽 하나가 import 목록에서
빠져 있다.
import 목록에서 빠진 리소스는 다음 plan에 새로 만들 대상으로 나타난다. import한 일곱 개는 state와 실제 리소스가 연결되지만,
빠진 하나는 Terraform이 “아직 없는 리소스”로 본다. 다음 apply는 그것을 새로
만들려 하고, 같은 이름이 이미 있으면 실패한다. -auto-approve로 실행하면 이
차이를 apply가 멈춘 뒤에야 알게 된다.
보관된 state에는 이 방화벽도 관리 대상으로 들어 있다. 어느 시점에 어떻게 연결됐는지는 저장소에 남아 있지 않다. 스크립트를 고쳐 다시 돌렸을 수도 있고, 손으로 한 줄을 실행했을 수도 있다.
선점 복구 디렉터리의 import 스크립트에는 [1/12]부터 [11/12]까지 번호가 붙어
있다. 12번째 단계는 없고, 실제 terraform import 줄은 열다섯이다. 버킷, Pub/Sub
topic, 함수, IAM 바인딩 세 개, 로그 메트릭, 알림 채널, 알림 정책, 그리고 API 다섯
개를 차례로 연결한다.
main.tf가 관리하는 리소스는 열다섯인데 이 목록이 가리키는 것은 중복 한 줄을 빼면
열넷이다. 빠진 하나는 함수 소스 객체로, Terraform이 ZIP에서 새로 만드는 대상이다.
두 줄은 자리 표시자인 채로 남아 있다. 아래에서 꺾쇠는 이 글에서 가린 값이고, 대괄호는 원본 스크립트에 그대로 있던 미기입 자리다.
terraform import google_monitoring_notification_channel.pubsub_channel ^
<PROJECT_ID>/notificationChannels/[CHANNEL_ID]
terraform import google_monitoring_alert_policy.alert_policy ^
<PROJECT_ID>/alertPolicies/[ALERT_POLICY_ID]
[CHANNEL_ID]와 [ALERT_POLICY_ID]는 실행하기 전에 실제 값으로 바꿔야 한다.
번호가 중간에서 끊기고 자리 표시자가 남아 있어, 이 파일을 처음부터 끝까지 실행했다는
근거로 사용할 수 없다.
마지막 줄은 같은 로그 메트릭을 서로 다른 ID 형식으로 두 번 import한다.
terraform import google_logging_metric.vm_stopped_metric ^
projects/<PROJECT_ID>/metrics/<LOG_METRIC_NAME>
...
terraform import google_logging_metric.vm_stopped_metric <LOG_METRIC_NAME>
어느 형식이 맞는지 확신하지 못한 채 둘 다 적어 둔 형태다. 먼저 성공한 쪽이 있으면 나머지는 “이미 관리 중”이라는 오류를 낸다.
백업 정리 디렉터리의 셸 스크립트는 다른 방식으로 같은 불확실성을 다뤘다.
terraform import \
google_storage_bucket.function_source_bucket \
"${FUNC_SOURCE_BUCKET}" || true
모든 import에 || true가 붙어 있다. 이미 import된 리소스를 다시 부를 때 스크립트가
멈추지 않게 하려는 의도로 보인다. 대신 진짜 실패도 함께 삼킨다. 권한이 없어서
실패했든 리소스 이름이 틀렸든, 스크립트는 끝까지 돌고 성공으로 끝난다. 어디까지
연결됐는지는 실행 뒤 terraform plan을 따로 읽어야 알 수 있다.
같은 값을 배치 파일과 변수 파일에 따로 적었다
서버 import 배치 파일은 첫머리에 상수를 선언한다.
set PROJECT_ID=<PROJECT_ID>
set ZONE=<ZONE>
set BACKUP_BUCKET=<BACKUP_BUCKET>
set MINECRAFT_PORT=<GAME_PORT>
set RCON_PORT=<RCON_PORT>
같은 값이 variables.tf의 기본값에도 있었다. 복구 쪽 배치 파일은 프로젝트, 리전,
존, 인스턴스와 함수 이름을 따로 선언했고, 백업 정리 쪽은 셸 스크립트라 선언 형태가
또 달랐다. 프로젝트 ID를 바꾸려면 변수 파일과 스크립트를 함께 고쳐야 했고, 한쪽만
고치면 import 대상과 apply 대상이 달라진다.
코드 밖에 남은 배포 조건
당시 배포 절차를 순서대로 놓으면 이렇게 된다.
flowchart LR A["Console·gcloud로 리소스 생성"] --> B["HCL 작성"] B --> C["import 스크립트로 state 연결"] C --> D["ZIP 생성"] D --> E["apply -auto-approve"] E --> F["출력으로 결과 확인"]
이 순서에는 실제 사정이 있다. 서버가 이미 돌아가고 있었고, 플레이어가 접속 중이었다. 실행 중인 서버를 전부 지우고 코드로 다시 만드는 방법은 당시 선택하지 않았다. 기존 리소스를 유지하면서 Terraform 관리 대상으로 전환하려면 import가 필요했다.
문제는 그다음이었다. import가 어디까지 성공했는지 확인하는 단계가 절차에 없었다.
terraform plan을 저장해 읽었다면 빠진 방화벽 하나가 “생성 예정”으로 보였을
것이다. -auto-approve는 계획 출력 뒤의 승인 입력을 생략했다.
배포 결과는 저장소의 HCL만으로 결정되지 않았다. 어떤 ZIP이 그 순간 디렉터리에 있었는지, import를 어디까지 돌렸는지, 배치 파일의 상수와 변수 기본값이 일치했는지에 따라 달라졌다. 같은 코드로 같은 결과를 다시 만들려면 코드 밖의 이 조건들까지 맞춰야 한다.
지금 구축 트랙이 다르게 하는 것
구축 트랙은 계획을 파일로 저장하고, 저장한 계획만 적용한다.
terraform plan -out=minecraft.tfplan
scripts/review-plan.sh minecraft.tfplan
terraform apply minecraft.tfplan
중간의 검토 스크립트는 첫 배포 계획에 수정, 교체, 삭제가 있거나 필수 리소스 종류가
빠지면 실패한다. 다만 일부 리소스만 import한 뒤 나머지가 생성으로 표시되는 계획은
통과할 수 있다. 이미 같은 이름의 원격 리소스가 있으면 그 충돌도 plan만으로 확인되지
않고 apply에서 드러날 수 있다. import 목록과 terraform show를 사람이 함께 읽어야
하는 이유다.
같은 이름의 리소스가 이미 있다는 오류가 나면 구축 트랙은 새 이름을 쓸지, 소유권을 확인하고 import할지 먼저 정하라고 안내한다. 검토 스크립트가 이 판단을 대신하지는 않는다.
배포 묶음은 아예 줄였다. 서버 JAR는 ZIP으로 넣지 않고 URL과 SHA-256으로 전달한다.
04편의 예제 루트에는
.terraform.lock.hcl이 포함돼 Provider 버전이 고정된다.
import는 실행 중인 리소스를 유지하면서 Terraform 관리 대상으로 전환하는 경로였다. 무엇이 연결됐고 무엇이 남았는지 확인하는 단계가 절차에 없었고, 보관된 세 state에는 그 뒤 겹쳐 관리한 리소스가 남았다.
참고
Comments
아직 댓글이 없습니다. 첫 댓글을 남겨주세요.
검토 대기 중