DDetB.Log
article thumbnail
Published 2023. 7. 9. 15:05
Terraform 기본 사용 (2) DevOps/Terraform
이 포스팅은 CloudNet@ 팀이 진행하는 테라폼 기초 입문 스터디에 참여하며 ‘테라폼으로 시작하는 IaC’ 책을 기준하여 정리한 글입니다.

Blocks

Terraform Block

테라폼의 구성을 명시하는데 사용됩니다. 팀 단위 협업 시 테라폼 버전 및 프로바이더 버전을 명시적으로 선언하여 실행 오류를 최소화할 수 있습니다.

Terraform block example

terraform {
  required_version = "~> 1.3.0" # 테라폼 버전

  required_providers { # 프로바이더 버전을 나열
    random = {
      version = ">= 3.0.0, < 3.1.0"
    }
    aws = {
      version = "4.2.0"
    }
  }

  cloud { # Cloud/Enterprise 같은 원격 실행을 위한 정보
    organization = "<MY_ORG_NAME>"
    workspaces {
      name = "my-first-workspace"
    }
  }

  backend "local" { # state를 보관하는 위치를 지정
    path = "relative/path/to/terraform.tfstate"
  }
}

e.g.) 선언 방법에 따른 각 의미

선언한 버전 의미
1.0.0
테라폼 v1.0.0만을 허용한다
>=1.0.0
테라폼 v1.0.0 이상의 모든 버전을 허용한다
~>1.0.0
테라폼 v1.0.0을 포함한 v1.0.x 버전을 하용하고 v1.x는 허용하지 않는다
>= 1.0, < 2.0.0
테라폼 v1.0.0 이상 v2.0.0 미만인 버전을 허용한다

Test

현재 버전 정보 확인

$ terraform version
Terraform v1.5.2
on linux_amd64
+ provider registry.terraform.io/hashicorp/local v2.4.0

main.tf

terraform {
  required_version = "< 1.0.0"
}

resource "local_file" "abc" {
  content  = "abc!"
  filename = "${path.module}/abc.txt"
}

실행

$ terraform init
Initializing the backend...
╷
│ Error: Unsupported Terraform Core version
│ 
│   on main.tf line 2, in terraform:
│    2:   required_version = "< 1.0.0"
│ 
│ This configuration does not support Terraform version 1.5.2. To proceed, either choose another supported Terraform version or update this
│ version constraint. Version constraints are normally set for good reason, so updating the constraint may lead to other errors or unexpected
│ behavior.
╵

$ terraform plan
╷
│ Error: Unsupported Terraform Core version
│ 
│   on main.tf line 2, in terraform:
│    2:   required_version = "< 1.0.0"
│ 
│ This configuration does not support Terraform version 1.5.2. To proceed, either choose another supported Terraform version or update this
│ version constraint. Version constraints are normally set for good reason, so updating the constraint may lead to other errors or unexpected
│ behavior.
╵

=> 에러 발생

Backend Block

테라폼은 State(상태 파일)의 데이터를 사용해 코드로 관리된 리소스를 탐색하고 추적하는데, 백엔드 블록은 테라폼 실행 시 저장되는 State저장 위치를 선언합니다. 작업자 간의 협업을 고려한다면 상태 파일을 공유할 수 있도록  외부 백엔드 저장소가 필요합니다.

하나의 백엔드만 허용됨
외부로 노출되면 안 되는 패스워드 또는 인증서 정보 같은 민감한 데이터들이 포함될 수 있으므로 State의 접근 제어가 필요

테라폼이 실행되는 동안 .terraform.tfstate.lock.info 파일이 생성되면서 해당 State를 동시에 사용하지 못하도록 잠금 처리를 합니다.(terraform apply 실행 후 yes/no 입력 대기중인 상태에서 확인 가능)

.terraform.tfstate.lock.info

