본문 바로가기
스터디 이야기/Terraform

[T101] Terraform - OpenTofu

by lakescript 2024. 8. 1.

 

더보기

이 스터디는 CloudNet@에서 진행하는 T101 스터디를 참여하면서 공부하는 내용을 기록하는 블로그 포스팅입니다.

CloudNet@에서 제공해주는 자료들과 테라폼으로 시작하는 IaC 를 바탕으로 작성되었습니다.

OpenTofu

OpenTofu란 Terraform이 라이센스를 비즈니스 레벨로 변경함에 따라 Terraform의 포크(fork) 버전입니다. 이전의 이름은 OpenTF였지만, OpenTofu로 이름을 변경하였고, 오픈 소스, 커뮤니티 중심, Linux Foundation에서 관리합니다.

 

또한, OpenTofu는 클라우드와 온프레미스 리소스를 모두 사람이 읽을 수 있는 구성 파일에 정의하여 버전 관리, 재사용 및 공유할 수 있는 코드 도구로서의 인프라입니다. 더하여 워크플로를 통해 라이프사이클 전체에 걸쳐 모든 인프라를 프로비저닝하고 관리할 수 있습니다.

 

등장배경

https://news.hada.io/topic?id=10372

 

OpenTF 선언문 | GeekNews

OpenTF 선언문은 HashiCorp가 Terraform의 라이선스를 Mozilla Public License (MPL)에서 비오픈 소스 라이선스인 Business Source License (BUSL)로 변경한 결정에 대한 반응이 변경은 2023년 8월 10일에 커뮤니티로부터

news.hada.io

 

위의 GeekNews 내용을 토대로 OpenTofu의 등장배경에 대해 나열해보겠습니다.

‘23.08

HashiCorp가 Terraform 라이선스를 Mozilla Public License (MPL)에서 비오픈 소스 라이선스인 Business Source License (BUSL)로 변경하였습니다.

 

 

하지만 이 변경은 2023년 8월 10일에 커뮤니티로부터의 충분한 사전 고지나 논의없이 이루어졌습니다. BUSL 라이선스는 Terraform에 대한 "독약"이며, 이 소프트웨어를 사용하는 기업과 개발자에게 법적 위험을 초래한다고 주장하였고 라이선스 변경이 개발자와 기업들이 진정한 오픈 소스 대안을 선택하게 만들어 Terraform 생태계가 줄어들고 쇠퇴하게 될 것이라고 예상했습니다.

‘23.09

하지만 Hashicorp측에서 어떠한 액션도 취하지 않아 OpenTF 저장소가 공개되었고, 테라폼의 포크버전 OpenTFOpenTofu로 이름을 변경합니다.

'20.04

IBM이 HashiCorp를 64억 달러에 인수합니다.

'24.05

Oracle에서 기업용 제품에서 사용중인 TerraformOpenTofu교체합니다. 즉, Oracle E-Business Suite (EBS) Cloud Manager의 최신 버전에서 Terraform 대신 오픈소스 포크인 OpenTofu를 사용합니다.

 

동작 방식

https://www.linkedin.com/pulse/opentofu-terraform-under-hood-graph-management-shailender-singh-wen7c/

 

OpenTofu는 API를 통해 리소스를 생성하고 관리합니다. Provider는 OpenTofu가 접근 가능한 API를 통해 사실상 모든 플랫폼이나 서비스와 함께 작업할 수 있도록 합니다. 현재 OpenTofu 커뮤니티는 이미 다양한 유형의 리소스와 서비스를 관리하기 위해 수천 개의 공급자를 연결했습니다. Amazon Web Services(AWS), Azure, Google Cloud Platform(GCP), Kubernetes, Helm, GitHub, Splunk, DataDog 등을 포함하여 Public OpenTofu Registry 에서 공개적으로 사용 가능한 모든 provide를 사용할 수 있습니다.

 

핵심 workflow

OpenTofu의 핵심 워크플로는 세 단계로 구성됩니다.

Write(쓰기)

리소스를 정의합니다. 

Plan (계획)

OpenTofu는 기존 인프라와 구성에 따라 생성, 업데이트 또는 파괴할 인프라를 설명하는 실행 계획을 만듭니다.

Apply (적용)

승인 시 OpenTofu는 모든 리소스 종속성을 고려하여 정의한 순서대로 적용합니다.

 

OpenTofu와 Terraform의 차이점은 무엇인가요?

