테라폼 시작하기
테라폼을 시작하기에 앞서 해당 챕터에서는 이런 내용을 다룹니다.
"테라폼 구성을 작성하는 방법"
간단하게 AWS VPC를 생성하는 테라폼 구성으로 시작하여 해당 구성을 개선합니다.
테라폼 버전은 1.9.5 버전 을 이용 하며 설치 방법과 문법을 해당 챕터에서 다루게 됩니다.
Install with Mac OS
테라폼을 설치하기 위해 테라폼을 직접적으로 설치할 수 있지만 테라폼의 다양한 버전을 관리할 수 있는 오픈소스를 활용합니다.
Copy 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
Pre Setting
AWS VPC 를 생성해야 하기 때문에 권한이 있는(자격 증명) AWS 인증 정보가 필요합니다.
Copy export AWS_ACCESS_KEY_ID={secret}
export AWS_SECRET_ACCESS_KEY={secret}
테라폼 구성을 작성하기 위해 알아야 할 파일 구성과 워크 플로우 참고
Files
Workflows
Summary테라폼 구성은 주로 providers.tf
, backend.tf
, main.tf
, outputs.tf
, variables.tf
등으로 구성
providers.tf
: 인프라 리소스가 구성될 환경(AWS, GCP, 쿠버네티스 등)을 정의
main.tf
: 구성하고자 하는 인프라 리소스를 정의
backend.tf
: 구성된 인프라 리소스의 상태 저장 방법(로컬, S3, GCS 등)을 정의
테라폼 워크 플로우는 terraform init
, terraform plan
, terraform apply
로 구성
terraform init
: 프로바이더의 코드와 백엔드 환경 구성
terraform plan
: 테라폼에 정의된 리소스의 상태를 바탕으로 어떤 변경사항들이 발생할지 정보 제공
terraform apply
: 테라폼에 정의된 리소스의 상태를 실제 인프라에 반영
AWS VPC 기본 구성하기
providers.tf main.tf
Copy provider "aws" {
region = "ap-northeast-2"
}
terraform {
required_version = "= 1.9.5"
}
Copy resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
tags = {
Name = "oimarket-apne2"
}
}
위 이론에서 학습 했듯이 AWS 프로바이더를 어디로 설정 하는지, 상태를 어디에 저장할 것인지 초기 작업을 진행 하고 테라폼이 수행 되기 위한 숨김 파일을 생성한다.
AWS ACCESS KEY, SECRET ACCESS KEY를 환경 변수에 등록 되어 있다면 위 처럼 정상적으로 어떤 리소스를 생성할 것인지 또는 어떤 작업을 수행하는지에 대한 내용이 나온다.
AWS Console
Apply로 변경사항을 적용 했다면 콘솔에서 아래와 같이 생성된 VPC를 확인할 수 있다.
Hands-On Summaryproviders.tf
파일에 인프라 리소스가 구성 될 환경을 정의한다.
main.tf
파일에 구성 하고자 하는 인프라 리소스를 정의 한다.
테라폼 워크플로우인 terraform init
-> terraform plan
-> terraofrm apply
의 순서대로 진행한다.
How to use HCL?
Re use resources by variables
다양한 방법 중 가장 대표적으로 사용 되는 방법인 "환경변수
", "var
" 등을 사용하게 되면 코드로 남지 않는 단점이 존재한다.
변수를 관리할 수 있는 파일인 terraform.tfvars
을 생성 하여 해당 파일에서 관리할 수 있도록 구성한다.
main.tf variables.tf terraform.tfvars
Copy resource "aws_vpc" "main" {
cidr_block = var.cidr_block
tags = {
Name = var.vpc_name
}
}
Copy variable "cidr_block" {
description = "The cidr block of vpc"
}
variable "vpc_name" {
description = "The name of vpc"
}
Hands-On
Reference other resources
"미리 정의한 리소스들의 정보를 재사용할 수 없을까?"
outputs.tf
Copy output "vpc_id" {
value = aws_vpc.main.id
}
Hands-On Summaryoutput 블록을 통해서 출력을 정의할 수 있다.
출력값은 향후 서로 다른 테라폼 구성 간 값을 참조할 때 유용하게 사용할 수 있다.
Resoucre Dependency
VPC 의 기본 골조를 갖췄으니 인터넷 망과 통신할 수 있는 IGW를 생성 하면서 사전에 만들어진 VPC가 먼저 생성 되어 있어야 하는 상황에서 의존성을 기반으로 리소스를 생성한다.
AS-IS(main.tf) TO-BE(main.tf)
Copy resource "aws_vpc" "main" {
cidr_block = var.cidr_block
tags = {
Name = var.vpc_name
}
}
Copy 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"
}
}
"암시적 의존성 vs 명시적 의존성"
암시적 의존성 명시적 의존성
Copy 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"
}
}
Copy 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
depends_on = [aws_vpc.main]
tags = {
Name = "${var.vpc_name}=igw"
}
}
Hands-On
학습한 의존성을 바탕으로 VPC 대역에대 할당할 수 있는 IP 서브넷팅을 위한 외부망 서브넷을 생성 한다.
main.tf
Copy 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"
}
}
Hands-On Summary서로 다른 리소스를 참조 하며 먼저 생성 된 후 리소스를 만들어야 하는 의존성을 관리할 수 있다.
의존성은 대표적으로 암묵적과 명시적 의존성이 있으며 현업에서 암묵적 의존성 방식을 선호한다.
Use loop syntax
"비슷한 코드에서 살짝만 다른 리소스들 어떻게 편리하게 생성할 수 없을까?"
동일한 리소스에서 가용 영역만 다른 두 서브넷이 있다. 만약 4개, 8개 등 더 많은 서브넷을 생성 해야 한다면 main.tf
가 굉장히 길어지기 때문에 이런 경우 반복문을 활용하면 쉽게 리소스를 정의할 수 있다.
"변수와 Count 지시자를 활용한 반복문 사용 방법"
AS-IS(main.tf) TO-BE(main.tf) TO-BE(variables.tf) TO-BE(terraform.tfvars)
Copy
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"
}
}
Copy resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
count = length(var.availability_zones)
cidr_block = cidrsubnet(var.cidr_block, 8, count.index + 1)
availability_zone = "ap-northeast-2${var.availability_zones[count.index]}"
map_public_ip_on_launch = true
tags = {
Name = "${var.vpc_name}-public-subnet-${var.availability_zones[count.index]}"
}
}
Copy variable "cidr_block" {
description = "The cidr block of vpc"
}
variable "vpc_name" {
description = "The name of vpc"
}
variable "availability_zones" {
type = list(string)
description = "az of subnets"
}
Hands-On
이렇게 작성한 리소스를 AWS에 적용 하기 위해 terraform plan
을 입력하는 순간 테라폼은 별도의 리소스로 확인 하고 삭제 및 생성을 한다. 기존과 같았으면 "No Changes"가 나와야 하지만 그렇지 않다는 것을 아래 이미지로 확인할 수 있다.
테라폼 구성 리팩터링 과정에서 발생할 수 있는 가장 흔한 이슈 중 하나인데, 이는 똑같은 리소스 코드를 정의 했지만 위 이미지 처럼 삭제하고 다시 생성하는 것이다.
Summarycount 지시자를 사용해서 반복문을 사용할 수 있다.
반복문을 사용하면 테라폼 구성을 더 효율적으로 만들 수 있다.
State file control
"테라폼 구성을 변경할 때 같은 리소스이지만 자꾸 삭제 후 생성 하는데 어떻게 해야할까?"
"왜, 위와 같이 같은 리소스를 정의 했지만 삭제 후 재생성이 되었을까?" 이 질문에 대한 답을 찾기 쉬운 방법은 Plan에서 삭제 되는 출력과 생성 되는 출력을 1차적으로 비교해 볼 수 있다. 그럼 이와 같은 결과가 나오게 된다.
Console"어떻게 해결할까?"
테라폼은 기본적으로 상태파일을 관리하며 이 상태에 따라 프로바이더에 적용할 지 계획을 세우게 되는데, 그 상태를 관리할 수 있는 명령어를 통해 문제를 해결할 수 있다.
Command
Copy 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 출력을 무시하지 말아야한다.
AS-IS TO-BE
terraform state list
terraform state show aws_subnet.public_a
terraform state plan
terraform state list
Hands-On
추가적인 옵션 알아보기
"테라폼 상태에서 삭제하기"
terraform state rm
더이상 테라폼을 사용해서 리소스를 관리하지 않을 때, 이는 리소스를 제거하는 것이 아닌 상태 파일에서 제거 하는 것이기 때문에 더 이상 Plan 에서 changed를 관리 하고 싶지 않을 때 사용한다.
Copy terraform state rm aws_subnet.public\[1\]
"테라폼 상태로 가져오기"
terraform import
리소스를 테라폼을 사용해 관리하기 위할 때, 이는 기존 콘솔에서 관리하던 리소스를 테라폼으로 관리해야 할 때 사용한다.
Copy terraform import aws_subnet.public\[1\] subnet-05dba3eb205ad6dd6
Hands-On Summaryterraform state
명령을 사용해서 테라폼 상태파일을 조작할 수 있다.
terraform state mv
명령을 사용해서 리소스 상태를 이전할 수 있다.
코드 구성을 리팩터링 하면서 테라폼 구성 내에서 이름이 바뀌는 경우 많이 사용된다
terraform state rm
명령을 사용해서 더이상 테라폼으로 리소스를 관리하지 않게 제거할 수 있다.
terraform import
명령을 사용해서 리소스의 상태 정보를 테라폼 상태파일로 가져올 수 있다.
Data types
"복잡한 타입의 변수를 사용 해서 효율적으로 리소스를 관리하고 싶은데?"
HCL도 코드의 영역이기 때문에 가독성을 향상 시키는 방법 중 다양한 자료구조를 활용해서 알고리즘과 같은 형식을 단순 변수 사용으로 변경 하는 과정
list(object) type
main.tf variables.tf terraform.tfvars
Copy 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
}
}
Copy variable "public_subnets" {
type = list(object({
name = string
availability_zone = string
cidr_block = string
}))
}
Hands-On
List type의 치명적인 단점
아래와 같이 서브넷의 순서가 변경 되었다면 테라폼은 변경이 있음을 알아 차리고 Plan
에서 삭제 후 재생성이 발생한다.
이유는 0번 인덱스에 있는 리소스가 "b"로 변경 되었기 때문에 상태 파일과 달라서 발생하는 상황이다.
Copy 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 반복 형식을 활용
main.tf variables.tf terraform.tfvars
Copy 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
}
}
Copy variable "public_subnets" {
type = map(object({
availability_zone = string
cidr_block = string
}))
}
코드를 수정하고 다시 한 번 terraform plan을 입력 해보면 삭제 후 재생성 하는 이슈를 다시 만날 수 있다.
이에 대한 이유는 기존 List자료형은 상태 파일을 저장할 때 aws_subnet_public[0]
이런식으로 인덱스를 기반으로 해서 상태파일이 저장 되었었다.
하지만, Map은 Key를 기준으로 상태파일을 생성하기 때문에 aws_subnet.public["oimarket-apne2-public-subnet-a"]
로 변경 되어 리소스를 삭제 후 재생성한다.
이러한 이슈를 해결하는 방법을 우린 위에서 이미 배웠기 때문에 당황하지 않고 상태를 관리하면 된다.
Copy 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 을 확인 해보면 성공적으로 리팩터링이 완료된 것을 알 수 있다.
"그렇다면 변수에 선언한 값의 순서를 바꿔도 동일할까?"
Copy 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"
}
}
Hands-On Summary테라폼은 string, number, bool, list, set, map 등 다양한 타입의 변수를 지원한다
list(object) 는 반복문을 사용할 때 count
로 사용하며 인덱스로 접근하기 때문에 순서가 매우 중요하다
map(object) 는 반복문을 사용할 때 for_each
로 사용하며 순서에 영향을 받지 않는다
Conditional
"조건에 따라 다른 리소스를 생성할 수 없을까?"
매번 리소스를 정의할 때 한가지 상황만 가지고 정의할 수 없다. 만약, 테라폼 구성을 할 때 "개발" 환경과 "운영" 환경의 차이가 있다면 어떻게 구성을 분리하여 리소스를 정의할 수 있을지 고민하게 된다.
그럴 때 프로그래밍 단계에서의 꽃이라고 불리울 수 있는 조건을 이용한 리소스 정의 방식을 이용한다.
NAT Gateway를 조건문으로 추가하는 시나리오 만들어보기
실제 NAT Gateway를 생성 하는 시점 이후 부터 비용이 발생한다. 그렇기 때문에 프로비저닝 하지 않고 시나리오를 통해 어떤 방식으로 조건문을 활용하는지 알아보는 방식으로 학습한다.
조건문 사용 방법
condition ? true : false
example
Copy 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
main.tf variables.tf terraform.tfvars
Copy 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
}
Copy variable "private_subnets" {
type = map(object({
availability_zone = string
cidr_block = string
}))
}
내부망에 정의된 서브넷이 없다면 NAT Gateway를 생성 하지 않고 있다면 생성하는 내용이다. 이렇게 특정 상황일 때 조건문을 통해 리소스를 정의하면 테라폼 구성을 관리하기 편리하다.
위에 있듯이 NAT Gateway를 사용하기 위해서 공용 IP가 필요하다. NAT가 외부망으로 나가기 위해 공용 IP로 나가야 하며, 이 때 SNAT를 통해 나가기 때문이다.
위 코드를 작성 했다면 Plan
을 살펴보자. 그렇다면 생성 항목이 6개가 나온다면 정상이다. 하지만, 이 코드를 AWS에 반영하여 프로비저닝 하진 않고 실행 계획에서 어떤 리소스가 생성 되는지만 살펴볼 것이다.
Summary테라폼에서 조건문은 condition ? true : false
형태로 사용할 수 있다.