Tutorial
Terraform tutorial
테라폼 시작하기
테라폼을 시작하기에 앞서 해당 챕터에서는 이런 내용을 다룹니다.
"테라폼 구성을 작성하는 방법"
간단하게 AWS VPC를 생성하는 테라폼 구성으로 시작하여 해당 구성을 개선합니다.
테라폼 버전은 1.9.5 버전을 이용 하며 설치 방법과 문법을 해당 챕터에서 다루게 됩니다.
Install with Mac OS
테라폼을 설치하기 위해 테라폼을 직접적으로 설치할 수 있지만 테라폼의 다양한 버전을 관리할 수 있는 오픈소스를 활용합니다.
brew install tfenv
tfenv install 1.9.5
tfenv use 1.9.5
Optional
JetBrains IDE Plugin Install
Plugins - "Terraform and HCL" Install
Visual Studio Code Plugin Install
recommend 👍
Plugins - "Hashi Corp Terraform"
Editor - Auto save to formatter
Git repository ignore
"intellij", "Terraform"
Pre Setting
AWS VPC 를 생성해야 하기 때문에 권한이 있는(자격 증명) AWS 인증 정보가 필요합니다.
export AWS_ACCESS_KEY_ID={secret}
export AWS_SECRET_ACCESS_KEY={secret}
Terraform Config
테라폼 구성을 작성하기 위해 알아야 할 파일 구성과 워크 플로우 참고
https://developer.hashicorp.com/terraform/language
Files

Workflows

Basic Terraform Structure
How to use HCL?
Re use resources by variables
"변수 값은 어디에 저장 할까?"
다양한 방법 중 가장 대표적으로 사용 되는 방법인 "환경변수
", "var
" 등을 사용하게 되면 코드로 남지 않는 단점이 존재한다.
변수를 관리할 수 있는 파일인 terraform.tfvars
을 생성 하여 해당 파일에서 관리할 수 있도록 구성한다.
resource "aws_vpc" "main" {
cidr_block = var.cidr_block
tags = {
Name = var.vpc_name
}
}
Reference other resources
output "vpc_id" {
value = aws_vpc.main.id
}
Resoucre Dependency

VPC 의 기본 골조를 갖췄으니 인터넷 망과 통신할 수 있는 IGW를 생성 하면서 사전에 만들어진 VPC가 먼저 생성 되어 있어야 하는 상황에서 의존성을 기반으로 리소스를 생성한다.
resource "aws_vpc" "main" {
cidr_block = var.cidr_block
tags = {
Name = var.vpc_name
}
}
"암시적 의존성 vs 명시적 의존성"
resource "aws_vpc" "main" {
cidr_block = var.cidr_block
tags = {
Name = var.vpc_name
}
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.vpc_name}=igw"
}
}
학습한 의존성을 바탕으로 VPC 대역에대 할당할 수 있는 IP 서브넷팅을 위한 외부망 서브넷을 생성 한다.
ap-northeast-2a
10.0.1.0/24
ap-northeast-2b
10.0.2.0/24
resource "aws_vpc" "main" {
cidr_block = var.cidr_block
tags = {
Name = var.vpc_name
}
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.vpc_name}-igw"
}
}
resource "aws_subnet" "public_a" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
availability_zone = "ap-northeast-2a"
map_public_ip_on_launch = true
tags = {
Name = "${var.vpc_name}-public-subnet-a"
}
}
resource "aws_subnet" "public_b" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.2.0/24"
availability_zone = "ap-northeast-2b"
map_public_ip_on_launch = true
tags = {
Name = "${var.vpc_name}-public-subnet-b"
}
}
Use loop syntax
"비슷한 코드에서 살짝만 다른 리소스들 어떻게 편리하게 생성할 수 없을까?"

동일한 리소스에서 가용 영역만 다른 두 서브넷이 있다. 만약 4개, 8개 등 더 많은 서브넷을 생성 해야 한다면 main.tf
가 굉장히 길어지기 때문에 이런 경우 반복문을 활용하면 쉽게 리소스를 정의할 수 있다.
"변수와 Count 지시자를 활용한 반복문 사용 방법"
resource "aws_subnet" "public_a" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
availability_zone = "ap-northeast-2a"
map_public_ip_on_launch = true
tags = {
Name = "${var.vpc_name}-public-subnet-a"
}
}
resource "aws_subnet" "public_b" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.2.0/24"
availability_zone = "ap-northeast-2b"
map_public_ip_on_launch = true
tags = {
Name = "${var.vpc_name}-public-subnet-b"
}
}
이렇게 작성한 리소스를 AWS에 적용 하기 위해 terraform plan
을 입력하는 순간 테라폼은 별도의 리소스로 확인 하고 삭제 및 생성을 한다. 기존과 같았으면 "No Changes"가 나와야 하지만 그렇지 않다는 것을 아래 이미지로 확인할 수 있다.