기술적인 측면에서 OpenTofu 1.6.x는 Terraform 1.6.x와 기능적으로 매우 유사하지만 앞으로는 프로젝트 기능 세트가 나눠질 것 입니다. 또 다른 주요 차이점은 OpenTofu는 오픈 소스라는 점이며, 하나의 회사가 로드맵을 지시할 수 없는 협력적인 방식으로 구성됩니다.

Terraform의 드롭인 대체품으로 OpenTofu를 사용할 수 있나요? OpenTofu는 프로덕션 사용에 적합할까요?

OpenTofu는 Terraform 버전 1.5.x 및 대부분 1.6.x와 호환되므로 Terraform의 대체 도구입니다. 호환성을 보장하기 위해 코드를 변경할 필요가 없습니다.

OpenTofu가 기존 상태 파일(terraform.tfstate)과 호환되나요?

OpenTofu는 Terraform 버전 1.5.x로 생성된 파일까지 기존 상태 파일을 지원합니다.

OpenTofu는 Terraform이 협력하는 모든 공급업체와 호환되나요?

OpenTofu는 자체 공급자가 없습니다. Terraform 공급자는 라이선스를 변경하지 않았으며, 변경 가능성은 사실상 0에 가깝습니다. 또한, OpenTofu는 현재 Terraform 공급자와 함께 작동하지만 별도의 레지스트리를 사용합니다.

 

 

opentofu 실습

tenv 설치

tenv란?

 

공식홈페이지를 참고하여 OpenTofu, Terraform, Terragrunt 및 Atmos의 버전관리를 해주는 tenv를 먼저 설치합니다.

brew install tenv

설치 확인

tenv --version

 

자동완성 설정

원활한 실습을 위해 자동완성 기능을 zsh에 설정해줍니다.

tenv completion zsh > ~/.tenv.completion.zsh
echo "source '~/.tenv.completion.zsh'" >> ~/.zshrc

 

OpenTofu 설치

OpenTofu 확인

tenv tofu list

tenv로 Opentofu를 버전관리하기 때문에 먼저 OpenTofu 리스트를 확인합니다.

현재 어떠한 OpenTofu 버전이 설치되어있지 않기 때문에 아무것도 보이지 않습니다.

tenv tofu list-remote

현재 설치가능한 OpenTofu 버전 목록을 확인하면 아래와 같습니다.

OpenTofu 설치

tenv tofu install 1.7.3

 

1.7.3 버전을 다운로드 합니다.

OpenTofu 버전 설정

tenv tofu use 1.7.3

해당 버전을 사용합니다.

OpenTofu 정보 확인

tenv tofu detect

현재 사용중인 OpenTofu의 정보를 확인하려면 위의 명령어를 입력합니다.

 

 

Terraform VS OpenTofu

OpenTofu는 Terraform의 fork 버전이기 때문에 기능이 상당히 유사합니다. 하지만 OpenTofu에서만 지원되는 기능이 몇가지 있어서 실습을 통해 살펴보겠습니다.

 

Provider-defined functions

OpenTofu에서는 Terraform에는 없는 Provider-defined Functions을 사용할 수 있습니다. provider 블록안에 함수를 지정해서 사용할 수 있습니다. 이 기능은 데이터 소스를 사용해도 tf.statefile의 크기가 증가하지 않고 코드 작성이 덜 필요하기 때문에 데이터 소스를 사용하는 것에 비해 크게 개선된 것입니다.

main.tf

terraform {
  required_providers {
    corefunc = {
      source = "northwood-labs/corefunc"
      version = "1.4.0"
    }
  }
}

provider "corefunc" {
}

output "test" {
  value = provider::corefunc::str_snake("Hello world!")
}

여기서 사용하는 corefunc의  str_snake는 공식문서에서 확인하실 수 있는데, 문자열을 camelCase로 변환하여 영숫자가 아닌 문자를 제거하는 함수입니다. 그렇다면 위에서 값을 넣어준 Hello workd!라는 값은 str_snake에 의해 Hello_world로 output으로 나와야 합니다.

 

tofu init

tofu init

 

Terraform과 마찬가지로 초기화 작업을 진행합니다. (terraform init과 같습니다.)

 

 

이때, tree 명령어로 providers의 구조를 살펴보면 registry.opentofu.org에서 다운로드된것을 확인하실 수 있습니다. (terraform registry가 아님!)

 

tofu plan

tofu plan

이것 역시 terraform과 마찬가지로 plan을 하여 적용하기 전 검사를 합니다.

보시는 바와 같이 알파벳이 아닌 문자는 _로 표시되었습니다.

Loopable import blocks

