Terraform

[ Terraform101 Study - 3w ] 함수 & 프로비저너

su''@ 2024. 6. 30. 04:39
Terrafrom T101 4기 실습 스터디 게시글입니다.
"테라폼으로 시작하는 IaC" 도서를 참고하여 정리했습니다. 
함수

 

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

  • 단, 내장된 함수 외에 사용자가 구현하는 별도의 사용자 정의 함수를 지원하지는 않는다.
  • 함수 종류에는 숫자, 문자열, 컬렉션, 인코딩, 파일 시스템, 날짜/시간, 해시/암호화, IP 네트워크, 유형 변환이 있다.
  • 테라폼 코드에 함수를 적용하면 변수, 리소스 속성, 데이터 소스 속성, 출력 값 표현 시 작업을 동적이고 효과적으로 수행할 수 있다.
  • 실습을 위해서 3.11 디렉터리를 신규 생성 후 열기 → main.tf 파일 생성
    mkdir 3.11 && cd 3.11
    touch main.tf

    resource "local_file" "foo" {
      content  = upper("foo! bar!")
      filename = "${path.module}/foo.bar"
    }
  • 실행
    #
    terraform init && terraform plan && terraform apply -auto-approve
    cat foo.bar ; echo
    
    # 내장 함수 간단 사용
    terraform console
    -----------------
    upper("foo!")
    max(5, 12, 9)
    lower(local_file.foo.content)
    upper(local_file.foo.content)
    
    cidrnetmask("172.16.0.0/12")
    
    cidrsubnet("1.1.1.0/24", 1, 0)
    cidrsubnet("1.1.1.0/24", 1, 1)
    cidrsubnet("1.1.1.0/24", 2, 2)
    
    cidrsubnet("1.1.1.0/24", 2, 0)
    cidrsubnet("1.1.1.0/24", 2, 1)
    cidrsubnet("1.1.1.0/24", 2, 2)
    cidrsubnet("1.1.1.0/24", 2, 3)
    
    cidrsubnets("10.1.0.0/16", 4, 4, 8, 4)
    
    exit
    -----------------

 


 

 

프로비저너

 

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

 

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

  • 예를 들어 AWS EC2 생성 후 특정 패키지를 설치해야 하거나 파일을 생성해야 하는 경우, 이것들은 테라폼의 구성과 별개로 동작해야 한다.
  • 프로비저너로 실행된 결과는 테라폼의 상태 파일과 동기화되지 않으므로 프로비저닝에 대한 결과가 항상 같다고 보장할 수 없다선언적 보장 안됨
  • 따라서 프로비저너 사용을 최소화하는 것이 좋다. 프로비저너의 종류에는 파일 복사와 명령어 실행을 위한 file, local-exec, remote-exec가 있다.
👉🏻 인프라 배포 후 애플리케이션 설정 할 수 있는 다양한 방법이 있고 장단점이 있습니다. 좀 더 견고하고 안정적이며 신뢰할 수 있는 방법이 무엇일까요? - 참고링크 링크

