11 분 소요

본 시리즈는 T101(테라폼으로 시작하는 IaC) 3기 스터디 진행 내용입니다. 테라폼으로 시작하는 IaC 책을 기반으로 내용 정리하였습니다.

도서정보

실습 코드

반복문

Count

  • 정의: list 형태의 값 목록이나 Key-Value 형태의 문자열 집합인 데이터가 있는 경우 동일한 내용에 대해 테라폼 구성 정의를 반복적으로 하지 않고 관리할 수 있다.

  • count : 리소스 또는 모듈 블록에 count 값이 정수인 인수가 포함된 경우 선언된 정수 값만큼 리소스나 모듈을 생성하게 된다.

resource "local_file" "abc" {
  count    = 5 # 
  content  = "abc"
  filename = "${path.module}/abc.txt"
}

output "filecontent" {
  value = local_file.abc.*.content
}

output "fileid" {
  value = local_file.abc.*.id
}

output "filename" {
  value = local_file.abc.*.filename
}
  • 실행 후 확인 : 5개의 파일이 생성되어야 하지만 파일명이 동일하여 결과적으로 하나의 파일만 존재 ← count 사용 시 주의!!> count.index 활용!
    terraform init && terraform apply -auto-approve
    terraform state list
    echo "local_file.abc[0]" | terraform console
    ls *.txt
    terraform output
    
  • count에서 생성되는 참조 값은 count.index이며, 반복하는 경우 0부터 1씩 증가해 인덱스가 부여된다.
  • 여러개 파일 생성을 위해 아래 코드로 테레폼 코드 변경
   content  = "abc${count.index}"

때때로 여러 리소스나 모듈의 count로 지정되는 수량이 동일해야 하는 상황이 있다. 이 경우 count에 부여되는 정수 값을 외부 변수에 식별되도록 구성할 수 있다.

ist 형태의 배열을 활용한 반복문 동작 구성

variable "names" {
  type    = list(string)
  default = ["a", "b", "c"]
}

resource "local_file" "abc" {
  count   = length(var.names)
  content = "abc"
  # 변수 인덱스에 직접 접근
  filename = "${path.module}/abc-${var.names[count.index]}.txt"
}

resource "local_file" "def" {
  count   = length(var.names)
  content = local_file.abc[count.index].content
  # element function 활용
  filename = "${path.module}/def-${element(var.names, count.index)}.txt"
}

local_file.def의 경우 local_file.abc와 개수가 같아야 content에 선언되는 인수 값에 오류가 없을 것이므로 서로 참조되는 리소스와 모듈의 반복정의에 대한 공통의 영향을 주는 변수로 관리할 수 있다.

  • count로 생성되는 리소스의 경우 <리소스 타입="">.<이름>[<인덱스 번호="">], 모듈의 경우 module.<모듈 이름="">[<인덱스 번호="">]로 해당 리소스의 값을 참조한다.
  • 단, 모듈 내에 count 적용이 불가능한 선언이 있으므로 주의해야 한다.
    • 예를 들어 provider 블록 선언부가 포함되어 있는 경우에는 count 적용이 불가능하다 → provider 분리
    • 또한 외부 변수가 list 타입인 경우 중간에 값이 삭제되면 인덱스가 줄어들어 의도했던 중간 값에 대한 리소스만 삭제되는 것이 아니라 이후의 정의된 리소스들도 삭제되고 재생성된다.

for_each

