들어가며
* 본 스터디의 자료는 아래의 책을 기반으로 합니다.
테라폼으로 시작하는 IaC | 김민수, 김재준, 이규석, 이유종 | 링크
6월인데 대구는 많이 덥습니다..
스터디 멤버 분들과 블로그를 찾아주시는 분들 모두 쾌적한 한 주를 보내셨기 바랍니다.
이번 주차에는 테라폼 기초 사용법 중 데이터 소스, 입력 변수, 지역 값, 출력, 반복문에 대해 스터디를 진행했습니다.
데이터 소스
데이터 소스는 테라폼으로 정의되지 않은 외부의 리소스 또는 저장된 정보를 테라폼 내에서 참조하고 싶을 때 사용하는 블록입니다.
데이터 소스를 정의하는 방식은 다음 포맷과 같습니다.
# 블록 정의시
data "<리소스 유형>" "<이름>" {
<인수> = <값>
}
# 참조시 접근법
data.<리소스 유형>.<이름>.<속성>
활용 예시는 다음과 같습니다.
data "aws_availability_zones" "seoul" {
state = "available"
}
입력 변수 Variable
입력 변수는 인프라 구성시 필요한 속성값을 정의하여 코드의 변경 없이도 여러 인프라를 생성하는 용도로 사용합니다.
테라폼에서는 입력변수를 Input Variables를 이용하여 정의합니다. 입력 변수 정의 방식은 아래와 같습니다.
# variable 선언 포맷
variable "<이름>" {
<인수> = <값>
}
variable "image_id" {
type = string
}
* 주의사항: 테라폼 예악 변수들은 변수 <이름>으로 사용할 수 없습니다. (source, version, providers 등..)
입력 변수는 다양하게 넣을 수 있기 때문에 메타인수를 통해 validation을 수행할 수 있습니다.
변수 정의 시 사용 가능한 메타인수는 다음과 같습니다.
- default : 기본값 설정
- type : 변수 값 유형 정의
- string : 글자 유형
- number : 숫자 유형
- bool : true 또는 false
- any : 명시적으로 모든 유형이 허용됨을 표시함
- list : 인덱스 기반 집합 (배열 느낌)
- map : key, value 구성, 키값 기준 정렬
- set : 값 기반 집합으로, 정렬 키값 기준 정렬
- object : {key=value, key2=value2,... }
- tuple: [<유형>, <유형 2>,... ]
- description : 입력 변수 설명
- validation : 변수 유효성 검사 규칙을 정의
- sensitive : 민감한 변수 값임을 알리고 값 노출 제한
- nullable : 변수에 값이 없어도 됨을 지정
코드 예시는 다음과 같습니다.
variable "string" {
type = string
description = "Devlos"
default = "devlos"
}
resource "local_file" devlos" {
content = var.devlos
filename = "${path.module}/devlos.txt"
}
variable "number" {
type = number
default = 4
validation {
condition = length(var.number) <= 4
error_message = "The number value should less than 4."
}
}
variable "boolean" {
default = true
}
variable "list" {
default = [
"PKOS",
"AWES",
"T101-3"
"T101-4"
]
}
variable "map" {
default = {
pkos = "Kubernetes-kops",
awes = "kubernetes-EKS",
t101 = "Terraform"
}
}
variable "set" {
type = set(string)
default = [
"pkos",
"awes",
"t101"
]
}
variable "object" {
type = object({ name = string, age = number})
default = {
name = "devlos",
age = 20
}
sensitive = true
}
variable "tuple" {
type = tuple([string, number, bool])
default = ["abc", 123, true]
}
# list에 optional 설정 가능
variable "ingress_rules" {
type = list(object({
port = number,
description = optional(string),
protocol = optional(string, "tcp")
}))
default = [
{
port = 80, description = "web"
}, {
port = 53, protocol = "udp"
}
]
}
terraform init && terraform plan && terraform apply -auto-approve
output "list_index_0" {
value = var.list.0
}
output "list_all" {
value = [
for name in var.list : upper(name)
]
}
실행결과는 다음과 같습니다.
실습 1 - VPC + 보안그룹 + EC2 배포 [ + 도전과제 2 : 리소스 이름을 닉네임으로 변경 후 배포 해보기]
해당 실습은 vpc와 subnet, routing table, ec2 등을 테라폼으로 생성하는 예제입니다.
provider "aws" {
region = "ap-northeast-2"
}
resource "aws_vpc" "devlos_vpc" {
cidr_block = "10.10.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "t101-study"
}
}
resource "aws_subnet" "devlos_subnet1" {
vpc_id = aws_vpc.devlos_vpc.id
cidr_block = "10.10.1.0/24"
availability_zone = "ap-northeast-2a"
tags = {
Name = "devlos-subnet1"
}
}
resource "aws_subnet" "devlos_subnet2" {
vpc_id = aws_vpc.devlos_vpc.id
cidr_block = "10.10.2.0/24"
availability_zone = "ap-northeast-2c"
tags = {
Name = "devlos-subnet2"
}
}
resource "aws_internet_gateway" "devlos_igw" {
vpc_id = aws_vpc.devlos_vpc.id
tags = {
Name = "devlos-igw"
}
}
resource "aws_route_table" "devlos_rt" {
vpc_id = aws_vpc.devlos_vpc.id
tags = {
Name = "devlos-rt"
}
}
resource "aws_route_table_association" "devlos_rtassociation1" {
subnet_id = aws_subnet.devlos_subnet1.id
route_table_id = aws_route_table.devlos_rt.id
}
resource "aws_route_table_association" "devlos_rtassociation2" {
subnet_id = aws_subnet.mysubnet2.id
route_table_id = aws_route_table.devlos_rt.id
}
resource "aws_route" "mydefaultroute" {
route_table_id = aws_route_table.devlos_rt.id
destination_cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.devlos_igw.id
}
output "aws_vpc_id" {
value = aws_vpc.devlos_vpc.id
}
resource "aws_security_group" "mysg" {
vpc_id = aws_vpc.devlos_vpc.id
name = "T101 SG"
description = "T101 Study SG"
}
resource "aws_security_group_rule" "mysginbound" {
type = "ingress"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
security_group_id = aws_security_group.mysg.id
}
resource "aws_security_group_rule" "mysgoutbound" {
type = "egress"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
security_group_id = aws_security_group.mysg.id
}
output "aws_security_group_id" {
value = aws_security_group.mysg.id
}
data "aws_ami" "my_amazonlinux2" {
most_recent = true
filter {
name = "owner-alias"
values = ["amazon"]
}
filter {
name = "name"
values = ["amzn2-ami-hvm-*-x86_64-ebs"]
}
owners = ["amazon"]
}
resource "aws_instance" "myec2" {
depends_on = [
aws_internet_gateway.devlos_igw
]
ami = data.aws_ami.my_amazonlinux2.id
associate_public_ip_address = true
instance_type = "t2.micro"
vpc_security_group_ids = ["${aws_security_group.mysg.id}"]
subnet_id = aws_subnet.mysubnet1.id
user_data = <<-EOF
#!/bin/bash
wget https://busybox.net/downloads/binaries/1.31.0-defconfig-multiarch-musl/busybox-x86_64
mv busybox-x86_64 busybox
chmod +x busybox
RZAZ=$(curl http://169.254.169.254/latest/meta-data/placement/availability-zone-id)
IID=$(curl 169.254.169.254/latest/meta-data/instance-id)
LIP=$(curl 169.254.169.254/latest/meta-data/local-ipv4)
echo "<h1>RegionAz($RZAZ) : Instance ID($IID) : Private IP($LIP) : Web Server</h1>" > index.html
nohup ./busybox httpd -f -p 80 &
EOF
user_data_replace_on_change = true
tags = {
Name = "t101-myec2"
}
}
output "myec2_public_ip" {
value = aws_instance.myec2.public_ip
description = "The public IP of the Instance"
}
terraform plan && terraform apply -auto-approve
terraform state list
# 라우팅 테이블 확인
aws ec2 describe-route-tables --filters 'Name=tag:Name,Values=devlos-rt' --output table
# graph 확인 > graph.dot 파일 선택 후 오른쪽 상단 DOT 클릭
terraform graph > graph.dot
# 리소스 삭제
terraform destroy -auto-approve
리소스 생성 후 terraform state list를 활용하여 생성 결과를 확인합니다.
라우팅 테이블이 정상적으로 등록되었는지도 확인합니다.
그래프를 확인해 보면 복잡하지만 닉네임 기반 네이밍을 통해 생성된 리소스들의 종속성을 확인할 수 있습니다.
local 지역 값
local 지역 값은 코드 내에서 사용자가 지정한 값이나 속성 값을 선언하여 사용하는 것을 말합니다. 선언된 모듈 내에서만 접근할 수 있고, 실행 시 입력받을 수 없습니다. 하지만 타 언어의 local 변수처럼 빈번하게 사용될 경우 추적이 어려워져 유지관리하기가 어려워서 주의하여 사용해야 합니다.
local 지역값은 다음과 같이 사용합니다.
variable "prefix" {
default = "hello"
}
locals {
name = "devlos"
content = "${var.prefix} ${local.name}" #선언된 local 값을 참조할 수 있음
my_info = {
age = 20
region = "KR"
}
my_nums = [1, 2, 3, 4, 5]
}
# 중복 선언하면 오류가 발생
# locals {
# content = "content2"
# }
terraform init
terraform apply -auto-approve
terraform state list
locals를 중복생성하였을 때, 아래와 같은 오류가 발생합니다.
실습 2 - AWS IAM User 생성
local 지역값을 선언하여 리소스 생성에 활용합니다.
provider "aws" {
region = "ap-northeast-2"
}
locals {
name = "mytest"
team = {
group = "dev"
}
}
resource "aws_iam_user" "myiamuser1" {
name = "${local.name}1" #local 지역값 참조
tags = local.team
}
resource "aws_iam_user" "myiamuser2" {
name = "${local.name}2" #local 지역값 참조
tags = local.team
}
terraform init && terraform apply -auto-approve
terraform state list
terraform state show aws_iam_user.myiamuser1
terraform state show aws_iam_user.myiamuser2
#참조 그래프 확인
terraform graph > graph.dot
#리소스 삭제
terraform destroy -auto-approve
생성된 리소스를 확인해 보면 다음과 같습니다.
그래프를 확인해 보면 다음과 같이 동일한 계층의 사용자가 생성된 것을 확인할 수 있습니다.
출력 output
output은 테라폼 코드 프로비저닝 수행 후에 결과 속성값을 확인하는 용도로 사용합니다. 또한 Java의 Getter처럼 다른 모듈이나 워크스페이스 간의 데이터 접근 요소로도 사용할 수 있습니다.
resource "local_file" "abc" {
content = "abc123"
filename = "${path.module}/abc.txt"
}
# ID값 확인
output "file_id" {
value = local_file.abc.id
}
# 파일의 경로를 절대경로로 출력
output "file_abspath" {
value = abspath(local_file.abc.filename)
}
terraform init && terraform plan
terraform apply -auto-approve
# graph 확인 > graph.dot 파일 선택 후 오른쪽 상단 DOT 클릭
terraform graph > graph.dot
# 파일 경로 비교 확인
terraform state list
terraform state show local_file.abc
echo "local_file.abc" | terraform console
echo "local_file.abc.filename" | terraform console
terraform output -raw file_abspath ; echo
plan을 수행하면, 생성되지 않은 file_id 값은 출력되지 않습니다. apply 시에는 다음과 같이 출력되는 것을 확인할 수 있습니다.
리소스의 상세 정보를 확인해 보면 다음과 같습니다.
그래프를 통해 리소스의 관계를 확인하였습니다.
반복문
테라폼에서 제공하는 반복문은 동일한 내용에 대해 테라폼 구성 정의를 반복적으로 하지 않고 관리할 수 있으며, 주로 list 형태나, key-value 문자열 집합 데이터에 사용합니다.
단순히 count의 index를 사용하는 예제는 다음과 같습니다.
resource "local_file" "abc" {
count = 5
content = "This is filename abc${count.index}.txt"
filename = "${path.module}/abc${count.index}.txt"
}
output "filecontent" {
value = local_file.abc.*.content
}
output "fileid" {
value = local_file.abc.*.id
}
output "filename" {
value = local_file.abc.*.filename
}
terraform init && terraform apply -auto-approve
terraform state list
echo "local_file.abc" | terraform console
echo "local_file.abc[0]" | terraform console
echo "local_file.abc[4]" | terraform console
terraform state show 'local_file.abc[0]'
terraform state show 'local_file.abc[4]'
# graph 확인
terraform graph > graph.dot
반복문을 통해 리소스를 생성하고, 결괏값을 *를통해 출력하면 다음과 같이 count 만큼의 리소스가 생성된 것을 확인할 수 있습니다.
만약 list를 활용해야 하는 상황이 있다면 다음의 예제처럼 사용할 수 있습니다.
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"
}
terraform apply -auto-approve
terraform state list
ls *.txt
diff abc-a.txt abc-b.txt
diff abc-a.txt def-c.txt
cat abc-a.txt abc-b.txt abc-c.txt
cat def-a.txt def-b.txt def-c.txt
echo "local_file.abc" | terraform console
echo "local_file.abc[0].content" | terraform console
echo "local_file.abc[2].content" | terraform console
terraform state show 'local_file.abc[0]'
terraform state show 'local_file.abc[2]'
# graph 확인 > graph.dot 파일 선택 후 오른쪽 상단 DOT 클릭
terraform graph > graph.dot
실행 결과 생성된 리소스를 확인해 보면 다음과 같습니다.
실습 3 - 반복문을 이용하여 IAM 사용자 3명 생성하기
다음과 같이 count를 이용하여 여러 명의 IAM 사용자를 생성할 수 있습니다.
count index를 이용하는 방법은 다음과 같습니다.
provider "aws" {
region = "ap-northeast-2"
}
resource "aws_iam_user" "myiam" {
count = 3
name = "myuser.${count.index}"
}
terraform init && terraform plan
terraform apply -auto-approve
# 확인
aws iam list-users | jq
terraform state list
실행 후 3개의 사용자 계정이 생긴 것을 확인할 수 있습니다.
단순히 인덱스를 활용하는 것이 아니라, 사용자 이름을 이용하여 계정을 생성하려면 다음과 같이 list를 활용합니다.
variable "user_names" {
description = "Create IAM users with these names"
type = list(string)
default = ["devlos", "piter", "hitherex"]
}
provider "aws" {
region = "ap-northeast-2"
}
resource "aws_iam_user" "myiam" {
count = length(var.user_names)
name = var.user_names[count.index]
}
output "all_arns" {
value = aws_iam_user.myiam[*].arn #스플랫 연산자 * 를 사용
description = "The ARNs for all users"
}
terraform plan
terraform apply -auto-approve
terraform state list
실행 결과는 다음과 같습니다.
스페셜 실습 - 약분님의 반복문 시나리오
깃허브 링크
https://www.youtube.com/watch?v=enhSdIJ9xxQ
시나리오 1~6
- AWS VPC Subnet 테라폼 코드 작성
- subnet cidr를 count로 여러 개 받도록 구현
- AZ는 변수로 입력해서 적용
- subnet에 태그 추가하기
- subnet 태그를 고유한 이름으로 적용하기
- subnet 설정 변수를 한 변수로 설정하도록 리펙토링
#terraform.tfvars
vpc_cidr = "192.168.0.0/16"
subnets = [
{
cidr = "192.168.1.0/24",
az = "ap-northeast-2a",
tags = {
Name = "public-subnet"
Environment = "dev"
}
},
{
cidr = "192.168.2.0/24",
az = "ap-northeast-2c",
tags = {
Name = "private-subnet"
Environment = "dev"
}
}
]
#variables.tf
variable "vpc_cidr" {
type = string
}
variable "subnets" {
type = list(object({
cidr = string
az = string
tags = map(string)
}))
}
#main.tf
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
tags = {
Name = "terraform VPC"
}
}
resource "aws_subnet" "main" {
count = length(var.subnets)
vpc_id = aws_vpc.main.id
cidr_block = var.subnets[count.index].cidr
availability_zone = var.subnets[count.index].az
tags = var.subnets[count.index].tags
}
resource "aws_instance" "server" {
ami = "ami-0e8bd0820b6e1360b"
instance_type = "t4g.nano"
subnet_id = aws_subnet.main[1].id
# index 접근 방법 오류 해결 코드
# subnet_id = index(aws_subnet.main.*.cidr_block, "192.168.2.0/24")
tags = {
Name = "Terraform demo"
}
}
output "myvpc_id" {
value = aws_vpc.main.id
}
# 신규 터미널 : 모니터링
while true; do aws ec2 describe-subnets --filters --query "Subnets[*].{ID:SubnetId,CIDR:CidrBlock,AZ:AvailabilityZone}" --output text; echo; time; sleep 1; done
# 배포 실행
terraform plan && terraform apply -auto-approve
# 실행 결과 확인
terraform state list
echo "aws_subnet.main[0].tags_all" | terraform console
echo "aws_subnet.main[1].tags_all" | terraform console
# 신규 생성한 VPC 내에 서브넷 확인
aws ec2 describe-subnets --filters "Name=vpc-id,Values=$(terraform output -raw myvpc_id)" --query "Subnets[*].{ID:SubnetId,CIDR:CidrBlock,AZ:AvailabilityZone,Name:Tags[*]|[*].Value}" --output table
aws ec2 describe-subnets --filters "Name=vpc-id,Values=$(terraform output -raw myvpc_id)" --query "Subnets[*].{ID:SubnetId,CIDR:CidrBlock,AZ:AvailabilityZone,Name:Tags[*]|[*].Value}" --output text
#리소스 삭제
terraform destroy -auto-approve
6번까지의 시나리오를 진행하면 다음과 같이 두 개의 서브넷과 EC2 인스턴스를 생성할 수 있습니다.
시나리오 7
- subnet 중 하나를 삭제하여 장애상황 발생 시키기
시나리오 6번 실행 후 서브넷 첫 번째를 주석처리합니다.
#terraform.tfvars
vpc_cidr = "192.168.0.0/16"
subnets = [
# {
# cidr = "192.168.1.0/24",
# az = "ap-northeast-2a",
# tags = {
# Name = "public-subnet"
# Environment = "dev"
# }
# },
{
cidr = "192.168.2.0/24",
az = "ap-northeast-2c",
tags = {
Name = "private-subnet"
Environment = "dev"
}
}
]
terraform plan
# 실행 : 수행 과정 로그를 잘 살펴 보자!
terraform apply -auto-approve
주석 후 plan을 실행하면 인덱스 오류가 발생하게 됩니다.
또한 서브넷을 변경시키려 했음에도 불구하고 서버가 새로 생성되며, 다음과 같이 az와 cider 블록이 변경됩니다.
여기서 확인해야 할 점은 기존 [0] 번째 인덱스의 값이 과거 [1] 번의 리소스로 교체가 된다는 것입니다.
Plan 이후 Apply를 수행하면 다음과 같이 CIDR 블록이 중복되었다는 오류를 확인할 수 있습니다.
시나리오 8
- subnet 중 하나를 추가하여 장애상황 발생 시키기
시나리오 6번 실행 후 서브넷 리스트 사이에 새 서브넷을.
#terraform.tfvars
vpc_cidr = "192.168.0.0/16"
subnets = [
{
cidr = "192.168.1.0/24",
az = "ap-northeast-2a",
tags = {
Name = "public-subnet"
Environment = "dev"
}
},
{
cidr = "192.168.5.0/24", #추가
az = "ap-northeast-2a",
tags = {
Name = "public-subnet"
Environment = "dev"
}
},
{
cidr = "192.168.2.0/24",
az = "ap-northeast-2c",
tags = {
Name = "private-subnet"
Environment = "dev"
}
}
]
terraform plan
# 실행 : 수행 과정 로그를 잘 살펴 보자!
terraform apply -auto-approve
다음을 실행하게 되면, 동작중인 서버를 삭제하고, 추가된 인덱스 이후의 모든 서브넷을 삭제 후 다시 만들어 냅니다. 하지만, 기존 서브넷과 충돌이 발생하여 서비스를 배포할 수 없게 됩니다.
count를 이용한 index를 이용하여 list에 접근하게 되면, 요소를 삭제 시 인덱스 구성이 기존과 다르게 변경되기 때문에, 이런 상황에는 foreach 사용을 권고하고 있습니다. - 링크
도전과제 1 - 리전 내에서 사용가능한 가용영역 목록 가져오기를 사용한 VPC 리소스 생성 실습
VPC 리소스 생성방법은 다음과 같습니다.
data "aws_vpc" "selected" {
# VPC를 식별하기 위한 필터 추가
filter {
name = "tag:Name"
values = ["devlos-vpc"]
}
}
data "aws_availability_zones" "available" {
state = "available"
}
resource "aws_subnet" "primary" {
vpc_id = data.aws_vpc.selected.id
availability_zone = data.aws_availability_zones.available.names[0]
cidr_block = "10.10.3.0/24"
}
resource "aws_subnet" "secondary" {
vpc_id = data.aws_vpc.selected.id
availability_zone = data.aws_availability_zones.available.names[1]
cidr_block = "10.10.2.0/24"
}
리소스를 생성한 결과는 다음과 같습니다.
마치며
이번 포스팅에서는 테라폼의 기본 사용법에 대해 이어 학습한 내용을 정리해 보았습니다.
실습 분량이 많아서 시간이 오래 걸리긴 했지만, 예제를 통해서 이전보다 더욱 테라폼에 익숙해질 수 있었습니다.
스터디를 할 수 있게 시나리오와 예제들을 준비해 주신 스터디 리더분들이 고생하시는 만큼 조금이라도 더 배워 나가고 싶네요..
다음 주차 스터디 후 포스팅에서 뵙겠습니다.
'클라우드 컴퓨팅 & NoSQL > [T101] 테라폼 4기 스터디' 카테고리의 다른 글
[6주차 - Well-Architected 방식으로 워크로드를 안전하게 마이그레이션 및 현대화하기 Workshop ] T101 4기 스터디 (24.07.14) (0) | 2024.07.17 |
---|---|
[5주차 - 모듈 & Runner] T101 4기 스터디 (24.07.07) (1) | 2024.07.13 |
[4주차 - Provider & State] T101 4기 스터디 (24.06.30) (0) | 2024.07.04 |
[3주차 - 기본사용#3] T101 4기 스터디 (24.06.23) (0) | 2024.06.29 |
[1주차 - 기본사용] T101 4기 스터디 (24.06.09) (1) | 2024.06.15 |