팀 인프라 개선 프로젝트 : AWS 전환, 컨테이너화, IaC, 배포 정책 등 본문
인프라 땅울림
회사 레거시 앱들의 배포, 인프라 관리가 많이 아쉬웠다. VM 여러 개에 Jar 파일을 직접 배포하는 식이었고, 로깅은 파일로 확인, 매트릭 모니터링, 배포 자동화는 전혀 안되어 있었다. 매트릭을 모니터링하기 시작했고, Loki와 CloudWatch로 로그를 수집, 검색할 수 있도록 하였다. APM을 교체했고, 배포 전략을 구체화했다. 이런 자잘한 개선들과 자동화를 꾸준히 만들어왔지만, 근본적인 문제는 결국 개선되지 않고 있었다.
// 기존 제약 사항
1. 서비스가 다운되면 직접 확인하고, 배포해야 했다.
2. 애플리케이션 리소스가 VM 타입에 제한적이었고, 서비스가 VM의 상태에 의존되었다.
3. 헬스 체크와 이를 바탕으로한 라우팅 규칙 수정이 수동적이었다.
급한 프로젝트들을 어느 정도 정리하고 팀에 시간이 생겼다. 현재 운영 중인 인프라들을 개선하고, 전략을 재정비할 수 있는 시간을 아예 프로젝트로 따낼 수 있었다. 개선한 이후에 변화는 다음과 같다.
// 개선 이후 변화
1. 인프라 자원들을 코드로 관리한다.
2. 무중단 롤링 배포, Blue/Green 배포가 가능하다.
3. 프로세스들이 헬스 체크되어 정해진 수를 유지한다.
4. 모니터링, 로깅, APM을 개선하고 비용을 줄였다.
그 개선 과정에서 고민했던 재밌었던 키워드들을 정리한다.
코드로 인프라 관리하기
이전 인프라를 관리했던 팀원들이 이제 팀에 남지 않았다. 히스토리를 모르는 경우가 많아, 운영과 개선에 어려움이 많았다. 기존 Azure에서 AWS, ECS로 서비스를 이전하면서, 새로 생성하는 AWS 리소스들은 코드로 관리하는 것을 원칙으로 하였다. Terraform을 사용하여 Vpc와 Subnet, LB, Route Table 등 서비스를 위한 AWS 자원들을 모두 코드로 생성하고 변경한다.
커멘드 한 번으로 기존 인프라를 동일하게 구성하거나 쉽게 수정할 수 있고, 작업자와 변경 일자, 변경 포인트가 모두 남아 이력 관리가 가능했다. 무엇보다 다른 블로그를 찾아가며 메뉴를 선택해야 했던 AWS 콘솔 사용의 불편함에서 벗어나서, 이제는 인텔리제이 안에서 코드만으로 리소스 구성 요소를 찾아갈 수 있다. 예를 들어 우리 팀에선 locals 스코프 값 변경만으로, ECS의 리소스 타입이나 사이즈를 쉽게 수정할 수 있도록 구성했다. 비슷한 리소스를 복제하기도 쉽고, 이런 중복들을 함수로 만들 수도 있다. 틀을 한번 잡아놓으니, 인프라를 효율적으로 관리할 수 있는 강력한 무기가 되었다.
Spot 비율 조정, 싸면서도 안전하게
Spot은 온디멘드보다 70% 정도 저렴하다. 대신 언제 리소스가 반환될지 모르는 큰 위험이 있다. ECS 같은 오케스트레이션을 이용하면 리소스가 반환되어도 자동 재배포되기에 Spot이랑 사용하기 좋다. 대신 모든 리소스를 Spot으로 하게 되면 서비스 중단이 발생할 수 있으니, 서비스를 유지하기 위한 최소한의 수는 온디멘드로, 그 외에는 Spot으로 구성하는 것이 좋다.
Capacity Provider Strategy를 사용하면 ECS에서 관리되는 태스크의 Provider 타입의 비율을 지정할 수 있다. 예를 들어 아래처럼 구성하게 되면, 1개는 온디멘드로 보장하고, 그 외는 Spot으로 구성한다. 이를 테면 Desired count가 2라면 1개의 온디멘드, 1개의 Spot으로 구성된다. 그 상태에서 스케일-아웃이 발생하면 Spot이 추가, 스케일-인되면 Spot이 먼저 제거된다. 여러 태스크를 운영하여 HA와 성능을 챙기면서도, 저렴한 Spot으로 비용을 아낄 수 있도록 그 비율을 정책화할 수 있는 것이다.
resource "aws_ecs_service" "ecs_service_openapi" {
launch_type = null
capacity_provider_strategy {
capacity_provider = "FARGATE"
weight = 0
base = 1
}
capacity_provider_strategy {
capacity_provider = "FARGATE_SPOT"
weight = 1
base = 0
}
}
내부 DNS 기반 매트릭 수집
ECS를 사용하면 각 태스크의 엔드포인트가 고정적이지 않다. 태스크가 성성되면 ip가 바뀌고, 오토 스케일링으로 태스크가 동적으로 늘어나고 줄어든다. 이런 상황에서 각 태스크에 대한 매트릭을 어떻게 수집하면 좋을까를 고민했다.
AWS CloudMap은 동적 서비스 디스커버리를 위한 리소스이다. VPC 내부에 생성하면, 내부의 Fargate, EC2, ALB 등 다양한 자원에 내부에서 사용할 수 있는 도메인 네임을 지정할 수 있다. 이를 이용해서 ECS Service로 관리되는 여러 테스크를 한 도메인으로 묶을 수 있다. 이를 테면 Bastion 서버에서 A Service에 지정한 도메인의 IP를 검색하면, 해당 서비스로 실행된 Task의 내부 Ip가 쿼리된다.
프로메테우스에는 DNS 기반 서비스 디스커버리(dns_sd_configs)를 지원한다. n초(기본 30초)에 한 번씩 매트릭을 쿼리할 DNS로부터 Ip 목록을 확인하고, 모든 인스턴스들에 매트릭을 요청한다. 그라파나에서는 수집한 데이터의 인스턴스를 기준으로 대시보드를 구성하면 된다. 아래는 현재 up 상태의 인스턴스 목록을 Variable로 지정하여, 현재 서비스되고 있는 태스크의 매트릭을 찾아 출력할 수 있도록 구성한 결과이다. 매트릭을 확인하여 보고 싶은 인스턴스를 선택하여, 대시보드에 출력할 서비스를 결정한다.
처리 중인 요청까지 안전하게
ECS의 기본 업데이트 정책은 새로운 버전 태스크를 요구 수만큼 최대한 가동하고, 헬스 체크에 성공할 때마다 이전 버전 태스크를 제거하는 롤링 배포이다. 이때 기존 태스크를 바로 제거하게 되면, 현재 처리 중인 요청을 정상적으로 마무리할 수 없을 것이다. 이를 위해 ECS는 태스크 교체 시, 기존 태스크를 바로 종료하지 않고, 종료 신호 전달 후 Fargate의 정상 종료를 대기한다. 그 상태를 Draining이라 한다.
서버 애플리케이션 측에서도, 종료 신호에 현재 처리 중인 요청을 마무리하기 위한 정책이 필요하다. 이를 위해 Spring boot에선 Graceful shutdown이 옵션으로 제공한다. 이를 사용하면, SIGTERM을 전달받았을 때, 그 이후의 요청 수신을 막고, 현재 처리 중이었던 요청의 처리 결과를 확인한다. 기본 30초 동안 50ms 간격으로 남아있는 처리 중인 요청을 확인하길 반복하고, 모두 정상 처리 완료되었을 때 그제야 프로세스를 종료한다. (Github - Spring boot graceful shutdown 동작 원리)
ECS의 Drain 정책과 Spring boot의 Graceful shutdown 옵션을 함께 활용하면, 태스크가 교체되는 과정에서, 처리 중인 요청을 안전하게 마무리할 수 있다. 이때 ECS의 grace period를 Spring boot의 Graceful shutdown 대기 시간보다 같거나 길게 해야 할 것이다.
// ECS 테스크 교체 과정 (v1 -> v2)
1. v2 테스크를 실행한다.
2. v2 테스크가 요청을 처리할 준비가 되었는지 헬스 체크한다.
3. 라우팅 규칙을 수정, v1 테스크 대신 헬스 체크에 성공한 v2 테스크가 요청을 수신한다.
4. v1 테스크는 SIGTERM을 애플리케이션에 전달하여 종료를 표시한다.
5. Spring boot의 Graceful shutdown 정책으로 처리 중인 요청 처리를 마치길 대기한다. (기본 30초)
6. v1 테스크는 애플리케이션의 정상 종료를 대기한다.
7. 대기 시간 이후에도 애플리케이션이 정상 종료되지 않는다면 SIGKILL로 강제 종료한다. (기본 30초)
Blue / Green 배포 준비
앞선 롤링 업데이트 방식은 교체 중 이전 버전과 새로운 버전의 태스크가 공존하는 시간이 발생한다. 버전 간 공존이 있어선 안될 배포를 위해 ECS 전환 후 블루, 그린 배포 방법을 확인하고 테스트해 보았다.
방법은 간단하다. 새로운 버전의 Service를 하나 더 생성하고, 전면 LB에서 라우팅 규칙을 전환한다. LB에서 규칙을 수정한 순간 모든 요청을 새로운 버전의 Service로 전달하기에, 서로 다른 버전의 태스크가 공존하는 경우가 사라진다.
아래는 ALB에서의 예시인데, 규칙을 수정하고 대략 10초 정도 이후에 규칙이 반영되어 전환되는 것을 확인하였다. 전환은 무중단으로 이뤄지며, 기존 처리 중인 요청에 대한 세션을 잃지 않기에, 처리 중인 요청이 끊기지 않고 완료된다.
오픈 소스 APM
기존에는 데이터 독을 사용하고 있었다. 사실 팀에서 꽤 오래된 설정이 이어져온 것이어서, 팀에 사용 방법이나 히스토리를 알고 있는 사람이 남지 않았고, 더 이상 사용하지 않는 서비스나 설정들이 많아 오히려 잘 보지 않게 되었다. 무엇보다 이런 팀의 활용도에 비해 달마다 지불해야 했던 구독비가 아까웠다. 지금 팀에서 필요한 기능들을 커버할 수 있는 툴을 찾기 시작했다.
그리고 OpenTelemetry와 Jaeger를 찾았다. OpenTelemetry으로 트레이스 내의 스팬(API 요청, DB 쿼리 시간, Redis 액세스 등)을 수집하여 전달하고, Jaeger는 그 정보를 저장, 검색하고 대시보드에 출력하는 역할을 한다. 기록을 위해 여러 스토리지를 선택할 수 있는데, 로컬에서는 인-메모리로 빠르게 구성하거나, 운영 환경에선 ES와 TTL을 사용하여, 필요한 일정 기간 동안 기록할 수 있도록 정책을 잡을 수 있다.
아래는 이 둘을 이용하여, 응답 속도가 느린 API의 성능 분석을 확인한 결과 예시이다. 한 요청 안에서 어느 시점까지 어떤 레디스 커멘드 수행이 이어지고, 몇 개의 외부 API 호출이 동시에 수행되었으며 각각이 얼마나 걸렸는지, 그리고 그 이후에 어떤 DB 쿼리가 발생했는지 한눈에 확인하고 개선점을 찾을 수 있다. 메인 화면에서는 특정 기간 동안의 응답 분포를 확인하거나, 문제가 된 응답 시간만을 쿼리할 수 있고, 과거 이슈가 있던 요청 처리를 검색하여 디버깅에 사용할 수 있다.
생각보다 비싼 로그
로그 관리에 고민이 많았다. 처음 AWS에 인프라를 구축하면서 가장 편하게 구성했던 것은 역시 CloudWatch다. 테라폼에서 ECS 태스크에 'LogConfiguration' 정의 정도면 Task에서 출력한 Console 아웃풋이 CloudWatch에 기록된다. Retention 설정으로 보관 기간을 지정할 수 있고, S3에 쉽게 백업할 수 있다. Log insight도 강력하여, 빠르면서도 쿼리가 어렵지 않게 로그를 검색할 수 있었다. 대신 비싸다. 보관을 위한 저장 공간이 아니라, 데이터를 전송하는 비용이 비쌌다. 그래서 단순히 보관 기간을 줄이는 것은 의미가 없었고, CloudWatch로 전달하는 로그 양을 줄이는 것이 의미가 컸다.
이를 위해 Loki와 CloudWatch를 동시에 사용하게 되었다. Loki는 기존 인프라 구성에서도 사용하고 있던 방법이라, Promtail 설정이나 팀이 받아들이는데 문제가 전혀 되지 않았다. Loki는 구체적인 로그 검색과 디버깅을 위해 사용했다면, CloudWatch는 AWS 콘솔로 Task 정보와 함께 로그를 살피기 위한 용도로 사용했다. Task 자체가 정상적으로 배포되었는지 확인하거나, 어떤 에러로 종료되었을 때를 확인하는 데는 Loki보다는 AWS 콘솔과 CloudWatch가 더 편리했다. 모든 로그가 아닌, 서버 에러나 배포 설정과 같은 주요 로그만을 출력하기에 비용 문제도 없었다.
또, 로그 파일을 압축한 파일들을 S3로 주기적으로 업로드하는 스케줄러를 만들었다. Logback으로부터 나온 .log 파일은 Promtail이 읽어 2주간 Loki에서 검색되도록 관리하고, .zip으로 압축된 로그 파일은 스케줄러에 의해 주기적으로 S3에 업로드되어 관리된다. S3 버킷 설정으로, 60일 이후의 파일은 Glacier로 관리하고, 1년이 지난 파일은 자동으로 제거하는 정책을 추가하여, S3 관리 안에서도 오래된 정도에 따라 비용을 절감할 수 있도록 준비하였다. (S3는 EFS보다 13배 저렴, Glacier는 S3 Standard보다 5배 저렴하다.)
정리
작업 : 팀 인프라 개선, AWS 전환 및 컨테이너화
처리 :
- Azure to AWS 전환
- Azure에서 MariaDB 지원 종료, DB Dump와 Mysql (RDS) 전환
- Terraform 도입, AWS 리소스를 코드로 관리
- 컨테이너화, ECS 배포
- 배포 자동화, Gradle build > Container build > ECR upload > ECS image update > Terraform apply
- 배포 정책 정립, 무중단 배포와 블루/그린 배포
- Spot 인스턴스로 비용 절감, Task Provider 정책으로 On-demand 비율 유지
- 모니터링, APM
- 서비스 디스커버리, DNS 매트릭 수집
- ECS Task 동적 변화에도 매트릭 수집 처리
- DataDog 제거, 오픈소스 APM (OpenTelemetry / Jaeger) 구축으로 비용 절감
- 로깅 정책
- 비용 절감을 위한 CloudWatch, Loki 병행
- CloudWatch에는 Task 생명 주기와 관련된 최소한의 로그, Loki는 디버깅과 로그 검색
- 압축된 로그 S3 백업 스케줄러 개발, Glacier와 보관 기간 정책으로 비용 절감
'KimJinHwan > Project' 카테고리의 다른 글
포인트 차감과 보상 로직 구현, 동시성 문제 처리 (4) | 2025.09.08 |
---|---|
논 블록킹 처리, 처리량과 스레드 사용량 개선 (0) | 2025.09.04 |
이벤트 파이프라인 개선, 캐시 도입으로 처리량 높이기 (1) | 2025.08.28 |
클라우드 비용 절감기, 월 천만 원을 절약한 스케일 다운 (0) | 2025.04.05 |
팀에서 테라폼을 도입하고 얻은 것들 (0) | 2025.01.01 |