반복문, 선언된 key 값 개수만큼 리소스를 생성

  • 리소스 또는 모듈 블록에서 for_each에 입력된 데이터 형태가 map 또는 set이면, 선언된 key 값 개수만큼 리소스를 생성하게 된다.
    resource "local_file" "abc" {
    for_each = {
      a = "content a"
      b = "content b"
    }
    content  = each.value
    filename = "${path.module}/${each.key}.txt"
    }
    
  • for_each가 설정된 블록에서는 each 속성을 사용해 구성을 수정할 수 있다
    • each.key : 이 인스턴스에 해당하는 map 타입의 key 값
    • each.value : 이 인스턴스에 해당하는 map의 value 값
  • 생성되는 리소스의 경우 <리소스 타입="">.<이름>[], 모듈의 경우 module.<모듈 이름="">[]로 해당 리소스의 값을 참조한다.
  • 이 참조 방식을 통해 리소스 간 종속성을 정의하기도 하고 변수로 다른 리소스에서 사용하거나 출력을 위한 결과 값으로 사용한다.
  • main.tf 파일 수정 : local_file.abc는 변수의 map 형태의 값을 참조, local_file.def의 경우 local_file.abc 도한 결과가 map으로 반환되므로 다시 for_each 구문을 사용할 수 있다
variable "names" {
  default = {
    a = "content a"
    b = "content b"
    c = "content c"
  }
}

resource "local_file" "abc" {
  for_each = var.names
  content  = each.value
  filename = "${path.module}/abc-${each.key}.txt"
}

resource "local_file" "def" {
  for_each = local_file.abc
  content  = each.value.content
  filename = "${path.module}/def-${each.key}.txt"
}

조건문

테라폼에서의 조건식은 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"  # 권장
  • 예제
  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 : ""
}

[도전과제1]

조건문을 활용하여 (각자 편리한) AWS 리소스를 배포하는 코드를 작성해보자!

variable "create_vpc" {
  description = "Set to true to create the VPC, false to skip."
  type        = bool
  default     = true
}
  
# AWS provider 설정
provider "aws" {
  region = "ap-northeast-2" 
}

# VPC 리소스 정의
resource "aws_vpc" "yuran_vpc" {
  count = var.create_vpc ? 1 : 0
  cidr_block = "10.0.0.0/16"
  enable_dns_support = true
  enable_dns_hostnames = true
  tags = {
    Name = "yuranVPC"
  }
}

# 서브넷 리소스 정의
resource "aws_subnet" "yuran_subnet" {
  count = var.create_vpc ? 1 : 0
  vpc_id     = aws_vpc.yuran_vpc[0].id
  cidr_block = "10.0.0.0/24"
  availability_zone = "ap-northeast-2a" 
  tags = {
    Name = "yuranSubnet"
  }
}

함수

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

[도전과제2]

내장 함수을 활용하여 (각자 편리한) 리소스를 배포하는 코드를 작성해보자! 참고

cidrsubnet() 함수를 활용한 vpc 만들기

# AWS provider 설정
provider "aws" {
  region = "ap-northeast-2" 
}

# VPC CIDR 블록 정의
variable "vpc_cidr_block" {
  description = "CIDR block for the VPC"
  default     = "10.0.0.0/16"
}

# 서브넷 수 정의
variable "subnet_count" {
  description = "Number of subnets to create"
  default     = 3
}

# VPC 리소스 정의
resource "aws_vpc" "yuran_vpc" {
  cidr_block = var.vpc_cidr_block
  enable_dns_support = true
  enable_dns_hostnames = true
  tags = {
    Name = "yuranVPC"
  }
}

# 서브넷 리소스 정의
resource "aws_subnet" "yuran_subnet" {
  count = var.subnet_count
  vpc_id     = aws_vpc.yuran_vpc.id
  cidr_block = cidrsubnet(var.vpc_cidr_block, 8, count.index)
  availability_zone = "ap-northeast-2a"  
  tags = {
    Name = "yuranSubnet-${count.index + 1}"
  }
}

프로비저너

