본문 바로가기
Tech/Terraform

[T101_3기] 3주차 - 기본 사용 및 프로바이더 (2/3) - 기본 사용(2/2)

by 구름_쟁이 2023. 9. 10.

본 시리즈는 가시다님의 T101(테라폼으로 시작하는 IaC) 3기 진행 내용입니다. (가시다님 노션)

 

도서 정보

https://www.yes24.com/Product/Goods/119179333

 

테라폼으로 시작하는 IaC - 예스24

“현업에서 요구하는 진짜 IaC 사용법”테라폼으로 배우는 인프라 운영의 모든 것IaC는 효율적인 데브옵스와 클라우드 자동화 구축을 위해 꼭 필요한 기술로 각광받고 있다. 그중에서도 테라폼

www.yes24.com

실습 코드

https://github.com/terraform101

 

목차

1. 기본 사용 - 조건문, 함수, 프로비저너 (링크)
2. 기본 사용 - null_resource와 terraform_data , moved 블록, CLI를 위한 시스템 환경 변수
3. 프로바이더

 

 


1. null_resource와 terraform_data

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

null_resource : 아무 작업도 수행하지 않는 리소스를 구현 - 링크

  • 이런 리소스가 필요한 이유는 테라폼 프로비저닝 동작을 설계하면서 사용자가 의도적으로 프로비저닝하는 동작을 조율해야 하는 상황이 발생하여, 프로바이더가 제공하는 리소스 수명주기 관리만으로는 이를 해결하기 어렵기 때문이다.
  • 주로 사용되는 시나리오
    • 프로비저닝 수행 과정에서 명령어 실행
    • 프로비저너와 함께 사용
    • 모듈, 반복문, 데이터 소스, 로컬 변수와 함께 사용
    • 출력을 위한 데이터 가공
  • 예를 들어 다음의 상황을 가정
    • AWS EC2 인스턴스를 프로비저닝하면서 웹서비스를 실행시키고 싶다
    • 웹서비스 설정에는 노출되어야 하는 고정된 외부 IP가 포함된 구성이 필요하다. 따라서 aws_eip 리소스를 생성해야 한다.
  • AWS EC2 인스턴스를 프로비저닝하기 위해 aws_instance 리소스 구성 시 앞서 확인한 프로비저너를 활용하여 웹서비스를 실행하고자 한다
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.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-0d3462967fbf9ccd2"
  private_ip             = "172.31.0.100"
  key_name               = "test-netcloudy" # 각자 자신의 EC2 SSH Keypair 이름 지정
  vpc_security_group_ids = [aws_security_group.instance.id]

  user_data = <<-EOF
              #!/bin/bash
              echo "Hello, T101 Study Netcloudy" > 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("/home/key/net-cloudy.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"
}

선행 작업

  • 키페어 생성
    • 실행
# 프로비저너 필요로 설치
terraform plan
terraform init -upgrade

# 실행 : EIP 할당 전 (임시) 유동 공인 IP 출력
terraform plan
terraform apply -auto-approve
...
null_resource.echomyeip (remote-exec): Connected!
null_resource.echomyeip (remote-exec): 52.79.174.244
...
Outputs:
eip = "52.79.174.244"
public_ip = "3.34.135.96"

#
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 정의와 동작 예제
resource "null_resource" "foo" {
  triggers = {
    ec2_id = aws_instance.bar.id # instance의 id가 변경되는 경우 재실행
  }
  ...생략...
}

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

 

terraform_data : ‘잘가, null_resource’ - 링크 링크2

  • 이 리소스 또한 자체적으로 아무것도 수행하지 않지만 null_resource는 별도의 프로바이더 구성이 필요하다는 점과 비교하여 추가 프로바이더 없이 테라폼 자체에 포함된 기본 수명주기 관리자가 제공된다는 것이 장점이다.
  • 사용 시나리오는 기본 null_resource와 동일하며 강제 재실행을 위한 trigger_replace와 상태 저장을 위한 input 인수와 input에 저장된 값을 출력하는 output 속성이 제공된다.
  • triggers_replace에 정의되는 값이 기존 map 형태에서 tuple로 변경되어 쓰임이 더 간단해졌다
  • terraform_data 리소스의 trigger_replace 정의와 동작 예제
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"
}

 

[도전과제4] terraform_data 리소스나 trigger_replace 를 사용한 테라폼 코드를 작성해보자!

 

terraform_data를 사용해보자

인스턴스 두개를 만들고 인스턴스 instance_type이 변경될 때마다 terraform_data가 실행되는 코드이다.


resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"
}


resource "aws_subnet" "primary" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.1.0/24"
}

resource "aws_subnet" "secondary" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.2.0/24"
}

