본문 바로가기

DevOps/Terraform

[T102 3주차] (8) 테라폼 기본사용법(3)

cloudNet@ 팀의 가시다 님이 진행하는 테라폼 102 스터디 3주차 정리입니다.

3주차 내용: 

테라폼 기본사용법 (3)

  • 조건문
  • 함수
  • 프로비저너 (local, remote, file )
  • null resource와 terraform_data
  • moved 블록

 


1. 조건문 

테라폼에서의 조건식3항 연산자 형태를 갖는다. 조건은 true 또는 false로 확인되는 모든 표현식을 사용할 수 있다

  • 비교, 논리 연산자를 사용해 조건을 확인한다
  • 조건식은 (?) 기호를 기준으로 왼쪽조건이며, 오른쪽: 기호를 기준으로 왼쪽이 조건에 대해 true가 반환되는 경우이고 오른쪽false가 반환되는 경우다.
# <조건 정의> ? <옳은 경우> : <틀린 경우>
var.a != "" ? var.a : "default-a"

? 기준으로 왼쪽 = 조건

: 기호를 기준으로 조건이 true 일때 왼쪽 반환, false일때 오른쪽 반환

 

2.함수

테라폼은 프로그래밍 언어적인 특성을 가지고 있어서, 값의 유형을 변경하거나 조합할 수 있는 내장 함수를 사용 할 수 있다 - 링크

어디서 쓸수 있나 내장함수?
resource "local_file" "foo" {
  content  = upper("foo! bar!")
  filename = "${path.module}/foo.bar"
}

resource "aws_instance" "example" {
  for_each = toset([for fruit in var.fruits : format("%s-%s", fruit, var.postfix)])
  ami           = "ami-0a0064415cdedc552"
  instance_type = "t2.micro"
  tags = {
     Name = "${each.key}"
     timestamp = "${local.timestamp_sanitized}"
  }
}

1번 , resource 선언할때

locals {
  timestamp = "${timestamp()}"
  timestamp_sanitized = "${replace("${local.timestamp}", "/[- TZ:]/", "")}"

}

2번, local 변수

#terraform console

> jsonencode([for s in var.names : upper(s)])
"[\"A\",\"B\",\"C\"]"

3번, terraform console에서


3.프로비저너

프로비저너는 프로바이더와 비슷하게 ‘제공자’로 해석되나, 프로바이더로 실행되지 않는 커맨드와 파일 복사 같은 역할을 수행 - 링크

  • 예를 들어 AWS EC2 생성 후 특정 패키지를 설치해야 하거나 파일을 생성해야 하는 경우, 이것들은 테라폼의 구성과 별개로 동작해야 한다.
  • 프로비저너로 실행된 결과는 테라폼의 상태 파일과 동기화되지 않으므로 프로비저닝에 대한 결과가 항상 같다고 보장할 수 없다선언적 보장 안됨 ( 멱등성 )
  • 따라서 프로비저너 사용을 최소화하는 것이 좋다
local-exec 프로비저너
remote-exec 프로비저너
file 프로비저너등이 있다.
# 프로비저너의 경우 리소스 프로비저닝 이후 동작하도록 구성할 수 있다. 
# 예를 들어 AWS EC2 생성 후 CLI를 통해 별도 작업 수행 상황을 가정


variable "sensitive_content" {
  default   = "secret"
  #sensitive = true
}

resource "local_file" "foo" {
  content  = upper(var.sensitive_content)
  filename = "${path.module}/foo.bar"

  provisioner "local-exec" {
    command = "echo The content is ${self.content}"
  }

  provisioner "local-exec" {
    command    = "abc"
    on_failure = continue
  }

  provisioner "local-exec" {
    when    = destroy
    command = "echo The deleting filename is ${self.filename}"
  }
}


# 코드 입력 후 init / plan
terraform init && terraform plan

# 
terraform apply -auto-approve

...
Plan: 1 to add, 0 to change, 0 to destroy.
local_file.foo: Creating...
local_file.foo: Provisioning with 'local-exec'...
local_file.foo (local-exec): Executing: ["/bin/sh" "-c" "echo The content is SECRET"]
local_file.foo (local-exec): The content is SECRET
local_file.foo: Provisioning with 'local-exec'...
local_file.foo (local-exec): Executing: ["/bin/sh" "-c" "abc"]
local_file.foo (local-exec): /bin/sh: abc: command not found
local_file.foo: Creation complete after 0s [id=3c3b274d119ff5a5ec6c1e215c1cb794d9973ac1]
  • 테라폼 상태에 프로비저너로 동작한 실행 및 결과는 담기지 않는다.
