[t101-3기] Terraform 스터디 3주차
본 시리즈는 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"
]
}
}
댓글남기기