Home 서버 리소스 변경으로 인한 롤링 배포 실패 해결기
Post
Cancel

서버 리소스 변경으로 인한 롤링 배포 실패 해결기

1. 글을 작성하게 된 계기


프로젝트에서 ECS 컨테이너 스펙을 다운그레이드 한 후, 배포가 실패 했습니다. 원인이 복합적이라 정말 찾기 까다로웠는데요, 어떤 문제가 있었고, 어떻게 해결했는지를 정리하기 위해 글을 작성하게 되었습니다.

프로젝트는 ECS/EC2, 테라폼(Terraform)을 사용했으며, 이를 기준으로 설명하겠습니다.





2. 문제 상황


프로젝트에서 배포 전략으로 롤링 배포(Rolling Deployment) 방식을 택했습니다. 사용자 트래픽과 인스턴스 개수가 적어서 배포 중, 일시적으로 한 서버가 부하를 받더라도 큰 문제가 없을 거라 판단했기 때문입니다.

A rolling deployment is a deployment strategy that slowly replaces previous versions of an application with new versions of an application by completely replacing the infrastructure on which the application is running.





문제는 부하 테스트 후, 비용 절감 을 위해 서버 리소스를 다운그레이드 하면서 발생했습니다. 정확히는 인스턴스 컨테이너의 CPU 를요. 컨테이너에 할당한 CPU 스펙이 낮아지며 애플리케이션 초기화 시간이 증가 했고, 초기화 완료 전, ALB의 헬스 체크가 실행 되어 UnHealthy 상태 가 되었기 때문입니다.

1
2
3
4
5
6
7
8
9
10
11
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.2.4)

2024-09-08T03:21:39.165+09:00  INFO 4456 --- [           main] project.dailyge.app.DailygeApplication   : Starting DailygeApplication using Java 17.0.9 with PID 4456 (/Users/mac/jun/dailyge/dailyge/dailyge-api/out/production/classes started by mac in /Users/mac/jun/dailyge/dailyge)
2024-09-08T03:21:39.166+09:00  INFO 4456 --- [           main] project.dailyge.app.DailygeApplication   : No active profile set, falling back to 1 default profile: "default"
......

애플리케이션 초기화는 애플리케이션이 새로 시작하는 것을 말합니다.





이를 그림으로 보면 다음과 같습니다. 배포될 애플리케이션의 초기화까지 시간이 오래 걸리기 때문에 헬스 체크에 응답하지 않았고, 헬스 체크가 실패하며 대상 그룹에서 제외되었습니다. 대상 그룹에서 제외되니 배포가 되지 않고요.

image





즉, 새로 배포할 애플리케이션이 실행되기 전 헬스 체크를 했고, 헬스 체크 API 호출이 실패 하며 로드 밸런서의 대상 그룹에서 제외되며, 기존 서버가 새로운 서버로 교체될 수 없었던 것 입니다. 해결하고 보니 많은 개념이 얽혀 있었고, 이해해야 할 부분이 많았던 문제였는데, 하나씩 살펴보죠.

1
2
3
4
5
6
7
8
9
10
11
@RestController
@RequiredArgsConstructor
@RequestMapping(path = {"/api"})
public class CommonApi {

    // 애플리케이션이 초기화 되지 않았으니, API 호출 자체가 되지 않는다.
    @GetMapping(path = {"/health-check"})
    public ApiResponse<String> healthCheck() {
        return ApiResponse.from(OK);
    }
}







3. 알고 있어야 할 개념


원인을 이해하기 위해서는 ALB의 상태, 인스턴스가 ALB에 등록되는 과정, 서버 리소스 따른 애플리케이션의 초기화 시간 등 몇 가지 개념을 알아야 합니다.

  1. ALB의 상태
  2. 인스턴스가 ALB에 등록되는 과정
  3. 서버 스펙에 따른 애플리케이션의 초기화 시간





3-1. ALB의 상태

ALB(Application Load Balancer)에는 Initial, Draining, Healthy, UnHealthy 와 같은 상태들이 존재합니다. 몇 가지 상태가 더 존재하지만, 위 네 가지에 대해서만 살펴보겠습니다. 다른 상태도 관심이 있다면 해당 링크를 참조해 주세요.

image





3-1-1. Initial

