OpenTofu 완벽 가이드 — Terraform 대체, 마이그레이션, 실전 운영

OpenTofu 완벽 가이드 — Terraform 대체, 마이그레이션, 실전 운영

이 글의 핵심

HashiCorp의 Terraform BSL 라이선스 전환 이후 등장한 OpenTofu는 Linux Foundation 산하 오픈소스 포크로, 기존 Terraform 설정을 대부분 그대로 사용할 수 있습니다. 설치·마이그레이션·providers·모듈·원격 state·CI/CD·정책·시크릿까지 실전 중심으로 정리합니다.

이 글의 핵심

OpenTofu는 HashiCorp가 2023년 8월 Terraform 라이선스를 MPL(오픈소스)에서 BSL(Business Source License)로 변경하자, Linux Foundation 산하에서 포크된 오픈소스 IaC 도구입니다. 2024년 1.6 GA 이후 매 분기 기능을 추가하며 2026년 현재 1.8 기준으로 Terraform 1.5 문법 100% 호환 + 독자 기능(provider iteration, -exclude 플래그, 내장 state 암호화)을 갖춘 성숙한 대안입니다.

이 글에서는 설치부터 tfstate 마이그레이션, 모듈 구조, 원격 백엔드, CI/CD, 시크릿 관리, 정책 검증까지 실무 운영에서 바로 쓸 수 있는 패턴을 정리합니다.

왜 OpenTofu인가

1. 라이선스 리스크 회피

BSL은 “경쟁 제품에 Terraform을 포함할 수 없다”는 제약이 있습니다. 일반 엔터프라이즈 사용엔 문제가 없지만, 다음 경우에 이슈가 됩니다.

  • SaaS 제품에 Terraform을 번들로 제공
  • Terraform 래퍼를 상업적으로 재배포 (예: Spacelift, env0, Scalr)
  • 사내 플랫폼이지만 다른 부서에 “Terraform-as-a-Service”로 과금

OpenTofu는 MPL 2.0으로 이런 제약이 없습니다.

2. 중립 거버넌스

Linux Foundation 산하 OpenTofu Foundation이 운영하며 HashiCorp 단독 결정에 종속되지 않습니다. 기여자는 Harness, env0, Spacelift, Scalr, GitLab, Oracle 등 다수 기업이며 PR 처리 속도가 Terraform보다 빠르다고 평가됩니다.

3. Terraform에 없는 고유 기능

provider "aws" {
  for_each = toset(["us-east-1", "eu-west-1", "ap-northeast-2"])
  alias    = "region"
  region   = each.key
}

resource "aws_s3_bucket" "multi_region" {
  for_each = aws_provider["region"]
  provider = aws.region[each.key]
  bucket   = "my-app-${each.key}"
}

provider for_each는 Terraform엔 아직 없는 기능으로, 다중 리전 리소스를 DRY하게 선언할 수 있습니다.

또한 tofu plan -exclude=module.legacy로 특정 리소스를 제외한 plan도 지원합니다(Terraform은 -target만 지원).

설치

공식 바이너리 (권장)

# macOS (Homebrew)
brew install opentofu

# Linux (Snap / 공식 스크립트)
curl --proto '=https' --tlsv1.2 -fsSL https://get.opentofu.org/install-opentofu.sh \
  -o install-opentofu.sh
chmod +x install-opentofu.sh
./install-opentofu.sh --install-method standalone
sudo mv /usr/local/bin/tofu /usr/local/bin/

# Windows (Scoop)
scoop bucket add main
scoop install opentofu

Docker

docker run --rm -v "$(pwd):/work" -w /work ghcr.io/opentofu/opentofu:1.8 init

tfenv 스타일 버전 관리

# tenv (OpenTofu / Terraform / Terragrunt 통합 버전 매니저)
brew install cosmtrek/tap/tenv
tenv tofu install 1.8.0
tenv tofu use 1.8.0

버전을 .opentofu-version 파일로 고정해 팀 전체 일관성을 유지하세요.

첫 번째 설정: AWS S3 + DynamoDB state

# versions.tf
terraform {
  required_version = ">= 1.6"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.70"
    }
  }

  backend "s3" {
    bucket         = "my-team-tfstate"
    key            = "prod/network/terraform.tfstate"
    region         = "ap-northeast-2"
    dynamodb_table = "tf-state-locks"
    encrypt        = true
  }
}

provider "aws" {
  region = "ap-northeast-2"
  default_tags {
    tags = {
      ManagedBy = "OpenTofu"
      Project   = "pkglog"
    }
  }
}
# main.tf
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.13"

  name = "prod-vpc"
  cidr = "10.0.0.0/16"

  azs             = ["ap-northeast-2a", "ap-northeast-2b", "ap-northeast-2c"]
  private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
  public_subnets  = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]

  enable_nat_gateway = true
  single_nat_gateway = false
}
tofu init        # 백엔드 초기화 + provider 다운로드
tofu fmt         # 코드 정리
tofu validate    # 문법 검증
tofu plan        # 변경 계획 검토
tofu apply       # 실제 적용

