Terraform에서 OpenTofu로 이관하며 삽질한 3주

지난 3주 동안 팀 인프라 코드를 Terraform 1.5.7에서 OpenTofu 1.10으로 넘겼다. "5분이면 된다"는 블로그 글들에 낚였다. 실제로는 5분이 아니라 3주였고, 그 중 절반은 예상 못 한 곳에서 시간이 갔다.
기록해둔다. 언젠가 같은 삽을 뜨는 사람이 이 글을 보길.
왜 굳이 지금 옮겼나
솔직히 얼마 전까지는 옮길 이유가 별로 없다고 봤다. HCL 문법도 거의 같고 프로바이더도 호환되고, 우리 팀 규모(모듈 40개, workspace 12개)에서 라이선스 이슈가 당장 발등의 불도 아니었다.
바뀐 계기는 두 가지였다.
첫째, state encryption. OpenTofu 1.10에는 native state encryption이 들어있는데 Terraform은 아직 없다. 우리는 지금까지 S3 bucket encryption + KMS로 커버해왔지만, 로컬 백업이나 CI 러너의 임시 state 파일이 평문으로 굴러다니는 게 계속 신경 쓰였다. 감사팀에서도 이 부분을 슬슬 지적하기 시작했다.
둘째, provider-defined functions와 for_each on providers. 우리 팀은 멀티리전 배포가 많은데, 이걸 지금까지 provider alias를 module 안에서 못 쓰는 것 때문에 억지로 module 여러 개 호출로 우회하고 있었다. OpenTofu에서 이게 자연스럽게 되니까, 리팩터링 하고 싶은 코드가 눈에 밟혔다.
첫 주: tofu init은 정말 잘 됐다
첫날은 신났다. 로컬에서 brew install opentofu 하고 프로젝트 하나 골라서 tofu init, tofu plan 하니까 정말 아무 문제 없이 돌아갔다. plan diff도 0.
이 상태로 팀 슬랙에 "이관 별거 없네요 ㅎㅎ" 하고 흘렸다가 나중에 진심으로 후회했다.
두 번째 주: state migration에서 진짜 문제 시작
문제는 remote state를 옮기는 순간부터 터졌다.
우리는 S3 백엔드에 state를 두고 DynamoDB로 lock을 잡는다. OpenTofu에서 같은 백엔드를 쓰는 건 되는데, 팀원 절반이 아직 Terraform을 쓰고 있는 상태에서 같은 state 파일을 계속 나왔다.
가장 큰 게 이거였다. OpenTofu는 state 파일 상단에 "terraform_version" 필드를 자기 버전으로 쓴다. 그러면 Terraform 쪽에서 그 state를 열 때 Error: state was created with a newer version of Terraform 이 뜬다. 실제로 신 버전인 것도 아니고 그냥 문자열 비교라 그런데, 이거 우회하려면:
terraform {
required_version = ">= 1.5.0, < 2.0.0"
}
이렇게 상한만 열어두고, tofu 쪽에서는 -state 파일을 apply 후에 직접 sed로 버전 문자열을 되돌려놓는 지저분한 스크립트를 하나 파야 했다. 팀 전체가 넘어갈 때까지는 이 워크어라운드가 필요했다.
두 번째로 뜨악했던 건 .terraform.lock.hcl. OpenTofu가 프로바이더를 자기 registry(registry.opentofu.org)에서 받으니까 lock file의 hash가 달라진다. 같은 프로바이더인데. 그래서 브랜치에서 tofu init 한 번 돌린 걸 PR 올리면 CI에서 terraform init이 hash mismatch로 실패한다.
결국 이렇게 정리했다.
- 모든 모듈의
source를 명시적으로 registry URL로 바꾼다. - lock file은 두 도구가 모두 이해할 수 있도록
.terraform.lock.hcl을 커밋에서 제외하고.gitignore에 넣고, CI에서 매번init -upgrade로 재생성. 이건 원래 권장되는 방식은 아닌데, 이관 기간 중에는 어쩔 수 없었다.
이관 완료 후에 다시 lock file을 커밋으로 되돌렸다.
세 번째 주: state encryption 붙이려다 하루 날림
기왕 이관한 김에 state encryption을 켜기로 했다. 문서만 보면 간단해 보였다.
terraform {
encryption {
key_provider "aws_kms" "primary" {
kms_key_id = "arn:aws:kms:ap-northeast-2:..."
region = "ap-northeast-2"
}
method "aes_gcm" "primary" {
keys = key_provider.aws_kms.primary
}
state {
method = method.aes_gcm.primary
enforced = false
}
}
}
enforced = false 로 켜서 기존 평문 state를 읽을 수 있게 한 상태에서 tofu apply 한 번 돌리면 다음부터는 암호화된다.
여기까지는 됐다. 문제는 CI 러너였다. 우리는 GitHub Actions에서 IRSA로 KMS 권한을 받는데, 초기에는 kms:Decrypt 만 주고 kms:GenerateDataKey 를 빼먹었다. 이러니까 첫 apply는 되는데 (기존 state 복호화만 필요) 그 다음부터 새 state를 쓸 때마다 실패한다. 로그가 좀 애매해서 처음에는 IAM 문제인지도 못 알아챘다.
정확히 필요한 권한은 이거다.
{
"Effect": "Allow",
"Action": [
"kms:Decrypt",
"kms:GenerateDataKey",
"kms:DescribeKey"
],
"Resource": "arn:aws:kms:ap-northeast-2:...:key/..."
}
한 번 되니까 문제는 없는데, 이걸로 반나절을 날렸다. 문서에는 Decrypt 만 예시에 있어서.
그래서 다시 하라면
만약 지금 시작하는 사람이 있다면 이 순서를 권한다.
- 팀원 전원이 OpenTofu 설치. Terraform 병행 사용은 그냥 하지 마라. 어차피 짧게 갈 거면 하루 날 잡고 전원 전환이 낫다.
.terraform.lock.hcl은 이관 기간에는 gitignore. 나중에 되돌린다.- state encryption은 이관 완료 후 별도 PR로. 이관과 암호화를 한꺼번에 하면 뭐가 문제인지 구분이 안 된다.
- CI IAM 정책 미리 손 대두기.
Decrypt,GenerateDataKey,DescribeKey세 개. - 프로덕션 workspace는 마지막에. 스테이징과 개발 환경에서 최소 일주일 굴려보고 옮겨라.
옮길 만한 가치는 있었나
지금 시점에서는 그렇다고 본다. state encryption 하나만으로도 감사팀 티켓이 하나 사라졌고, provider for_each 덕분에 멀티리전 모듈 코드가 30% 정도 짧아졌다. 다만 팀 규모가 작거나 이미 다른 우선순위가 있으면 급할 이유는 없다고 생각한다. HashiCorp가 언제 라이선스를 또 바꿀지, 아니면 그냥 이 상태로 갈지도 모르는 상황이고.
다음번엔 OpenTofu Stacks가 GA 되면 그것도 한 번 정리해보려고 한다.
혹시 같은 이관 하신 분들 계시면, state 파일 버전 문자열 문제 어떻게 푸셨는지 궁금하다. 우리는 결국 sed 스크립트로 밀고 갔는데 더 우아한 방법이 있을 것 같다.