resource "aws_instance" "primary" {
  instance_type = "t3.medium"
  ami           = "ami-0c9c942bd7bf113a2"
  subnet_id     = aws_subnet.primary.id
}

resource "aws_instance" "secondary" {
  instance_type = "t3.small"
  ami           = "ami-0c9c942bd7bf113a2"
  subnet_id     = aws_subnet.secondary.id
}

resource "terraform_data" "primary" {
  triggers_replace = [
    aws_instance.primary.instance_type
  ]
  provisioner "local-exec" {
    command = <<EOF
      echo ${timestamp()} ${aws_instance.primary.id} ${aws_instance.primary.instance_type} >> primary_file.txt
      EOF
    interpreter = [ "bash" , "-c" ]
    working_dir = "/home/hw038/t101/terraform_data"
    }

  input = "${aws_instance.primary.id}'s Current instance_type => ${aws_instance.primary.instance_type}"
}

resource "terraform_data" "secondary" {
  triggers_replace = [
    aws_instance.secondary.instance_type
  ]

  provisioner "local-exec" {
    command = <<EOF
      echo ${timestamp()} ${aws_instance.secondary.id} ${aws_instance.secondary.instance_type} >> secondary_file.txt
      EOF
    interpreter = [ "bash" , "-c" ]
    working_dir = "/home/hw038/t101/terraform_data"
    }

  input = "${aws_instance.secondary.id}'s Current instance_type => ${aws_instance.secondary.instance_type}"
}

output "terraform_data_primary_output" {
  value = terraform_data.primary.output
}


output "terraform_data_secondary_output" {
  value = terraform_data.secondary.output
}

처음 실행 시

바로 재실행 시

.txt 파일의 내용은 그대로이고 output역시 이전과 동일하다.

 

primary instance의 type을 t3.small로 변경해보자.

변경에 관한 내용. 그에 따른 terraform_data(primary) 가 수행되는 것을 알 수 있다.

그렇다면 .txt 파일도 잘 반영이 되었는지 살펴보자.

primary만 변경했기 때문에 primary.txt 에만 내용이 추가된 것을 확인할 수 있다.

 

 

 

 

 

 

 


2. moved 블록

 

moved 블록 - 링크 실습

    • 테라폼의 State에 기록되는 리소스 주소의 이름이 변경되면 기존 리소스는 삭제되고 새로운 리소스가 생성됨을 앞서 설명에서 확인했다.
    • 하지만 테라폼 리소스를 선언하다 보면 이름을 변경해야 하는 상황이 발생하기도 하는데, 예를 들면 다음과 같다.
      • 리소스 이름을 변경
      • count로 처리하던 반복문을 for_each로 변경
      • 리소스가 모듈로 이동하여 참조되는 주소가 변경
    • 리소스의 이름은 변경되지만 이미 테라폼으로 프로비저닝된 환경을 그대로 유지하고자 하는 경우 테라폼 1.1 버전부터 moved 블록을 사용할 수 있다.
    • ‘moved’라는 단어가 의미하는 것처럼 테라폼 State에서 옮겨진 대상의 이전 주소와 새 주소를 알리는 역할을 수행한다.
    • moved 블록 이전에는 State를 직접 편집하는 terraform state mv 명령을 사용하여 State를 건드려야 하는 부담이 있었다면, moved 블록은 State에 접근 권한이 없는 사용자라도 변경되는 주소를 리소스 영향 없이 반영할 수 있다.
    • 실습을 위해서 3.14 디렉터리를 신규 생성 후 열기 → main.tf 파일 생성
resource "local_file" "a" {
  content  = "foo!"
  filename = "${path.module}/foo.bar"
}

output "file_content" {
  value = local_file.a.content
}
    • 실행
#
terraform init && terraform plan && terraform apply -auto-approve
cat foo.bar ; echo

#
terraform state list
echo "local_file.a" | terraform console
    • main.tf 파일 내용 변경 : 아래 local_file 의 이름을 a → b로 변경 가정
resource "local_file" "b" {
  content  = "foo!"
  filename = "${path.module}/foo.bar"
}

output "file_content" {
  value = local_file.b.content
}
    • plan 확인 : 기존 리소스를 제거하고 새로운 리소스를 생성하려 계획
#
terraform plan
...
Plan: 1 to add, 0 to change, 1 to destroy.
    • main.tf 파일 내용 변경 : local_file.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.b.content
}
    • 실행 : 제거나 생성 없음!
#
terraform plan
...
Terraform will perform the following actions:

  # local_file.a has moved to local_file.b
    resource "local_file" "b" {
        id                   = "4bf3e335199107182c6f7638efaad377acc7f452"
        # (10 unchanged attributes hidden)
    }

Plan: 0 to add, 0 to change, 0 to destroy.

