3주차 테라폼 기초 - 반복문, 조건문, 함수, 프로비저, null_resource와 terraform_data, moved 블록, CLI를 위한 시스템 변수

1. 반복문

  • 테라폼으로 정의 되지 않은 외부 리소스 혹은 저장된 정보를 테라폼 내에서 참조해야 될때 사용합니다.

data "local_file" "abc" {    // 데이터 소스 블록은 data 로 시작,
                             // 데이터 소스 유형은 `프로바이더이름_리소스 유형`
  filename = "${path.module}/abc.txt"   // 구성 인수들은 { } 안에 선언
}

A. for each

  • for each 는 반복을 할때 타입 값을 하나씩 object로 접근하는 의미입니다. count 의 불편한 부분을 보완하고 있습니다.

  • for eachmap, set 타입만 허용합니다.

  • each 데이터 타입은 key, value 와 같은 값을 가지고 있습니다.

resource "local_file" "abc" {
  for_each = {
    a = "content a"
    b = "content b"
  }
  ///   for_each = toset(["Todd", "James", "Alice", "Dottie"]) -> set 일때
  /// for_each = tomap({  -> map 일때 
//     a_group       = "eastus"
//    another_group = "westus2"
//  })

  content  = each.value
  filename = "${path.module}/${each.key}.txt"
}
  • 다음과 같이 for_each 에 값을 할당하고 사용할 수 있습니다.

다음과 같이 console에서 값을 확인 할 수 있습니다.

  • 다음과 같이 데이터 형태가 map 혹은 set이어야 합니다. 리스트 일때 다음과 같은 오류가 생성됩니다.

resource "aws_iam_user" "the-accounts" {
  for_each = ["Todd", "James", "Alice", "Dottie"]
  name     = each.key
}

B. for each vs count 비교

  • count는 index를 통해서 삭제시에 그전 인덱스들도 삭제되는 문제가 있는데 for each는 그런 문제로부터 자유로운것을 확인 할 수 있습니다.

  • 또한, index로 몇번에 어떤 내용이 있는지 알 수 없습니다.

  • for_each는 리소스를 맵으로 처리해서 중간의 항목을 안전하게 처리할 수 있어서 count 보다 안전합니다.

  • count로 리소스를 생성 했을때, index 값을 확인 할 수 있습니다.

resource "local_file" "abc" {
  count    = 3
  content  = "This is filename abc${count.index}.txt"
  filename = "${path.module}/abc${count.index}.txt"
}
  • 다음과 같이 index_key가 0으로 생성이 됩니다.

하지만 그와 다르게 for_each로 생성하게 되면 key값을 user_id로 가지는것을 확인할 수 있습니다.

provider "aws" {
  region = "ap-northeast-2"
}

resource "aws_iam_user" "myiam" {
  for_each = toset(var.user_names)
  name     = each.value
}

variable "user_names" {
  description = "Create IAM users with these names"
  type        = list(string)
  default     = ["gasida", "akbun", "ssoon"]
}

output "all_users" {
  value = aws_iam_user.myiam
}
  • 중간에 값을 추가 했을때, 값을 확인 할 수 있습니다.

C. For Expressions

  • forexpression 은 동적으로 variable을 참조해서 변환 작업을 진행합니다.

  • for 을 사용하는 방법은 여러 방법이 있습니다.

variable "names" {
  default = ["a", "b", "c"]
}

resource "local_file" "abc" {
  content  = jsonencode(var.names) # 결과 : ["a", "b", "c"]
  filename = "${path.module}/abc.txt"
}

output "file_content" {
  value = local_file.abc.content
}
  • 콘솔에 들어가 다음과 같은 방식으로 iteration을 할 수 있습니다.

  • 다음과 같이 간단하게 값들을 iteration 할 수 있습니다.

https://www.youtube.com/watch?v=Hj_FGLNmXNI 출처: 악분님의 소개 유튜브
variable "names" {
  type    = list(string)
  default = ["a", "b"]
}

output "A_upper_value" {
  value = [for v in var.names : upper(v)]  // value 값만 iteratoin
}

