Programming/IaC

[IaC] 기본 사용법( condition, function, provisioner, null_resource와 terraform_data, moved block, cli를 위한 시스템 환경 변수) & 프로바이더

Hayley Shim 2023. 10. 28. 18:01

안녕하세요. 최근 도서 “테라폼으로 시작하는 IaC” 의 내용을 기준으로 스터디한 내용을 정리했습니다. 지난 [IaC] 기본 사용법( data source, input variables, local, output, loop) 글에 이어 해당 글에서는 기본 사용법( condition, function, provisioner, null_resource와 terraform_data, moved block, cli를 위한 시스템 환경 변수)와 프로바이더(Provider)에 대해 알아보겠습니다.

 

기본 명령어

6. 조건문

  • 테라폼에서의 조건식은 3항 연산자 형태를 갖는다.
  • 조건은 true 또는 false로 확인되는 모든 표현식을 사용할 수 있다 — 링크
  • 일반적으로 비교, 논리 연산자를 사용해 조건을 확인한다.
  • 조건식은 ? 기호를 기준으로 왼쪽 조건이며, 오른쪽 : 기호를 기준으로 왼쪽이 조건에 대해 true가 반환되는 경우이고 오른쪽 false가 반환되는 경우다.
  • 다음의 예에서 var.a가 빈 문자열이 아니라면 var.a를 나타내지만, 비어 있을 때는 “default-a”를 반환한다.
# <조건 정의> ? <옳은 경우> : <틀린 경우>
var.a != "" ? var.a : "default-a"
  • 조건식의 각 조건은 비교 대상의 형태가 다르면 테라폼 실행 시 조건 비교를 위해 형태를 추론하여 자동으로 변환하는데, 명시적인 형태 작성을 권장한다.
# 조건식 형태 권장 사항 
var.example ? 12 : "hello"            # 비권장
var.example ? "12" : "hello"          # 권장
var.example ? tostring(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 : ""
}

# 실행

# 변수 우선순위3 : 환경 변수 (TF_VAR 변수 이름)
export TF_VAR_enable_file=false
export | grep TF_VAR_enable_file

# 
terraform init && terraform plan && terraform apply -auto-approve
terraform state list

# 환경 변수 삭제
unset TF_VAR_enable_file
export | grep TF_VAR_enable_file

# 재실행
terraform plan && terraform apply -auto-approve
terraform state list

#
echo "local_file.foo[0]" | terraform console
echo "local_file.foo[0].content" | terraform console

7.함수

  • 테라폼은 프로그래밍 언어적인 특성을 가지고 있어서, 값의 유형을 변경하거나 조합할 수 있는 내장 함수를 사용 할 수 있다 .— 링크
  • 내장된 함수 외에 사용자가 구현하는 별도의 사용자 정의 함수를 지원하지는 않는다.
  • 함수 종류에는 숫자, 문자열, 컬렉션, 인코딩, 파일 시스템, 날짜/시간, 해시/암호화, IP 네트워크, 유형 변환이 있다.
  • 테라폼 코드에 함수를 적용하면 변수, 리소스 속성, 데이터 소스 속성, 출력 값 표현 시 작업을 동적이고 효과적으로 수행할 수 있다.
mkdir 3.11 && cd 3.11

# 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)

cidrnetmask("172.16.0.0/12")

exit
-----------------

8. 프로비저너

  • 프로비저너는 프로바이더와 비슷하게 ‘제공자’로 해석되나, 프로바이더로 실행되지 않는 커맨드와 파일 복사 같은 역할을 수행 — 링크
  • 프로비저너로 실행된 결과는 테라폼의 상태 파일과 동기화되지 않으므로 프로비저닝에 대한 결과가 항상 같다고 보장할 수 없다 ⇒ 선언적 보장 안됨
  • 따라서 프로비저너 사용을 최소화하는 것이 좋다. 프로비저너의 종류에는 파일 복사와 명령어 실행을 위한 file, local-exec, remote-exec가 있다.
mkdir 3.12 && cd 3.12

# 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}"
  }
}


# 
terraform init
 
# 실행 계획에서는 결과 유추 불가능
terraform plan

terraform apply -auto-approve