terraform-aws-modules 같은 Terraform Registry의 public 모듈도 그대로 사용 가능합니다. OpenTofu가 자동으로 registry를 routing합니다.

Terraform에서 OpenTofu로 마이그레이션

체크리스트

# 1) 현재 state 백업
terraform state pull > backup.tfstate
aws s3 cp backup.tfstate s3://my-backup-bucket/backup-$(date +%Y%m%d).tfstate

# 2) Terraform Cloud를 쓰고 있다면 `cloud {}` 블록 제거, 표준 백엔드로 이전
# 3) .terraform 디렉터리 삭제
rm -rf .terraform .terraform.lock.hcl

# 4) OpenTofu로 재초기화
tofu init

# 5) plan 결과가 "No changes" 인지 확인 (중요!)
tofu plan

tofu plan에서 drift(상태와 실제 리소스 불일치)가 나오면 실제 변경이 생기는지 꼼꼼히 확인합니다. 대부분은 provider 버전 차이로 인한 속성 정규화 차이이며 tofu apply -refresh-only로 해결됩니다.

호환되지 않는 부분

기능TerraformOpenTofu
cloud {} 블록 (Terraform Cloud 원격 실행)지원미지원 (표준 백엔드 사용)
terraform_remote_state with TFC지원미지원
Sentinel 정책지원미지원 (OPA·Conftest 대체)
terraform test 프레임워크v1.6+자체 구현, 세부 동작 차이

모듈 구조: 실무 표준

infrastructure/
├── modules/
│   ├── vpc/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── outputs.tf
│   ├── rds/
│   └── eks/
├── envs/
│   ├── dev/
│   │   ├── main.tf       # modules/* 조합
│   │   ├── backend.tf
│   │   └── terraform.tfvars
│   ├── staging/
│   └── prod/
└── .opentofu-version

각 환경 디렉터리는 독립된 state를 가지며 동일 모듈을 variables만 다르게 호출합니다. 이 구조는 blast radius를 제한하고 환경별 변경을 명확히 분리합니다.

모듈 사용 예제

# envs/prod/main.tf
module "vpc" {
  source = "../../modules/vpc"

  name = "prod"
  cidr = "10.0.0.0/16"
  az_count = 3
}

module "eks" {
  source = "../../modules/eks"

  cluster_name = "prod"
  vpc_id       = module.vpc.vpc_id
  subnet_ids   = module.vpc.private_subnets

  node_groups = {
    general = {
      instance_types = ["m6i.xlarge"]
      min_size       = 3
      max_size       = 10
      desired_size   = 5
    }
  }
}

원격 state 백엔드 전략

S3 + DynamoDB (AWS 표준)

backend "s3" {
  bucket         = "my-team-tfstate"
  key            = "prod/app/terraform.tfstate"
  region         = "ap-northeast-2"
  dynamodb_table = "tf-state-locks"
  encrypt        = true

  # OpenTofu 1.7+ 내장 암호화 (KMS)
  kms_key_id = "alias/tfstate-encryption"
}

S3 버킷은 별도 “관리 계정”에 두고 각 워크로드 계정은 IAM Role assume으로 접근하도록 하세요. DynamoDB 잠금은 필수입니다 — 팀원 둘이 동시에 apply하는 사고를 막습니다.

OpenTofu 1.7 내장 state 암호화

terraform {
  encryption {
    key_provider "aws_kms" "main" {
      kms_key_id = "alias/tfstate"
      region     = "ap-northeast-2"
      key_spec   = "AES_256"
    }

    method "aes_gcm" "main" {
      keys = key_provider.aws_kms.main
    }

    state {
      method = method.aes_gcm.main
    }

    plan {
      method = method.aes_gcm.main
    }
  }
}

tfstate에 포함된 비밀번호·키가 KMS로 추가 암호화됩니다. Terraform엔 없는 기능입니다.

CI/CD 파이프라인: GitHub Actions

name: OpenTofu CI

on:
  pull_request:
    paths: ['infrastructure/**']

jobs:
  plan:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write
      id-token: write  # OIDC for AWS

    steps:
      - uses: actions/checkout@v4

      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789:role/GithubActionsRole
          aws-region: ap-northeast-2

      - uses: opentofu/setup-opentofu@v1
        with:
          tofu_version: 1.8.0

      - name: Format check
        run: tofu fmt -check -recursive

      - name: Init
        working-directory: infrastructure/envs/prod
        run: tofu init -backend-config=backend.hcl

      - name: Validate
        working-directory: infrastructure/envs/prod
        run: tofu validate

      - name: Plan
        id: plan
        working-directory: infrastructure/envs/prod
        run: tofu plan -out=tfplan -no-color | tee plan.txt

      - name: Comment plan on PR
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            const plan = fs.readFileSync('infrastructure/envs/prod/plan.txt', 'utf8');
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: `### OpenTofu Plan\n\`\`\`\n${plan.slice(0, 60000)}\n\`\`\``
            });

PR에 자동으로 tofu plan 결과를 코멘트로 남깁니다. 리뷰어는 실제 변경 내역을 코드 diff와 함께 확인합니다.

