[Terraform] 테라폼 기초 (2) 본문
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.content
의 sensitive
가 true
로 되어있기 때문에 (output suppressed due to sensitive value in config)
가 화면에 표기된다.
provisioner "local-exec" {
command = "abc"
on_failure = continue
}
이후 두 번째 프로비저너가 수행되지만, abc
라는 커맨드가 존재하지 않기 때문에 apply
명령어가 실패해야 하지만 on_failure
이 continue
이기 때문에 중지되지 않고 다음 프로비저너로 넘어가는 것을 확인할 수 있다.
provisioner "local-exec" {
when = destroy
command = "echo The deleting filename is ${self.filename}"
}
세 번째 프로비저너는 when
이 destroy
로 설정되어 있기 때문에 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-exec
와 file
프로비저너를 사용하기 위해서는 원격지에 연결할 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 |