aws의 리소스를 Terraform 혹은 OpenTofu로 관리하기 위해 terraform state(tfstate)파일을 생성해야 합니다. 하지만 현재 내 local에는 tfstate 파일이 없을 때 remote 공간에서 관리되는 tfstate 파일을 로컬로 가져와 관리하기 위해 import 기능을 제공합니다.

OpenTufo에서는 이러한 기본 기능에서 문제가 되었던 import할 resource가 여러 개일때 복잡하기에 Importing multiple resources 기능을 제공합니다. 이때 for_each문을 사용해서 여러 리소스를 가져올 수 있고, set, tuple 또는 map 형식으로 가져오며 개별 요소를 가져오기 위해 each.key, each.value 변수를 사용합니다.

main.tf

data "aws_ami" "ubuntu" {
    most_recent = true

    filter {
        name   = "name"
        values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
    }

    filter {
        name   = "virtualization-type"
        values = ["hvm"]
    }

    owners = ["099720109477"] # Canonical
}

variable "instance_tags" {
  type = list(string)
  default = ["web", "app"]
}

resource "aws_instance" "this" {
  count = length(var.instance_tags)
  ami                    = data.aws_ami.ubuntu.id
  instance_type          = "t3.micro"

  tags = {
    Name = var.instance_tags[count.index]
  }
}

 

코드를 살펴보면, aws_ami data 블록을 통해 가장 최근의 ubuntu image를 가져옵니다. web과 app이라는 담고 있는 변수인 instance_tags를 선언합니다. 마지막으로 aws_instance resource에 var.instace_tags의 길이만큼 count를 주어 2번 실행하게 설정하고 각각 web과 app이라는 Tag를 설정합니다.

 

tofu init

tofu init

tofu init을 통해 초기화를 진행합니다.

 

그 후 받아온 provider인 aws의 정보를 살펴보면 위와 같습니다.

 

tofu apply

tofu apply -auto-approve

tofu apply를 통해 배포를 진행합니다.

배포된 EC2 확인

while true; do aws ec2 describe-instances --query "Reservations[*].Instances[*].{PublicIPAdd:PublicIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value,Status:State.Name}" --filters Name=instance-state-name,Values=running --output text ; echo "------------------------------" ; sleep 1; done

 

위 명령어를 통해 ec2 목록을 확인해보겠습니다.

2개의 EC2가 생성되어 실행중인 것을 확인하실 수 있습니다.

state list 확인

tofu state list

 

 

 

문제 발생 시키기

임의로 tfstate 파일을 삭제하여 문제 상황을 만들어보겠습니다.

rm -rf .terraform* terraform.tfstate*

 

.terraform 파일과 terraform.state 파일을 전부 삭제합니다.

 

그 후 tree 명령어로 현재 main.tf 파일만 존재하는 것을 확인합니다.

 

EC2 확인

이제 local에서 배포했었던 EC2를 입력하고 import를 진행해보겠습니다.

aws ec2 describe-instances --query 'Reservations[*].Instances[*].{InstanceID:InstanceId,PublicIP:PublicIpAddress,Name:Tags[?Key==`Name`]|[0].Value}' --output json | jq -r '.[][] | "\(.InstanceID)\t\(.PublicIP)\t\(.Name)"'

 

명령어를 통해서 instance ID 값을 확인합니다.

 

main.tf 수정

data "aws_ami" "ubuntu" {
    most_recent = true

    filter {
        name   = "name"
        values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
    }

    filter {
        name   = "virtualization-type"
        values = ["hvm"]
    }

    owners = ["099720109477"] # Canonical
}

variable "instance_ids" {
  type = list(string)
  default = ["i-056fd5732a24d67fc", "i-0c55ee79975a36f58"]
}

variable "instance_tags" {
  type = list(string)
  default = ["web", "app"]
}

resource "aws_instance" "this" {
  count = length(var.instance_tags)
  ami                    = data.aws_ami.ubuntu.id
  instance_type          = "t3.micro"

  tags = {
    Name = var.instance_tags[count.index]
  }
}

import {
  for_each = { for idx, item in var.instance_ids : idx => item }
  to = aws_instance.this[tonumber(each.key)]
  id = each.value
}

 

main.tf 에서 variable.instance_ids에 위에서 확인했던 EC2 ID를 입력합니다. 위에서 로컬의 tfstate 파일을 삭제 했기 때문에 현재 내 local에는 EC2에 대한 상태 정보가 없습니다. 그렇기에 하단에 import하여 현재 배포되어있는 EC2들을 tfstate로 가져오는 블록을 추가합니다. 