# 테라폼 상태에 프로비저너 정보(실행 및 결과)가 없다
terraform state list
terraform state show local_file.foo
cat foo.bar ; echo
cat terraform.tfstate | jq
  • graph 확인 시에도 프로비저너 정보는 없다.
# graph 확인 : 프로비저너 정보(의존성)이 없다
terraform graph > graph.dot

3-2. local- exec

  • 리눅스나 윈도우등 테라폼을 실행하는 환경에 맞게 커맨드를 정의, 아래 사용하는 인수 값
    • **command(**필수) : 실행할 명령줄을 입력하며 << 연산자를 통해 여러 줄의 커맨드 입력 가능
    • working_dir(선택) : command의 명령을 실행할 디렉터리를 지정해야 하고 상대/절대 경로로 설정
    • interpreter(선택) : 명령을 실행하는 데 필요한 인터프리터를 지정하며, 첫 번째 인수로 인터프리터 이름이고 두 번째부터는 인터프리터 인수 값
    • environment(선택) : 실행 시 환경 변수 는 실행 환경의 값을 상속받으면, 추가 또는 재할당하려는 경우 해당 인수에 key = value 형태로 설정
  • main.tf 작성
resource "null_resource" "example1" {
  
  provisioner "local-exec" {
    command = <<EOF
      echo Hello!! > file.txt
      echo $ENV >> file.txt
      EOF
    
    interpreter = [ "bash" , "-c" ]

    working_dir = "/tmp"

    environment = {
      ENV = "world!!"
    }

  }
}


## terraform init && terraform apply -auto-approve

3-2. remote -exec

 

  • remote-exec와 file 프로비저너를 사용하기 위해서는 원격지에 대한 SSH , WinRM 연결 정의가 필요하다.
  • connection 블록 리소스 선언 시, 해당 리소스 내에 구성된 프로비저너에 대해 공통으로 선언되고, 프로비저너 내에 선언되는 경우, 해당 프로비저너에서만 적용된다.
# connection 블록으로 원격지 연결 정의
resource "null_resource" "example1" {
  
  connection {
    type     = "ssh"
    user     = "root"
    password = var.root_password
    host     = var.host
  }

  provisioner "file" {
    source      = "conf/myapp.conf"
    destination = "/etc/myapp.conf"
  }

  provisioner "file" {
    source      = "conf/myapp.conf"
    destination = "C:/App/myapp.conf"

    connection {
        type     = "winrm"
        user     = "Administrator"
        password = var.admin_password
        host     = var.host
    }
  }
}

 

  • 원격지 환경에서 실행할 커맨드와 스크립트를 정의 - 링크
resource "aws_instance" "web" {
  # ...

  # Establishes connection to be used by all
  # generic remote provisioners (i.e. file/remote-exec)
  connection {
    type     = "ssh"
    user     = "root"
    password = var.root_password
    host     = self.public_ip
  }

  provisioner "file" {
    source      = "script.sh"
    destination = "/tmp/script.sh"
  }

  provisioner "remote-exec" {
    inline = [
      "chmod +x /tmp/script.sh",
      "/tmp/script.sh args",
    ]
  }
}
  • 위의 내용은 AWS의 EC2 인스턴스를 생성하고 해당 VM에 접속하여 명령어를 실행하고 패키지를 설치하는 등의 동작을 한다.
사용하는 인수는 다음과 같고 각 인수는 서로 배타적이다(?)
 - inline
- script
- scripts
-> 배타적이다. 같이 쓸수 없다는것 

4. null resource와 terraform_data

  • 테라폼 1.4 버전이 릴리즈되면서 기존 null_resource 리소스를 대체하는 terraform_data 리소스가 추가되었다
  • null_resource: 아무 작업도 수행하지 않는 리소스를 구현 
  • terraform_data: 이 리소스 또한 자체적으로 아무것도 수행하지 않지만 null_resource는 별도의 프로바이더 구성이 필요하다는 점과 비교하여 추가 프로바이더 없이 테라폼 자체에 포함된 기본 수명주기 관리자가 제공된다는 것이 장점이다. 

4-1. null_resource

이런 리소스가 필요한 이유는 테라폼 프로비저닝 동작을 설계하면서 사용자가 의도적으로 프로비저닝하는 동작을 조율해야 하는 상황이 발생하여, 프로바이더가 제공하는 리소스 수명주기 관리만으로는 이를 해결하기 어렵기 때문이다.

  • 주로 사용되는 시나리오 
    • 프로비저닝 수행 과정에서 명령어 실행
    • 프로비저너와 함께 사용
    • 모듈, 반복문, 데이터 소스, 로컬 변수와 함께 사용
    • 출력을 위한 데이터 가공
  • 예를 들어 다음의 상황을 가정
    • AWS EC2 인스턴스를 프로비저닝하면서 웹서비스를 실행시키고 싶다
    • 웹서비스 설정에는 노출되어야 하는 고정된 외부 IP가 포함된 구성이 필요하다. 따라서 aws_eip 리소스를 생성해야 한다.