#
terraform apply -auto-approve
terraform state list
echo "local_file.b" | terraform console
    • main.tf 파일 내용 변경 : 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.b.content
}

 

[도전과제5] moved 블록을 사용한 테라폼 코드 리팩터링을 수행해보세요 - 실습

 

 

 

 

 


3. CLI를 위한 시스템 환경 변수

 

테라폼은 환경 변수를 통해 실행 방식과 출력 내용에 대한 옵션을 조절할 수 있다 - 링크

  • 시스템 환경 변수를 설정하면, 영구적으로 로컬 환경에 적용되는 옵션이나 별도 서버 환경에서 실행하기 위한 옵션을 부여할 수 있다.
  • 이를 통해 로컬 작업 환경과 다른 환경 구성에서만 사용될 특정 옵션을 적용한다.
Mac/리눅스/유닉스: export <환경 변수 이름>=<값>
Windows CMD: set <환경 변수 이름>=<값>
Windows PowerShell: $Env:<환경 변수 이름>='<값>'

 

TF_LOG : 테라폼의 stderr 로그에 대한 레벨을 정의

  • trace, debug, info, warn, error, off를 설정할 수 있고 관련 환경 변수가 없는 경우 off와 동일하다
  • 디버깅을 위한 로그 관련 환경 변수 설명은 다음과 같다
    • TF_LOG: 로깅 레벨 지정 또는 해제
    • TF_LOG_PATH: 로그 출력 파일 위치 지정
    • TF_LOG_CORE: TF_LOG와 별도로 테라폼 자체 코어에 대한 로깅 레벨 지정 또는 해제
    • TF_LOG_PROVIDER: TF_LOG와 별도로 테라폼에서 사용하는 프로바이더에 대한 로깅 레벨 지정 또는 해제
  • 환경에 맞게 TF_LOG를 info로 설정하고, terraform plan 동작을 실행하면 테라폼 출력에 관련 로그가 출력된다
TF_LOG=info terraform plan
...

TF_INPUT : 값을 false 또는 0으로 설정하면 테라폼 실행 시 인수에 -input=false 를 추가한 것과 동일한 수행 결과를 확인

    • 환경에 맞게 TF_INPUT을 0으로 설정하고 terraform plan 동작 실행하면 입력받는 동작을 수행하지 않으므로 입력 변수를 입력해야 하는 경우 에러가 출력된다
TF_INPUT=0 terraform plan
Error : No value for required variable

TF_VAR_name : TF_VAR_<변수 이름>을 사용하면 입력 시 또는 default로 선언된 변수 값을 대체한다 ← 3.6절에서 확인!

 

TF_CLI_ARGS TF_CLI_ARGS_subcommand : 테라폼 실행 시 추가할 인수를 정의

# TF_CLI_ARGS="-input=false" terraform apply -auto-approve 는 terraform apply -input=false -auto-approve 와 같다
TF_CLI_ARGS="-input=false" terraform apply -auto-approve
Error: No value for required variable

# TF_CLI_ARGS_apply로 인수를 정의하면 terraform apply 커맨드 수행 시에만 동작한다
export TF_CLI_ARGS_apply="-input=false"
terraform apply -auto-approve
<에러>

terraform plan
<정상 계획 예측 출력>

 

TF_DATA_DIR : State 저장 백엔드 설정과 같은 작업 디렉터리별 데이터를 보관하는 위치를 지정

  • 이 데이터는 .terraform 디렉터리 위치에 기록되지만 TF_DATA_DIR에 경로가 정의되면 기본 경로를 대체하여 사용된다.
  • 일관된 테라폼 사용을 위해서 해당 변수는 실행 시마다 일관되게 적용될 수 있도록 설정하는 것이 중요하다.
  • 설정 값이 이전 실행 시에만 적용되는 경우 init 명령으로 수행된 모듈, 아티팩트 등의 파일을 찾지 못한다.
  • 이미 terraform init이 수행된 상태에서 TF_DATA_DIR로 경로를 재지정하고 실행하는 경우 플러그인 설치가 필요하다는 메시지 출력을 확인할 수 있다.
TF_DATA_DIR=./.terraform_tmp terraform plan
Error: Required plugins anr not installed

 

 

 

 

 

결론

null_resource도 모르던 내가 terraform_data 까지 알게 되었다.

이 부분을 조금이라도 깊게 테스트해보고자 도전과제도 수행했고, 재밌게 실습할 수 있었다.

(역시 도전과제는 조금 더 머리를 써야하고, 결과를 보기 쉽게 만들기 위해서 이런저런 수정을 거쳐 진행하느라 시간이 더 걸리는 감이 있다)

하지만 항상 새로운 것을 배움에 즐겁다

 

댓글