Terraform
[ Terraform101 Study - 3w ] null_resource와 terraform_data
su''@
2024. 6. 30. 04:41
Terrafrom T101 4기 실습 스터디 게시글입니다.
"테라폼으로 시작하는 IaC" 도서를 참고하여 정리했습니다.
테라폼 1.4 버전이 릴리즈되면서 기존 null_resource 리소스를 대체하는 terraform_data 리소스가 추가되었다
null_resource : 아무 작업도 수행하지 않는 리소스를 구현 - 링크 Blog
- 이런 리소스가 필요한 이유는 테라폼 프로비저닝 동작을 설계하면서 사용자가 의도적으로 프로비저닝하는 동작을 조율해야 하는 상황이 발생하여, 프로바이더가 제공하는 리소스 수명주기 관리만으로는 이를 해결하기 어렵기 때문이다.
- 주로 사용되는 시나리오
- 프로비저닝 수행 과정에서 명령어 실행
- 프로비저너와 함께 사용
- 모듈, 반복문, 데이터 소스, 로컬 변수와 함께 사용
- 출력을 위한 데이터 가공
- 예를 들어 다음의 상황을 가정
- AWS EC2 인스턴스를 프로비저닝하면서 웹서비스를 실행시키고 싶다
- 웹서비스 설정에는 노출되어야 하는 고정된 외부 IP가 포함된 구성이 필요하다. 따라서 aws_eip 리소스를 생성해야 한다.
- AWS EC2 인스턴스를 프로비저닝하기 위해 aws_instance 리소스 구성 시 앞서 확인한 프로비저너를 활용하여 웹서비스를 실행하고자 한다
- 실습을 위해서 3.13 디렉터리를 신규 생성 후 main.tf 열기
mkdir 3.13 && cd 3.13 touch main.tf
- main.tf
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" # 각자 default VPC에 subnet ID 아무거나 private_ip = "172.31.1.100" 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" } provisioner "remote-exec" { inline = [ "echo ${aws_eip.myeip.public_ip}" ] } } resource "aws_eip" "myeip" { #vpc = true instance = aws_instance.example.id associate_with_private_ip = "172.31.1.100" } output "public_ip" { value = aws_instance.example.public_ip description = "The public IP of the Instance" }
- aws_eip가 생성되는 고정된 IP를 할당하기 위해서는 대상인 aws_instance의 id값이 필요하다
- aws_instance의 프로비저너 동작에서는 aws_eip가 생성하는 속성 값인 public_ip가 필요하다
- 실행 : 테라폼 구성 정의에서 상호 참조가 발생하는 상황으로, 실제 실행되는 코드를 작성하여 plan 수행 시 에러 발생
# 두 리소스의 종속성이 상호 참조되어 발생하는 에러 terraform init terraform plan Error: Cycle: aws_eip.myeip, aws_instance.example
- main.tf 파일 내용 수정 : 둘 중 하나의 실행 시점을 한 단계 뒤로 미뤄야 한다.
- 이런 경우 실행에 간격을 추가하여 실제 리소스와는 무관한 동작을 수행하기 위해 null_resource를 활용한다.
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" }
- 이런 경우 실행에 간격을 추가하여 실제 리소스와는 무관한 동작을 수행하기 위해 null_resource를 활용한다.
- 실행
# 프로비저너 필요로 설치 terraform plan terraform init -upgrade # 실행 : EIP 할당 전 (임시) 유동 공인 IP 출력 terraform plan terraform apply -auto-approve ... null_resource.echomyeip (remote-exec): Connected! null_resource.echomyeip (remote-exec): 13.125.25.238 ... Outputs: eip = "13.125.25.238" public_ip = "43.201.63.58" # terraform state list terraform state show aws_eip.myeip terraform state show aws_instance.example terraform state show null_resource.echomyeip # graph 확인 > graph.dot 파일 선택 후 오른쪽 상단 DOT 클릭 terraform graph > graph.dot # 데이터소스 값 확인 echo "aws_instance.example.public_ip" | terraform console echo "aws_eip.myeip.public_ip" | terraform console # 출력된 EC2 퍼블릭IP로 curl 접속 확인 MYIP=$(terraform output -raw eip) while true; do curl --connect-timeout 1 http://$MYIP/ ; echo "------------------------------"; date; sleep 1; done # (임시) 유동 공인 IP로 SSH 접속이 될까요?
- 삭제: terraform destroy -auto-approve
- null_resource는 정의된 속성이 ‘id’가 전부이므로, 선언된 내부의 구성이 변경되더라도 새로운 Plan 과정에서 실행 계획에 포함되지 못한다.
- 따라서 사용자가 null_resource에 정의된 내용을 강제로 다시 실행하기 위한 인수로 trigger가 제공된다.
- trigger는 임의의 string 형태의 map 데이터를 정의하는데, 정의된 값이 변경되면 null_resource 내부에 정의된 행위를 다시 실행한다.
- trigger 정의와 동작 예제
terraform_data: ‘잘가, null_resource’ -링크 링크2
- 이 리소스 또한 자체적으로 아무것도 수행하지 않지만 null_resource는 별도의 프로바이더 구성이 필요하다는 점과 비교하여 추가 프로바이더 없이 테라폼 자체에 포함된 기본 수명주기 관리자가 제공된다는 것이 장점이다.
- 사용 시나리오는 기본 null_resourcr와 동일하며 강제 재실행을 위한 trigger_replace와 상태 저장을 위한 input 인수와 input에 저장된 값을 출력하는 output 속성이 제공된다.
- triggers_replace에 정의되는 값이 기존 map 형태에서 tuple로 변경되어 쓰임이 더 간단해졌다
- terraform_data 리소스의 trigger_replace 정의와 동작 예제
resource "aws_instance" "web" { # ... } resource "aws_instance" "database" { # ... } # A use-case for terraform_data is as a do-nothing container # for arbitrary actions taken by a provisioner. resource "terraform_data" "bootstrap" { triggers_replace = [ aws_instance.web.id, aws_instance.database.id ] provisioner "local-exec" { command = "bootstrap-hosts.sh" } }
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" }