null resource는 file 프로비저너, remote-exec 프로비저너, local-exec  프로비저너를 선언할때 사용한다.
단점으론, state 관리가 안된다 ! 
provider "aws" {
  region = "ap-northeast-2"
}

resource "aws_security_group" "instance" {
  name = "t101sg"

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

}

resource "aws_instance" "example" {
  ami                    = "ami-0c9c942bd7bf113a2"
  instance_type          = "t2.micro"
  subnet_id              = "subnet-dbc571b0"
  private_ip             = "172.31.0.100"
  key_name               = "kp-gasida" # 각자 자신의 EC2 SSH Keypair 이름 지정
  vpc_security_group_ids = [aws_security_group.instance.id]

  user_data = <<-EOF
              #!/bin/bash
              echo "Hello, T101 Study" > index.html
              nohup busybox httpd -f -p 80 &
              EOF

  tags = {
    Name = "Single-WebSrv"
  }

}

resource "aws_eip" "myeip" {
  #vpc = true
  instance = aws_instance.example.id
  associate_with_private_ip = "172.31.0.100"
}

resource "null_resource" "echomyeip" {
  provisioner "remote-exec" {
    connection {
      host = aws_eip.myeip.public_ip
      type = "ssh"
      user = "ubuntu"
      private_key =  file("/Users/gasida/.ssh/kp-gasida.pem") # 각자 자신의 EC2 SSH Keypair 파일 위치 지정
      #password = "qwe123"
    }
    inline = [
      "echo ${aws_eip.myeip.public_ip}"
      ]
  }
}

output "public_ip" {
  value       = aws_instance.example.public_ip
  description = "The public IP of the Instance"
}

output "eip" {
  value       = aws_eip.myeip.public_ip
  description = "The EIP of the Instance"
}
  • terraform init & apply
  • terraform state list
aws_eip.myeip
aws_instance.example
aws_security_group.instance
null_resource.echomyeip
  • null resource를 통해 remote-exec을 실행한것을 알수있다.
resource "null_resource" "echomyeip" {
  provisioner "remote-exec" {
    connection {
      host = aws_eip.myeip.public_ip
      type = "ssh"
      user = "ubuntu"
      private_key =  file("/Users/mzc01-kook/Downloads/koo-seoul.pem") # 각자 자신의 EC2 SSH Keypair 파일 위치 지정
      #password = "qwe123"
    }
    inline = [
      "echo ${aws_eip.myeip.public_ip}"
      ]
  }
}

4-2. terraform_data

terraform_data잘가, null_resource’

  • null_resource의 단점을 극복하기 위해 대체품으로 나옴 
  • 이 리소스 또한 자체적으로 아무것도 수행하지 않지만 null_resource는 별도의 프로바이더 구성이 필요하다는 점과 비교하여 추가 프로바이더 없이 테라폼 자체에 포함된 기본 수명주기 관리자가 제공된다는 것이 장점이다.
  • triggers_replace에 정의되는 값이 기존 map 형태에서 tuple로 변경되어 쓰임이 더 간단해졌다
  • trigger_replace와 상태 저장을 위한 input 인수와 input에 저장된 값을 출력하는 output 속성이 제공된다.
resource "terraform_data" "foo" {
  triggers_replace = [
    aws_instance.foo.id,
    aws_instance.bar.id
  ]

  input = "world"
}

output "terraform_data_output" {
  value = terraform_data.foo.output  # 출력 결과는 "world"
}
테라폼 1.4에서 부터는 null_resource 보다는 terraform_data를 사용하게 권장한다.

5. moved 블록

  • 테라폼의 State에 기록되는 리소스 주소의 이름이 변경되면 기존 리소스는 삭제되고 새로운 리소스가 생성됨 ( immutable)
  • 테라폼 리소스를 선언하다 보면 이름을 변경해야 하는 상황
    • 리소스 이름을 변경
    • count로 처리하던 반복문을 for_each로 변경
    • 리소스가 모듈로 이동하여 참조되는 주소가 변경
  • 리소스의 이름은 변경되지만 이미 테라폼으로 프로비저닝된 환경을 그대로 유지하고자 하는 경우 테라폼 1.1 버전부터 moved 블록을 사용할 수 있다.
resource "local_file" "a" {
  content  = "foo!"
  filename = "${path.module}/foo.bar"
}

output "file_content" {
  value = local_file.a.content
}
  • init & apply
  • Terraform state list