테라폼 코드 userdata 사용, cloud-init 사용, Packer 활용, Provisiner Connections 활용, 별도의 설정 관리 툴 사용(Chef, Habitat, Puppet 등) ⇒ 이전에는 local-exec provisioners를 통해서 ansible과 연동하여 인프라 배포 후 구성관리를 많이하였으나, 최근에 이러한 부분을 개선하기 위해 terraform-provider-ansible이 제공된다고 합니다https://registry.terraform.io/providers/ansible/ansible/latest/docs
https://github.com/ansible/terraform-provider-ansible/tree/main

 

  • 프로비저너의 경우 리소스 프로비저닝 이후 동작하도록 구성할 수 있다. 예를 들어 AWS EC2 생성 후 CLI를 통해 별도 작업 수행 상황을 가정
  • 실습을 위해서 3.12 디렉터리를 신규 생성 후 열기 → main.tf 파일 생성
    mkdir 3.12 && cd 3.12
    touch main.tf

    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 확인 : 프로비저너 정보(의존성)이 없다
    terraform graph > graph.dot
    
    # 삭제
    terraform destroy -auto-approve
    ...
    Plan: 0 to add, 0 to change, 1 to destroy.
    local_file.foo: Destroying... [id=3c3b274d119ff5a5ec6c1e215c1cb794d9973ac1]
    local_file.foo: Provisioning with 'local-exec'...
    local_file.foo (local-exec): Executing: ["/bin/sh" "-c" "echo The deleting filename is ./foo.bar"]
    local_file.foo (local-exec): The deleting filename is ./foo.bar
    local_file.foo: Destruction complete after 0s
  • main.tf 파일 내용 수정
    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}"
      }
    }
  • 실행2
    # 민감 정보 참조 부분의 실행 및 결과 내용은 출력 안됨
    # 실행 실패 시 에러 발생되면 중지
    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): (output suppressed due to sensitive value in config)
    local_file.foo (local-exec): (output suppressed due to sensitive value in config)
    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
    ╷
    │ Error: local-exec provisioner error
    │ 
    │   with local_file.foo,
    │   on main.tf line 14, in resource "local_file" "foo":
    │   14:   provisioner "local-exec" {
    │ 
    │ Error running command 'abc': exit status 127. Output: /bin/sh: abc: command not found
    │
프로비저너는 프로바이더와 비슷하게 ‘제공자’로 해석되나, 프로바이더로 실행되지 않는
커맨드와 파일 복사 같은 역할을 수행  링크 Tutorial
  • 리눅스나 윈도우등 테라폼을 실행하는 환경에 맞게 커맨드를 정의, 아래 사용하는 인수 값
    • command(필수) : 실행할 명령줄을 입력하며 << 연산자를 통해 여러 줄의 커맨드 입력 가능
    • working_dir(선택) : command의 명령을 실행할 디렉터리를 지정해야 하고 상대/절대 경로로 설정
    • interpreter(선택) : 명령을 실행하는 데 필요한 인터프리터를 지정하며, 첫 번째 인수로 인터프리터 이름이고 두 번째부터는 인터프리터 인수 값
    • environment(선택) : 실행 시 환경 변수 는 실행 환경의 값을 상속받으면, 추가 또는 재할당하려는 경우 해당 인수에 key = value 형태로 설정
    • 예시 코드
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의 << 연산자를 통해 다중 라인의 명령을 수행하여 각 환경에 맞는 인터프리터를 지정해 해당 명령을 수행한다.
      • Apply 수행 시 이 명령의 실행 위치를 working_dir를 사용해 지정하고 command에서 사용하는 환경 변수에 대해 environment에서 지정한다.
      • main.tf 파일 내용 수정 : macOS/Linux 경우 → 윈도우는 수정해서 사용할것!
        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 -upgrade
        
        # 
        terraform plan && terraform apply -auto-approve
        ...
        null_resource.example1: Creating...
        null_resource.example1: Provisioning with 'local-exec'...
        null_resource.example1 (local-exec): Executing: ["bash" "-c" "      echo Hello!! > file.txt\n      echo $ENV >> file.txt\n"]
        ...
        
        # 
        terraform state list
        terraform state show null_resource.example1
        cat /tmp/file.txt
원격지 연결 - 링크
  • 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
        }
      }
    }
  • 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
  • 베스천 호스트를 통해 연결하는 경우 관련 인수를 지원한다 - 링크
file 프로비저너 : 테라폼을 실행하는 시스템에서 연결 대상으로 파일 또는 디렉터리를 복사하는 데 사용
- 링크
  • 테라폼을 실행하는 시스템에서 연결 대상으로 파일 또는 디렉터리를 복사하는 데 사용
  • 사용되는 인수
    • source : 소스 파일 또는 디렉터리로, 현재 작업 중인 디렉터리에 대한 상태 경로 또는 절대 경로로 지정할 수 있다. content와 함께 사용할 수 없다.
    • content : 연결 대상에 복사할 내용을 정의하며 대상이 디렉터리인 경우 tf-file-content 파일이 생성되고, 파일인 경우 해당 파일에 내용이 기록된다. source와 함께 사용할 수 없다.
    • destination : 필수 항목으로 항상 절대 경로로 지정되어야 하며, 파일 또는 디렉터리다.
  • destination 지정 시 주의해야 할 점은 ssh 연결의 경우 대상 디렉터리가 존재해야 하며, winrm 연결은 디렉터리가 없는 경우 자동으로 생성함
  • 디렉터리를 대상으로 하는 경우에는 source 경로 형태에 따라 동작에 차이가 생긴다.
  • destination이 /tmp인 경우 source가 디렉터리로 /foo 처럼 마지막에 /가 없는 경우 대상 디렉터리에 지정한 디렉터리가 업로드되어 연결된 시스템에 /tmp/foo 디렉터리가 업로드된다.
  • source가 디렉터리로 /foo/ 처럼 마지막에 /가 포함되는 경우 source 디렉터리 내의 파이란 /tmp 디렉터리에 업로드된다.
  • file 프로비저너 구성 예
    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 프로비저너 : 원격지 환경에서 실행할 커맨드와 스크립트를 정의 - 링크
  • 예를 들면 AWS의 EC2 인스턴스를 생성하고 해당 VM에서 명령을 실행하고 패키지를 설치하는 등의 동작을 의미한다.
  • 사용하는 인수는 다음과 같고 각 인수는 서로 배타적이다.
    • inline : 명령에 대한 목록으로 [ ] 블록 내에 “ “로 묶인 다수의 명령을 , 로 구분해 구성한다.
    • script : 로컬의 스크립트 경로를 넣고 원격에 복사해 실행한다.
    • scripts : 로컬의 스크립트 경로의 목록으로 [ ] 블록 내에 “ “로 묶인 다수의 스크립트 경로를 , 로 구분해 구성한다
  • script 또는 scripts의 대상 스크립트 실행에 필요한 인수는 관련 구성에서 선언할 수 없으므로 필요할 때 file 프로바이더로 해당 스크립트를 업로드하고 inline 인수를 활용해 스크립트에 인수를 추가한다.
  • 구성 예
    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 배포 시 remote-exec/file 프로비저너 혹은 terraform-provider-ansible를 활용하는 코드를 작성해보자! - 링크 링크2