Search

dynamic 블록으로 AWS SG ingress rules 선언

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 자료형에 대해 설명한다.
복합 자료형이 아닌 원시 자료형(primitive types)은 코딩 경험이 없더라도 아주 간단하므로 예시만 나열한다:
string: "hello".
number: 15,  6.283185.
bool: true, false.
복합 자료형은 크게 집합 자료형(collection types)과 구조적 자료형으로 나뉘는데 둘의 큰 차이는 schema의 유무이다.
예를 들어 집합 자료형의 list 와 구조적 자료형의 tuple 을 비교할 수 있다. 둘 다 리스트, 자료형의 목록을 담는 자료형이지만 schema 유무에 따라 사용법이 다르다.
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)
코드에서 굳이 저렇게 쓴 이유는 tflint가 auto correct 한 결과인데, 나중에 따로 소개해보겠다.
list([TYPE]) 으로 목록에 들어올 자료형을 제한할 수도 있다.
mapobject 의 관계도 이와 마찬가지이다. 순서대로 키-값 쌍의 집합, 구조적 자료형이다. 다른 언어에서 “맵”, “오브젝트”(사전형(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 를 보면 부연설명이 필요 없는 사람도 있을 것이다.
테라폼 HCL이 프로그래밍 언어적 특성이 있지만 모든 곳에서 for_each 를 쓸 수 있는 것은 아니다. dynamic 블록for_each 인수에 할당한 object 에 대해 순회하며 키(key)와 값(value)에 접근할 수 있고 ingress 인수와 같은 리소스 블록 내의 중첩 블록에서 활용이 가능하다. 그리고 content 엔 실제 ingress 인수 블록에 들어갈 키 값 내용을 써주고 여기서 각 object 키-값 쌍에 접근하기 위해 dynamic 블록의 이름(ingress)로 접근하게 된다.
object 키를 “포트”로 활용했다.
이는 ingressfrom_portto_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
복사
(보안그룹 이름은 안바꿨지만) 새로 가볍게? 태어난 테라폼 리소스 이름만 바꾸기 위해 moved 블록을 사용했다. 이것도 나중에 기회가 되면 따로 정리해야겠다.

습득 교훈

스터디 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에서 변경됨