Apply는 별도 잡으로

  apply:
    needs: plan
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    environment: production  # GitHub Environment 승인 게이트
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: opentofu/setup-opentofu@v1
      - run: tofu init -backend-config=backend.hcl
      - run: tofu apply -auto-approve

environment: production은 GitHub Environment의 필수 리뷰어 승인을 강제하는 트릭입니다. 수동 승인 없이는 apply가 진행되지 않습니다.

정책 검증: OPA + Conftest

Sentinel(유료) 대신 OPA(Open Policy Agent)의 conftest를 사용해 OpenTofu plan을 검증할 수 있습니다.

tofu plan -out=tfplan
tofu show -json tfplan > plan.json
conftest test plan.json --policy policy/
# policy/s3.rego
package main

deny[msg] {
  resource := input.resource_changes[_]
  resource.type == "aws_s3_bucket"
  resource.change.after.acl == "public-read"
  msg := sprintf("S3 bucket %s must not be public", [resource.address])
}

deny[msg] {
  resource := input.resource_changes[_]
  resource.type == "aws_instance"
  not resource.change.after.tags.Owner
  msg := sprintf("EC2 instance %s must have Owner tag", [resource.address])
}

공공 S3·보안그룹 0.0.0.0/0 개방·태그 누락 등을 CI에서 차단합니다.

시크릿 관리

tfvars에 비밀을 커밋하지 마세요. 다음 중 하나를 택합니다.

1. AWS SSM Parameter Store / Secrets Manager

data "aws_ssm_parameter" "db_password" {
  name = "/prod/db/password"
  with_decryption = true
}

resource "aws_db_instance" "main" {
  password = data.aws_ssm_parameter.db_password.value
}

2. 환경변수 + TF_VAR_*

export TF_VAR_db_password=$(aws secretsmanager get-secret-value \
  --secret-id prod/db --query SecretString --output text)
tofu apply

3. SOPS + age

# secrets.enc.yaml (암호화 상태로 커밋 가능)
db_password: ENC[AES256_GCM,data:...]
sops -d secrets.enc.yaml | tofu apply -var-file=-

실전: drift 감지 + 자동 경보

매일 새벽 tofu plan을 돌려 drift(수동 콘솔 변경)를 감지하고 Slack으로 알립니다.

on:
  schedule:
    - cron: '0 18 * * *'  # 매일 새벽 3시 KST

jobs:
  drift:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: opentofu/setup-opentofu@v1
      - run: tofu init
      - id: plan
        run: |
          if tofu plan -detailed-exitcode -no-color > plan.txt; then
            echo "drift=false" >> $GITHUB_OUTPUT
          elif [ $? -eq 2 ]; then
            echo "drift=true" >> $GITHUB_OUTPUT
          fi

      - if: steps.plan.outputs.drift == 'true'
        uses: slackapi/slack-github-action@v1
        with:
          payload: |
            {"text":"⚠️ Infra drift detected in prod","blocks":[...]}

문제 해결

Error: Failed to query available provider packages

  • .terraform.lock.hcl 삭제 후 tofu init -upgrade
  • registry 문제면 OPENTOFU_REGISTRY=registry.terraform.io 임시 지정

state 잠금 안 풀림

tofu force-unlock <LOCK_ID>

DynamoDB 콘솔에서 tf-state-locks 테이블의 해당 LockID 항목을 삭제해도 됩니다.

provider 버전 충돌

모듈마다 다른 버전을 요구할 때 required_providersversion 제약을 가장 넓은 교집합으로 조정하거나, 모듈을 쪼개 독립 workspace로 분리하세요.

plan에 “known after apply” 폭발

lifecycle { ignore_changes = [tags["CreatedAt"]] } 같은 자동 갱신 속성은 명시적으로 무시해 noise를 줄입니다.

체크리스트: 프로덕션 도입 전

  • tfstate 백엔드 S3 버전 관리·암호화·접근 제한 설정
  • DynamoDB 잠금 테이블 생성 (LockID 파티션 키)
  • 환경별 디렉터리 분리 (envs/{dev,staging,prod})
  • opentofu/setup-opentofu@v1 + OIDC 기반 CI
  • PR plan 자동 코멘트, apply는 승인 게이트
  • OPA/Conftest 보안 정책
  • drift 감지 스케줄 잡
  • 시크릿은 SSM/Secrets Manager/SOPS
  • tofu fmt -check -recursive pre-commit hook
  • runbook: state 손상·잠금 해제·롤백 절차 문서화

마무리

OpenTofu는 이미 Terraform의 단순 포크가 아닌 독립된 프로젝트입니다. provider iteration, 내장 state 암호화, -exclude 같은 기능은 실무에 직접적 가치를 줍니다. BSL 라이선스가 불편하거나 오픈소스 거버넌스를 중시한다면 지금이 전환하기 가장 좋은 시점입니다. 기존 Terraform 설정을 그대로 두고 tofu init 한 줄로 실험할 수 있으니, 스테이징 환경부터 PoC를 시작해보세요.

관련 글

  • Terraform 완벽 가이드
  • Docker 완벽 가이드
  • Kubernetes 완벽 가이드
  • GitHub Actions CI/CD 가이드