IT/IaC

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

gfrog 2026. 8. 5. 18:19

 

지난 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 만 예시에 있어서.

그래서 다시 하라면

만약 지금 시작하는 사람이 있다면 이 순서를 권한다.

  1. 팀원 전원이 OpenTofu 설치. Terraform 병행 사용은 그냥 하지 마라. 어차피 짧게 갈 거면 하루 날 잡고 전원 전환이 낫다.
  2. .terraform.lock.hcl 은 이관 기간에는 gitignore. 나중에 되돌린다.
  3. state encryption은 이관 완료 후 별도 PR로. 이관과 암호화를 한꺼번에 하면 뭐가 문제인지 구분이 안 된다.
  4. CI IAM 정책 미리 손 대두기. Decrypt, GenerateDataKey, DescribeKey 세 개.
  5. 프로덕션 workspace는 마지막에. 스테이징과 개발 환경에서 최소 일주일 굴려보고 옮겨라.

옮길 만한 가치는 있었나

지금 시점에서는 그렇다고 본다. state encryption 하나만으로도 감사팀 티켓이 하나 사라졌고, provider for_each 덕분에 멀티리전 모듈 코드가 30% 정도 짧아졌다. 다만 팀 규모가 작거나 이미 다른 우선순위가 있으면 급할 이유는 없다고 생각한다. HashiCorp가 언제 라이선스를 또 바꿀지, 아니면 그냥 이 상태로 갈지도 모르는 상황이고.

다음번엔 OpenTofu Stacks가 GA 되면 그것도 한 번 정리해보려고 한다.

혹시 같은 이관 하신 분들 계시면, state 파일 버전 문자열 문제 어떻게 푸셨는지 궁금하다. 우리는 결국 sed 스크립트로 밀고 갔는데 더 우아한 방법이 있을 것 같다.