8주차 - OpenTofu

1. OpenTofu 소개

1.1 OpenTofu 가 생성된 이유는?

  • Hashicorp에서 Terraform의 라이센스를 MPL 에서 BUSL로 변경하면서 법적으로 애매모호한 포지션을 가지고 가게 됩니다.

  • 이에 대한 대응책으로, Linux Foundation에서 오픈소스 정책을 지원하기 위해서 Terraform(MPL)을 Fork하게 됩니다.

  • OpenTofu 1.6 버전대와 Terraform 1.6 버전대는 기능 적으로 매우 유사하고 앞으로는 기능 변경이 계속 진행될 예정입니다.

  • OpenToful는 Provider의 Terraform을 사용해서 동작하며 Registry만 다릅니다.

  • 가장 큰 차이점은 현재로써는 법적 리스크입니다.

1.2 OpenTofu 설치


# Tenv 소개 : OpenTofu / Terraform / Terragrunt and Atmos version manager
# (옵션) tfenv 제거
brew remove tfenv

# Tenv 설치
## brew install cosign # 설치 권장
brew install tenv
tenv -v
tenv -h
tenv tofu -h
which tenv

## Linux Ubuntu  
LATEST_VERSION=$(curl --silent https://api.github.com/repos/tofuutils/tenv/releases/latest | jq -r .tag_name)
curl -O -L "https://github.com/tofuutils/tenv/releases/latest/download/tenv_${LATEST_VERSION}_amd64.deb"
sudo dpkg -i "tenv_${LATEST_VERSION}_amd64.deb"

# (옵션) Install shell completion
tenv completion zsh > ~/.tenv.completion.zsh
echo "source '~/.tenv.completion.zsh'" >> ~/.zshrc

#
tenv tofu -h
tenv tofu list
tenv tofu list-remote

# 설치
tenv tofu install 1.7.3
tenv tofu list
tenv tofu use 1.7.3
tenv tofu detect

# tofu 확인
tofu -h
tofu version

## 기존에 사용한 tfenv 삭제후에 tenv 설치합니다.
install 완료

2. OpenTofu 실습

2.1 [실습] 프로바이더 정보를 통해서 사용자 정의 함수 사용

  • 테라폼과 다른점은 사용자 정의 함수를 프로바이더 정보를 통해서 바꿔 사용할 수 있습니다.

  • 아래의 실습에서는 northwood-labs의 corefunc를 사용합니다.

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

provider "corefunc" {
}

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

# 초기화
tofu init
# Plan
tofu plan
# Apply
tofu apply
terraform {
  required_providers {
    corefunc = {
      source = "northwood-labs/corefunc"
      version = "1.4.0"
    }
  }
}

provider "corefunc" {
}

output "test" {
  value = provider::corefunc::str_camel("Hello world!") // 여기에 str_camel 함수 사용
  # Prints: hello_world
}

2.2 [실습] Import blocks 사용하기

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]
  }
}

# 초기화
tofu init
                
# Apply
tofu apply -auto-approve

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


# 확인
tofu state list
tofu state ls
생성 및 실행 확인
  • 문제상황: tfstate 파일이 삭제가 되고 다시 import 시키는 상황을 재연해보자

  • instance-ids를 보면 import id 가 instanceIds 인것을 알 수 있다.

# 문제 상황 재연 : tfstate 파일 삭제
rm -rf .terraform* terraform.tfstate*


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-0e2d4475790337a81", "i-00a4daebb71942280"]
}

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
}
# 초기화
tofu init -json
tree .terraform

#  
tofu apply -auto-approve

2.3 [실습] Statefile Encryption 하기

  • statefile을 opentofu에서 `terraform_remote_state` 키워드를 통해서 암호화할 수 있습니다.


terraform {
  encryption {
    ## Step 1: Add the unencrypted method:
    method "unencrypted" "migrate" {}

    ## Step 2: Add the desired key provider:
    key_provider "pbkdf2" "my_passphrase" {
      # Change this to be at least 16 characters long:
      passphrase = "dsdfasldkfmasdfsadff3!"
    }

    ## Step 3: Add the desired encryption method:
    method "aes_gcm" "new_method" {
      keys = key_provider.pbkdf2.my_passphrase
    }

    state {
      ## Step 4: Link the desired encryption method:
      method = method.aes_gcm.new_method

      ## Step 5: Add the "fallback" block referencing the
      ## "unencrypted" method.
      fallback {
        method = method.unencrypted.migrate
      }

      ## Step 6: Run "tofu apply".

      ## Step 7: Remove the "fallback" block above and
      ## consider adding the "enforced" option:
      # enforced = true
    }

    ## Step 8: Repeat steps 4-8 for plan{} if needed.
  }
}

