서버, 복구, 백업을 따로 배포한 Terraform
서버, 복구 함수와 백업 정리를 각각 plan하고 apply하도록 세 작업 디렉터리로 나눈 실제 구성
start-minecraft-server/apply.bat의 배포 절차는 짧았다. Discord 봇을 ZIP으로 만든 뒤
Terraform을 바로 적용했다.
powershell -NoLogo -NoProfile -Command ^
"Compress-Archive -Path 'scripts\discord-v2\*' ^
-DestinationPath 'scripts\discord-v2.zip' -Force"
terraform apply -auto-approve
선점 복구 디렉터리의 배치 파일도 같은 형태였다. Python 함수 ZIP을 만들고
terraform apply -auto-approve를 실행했다. 한 저장소 안에 있었지만 서버와 복구 함수는
서로 다른 명령으로 배포됐다. 백업 정리는 별도 작업 디렉터리를 가졌으나 같은 형태의
apply 배치 파일은 남아 있지 않다.
세 디렉터리와 세 번의 apply
세 작업 디렉터리는 서비스별 배포 진입점이었다. 운영자는 바꾸려는 서비스의 디렉터리로 이동해 Terraform을 실행했다.
gcp-terraform-minecraft/
├── start-minecraft-server/
│ ├── main.tf
│ ├── variables.tf
│ ├── outputs.tf
│ └── apply.bat
├── restart-preempted-instance/
│ ├── main.tf
│ ├── variabels.tf
│ ├── outputs.tf
│ └── apply.bat
└── bucket-backup-cleanup/
├── main.tf
├── variables.tf
└── outputs.tf
variabels.tf는 당시 저장소에 남은 실제 파일명이다. 통상 쓰는 variables.tf에서 철자가
어긋났지만 Terraform은 작업 디렉터리의 모든 .tf 파일을 읽었다. 파일명 오타와 관계없이
변수 선언은 로딩 대상이었다.
각 디렉터리에서 plan에 나타나는 리소스, apply가 변경하는 서비스와 state에 저장되는 resource address가 달라졌다. 세 폴더가 각각 하나의 배포 진입점으로 쓰였다.
Minecraft 서버 배포
start-minecraft-server는 플레이어가 접속할 서버를 만들었다.
| Terraform resource | 배포 결과 |
|---|---|
google_compute_address | 고정 외부 IP |
google_compute_firewall | 게임, RCON, Dynmap과 SSH 접속 규칙 |
google_compute_instance | Minecraft VM과 metadata |
google_storage_bucket | 월드 백업 버킷 |
Discord 봇 ZIP도 VM resource의 metadata에 포함됐다. 배치 파일에서 ZIP 내용이 바뀌면 다음 apply가 VM metadata를 변경했다. 인프라와 서버 안의 애플리케이션 파일이 같은 배포 단위에 있었다.
서버 구성에는 별도의 빌드 번호나 artifact 저장소가 없었다. 봇 소스는 Git에 남았지만 생성된 ZIP과 SHA-256은 추적하지 않았다. 어느 커밋에서 만든 ZIP을 실제로 적용했는지는 Git만으로 식별할 수 없다.
선점 복구 함수 배포
restart-preempted-instance는 종료된 VM을 감지하고 같은 인스턴스에 시작 요청을 보낼
Google Cloud 리소스를 배포했다.
| Terraform resource | 배포 결과 |
|---|---|
google_logging_metric | VM 중지 로그를 집계할 지표 |
google_monitoring_alert_policy | 지표 조건을 감시할 정책 |
google_pubsub_topic | 알림을 함수로 전달할 topic |
google_cloudfunctions_function | VM 상태 확인과 시작 요청 |
| IAM resource | 함수와 Monitoring에 필요한 권한 |
배치 파일은 function/main.py와 requirements.txt를 function.zip으로 압축했다. ZIP을
상위 디렉터리로 옮긴 다음 Terraform을 실행하는 순서였다.
cd function
powershell -NoLogo -NoProfile -Command ^
"Compress-Archive -Path 'main.py','requirements.txt' ^
-DestinationPath 'function.zip' -Force"
move function.zip ..
cd ..
terraform apply -auto-approve
디렉터리를 나눈 뒤에는 서버 VM의 사양이나 방화벽을 건드리지 않고 복구 코드만 배포할 수 있었다. 함수가 시작할 인스턴스 이름은 서버 구성에서 자동으로 전달되지 않았다.
백업 정리 함수 배포
bucket-backup-cleanup은 Scheduler가 Pub/Sub 메시지를 보내고, Cloud Function이 오래된
객체를 찾는 구성이었다.
flowchart LR SCHEDULER["Cloud Scheduler"] --> TOPIC["Pub/Sub"] TOPIC --> FUNCTION["백업 정리 함수"] FUNCTION --> BACKUP["백업 버킷의 backups/ 객체"]
정리 함수는 서버가 만든 백업 버킷의 이름을 입력값으로 받았다. 함수 소스용 버킷, Scheduler, Pub/Sub과 IAM은 이 디렉터리의 Terraform이 관리했다.
서버 쪽 백업 버킷에도 같은 보존 기간의 lifecycle rule이 있었다. 버킷을 소유한 state와 객체를 지우는 state가 갈린 이 겹침은 24편에서 따로 다룬다.
운영자 PC에 남은 local state
backend 선언은 세 디렉터리 어디에도 없었다. Terraform은 실행한 디렉터리에 local state를 저장했다.
| 작업 디렉터리 | state에 저장된 원격 대상 |
|---|---|
start-minecraft-server | IP, 방화벽, VM과 백업 버킷 |
restart-preempted-instance | 함수 소스, Pub/Sub, Monitoring, 함수와 IAM |
bucket-backup-cleanup | 함수 소스, Pub/Sub, Scheduler, 함수와 IAM |
보관된 세 state 파일에는 state 계보를 구분하는 lineage 값이 서로 다르게 들어 있었다.
한 디렉터리의 apply는 다른 디렉터리의 state를 읽지 않았다. 운영자 PC의 state 파일을
잃은 뒤 다른 컴퓨터에서 실행하려면 기존 원격 리소스를 다시 연결하는 절차가 필요했다.
저장소에는 import_all.bat, import_vm_restart.bat와 import_existing_resources.sh가
남아 있다. 세 스크립트는 기존 리소스를 state에 연결하도록 작성됐다. 실제 실행 성공
여부는 스크립트만으로 확인할 수 없다.
프로젝트, 인스턴스와 버킷 입력값
서버 구성은 인스턴스 이름과 백업 버킷 이름을 output으로 내보냈다.
output "instance_name" {
value = google_compute_instance.minecraft_server.name
}
output "backup_bucket" {
value = google_storage_bucket.backup_bucket.name
}
복구 구성은 프로젝트, region, zone과 인스턴스 이름을 변수로 다시 받았다. 백업 정리도
프로젝트, region과 대상 버킷 이름을 별도 입력으로 사용했다. 공통 module이나
terraform_remote_state 참조는 없었다.
복구 구성의 변수 선언만 줄이면 다음과 같다.
variable "project_id" {}
variable "region" {}
variable "zone" {}
variable "instance_name" {}
output과 input의 이름이 비슷해도 Terraform이 두 값을 참조해 주지는 않는다. 운영자가 같은 값을 입력해야 복구 함수가 실제 서버를 바라봤다.
저장하지 않은 plan과 자동 승인
두 apply 배치 파일에는 terraform plan -out 단계가 없다. -auto-approve는 Terraform이
apply 전에 묻는 승인을 생략한다. 계획은 터미널에 출력되지만, ZIP을 만든 뒤 변경을
읽고 중단하거나 승인하는 별도 입력 단계 없이 적용이 이어졌다.
이 방식이 실제 장애를 일으켰다는 기록은 찾지 못했다. 저장한 plan 파일이나 CI 실행 기록이 없어 당시 어떤 변경을 읽고 승인했는지는 Git만으로 복원할 수 없다. Git은 HCL과 스크립트를 보여 주지만 apply 직전의 변수, state와 생성된 ZIP까지 보관하지 않았다.
지금 바꿀 배포 절차
서비스별 작업 디렉터리는 그대로 두고, 각 디렉터리에서 plan을 만들고 승인하는 순서를 바꾼다.
| 단계 | 남길 근거 | 중단할 조건 |
|---|---|---|
| artifact 생성 | 소스 해시와 ZIP SHA-256 | 추적하지 않은 파일이나 비밀값 포함 |
| 정적 검사 | Terraform 버전, Provider lock과 검사 결과 | fmt, validate 실패 |
| plan | 저장한 plan과 사람이 읽을 요약 | 예상하지 않은 삭제나 교체 |
| apply | 적용한 plan의 실행 기록 | plan 생성 뒤 입력이나 state 변경 |
| 확인 | VM, 함수, Scheduler와 로그 상태 | 서비스가 준비 상태에 도달하지 못함 |
저장한 plan에는 민감한 값이 평문으로 들어갈 수 있으므로 Git에 커밋하지 않는다.
HashiCorp의 terraform plan
문서도 plan 파일을 민감한
artifact로 다룰 것을 안내한다. Provider 선택을 기록한
.terraform.lock.hcl은
반대로 각 작업 디렉터리에서 버전 관리한다.
local state는 여러 실행자가 공유할 중앙 보관소, 원격 locking과 IAM 접근 제어를 제공하는 GCS backend로 옮긴다. 세 작업 디렉터리는 서로 다른 prefix를 사용한다. 서버가 출력한 프로젝트, zone과 인스턴스 이름은 복구 구성의 명시적인 입력 파일로 전달하고, 실제 식별자가 들어간 파일은 Git에서 제외한다.
세 디렉터리는 배포할 서비스와 plan의 범위를 맞췄다. ZIP 해시, 검토한 plan과 원격 state를 함께 남기면 다음 배포에서 같은 입력과 승인 내역을 확인할 수 있다.
VM은 이 배포 묶음을 metadata에서 읽어 파일과 systemd 유닛으로 배치했다. metadata와 startup script의 배포 계약에 그 키와 소비 코드를 대조했다.
참고
Comments
아직 댓글이 없습니다. 첫 댓글을 남겨주세요.
검토 대기 중