본문 바로가기

[Terraform] 테라폼 기초 (2) 본문

DevOps

[Terraform] 테라폼 기초 (2)

겨울바람_ 2024. 8. 19. 22:26

Terraform 조건식

테라폼에서의 조건식은 삼항 연산자 형태를 갖는다.

 

<조건 정의> ? <true> : <false>

 

조건식의 각 조건은 비교 대상의 형태가 다르면 테라폼 실행 시 조건 비교를 위해 형태를 추론하여 자동으로 변환되는데, 혼란이 발생할 수 있기 때문에 명시적인 형태로 작성하는 편이 좋다.

var.example ? tostring(12) : "hello" # 권장
var.example ? "12" : "hello" # 권장

 

count와 조건식을 결합하여 사용하는 경우 특정 조건에 따라 리소스 생성 여부를 선택하는 방식으로 작성할 수 있다.

variable "enable_file" {
    default = true
}

resource "local_file" "foo" {
    count = var.enable_file ? 1 : 0
    content = "foo!"
    filename = "${path.module}/foo.bar"
}

output "content" {
    value = var.enable_file ? local_file.foo[0].content : ""
}

Terraform 함수

테라폼은 여러 내장 함수를 포함하고 있지만, 사용자가 구현하는 별도의 사용자 정의 함수를 지원하지 않는다. 함수 종류에는 숫자, 문자열, 컬렉션, 인코딩, 파일 시스템, 날짜/시간, 해시/암호화, IP 네트워크, 형변환 등이 있다.

 

함수 사용 방법을 익히고 결과를 확인하기 위해 매번 terraform plan, apply를 반복해서 사용하는 것은 비효율적이다.

 

단순히 함수 사용 결과를 확인하는 목적으로는 terraform console을 통해 함수와 값을 입력하여 곧장 확인하는 것이 더욱 효과적이다.

 

exit를 입력하여 console에서 빠져나올 수 있다.

Terraform Provisioner

프로비저너는 프로바이더로 실행되지 않는 커맨드와 파일 복사 같은 역할을 수행한다. 프로비저너로 실행된 결과는 테라폼의 상태 파일과 동기화 되지 않으므로 프로비저닝에 대한 결과가 항상 같다고 보장할 수 없다. 따라서 프로비저너 사용을 최소화 하는 것이 좋다.

 

프로비저너의 경우 리소스 프로비저닝 이후 동작하도록 구성할 수 있다. 예를 들어 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}"
    }
}

 

프로비저너는 선언된 리소스 블록의 작업이 종료되고 나서 지정한 동작을 수행한다. 작성된 예제와 같이 다수의 프로비저너를 반복적으로 선언할 수 있다.

 

terraform apply 명령어를 통해 main.tf 파일의 결과를 확인해보면 다음과 같다.

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

 

첫 번째 프로비저너가 수행되기 때문에 Provisioning with 'local-exec'라는 문구가 출력되지만, 출력하려는 self.contentsensitivetrue로 되어있기 때문에 (output suppressed due to sensitive value in config)가 화면에 표기된다.

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

 

이후 두 번째 프로비저너가 수행되지만, abc라는 커맨드가 존재하지 않기 때문에 apply 명령어가 실패해야 하지만 on_failurecontinue이기 때문에 중지되지 않고 다음 프로비저너로 넘어가는 것을 확인할 수 있다.

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

 

세 번째 프로비저너는 whendestroy로 설정되어 있기 때문에 terraform destroy 명령어가 수행될 때 동작한다. terraform destroy 명령어를 수행하면 아래와 같은 결과를 확인할 수 있다.

 

의도했던 문구가 정상적으로 출력됐다.

 

프로비저너에는 file, local-exec, remote-exec의 세 종류가 있다.

local-exec provisioner

local-exec는 테라폼이 실행되는 환경에서 수행할 명령어를 정의한다.

  • command
    • 필수
    • 실행할 명령어를 입력한다.
    • << 연산자를 통해 여러 줄의 명령어를 입력 가능
  • working_dir
    • 선택
    • command를 실행할 디렉터리를 지정해야 한다.
    • 상대/절대 경로로 설정
  • interpreter
    • 선택
    • 명령을 실행하는 데 필요한 인터프리터를 지정하며, 첫 번째 인수는 인터프리터 이름이고 두 번째부터는 인터프리터 인수 값을 의미한다.
  • environment
    • 선택
    • 실행 시 환경 변수는 실행 환경의 값을 상속받으며, 추가 또는 재할당하려는 경우 해당 인수에 key = value 형태로 설정한다.
resource "null_resource" "example" {
    provisioner "local-exec" {
        command = <<EOF
            echo Hello! > file.txt
            echo $ENV >> file.txt
            EOF

        interpreter = ["bash", "-c"]

        working_dir = "/tmp"

        environment = {
            ENV = "world!"
        }
    }
}

connection

remote-execfile 프로비저너를 사용하기 위해서는 원격지에 연결할 SSH, WinRM 연결 정보가 필요하다.

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 = "C:/App/myapp.conf"

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

 

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

 