# 테라폼 상태에 프로비저너 정보(실행 및 결과)가 없다
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



# main.tf 수정(on_failure 주석처리)
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 -auto-approve
  • local-exec 프로비저너: 테라폼이 실행되는 환경에서 수행할 커맨드를 정의 — 링크
  • command(필수) : 실행할 명령줄을 입력하며 << 연산자를 통해 여러 줄의 커맨드 입력 가능
  • working_dir(선택) : command의 명령을 실행할 디렉터리를 지정해야 하고 상대/절대 경로로 설정
  • interpreter(선택) : 명령을 실행하는 데 필요한 인터프리터를 지정하며, 첫 번째 인수로 인터프리터 이름이고 두 번째부터는 인터프리터 인수 값
  • environment(선택) : 실행 시 환경 변수 는 실행 환경의 값을 상속받으며, 추가 또는 재할당하려는 경우 해당 인수에 key = value 형태로 설정
# 예시 코드
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!!"
    }

  }
}
  • command의 << 연산자를 통해 다중 라인의 명령을 수행하여 각 환경에 맞는 인터프리터를 지정해 해당 명령을 수행한다.
  • Apply 수행 시 이 명령의 실행 위치를 working_dir를 사용해 지정하고 command에서 사용하는 환경 변수에 대해 environment에서 지정한다.
# 위 예시 코드 실행

# 
terraform init -upgrade

# 
terraform plan && terraform apply -auto-approve

# 
terraform state list
terraform state show null_resource.example1
cat /tmp/file.txt

9. null_resource와 terraform_data

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

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

  • 이런 리소스가 필요한 이유는 테라폼 프로비저닝 동작을 설계하면서 사용자가 의도적으로 프로비저닝하는 동작을 조율해야 하는 상황이 발생하여, 프로바이더가 제공하는 리소스 수명주기 관리만으로는 이를 해결하기 어렵기 때문이다.
주로 사용되는 시나리오

1. 프로비저닝 수행 과정에서 명령어 실행
2. 프로비저너와 함께 사용
3. 모듈, 반복문, 데이터 소스, 로컬 변수와 함께 사용
4. 출력을 위한 데이터 가공
  • AWS EC2 인스턴스를 프로비저닝하기 위해 aws_instance 리소스 구성 시 앞서 확인한 프로비저너를 활용하여 웹서비스를 실행하고자 한다.
mkdir 3.13 && cd 3.13

# main.tf
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를 활용한다.
  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-0b92d8356a0cbca38"
  private_ip             = "172.31.0.100"
  key_name               = "kp-kaje" # 각자 자신의 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("/home/kaje/kp-kaje.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 init -upgrade

# 실행 : EIP 할당 전 (임시) 유동 공인 IP 출력
terraform plan
terraform apply -auto-approve

#
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


#삭제  
terraform destroy -auto-approve
  • null_resource는 정의된 속성이 ‘id’가 전부이므로, 선언된 내부의 구성이 변경되더라도 새로운 Plan 과정에서 실행 계획에 포함되지 못한다.
  • 따라서 사용자가 null_resource에 정의된 내용을 강제로 다시 실행하기 위한 인수로 trigger가 제공된다.
  • trigger는 임의의 string 형태의 map 데이터를 정의하는데, 정의된 값이 변경되면 null_resource 내부에 정의된 행위를 다시 실행한다.

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

10. moved 블록

  • 리소스의 이름은 변경되지만 이미 테라폼으로 프로비저닝된 환경을 그대로 유지하고자 하는 경우 테라폼 1.1 버전부터 moved 블록을 사용할 수 있다.
  • ‘moved’라는 단어가 의미하는 것처럼 테라폼 State에서 옮겨진 대상의 이전 주소와 새 주소를 알리는 역할을 수행한다.
  • moved 블록 이전에는 State를 직접 편집하는 terraform state mv 명령을 사용하여 State를 건드려야 하는 부담이 있었다면, moved 블록은 State에 접근 권한이 없는 사용자라도 변경되는 주소를 리소스 영향 없이 반영할 수 있다.
mkdir 3.14 && cd 3.14

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

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
}

11. 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로 선언된 변수 값을 대체한다.

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

