VPC


VPC 생성


처음으로 VPC를 구성합니다. 자신의 환경에 맞게 cidr_block 값을 변경해주세요.

# main.tf
resource "aws_vpc" "DEV-VPC" {
  assign_generated_ipv6_cidr_block = false
  cidr_block                       = "10.0.0.0/16"
  enable_dns_hostnames             = true
  enable_dns_support               = true
  instance_tenancy                 = "default"
  tags = {
    Name    = "DEV-VPC"
    Service = "DEV"
  }
}

Subnet


Terraform Function – Count


일반적으로 한 개의 서브넷을 만들기 위하여 한 개의 리소스 블록을 생성합니다.

# main.tf
resource "aws_subnet" "DEV-Subnet" {
  vpc_id     = aws_vpc.DEV-VPC.id
  cidr_block = "10.0.0.0/24"
  tags = {
    Name    = "DEV-Subnet"
    Service = "DEV"
  }
}

두 개를 만들어야 한다면 리소스 블록을 두 개 만들면 됩니다.

# main.tf
resource "aws_subnet" "DEV-Subnet1" {
  vpc_id     = aws_vpc.DEV-VPC.id
  cidr_block = "10.0.0.0/24"
  tags = {
    Name    = "DEV-Subnet"
    Service = "DEV"
  }
}
resource "aws_subnet" "DEV-Subnet2" {
  vpc_id     = aws_vpc.DEV-VPC.id
  cidr_block = "10.0.1.0/24"
  tags = {
    Name    = "DEV-Subnet"
    Service = "DEV"
  }
}

추가로 서브넷을 더 생성할 때는, 리소스 블록을 복사합니다. 하지만 cidr_block만 다르고 다른 항목들은 전부 같아 코드가 길어지게 됩니다.

cidr_block = "10.0.0.0/24" #DEV-Subnet1
cidr_block = "10.0.1.0/24" #DEV-Subnet2

이때 Terraform에서 제공하는 Count 함수를 사용하여 리소스를 반복 생성하는 것이 좋습니다.

count = 2

반복하고 싶은 숫자를 입력하면, 해당 블록은 해당 숫자만큼 반복하여 실행합니다.


Python Function – Range


Terraform에서 count는 파이썬의 range 함수와 비슷합니다. 

# count.py
count = range(2)
print(list(count)) # -> [0, 1]

입력한 숫자 – 1 개의 숫자가 배열로 생성됩니다.


반복문에서도 range로 생성한 배열을 사용할 수 있습니다.

for index in count:
    print(f"이번 순서: {index}, CIDR Block: 10.0.{index}.0/24")

현재 값은 index로 확인합니다.

이번 순서: 0, CIDR Block: 10.0.0.0/24
이번 순서: 1, CIDR Block: 10.0.1.0/24

Subnet 생성


DEV-VPC 내부에 서브넷을 생성합니다.

count.index로 현재 값을 가져와 cidr_block을 동적으로 할당합니다.

# main.tf
resource "aws_subnet" "DEV-Subnet" {
  count = 2
  vpc_id     = aws_vpc.DEV-VPC.id
  cidr_block = "10.0.${count.index}.0/24"
  tags = {
    Name    = "DEV-Subnet"
    Service = "DEV"
  }
}

동적으로 생성한 서브넷은 아래 2개 입니다.

[10.0.0.0/24, 10.0.1.0/24]

보안그룹


보안그룹 생성


DEV-VPC에 속하는 보안그룹을 생성합니다. Port는 80, 443을 열어줍니다.

# main.tf
resource "aws_security_group" "DEV-FrontEnd-ALB" {
  name = "DEV-FrontEnd-ALB-SG"
  vpc_id = aws_vpc.DEV-VPC.id
  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  egress {
    from_port       = 0
    to_port         = 0
    protocol        = "-1"
    cidr_blocks     = ["0.0.0.0/0"]
    prefix_list_ids = []
  }
  tags = {
    Name    = "DEV-FrontEnd-ALB-SG"
    Service = "DEV"
  }
}

ALB


Splat Expression


DEV-Subnet를 Count로 2개 생성합니다. 생성한 결과로 Terraform은 아래와 같이 2개의 Subnet을 관리합니다.

DEV-Subnet = [
    {"id": "subnet-096", ...},
    {"id": "subnet-abd", ...}
]

Load Balancer를 생성할 때, DEV-Subnet에 속한 2개의 Subnet을 모두 추가하고 싶습니다.

이럴 때, Splat Expression을 활용하여 2개의 Subnet id를 추가할 수 있습니다.

subnets = aws_subnet.DEV-Subnet[*].id