Initial는 로드 밸런서에 새로 등록된 대상에 대한 초기 설정 이나 헬스 체크가 진행 중 인 상태입니다. 이는 대상이 트래픽을 받을 준비가 되었는지 확인하는 단계로, 애플리케이션이 초기화 전이거나, 로드 밸런서가 해당 애플리케이션에 대한 첫 번째 헬스 체크를 수행하는 중입니다.

The load balancer is in the process of registering the target or performing the initial health checks on the target.



예를 들어, 다음과 같은 상황입니다.

  1. 새로운 EC2 인스턴스 또는 컨테이너가 대상 그룹에 등록되었지만, 아직 헬스 체크를 통과하지 않은 경우.
  2. ALB가 대상 그룹에 등록된 대상에 헬스 체크를 진행하며, 요청을 처리할 준비가 되었는지 확인하는 경우.





3-1-2. Draining

Draining 상태는 대상이 등록 취소 되고 있으며, 연결 종료(Connection Draining) 가 진행 중인 상태를 의미합니다. 이 상태에서는 대상이 더 이상 새로운 트래픽을 받지 않지만, 기존에 연결된 트래픽은 정상적으로 처리하고 종료할 수 있도록 합니다. 로드 밸런서는 대상을 삭제하기 전, 모든 기존 연결을 안전하게 종료하기 위해 이 과정을 진행합니다.

The target is deregistering and connection draining is in process.



예를 들어, 다음과 같은 상황입니다.

  1. ALB의 대상 그룹에서 EC2 인스턴스 또는 컨테이너가 더 이상 사용되지 않거나, 서비스 중지 또는 배포 등의 이유로 등록 취소될 때 발생합니다. 등록 취소된 후, 대상은 더 이상 새로운 요청을 수신하지 않습니다.
  2. 이미 연결된 트래픽을 처리한 후 연결을 종료합니다. 새로운 요청을 받지 않으며, 기존 연결을 안전하게 종료하고 나면 대상은 완전히 제거됩니다.





3-1-3. Healthy

Healthy 상태는 EC2 인스턴스 또는 컨테이너가 정상적으로 동작하고 있으며, 헬스 체크를 성공적으로 통과한 상태 입니다.

The target is healthy.





3-1-4. UnHealthy

UnHealthy 상태는 대상이 헬스 체크에 실패 하거나, 응답이 없어 비정상적인 상태 입니다. 대상이 트래픽을 처리할 준비가 되어 있지 않기 때문에 로드 밸런서는 트래픽을 해당 대상에 라우팅하지 않습니다. 대상이 일정 횟수 이상 헬스 체크에 실패하면 UnHealthy 상태로 전환됩니다.

The target did not respond to a health check or failed a health check.






3-2. 인스턴스가 ALB에 등록되는 과정

다음은 인스턴스가 ALB에 등록되는 과정입니다. 프로젝트에서는 ECS와 EC2를 사용했기 때문에, 이를 기준으로 살펴보겠습니다. 이는 크게 새로운 Task 생성, 헬스 체크, 기존 Task 종료의 과정을 거칩니다.

  1. 새로운 Task 생성
  2. 헬스 체크
  3. 기존 Task 종료




3-2-1. 새로운 Task 생성

ECS 롤링 배포를 하면 먼저 새로운 Task를 생성 합니다. 생성 직후, Task는 아직 ALB의 헬스 체크를 받지 않습니다. 이때 EC2나 ECS의 Fargate를 사용하면 EC2와 Fargate가 대상이 되며, ECS와 EC2를 사용하면 컨테이너 가 대상이 됩니다.

image






3-2-2. 헬스 체크

Task가 시작되면 ALB는 ECS의 컨테이너로 헬스 체크를 진행합니다. 헬스 체크가 성공하면 해당 인스턴스(컨테이너)를 대상 그룹(Target Group)에 등록합니다. 만약 헬스 체크가 실패하면, 해당 인스턴스( 컨테이너)는 Unhealthy 상태로 전환되고, ECS는 이를 Draining 상태로 설정해 더 이상 트래픽을 받지 않도록 합니다. 참고로 Draining 상태로 만드는 주체는 ALB가 아닌 ECS입니다. ALB는 해당 상태를 확인 후, 타겟 그룹에서 제외할 뿐입니다.