프로바이더

  • 테라폼은 terraform 바이너리 파일을 시작으로 로컬 환경에나 배포 서버와 같은 원격 환경에서 원하는 대상을 호출하는 방식으로 실행된다. 이때 ‘원하는 대상’은 호출하는 방식이 서로 다르지만 대상의 공급자, 즉 프로바이더가 제공하는 API를 호출해 상호작용을 한다. 여기서 테라폼이 대상과의 상호작용을 할 수 있도록 하는 것이 ‘프로바이더’다.
  • 각 프로바이더의 API구현은 서로 다르지만 테라폼의 고유 문법으로 동일한 동작을 수행하도록 구현되어 있다. 프로바이더는 플러그인 형태로 테라폼에 결합되어 대상이 되는 클라우드, SaaS, 기타 서비스 API를 사용해 동작을 수행한다. 각 프로바이더는 테라폼이 관리하는 리소스 유형과 데이터 소스를 사용할 수 있도록 연결한다.
  • 즉, 테라폼은 프로바이더 없이는 어떤 종류의 인프라와 서비스도 관리할 수 없다는 의미다. 대부분의 프로바이더는 대상 인프라 환경이나 서비스 환경에 대한 리소스를 관리하므로, 프로바이더를 구성할 때는 대상과의 연결과 인증에 필요한 정보가 제공되어야 한다.
  • 각 프로바이더는 테라폼 실행 파일과는 별도로 자체 관리되고 게시된다. 테라폼 레지스트리 사이트에서 주요 프로바이더와 관련 문서를 확인 가능
  1. 프로바이더 구성
  • 테라폼 레지스트리의 프로바이더 목록에는 유지 보수 및 게시에 대한 권한에 따라 Tier 정보가 제공 — 링크

로컬 이름과 프로바이더 지정

  • terraform 블록의 required_providers 블록 내에 <로컬 이름> = { } 으로 여러 개의 프로바이더를 정의할 수 있다. 여기서 사용되는 로컬 이름은 테라폼 모듈 내에서 고유해야 한다. 로컬 이름과 리소스 접두사는 독립적으로 선언되며, 각 프로바이더의 소스 경로가 지정되면 프로바이더의 고유 접두사가 제공된다. 만약 동일한 접두사를 사용하는 프로바이더가 선언되는 경우 로컬 이름을 달리해 관련 리소스에서 어떤 프로바이더를 사용하는지 명시적으로 지정할 수 있다.
  • 예시 : hashicorp/http, terraform-aws-modules/http
  • 예를 들어 다음의 main.tf에서처럼 동일한 http 이름을 사용하는 다수의 프로바이더가 있는 경우 각 프로바이더에 고유한 이름을 부여하고 리소스와 데이터 소스에 어떤 프로바이더를 사용할지 provider 인수에 명시한다. 단, 동일한 source에 대해 다수의 정의는 불가능하다.
  • 동일한 http 접두사를 사용하는 다수의 프로바이더 사용 정의 → main.tf 예제
terraform {
  required_providers {
    architech-http = {
      source = "architect-team/http"
      version = "~> 3.0"
    }
    http = {
      source = "hashicorp/http"
    }
    aws-http = {
      source = "terraform-aws-modules/http"
    }
  }
}

data "http" "example" {
  provider = aws-http
  url = "https://checkpoint-api.hashicorp.com/v1/check/terraform"

  request_headers = {
    Accept = "application/json"
  }
}

단일 프로바이더의 다중 정의

  • 동일한 프로바이더를 사용하지만 다른 조건을 갖는 경우, 사용되는 리소스마다 별도로 선언된 프로바이더를 지정해야 하는 경우가 있다.
  • 예를 들면, AWS 프로바이더를 사용하는데 서로 다른 권한의 IAM을 갖는 Access ID 또는 대상 리전을 지정해야 하는 경우다. 이때는 프로바이더 선언에서 alias를 명시하고 사용하는 리소스와 데이터 소스에서는 provider 메타인수를 사용해 특정 프로바이더를 지정할 수 있다.
  • provider 메타인수에 지정되지 않은 경우 alias가 없는 프로바이더가 기본 프로바이더로 동작한다.
mkdir 4.1 && cd 4.1