프로비저너는 프로바이더와 비슷하게 ‘제공자’로 해석되나, 프로바이더로 실행되지 않는 커맨드와 파일 복사 같은 역할을 수행

  • 예를 들어 AWS EC2 생성 후 특정 패키지를 설치해야 하거나 파일을 생성해야 하는 경우, 이것들은 테라폼의 구성과 별개로 동작해야 한다.
  • 프로비저너로 실행된 결과는 테라폼의 상태 파일과 동기화되지 않으므로 프로비저닝에 대한 결과가 항상 같다고 보장할 수 없다
  • 따라서 프로비저너 사용을 최소화하는 것이 좋다. 프로비저너의 종류에는 파일 복사와 명령어 실행을 위한 file, local-exec, remote-exec가 있다.

  • local-exec 프로비저너: 테라폼이 실행되는 환경에서 수행할 커맨드를 정의
    • 리눅스나 윈도우 등 테라폼을 실행하는 환경에 맞게 커맨드를 정의, 아래 사용하는 인수 값
      • command(필수) : 실행할 명령줄을 입력하며 « 연산자를 통해 여러 줄의 커맨드 입력 가능
      • working_dir(선택) : command의 명령을 실행할 디렉터리를 지정해야 하고 상대/절대 경로로 설정
      • interpreter(선택) : 명령을 실행하는 데 필요한 인터프리터를 지정하며, 첫 번째 인수로 인터프리터 이름이고 두 번째부터는 인터프리터 인수 값
      • environment(선택) : 실행 시 환경 변수 는 실행 환경의 값을 상속받으며, 추가 또는 재할당하려는 경우 해당 인수에 key = value 형태로 설정
  • 원격지 연결 - 링크
    • remote-exec와 file 프로비저너를 사용하기 위해 원격지에 연결할 SSH, WinRM 연결 정의가 필요하다
    • connection 블록 리소스 선언 시, 해당 리소스 내에 구성된 프로비저너에 대해 공통으로 선언되고, 프로비저너 내에 선언되는 경우, 해당 프로비저너에서만 적용된다.

[도전과제3]

AWS EC2 배포 시 remote-exec/file 프로비저너 혹은 terraform-provider-ansible를 활용하는 코드를 작성해보자!

# AWS provider 설정
provider "aws" {
  region = "ap-northeast-2"
}

# 최신 Ubuntu 20.04 LTS AMI ID
data "aws_ami" "ubuntu" {
  most_recent = true

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

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }

  owners = ["099720109477"]  
}

# VPC CIDR 블록 정의
variable "vpc_cidr_block" {
  description = "CIDR block for the VPC"
  default     = "10.0.0.0/16"
}

# 서브넷 수 정의
variable "subnet_count" {
  description = "Number of subnets to create"
  default     = 3
}

# VPC 리소스 정의
resource "aws_vpc" "yuran_vpc" {
  cidr_block           = var.vpc_cidr_block
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = {
    Name = "yuranVPC"
  }
}

# 서브넷 리소스 정의
resource "aws_subnet" "yuran_subnet" {
  count = var.subnet_count
  vpc_id           = aws_vpc.yuran_vpc.id
  cidr_block       = cidrsubnet(var.vpc_cidr_block, 8, count.index)
  availability_zone = "ap-northeast-2a"  
  tags = {
    Name = "yuranSubnet-${count.index + 1}"
  }
}

# EC2 인스턴스 및 SSH 연결 설정
resource "aws_instance" "yuran_instance" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t2.micro"    

  # 접속대상에 연결하기 위한 설정
  connection {
    user        = "ubuntu"
    type        = "ssh"
    private_key = file("yuran.pem")
    host        = self.public_ip
  }

  # remote-exec를 사용해 접속대상에서 test-remote-exec 폴더 생성
  provisioner "remote-exec" {
    inline = [
      "sudo mkdir test-remote-exec"
    ]
  }
    subnet_id = aws_subnet.yuran_subnet[0].id  
}

null_resource와 terraform_data

null_resource