image





3-2-3. 기존 Task 종료

헬스 체크가 통과하면 기존 Task를 종료하며, 더 이상 트래픽을 전달하지 않습니다. 이를 통해 새로 등록된 Task로 교체되는 것이죠. 롤링 배포의 원리이기도 하고요.

image

image





3-3. 서버 스펙에 따른 애플리케이션 초기화 시간

서버 또는 컨테이너에 할당한 리소스에 따라 애플리케이션 구동 시간이 다릅니다. 예를 들어, t3a.medium을 사용했을 때, 컨테이너 CPU를 1,024로 할당하면 약 35초 정도 만에 애플리케이션이 초기화됐지만, CPU를 256으로 할당할 경우, 약 2분 30초 정도 초기화 시간이 걸렸습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
  ......
  "containerDefinitions": [
    {
      ......,
      
      "cpu": 1024,
      "memory": 1024,
      "memoryReservation": 512,
      
      ......
              
    }
  ],

  ......
          
}

어찌보면 너무 당연하지만, 이 부분을 간과해 원인을 찾기 까지 정말 많은 시간이 걸렸습니다.







4. 발생 원인


이제 문제가 왜 발생했는지 살펴보겠습니다. 문제가 발생한 원인은 부하 테스트가 끝난 후, 컨테이너 스펙 다운 그레이드, 짧은 헬스 체크 주기, 짧은 헬스 체크 유예 기간 이 복합적으로 얽혀 발생했습니다.

  1. 컨테이너 스펙 다운 그레이드
  2. 짧은 헬스 체크 주기
  3. 짧은 헬스 체크 유예 기간





4-1. 컨테이너 스펙 다운 그레이드

부하 테스트 시, t3a.medium 인스턴스를 사용했습니다. 테스트가 끝난 후, 비용 절감 을 위해 인스턴스 갯수를 줄이며, 컨테이너 스펙도 함께 다운 그레이드 했습니다. 사용자 수를 볼 때, 인스턴스 한 개 만으로도 충분하다고 판단했기 때문입니다. 가용 인스턴스, 즉 서버 리소스가 줄었기 때문에, 그만큼 컨테이너 스펙도 함께 축소했습니다. 이 때문에 재앙이 시작됐죠. 👿

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
  ......
  "containerDefinitions": [
    {
      ......,
      
      "cpu": 256,
      "memory": 512,
      "memoryReservation": 512,
      
      ......
              
    }
  ],

  ......
          
}





기존에는 35초 정도면 스프링 애플리케이션이 초기화됐지만, 컨테이너 스펙을 다운 그레이드 한 후, 초기화에 2분 30초 이상의 시간이 걸렸습니다.

image






4-2. 짧은 헬스 체크 주기

다음 문제는 헬스 체크 주기입니다. 문제가 발생했을 당시, 15초마다 헬스 체크를 하고 있었는데요, 이때, 3번 이상 실패하면 인스턴스를 대상 그룹에서 제외했습니다. 즉, 애플리케이션이 초기화 시간은 증가 했는데 헬스 체크 주기가 짧으니, 당연히 대상 그룹에 등록이 안 되겠죠?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
resource "aws_lb_target_group" "dailyge_api_alb_target_group_8080" {
  vpc_id               = var.vpc_id
  name                 = "dailyge-api-8080"
  port                 = 8080
  protocol             = "HTTP"
  target_type          = "instance"
  deregistration_delay = 30

  health_check {
    enabled             = true
    interval            = 15                    # 헬스 체크 주기
    path                = "/api/health-check"
    port                = "traffic-port"
    protocol            = "HTTP"
    timeout             = 5
    healthy_threshold   = 2
    unhealthy_threshold = 3                     # 헬스 체크 실패 횟수
    matcher             = "200-299"
  }

  tags = {
    Name = "Admin-Api prod target group."
  }
}






4-3. 짧은 헬스 체크 유예 기간

마지막으로 짧은 헬스 체크 유예 기간입니다. ECS를 사용할 때, 새로운 Task가 생성되면 리소스 초기화 까지, ALB가 헬스 체크를 하지 않는 기간 을 설정할 수 있습니다. 즉, 아래와 같이 기존 Task와 새로운 Task는 공존 하지만, 새로운 Task에는 일정 기간 헬스 체크를 하지 않는 것 이죠.