output "B_index_and_value" {
  value = [for i, v in var.names : "${i} is ${v}"] // index 와 value 값을 iteration
}

output "C_make_object" {
  value = { for v in var.names : v => upper(v) } // value 값을 iteration 후에 object로
}

output "D_with_filter" {
  value = [for v in var.names : upper(v) if v != "a"] // value 값을 iteration 하고 if 문으로 필터터
}
  • 해당 access 방법들에 대해서 결과값은 다음과 같다.

  • 실제로 이용되는 for expression을 사용해보자

  • aws 에서 storage 값을 다음과 같이 postfix를 사용해서 buekct을 생성할 수 있다. (악분님 유튜브)

variable "fruits" {
  type        = set(string)
  default     = ["apple", "banana"]
  description = "fruit example"
}

variable "postfix" {
  type        = string
  default     = "test"
  description = "postfix"
}


provider "aws" {
  region = "ap-northeast-2"
}

resource "aws_s3_bucket" "mys3bucket" {
  for_each = toset([for fruit in var.fruits : format("%s-%s", fruit, var.postfix)])
  bucket   = "akbun-t101study-${each.key}"
}
  • 다음과 같은 s3 버킷들이 잘 생기는 것을 확인 할 수 있다.

  • 삭제도 하나만 삭제하면 정상적으로 잘되니 한번 실습해보시는것도 좋을 것 같다.

D. dynamic

  • dynamic 리소스 내부 블록을 동적인 블록으로 생성

  • 어떤 리소스에 대해서 동적으로 리소스를 생성하는 방법으로 동일 리소스를 한번에 생성하는 역할을 한다.

다음과 같이 생성하기 보다 dynamic 으로 생성하면 손쉽게 생성할 수 있다.

일반적인 블록 속성 반복 적용 시

dynamic 블록 적용 시

resource "provider_resource" "name"

{ name = "some_resource" some_setting { key = a_value } some_setting { key = b_value } some_setting { key = c_value } some_setting { key = d_value } }

resource "provider_resource" "name"

{

name = "some_resource" dynamic "some_setting"

{ for_each = {

a_key = a_value

b_key = b_value

c_key = c_value

d_key = d_value

} content

{ key = some_setting.value } }

}

  • 한번 생성 해보자!

variable "names" {
  default = {
    a = "hello a"
    b = "hello b"
    c = "hello c"
  }
}

data "archive_file" "dotfiles" {
  type        = "zip"
  output_path = "${path.module}/dotfiles.zip"

  dynamic "source" {
    for_each = var.names
    content {
      content  = source.value
      filename = "${path.module}/${source.key}.txt"
    }
  }
}

  • 한개씩 만드는것과 동일한 결과가 나온다.

  • 이런 코드를 응집도가 높은것인데 하나의 프로비저닝 로직이 하나의 블럭에 들어가서 코드의 가시성이 높아진다.

2. 조건문

A. Conditional Expression

  • 조건식은 3항 연산자로 생성됩니다.

# <조건 정의> ? <옳은 경우> : <틀린 경우>
var.a != "" ? var.a : "default-a"
  • 간단하게 사용해보자

variable "enable_file" {
  default = true
}

resource "local_file" "foo" {
  count    = var.enable_file ? 1 : 0 // 1개가 만들어지면 된다
  content  = "foo!"
  filename = "${path.module}/foo.bar"
}

output "content" {
  value = var.enable_file ? local_file.foo[0].content : ""
}
  • enable_file 값이 true로 파일이 생성되면 된다.

  • 그럼 한번 enable_file을 false로 설정하고 실행해보자

  • 삭제가 되는것을 확인할 수 있다.

3. 함수

A. 내장 함수

  • 해당 링크로 가보면 다음과 같은 내장 함수들을 제공하고 있다.

  • 그 중에 몇개만 사용해보자

terraform console
-----------------
> 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"
> 

> cidrsubnet("1.1.1.0/24", 1, 0)
"1.1.1.0/25"
> cidrsubnet("1.1.1.0/24", 1, 1) // 첫번째 서브넷에서 제일 첫번째 IP를 지정할 수 있다.
"1.1.1.128/25"
> cidrsubnet("1.1.1.0/24", 2, 2)
"1.1.1.128/26"
> 