아래는 Splat Expression과 동일한 표현입니다.

subnets = [subnet-096..., subnet-abd2...]

ALB 구성


보안그룹과 서브넷을 참조하여 로드밸런서를 생성합니다.

# main.tf
resource "aws_lb" "DEV-Front-ALB" {
  idle_timeout    = 60
  internal        = false
  name            = "DEV-Front-ALB"
  security_groups = [aws_security_group.DEV-FrontEnd-ALB.id]
  subnets         = aws_subnet.DEV-Subnet[*].id
  tags = {
    Name    = "DEV-Front-ALB"
    Service = "DEV"
  }
}

변수 생성 – 대상


variables.tf 파일에 타겟의 이름과 포트, 경로를 작성합니다.

이렇게 하면, 타겟이 변경될 때 variables.tf 파일만 수정하여 대응할 수 있습니다.

# variables.tf
variable "targets" {
  default = [
    {
      name = "user1"
      port = 1010
      path = "/user"
    },
      name = "user2"
      port = 2020
      path = "/v2/user"
    }
  ]
}
    

변수 생성 – 유지보수


EC2의 유형을 변경하거나, ALB Target Group에서 제외하는 경우 이를 Terraform으로 제어할 수 있습니다.


maintenance-mode 변수는 여러 서버 중 유지보수할 서버 및 ALB를 선택할 때 사용하며 반드시 EC2와 ALB가 같은 값일 필요는 없습니다.

# variables.tf
variable "maintenance-mode" {
  default = {
    user = {
      ec2 = 0
      alb = 0
    },
    web = {
      ec2 = 0
      alb = 0
    }
    (...)
  }
}

 0은 일반적인 상황에 적용하고, 유지보수 모드로 들어갈 경우 1을 사용합니다.


maintenance-instance 변수는 위에서 정의한 maintenance-mode와 함께 동작하도록 설계한 변수입니다.

유지보수할 서버를 선택하여 main.tf의 리소스에서 제외할 수 있습니다.

# variables.tf
variable "maintenance-instance" {
  default     = 0
}

서버가 4대일 때, 두번째 서버 유지보수는 maintenance-instance값을 1로 변경하여 ALB에서 제외한 후 진행합니다.


Target group ( + for_each)


variables.tf에서 정의한 변수 targets를 대상으로 target group을 생성합니다.

이 때 리소스를 동적으로 생성할 수 있는 for_each 표현을 활용합니다.

for_each = { for target in var.targets : target.name => target }

Terraform언어의 for_each를 자세히 알아보기 위하여 비슷한 문법인 Javascript언어의 foreach를 통해 알아봅니다.

배열 속 요소를 변수 element에 저장한 후 출력하는 예제입니다.

const array1 = ['a', 'b', 'c'];
array1.forEach(element => console.log(element));
// expected output: "a"
// expected output: "b"
// expected output: "c"

가장 중요한 것은 배열 내 변수를 순서대로 불러와 element변수에 저장을 한다는 사실입니다.


Terraform에서 사용하는 문법과 Javascript에서 사용하는 문법을 비교해봅니다.

반복할 배열(Array)을 지정합니다.

# Terraform
for target in var.targets
// Javascript
array1

배열에 포함된 요소로 수행할 내용을 정의합니다.

# Terraform
target.name => target
// Javascript
element => console.log(element)

for_each 변수는 반복하여 target을 each.key와 each.value로 할당합니다.

# main.tf
resource "aws_lb_target_group" "ALB-Target-User" {
  for_each = { for target in var.targets : target.name => target }
  vpc_id   = aws_vpc.PROD-VPC.id
  name     = "Front-ALB-${each.value.name}-TG"
  port     = each.value.port
  protocol = "HTTP"
  tags = {
    Name    = "Front-ALB-${each.value.name}-Target-Group"
    Path    = each.value.path
  }
}

Target Group Attatchment ( + 조건문, Element, Floor)


Target Group Attachment는 유지보수 유무에 따라 리소스 생성 개수를 동적으로 변화합니다.

이 때 필요한 문법이 바로 조건문입니다.

조건문이란 조건(condition)이 참(True)인 경우 A를 수행하며, 그렇지 않은 경우 B를 수행합니다.

count = condition ? A : B

count 변수는 조건이 참인 경우 A를 할당받고 거짓인 경우는 B를 할당받습니다.


USER EC2 한 대를 ALB에서 제외하려는 상황이라고 가정해보죠.

EC2의 유형은 변경하지 않고 ALB만 제외하기 때문에 alb 값을 0에서 1로 변경합니다.