# 리전(region)을 다르게 구성한 AWS 프로바이더를 aws_instance에 지정

# region 1
provider "aws" {
  region = "ap-southeast-1"
}

# region 2
provider "aws" {
  alias = "seoul"
  region = "ap-northeast-2"
}

resource "aws_instance" "app_server1" {
  ami           = "ami-06b79cf2aee0d5c92"
  instance_type = "t2.micro"
}

resource "aws_instance" "app_server2" {
  provider      = aws.seoul
  ami           = "ami-0ea4d4b8dc1e46212"
  instance_type = "t2.micro"
}
#
terraform init && terraform plan
terraform apply -auto-approve

#
terraform state list
echo "aws_instance.app_server1.public_ip" | terraform console
echo "aws_instance.app_server2.public_ip" | terraform console
aws ec2 describe-instances --filters Name=instance-state-name,Values=running --output table
aws ec2 describe-instances --filters Name=instance-state-name,Values=running --output table --region ap-southeast-1

# 삭제
terraform destroy -auto-approve

프로바이더 요구사항 정의

  • 테라폼 실행 시 요구되는 프로바이더 요구사항은 terraform 블록의 required_providers 블록에 여러 개를 정의할 수 있다. source에는 프로바이더 다운로드 경로를 지정하고 version은 버전 제약을 명시한다.
  • 프로바이더 요구사항 정의 블록
terraform {
  required_providers {
    <프로바이더 로컬 이름> = {
      source = [<호스트 주소>/]<네임스페이스>/<유형>
      version = <버전 제약>
    }
    ...
  }
}

# 호스트 주소 : 프로바이더를 배포하는 주소로서 기본값은 registry.terraform.io
# 네임스페이스 : 지정된 레지스트리 내에서 구분하는 네임스페이스로, 공개된 레지스트리 및 Terraform Cloud의 비공개 레지스트리의 프로바이더를 게시하는 조직을 의미
# 유형 : 프로바이더에서 관리되는 플랫폼이나 서비스 이름으로 일반적으로 접두사와 일치하나 일부 예외가 있을 수 있음
  • 프로바이더는 기능이나 조건이 시간이 지남에 따라 변경될 수 있다. 이 같은 변경에 특정 버전을 명시하거나 버전 호환성을 정의할 때, version에 명시할 수 있다. 이 값이 생략되는 경우 terraform init을 하는 당시의 가장 최신 버전으로 선택된다. (버전 상세 내용은 3.3장 버전 설정 확인)

프로바이더 설치

  • 테라폼을 실행하기 전 terraform init 명령을 통해 정의된 프로바이더를 다운로드, 복사, 캐시에서 읽어오게 된다.
  • 항상 지정된 구성에 대해 동일한 프로바이더를 설치하도록 하려면 테라폼 구성에 사용되는 프로바이더에 대해 명시적으로 terraform 블록에 정의하거나 .terraform.lock.hcl 잠금 파일을 코드 저장소에 공유하는 방안이 요구된다.
  • 2장에서 살펴본 것처럼 required_providers에 지정된 프로바이더가 있는 경우 코드상 구성에서 사용 여부에 관계없이 프로바이더를 다운로드하게 되고, required_providers에 지정하지 않더라고 테라폼 구성 코드사에서 사용된 프로바이더를 테라폼에서 추론해 최신 버전의 프로바이더를 다운로드 한다.

프로바이더 간 전환 여부

  • 클라우드를 대상으로 테라폼을 사용하는 경우 다른 클라우드 프로바이더로 전환이 가능할까? ⇒ 불가능하다
  • 테라폼은 인프라에 대한 단일 프로비저닝 도구로 사용되지만 대상이 되는 환경은 서로 다른 API로 구현된 프로바이더가 제공된다.
  • 프로바이던간 대응되는 리소스 예시

2. 프로바이더 에코시스템

  • 테라폼의 에코시스템은 사용자가 사용하는 방식과 구조에 테라폼을 적용할 수 있도록 설계된다. — Docs, 참고
