Judaeng

Terraform Up & Running (3-2) - 테라폼 시작하기(웹 서버 클러스터 구성하기, 로드 밸런서 배포하기) 본문

DevOps/Terraform

Terraform Up & Running (3-2) - 테라폼 시작하기(웹 서버 클러스터 구성하기, 로드 밸런서 배포하기)

Judaeng 2023. 7. 19. 22:00

위 제목의 책을 통해 테라폼의 기본 개념, 정의 등을 공부하고 설치에서 운영까지 알아보자.

웹 서버 클러스터 구성하기


단일 서버를 기동시키는 것은 좋은 시작이었으나 실제 환경에서는 하나의 웹 서버만 존재한다면 단일 장애점 요소(SPOF, Single Point Of Failure)가 될 것이다.

서버가 크래쉬(crash) 되거나 트래픽 부하를 견디지 못했을 때 사용자는 서비스에 접근하지 못한다.

이와 같은 문제점을 제거하기 위해 웹 서버클러스터로 구성하고 하나의 서버에 문제가 발생하더라도 다른 서버들로 트래픽을 분산시켜 트래픽 규모에 맞춰서 클러스터를 늘리거나 줄여야 한다.

 

클러스터를 수동으로 운영하기 위해서는 많은 작업이 필요하다.

다행히도 아마존 웹 서비스에서는 자동 스케일링 그룹(ASG, Auto Scaling Group)을 지원하여 EC2 인스턴스를 관리하거나, 서버의 상태 정보를 감시하여 문제 있는 서버를 제외한다.

이렇게 클러스터 개수를 부하에 맞춰서 조정함으로써 클러스터 관리를 자동화할 수 있다.

 

자동 스케일링 그룹을 구성하기 위한 첫 번째 작업은 EC2 인스턴스를 ASG에 설정하는 시작 구성(launch configuration)을 생성하는 것이다.

테라폼의 리소스 이름은 aws_launch_configuration이며, aws_instance 리소스의 매개변수와 항목이 거의 비슷하다.

다음과 같이 간단하게 작성할 수 있다.

resource "aws_launch_configuration" "example" {
  image_id               = "ami-06ca3ca175f37dd66"
  instance_type          = "t2.micro"
  security_groups        = ["${aws_security_group.instance.id}"]
  
  user_data =  <<-EOF
              #!/bin/bash
              echo "Hello, World" > index.html
              nohup [python3 위치, 경로] -m http.server "${var.server_port}" &
              EOF
  
  lifecycle  {
    create_before_destroy = true
  }
}

한 가지 다른 점은 ASG의 시작 구성 설정을 위해 lifecycle 변수가 추가된 것이다.

lifecycle 변수는 meta-parameter의 한 예시이며, 테라폼의 모든 리소스에 존재하는 매개변수다.

lifecycle 항목을 모든 리소스에 추가하여 해당 리소스를 생성, 업데이트 또는 삭제하는 방법을 구성할 수 있다고 한다.

 

lifecycle에서 한 가지 가능한 설정은 create_before_destroy 값이며, true로 설정한다면 테라폼은 항상 기존 리소스가 삭제되기 전에 새로운 리소스를 생성한다.

예를 들어, EC2 인스턴스의 변화에 대해 create_before_destroy 값을 true로 한다면 항상 기존 인스턴스를 삭제하기 전에 새로운 인스턴스를 생성할 것이다.

 

어떤 리소스에 대해 create_before_destroy 값을 설정한다면 그 리소스에 관련된 모든 리소스를 같이 설정해야 한다(만약 설정되어 있지 않다면 의존성 에러가 발생한다고 한다).

시작 구성에 대해서는 보안 그룹에 대해 같이 create_before_destroy 값을 설정해야 한다고 한다.

resource "aws_security_group" "instance" {
  name = "terraform-example-instance"
  
  ingress {
    from_port   = "${var.server_port}"
    to_port     = "${var.server_port}"
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    }
    
    lifecycle  {
      create_before_destroy = true
    }
}

이제 ASG를 aws_autoscaling_group 리소스를 통해 생성한다.