import에 for_each를 지원한다는 것을 유심히 보셔야합니다. for_each가 지원되지 않았으면 각각 리소스를 import해줘야 해서 아래와 같이 2개의 import 문을 작성했어야 했습니다.

 

...

import {
  to = aws_instance.this[0]
  id = "i-056fd5732a24d67fc"
}
import {
  to = aws_instance.this[1]
  id = "i-0c55ee79975a36f58"
}

 

 

tofu init

tofu init -json

 tofu init에 json 형태의 출력을 지원하기 때문에 해당 옵션을 붙여 명령어를 실행합니다.

tofu apply

tofu apply --auto-approve

tofu apply를 진행합니다.

그럼 위와 같이 import되었다는 메시지가 호출되는 것을 확인하실 수 있습니다.

 

State file encryption

Terraform에서는 state file이 암호화되어 저장되지 않아 다른 third party의 도움을 받아야했는데, OpenTofu는 State and Plan Encryption를 통해 로컬 스토리지와 백엔드를 사용할 때 파일을 암호화하는 것을 지원합니다. 또한 terraform_remote_state 데이터 소스와 함께 암호화를 사용할 수도 있습니다. 

예시

terraform {
  encryption {
    key_provider "some_key_provider" "some_name" {
      # 여기에 Key provider options을 입력합니다.
    }

    method "some_method" "some_method_name" {
      # 여기에 Method options을 입력합니다.
      keys = key_provider.some_key_provider.some_name
    }

    state {
      # Encryption/decryption할 state data를 입력합니다.
      method = method.some_method.some_method_name
    }

    plan {
      # Encryption/decryption할 plan data를 입력합니다.
      method = method.some_method.some_method_name
    }

    remote_state_data_sources {
      # See below
    }
  }
}

encryption 블록안에 암호화할 data의 정보를 넣어주면 됩니다. 하지만 신규 암호화 설정 변경 설정 후 실행 시 문제 발생 시, 자동으로 이전 암호화 설정으로 전환 적용(fallback)하는 key and method rollove 구문도 제공합니다.

 

terraform {
  encryption {
    # Methods와 key providers를 입력합니다.

    state {
      method = method.some_method.new_method
      fallback {
        method = method.some_method.old_method
      }
    }

    plan {
      method = method.some_method.new_method
      fallback {
        method = method.some_method.old_method
      }
    }
  }
}

 

암호화된 내용을 평문으로 마이그레이션하는 Rolling back encryption 구문도 제공합니다. 이때 unencrypted를 사용하여 암호화되지 않은 상태 및 계획 파일로 마이그레이션합니다.

terraform {
  encryption {
    ## Step 1: 원래 암호화 방식을 그대로 유지
    method "some_method" "old_method" {
      ## Parameters for the old method here.
    }

    # Step 2: 암호화되지 않은 메서드를 추가
    method "unencrypted" "migrate" {}

    state {
      ## Step 3: "enforced" 옵션 사용 안 함 또는 제거
      enforced = false

      ## Step 4: 원래 암호화 방법을 "fallback" 블록으로 이동
      fallback {
        method = method.some_method.old_method
      }

      ## Step 5: 암호화되지 않은 방법을 기본 "암호화" 방법으로 참조
      method = method.unencrypted.migrate
    }

    ## Step 6: "tofu apply" 실행

    ## Step 7: 마이그레이션이 완료되면 "상태" 블록을 제거

    ## Step 8: 필요한 경우 plan{}에 대해 3-7단계를 반복
  }
}

 

main.tf 생성

아까 위에서 실습했던 main.tf 파일을 그대로 가져옵니다.

data "aws_ami" "ubuntu" {
    most_recent = true

    filter {
        name   = "name"
        values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
    }

    filter {
        name   = "virtualization-type"
        values = ["hvm"]
    }

    owners = ["099720109477"] # Canonical
}

variable "instance_ids" {
  type = list(string)
  default = ["i-056fd5732a24d67fc", "i-0c55ee79975a36f58"]
}

variable "instance_tags" {
  type = list(string)
  default = ["web", "app"]
}

resource "aws_instance" "this" {
  count = length(var.instance_tags)
  ami                    = data.aws_ami.ubuntu.id
  instance_type          = "t3.micro"

  tags = {
    Name = var.instance_tags[count.index]
  }
}

import {
  for_each = { for idx, item in var.instance_ids : idx => item }
  to = aws_instance.this[tonumber(each.key)]
  id = each.value
}

 

 

backend.tf