참고 : https://developer.hashicorp.com/terraform/docs/partnerships
  • 에코시스템을 위한 테라폼 통합은 워크플로 파트너 인프라 파트너로 나눈다.
  • 워크플로 파트너는 테라폼 실행 및 Terraform Cloud/Enterprise와 연계하여 동작하는 기능을 제공하는 항목으로 이루어져 있다. → Run Tasks
  • 대표적으로 테라폼 구성을 관리하기 위한 VCS를 제공하는 깃허브, 깃랩, 비트버킷, 애저 데브옵스가 이 항목에 해당된다.
  • 프로바이더의 경우 인프라 파트너로 해당된다.
  • 인프라 파트너는 사용자가 테라폼으로 대상 플랫폼의 API로 상호작용 가능한 리소스를 관리할 수 있도록 한다.

3. 프로바이더 경험해보기

실습 : 2개의 리전에 Ubuntu EC2 배포

mkdir 4.3 && cd 4.3

# provider_data.tf 파일 생성 : 리전 별 AMI ID값이 다르므로, 필터(filter)를 활용
provider "aws" {
  region = "ap-northeast-2"
  alias  = "region_1"
}

provider "aws" {
  region = "ap-southeast-1"
  alias  = "region_2"
}

data "aws_region" "region_1" {
  provider = aws.region_1
}

data "aws_region" "region_2" {
  provider = aws.region_2
}

data "aws_ami" "ubuntu_region_1" {
  provider = aws.region_1

  most_recent = true
  owners      = ["099720109477"] # Canonical

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
  }
}

data "aws_ami" "ubuntu_region_2" {
  provider = aws.region_2

  most_recent = true
  owners      = ["099720109477"] # Canonical

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
  }
}
  • ec2.tf 파일 생성 : 리전, ami 참조
resource "aws_instance" "region_1" {
  provider = aws.region_1

  ami           = data.aws_ami.ubuntu_region_1.id
  instance_type = "t2.micro"
}

resource "aws_instance" "region_2" {
  provider = aws.region_2

  ami           = data.aws_ami.ubuntu_region_2.id
  instance_type = "t2.micro"
}
# 실행

# [터미널1] ap-northeast-2
while true; do aws ec2 describe-instances **--region** ap-northeast-2 --query "Reservations[*].Instances[*].{PublicIPAdd:PublicIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value,Status:State.Name}" --filters Name=instance-state-name,Values=running --output text ; echo "------------------------------" ; sleep 1; done

# [터미널2] ap-southeast-1
while true; do aws ec2 describe-instances **--region** ap-southeast-1 --query "Reservations[*].Instances[*].{PublicIPAdd:PublicIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value,Status:State.Name}" --filters Name=instance-state-name,Values=running --output text ; echo "------------------------------" ; sleep 1; done

# init & plan & apply
**terraform init && terraform plan && terraform apply -auto-approve
terraform state list**

# 확인
aws ec2 describe-instances **--region** ap-northeast-2 --query "Reservations[*].Instances[*].{PublicIPAdd:PublicIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value,Status:State.Name}" --filters Name=instance-state-name,Values=running --output text
aws ec2 describe-instances **--region** ap-southeast-1 --query "Reservations[*].Instances[*].{PublicIPAdd:PublicIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value,Status:State.Name}" --filters Name=instance-state-name,Values=running --output text

2가지 주의 사항

  1. Warning 1 : Multi-region is hard 프로덕션 수준의 멀티 리전은 어렵다
  • Active-Active 멀티 리전 서비스를 위해서 ‘지역간 지연 시간, 고유 ID, 최종 일관성’ 등 여러가지 고려사항이 많아서 쉽지 않다.

2. Warning 2 : Use aliases sparingly Alias 를 빈번하게 사용하지 말자

  • 별칭을 사용하여 두 리전에 배포하는 단일 테라폼 모듈은 한 리전이 다운 시, plan 과 apply 시도가 실패합니다
  • 프로덕션 환경은 멀티 리전의 별칭을 사용하는 것보다는, 3장에서 설명한 것 처럼 환경을 완전히 격리해야 합니다. 이를 통해 영향도를 최소화 할 수 있습니다.

실습 : Kubernetes 프로바이더를 사용한 Nginx Deployment + Service 배포 — [참고: Docs]

mkdir 4.4 && cd 4.4

# main.tf 파일 생성 : kubernetes 프로바이더 정의 및 KUBECONFIG 파일경로 정의