"ID":"53daf912-3de3-c0f1-7238-942a99be74cd"
"Operation":"OperationTypeApply"		# 어떤 동작으로 인해 해당 잠금 파일이 생성되었는지 명시
"Info":""
"Who":"aiden@ubuntu"				# 작업자 정보
"Version":"1.5.2"				# 실행한 테라폼 버전
"Created":"2023-07-09T03:32:38.951580806Z"
"Path":"terraform.tfstate"			# 잠긴 state 파일의 위치

Test

백엔드 설정 변경 테스트

main.tf

terraform {
  backend "local" {
    path = "state/terraform.tfstate"
  }
}

resource "local_file" "abc" {
  content  = "123456!"
  filename = "${path.module}/abc.txt"
}

실행

$ terraform init
Initializing the backend...
Do you want to copy existing state to the new backend?
  Pre-existing state was found while migrating the previous "local" backend to the
  newly configured "local" backend. No existing state was found in the newly
  configured "local" backend. Do you want to copy this state to the new "local"
  backend? Enter "yes" to copy and "no" to start with an empty state.

  Enter a value: yes
...

확인

$ ls state
terraform.tfstate

Resource

리소스는 테라폼이 프로비저닝 도구라는 측면에서 가장 중요한 요소리며, 리소스 블록은 선언한 항목을 생성하는 동작을 수행합니다. 선언할 수 있는 항목은 다음과 같습니다.

  • depends_on: 종속성을 선언하며, 선언된 구성요소와의 생성 시점에 대해 정의
  • count: 선언된 개수에 따라 여러 리소스를 생성
  • for_each: map 또는 set 타입의 데이터 배열의 값을 기준으로 여러 리소스를 생성
  • provider: 동일한 프로바이더가 다수 정의되어 있는 경우 지정
  • lifecycle: 리소스의 수명주기 관리
  • provisioner: 리소스 생성 후 추가 작업 정의
  • timeouts: 프로바이더에서 정의한 일부 리소스 유형에서는 create, update, delete에 대한 허용 시간을 정의 가능

종속성(depends_on)

main.tf

resource "local_file" "abc" {
  content  = "123!"
  filename = "${path.module}/abc.txt"
}

resource "local_file" "def" {
  content  = "456!"
  filename = "${path.module}/def.txt"
}

실행

서로 선후 관계 없이 동시 실행

$ terraform apply -auto-approve
...
Plan: 2 to add, 0 to change, 0 to destroy.
local_file.abc: Creating...
local_file.def: Creating...
local_file.abc: Creation complete after 0s [id=5f30576af23a25b7f44fa7f5fdf70325ee389155]
local_file.def: Creation complete after 0s [id=b9fbde4d33ab9c450a7ce303fb4788c9d2db9aed]

종속성 부여

resource "local_file" "abc" {
  content  = "123!"
  filename = "${path.module}/abc.txt"
}

resource "local_file" "def" {
  depends_on = [
    local_file.abc
  ]

  content  = "456!"
  filename = "${path.module}/def.txt"
}

실행

local_file.abc 생성 후 local_file.def 생성

$ terraform destroy -auto-approve
...

$ terraform apply -auto-approve
...
Plan: 2 to add, 0 to change, 0 to destroy.
local_file.abc: Creating...
local_file.abc: Creation complete after 0s [id=5f30576af23a25b7f44fa7f5fdf70325ee389155]
local_file.def: Creating...
local_file.def: Creation complete after 0s [id=b9fbde4d33ab9c450a7ce303fb4788c9d2db9aed]

수명주기(lifecycle)

lifecycle은 리소스의 기본 수명주기를 작업자가 의도적으로 변경하는 인수입니다. 메타인수 내에는 아래의 속성을 선언 가능합니다.

  • create_before_destroy (bool): 리소스 수정 시 신규 리소스를 우선 생성하고 기존 리소스를 삭제
  • prevent_destroy (bool): 해당 리소스를 삭제 Destroy 하려 할 때 명시적으로 거부
  • ignore_changes (list): 리소스 요소에 선언된 인수의 변경 사항을 테라폼 실행 시 무시
  • precondition: 리소스 요소에 선언해 인수의 조건을 검증
  • postcondition: Plan과 Apply 이후의 결과를 속성 값으로 검증