테라폼 구성 리팩터링 과정에서 발생할 수 있는 가장 흔한 이슈 중 하나인데, 이는 똑같은 리소스 코드를 정의 했지만 위 이미지 처럼 삭제하고 다시 생성하는 것이다.
State file control
"테라폼 구성을 변경할 때 같은 리소스이지만 자꾸 삭제 후 생성 하는데 어떻게 해야할까?"
"왜, 위와 같이 같은 리소스를 정의 했지만 삭제 후 재생성이 되었을까?" 이 질문에 대한 답을 찾기 쉬운 방법은 Plan에서 삭제 되는 출력과 생성 되는 출력을 1차적으로 비교해 볼 수 있다. 그럼 이와 같은 결과가 나오게 된다.

이렇게 바뀐 이유는 서브넷을 생성 하는 코드에서 명시적으로 public_a
, public_b
를 정의 했지만 리팩터링 단계에서 이를 반복문으로 교체하며 배열의 인덱스로 참조 했기 때문이다. 관련 코드는 해당 페이지의 "Use loop syntax" 의 코드 블럭을 확인 해보면 된다.
"어떻게 해결할까?"
테라폼은 기본적으로 상태파일을 관리하며 이 상태에 따라 프로바이더에 적용할 지 계획을 세우게 되는데, 그 상태를 관리할 수 있는 명령어를 통해 문제를 해결할 수 있다.
Command
terraform state mv aws_subnet.public_a aws_subnet.public\[0\]
terraform state mv aws_subnet.public_b aws_subnet.public\[1\]
위 커맨드를 입력한 후 다시 terraform plan을 입력 해보면 "No changes"가 나오는 확인할 수 있는데 결정적으로 리팩터링 과정에서 리소스의 명칭이 변경 된다면 이와 같은 이슈를 만날 수 있다는 것을 꼭 인지 해야한다.
이 이슈를 마주했을 땐 상태 파일을 제어 하여 해결할 수 있다는 것을 기억하고 Plan 출력을 무시하지 말아야한다.
terraform state list

terraform state show aws_subnet.public_a

추가적인 옵션 알아보기
"테라폼 상태에서 삭제하기"
terraform state rm
더이상 테라폼을 사용해서 리소스를 관리하지 않을 때, 이는 리소스를 제거하는 것이 아닌 상태 파일에서 제거 하는 것이기 때문에 더 이상 Plan 에서 changed를 관리 하고 싶지 않을 때 사용한다.
terraform state rm aws_subnet.public\[1\]

"테라폼 상태로 가져오기"
terraform import
리소스를 테라폼을 사용해 관리하기 위할 때, 이는 기존 콘솔에서 관리하던 리소스를 테라폼으로 관리해야 할 때 사용한다.
리소스를 가져올 때 입력해야 하는 값이 리소스 별로 다르기 때문에 항상 공식문서에서 어떤 값이 필요한지 확인 해보고 입력 해야한다. 서브넷 가져오기 공식문서
terraform import aws_subnet.public\[1\] subnet-05dba3eb205ad6dd6