terraform {
  required_providers {
    kubernetes = {
      source = "hashicorp/kubernetes"
    }
  }
}

provider "kubernetes" {
  config_path    = "~/.kube/config"
}
  • kubernetes.tf 파일 생성 : Deployment, Service 리소스 정의 — [참고: K8s YAML to HCL]
resource "kubernetes_deployment" "nginx" {
  metadata {
    name = "nginx-example"
    labels = {
      App = "t101-nginx"
    }
  }
  spec {
    replicas = 2
    selector {
      match_labels = {
        App = "t101-nginx"
      }
    }
    template {
      metadata {
        labels = {
          App = "t101-nginx"
        }
      }
      spec {
        container {
          image = "nginx:1.7.8"
          name  = "example"

          port {
            container_port = 80
          }
        }
      }
    }
  }
}

resource "kubernetes_service" "nginx" {
  metadata {
    name = "nginx-example"
  }
  spec {
    selector = {
      App = kubernetes_deployment.nginx.spec.0.template.0.metadata[0].labels.App
    }
    port {
      node_port   = 30080
      port        = 80
      target_port = 80
    }

    type = "NodePort"
  }
}
# [터미널2] kubectl 명령
watch -n1 -d kubectl get pods,svc

# [터미널1] terraform init & plan & apply
terraform init && terraform plan && terraform apply -auto-approve
terraform state list

# [터미널2]결과
Every 1.0s: kubectl get pods,svc        hyungwook-Q9W5QX7FGY: Sun Sep 10 19:59:17 2023

NAME                                READY   STATUS    RESTARTS   AGE
pod/nginx-example-d4f75f48f-cpwdk   1/1     Running   0          11s
pod/nginx-example-d4f75f48f-llq8l   1/1     Running   0          11s

NAME                    TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
service/kubernetes      ClusterIP   10.96.0.1      <none>        443/TCP        18m
service/nginx-example   NodePort    10.96.248.69   <none>        80:30201/TCP   10s

# [터미널1] terraform destroy
terraform destroy -auto-approve

실습 : Helm 프로바이더를 사용한 Nginx 배포 — [참고: Docs]

mkdir 4.5 && cd 4.5

# main.tf
terraform {
  required_providers {
    helm = {
      source  = "hashicorp/helm"
      version = "2.11.0"
    }
  }
}

provider "helm" {
  kubernetes {
    config_path = "~/.kube/config"
  }
}


# helm.tf
resource "helm_release" "nginx" {
  name       = "nginx"
  repository = "https://charts.bitnami.com/bitnami"
  chart      = "nginx"

  values = [
    file("${path.module}/nginx-values.yaml")
  ]
}

# nginx-values.yaml 
replicaCount: 1
service:
  type: NodePort


# 실행
# [터미널1] terraform init & plan & apply
terraform init && terraform plan && terraform apply -auto-approve

# [터미널2]helm 배포확인
helm list | grep nginx

# [터미널2]파드배포 확인
kubectl get pods,svc

# [터미널1] terraform destroy
terraform destroy -auto-approve
 

위 내용 외에 도서 ‘Terraform: Up & Running(By Yevgeniy Brikman)’ 를 기준으로 작성된 지난 블로그 글을 참고하여 테라폼 학습에 도움되시길 바랍니다.

[IaC] Terraform Syntax-AWS

[IaC] Terraform Syntax-GCP

[IaC] GKE configuration

[IaC] Terraform state -상태파일공유

[IaC] Terraform state -상태파일격리

[IaC] Terraform modules

[IaC] Terraform tips & tricks — Loops & Conditions

[IaC] Managing Secrets with Terraform

[IaC] Production-grade Terraform code

[IaC] GKE configuration deep dive

[IaC] Working with Multiple Providers

 

 

blog migration project

written in 2023.9.16

https://medium.com/techblog-hayleyshim/iac-%EA%B8%B0%EB%B3%B8-%EC%82%AC%EC%9A%A9%EB%B2%95-condition-function-provisioner-null-resource%EC%99%80-terraform-data-moved-block-cli%EB%A5%BC-%EC%9C%84%ED%95%9C-dc9ee845a92d