아무 작업도 수행하지 않는 리소스를 구현

  • 이런 리소스가 필요한 이유는 테라폼 프로비저닝 동작을 설계하면서 사용자가 의도적으로 프로비저닝하는 동작을 조율해야 하는 상황이 발생하여, 프로바이더가 제공하는 리소스 수명주기 관리만으로는 이를 해결하기 어렵기 때문이다.
  • 주로 사용되는 시나리오
    • 프로비저닝 수행 과정에서 명령어 실행
    • 프로비저너와 함께 사용
    • 모듈, 반복문, 데이터 소스, 로컬 변수와 함께 사용
    • 출력을 위한 데이터 가공
  • 예를 들어 다음의 상황을 가정
    • AWS EC2 인스턴스를 프로비저닝하면서 웹서비스를 실행시키고 싶다
    • 웹서비스 설정에는 노출되어야 하는 고정된 외부 IP가 포함된 구성이 필요하다. 따라서 aws_eip 리소스를 생성해야 한다.
  • AWS EC2 인스턴스를 프로비저닝하기 위해 aws_instance 리소스 구성 시 앞서 확인한 프로비저너를 활용하여 웹서비스를 실행하고자 한다
# AWS provider 설정
provider "aws" {
  region = "ap-northeast-2"
}

# 최신 Ubuntu 20.04 LTS AMI ID
data "aws_ami" "ubuntu" {
  most_recent = true

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

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }

  owners = ["099720109477"]
}

# VPC CIDR 블록 정의
variable "vpc_cidr_block" {
  description = "CIDR block for the VPC"
  default     = "10.0.0.0/16"
}

# 서브넷 수 정의
variable "subnet_count" {
  description = "Number of subnets to create"
  default     = 3
}

# VPC 리소스 정의
resource "aws_vpc" "yuran_vpc" {
  cidr_block           = var.vpc_cidr_block
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = {
    Name = "yuranVPC"
  }
}

# 서브넷 리소스 정의
resource "aws_subnet" "yuran_subnet" {
  count = var.subnet_count
  vpc_id           = aws_vpc.yuran_vpc.id
  cidr_block       = cidrsubnet(var.vpc_cidr_block, 8, count.index)
  availability_zone = "ap-northeast-2a"
  tags = {
    Name = "yuranSubnet-${count.index + 1}"
  }
}

# EC2 인스턴스 및 SSH 연결 설정
resource "aws_instance" "yuran_instance" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t2.micro"
  subnet_id     = aws_subnet.yuran_subnet[0].id

  # 접속대상에 연결하기 위한 설정
  connection {
    user        = "ubuntu"
    type        = "ssh"
    private_key = file("yuran.pem")
    host        = self.public_ip
  }
}

# null_resource를 사용하여 원격 명령 실행
resource "null_resource" "echomyeip" {
  provisioner "remote-exec" {
    connection {
      user        = "ubuntu"
      type        = "ssh"
      private_key = file("yuran.pem")
      host        = aws_instance.yuran_instance.private_ip # private_ip로 변경
    }
    inline = [
      "sudo mkdir test-remote-exec"
    ]
  }
}

terraform_data

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

  input = "world"
}

[도전과제4]

terraform_data 리소스나 trigger_replace 를 사용한 테라폼 코드를 작성해보자!

# AWS provider 설정
provider "aws" {
  region = "ap-northeast-2"
}

# 최신 Ubuntu 20.04 LTS AMI ID
data "aws_ami" "ubuntu" {
  most_recent = true

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

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }

  owners = ["099720109477"]
}

# VPC CIDR 블록 정의
variable "vpc_cidr_block" {
  description = "CIDR block for the VPC"
  default     = "10.0.0.0/16"
}

# 서브넷 수 정의
variable "subnet_count" {
  description = "Number of subnets to create"
  default     = 3
}

# VPC 리소스 정의
resource "aws_vpc" "yuran_vpc" {
  cidr_block           = var.vpc_cidr_block
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = {
    Name = "yuranVPC"
  }
}

# 서브넷 리소스 정의
resource "aws_subnet" "yuran_subnet" {
  count = var.subnet_count
  vpc_id           = aws_vpc.yuran_vpc.id
  cidr_block       = cidrsubnet(var.vpc_cidr_block, 8, count.index)
  availability_zone = "ap-northeast-2a"
  tags = {
    Name = "yuranSubnet-${count.index + 1}"
  }
}

# EC2 인스턴스 및 SSH 연결 설정
resource "aws_instance" "yuran_instance" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t2.micro"
  subnet_id     = aws_subnet.yuran_subnet[0].id

  # 접속대상에 연결하기 위한 설정
  connection {
    user        = "ubuntu"
    type        = "ssh"
    private_key = file("yuran.pem")
    host        = self.public_ip
  }
}