image






문제가 발생했을 당시, 이 설정이 15초로 돼 있었습니다. 즉, 컨테이너 스펙을 다운 그레이드 하면서 애플리케이션이 초기화되는 시간은 증가 했지만, 헬스 체크 유예 기간이 짧아 초기화 중, 헬스 체크를 받게 된 것입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
resource "aws_ecs_service" "dailyge_api_prod_service" {
  name            = "${var.cluster_name}-api-prod-service"
  cluster         = aws_ecs_cluster.dailyge_ecs_cluster.id
  task_definition = aws_ecs_task_definition.dailyge_api_prod_deploy_task_def.arn
  desired_count   = 2
  launch_type     = "EC2"

  deployment_controller {
    type = "ECS"
  }

  deployment_minimum_healthy_percent = 50
  deployment_maximum_percent         = 200

  load_balancer {
    target_group_arn = var.dailyge_api_target_group_arn_8080
    container_name   = "dailyge-api-prod-container"
    container_port   = 8080
  }

  deployment_circuit_breaker {
    enable   = true
    rollback = true
  }

  health_check_grace_period_seconds = 15           # 헬스 체크 유예 기간

  depends_on = [
    aws_iam_role_policy_attachment.ecs_service_role_policy_attach,
    aws_iam_policy.ecs_service_role_policy,
    aws_iam_role_policy_attachment.ecs_task_execution_role_policy_attach
  ]
}






위에서도 설명했지만, 애플리케이션이 실행되지 않았으니 당연히 API 호출은 실패하겠죠?

1
2
3
4
5
6
7
8
9
10
@RestController
@RequiredArgsConstructor
@RequestMapping(path = {"/api"})
public class CommonApi {

    @GetMapping(path = {"/health-check"})
    public ApiResponse<String> healthCheck() {
        return ApiResponse.from(OK);
    }
}






4-4. 정리

이를 정리해 보면, “서버 리소스를 다운 그레이드하며 애플리케이션이 초기화 시간은 증가 했지만, 헬스 체크 유예 기간이 짧아 초기화 과정에도 헬스 체크 를 받았고, 짧은 헬스 체크 간격 으로 인해 초기화되지 않은 애플리케이션에 너무 잦은 헬스 체크 를 했다” 입니다.

원인을 찾는데 정말 많은 시간이 걸렸는데, 결과만 놓고보니 참 간단해 보이네요? 😡







5. 문제 해결


원인이 파악되었으니, 해결책은 간단합니다. 컨테이너 스펙은 그대로 두고, 헬스 체크 기간과 헬스 체크 유예 기간을 증가시키는 것입니다. 현재 프로젝트는 테라폼을 사용하고 있기 때문에, 설정값 두 개만 바꿔서 문제를 해결할 수 있었습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
resource "aws_lb_target_group" "dailyge_api_alb_target_group_8080" {
  
  ......

  health_check {
    enabled             = true
    interval            = 30                    # 헬스 체크 주기
    path                = "/api/health-check"
    port                = "traffic-port"
    protocol            = "HTTP"
    timeout             = 5
    healthy_threshold   = 2
    unhealthy_threshold = 7                     # 헬스 체크 실패 횟수
    matcher             = "200-299"
  }

  ......
  
}
1
2
3
4
5
6
7
8
9
resource "aws_ecs_service" "dailyge_api_prod_service" {
  
  ......

  health_check_grace_period_seconds = 120           # 헬스 체크 유예 기간

  ......
  
}

후에는 다른 이슈가 생겨 CPU를 512로 올렸고, 애플리케이션 초기화 시간도 1분 20초 정도로 단축됐습니다.







6. 정리


프로젝트 초기부터 지금까지 정말 악랄하게 괴롭혔던 문제 중 하나였습니다. 잘 되다가 컨테이너 설정만 조금 바꾸면 배포 파이프라인 이 모조리 깨졌으니까요. 부하 테스트 이전에도 처음 CI/CD를 구축할 때, 테라폼 연습할 때 등 정말 많이 힘들게 했던 문제 중 하나입니다. 여튼, 결론은 서버 리소스를 변경하면 이로 인해 영향 받는 것들을 살펴보자 입니다.


This post is licensed under CC BY 4.0 by the author.