# variables.tf
variable "maintenance-mode" {
  default = {
    user = {
      ec2 = 0
      alb = 1
    },
    (...)
  }
}

만약 아래와 같은 조건이 있는 경우라면, user의 alb값을 1로 바꿨기 때문에 조건을 만족하게 됩니다.

var.maintenance-mode["user"]["alb"] == 1

조건을 만족할 때, Target Group Attatchment는 User Target Group개수와 User EC2개수에서 1을 뺀 값의 곱만큼 생성합니다.

length(aws_lb_target_group.ALB-Target-User) * (length(aws_instance.user) - 1)

1을 제외하는 이유는 유지보수를 위하여 한 대를 제외하였기 때문입니다.


만약 조건을 만족하지 않은 경우라면 어떨까요?

그 때는 User EC2개수를 그대로 곱합니다.

length(aws_lb_target_group.ALB-Target-User) * length(aws_instance.user)

생성할 리소스의 개수는 정했습니다.

다음으로 필요한 값은 Target Group Arn입니다.

먼저 Arn을 동적으로 추가하기 위하여 사용할 Element 함수와 Floor 함수를 살펴봅니다.

Element 함수는 주어진 배열내에서 주어진 인덱스 값에 해당하는 요소를 반환하는 함수입니다.

element(["a", "b", "c"], 1)

결과는 배열 내 두번째에 위치하고 있는 문자열 b를 반환하겠죠?


Floor 함수는 소수점 이하의 값을 버리는 내림 함수입니다.

floor(4.9)

소수점 첫째 자리인 9를 버려, 결과는 숫자 4를 반환합니다.


유지보수가 진행된다면, arn는 아래의 값을 할당받습니다.

aws_lb_target_group.ALB-Target-User[element(var.targets, floor(count.index / (length(aws_instance.user) - 1))).name].arn

Target Group Arn은 Floor의 값이 변화할 때 Arn의 값이 변합니다. 즉 A A A / B B B / C C C 순서로 할당합니다.


Target Id를 동적으로 할당할 때 사용하는 연산자는 % 입니다.

% 연산자는 나머지를 반환합니다.

10 % 8 # 2
 4 % 4 # 0
 3 % 4 # 3
 2 % 4 # 2

결과는 주석으로 나타냈으니 참고하세요.


유지보수 모드인 경우 Target Id는 아래의 값을 할당받습니다.

aws_instance.user[[for number in range(length(aws_instance.user)) : number if number != var.maintenance-instance][count.index % (length(aws_instance.user) - 1)]].id

유지보수 중인 인스턴스를 제외한 나머지 인스턴스를 추가합니다. Id는 A B C / A B C / A B C 순서로 할당합니다.


모든 코드를 정리하면 아래와 같이 표현할 수 있습니다.

resource "aws_lb_target_group_attachment" "ALB-Target-Attach-User" {
  count            = (
    var.maintenance-mode["user"]["alb"] == 1 ?
    length(aws_lb_target_group.ALB-Target-User) * (length(aws_instance.user) - 1) :
    length(aws_lb_target_group.ALB-Target-User) * length(aws_instance.user)
  )
  target_group_arn = (
    var.maintenance-mode["user"]["alb"] == 1 ?
    aws_lb_target_group.ALB-Target-User[element(var.targets, floor(count.index / (length(aws_instance.user) - 1))).name].arn :
    aws_lb_target_group.ALB-Target-User[element(var.targets, floor(count.index / length(aws_instance.user))).name].arn
  )
  target_id        = (
    var.maintenance-mode["user"]["alb"] == 1 ?
    aws_instance.user[[for number in range(length(aws_instance.user)) : number if number != var.maintenance-instance][count.index % (length(aws_instance.user) - 1)]].id :
    aws_instance.user[count.index % length(aws_instance.user)].id
  )
}

Listener rule


경로 기반으로 작성할 경우 실제 경로 뒤에 “/*” 을 추가합니다.

resource "aws_lb_listener_rule" "https-user" {
  count        = length(aws_lb_target_group.ALB-Target-User)
  listener_arn = aws_lb_listener.https.arn
  action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.ALB-Target-User[element(var.targets, count.index).name].arn
  }
  condition {
    path_pattern {
      values = ["${aws_lb_target_group.ALB-Target-User[element(var.targets, count.index).name].tags["Path"]}/*"]
    }
  }
}

이 설명은 테라폼과는 관련없지만 생성 시 알게된 부분이라 추가하였습니다.


마지막으로,

끝까지 읽어주신 모든 분들께 감사드립니다.


다음 글 보기

이전 글 보기