connection에서 사용가능한 인수의 목록은 다음과 같다. 원격 연결을 사용하는 일이 잦기 때문에 표로 정리해두었다. 참고하도록 하자

인수 연결 타입 설명 기본값
type SSH/WinRM 연결 유형으로 ssh 또는 winrm ssh
user SSH/WinRM 연결에 사용되는 사용자 ssh: root winrm: Administrator
password SSH/WinRM 연결에 사용되는 비밀번호  
host SSH/WinRM (필수) 연결 대상 주소  
port SSH/WinRM 연결 대상의 타임별 사용 포트 ssh: 22 winrm: 5985
timeout SSH/WinRM 연결 시도에 대한 대기 값 5m
script_path SSH/WinRM 스크립트 복제 시 생성되는 경로 (추가 설명)
private_key SSH 연결 시 사용할 SSH key를 지정하며, password 인수보다 우선함  
certificate SSH 서명된 CA 인증서로 사용 시 private_key와 함께 사용  
agent SSH ssh-agent를 사용해 인증하지 않는 경우 false로 설정하며 Windows의 경우 Pageant만 사용 가능  
agent_identity SSH 인증을 위한 ssh-agent의 기본 사용자  
host_key SSH 원격 호스트 또는 서명된 CA의 연결을 확인하는 데 사용되는 공개키  
target_platform SSH 연결 대상 플랫폼으로 windows 또는 unix unix
https WinRM true인 경우 HTTPS로 연결 false
insecure WinRM true인 경우 HTTPS 유효성 무시 false
use_ntlm WinRM true인 경우 NTLM 인증을 사용 false
cacert WinRM 유효성 검증을 위한 CA 인증서  

 

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

 

주로 사용하는 리눅스 환경에서의 경로는 /tmp/terraform_%RAND%.sh 다. Bastion Host를 통해 연결하는 경우에도 관련 인수를 지원한다. 인수는 다음과 같다.

인수 설명 기본값
bastion_host io 설정하게 되면 바스티온 호스트 연결이 활성화되며, 연결 대상 호스트를 지정  
bastion_host_key 호스트 연결을 위한 공개키  
bastion_port 호스트 연결을 위한 포트 번호 port 인수 값
bastion_user 호스트에 연결할 사용자 user 인수 값
bastion_password 호스트 연결에 사용할 비밀번호 password 인수 값
bastion_private_key 호스트 연결에 사용할 SSH 키파일 private_key 인수 값
bastion_certificate 서명된 CA 인증서 내용으로 bastion_private_key와 함께 사용  

file provisioner

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

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

디렉토리를 대상으로 하는 경우 source의 경로 작성 방법에 따라 동작에 차이가 발생한다. 예를 들어 destination/tmp인 경우 source/foo와 같이 뒤에 /가 붙지 않는 형태일 때는 원격 대상에 /tmp/foo 디렉토리가 업로드 된다.

 

하지만, source/foo/와 같이 뒤에 /가 붙는다면, /foo 디렉토리 내부에 위치한 파일들만 /tmp 디렉토리 내부로 업로드 된다.

remote-exec provisioner

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

 

사용되는 인수는 다음과 같으며, 각 인수는 서로 배타적이다.

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

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