# null_resource를 사용하여 원격 명령 실행
resource "null_resource" "echomyeip" {
  # trigger_replace로 subnet_id 변경 시 EC2 인스턴스 재생성
  triggers = {
    subnet_id = var.subnet_id
  }
  
  provisioner "remote-exec" {
    connection {
      user        = "ubuntu"
      type        = "ssh"
      private_key = file("yuran.pem")
      host        = aws_instance.yuran_instance.private_ip # private_ip로 변경
    }
    inline = [
      "sudo mkdir test-remote-exec"
    ]
  }
}

moved 블록

  • moved 블록
    • 테라폼의 State에 기록되는 리소스 주소의 이름이 변경되면 기존 리소스는 삭제되고 새로운 리소스가 생성됨을 앞서 설명에서 확인했다.
    • 하지만 테라폼 리소스를 선언하다 보면 이름을 변경해야 하는 상황이 발생하기도 하는데, 이때 리소스의 이름은 변경되지만 이미 테라폼으로 프로비저닝된 환경을 그대로 유지하고자 하는 경우 테라폼 1.1 버전부터 moved 블록을 사용할 수 있다.
    • ‘moved’라는 단어가 의미하는 것처럼 테라폼 State에서 옮겨진 대상의 이전 주소와 새 주소를 알리는 역할을 수행한다.
    • moved 블록 이전에는 State를 직접 편집하는 terraform state mv 명령을 사용하여 State를 건드려야 하는 부담이 있었다면, moved 블록은 State에 접근 권한이 없는 사용자라도 변경되는 주소를 리소스 영향 없이 반영할 수 있다.

[도전과제5]

moved 블록을 사용한 테라폼 코드 리팩터링을 수행해보세요

# AWS provider 설정
provider "aws" {
  region = "ap-northeast-2"
}

# 최신 Ubuntu 20.04 LTS AMI ID
data "aws_ami" "ubuntu" {
  most_recent = true

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

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }

  owners = ["099720109477"]
}

# VPC CIDR 블록 정의
variable "vpc_cidr_block" {
  description = "CIDR block for the VPC"
  default     = "10.0.0.0/16"
}

# 서브넷 수 정의
variable "subnet_count" {
  description = "Number of subnets to create"
  default     = 3
}

# VPC 리소스 정의
resource "aws_vpc" "yuran_vpc" {
  cidr_block           = var.vpc_cidr_block
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = {
    Name = "yuranVPC"
  }
}

# 서브넷 리소스 정의
resource "aws_subnet" "yuran_subnet" {
  count = var.subnet_count
  vpc_id           = aws_vpc.yuran_vpc.id
  cidr_block       = cidrsubnet(var.vpc_cidr_block, 8, count.index)
  availability_zone = "ap-northeast-2a"
  tags = {
    Name = "yuranSubnet-${count.index + 1}"
  }
}

# EC2 인스턴스 및 SSH 연결 설정
resource "aws_instance" "yuran_instance" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t2.micro"
  subnet_id     = aws_subnet.yuran_subnet[0].id

  # 접속대상에 연결하기 위한 설정
  connection {
    user        = "ubuntu"
    type        = "ssh"
    private_key = file("yuran.pem")
    host        = self.public_ip
  }
}

# null_resource를 사용하여 원격 명령 실행
resource "null_resource" "echomyeip" {
  triggers = {
    subnet_id = var.subnet_id
  }

  # moved 블록 사용하여 코드 이동
  moved {
    to = aws_instance.yuran_instance
  }
  
  provisioner "remote-exec" {
    connection {
      user        = "ubuntu"
      type        = "ssh"
      private_key = file("yuran.pem")
      host        = aws_instance.yuran_instance.private_ip # private_ip로 변경
    }
    inline = [
      "sudo mkdir test-remote-exec"
    ]
  }
}

댓글남기기