resource "aws_security_group" "as_is_long" {
name = "as-is-long"
description = "Allow SSH, HTTP, HTTPS, and all outbound traffic"
ingress {
description = "SSH from IGW"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = [
"0.0.0.0/0"
]
}
ingress {
description = "HTTP from IGW"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = [
"0.0.0.0/0"
]
}
ingress {
description = "HTTPS from IGW"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = [
"0.0.0.0/0"
]
}
ingress {
description = "Custom HTTP from IGW"
from_port = 8080
to_port = 8080
protocol = "tcp"
cidr_blocks = [
"0.0.0.0/0"
]
}
egress {
description = "All outbound traffic"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = [
"0.0.0.0/0"
]
}
}
Ruby
복사
locals {
ingress_ports_info = {
22 = { description = "SSH from IGW", protocol = "tcp", cidr_blocks = ["0.0.0.0/0"] }
80 = { description = "HTTP from IGW", protocol = "tcp", cidr_blocks = ["0.0.0.0/0"] }
443 = { description = "HTTPS from IGW", protocol = "tcp", cidr_blocks = ["0.0.0.0/0"] }
8080 = { description = "Custom HTTP from IGW", protocol = "tcp", cidr_blocks = ["0.0.0.0/0"] }
}
}
resource "aws_security_group" "to_be_short_with_dynamic_block" {
name = "to-be-short"
description = "Allow SSH, HTTP, HTTPS, and all outbound traffic"
dynamic "ingress" {
for_each = local.ingress_ports_info
content {
description = ingress.value.description
from_port = ingress.key
to_port = ingress.key
protocol = ingress.value.protocol
cidr_blocks = ingress.value.cidr_blocks
}
}
egress {
description = "All outbound traffic"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = [
"0.0.0.0/0"
]
}
}
Ruby
복사
왼쪽의 긴 AS-IS 코드를 오른쪽의 TO-BE의 짧은 것으로 바꿀 수 있다. 양쪽 테라폼 코드의 결과물은, inbound 규칙 여러개를 포함하는, 같은 AWS 보안그룹을 생성한다. 어떻게 오른쪽의 코드로 짧고 가독성 좋게 바꿀 수 있게 하는 테라폼 HCL의 문법과 기능에 대해 설명한다.
AS-IS
terraform {
required_version = "~> 1.5.6"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.15"
}
}
}
provider "aws" {
region = "ap-northeast-2"
}
# write security group that ingress for some well-known ports
# 22, 80, 443, 8080
# and egress for all ports
resource "aws_security_group" "as_is_long" {
name = "as-is-long"
description = "Allow SSH, HTTP, HTTPS, and all outbound traffic"
ingress {
description = "SSH from IGW"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = [
"0.0.0.0/0"
]
}
ingress {
description = "HTTP from IGW"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = [
"0.0.0.0/0"
]
}
ingress {
description = "HTTPS from IGW"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = [
"0.0.0.0/0"
]
}
ingress {
description = "Custom HTTP from IGW"
from_port = 8080
to_port = 8080
protocol = "tcp"
cidr_blocks = [
"0.0.0.0/0"
]
}
egress {
description = "All outbound traffic"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = [
"0.0.0.0/0"
]
}
}
Ruby
복사
모든 대역의 22, 80, 443 그리고 8080 등 몇 개의 포트의 ingress를 허용하는 보안그룹이다. 콘솔에서 생성된 보안그룹의 인바운드 규칙을 확인하면 다음과 같다.
어째 웹 콘솔의 테이블 뷰가 HCL 코드보다 더 깔끔해보이지 않는가? 실제로 내가 회사에서 보안그룹을 테라포밍하며 느꼈던 불편함이다. 웹 콘솔에 비해 코드가 세로로 길어 한눈에 파악하기 어렵다. 불필요하게 ingress 라는 인수가 반복된다. ingress 인수는 Attribute-as-blocks으로써(확실하진 않다) 블록들의 목록으로도 쓸 수 있다(list(object) , 테라폼 타입에 대해 후술). 하지만 코드가 길어지는 것은 마찬가지이다.
Complex Types; Collection and Structural Types
위에서 살펴본 TO-BE 오른쪽 코드처럼 가기 위해 일단 다음 지역변수(locals)를 정의한다:
locals {
ingress_ports_info = {
22 = { description = "SSH from IGW", protocol = "tcp", cidr_blocks = ["0.0.0.0/0"] }
80 = { description = "HTTP from IGW", protocol = "tcp", cidr_blocks = ["0.0.0.0/0"] }
443 = { description = "HTTPS from IGW", protocol = "tcp", cidr_blocks = ["0.0.0.0/0"] }
8080 = { description = "Custom HTTP from IGW", protocol = "tcp", cidr_blocks = ["0.0.0.0/0"] }
}
}
Ruby
복사
local.ingress_ports_info 로 접근할 수 있는 이 지역변수의 자료형은 object 이다. object 는 복합 자료형(collection type) 중에 하나인 구조적 자료형(structural type)이다. 갑자기 여러 개념이 나왔는데 다시 돌아가 테라폼 HCL 자료형에 대해 설명한다.
•
•
•
variable "list_example" {
type = list(any)
default = [
"a",
1,
true,
]
}
variable "tuple_example" {
type = tuple([number, string])
default = [
"a",
1,
]
}
Ruby
복사
다음 코드를 실행하면 var.list_example default 값은 문제가 없지만 var.tuple_example 은 다음 오류가 발생한다.
╷
│ Error: Invalid default value for variable
│
│ on main.tf line 36, in variable "tuple_example":
│ 36: default = [
│ 37: "a",
│ 38: 1,
│ 39: ]
│
│ This default value is not compatible with the variable's type constraint: a number is required.
Ruby
복사
tuple 은 schema라는 제약을 받는 자료형이다. 정의한대로(tuple([number, string])) 숫자와 문자열 하나씩만 순서대로 들어올 수 있다. list 는 이런 제약이 없다.
•
사실 list 는 위 코드에 쓰인 것처럼 list(any) 와 같다(alias)
◦
•
list([TYPE]) 으로 목록에 들어올 자료형을 제한할 수도 있다.
map 과 object 의 관계도 이와 마찬가지이다. 순서대로 키-값 쌍의 집합, 구조적 자료형이다. 다른 언어에서 “맵”, “오브젝트”(사전형(dictionary), 레이블 등등 …)가 이런 비슷한 의미로 각각 쓰이지만 테라폼 HCL에선 schema 유무의 차이로 구분할 수 있어야 한다.
그럼 다시 locals.ingress_ports_info 의 자료형은 무엇일까?
# tf console
> type(local.ingress_ports_info)
object({
22: object({
cidr_blocks: tuple([
string,
]),
description: string,
protocol: string,
}),
443: object({
cidr_blocks: tuple([
string,
]),
description: string,
protocol: string,
}),
80: object({
cidr_blocks: tuple([
string,
]),
description: string,
protocol: string,
}),
8080: object({
cidr_blocks: tuple([
string,
]),
description: string,
protocol: string,
}),
})
Shell
복사
object(object) 이다. 이런 자료형을 사용하는 이유는 단순히 가독성을 올리기 위해서 뿐만 아니라 뒤에 이어지는 dynamic 블록에서 사용하기 위함이다.
dynamic 블록
locals {
ingress_ports_info = {
22 = { description = "SSH from IGW", protocol = "tcp", cidr_blocks = ["0.0.0.0/0"] }
80 = { description = "HTTP from IGW", protocol = "tcp", cidr_blocks = ["0.0.0.0/0"] }
443 = { description = "HTTPS from IGW", protocol = "tcp", cidr_blocks = ["0.0.0.0/0"] }
8080 = { description = "Custom HTTP from IGW", protocol = "tcp", cidr_blocks = ["0.0.0.0/0"] }
}
}
resource "aws_security_group" "as_is_long" {
...
dynamic "ingress" {
for_each = local.ingress_ports_info
content {
description = ingress.value.description
from_port = ingress.key
to_port = ingress.key
protocol = ingress.value.protocol
cidr_blocks = ingress.value.cidr_blocks
}
}
...
}
Shell
복사
위 보안그룹 리소스 블록에서 ingress 인수 블록을 모두 지우고 위처럼 바꾸면 이전과 완전한 동일한 동작을 하는 코드이다(tf plan 을 하면 No changes여야 한다). 사실 for_each 를 보면 부연설명이 필요 없는 사람도 있을 것이다.
•
object 키를 “포트”로 활용했다.
◦
이는 ingress 의 from_port 와 to_port 가 같아서, port range를 정의하는 룰이 아니라서 가능하다.
•
port range나 더 복잡한 다른 ingress 의 선택 인수에 대해서도 정의하려면 값의 object 에 더 여러 키를 선언하고 접근해야 할 것이다.
TO-BE
이 과정은 리팩토링이다. 가독성을 높이고 복붙을 줄인 DRY 코드로 리팩토링하자.
terraform {
required_version = "~> 1.5.6"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.15"
}
}
}
provider "aws" {
region = "ap-northeast-2"
}
locals {
ingress_ports_info = {
22 = { description = "SSH from IGW", protocol = "tcp", cidr_blocks = ["0.0.0.0/0"] }
80 = { description = "HTTP from IGW", protocol = "tcp", cidr_blocks = ["0.0.0.0/0"] }
443 = { description = "HTTPS from IGW", protocol = "tcp", cidr_blocks = ["0.0.0.0/0"] }
8080 = { description = "Custom HTTP from IGW", protocol = "tcp", cidr_blocks = ["0.0.0.0/0"] }
}
}
moved {
from = aws_security_group.as_is_long
to = aws_security_group.to_be_short_with_dynamic_block
}
# write security group that ingress for some well-known ports
# 22, 80, 443, 8080
# and egress for all ports
resource "aws_security_group" "to_be_short_with_dynamic_block" {
name = "as-is-long"
description = "Allow SSH, HTTP, HTTPS, and all outbound traffic"
dynamic "ingress" {
for_each = local.ingress_ports_info
content {
description = ingress.value.description
from_port = ingress.key
to_port = ingress.key
protocol = ingress.value.protocol
cidr_blocks = ingress.value.cidr_blocks
}
}
egress {
description = "All outbound traffic"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = [
"0.0.0.0/0"
]
}
}
Shell
복사
습득 교훈
•
스터디 2주차만에 엄청난 수확이다. 보안그룹의 ingress/egress 뿐만 아니라 곳곳에 반복해서 선언하는, 보기에도 관리하기도 안좋은 코드가 많았는데 리팩토링 시작이다!
◦
dynamic 블록 뿐만 아니라 count , for 등도 적절히 활용하여 리팩토링 해야겠다
•
for_each 에 들어갈 object 스키마에 대한 많은 예제 학습이 필요할 거 같다.
◦
첨엔 “list(object) 로도 순회시켜주지”라는 생각이었다. 키로 활용할 것에 대한 아이디어가 없어서
◦
여전히 ingress.key 가 포트라는 사실은 스키마를 알지 못하면 직관적이지 않다(인지부하)?
도전과제
도전과제1 리전 내에서 사용 가능한 가용영역 목록 가져오기를 사용한 VPC 리소스 생성 실습 진행 - 링크 혹은 아무거나 데이터 소스를 사용한 실습 진행
terraform {
required_version = "~> 1.5.6"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.15"
}
}
}
provider "aws" {
region = "ap-northeast-2"
}
locals {
vpc_cidr_block = "10.0.0.0/16"
subnet_cidr_blocks = cidrsubnets(local.vpc_cidr_block, 3, 3)
}
data "aws_availability_zones" "available" {
state = "available"
}
resource "aws_vpc" "week_two_challenge" {
cidr_block = local.vpc_cidr_block
}
resource "aws_subnet" "week_two_challenge_primary" {
vpc_id = aws_vpc.week_two_challenge.id
availability_zone = data.aws_availability_zones.available.names[0]
cidr_block = local.subnet_cidr_blocks[0]
}
resource "aws_subnet" "week_two_challenge_secondary" {
vpc_id = aws_vpc.week_two_challenge.id
availability_zone = data.aws_availability_zones.available.names[1]
cidr_block = local.subnet_cidr_blocks[1]
}
Ruby
복사
도전과제2 : 위 3개 코드 파일 내용에 리소스의 이름(myvpc, mysubnet1 등)을 반드시! 꼭! 자신의 닉네임으로 변경해서 배포 실습해보세요!
•
리소스의 유형과 리소스의 이름이 차이를 알고, 리소스의 속성(예. ID)를 참조하는 방법에 대해서 익숙해지자
위에서 작성한 코드들로 대체…
도전과제3 : 입력변수를 활용해서 리소스(어떤 리소스든지 상관없음)를 배포해보고, 해당 코드를 정리해주세요!
도전과제4 : local를 활용해서 리소스(어떤 리소스든지 상관없음)를 배포해보고, 해당 코드를 정리해주세요!
포스트 본문 코드 및 도전과제1로 대체
도전과제5 : count, for_each 반복문, for문, dynamic문 을 활용해서 리소스(어떤 리소스든지 상관없음)를 배포해보고, 해당 코드를 정리해주세요!
terraform {
required_version = "~> 1.5.6"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.15"
}
}
}
provider "aws" {
region = "ap-northeast-2"
}
locals {
vpc_cidr_block = "10.0.0.0/16"
subnet_cidr_blocks = [for i in range(2) : "10.0.${i * 32}.0/19"] # for 사용
}
data "aws_availability_zones" "available" {
state = "available"
}
resource "aws_vpc" "week_two_challenge" {
cidr_block = local.vpc_cidr_block
}
resource "aws_subnet" "week_two_challenge_subnet" {
count = 2 # count 사용
vpc_id = aws_vpc.week_two_challenge.id
availability_zone = data.aws_availability_zones.available.names[count.index]
cidr_block = local.subnet_cidr_blocks[count.index]
}
moved {
from = aws_subnet.week_two_challenge_primary
to = aws_subnet.week_two_challenge_subnet[0]
}
moved {
from = aws_subnet.week_two_challenge_secondary
to = aws_subnet.week_two_challenge_subnet[1]
}
Ruby
복사
count , for 사용
terraform {
required_version = "~> 1.5.6"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.15"
}
}
}
provider "aws" {
region = "ap-northeast-2"
}
locals {
vpc_cidr_block = "10.0.0.0/16"
subnet_cidr_blocks = [for i in range(2) : "10.0.${i * 32}.0/19"]
}
data "aws_availability_zones" "available" {
state = "available"
}
resource "aws_vpc" "week_two_challenge" {
cidr_block = local.vpc_cidr_block
}
resource "aws_subnet" "week_two_challenge_subnet" {
for_each = {
primary = { cidr = "10.0.0.0/19", az = data.aws_availability_zones.available.names[0] }
secondary = { cidr = "10.0.32.0/19", az = data.aws_availability_zones.available.names[1] }
}
vpc_id = aws_vpc.week_two_challenge.id
availability_zone = each.value.az
cidr_block = each.value.cidr
}
moved {
from = aws_subnet.week_two_challenge_subnet[0]
to = aws_subnet.week_two_challenge_subnet["primary"]
}
moved {
from = aws_subnet.week_two_challenge_subnet[1]
to = aws_subnet.week_two_challenge_subnet["secondary"]
}
Ruby
복사
for_each 사용
테라폼 리소스 이름만 코드/state에서 변경됨