> cidrsubnet("1.1.1.0/24", 2, 0)
"1.1.1.0/26"
> cidrsubnet("1.1.1.0/24", 2, 1)
"1.1.1.64/26"
> cidrsubnet("1.1.1.0/24", 2, 2)
"1.1.1.128/26"
> cidrsubnet("1.1.1.0/24", 2, 3)
"1.1.1.192/26"
> 

> cidrsubnets("10.1.0.0/16", 4, 4, 8, 4)
tolist([
  "10.1.0.0/20",
  "10.1.16.0/20",
  "10.1.32.0/24",
  "10.1.48.0/20",
])
>  

4. 프로비저너

  • 프로비저너는 커맨드와 파일 복사와 같은 역할을 하는데 docker file의 command와 동일하다고 생각하면 된다.

  • 마음이 아프게도 프로비저너로 실행된 결과는 tfstate 파일에 동기화되지 않아서 선언적 보장이 되지 않는다.

  • 만약에 프로비져너로 인한 웹서버가 실행에 실패해도 전혀 알 수 없다.

  • 이때문에 terraform-provider-ansible을 사용해서 ansible을 사용해서 진행하는 것을 권장한다.

A. local-exec 프로비저너

  • 테라폼이 실행되는 환경에서 수행할 커맨드를 명시합니다.

  • 윈도우와 리눅스 둘다 환경에 맞게 설정해주면 됩니다.

resource "null_resource" "example1" {
  
  provisioner "local-exec" {
    command = <<EOF   // 필수 값
      echo Hello!! > file.txt
      echo $ENV >> file.txt
      EOF
    
    interpreter = [ "bash" , "-c" ]  // Interpreter 지정을 할 수 있습니다.

    working_dir = "/tmp"   // 커맨드가 돌아가는 지정 폴더 입니다.

    environment = {
      ENV = "world!!"
    }

  }
}
  • 다음과 같이 실행하고 file.txt 값을 생성합니다.

B. 원격지 연결

  • 원격지를 정의하고 해당 원격지에서 수행할 커맨드를 명시합니다.

resource "null_resource" "example1" {
  
  connection {
    type     = "ssh"   //  어떤 connection을 사용할지
    user     = "root" //  어떤 user 로 로그인 할지
    password = var.root_password
    host     = var.host  // 대상 호스트
  }

  provisioner "file" {
    source      = "conf/myapp.conf"
    destination = "/etc/myapp.conf"
  }

  provisioner "file" {   // 해당 프로비저너에서만으로 connection을 설정될 수 있습니다.
    source      = "conf/myapp.conf"
    destination = "C:/App/myapp.conf"

    connection {
        type     = "winrm"   // winrm도 사용이 가능합니다.
        user     = "Administrator"
        password = var.admin_password
        host     = var.host
    }
  }
}

C. file 프로비저너

  • 테라폼을 실행하는 시스템에서 연결대상으로 파일 또는 디렉토리를 복사하는데 사용합니다.

resource "null_resource" "example1" {
  
  provisioner "file" {
    source      = "conf/myapp.conf"  // terraform 을 실행하는 host의 파일 위치
    destination = "/etc/myapp.conf"  // provisioning 되는 호스트의 파일을 전달합니다.
  }
}

  • 주의점

    • destination 지정 시 주의해야 할 점은 ssh 연결의 경우 대상 디렉터리가 존재해야 한다.

    • 디렉터리를 대상으로 하는 경우에는 source 경로 형태에 따라 동작에 차이가 있습니다.

  provisioner "file" {
    content     = "ami used: ${self.ami}"
    destination = "/tmp/file.log"  // content의 내용이 /tmp/file.log 파일로 생성
  }
  
  provisioner "file" {
    source      = "conf/configs.d" 
    destination = "/etc" //  configs.d 디렉터리가 /etc/configs.d 로 업로드
  }
  
  
  provisioner "file" {
    source      = "apps/app1/"
    destination = "D:/IIS/webapp1" //apps/app1 디렉터리 내의 파일들만 D:/IIS/webapp1 디렉터리 내에 업로드
  }