create_before_destroy

create_before_destroy = false 가 기본값이며, true로 설정 후 동작을 비교합니다.

main.tf

resource "local_file" "abc" {
  content  = "lifecycle - step 1"
  filename = "${path.module}/abc.txt"

  lifecycle {
    create_before_destroy = false
  }
}

실행 및 확인

$ terraform init && terraform plan && terraform apply -auto-approve
...
$ cat abc.txt  
lifecycle - step 1%

main.tf 수정

resource "local_file" "abc" {
  content  = "lifecycle - step 2"
  filename = "${path.module}/abc.txt"

  lifecycle {
    create_before_destroy = false
  }
}

실행 및 동작 확인

$ terraform plan && terraform apply -auto-approve
...
-/+ destroy and then create replacement

Terraform will perform the following actions:

  # local_file.abc must be replaced
-/+ resource "local_file" "abc" {
      ~ content              = "lifecycle - step 1" -> "lifecycle - step 2" # forces replacement
...
Plan: 1 to add, 0 to change, 1 to destroy.
local_file.abc: Destroying... [id=7e817d811209db576335597bcd326518fe13398b]
local_file.abc: Destruction complete after 0s
local_file.abc: Creating...
local_file.abc: Creation complete after 0s [id=43809e4e5139be51422bfdcb41cab0852741ec10]

$ cat abc.txt
lifecycle - step 2%

=> Destroying -> Creating 확인

main.tf 수정

resource "local_file" "abc" {
  content  = "lifecycle - step 3"
  filename = "${path.module}/abc.txt"

  lifecycle {
    create_before_destroy = true
  }
}

실행 및 동작 확인

$ terraform plan && terraform apply -auto-approve
...
+/- create replacement and then destroy

Terraform will perform the following actions:

  # local_file.abc must be replaced
+/- resource "local_file" "abc" {
      ~ content              = "lifecycle - step 2" -> "lifecycle - step 3" # forces replacement
...
Plan: 1 to add, 0 to change, 1 to destroy.
local_file.abc: Creating...
local_file.abc: Creation complete after 0s [id=43809e4e5139be51422bfdcb41cab0852741ec10]
local_file.abc (deposed object daf967ef): Destroying... [id=10250e15edc8a551b7f85e55efd9575c3c8c3113]

$ cat abc.txt
cat: abc.txt: No such file or directory

=> Creating -> Destroying 확인

=> create_before_destroy = true 적용 시, 새로운 리소스 생성 후에 이전 리소스를 삭제하여 무중단 배포가 가능하게 함

=> 하지만 이 테스트에서는 이전 리소스의 파일명이 abc.txt 으로 새 리소스의 파일명과 같으므로 생성 후 삭제가 되어버림

prevent_destroy 

해당 리소스를 삭제 Destroy 하려 할 때 명시적으로 거부

main.tf

resource "local_file" "abc" {
  content  = "lifecycle - step 3"
  filename = "${path.module}/abc.txt"

  lifecycle {
    prevent_destroy = true
  }
}

실행 및 결과 확인

$ terraform plan
local_file.abc: Refreshing state... [id=57b7d0c9d55b33110ce01703cb26078653551b59]

Planning failed. Terraform encountered an error while generating this plan.

╷
│ Error: Instance cannot be destroyed
│ 
│   on main.tf line 1:
│    1: resource "local_file" "abc" {
│ 
│ Resource local_file.abc has lifecycle.prevent_destroy set, but the plan calls for this resource to be destroyed. To avoid this error and
│ continue with the plan, either disable lifecycle.prevent_destroy or reduce the scope of the plan using the -target flag.
╵

ignore_changes 

리소스 요소에 선언된 인수의 변경 사항을 테라폼 실행 시 무시

main.tf

resource "local_file" "abc" {
  content  = "lifecycle - step 4"
  filename = "${path.module}/abc.txt"

  lifecycle {
    ignore_changes = []
  }
}

실행 및 결과 확인

$ terraform apply -auto-approve 
...

$ cat abc.txt
lifecycle - step 4%

=> ignore_changes 에 아무런 인자가 없어 정상적으로 변경 됨

main.tf 수정

resource "local_file" "abc" {
  content  = "lifecycle - step 5"
  filename = "${path.module}/abc.txt"

  lifecycle {
    ignore_changes = [
      content
    ]
  }
}

실행 및 결과 확인

$ terraform apply -auto-approve
...

$ cat abc.txt
lifecycle - step 4%

=> content의 변경 사항이 반영되지 않음

precondition

리소스 요소에 선언해 인수의 조건을 검증

main.tf

variable "file_name" {
  default = "step0.txt"
}

resource "local_file" "abc" {
  content  = "lifecycle - step 6"
  filename = "${path.module}/${var.file_name}"

  lifecycle {
    precondition {
      condition     = var.file_name == "step6.txt"
      error_message = "file name is not \"step6.txt\""
    }
  }
}

실행 및 결과 확인

$ terraform plan
local_file.abc: Refreshing state... [id=63cd1a0c571d59f5b4337364e612786cc63c1bb8]

Planning failed. Terraform encountered an error while generating this plan.

╷
│ Error: Resource precondition failed
│ 
│   on main.tf line 11, in resource "local_file" "abc":
│   11:       condition     = var.file_name == "step6.txt"
│     ├────────────────
│     │ var.file_name is "step0.txt"
│ 
│ file name is not "step6.txt"
╵

=> 리소스 생성 이전에 입력된 변수가 "step6.txt"인지 여부를 검증했고, 실패하여 오류 메시지 출력

postcondition

Plan과 Apply 이후의 결과를 속성 값으로 검증

main.tf

resource "local_file" "abc" {
  content  = ""
  filename = "${path.module}/step7.txt"

  lifecycle {
    postcondition {
      condition     = self.content != ""
      error_message = "content cannot empty"
    }
  }
}

output "step7_content" {
  value = local_file.abc.id
}

실행 및 결과 확인

$ terraform apply -auto-approve
...
-/+ destroy and then create replacement

Terraform planned the following actions, but then encountered a problem:

  # local_file.abc must be replaced
-/+ resource "local_file" "abc" {
      - content              = "lifecycle - step 4" -> null # forces replacement
...
Plan: 1 to add, 0 to change, 1 to destroy.
╷
│ Error: Resource postcondition failed
│ 
│   on main.tf line 7, in resource "local_file" "abc":
│    7:       condition     = self.content != ""
│     ├────────────────
│     │ self.content is ""
│ 
│ content cannot empty
╵

$ terraform state list
local_file.abc

$ cat step7.txt 
cat: step7.txt: No such file or directory

=> 프로비저닝 이후에 생성되는 속성 값이 없어, 영향을 받는 다른 리소스가 생성되기 전에 예상되지 않은 프로비저닝 작업을 방지

main.tf 수정

resource "local_file" "abc" {
  content  = "step7 file ok"
  filename = "${path.module}/step7.txt"

  lifecycle {
    postcondition {
      condition     = self.content != ""
      error_message = "content cannot empty"
    }
  }
}

output "step7_content" {
  value = local_file.abc.id
}

실행 및 결과 확인

$ terraform apply -auto-approve
...
Plan: 1 to add, 0 to change, 1 to destroy.

Changes to Outputs:
  + step7_content = (known after apply)
local_file.abc: Destroying... [id=63cd1a0c571d59f5b4337364e612786cc63c1bb8]
local_file.abc: Destruction complete after 0s
local_file.abc: Creating...
local_file.abc: Creation complete after 0s [id=653353c86e0b78f26dfe85f8e86f4f9d07c0d505]

$ terraform state list
local_file.abc

$ cat step7.txt
step7 file ok%

=> 프로비저닝 이후에 content가 비어있지 않음을 검증하여, 정상적으로 실행됨

profile

DDetB.Log

@DDetMok

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!