resource "aws_autoscaling_group" "example" {
  launch_configuration = "${aws_launch_configuration.example.id}"
  
  min_size = 2
  max_size = 10
  
  tag  {
    key                  = "Name"
    value                = "terraform-asg-example"
    propagate_at_launch  = true
  }
}

ASG는 2개에서 10개의 EC2 인스턴스를 생성하고(기본적으로 초기 구성은 2개), 모든 EC2 인스턴스 태그 이름'terraform-asg-example'정의하였으며, 시작 구성에 대해서는 '${aws_launch_configuration.example.id}'와 같이 채움 참조 구문으로 변수 처리되었다.

또한, ASG를 원활하게 동작시키기 위해서는 avilability_zones 값을 하나 이상 정의해야 하며, 이 값은 ASG를 통해 생성되는 EC2 인스턴스를 어느 가용 영역에 배치할지를 정하는 설정이다.

각 AZ아마존 웹 서비스의 독립된 데이터 센터이며, 인스턴스는 여러 AZ에 걸쳐서 생성할 수 있다.

아마존 웹 서비스에 서비스를 구성할 때는 AZ에 문제가 발생할 수도 있다고 가정하고 설계를 해야 한다.

AZ 정보에 대해서 ["us-east-la", 'us-east-1b'] 형태로 직접 입력할 수 있으나 아마존 웹 서비스 계정마다 AZ 목록에 대해 차이가 있으므로 aws_availability_zones의 데이터 소스로 아마존 웹 서비스 계정에 있는 모든 가용 AZ를 가져오도록 설정해야 한다.

data "aws_availability_zones" "all" {}

데이터 소스는 공급자가 제공하는 읽기 전용 정보를 테라폼이 수행될 때마다 가져올 수 있다.

데이터 소스는 테라폼의 설정에 새로운 것을 추가하는 게 아니라 공급자의 API를 통해서 가지고 오는 것이다.

가용 영역의 정보뿐 아니라 AMI ID, IP 주소 대역, 현재 사용자의 정보 등을 가져올 수 있다.

 

데이터 소스를 참조하기 위해서는 다음과 같은 형태로 테라폼 코드를 작성한다.

"${data.TYPE.NAME.ATTRIBUTE}"

예를 들어, 다음은 aws_avilability_zones 데이터 소스에서 ASG의 avilability_zones 매개변수로 AZ의 이름을 전달하는 방법이다.

resource "aws_autoscaling_group" "example" {
  launch_configuration = "${aws_launch_configuration.example.id}"
  avilability_zones    = ["${data.aws_avilability_zones.all.names}"]
  
  min_size = 2
  max_size = 10
  
  tag  {
    key                  = "Name"
    value                = "terraform-asg-example"
    propagate_at_launch  = true
  }
}

지금까지 작성한 코드 확인

provider "aws" {
  region = "us-east-1"
}

resource "aws_launch_configuration" "example" {
  image_id               = "ami-06ca3ca175f37dd66"
  instance_type          = "t2.micro"
  security_groups        = ["${aws_security_group.instance.id}"]
  
  user_data =  <<-EOF
              #!/bin/bash
              echo "Hello, World" > index.html
              nohup [python3 위치, 경로] -m http.server "${var.server_port}" &
              EOF
  
  lifecycle  {
    create_before_destroy = true
  }
}