resource "aws_instance" "web" {
    ...

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

null_resource

null_resource는 아무 작업도 수행하지 않는 리소스를 구현한다. 해당 리소스는 테라폼 프로비저닝 동작을 설계하면서 사용자가 의도적으로 프로비저닝하는 동작을 조율해야 하는 상황이 발생하며, 프로바이더가 제공하는 리소스 수명주기 관리만으로는 이를 해결하기 어렵기 때문이다.

 

주로 다음과 같은 상황에서 사용된다.

  • 프로비저닝 수행 과정에서 명령어 실행
  • 프로비저너와 함께 사용
  • 모듈, 반복문, 데이터 소스, 로컬 변수와 함께 사용
  • 출력을 위한 데이터 가공

간단한 사용 예를 다음 상황을 통해 가정해보자.

  • AWS EC2 인스턴스를 프로비저닝하면서 웹 서비스를 실행시키고자 한다.
  • 웹 서비스는 노출되어야 하는 고정 IP가 필요하기 때문에 AWS EIP 리소스를 생성해야 한다.
resource "aws_instance" "foo" {
    ami = "ami-5189a661"
    instance_type = "t2.micro"

    private_ip = "10.0.0.12"
    subnet_id = aws_subnet.tf_test_subnet.id

    provisioner "remote-exec" {
        inline = [
            "echo ${aws_eip.bar.public_ip}"
        ]
    }
}

resource "aws_eip" "bar" {
    vpc = true

    instance = aws.instance.foo.id
    associate_with_private_ip = "10.0.0.12"
    depends_on = [aws_internet_gateway.gw]
}

 

aws_eip가 생성하는 고정된 IP를 할당하기 위해서는 대상인 `aws_instance`의 `id`값이 필요하다. 하지만 `aws_instance`가 프로비저닝 되기 위해서는 `aws_eip`가 생성하는 `public_ip`가 필요하다.

 

어디까지나 예시이기 때문에 `terraform plan` 명령어를 실행해도 정상적으로 동작하지는 않지만, 만약 정상적인 코드라고 가정했을 때 위와 같은 순환 참조가 발생할 경우 `Cycle` 에러가 발생하게 된다.

 

상호 참조되는 종속성을 끊기 위해서는 둘 중 하나의 실행 시점을 한 단계 뒤로 미뤄야 한다. 이런 경우 실행에 간격을 추가하여 실제 리소스와는 무관한 동작을 수행하기 위해 `null_resource`를 활용한다.

 

위의 코드를 다음과 같이 수정할 수 있다.

resource "aws_instance" "foo" {
    ami = "ami-5189a661"
    instance_type = "t2.micro"

    private_ip = "10.0.0.12"
    subnet_id = aws_subnet.tf_test_subnet.id
}

resource "aws_eip" "bar" {
    vpc = true

    instance = aws.instance.foo.id
    associate_with_private_ip = "10.0.0.12"
    depends_on = [aws_internet_gateway.gw]
}

resource "null_resource" "barz" {
    provisioner "remote-exec" {
        connection {
            host = aws_eip.bar.public_ip
        }
        inline = [
            "echo ${aws_eip.bar.public_ip}"
        ]
    }
}

 

null_resource는 정의된 속성이 id가 전부이므로, 선언된 내부의 구성이 변경되더라도 새로운 Plan 과정에서 실행 게획에 포함되지 못한다.

 

따라서 사용자가 null_resource에 정의된 내용을 강제로 다시 실행하기 위한 인수로 trigger가 제공된다.

trigger는 임의의 string 형태의 map 데이터를 정의하는데, 정의된 값이 변경되면 null_resource 내부에 정의된 행위를 다시 실행한다.

resource "null_resource" "foo" {
    ...
    trigger = {
        ec2_id = aws.instance.bar.id # instance의 id가 변경되는 경우 재실행
    }
}

resource "null_resource" "bar" {
    ...
    trigger = {
        ec2_id = time() # 테라폼으로 실행 계획을 생성할 때마다 재실행
    }
}

terraform_data

테라폼 1.4 버전이 릴리즈되면서 기존 null_resource 리소스를 대체하는 terraform_data 리소스가 추가됐다.

 

terraform_data 리소스 또한 자체적으로는 아무것도 수행하지 않지만, 추가 프로바이더 없이 테라폼 자체에 포함된 기본 수명주기 관리자가 제공된다는 것이 장점이다.

 

사용 시나리오는 null_resource와 동일하며 강제 재실행을 위한 triggers_replace와 상태 저장을 위한 input 인수와 input에 저장된 값을 출력하는 output 속성이 제공된다.

 

triggers_replace에 정의되는 값이 기존 map 형태에서 tuple로 변경되어 쓰임이 더 간단해졌다.

resource "terraform_data" "foo" {
    triggers_replace = [
        aws_instance.bar.id,
        aws_instance.barz.id
    ]

    input = "world"
}

output "terraform_data_output" {
    value = terraform_data.foo.output # 출력 결과 : world
}

moved Block

테라폼의 State에 기록되는 리소스 주소의 이름이 변경되면 기존 리소스는 삭제되고 새로운 리소스가 생성되지만, 테라폼 리소스의 이름을 변경해야 하는 상황이 발생할 수 있다.

 

리소스의 이름은 변경되지만 이미 테라폼으로 프로비저닝된 환경을 그대로 유지하고자 하는 경우 테라폼 1.1 버전부터 moved 블록을 사용할 수 있다.

 

moved 블록은 State에 접근 권한이 없는 사용자라도 변경되는 주소를 리소스 영향 없이 반영할 수 있다.

resource "local_file" "a" {
    content = "foo!"
    filename = "${path.module}/foo.bar"
}

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

 

위의 예제 코드에서 local_file의 이름을 a에서 b로 변경해야 할 때, 단순히 리소스의 이름을 a에서 b로 수정한 뒤 plan을 수행하면, a 리소스를 삭제하고 b를 새로 생성하려는 실행 계획이 발생한다.

 

a의 프로비저닝 결과를 유지한 채로 이름을 변경하기 위해서는 moved 블록을 활용하면 된다.

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

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

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

 

moved 블록을 추가하고 plan 명령어를 다시 실행하면 제거되거나 새롭게 생성되는 리소스가 존재한다는 메시지는 보이지 않고, 출력 결과에 local_file.a 주소가 local_file.b로 변경되었다는 메시지가 추가로 출력된다.

 

이후 apply를 수행하고 이후 moved 블록을 삭제하면 새로운 리소스 주소가 사용되어 리팩토링이 완료된 것을 확인할 수 있다.

'DevOps' 카테고리의 다른 글

jdeps와 jlink를 활용한 Java 기반 컨테이너 경량화  (1) 2024.09.12
[Terraform] 테라폼 기초 (3)  (1) 2024.08.28
[Helm] Helm Quick Start  (0) 2024.08.10
[Terraform] 테라폼 반복문  (0) 2024.08.09
[Terraform] 테라폼 기초 (1)  (0) 2024.08.03
Comments