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 설치합니다.
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