resource "aws_security_group" "instance" {
  name = "terraform-example-instance"

  ingress {
    from_port   = var.server_port
    to_port     = var.server_port
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  lifecycle {
    create_before_destroy = true
  }
}

resource "aws_autoscaling_group" "example" {
  launch_configuration = aws_launch_configuration.example.id
  vpc_zone_identifier = data.aws_subnet_ids.default.ids

  min_size = 2
  max_size = 10

  tag {
    key                 = "Name"
    value               = "terraform-asg-example"
    propagate_at_launch = true
  }
}

# VPC 설정
data "aws_subnet_ids" "default" {      
    vpc_id = data.aws_vpc.default.id   
}                                      

data "aws_vpc" "default" {             
    default = true                     
}        

variable "server_port" {
  description = "The port the server will user for HTTP requests"
  type        = number
  default     = 8080
}

⚠️에러 발생

│ Error: creating Auto Scaling Group (terraform-20230718083746951500000002): ValidationError: At least one Availability Zone or VPC Subnet is required.
│ 	status code: 400, request id: ca71a7c2-60bc-43d4-b142-8724e952b947
│
│   with aws_autoscaling_group.example,
│   on main.tf line 36, in resource "aws_autoscaling_group" "example":
│   36: resource "aws_autoscaling_group" "example" {

해결 방법

aws-vpc, aws-subnet

해결한 코드를 위에 지금까지 작성한 코드에 수정했다.

VPC 설정이 에러 부분을 수정한 곳이다.

로드 밸런서 배포하기


이전 실습에서 자동 스케일링 그룹을 배포하였지만, 한 가지 문제가 있다고 한다.

여러 개의 서버를 운영하기 위해서는 각 서버의 공인 IP가 아닌 하나의 대표 IP가 존재해야 한다고 한다.

이를 해결하기 위해 로드 밸런서를 활용해야 하며, 서비스 트래픽을 수용하기 위해 로드 밸런서의 IP 혹은 도메인 이름을 서비스 앞에 노출해야 한다.

이것은 높은 가용성과 다양한 확장성을 갖게 해준다.

아마존 웹 서비스에서는 다음 그림과 같이 ELB(Elastic Load Balancer)를 통해 부하 분산 서비스를 제공한다.

ELB를 테라폼에서 사용하기 위해 aws_elb의 리소스를 활용한다.

resource "aws_elb" "example"  {
  name               = "terraform-asg-example"
  availability_zones = ["${data.aws_availability_zones.all.names}"]
}

이 ELB는 여러 가용 영역에 걸쳐 구성되며, 라우팅 요청이 일어나기 전까지는 동작하지 않는다.

또한, 특정 포트에 대한 트래픽을 라우팅 하기 위해 하나 이상의 리스너를 설정한다.

resource "aws_elb" "example"  {
  name               = "terraform-aag-example"
  availability_zones = ["${data.aws_availability_zones.all.names}"]
  
  listener  {
    lb_port           = 80
    lb_protocol       = "http"
    instance_port     = "${var.server_port}"
    instance_protocol = "http"
  }
}

이 코드는 ELB가 포트 80(HTTP의 기본 포트)으로 HTTP 응답을 받아서 ASG에 있는 웹 서버로 트래픽을 전달하는 설정이다.

다시 한번 말하자면 ELB 역시 EC2와 같이 기본적으로 들어오고 나가는 트래픽에 대해서는 허용하지 않으며, 보안 그룹에 포트 80번 트래픽에 대해 정의한 후 연동시켜야 한다.

resource "aws_security_group" "elb"  {
  name               = "terraform-example-elb"
  
  ingress {
    from_port   = "80"
    to_port     = "80"
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

그리고 ELB의 security_groups 변수에 해당 보안 그룹을 추가한다.

resource "aws_elb" "example"  {
  name               = "terraform-aag-example"
  availability_zones = ["${data.aws_availability_zones.all.names}"]
  security_groups    = ["${aws_security_group.elb.id}"]
  
  listener  {
    lb_port           = 80
    lb_protocol       = "HTTP"
    instance_port     = "${var.server_port}"
    instance_protocol = "http"
  }
}

또한, ELB는 EC2 인스턴스의 상태를 지속해서 확인하는 기능이 있으며, 만약 인스턴스의 상태가 비정상적이면 해당 서버로 트래픽을 전달하지 않는다.

ELB에 상태 검사(health check) 블록을 설정하여 상태를 지속해서 확인할 수 있다.

예를 들어, 다음은 30초마다 '/' URL로 HTTP 요청을 하여 ASG에 있는 인스턴스의 응답이 "200 OK"인지 확인하는 설정이다.

resource "aws_elb" "example"  {
  name               = "terraform-aag-example"
  availability_zones = ["${data.aws_availability_zones.all.names}"]
  security_groups    = ["${aws_security_group.elb.id}"]
  
  listener  {
    lb_port           = 80
    lb_protocol       = "HTTP"
    instance_port     = "${var.server_port}"
    instance_protocol = "http"
  }
  
  health_check  {
    healthy_threshold   = 2
    unhealthy_threshold = 2
    timeout             = 3
    interval            = 30
    target = "HTTP:${var.server_port}/"
  }
}

상태 체크를 요청하기 위해서 ELB의 보안 그룹에 다음과 같이 내보내는 트래픽을 허용해야 한다.

resource "aws_security_group" "elb"  {
  name               = "terraform-example-elb"
  
  ingress {
    from_port   = "80"
    to_port     = "80"
    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"]
  }
}

어떻게 ELB는 요청을 보낼 EC2 인스턴스들을 알 수 있는가?

ELB의 인스턴스 매개변수를 사용하여 EC2 인스턴스의 고정된 목록을 ELB에 연결할 수 있지만, ASG를 사용하면 인스턴스가 언제든지 시작되거나 종료될 수 있으므로 고정된 목록을 활용할 수 없다.

대신에 aws_autoscaling_group 리소스에서 load_balancers 매개변수를 설정하여 인스턴스가 시작될 때 ELB에 각 인스턴스를 등록하도록 ASG에 요청할 수 있다고 한다.

resource "aws_autoscaling_group" "example"  {
  launch_configuration = "${aws_launch_configuration.example.id}"
  availability_zones   = ["${data.aws_availability_zones.all.names}"]
  
  load_balancers    = ["${aws_elb.example.name}"]
  health_check_type = "ELB"
  
  min_size = 2
  max_size = 10
  
  tag  {
    key                 = "Name"
    value               = "terraform-asg-example"
    propagate_at_launch = true
  }
}

health_check_type을 이제는 ELB로 설정하였으며, ASG를 통해 인스턴스가 정상적인지 판단하고 아니라면 다른 인스턴스로 교체하도록 요청한다.

output "elb_dns_name"  {
  value = "${aws_elb.example.dns_name}"
}

로드 밸런서를 배포하기 전에 마지막으로 단일 EC2 인스턴스에 설정한 IP 대신 ELB의 도메인 이름을 출력하도록 한다.

 

plan 명령어를 통해서 변경 사항이 정확히 적용되었는지 확인한다.

기존에 배포된 EC2 인스턴스는 삭제되고 새로 배포된 것을 확인할 수 있으며, 테라폼이 시작 구성, ASG, ELB 보안 그룹을 생성하는 것을 알 수 있다.

적용된 정보가 문제없는 것을 확인하고 apply 명령어를 수행이 완료되면 elb_dns_name이 출력된다.

Outputs:

elb_dns_name = "terraform-asg-example-1004241013.us-east-1.elb.amazonaws.com"

생성된 URL로 접속하게 되면 Hello, World가 잘 배포된 것을 확인할 수 있다.

출력된 URL을 복사한다.

인스턴스 시작 후 몇 분이 지나면 ELB에 인스턴스 상태 정보가 정상으로 확인될 것이다.

EC2 콘솔의 ASG 섹션을 통해서 배포된 것을 확인할 수 있으며, 아래 그림에서 ASG 생성 여부도 확인할 수 있다.

오토 스케일링 그룹 생성된 것을 확인

인스턴스 화면으로 이동하면 다음 아래 그림처럼 두 개의 인스턴스가 시작된 것을 확인할 수 있다.

인스턴스 2개가 배포된 것을 확인

마지막으로, 로드 밸런서 화면으로 이동 시 ELB가 생성된 것을 아래 그림에서 확인할 수 있다.

로드 밸런서도 생성된 것을 확인할 수 있다.

상태(status) 검사 항목에서 '2개의 인스턴스가 모두 다 서비스'라고 나오기까지 통상적으로 1~2분 소요된다.

여기까지 확인했다면 이전에 복사한 elb_dns_name을 복사하여 http 요청을 한다.

> curl http://<elb_dns_name>
"Hello, World"

curl elb_dns_name 데이터 전송 성공

성공했다!

이제부터 ELB는 EC2 인스턴스로 트래픽을 전달하며, URL을 요청할 때마다 다른 인스턴스에서 응답에 대해 요청을 하며, 웹 서버를 클러스터로 사용할 수 있게 되었다.

이 시점에서 클러스터가 새로운 인스턴스를 시작하거나, 이전 인스턴스를 종료하는데 어떻게 반응하는지 확인할 수 있다.

확인을 위해 EC2 대시보드의 인스턴스 탭으로 가서 인스턴스 중 하나를 선택한 뒤 콘솔 위에 있는 '작업' 버튼을 선택하고, 인스턴스 상태를 '종료(Terminate)'로 설정하여 해당 리소스를 삭제시킨다.

ELB가 하나의 인스턴스 상태가 비정상적임을 자동으로 감지하고 이에 대한 라우팅을 중지하므로 인스턴스를 삭제하더라도 요청에 대해 200 OK 응답을 받는다.

흥미롭게 인스턴스가 하나 삭제되어 상태가 비정상적인 상황을 ASG가 짧은 시간에 인지하여 자동으로 다시 두 개의 인스턴스를 맞추기 위해 새로 하나의 인스턴스를 생성한다(자체 복구).

만약 ASG의 인스턴스 수량을 변경하고자 하면, desired_capacity의 값을 변경하고 테라폼을 다시 수행한다.

 

지금까지 작성한 코드 확인

provider "aws" {
  region = "us-east-1"
}

# 이미지 설정
resource "aws_launch_configuration" "example" {
  image_id        = "ami-06ca3ca175f37dd66"
  instance_type   = "t2.micro"
  security_groups = ["${aws_security_group.instance.id}"]

  user_data = <<-EOF
              #!/bin/bash
              echo "Hello, World" > index.html
              nohup [python3 위치, 경로] -m http.server "${var.server_port}" &
              EOF

  lifecycle {
    create_before_destroy = true
  }
}

# 인스턴스에 대한 보안 그룹 설정
resource "aws_security_group" "instance" {
  name = "terraform-example-instance"

  ingress {
    from_port   = var.server_port
    to_port     = var.server_port
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  lifecycle {
    create_before_destroy = true
  }
}

# 로드 밸런서에 대한 보안 그룹 설정
resource "aws_security_group" "elb"  {
  name = "terraform-example-elb"
  
  ingress {
    from_port   = "80"
    to_port     = "80"
    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"]
  }
}

# 오토 스케일링 설정
resource "aws_autoscaling_group" "example"  {
  launch_configuration = "${aws_launch_configuration.example.id}"
  availability_zones = data.aws_availability_zones.all.names
  
  load_balancers    = ["${aws_elb.example.name}"]
  health_check_type = "ELB"
  
  min_size = 2
  max_size = 10
  
  tag  {
    key                 = "Name"
    value               = "terraform-asg-example"
    propagate_at_launch = true
  }
}

# 로드 밸랜서 설정
data "aws_availability_zones" "all" {}

resource "aws_elb" "example"  {
  name               = "terraform-asg-example"
  availability_zones = data.aws_availability_zones.all.names
  security_groups    = ["${aws_security_group.elb.id}"]
  
  listener  {
    lb_port           = 80
    lb_protocol       = "HTTP"
    instance_port     = "${var.server_port}"
    instance_protocol = "http"
  }
  
  health_check  {
    healthy_threshold   = 2
    unhealthy_threshold = 2
    timeout             = 3
    interval            = 30
    target = "HTTP:${var.server_port}/"
  }
}

# VPC 설정
variable "vpc_id" {}

data "aws_vpc" "example" {
  default = true
}

data "aws_subnets" "example" {
  filter {
    name   = "vpc-id"
    values = [var.vpc_id]
  }
}

variable "server_port" {
  description = "The port the server will user for HTTP requests"
  type        = number
  default     = 8080
}

output "elb_dns_name"  {
  value = "${aws_elb.example.dns_name}"
}

정리


지금까지 한 실습 부분에서 테라폼을 수행한 후에는 비용이 발행하지 않도록 작성한 모든 리소스를 제거하는 것을 권장한다.

테라폼은 사용자가 생성한 리소스를 추적하므로 다음과 같이 간단한 명령어로 정리가 가능하다.

> terraform destroy

Terraform used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
  - destroy

Terraform will perform the following actions:

  # aws_launch_configuration.example will be destroyed
  - resource "aws_launch_configuration" "example" {...}
  
Plan: 0 to add, 0 to change, 2 to destroy.

Do you really want to destroy all resources?
  Terraform will destroy all your managed infrastructure, as shown above.
  There is no undo. Only 'yes' will be accepted to confirm.

  Enter a value: yes

입력값을 'yes'로 작성하고 수행하면 테라폼의 의존성 그래프를 생성한 다음 우선순위에 맞게 최대한 병렬로 삭제한다.

1~2분 후에 아마존 웹 서비스 계정에 배포된 리소스가 삭제된다.

지금까지 작성한 테라폼 코드는 계속 유지하도록 하자.

배포된 실습 환경을 삭제하더라도 테라폼 코드는 계속해서 이 예제에 살을 덧붙일 것이므로 본인의 작업 공간에 저장해 놓아야 한다.

결국, 코드형 인프라의 장점이러한 리소스에 대한 모든 정보가 코드로 저장되고 언제든지 같은 인프라 환경을 한 줄의 코드(terraform apply)로 생성할 수 있다는 점이다.

또한, 깃을 사용하여 최근 변경사항을 커밋하면서 인프라에 대한 변경 사항을 추적할 수 있다.

결론


이제 테라폼 사용법에 대한 기본적인 지식을 얻었으며, 선언형 언어를 통해서 쉽게 원하는 인프라를 코드 형태로 만들 수 있는 부분을 확인하였다.

plan 명령어를 통해 변경 사항을 확인하고 생성할 때 발생할 오류에 대해 미리 확인할 수 있었다.

또한, 채움 참조 구문과 의존성을 통해 코드의 중복을 제거하고 더욱 설정이 유연한 형태로 만들 수 있었다.

 

하지만 아직 시작 단계에 불과하다고 한다.

이미 만들어진 인프라에 대해 어떻게 테라폼으로 유지할 수 있을지와 테라폼 코드를 구성하는 기본적인 방법에 대해 알아보자.

또한, 테라폼 모듈을 통해 어떻게 재사용할 수 있을지를 알아본다.

 

🤔 블로그 정리 후, 느낀 점

생각보다 에러가 많이 나서 당황했고, 고치는 게 쉽지 않았다.

어렵다고 하면 어려운 것 같고(?) 아니라면 아닌데... 무슨 말을 하려는지 잘 모르겠지만🥲 결국 테라폼에 대해 알고 있다면 쉬운 테라폼 실습이었던 것 같다.

VPC에 대한 내용은 책에 없었는데 생성해주지 않으면 에러 발생하는 경우가 위에 글에 발생한다.

이것을 해결하는 것이 쉽지 않았던 것 같다.

그리고 내 코드가 어떤 것을 가리키는지, 어떤 것을 생성, 변경할 것인지 확실하게 선언해야 돼서 잘 알고 선언했어야 되는 것 같다.

그리고 눈으로 "Hello, World"가 배포되는 것을 확인할 수 있어서 흥미로웠다.

그 외에 흥미로웠던 점은 코드를 작성하고 저장 후 "terraform apply!" 명령어를 입력하면 다 코드에 적힌 대로 생성되는 것이 재밌었다.

VPC(Virtual Private Cloud), ASG(오토 스케일링), Load Balancer(로드 밸런서), EC2 Instance(인스턴스), Security Groups(보안 그룹) 등이 다 적혀있는 대로 생성되는 게 신기하고 재밌었다.

물론 ASG(오토 스케일링)은 처음 생성해 보았고, 인스턴스를 하나 삭제시키면 문제가 있는 것을 감지하고 하나를 추가하는 것까지 재밌었던 것 같다.

시간이 또 된다면 이후에 책 뒷부분까지 할 수 있었으면 좋겠다.

 

📝 이번 게시물을 만들기 위해 참고한 사이트

1. AWS CLI 자격 증명하기 (aws configure 명령어) - 자격 증명 에러가 났을 때 해결함

Comments