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

[T101] 3-6. Terraform - 프로비저너

by lakescript 2024. 6. 28.

 

더보기

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

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

 

프로비저너

Provisioners are a Last Resort (다른 방안이 안되면, 최후의 수단으로 사용) 

프로비저너는 프로바이더와 비슷하게 '제공자'로 해석되지만 프로바이더로 실행되지 않는 커맨드와 파일 복사 같은 역할을 수행합니다. 예를 들어 AWS EC2 생성 후 특정 패키지를 설치해야 하거나 파일을 생성해야 하는 경우, 이것들은 테라폼의 구성과 별개로 동작해야 합니다. 그렇기에 프로비저너로 실행된 결과는 테라폼의 상태 파일과 동기화되지 않으므로 프로비저닝에 대한 결과가 항상 같다고 보장할 수 없습니다.(⇒ 선언적 보장 안됨) 따라서 프로비저너 사용을 최소화하는 것이 좋습니다. 프로비저너의 종류에는 파일 복사와 명령어 실행을 위한 file, local-exec, remote-exec가 있습니다.

 

프로비저너 사용 방법

프로비저너의 경우 리소스 프로비저닝 이후 동작하도록 구성할 수 있습니다. 

local_file 리소스 내 provisioner 선언 예시

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

 

프로비저너는 선언된 리소스 블록의 작업이 중단되고 나서 지정한 동작을 수행합니다. 위의 코드와 같이 다수의 프로비저너를 반복적으로 선언할 수 있으며 순차적으로 처리됩니다. 또한, 프로비저너는 self 값에 대한 참조가 가능합니다. 리소스 프로비저닝 작업 후 해당 속성 값들을 참조합니다. (위의 코드에서는 content, filename을 가져오도록 구성되어있습니다.) 

하지만 프로비저너를 terraform plan을 수행해보면 프로비저너와 관련된 메시지는 보이지 않습니다. 

위의 사진과 같이 terraform apply를 수행하면 프로비저너의 동작을 확인할 수 있습니다.

 

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

프로비저너의 선언 순서에 따라  Provisioning with 'local-exec'... 메시지는 content를 출력하려는 의도입니다. 하지만 output suppressed due to sensitive value in config 메시지와 함께 원하는 출력이 보이지 않습니다. 이것은 local_file의 content에 지정한 var.sensitive_content 변수의 속성이 sensitive가 설정(true)되어 있기 때문입니다. 따라서 테라폼은 연관된 프로비저너에서의 출력 또한 민감하다고 판단하여 화면에 출력하지 않습니다. 

 

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

두번째 프로비저너에서는 abc라고 하는 커멘트를 수행합니다. 대부분의 작업 환경에서 이러한 커맨드는 없을 것이므로 이 단계에서 Apply 동작은 실패해야 합니다. 하지만 on_failure = continue 선언이 있으므로 실패시에도 다음 단계로 넘어갑니다. 

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

해당 속성에 대해 주석 처리한 후 terraform apply를 실행하게 되면 아래와 같이 command not found 를 반환하며 실패합니다.

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

세번째 프로비저너에 대해서는 terraform apply를 실행했을 때 관련 출력을 찾을 수 없습니다. when = destroy 구문에 의해 terraform destroy를 수행할 때에만 정보를 출력합니다.

 

local-exec 프로비저너

local-exec는 테라폼이 실행되는 환경에서 수행할 커맨드를 정의합니다. 이때 리눅스나 윈도우등 테라폼을 실행하는 환경에 맞게 설정 커맨드를 정의해야 합니다.

  • command (필수) : 실행할 명령줄을 입력하며 << 연산자를 통해 여러 줄의 커맨드 입력 가능
  • working_dir (선택) : command의 명령을 실행할 디렉토리를 지정해야 하고 상대/절대 경로로 설정
  • interpreter(선택) : 명령을 실행하는 데 필요한 인터프리터를 지정하며, 첫 번째 인수는 인터프리터 이름이고 두번째부터는 인수 값
  • environment(선택) : 실행 시 환경 변수는 실행 환경의 값을 상속받으며, 추가 또는 재할당하려는 경우 해당 인수에 key=value 형태로 설정

작업 환경에 따라 각 인수 값을 활용한 테라폼 구성을 적용한 main.tf

Unix / Linux / MacOs Windows
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!!"
    }

  }
}
resource "null_resource" "example1" {
  
  provisioner "local-exec" {
    command = <<EOF
      Hello!! > file.txt
      Get-ChildItem Env:ENV >> file.txt
      EOF
    
    interpreter = [ "PowerShell" , "-Command" ]

    working_dir = "C:\\windows\temp"

    environment = {
      ENV = "world!!"
    }

  }
}

 

 