mzc01-kook@MZC01-KOOK 4.0 % terraform state list

local_file.a
  • 이름을 변경해보자.
  • 생성하고 지운다.

  • moved 블록을 사용하면? 

  • 삭제하지 않고 리소스 이름을 변경한다.

5. 시스템 환경 변수 ( TF_VAR )

  • 테라폼은 환경 변수를 통해 실행 방식과 출력 내용에 대한 옵션을 조절할 수 있다
Mac/리눅스/유닉스: export <환경 변수 이름>=<값>
Windows CMD: set <환경 변수 이름>=<값>
Windows PowerShell: $Env:<환경 변수 이름>='<값>'
  1. TF_LOG : 테라폼의 stderr 로그에 대한 레벨을 정의
TF_LOG=info terraform plan
...
2023-07-18T15:14:48.238+0900 [INFO]  Terraform version: 1.3.2
2023-07-18T15:14:48.239+0900 [INFO]  Go runtime version: go1.19.1
2023-07-18T15:14:48.239+0900 [INFO]  CLI args: []string{"/opt/homebrew/Cellar/tfenv/3.0.0/versions/1.3.2/terraform", "plan"}
2023-07-18T15:14:48.239+0900 [INFO]  CLI command args: []string{"plan"}
2023-07-18T15:14:48.250+0900 [INFO]  backend/local: starting Plan operation
2023-07-18T15:14:48.253+0900 [INFO]  provider: configuring client automatic mTLS
2023-07-18T15:14:48.270+0900 [INFO]  provider.terraform-provider-local_v2.4.0_x5: configuring server automatic mTLS: timestamp=2023-07-18T15:14:48.270+0900
2023-07-18T15:14:48.290+0900 [INFO]  ReferenceTransformer: reference not found: "path.module"
2023-07-18T15:14:48.290+0900 [INFO]  provider: configuring client automatic mTLS
2023-07-18T15:14:48.301+0900 [INFO]  provider.terraform-provider-local_v2.4.0_x5: configuring server automatic mTLS: timestamp=2023-07-18T15:14:48.301+0900
2023-07-18T15:14:48.320+0900 [INFO]  backend/local: plan calling Plan
2023-07-18T15:14:48.320+0900 [INFO]  ReferenceTransformer: reference not found: "path.module"
2023-07-18T15:14:48.320+0900 [INFO]  provider: configuring client automatic mTLS
2023-07-18T15:14:48.331+0900 [INFO]  provider.terraform-provider-local_v2.4.0_x5: configuring server automatic mTLS: timestamp=2023-07-18T15:14:48.331+0900
2023-07-18T15:14:48.348+0900 [INFO]  ReferenceTransformer: reference not found: "path.module"
local_file.b: Refreshing state... [id=4bf3e335199107182c6f7638efaad377acc7f452]
2023-07-18T15:14:48.353+0900 [INFO]  ReferenceTransformer: reference not found: "path.module"
2023-07-18T15:14:48.353+0900 [INFO]  backend/local: plan operation completed
  1. TF_INPUT : 값을 false 또는 0으로 설정하면 테라폼 실행 시 인수에 -input=false 를 추가한 것과 동일한 수행 결과를 확인
  2. TF_VAR_name : TF_VAR_<변수 이름>을 사용하면 입력 시 또는 default로 선언된 변수 값을 대체한다
# main.tf 예시 작성
variable "content" {
  description = "input any string "
  type        = string
}

resource "local_file" "b" {
  content  = "${var.content}"
  filename = "${path.module}/foo.bar"
}

moved {
  from = local_file.a
  to   = local_file.b
}

output "file_content" {
  value = local_file.b.content
}

# terraform plan 시 
# input 값 입력하게 되어있음 

# TF_INPUT=0 으로 하면 input 값 안넣음

$ TF_INPUT=0 terraform plan
Error : No value for required variable

# 하지만 에러 발생

$ TF_VAR_content=kook terraform plan
# 요렇게 하면 content variable에 "kook" 들어가면서 의도한 대로 사용 가능!
  1. TF_DATA_DIR : State 저장 백엔드 설정과 같은 작업 디렉터리별 데이터를 보관하는 위치를 지정
#  .terraform 디렉터리 위치에 init 명령시 모듈, 아티팩트 등 파일이 기록되지만 
# TF_DATA_DIR에 경로가 정의되면 기본 경로를 대체하여 사용된다.
# 일관된 테라폼 사용을 위해서 해당 변수는 실행 시마다 일관되게 적용될 수 있도록 설정하는 것이 중요하다.
# 설정 값이 이전 실행 시에만 적용되는 경우 init 명령으로 수행된 모듈, 아티팩트 등의 파일을 찾지 못한다.