// 다음과 같이 state file 이 encryption 정책을 따르는지 실행할 수 있습니다.

terraform {
    encryption {
        state {
            enforced = true
        }
        plan {
            enforced = true
        }
    }
}

  • 암호화된 파일을 다시 평문으로 마이그레이션 할 수 있습니다.


  terraform {
      encryption {
        key_provider "pbkdf2" "my_passphrase" {
          ## Enter a passphrase here:
          passphrase = "dsdfasldkfmasdfsadff3!"
        }
    
        method "aes_gcm" "my_method" {
          keys = key_provider.pbkdf2.my_passphrase
        }
    
        ## Remove this after the migration:
        method "unencrypted" "migration" {
        }
    
        state {
          method = method.unencrypted.migration
    
          ## Remove the fallback block after migration:
          fallback{
            method = method.aes_gcm.my_method
          }
          # Enable this after migration:
          enforced = false
        }
      }
    }

2.4 [실습] Statefile AWS KMS 를 이용해 Encryption 하기

#
aws s3 mb s3://<각자 유일한 S3 버킷명 이름> --region ap-northeast-2
aws s3 mb s3://gasida-t101 --region ap-northeast-2

# 확인
aws s3 ls

// aws 의 KMS 키를 생성합니다.

# 키 생성(기본값)
# aws kms create-key --description "my text encrypt descript demo"
CREATE_KEY_JSON=$(aws kms create-key --description "my text encrypt descript demo")
echo $CREATE_KEY_JSON | jq

# 키 ID확인
KEY_ID=$(echo $CREATE_KEY_JSON | jq -r ".KeyMetadata.KeyId")
echo $KEY_ID

# 키 alias 생성
export ALIAS_SUFFIX=<각자 닉네임>
export ALIAS_SUFFIX=gasida
aws kms create-alias --alias-name alias/$ALIAS_SUFFIX --target-key-id $KEY_ID

# 생성한 별칭 확인 : 키 ID 메모하두기, 아래 테라폼 코드에서 사용
aws kms list-aliases
aws kms list-aliases --query "Aliases[?AliasName=='alias/<각자 닉네임>'].TargetKeyId" --output text
aws kms list-aliases --query "Aliases[?AliasName=='alias/gasida'].TargetKeyId" --output text
c0bfc529-1ede-44f3-a7e5-ae60814c1ca1

# CMK로 평문을 암호화해보기
echo "Hello 123123" > secrect.txt
aws kms encrypt --key-id alias/$ALIAS_SUFFIX --cli-binary-format raw-in-base64-out --plaintext file://secrect.txt --output text --query CiphertextBlob | base64 --decode > secrect.txt.encrypted

# 암호문 확인
cat secrect.txt.encrypted

# 복호화해보기
aws kms decrypt --ciphertext-blob fileb://secrect.txt.encrypted --output text --query Plaintext | base64 --decode
Hello 123123

# 로컬 tfstate 파일 제거
rm -rf .terraform* terraform.tfstate*
tree

#
tofu init
Initializing the backend...

Successfully configured the backend "s3"! OpenTofu will automatically
use this backend unless the backend configuration changes.
...

#
tree .terraform 
.terraform
├── providers
│   └── registry.opentofu.org
│       └── hashicorp
│           └── aws
│               └── 5.60.0
│                   └── darwin_arm64 -> /Users/gasida/.terraform.d/plugin-cache/registry.opentofu.org/hashicorp/aws/5.60.0/darwin_arm64
└── terraform.tfstate

# 
cat .terraform/terraform.tfstate | jq

# import 실행
tofu apply -auto-approve
tofu state list
tofu show
ls -l terraform.tfstate*

# 원격 백엔드에 저장된 tfstate 파일 확인 및 로컬에 다운로드
aws s3 ls s3://<각자 자신의 S3 버킷명> --recursive
aws s3 cp s3://<각자 자신의 S3 버킷명>/terraform.tfstate .