command의 << 연산자를 통해 다중 라인의 명령을 수행하며 각 환경에 맞는 인터프리터를 지정하여 해당 명령을 수행합니다. (Unix / Linux / macOS - bach, Windows - PowerShell)

 

 Apply 수행 시 이 명령의 실행 위치를 working_dir를 사용해 지정하고 command에서 사용하는 환경 변수에 의해 enviroment에서 지정합니다. Apply 실행 시 working_dir 위치의 file.txt에 기록된 내용을 확인할 수 있습니다.

 

 

원격지 연결

remote-exec와 file 프로비저너를 사용하기 위해서는 원격자에 연결할 SSH, WinRM 연결 정의가 필요합니다.

connection 블록으로 원격지 연결 정의

resource "null_resource" "example" {
    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 = "conf/myapp.conf"

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

 

connection 블록은 리소스에 선언되는 경우 해당 리소스 내에 구성된 프로비저너에 대해 공통으로 선언되고, 프로비저너 내에 선언되는 경우 해당 프로비저너에만 적용됩니다.

 

connection 적용 인수와 설명

 

원격 연결이 요구되는 프로비저너의 경우 스크립트 파일을 원격 시스템에 업로드해 해당 시스템의 기본 쉘에서 실행하도록 script_path의 경우 적절한 위치를 지정하도록 합니다. 특히, 경로는 난수인 %RAND% 경로가 포함되어 생성됩니다.

  • Unix/Linux/macOS : /tmp/terraform_%RAND%.sh
  • Windows(cmd) : C:/windows/temp/terraform_%RAND%.cmd
  • Windows(PowerShell) : C:/windows/temp/terraform_%RAND%.ps1

connection 적용 시 bastion host를 설정하는 인수와 설명

 

 

 

file 프로비저너

file 프로비저너는 테라폼을 실행하는 시스템에서 연결 대상으로 파일 또는 디렉토리를 복사하는데 사용됩니다.

  • source : 소스 파일 또는 디렉토리이며 현재 작업 중인 디렉토리에 대한 상대 경로 또는 절대 경로로 지정할 수 있습니다(content와 함께 사용할 수 없습니다.)
  • content : 연결 대상에 복사할 내용을 정의하며 대상이 디렉토리인 경우 tf-file-content 파일이 생성되고, 파일인 경우 해당 파일에 내용이 기록됩니다. (source와 함께 사용할 수 없습니다.)
  • destination : 필수 항목으로 항상 절대 경로로 지정되어야 하며, 파일 또는 디렉토리 입니다.

destination 지정 시 주의해야 할 점은 ssh 연결의 경우 대상 디렉토리가 존재해야 하며, winrm연결은 디렉토리가 없는 경우 자동으로 생성한다는 점 입니다. 디렉토리를 대상으로 하는 경우에는 source 경로 형태에 따라 동작에 차이가 생깁니다. destination이 /tmp인 경우 source가 디렉토리로 /foo 처럼 마지막에 /가 없는 경우 대상 디렉토리에 지정한 디렉토리가 업로드되어 연결된 시스템에 /tmp/foo가 업로드 됩니다. source가 디렉토리로 /foo/ 처럼 마지막에 /가 포함되는 경우 source 디렉토리 내의 파일만 /tmp 디렉토리에 업로드 됩니다.

 

resource "null_resource" "foo" {
  
  # myapp.conf 파일이 /etc/myapp.conf 로 업로드
  provisioner "file" {
    source      = "conf/myapp.conf"
    destination = "/etc/myapp.conf"
  }
  
  # content의 내용이 /tmp/file.log 파일로 생성
  provisioner "file" {
    content     = "ami used: ${self.ami}"
    destination = "/tmp/file.log"
  }
  
  # configs.d 디렉터리가 /etc/configs.d 로 업로드
  provisioner "file" {
    source      = "conf/configs.d"
    destination = "/etc"
  }
  
  # apps/app1 디렉터리 내의 파일들만 D:/IIS/webapp1 디렉터리 내에 업로드
  provisioner "file" {
    source      = "apps/app1/"
    destination = "D:/IIS/webapp1"
  }

}

 

 

remote- exec 프로비저너

remote-exec는 원격지 환경에서 실행할 커맨드와 스크립트를 정의합니다. 예를 들면 AWS EC2 인스턴스를 생성하고 해당 VM에서 명령을 실행하고 패키지를 설치하는 등의 동작을 의미합니다. 

  • inline : 명령에 대한 목록으로 [ ] 블록 내에 " "로 붂인 다수의 명령을 , 로 구분하여 구성합니다.
  • script : 로컬의 스크립트 경로를 넣고 원격에 복사해 실행합니다.
  • scripts : 로컬의 스크립트 경로의 목록으로 [ ] 블록 내에 " "로 묶인 다수의 스크립트 경로를 , 로 구분해 구성합니다.

file 프로비저너와 remote-exec 구성 예시

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

script 또는 script의 대상 스크립트 실행에 필요한 인수는 관련 구성에 선언할 수 없으므로 필요할 때 file 프로비저너로 해당 스크립트를 업로드하고 inline 인수를 활용하여 스크립트에 인수를 추가합니다.