Data types
"복잡한 타입의 변수를 사용 해서 효율적으로 리소스를 관리하고 싶은데?"
string
string
number
integer
bool
true / false
list
arrays
set
unique value in array
map/object
key:value
HCL도 코드의 영역이기 때문에 가독성을 향상 시키는 방법 중 다양한 자료구조를 활용해서 알고리즘과 같은 형식을 단순 변수 사용으로 변경 하는 과정
list(object) type
순서가 매우 중요한 자료형
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
count = length(var.public_subnets)
cidr_block = var.public_subnets[count.index].cidr_block
availability_zone = var.public_subnets[count.index].availability_zone
map_public_ip_on_launch = true
tags = {
Name = var.public_subnets[count.index].name
}
}
List type의 치명적인 단점
아래와 같이 서브넷의 순서가 변경 되었다면 테라폼은 변경이 있음을 알아 차리고 Plan
에서 삭제 후 재생성이 발생한다.
이유는 0번 인덱스에 있는 리소스가 "b"로 변경 되었기 때문에 상태 파일과 달라서 발생하는 상황이다.
public_subnets = [
{
name = "oimarket-apne2-public-subnet-b"
availability_zone = "ap-northeast-2b"
cidr_block = "10.0.2.0/24"
},
{
name = "oimarket-apne2-public-subnet-a"
availability_zone = "ap-northeast-2a"
cidr_block = "10.0.1.0/24"
}
]
List
의 치명적 단점으로 중요한 리소스는 List
가 아닌 Map
으로 관리하는 것이 안정적이다.
map(object) type
count 반복 형식에서 for_each 반복 형식을 활용
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
for_each = var.public_subnets
cidr_block = each.value.cidr_block
availability_zone = each.value.availability_zone
map_public_ip_on_launch = true
tags = {
Name = each.key
}
}
코드를 수정하고 다시 한 번 terraform plan을 입력 해보면 삭제 후 재생성 하는 이슈를 다시 만날 수 있다.
이에 대한 이유는 기존 List자료형은 상태 파일을 저장할 때 aws_subnet_public[0]
이런식으로 인덱스를 기반으로 해서 상태파일이 저장 되었었다.
하지만, Map은 Key를 기준으로 상태파일을 생성하기 때문에 aws_subnet.public["oimarket-apne2-public-subnet-a"]
로 변경 되어 리소스를 삭제 후 재생성한다.
이러한 이슈를 해결하는 방법을 우린 위에서 이미 배웠기 때문에 당황하지 않고 상태를 관리하면 된다.
terraform state mv aws_subnet.public\[0\] aws_subnet.public\[\"oimarket-apne2-public-subnet-a\"\]
terraform state mv aws_subnet.public\[1\] aws_subnet.public\[\"oimarket-apne2-public-subnet-b\"\]
상태파일을 변경했다면 Plan을 확인 해보면 성공적으로 리팩터링이 완료된 것을 알 수 있다.

"그렇다면 변수에 선언한 값의 순서를 바꿔도 동일할까?"
public_subnets = {
"oimarket-apne2-public-subnet-b" = {
availability_zone = "ap-northeast-2b"
cidr_block = "10.0.2.0/24"
},
"oimarket-apne2-public-subnet-a" = {
availability_zone = "ap-northeast-2a"
cidr_block = "10.0.1.0/24"
}
}
💡 당연히 Map[object]
의 Key 값으로 접근하기 때문에 동일하다.
Conditional
"조건에 따라 다른 리소스를 생성할 수 없을까?"
매번 리소스를 정의할 때 한가지 상황만 가지고 정의할 수 없다. 만약, 테라폼 구성을 할 때 "개발" 환경과 "운영" 환경의 차이가 있다면 어떻게 구성을 분리하여 리소스를 정의할 수 있을지 고민하게 된다.
그럴 때 프로그래밍 단계에서의 꽃이라고 불리울 수 있는 조건을 이용한 리소스 정의 방식을 이용한다.
NAT Gateway를 조건문으로 추가하는 시나리오 만들어보기

실제 NAT Gateway를 생성 하는 시점 이후 부터 비용이 발생한다. 그렇기 때문에 프로비저닝 하지 않고 시나리오를 통해 어떤 방식으로 조건문을 활용하는지 알아보는 방식으로 학습한다.

조건문 사용 방법
condition ? true : false
resource "aws_instance" "example" {
instance_type = var.environment == "prod" ? "t3.medium" : "t3.micro"
ami = "ami-example12345678"
}
NAT Gateway를 위 조건문을 활용해서 한 번 만들어본다면 이렇게 만들어볼 수 있다.

여기서 잠깐!
"Map(object) type 을 접근할 때 인덱스로 접근할 수 없을까?" 🤔
서브넷 아이디는 Map(object)이기 때문에 인덱스로 접근할 수 없다. 그렇다면 어떻게 NAT Gateway가 인터넷망으로 통신 할 수 있도록 외부망 서브넷과 연결할 수 있을까? 아래 테라폼 공식문서에서 제시하는 방향대로 특정 함수를 이용하면 된다.
Code
resource "aws_subnet" "private" {
for_each = var.private_subnets
vpc_id = aws_vpc.main.id
cidr_block = each.value.cidr_block
availability_zone = each.value.availability_zone
map_public_ip_on_launch = false
tags = {
Name = each.key
}
}
resource "aws_eip" "nat_ips" {
count = var.private_subnets != {} ? length(var.private_subnets) : 0
}
resource "aws_nat_gateway" "gateways" {
count = var.private_subnets != {} ? length(var.private_subnets) : 0
allocation_id = aws_eip.nat_ips[count.index].id
subnet_id = tolist(values(aws_subnet.public))[count.index].id
}
내부망에 정의된 서브넷이 없다면 NAT Gateway를 생성 하지 않고 있다면 생성하는 내용이다. 이렇게 특정 상황일 때 조건문을 통해 리소스를 정의하면 테라폼 구성을 관리하기 편리하다.
위에 있듯이 NAT Gateway를 사용하기 위해서 공용 IP가 필요하다. NAT가 외부망으로 나가기 위해 공용 IP로 나가야 하며, 이 때 SNAT를 통해 나가기 때문이다.
위 코드를 작성 했다면 Plan
을 살펴보자. 그렇다면 생성 항목이 6개가 나온다면 정상이다. 하지만, 이 코드를 AWS에 반영하여 프로비저닝 하진 않고 실행 계획에서 어떤 리소스가 생성 되는지만 살펴볼 것이다.
Last updated