<1> 조건문 (실습) https://developer.hashicorp.com/terraform/language/expressions/conditionals Conditional Expressions - Configuration Language | Terraform | HashiCorp Developer Conditional expressions select one of two values. You can use them to define defaults to replace invalid values. developer.hashicorp.com 조건문에 따라 파일을 만드는 예제. 조건문이 참이면 파일을 만들고, 거짓이면 파일을 만들지 않는 예제이다. 0 cd mkdir 310 cd 310 1 # <조건 정의> ? <옳은 경우> : <틀린 경우> var.a != "" ? var.a : "default-a" 옳은 경우 앞에것 반영 , 틀릴경우 뒤에것을 반환한다. 2 # 조건식 형태 권장 사항 var.example ? 12 : "hello" # 비권장 , 숫자와 스트링은 비권장. var.example ? "12" : "hello" # 권장 var.example ? tostring(12) : "hello" # 권장 3 vi main.tf 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 : "" } // 설명 ? count = var.enable_file ? 1 : 0 var.enable_file이 조건 = 조건이 true 면 1, 아니면 0 조건은 var.enale , 디폴트가 true 이므로 실행된다. count는 1이 된다. local_file 을 만드는것이므로 파일이 만들어진다. terraform init && terraform plan && terraform apply -auto-approve terraform state list filename = "${path.module}/foo.bar" ls foo.bat rm -rf * 4 # 변수 우선순위3 : 환경 변수 (TF_VAR 변수 이름) false 로 먼저 테스트 해보자. 환경변수가 우선이다. vi main.tf 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 : "" } export TF_VAR_enable_file=false export | grep TF_VAR_enable_file ls 파일 없음. terraform plan && terraform apply -auto-approve terraform state list 파일로 만들어 지지 않는다. 5 # 환경 변수 삭제 unset TF_VAR_enable_file export | grep TF_VAR_enable_file # 재실행 terraform plan && terraform apply -auto-approve terraform state list 파일이 만들어진다. local_file.foo[0] ls foo.bar 6 테라폼 콘솔을 echo로 출력해서 보자. echo "local_file.foo[0]" | terraform console "filename" = "./foo.bar" echo "local_file.foo[0].content" | terraform console "foo!" 7 조건문을 가지고 AWS리소스를 만들어보자. https://developer.hashicorp.com/terraform/tutorials/configuration-language/expressions?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS Create Dynamic Expressions | Terraform | HashiCorp Developer Make your Terraform configurations more dynamic and reusable with expressions. Use locals to assign expressions to variables for reuse, conditionals to declare if/then scenarios, and the splat expression to return attributes from complex value types. developer.hashicorp.com <2> 내장 함수 사용해보자 (실습) 테라폼은 프로그래밍 언어적인 특성을 가지고 있어서, 값의 유형을 변경하거나 조합할 수 있는 내장 함수를 사용 할 수 있다. 단, 내장된 함수 외에 사용자가 구현하는 별도의 사용자 정의 함수를 지원하지는 않는다. 숫자, 문자열, 컬렉션, 인코딩, 파일 시스템, 날짜/시간, 해시/암호화, IP 네트워크, 유형 변환이 있다. 내장함수 https://developer.hashicorp.com/terraform/language/functions Functions - Configuration Language | Terraform | HashiCorp Developer An introduction to the built-in functions that you can use to transform and combine values in expressions. developer.hashicorp.com 1 cd mkdir 311 cd 311 2 vi main.tf resource "local_file" "foo" { content = upper("foo! bar!") filename = "${path.module}/foo.bar" } // upper 함수 사용 3 # terraform init && terraform plan && terraform apply -auto-approve cat foo.bar ; echo FOO! BAR! 4 # 내장 함수 간단 사용 = 테라폼 콘솔로 사용해보면 된다. terraform console > ----------------- upper("foo!") max(5, 12, 9) lower(local_file.foo.content) upper(local_file.foo.content) cidrnetmask("172.16.0.0/12") exit > upper("foo!") "FOO!" > max(5, 12, 9) 12 > lower(local_file.foo.content) "foo! bar!" > upper(local_file.foo.content) "FOO! BAR!" > cidrnetmask("172.16.0.0/12") "255.240.0.0" > exit 5 내장 함수 이용하기 예제 https://developer.hashicorp.com/terraform/language/functions Functions - Configuration Language | Terraform | HashiCorp Developer An introduction to the built-in functions that you can use to transform and combine values in expressions. developer.hashicorp.com <3> 프로비저너 = 커맨드와 파일 복사 같은 역할을 수행 (실습) https://developer.hashicorp.com/terraform/language/resources/provisioners/syntax 1 프로비저너를 사용하여 서비스를 위한 서버 또는 기타 인프라 개체를 준비하기 위해 로컬 시스템 또는 원격 시스템에서 특정 작업을 모델링할 수 있습니다. 프로비저너는 프로바이더와 비슷하게 ‘제공자’로 해석되나, 프로바이더로 실행되지 않는 커맨드와 파일 복사 같은 역할을 수행. AWS EC2 생성 후 특정 패키지를 설치해야 하거나 파일을 생성해야 하는 경우, 이것들은 테라폼의 구성과 별개로 동작해야 한다 사용자 편리성 때문에 지원은 한다. 프로비저닝 이후 동작할수 있다. 예를 들어 EC2 생성후 추가 작업이 가능하다. 2 단점 ? 프로비저너로 실행된 결과는 테라폼의 상태 파일과 동기화되지 않는다. 프로비저닝에 대한 결과가 항상 같다고 보장할 수 없다 ⇒ 선언적 보장 안됨 따라서, 프로비저너 사용을 최소화하는 것이 좋다. 동기화가 안된다. 3 테라폼 프로바이더 엔서블 제공함. https://registry.terraform.io/providers/ansible/ansible/latest/docs https://github.com/ansible/terraform-provider-ansible/tree/main 4 cd mkdir 312 cd 312 5 vi 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}" } } 6 terraform init && terraform plan terraform apply -auto-approve Plan: 1 to add, 0 to change, 0 to destroy. local_file.foo: Creating... local_file.foo: Provisioning with 'local-exec'... local_file.foo (local-exec): Executing: ["/bin/sh" "-c" "echo The content is SECRET"] local_file.foo (local-exec): The content is SECRET local_file.foo: Provisioning with 'local-exec'... local_file.foo (local-exec): Executing: ["/bin/sh" "-c" "abc"] local_file.foo (local-exec): /bin/sh: abc: command not found local_file.foo: Creation complete after 0s [id=3c3b274d119ff5a5ec6c1e215c1cb794d9973ac1] Apply complete! Resources: 1 added, 0 changed, 0 destroyed. [root@myeks2-bastion-EC2 312]# ls foo.bar main.tf terraform.tfstate // 설명 ? variable 로 컨텐츠 - local file 로 만든다. 첫번째 프로비저너 - 컨텐츠를 찍어본다 - 컨텐츠를 대문자로. SECRET 파일이름은 foo.bar 두번째 프로비저너 - local은 테라폼에서 실행하는 서버에서 실행한다. abc 실행한다. 에러 난다. 에러시 , 그냥 진행하라. 3번째 프로비저너 - 파일을 삭제할때만 동작한다. 실행할때는 동작 안한다. ls foo.bar main.tf terraform.tfstate foo.bar가 생김. 내용은 대문자. Provisioning with 'local-exec'... 실행되는 명령어. echo The content is SECRET The content is SECRET 두번째 실행 /bin/sh" "-c" "abc 에러. 실패해도 계속 하라고 했으니 실행. # 테라폼 상태에 프로비저너 정보(실행 및 결과)가 없다. 상태 정보를 보자 terraform state list 파일이 생긴다. local_file.foo terraform state show local_file.foo cat foo.bar ; echo SECRET cat terraform.tfstate | jq 선언적인 설정은 저장이 안된다. 프로비저너를 최소화 하라는 것이다. 7 # graph 확인 : 프로비저너 정보(의존성)이 없다 terraform graph > graph.dot 8 # 삭제 terraform destroy -auto-approve ... Plan: 0 to add, 0 to change, 1 to destroy. local_file.foo: Destroying... [id=3c3b274d119ff5a5ec6c1e215c1cb794d9973ac1] local_file.foo: Provisioning with 'local-exec'... local_file.foo (local-exec): Executing: ["/bin/sh" "-c" "echo The deleting filename is ./foo.bar"] local_file.foo (local-exec): The deleting filename is ./foo.bar local_file.foo: Destruction complete after 0s 삭제할때 동작한다. 파일이 삭제 되었다고 The deleting filename is ./foo.bar 9 2번쨰 실습 = sensitive = true vi 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}" } } 10 # 민감 정보 참조 부분의 실행 및 결과 내용은 출력 안됨 # 실행 실패 시 에러 발생되면 중지 terraform apply -auto-approve ... Plan: 1 to add, 0 to change, 0 to destroy. local_file.foo: Creating... local_file.foo: Provisioning with 'local-exec'... local_file.foo (local-exec): (output suppressed due to sensitive value in config) local_file.foo (local-exec): (output suppressed due to sensitive value in config) local_file.foo: Provisioning with 'local-exec'... local_file.foo (local-exec): Executing: ["/bin/sh" "-c" "abc"] local_file.foo (local-exec): /bin/sh: abc: command not found ╷ │ Error: local-exec provisioner error │ │ with local_file.foo, │ on main.tf line 29, in resource "local_file" "foo": │ 29: provisioner "local-exec" { │ │ Error running command 'abc': exit status 127. Output: /bin/sh: abc: command not found │ ╵ 설명 ? 1번째 프로비저너 출력 내용이 감추어짐 종속성이 있어 실행 내용이 안보이는 것이다. local_file.foo (local-exec): (output suppressed due to sensitive value in config) local_file.foo (local-exec): (output suppressed due to sensitive value in config) variable "sensitive_content" { default = "secret" sensitive = true } 11 2번째 프로비저너는 에러라 실행이 안된다. │ Error: local-exec provisioner error provisioner "local-exec" { command = "abc" #on_failure = continue } rm -rf * 12 local-exec 프로비저너 ? : 테라폼이 실행되는 환경에서 수행할 커맨드를 정의 << 실행 명령줄 vi main.tf resource "null_resource" "example1" { provisioner "local-exec" { command = < file.txt echo $ENV >> file.txt EOF interpreter = [ "bash" , "-c" ] working_dir = "/tmp" environment = { ENV = "world!!" } } } // 설명 ? null_resource 아무 작업도 수행하지 않는 리소스를 구현 이런 리소스가 필요한 이유는 테라폼 프로비저닝 동작을 설계하면서 사용자가 의도적으로 프로비저닝하는 동작을 조율해야 하는 상황이 발생 프로바이더 << 로 만듬. 인터프리터로 실행 작업 디렉토리 # init이 필요하다. = null 프로비너가 없으므로 ~ terraform init -upgrade # terraform plan && terraform apply -auto-approve ... null_resource.example1: Creating... null_resource.example1: Provisioning with 'local-exec'... null_resource.example1 (local-exec): Executing: ["bash" "-c" " echo Hello!! > file.txt\n echo $ENV >> file.txt\n"] ... 13 확인 # terraform state list null_resource.example1 terraform state show null_resource.example1 cat /tmp/file.txt cat /tmp/file.txt Hello!! world!! tmp 는 작업자 로컬 pc나 서버 디렉토리이다. mac/ linux 경우 가능. windows 경우 디렉토리를 만들어여 한다. <4> 프로비저너 - 원격지 연결 (이론) https://developer.hashicorp.com/terraform/language/resources/provisioners/remote-exec Provisioner: remote-exec | Terraform | HashiCorp Developer The `remote-exec` provisioner invokes a script on a remote resource after it is created. This can be used to run a configuration management tool, bootstrap into a cluster, etc. To invoke a local process, see the `local-exec` provisioner instead. The `remot developer.hashicorp.com 1 원격지 서버 연결해 사용하려면 ssh 에 대한 정의가 필요하다. # connection 블록으로 원격지 연결 정의 한다. // 아래 프로비저너는 공통인 connection { 을 사용한다. rm -rf * vi main.tf resource "null_resource" "example1" { 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을 사용한다. 2 connection 적용 인수와 설명 https://developer.hashicorp.com/terraform/language/resources/provisioners/connection Provisioner Connection Settings | Terraform | HashiCorp Developer The connection block allows you to manage provisioner connection defaults for SSH and WinRM. developer.hashicorp.com 참고 자료 https://developer.hashicorp.com/terraform/language/resources/provisioners/remote-exec 3 원격 연결이 요구되는 프로비저너의 경우 스크립트 파일을 원격 시스템에 업로드해 해당 시스템의 기본 쉘에서 실행하도록 하므로 script_path의 경우 적절한 위치를 지정하도록 한다. Unix/Linux/macOS : /tmp/terraform_%RAND%.sh Windows(cmd) : C:/windows/temp/terraform_%RAND%.cmd Windows(PowerShell) : C:/windows/temp/terraform_%RAND%.ps1 4 베스천 호스트를 통해 연결하는 경우 관련 인수를 지원한다 https://developer.hashicorp.com/terraform/language/resources/provisioners/connection#connecting-through-a-bastion-host-with-ssh 5 파일 프로비저너 (이론) resource "null_resource" "foo" { # myapp.conf 파일이 /etc/myapp.conf 로 업로드 provisioner "file" { source = "conf/myapp.conf" destination = "/etc/myapp.conf" } # content의 내용이 /tmp/file.log 파일로 생성 provisioner "file" { content = "ami used: ${self.ami}" destination = "/tmp/file.log" } # configs.d 디렉터리가 /etc/configs.d 로 업로드 provisioner "file" { source = "conf/configs.d" destination = "/etc" } # apps/app1 디렉터리 내의 파일들만 D:/IIS/webapp1 디렉터리 내에 업로드 provisioner "file" { source = "apps/app1/" destination = "D:/IIS/webapp1" } } provisioner "file" { source = "conf/configs.d" destination = "/etc" source = "conf/configs.d" // 디렉토리 포함 업로드 provisioner "file" { source = "apps/app1/" destination = "D:/IIS/webapp1" // 파일만 업로드 됨 <5> null_resource (실습) 1 null_resource 아무 작업도 수행하지 않는 리소스를 구현 이런 리소스가 필요한 이유는 테라폼 프로비저닝 동작을 설계하면서 사용자가 의도적으로 프로비저닝하는 동작을 조율해야 하는 상황이 발생 https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource 테라폼 1.4 버전이 릴리즈되면서 기존 null_resource 리소스를 대체하는 terraform_data 리소스가 추가 됨. 앞으로는 terraform_data 사용하자. 2 주로 사용되는 시나리오? 프로비저닝 수행 과정에서 명령어 실행 AWS EC2 인스턴스를 프로비저닝하면서 웹서비스를 실행시 사용 웹서비스 설정에는 노출되어야 하는 고정된 외부 IP가 포함된 구성이 필요하다. 따라서 aws_eip 리소스를 생성시 사용 3 cd mkdir 313 cd 313 4 vi 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" 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" } // EC2 생성 되고 나서 원격에서 아래 명령어 실행하라. 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" } 5 # 두 리소스의 종속성이 상호 참조되어 발생하는 에러 terraform init terraform plan Error: Cycle: aws_eip.myeip, aws_instance.example 2개 부분 충돌이 나는 것이다. resource "aws_eip" "myeip" { resource "aws_instance" "example" { // 설명? resource "aws_eip" "myeip" { #vpc = true instance = aws_instance.example.id 서로 상호 참조하도록 코드가 되어 있어 중복. resource "aws_eip" "myeip" { 6 // 수정 중간에 NULL 리소스를 만들어서 해결하자. 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-0116666fed39e2131" private_ip = "172.31.0.100" key_name = "kp-xxxxxxxxx" # 각자 자신의 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/xxxxx/.ssh/kp-xxxxx.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" } 체크 ? // 172.31.0.100 에 해당 하는 서브넷 id로 수정 필요. subnet-0824122a3f45618a2 associate_with_private_ip = "172.31.0.100" private ip 지정함. 100. connection { host = aws_eip.myeip.public_ip resource "aws_instance" "example" { ami = "ami-0c9c942bd7bf113a2" 리소스는 우분트로함. private_key = file("/Users/xxxxx/.ssh/kp-xxxxxxx.pem") # 각자 자신의 EC2 SSH Keypair 파일 위치 지정 ssh 키 위치, 절대 경로. [root@myeks2-bastion-EC2 313]# vi 0717-1.pem [root@myeks2-bastion-EC2 313]# chmod 400 0717-1.pem 7 # null 프로 비저너가 필요함. terraform plan 에러 남. null 프로비저너가 없으니 초기화 한번 해줘야 한다. 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" 콘솔가서 eip 확인 # terraform state list aws_eip.myeip aws_instance.example aws_security_group.instance null_resource.echomyeip 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 유동공인 ip echo "aws_eip.myeip.public_ip" | terraform console 고정 eip # 출력된 EC2 퍼블릭IP로 curl 접속 확인 MYIP=$(terraform output -raw eip) while true; do curl --connect-timeout 1 http://$MYIP/ ; echo "------------------------------"; date; sleep 1; done # (임시) 유동 공인 IP로 SSH 접속이 될까요? 안되죠. 7 삭제 terraform destroy -auto-approve 8 null 단점? 별도 프로바이더 필요 null 리소스 사용하면 init 다시 해야 한다. <6> terraform_data (이론) 1 terraform_data 는 null_resource 대체 버전임 https://ghdwlsgur.github.io/docs/Terraform/terraform_data 잘가, null_resource | HJH 기술블로그 null_resource 대신 terraform_data ghdwlsgur.github.io https://developer.hashicorp.com/terraform/language/resources/terraform-data The terraform_data Managed Resource Type | Terraform | HashiCorp Developer Retrieves the root module output values from a Terraform state snapshot stored in a remote backend. developer.hashicorp.com 2 vi main.tf 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" } terraform init terraform plan terraform apply -auto-approve terraform destroy -auto-approve