D. remote-exec 프로비저너

  • 원격지에서 실행할 커맨드와 스크립틀르 명시합니다.

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",
    ]
  }
}
  • 다음과 같이 remote-exec 을 통해 스크립트를 실행할 수 있습니다.

5. null_resource와 terraform_data

  • null resource는 아무 작업도 하지 않는 리소스로 의도적으로 선행 리소스를 먼저 프로비저닝을 하고 사후 작업을 지정할 때 사용한다.

A. null_resource

  • 유즈케이스:

    • 프로비저닝 수행 과정에서 명령어 실행

    • 모듈, 반복문, 데이터 소스, 로컬 변수 사용

    • 출력을 위한 데이터 가공

  • 예시 케이스:

    • aws ec2 프로비저닝하면서 웹서비스 실행

    • 웹 서비스는 노출이 필요한 aws_epi가 필요하다.

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"
}
  • 실행을 시키면 다음과 같은 에러를 마주하게 됩니다.

  • 서로 프로비져닝을 하기위해서 서로의 값이 필요한 cycle이 생성이 되고 이때 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-" // 삭제처리리
  private_ip             = "172.31.0.100"
  key_name               = "my-eks.pem" 
  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/simon/my-eks.pem") 
      #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"
}
```
  • 다음과 같이 변경하면 잘 실행되는것을 확인할 수 있다.

  • 다음과 같이 트리거를 생성해서 사용할 수 있다.

resource "null_resource" "foo" {
  triggers = {
    ec2_id = aws_instance.bar.id // instance의 id가 변경되는 경우 재실행
  }
}

B. terraform_data

  • null resource가 별도의 프로바이더 구성이 필요하지만 terraform_data는 기본 수명 주기 관리자가 제공되어서 비슷한 유즈케이스에 활용 될 수 있다.

  • 사용 방법

    • 강제 재실행을 위한 trigger replace

    • 상태저장을 위한 input 인수와 저장된 값을 출력하는 output 속성을 제공합니다.

resource "terraform_data" "bootstrap" {
  triggers_replace = [
    aws_instance.web.id, // null resource 프로바이더 없이 replace시 바로 사용 가능하다.
    aws_instance.database.id
  ]

  provisioner "local-exec" {
    command = "bootstrap-hosts.sh" // 커맨드 사용 가능하다.
  }
}

6. moved 블록

  • 테라폼의 State에 기록되는 리소스 주소의 이름이 변경되면 삭제가 된다!!

  • 삭제되는 케이스들

    • 리소스 이름 변경

    • count 리소스 인덱스 변경

    • 리소스가 모듈로 이동해서 참조되는 주소 변경

  • moved는 테라폼 state의 대상을 삭제하지 않고 이름을 변경하고 프로비져닝 상태를 유지시키려는 목적이다.

A. moved 블록

  • 로컬파일을 생성하고 moved를 이용해 옮겨보자

resource "local_file" "a" {
  content  = "foo!"
  filename = "${path.module}/foo.bar"
}

output "file_content" {
  value = local_file.a.content
}
  • 리소스를 생성하였다.

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
}
  • 다음 코드로 리소스를 옮겨보자

  • 다음과 같이 삭제 없이 이동만 할 수 있게 되었다.

  • 꼭 완료이후에는 삭제해주자

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

A. 시스템 환경 변수

  • 환경 변수로 로깅 레벨 이나 실행에 대한 옵션을 설정할 수 있습니다.

TF_LOG : 테라폼의 stderr 로그 레벨을 설정합니다.

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

TF_VAR_name : TF_VAR_<변수 이름>을 사용하면 입력 시 또는 default로 선언된 변수 값을 대체할수 있습니다. -> 위의 var_enable_file 도 해당 로직을 사용함

TF_DATA_DIR : State 저장 작업 디렉터리별 데이터를 보관하는 위치를 지정할 수 있습니다.

TF_LOG=info terraform plan 

// 다음과 같이 로그 레벨을 info 로 변경할 수 있습니다.

Last updated