들어가며
이번 주차에는 테라폼 기초 개념에 이어 조금 더 본격적으로 프로그래밍 적인 문법을 배웠습니다. 특히 변수 정의, 출력, 반복문에 관련된 내용은 관리의 기초이며 다른 소스코드를 해석함에 있어서도 가장 중요한 부분이라고 생각합니다. 스터디 진행자분께서 해당 내용들을 좋은 예시들을 기반으로 자세히 설명해 주셔서 쉽게 개념을 이해할 수 있었습니다.
* 본 스터디의 자료는 아래의 책을 기반으로 합니다.
테라폼으로 시작하는 IaC | 김민수, 김재준, 이규석, 이유종 | 링크
데이터 소스
데이터 소스는 테라폼으로 정의되지 않은 외부 리소스 또는 저장된 정보를 테라폼 내부에서 참조할 때 사용합니다. 데이터 소스 블록은 'data'로 시작하며, 데이터 소스 유형, 이름으로 구성되어 있습니다. 이름 뒤에는 { } 를 이용하여 내부의 메타인수를 정의합니다.
데이터 소스 유형은 _ 를 기준으로 [프로바이더]_[리소스 유형]으로 구성됩니다.
# Terraform Code
data "<리소스 유형>" "<이름>" {
<인수> = <값>
}
# 데이터 소스 참조
data.<리소스 유형>.<이름>.<속성>
data "local_file" "devlos" {
filename = "${path.module}/devlos.txt"
}
# 실행
terraform init $$ terrafor plan && terraform apply -auto-approve
# 데이터 소스의 참조 확인
echo "data.local_file.devlos" | terraform console
테라폼 data를 생성하면 이 값은 state에 저장되지 않기 때문에 terraform consloe에서만 확인할 수 있습니다.
data는 다음과 같은 메타 인수를 사용할 수 있습니다.
- depends_on : 종속성을 선언하며, 선언된 구성요소와의 생성 시점에 대해 정의 합니다.
- count : 선언된 개수를 기준으로 여러 개의 리소스를 생성합니다.
- for_each : map 또는 set 타입의 데이터 배열의 값을 기준으로 여러개의 리소스를 생성합니다.
- lifecycle : 리소스의 수명주기를 관리합니다.
아래와 같이 프로바이더의 리소스를 가져와서 data로 만들어 사용할 수도 있습니다.
# Declare the data source
data "aws_availability_zones" "available" {
state = "available"
}
resource "aws_subnet" "primary" {
availability_zone = data.aws_availability_zones.available.names[0]
# e.g. ap-northeast-2a
}
resource "aws_subnet" "secondary" {
availability_zone = data.aws_availability_zones.available.names[1]
# e.g. ap-northeast-2b
}
이 내용에 대한 실습은 아래편 도전과제에서 다시 설명드립니다.
입력 변수 Variable
입력 변수는 인프라를 구성하는데 필요한 속성 값을 정이해 코드의 변경 없이 여러 인프라를 생성하는데 목적이 있습니다. 즉 인프라에서 변경이 될 수 있는 환경변수를 코드와 분리하여 쉽게 인프라를 관리한다는 의미입니다.
변수는 variable로 시작되는 블록으로 구성됩니다. 변수의 블록 뒤 이름은 동일한 모듈 내의 모든 변수 선언에서 고유해야 하며, 선언된 이름으로 다른 코드 내부에서 참조됩니다.
변수의 구성은 다음과 같습니다.
# variable 블록 선언의 예
variable "<이름>" {
<인수> = <값>
}
variable "image_id" {
type = string
}
다른 언어들과 마찬기지로 예약 변수는 이름으로 사용이 불가능합니다.
(source, version, providers, count, for_each, lifecycle, depends_on, locals)
다음과 같은 메타 인수를 사용할 수 있습니다.
- default : 변수 값을 전달하는 여러 가지 방법을 지정하지 않으면 기본값이 전달됩니다. 이전 스터디에서 확인했던 것처럼 기본값이 없으면 대화식으로 사용자에게 변수에 대한 정보를 물어보게 됩니다.
- type : 변수에 허용되는 값 유형 정의, string number bool list map set object tuple와 유형을 지정하지 않으면 any 유형으로 간주합니다.
- description : 입력 변수의 설명 부분입니다.
- validation : 변수 선언의 제약조건을 추가해 유효성 검사 규칙을 정의할 수 있습니다.
- sensitive : 민감한 변수 값임을 알리고 테라폼의 출력문에서 값 노출을 제한합니다. 출력문에서만 값의 노출이 제한되기 때문에 보안상의 큰 의미는 없는 것으로 보입니다.
- nullable : 변수에 값이 없어도 됨을 지정할 수 있습니다.
변수의 유형은 기본 유형과, 집합 유형으로 나뉩니다.
기본 유형에는 string, number, bool, any(명시적으로 모든 유형이 허용)이 있습니다.
집합 유형에는 list, map, set, object, tuple이 있습니다.
variable "string" {
type = string
description = "var String"
default = "myString"
}
variable "number" {
type = number
default = 123
}
variable "boolean" {
default = true
}
variable "list" {
default = [
"google",
"vmware",
"amazon",
"microsoft"
]
}
variable "map" { # Sorting
default = {
aws = "amazon",
azure = "microsoft",
gcp = "google"
}
}
variable "set" { # Sorting
type = set(string)
default = [
"google",
"vmware",
"amazon",
"microsoft"
]
}
variable "object" {
type = object({ name = string, age = number })
default = {
name = "abc"
age = 12
}
}
variable "tuple" {
type = tuple([string, number, bool])
default = ["abc", 123, true]
}
variable "ingress_rules" { # optional ( >= terraform 1.3.0)
type = list(object({
port = number,
description = optional(string),
protocol = optional(string, "tcp"),
}))
default = [
{ port = 80, description = "web" },
{ port = 53, protocol = "udp" }]
}
output "list_index_0" {
value = var.list.0
}
output "list_all" {
value = [
for name in var.list : upper(name)
]
}
입력 변수는 테라폼 함수를 이용하여 사용자 지정 유효성 검사를 수행할 수 있습니다. 유효성 검사는 validation.condition 조건을 통해 설정이 가능합니다. 또한 유효성 검사에 실패하면 validation.error_message를 출력할 수 있습니다.
variable "image_id" {
type = string
description = "The id of the machine image (AMI) to use for the server."
validation {
condition = length(var.image_id) > 4
error_message = "The image_id value must exceed 4."
}
validation {
# regex(...) fails if it cannot find a match
condition = can(regex("^ami-", var.image_id))
error_message = "The image_id value must starting with \"ami-\"."
}
}
local 지역 값
local은 코드 내에서 사용자가 지정한 값을 통해 코드 내에서만 적용되는 값입니다. 입력 변수와는 달리 선언된 모듈 내에서만 접근이 가능하고, 입력을 받을 수 없습니다.
local은 사용자가 테라폼 코드를 구현할 때 값이나 표현식을 반복적으로 사용할 수 있는 편의성을 제공하지만, 파편화될 가능성이 있으므로 관리적인 측면에서 부담이 발생할 수 있으므로 주의해야 합니다.
참고로 테라폼 구성 파일이 분리되어 있어도, 실행 시점에는 하나의 구성파일로 동작합니다.
variable "prefix" {
default = "hello"
}
locals {
name = "terraform"
content = "${var.prefix} ${local.name}"
my_info = {
age = 20
region = "KR"
}
my_nums = [1, 2, 3, 4, 5]
}
locals {
content = "content2" # 중복 선언되었으므로 오류가 발생한다.
}
출력 output
출력 값은 주로 테라폼 코드의 프로비저닝 수행 후의 결과 속성 값을 확인하는 용도로 사용됩니다. 또한 테라폼 모듈 간 데이터 접근 요소로도 활용될 수 있다고 합니다. 이러한 패턴의 사용예시는 다음과 같습니다.
- 루트 모듈에서 사용자가 확인하고자 하는 특정 속성을 출력합니다.
- 자식 모듈의 특정 값을 정의하고 루트 모듈에서 결과를 참조합니다.
- 서로 다른 루트 모듈의 결과를 원격으로 읽기 위한 접근 요소로 활용됩니다.
resource "local_file" "abc" {
content = "abc123"
filename = "${path.module}/abc.txt"
}
output "file_id" {
value = local_file.abc.id
}
output "file_abspath" {
value = abspath(local_file.abc.filename)
}
Plan 단계의 output은 프로비저닝 이후 출력 할 수 있는 값도 있습니다. 그럴 경우 "known after apply"라는 문구를 출력합니다.
반복문
반복문은 동일한 리소스나 데이터를 여러 번 생성하거나, 다양한 값을 가진 리소스나 데이터를 생성할 수 있습니다. 테라폼에서는 주로 count와 for_each를 주로 사용하여 반복문을 구성한다고 합니다. 이 외에도 for과 dynamic을 활용할 수 있습니다.
count의 특징은 다음과 같습니다.
- count는 숫자를 기반으로 리소스를 여러 번 생성합니다.
- count.index를 사용하여 현재 반복의 인덱스에 액세스 할 수 있습니다.
- 주로 연속적인 숫자 범위에 기반한 리소스 생성에 사용됩니다.
- provider 블록 선언부가 포함되어 있는 경우에는 count 적용이 불가능하므로 provider 블록을 분리해야 합니다.
- 외부 변수가 list 타입인 경우 중간값이 삭제되면, 중간값 이후 값들도 모두 삭제되고 재생성됩니다.
resource "local_file" "abc" {
count = 5
content = "abc${count.index}"
filename = "${path.module}/abc${count.index}.txt"
}
output "fileid" {
value = local_file.abc.*.id
}
output "filename" {
value = local_file.abc.*.filename
}
output "filecontent" {
value = local_file.abc.*.content
}
count에 부여되는 정수 값을 외부 변수에 식별되도록 구성할 수 도 있습니다.
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"
}
이 결과를 그래프로 확인해 보면, var.names를 local_file.abc가 참조하고 이를 다시 local_file.def가 참조하는 것을 확인할 수 있습니다.
for_each의 특징은 다음과 같습니다.
- for_each는 맵 또는 세트와 같은 복잡한 데이터 타입을 기반으로 리소스를 생성합니다.
- 각 반복에서는 each.key와 each.value를 사용하여 현재 항목의 키와 값을 참조할 수 있습니다.
- each.key : 이 인스턴스에 해당하는 map 타입의 key 값
- each.value : 이 인스턴스에 해당하는 map의 value 값
- 주로 키-값 쌍이나 고유한 값의 세트에 기반한 리소스 생성에 사용됩니다.
- 데이터 타입에 따라 반환하는 내용이 다릅니다.
- list type: 값, 인덱스 (관용: v, i)
- map: 키, 값 (k, v)
- set: 키
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"
}
추가로 map의 key 값은 count와 달리 고유하므로, 중간에 값을 삭제한 후 다시 적용해도 삭제한 값에 대해서만 리소스를 삭제합니다.
for의 특징은 다음과 같습니다.
- 리스트, 맵, 세트를 생성하거나 변환하는 데 사용되는 반복문입니다.
- 기본 구문은 [ for <변수> in <입력값> : <출력값> ]입니다.
- 조건문을 추가하여 특정 조건을 만족하는 항목만 포함시킬 수도 있습니다.
다음은 list의 내용을 for로 읽어서 대문자로 변환하는 예제입니다.
# list의 내용을 대문자로 변경하는 예제
variable "names" {
default = ["a", "b", "c"]
}
resource "local_file" "abc" {
content = **jsonencode([for s in var.names : upper(s)])** # 결과 : ["A", "B", "C"]
filename = "${path.module}/abc.txt"
}
아래와 같이 for 문을 사용하며 if를 통해 예외처리도 가능합니다.
variable "members" {
type = map(object({
role = string
}))
default = {
ab = { role = "member", group = "dev" }
cd = { role = "admin", group = "dev" }
ef = { role = "member", group = "ops" }
}
}
output "A_to_tupple" {
value = [for k, v in var.members : "${k} is ${v.role}"]
}
output "B_get_only_role" {
value = {
for name, user in var.members : name => user.role
if user.role == "admin"
}
}
output "C_group" {
value = {
for name, user in var.members : user.role => name...
}
}
dynamic의 특징은 다음과 같습니다.
- dynamic 블록은 반복 가능한 중첩 블록을 생성하는 데 사용됩니다.
- 주로 리소스의 중첩된 구성 요소(예: 보안 그룹 규칙, 네트워크 인터페이스 등)를 동적으로 생성할 때 사용됩니다.
- for_each와 함께 사용하여 특정 데이터 세트를 기반으로 중첩 블록을 생성할 수 있습니다.
- content 블록 내에서 each.key와 each.value를 사용하여 현재 항목의 키와 값을 참조할 수 있습니다.
만약 이러한 형태로 반복적인 블록이 있다고 가정했을 때,
data "archive_file" "dotfiles" {
type = "zip"
output_path = "${path.module}/dotfiles.zip"
source {
content = "hello a"
filename = "${path.module}/a.txt"
}
source {
content = "hello b"
filename = "${path.module}/b.txt"
}
source {
content = "hello c"
filename = "${path.module}/c.txt"
}
}
dynamic을 활용하여 반복되는 코드를 줄일 수 있습니다.
variable "names" {
default = {
a = "hello a"
b = "hello b"
c = "hello c"
}
}
data "archive_file" "dotfiles" {
type = "zip"
output_path = "${path.module}/dotfiles.zip"
dynamic "source" {
for_each = var.names
content {
content = source.value
filename = "${path.module}/${source.key}.txt"
}
}
}
스터디 실습
실습 1 - VPC + 보안그룹 + EC2 배포
(간단한 AWS 인프라 환경을 구현해 볼 수 있는 좋은 예시였습니다!!)
provider "aws" {
region = "ap-northeast-2"
}
# 1. VPC 생성
resource "aws_vpc" "myvpc" {
cidr_block = "10.10.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "t101-study"
}
}
# 2. 서브넷 생성
resource "aws_subnet" "mysubnet1" {
vpc_id = aws_vpc.myvpc.id
cidr_block = "10.10.1.0/24"
availability_zone = "ap-northeast-2a"
tags = {
Name = "t101-subnet1"
}
}
resource "aws_subnet" "mysubnet2" {
vpc_id = aws_vpc.myvpc.id
cidr_block = "10.10.2.0/24"
availability_zone = "ap-northeast-2c"
tags = {
Name = "t101-subnet2"
}
}
# 3. igw 생성
resource "aws_internet_gateway" "myigw" {
vpc_id = aws_vpc.myvpc.id
tags = {
Name = "t101-igw"
}
}
# 4. route table 생성
resource "aws_route_table" "myrt" {
vpc_id = aws_vpc.myvpc.id
tags = {
Name = "t101-rt"
}
}
resource "aws_route_table_association" "myrtassociation1" {
subnet_id = aws_subnet.mysubnet1.id
route_table_id = aws_route_table.myrt.id
}
resource "aws_route_table_association" "myrtassociation2" {
subnet_id = aws_subnet.mysubnet2.id
route_table_id = aws_route_table.myrt.id
}
resource "aws_route" "mydefaultroute" {
route_table_id = aws_route_table.myrt.id
destination_cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.myigw.id
}
output "aws_vpc_id" {
value = aws_vpc.myvpc.id
}
dns support, dns hostnames 활성화
그래프 생성 결과
여기까지가 네트워크 환경에 대한 실습이었습니다. 다음은 준비된 VPC 안에서 보안 그룹과 ec2 인스턴스를 생성합니다.
# 6. 보안그룹을 만들고, inboud outbound rule을 설정
resource "aws_security_group" "mysg" {
vpc_id = aws_vpc.myvpc.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
}
아래와 같이 보안 그룹이 그래프에 추가됩니다.
마지막으로 ec2 인스턴스를 배치합니다. 배치할 때 vpc의 서브넷 1에 앞서 만든 sg를 적용하여 생성합니다.
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.myigw
]
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"
}
실습 2 - IAM User 생성
locals를 이용하여 동일한 태그를 가진 iam user를 생성합니다.
provider "aws" {
region = "ap-northeast-2"
}
locals {
name = "mytest"
team = {
group = "dev"
}
}
resource "aws_iam_user" "myiamuser1" {
name = "${local.name}1"
tags = local.team
}
resource "aws_iam_user" "myiamuser2" {
name = "${local.name}2"
tags = local.team
}
실습 3 - 반복문
실습 3-1 : IAM 사용자 3명 생성
provider "aws" {
region = "ap-northeast-2"
}
resource "aws_iam_user" "myiam" {
count = 3
name = "myuser.${count.index}"
}
실습 3-2: count 입력 변수를 통해 IAM 사용자 생성
# variables.tf
variable "user_names" {
description = "Create IAM users with these names"
type = list(string)
default = ["gasida", "akbun", "ssoon"]
}
# iam.tf
provider "aws" {
region = "ap-northeast-2"
}
resource "aws_iam_user" "myiam" {
count = length(var.user_names)
name = var.user_names[count.index]
}
# output.tf
output "first_arn" {
value = aws_iam_user.myiam[0].arn
description = "The ARN for the first user"
}
output "all_arns" {
value = aws_iam_user.myiam[*].arn
description = "The ARNs for all users"
}
실습 3-3: 배열 중간값 변경 시 변경된 인덱스 이후의 모든 리소스를 재생성
variable "user_names" {
description = "Create IAM users with these names"
type = list(string)
default = ["gasida", "ssoon"] #중간의 akbun을 삭제
}
실습 3-4: 반복적인 리소스 생성
실습 3-3에서 리소스가 모두 삭제되는 현상과 달리, for_each를 사용하여 리소스를 맵으로 처리하면 컬렉션 중간의 항목을 삭제해도 다른 리소스에 영향을 받지 않기 때문에 count보다 처리 이점이 더 큽니다. 그래서 리소스의 구별되는 여러 복사본을 만들 때는 count 대신 for_each를 사용하는 것이 좋은 방법입니다.
# variables.tf
variable "user_names" {
description = "Create IAM users with these names"
type = list(string)
default = ["gasida", "ssoon"]
}
# iam.tf
provider "aws" {
region = "ap-northeast-2"
}
# resource "aws_iam_user" "myiam" {
# count = length(var.user_names)
# name = var.user_names[count.index]
# }
resource "aws_iam_user" "myiam" {
for_each = toset(var.user_names)
name = each.value
}
# output.tf
output "all_users" {
value = aws_iam_user.myiam
}
도전과제 1 - 리전 내에서 사용 가능한 가용영역 목록 가져오기를 사용한 VPC 리소스 생성 실습 진행
점점 익숙해지는 것 같습니다 :)
# Declare the data source
data "aws_availability_zones" "available" {
state = "available"
}
resource "aws_subnet" "primary" {
vpc_id = aws_vpc.devlosvpc.id
availability_zone = data.aws_availability_zones.available.names[0]
cidr_block = "10.10.1.0/24"
# e.g. ap-northeast-2a
}
resource "aws_subnet" "secondary" {
vpc_id = aws_vpc.devlosvpc.id
availability_zone = data.aws_availability_zones.available.names[1]
cidr_block = "10.10.2.0/24"
# e.g. ap-northeast-2b
}
resource "aws_vpc" "devlosvpc" {
cidr_block = "10.10.0.0/16"
tags = {
Name = "Devlos vpc!"
}
}
마치며
이번 포스팅에서는 테라폼에서 사용하는 변수들과, 반복문을 활용하여 resource를 관리하는 법에 대해서 배운 내용을 정리했습니다. 스터디의 예제도 풍부하고, 단계별로 확장되는 형태로 진행되다 보니 따라 배우는 입장에서 정말 편했습니다. 다만 이렇게 커리큘럼을 준비하신 초대~현재 스터디장 분들의 노고가 느껴지기도 했습니다.
따라 하다 보니 자연스럽게 기본적인 AWS 네트워크 구성을 테라폼으로 진행하는 방법도 알 수 있게 되었습니다. 스터디 종료 후 halsholicker님께서 공유해 주신 EKS 테라폼 코드를 보면 이전보다 훨씬 더 잘 이해될 것 같다는 느낌이 드네요.
긴 포스팅을 따라와 주시느라 고생 많으셨습니다 :) 감사합니다! 이번주 스터디도 fire!!!
'클라우드 컴퓨팅 & NoSQL > [T101] 테라폼 기초 입문 스터디' 카테고리의 다른 글
[6주차] T101 테라폼 기초 입문 스터디 (23.10.08) (2) | 2023.10.10 |
---|---|
[5주차] T101 테라폼 기초 입문 스터디 (23.09.24) (1) | 2023.09.29 |
[4주차] T101 테라폼 기초 입문 스터디 (23.09.17) (0) | 2023.09.23 |
[3주차] T101 테라폼 기초 입문 스터디 (23.09.10) (2) | 2023.09.15 |
[1주차] T101 테라폼 기초 입문 스터디 (23.08.27) (0) | 2023.09.01 |