aws s3 ls s3://gasida-t101 --recursive
aws s3 cp s3://gasida-t101/terraform.tfstate .

# 다운받은 tfstate 파일 확인 : VSCODE에서 열어보기
cat terraform.tfstate | jq
  • 다음과 같이 kms 키로 encrypted 된 값이 s3에 저장되는 것을 확인할 수 있습니다.

2.5 [실습] Removed Block 하기

  • OpenTofu에서는 removed_block을 사용해 tfstate으로 관리하고 싶지 않은 리소스들을 제외 할 수 있습니다.

#
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-0e2d4475790337a81", "i-00a4daebb71942280"] # 각자 자신의 EC2 ID를 기입 할 것
}

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
}

#
tofu init
tree .terraform

# 2개 리소스는 import , 2개 리소스는 생성
tofu apply -auto-approve
Plan: 2 to import, 2 to add, 0 to change, 0 to destroy.
aws_instance.this[0]: Importing... [id=i-0e2d4475790337a81]
aws_instance.this[0]: Import complete [id=i-0e2d4475790337a81]
aws_instance.this[1]: Importing... [id=i-00a4daebb71942280]
aws_instance.this[1]: Import complete [id=i-00a4daebb71942280]
aws_ssm_parameter.this[1]: Creating...
aws_ssm_parameter.this[0]: Creating...
aws_ssm_parameter.this[1]: Creation complete after 0s [id=app]
aws_ssm_parameter.this[0]: Creation complete after 0s [id=web]

#
tofu state ls
tofu show
tofu state show 'aws_ssm_parameter.this[0]'
tofu state show 'aws_ssm_parameter.this[1]'

# tfstate 파일 확인 : VSCODE에서 열어보기
cat terraform.tfstate | jq
...
      "type": "aws_ssm_parameter",
      "name": "this",
      "provider": "provider[\"registry.opentofu.org/hashicorp/aws\"]",
      "instances": [
        {
          "index_key": 0,
          "schema_version": 0,
          "attributes": {
            "allowed_pattern": "",
            "arn": "arn:aws:ssm:ap-northeast-2:911283464785:parameter/web",
            "data_type": "text",
            "description": "",
            "id": "web",
            "insecure_value": null,
            "key_id": "",
            "name": "web",
            "overwrite": null,
            "tags": null,
            "tags_all": {},
            "tier": "Standard",
            "type": "String",
            "value": "i-0e2d4475790337a81",
...

# parameters 정보 확인
aws ssm describe-parameters | jq
aws ssm get-parameter --name "web"
aws ssm get-parameter --name "web" --query "Parameter.Value" --output text
aws ssm get-parameter --name "app"
aws ssm get-parameter --name "app" --query "Parameter.Value" --output text

파라미터 스토어 리소스만 tfstate 에서 제거하고, AWS 상에는 유지 하게 설정

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-0e2d4475790337a81", "i-00a4daebb71942280"]
}

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
# }

removed {
  from = aws_ssm_parameter.this
}
  • 다음과 같이 removed 를 설정해주면, 해당 리소스는 삭제가 되지 않습니다.

#
tofu apply -auto-approve

2.6 [실습] 테라폼에서 OpenTofu 로마이그레이션 하기

  • 손쉽게 terraform에서 opentofu로 마이그레이션이 가능합니다.

  • 일단 terraform으로 리소스를 생성합니다.

#
tenv tf -h
tenv tf list
tenv tf list-remote

# 설치
tenv tf install 1.8.5
tenv tf list
tenv tf use 1.8.5
tenv tf detect

#
terraform -version
Terraform v1.8.5
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]
  }
}
  • 해당 파일을 백업해주고 마이그레이션을 시작합니다.

cp terraform.tfstate terraform.backupfile
tofu init
  • 다음과 같이 opentofu로 정상적으로 생성된것을 확인할 수있습니다.

마치면서

  • 이 스터디를 진행하면서 terraform에 대한 구성원리를 이해하였고 실무에서 직접 사용할 수있는 꿀팁을 많이 얻어가게 됩니다.

  • Devops를 실제로 서비스를 하면서 많이 진행하면서 자동화가 될 부분이 많은데 배운 실무팁을 이용해 무자비한 자동화를 해 나갈 예정입니다.

    • Terraform을 이용해서 서비스를 자동 배포

    • statefile에 대한 encryption 처리

Last updated