terraform {
  encryption {
    key_provider "pbkdf2" "my_passphrase" {
      ## Enter a passphrase here:
      passphrase = "ChangeIt_123abcd"
    }

    method "aes_gcm" "my_method" {
      keys = key_provider.pbkdf2.my_passphrase
    }

    ## Remove this after the migration:
    method "unencrypted" "migration" {
    }

    state {
      method = method.aes_gcm.my_method

      ## Remove the fallback block after migration:
      fallback{
        method = method.unencrypted.migration
      }
      ## Enable this after migration:
      #enforced = true
    }
  }
}

encryption의 key_provider.pbkdf2.my_passphrase내용을 살펴보겠습니다. passphrase은 일종의 암호키라고 생각하면 됩니다. 이 Key를 이용해서 암호화를 진행합니다. 그 후 아래의 state 내용을 보면 위에서 선언한 method.aes_gcm.my_method의 방법으로 tfstate 파일의 암호화를 진행한다고 보시면 됩니다. fallback을 통해 암호화가 실패했을 시 이전의 암호화 방식으로 되돌린다는 것을 선언합니다.

 

tofu init

tofu init

마찬가지로 실행하기 앞서 초기화를 진행합니다.

tofu apply

tofu apply

 

tfstate 파일 확인

cat terraform.tfstate | jq

apply 실행 후 tfstate 파일을 살펴보면 아래와 같이 암호화가 진행된 것을 확인하실 수 있습니다.

 

 Removed block

Infrastructure에는 자원을 냅두고 더 이상 iaC로 관리하고 싶지 않을 때 tfstate 파일에서 해당 리소스를 삭제하기 위해 해당 기능을 사용합니다.

 

main.tf

data "aws_ami" "ubuntu" {
    most_recent = true

    filter {
        name   = "name"
        values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
    }

    filter {
        name   = "virtualization-type"
        values = ["hvm"]
    }

    owners = ["099720109477"] # Canonical
}

variable "instance_ids" {
  type = list(string)
  default = ["i-056fd5732a24d67fc", "i-0c55ee79975a36f58"]
}

variable "instance_tags" {
  type = list(string)
  default = ["web", "app"]
}

resource "aws_instance" "this" {
  count = length(var.instance_tags)
  ami                    = data.aws_ami.ubuntu.id
  instance_type          = "t3.micro"

  tags = {
    Name = var.instance_tags[count.index]
  }
}

import {
  for_each = { for idx, item in var.instance_ids : idx => item }
  to = aws_instance.this[tonumber(each.key)]
  id = each.value
}

resource "aws_ssm_parameter" "this" {
  count = length(var.instance_tags)
  name  = var.instance_tags[count.index]
  type  = "String"
  value = aws_instance.this[count.index].id
}

 

이번 실습에도 마찬가지로 위에서 사용했었던 main.tf를 가져옵니다. 하지만 밑에 resouce 블록을 하나 추가합니다.

 

tofu init

tofu init

 

 

tofu apply

tofu apply

아까 위에서 import한 리소스 제외하고 새롭게 resouce를 추가하여 2개의 resourc가 생성되었습니다.

 

 

terraform state 확인

cat terraform.tfstate | jq

aws_ssm_parameter resource가 tfstate 파일에 생성된 것을 확인하실 수 있습니다.

parameters 정보 확인

aws ssm describe-parameters | jq

 

main.tf 파일 수정

...

import {
  for_each = { for idx, item in var.instance_ids : idx => item }
  to = aws_instance.this[tonumber(each.key)]
  id = each.value
}

# resource "aws_ssm_parameter" "this" {
#   count = length(var.instance_tags)
#   name  = var.instance_tags[count.index]
#   type  = "String"
#   value = aws_instance.this[count.index].id
# }

removed {
  from = aws_ssm_parameter.this
}

앞서 선언한 aws_ssm_parameter resoruce 블록을 주석처리하고 removed 블록을 통해 tfstate의 aws_ssm_parameter을 제거합니다.

 

tofu apply

tofu apply -auto-approve

 

출력되는 메시지를 살펴보면 aws_ssm_parameter.this[0],aws_ssm_parameter.this[1]을 state 파일에서 제거했지만 실제 대상에서는 삭제되지 않았음을 알려줍니다.

parameters 정보 확인

aws ssm describe-parameters | jq

ssm parameter 정보를 출력해보면 2개가 배포되어있는 상태입니다.

tfstate 파일 확인

cat terraform.tfstate | jq

하지만 tfstate 파일을 확인해보면 aws_ssm_parameter 리소스가 존재하지 않는 것을 확인하실 수 있습니다.