Post

서비스 운영 과정에서 AWS 인프라 성장기

애플리케이션 개발 초기에 AWS 클라우드를 이용해 처음 서비스를 배포했던 시점부터 현재의 인프라 구조에 이르기까지 어떤 고민과 이유로 인프라를 발전시켜 왔는지 단계적으로 설명하고, 최종적으로 구축된 AWS 아키텍처를 소개하겠습니다.


1. 초보 개발자의 인프라 구축


처음 서비스를 배포할 때, 지식이 부족하여 가장 쉽게 접근할 수 있는 방식으로 환경을 구축했습니다. 당시 선택한 구조는 EC2의 프리티어 사양(t2.micro)에서 Spring 애플리케이션과 MySQL을 실행시키는 방식이었습니다. 클라이언트도 마찬가지로 보편적인 방법인 Nginx를 이용해 React 빌드 파일을 서빙하였습니다.

vesrion-1 Image


초기 환경(version-1)의 문제점

이 구조의 가장 큰 문제점은 뭘까라고 생각해보면, 백엔드 서버의 자원이 (1g mem, 1vCPU)로 매우 적어 사용자가 조금만 들어오더라도 CPU 부하가 심해진다는 것입니다. (1vCPU로 Spring 쓰레드와 MySQL 쓰레드가 동작하기 때문에 컨텍스트 스위칭이 너무 자주 일어난다.)

따라서, 다음 구조에서 MySQL을 AWS RDS를 이용해 분리하여 구축하였습니다. AWS의 RDS의경우 프리티어의 1년 무료 혜택을 활용하면 비용을 증가시키지 않으면서 서버 부하를 효과적으로 줄일 수 있다고 판단하였습니다. 또한, 여기에 CI / CD를 이용하여 배포를 자동화 하기 위해 Jenkins를 도입하였습니다.

version-2 Image

AWS RDS를 활용한 DB 분리 및 Jenkins를 통한 CI/CD 도입

가장 큰 변화는 기존에 단일 서버에서 MySQL과 Spring 애플리케이션을 함께 운영하던 구조에서 AWS RDS를 활용하여 데이터베이스를 분리한 점입니다. 프리티어로 제공되는 RDS는 t4g.micro(2vCPU, 1GB 메모리) 스펙으로 구성되어 있으며, 이를 통해 결과적으로 서버의 하드웨어 자원이 기존보다 확대되어 (3vCPU, 2GB 메모리) 수준으로 확장된 효과를 얻었습니다.

추가적으로 EC2 인스턴스에 매번 직접 접속하여 빌드 및 배포를 수행했던 기존 프로세스를 개선하고자 Jenkins를 도입했습니다. 당시 Jenkins는 이미 사용법이 널리 알려져 있었고, 잘 정리된 튜토리얼 덕분에 빠르게 적용할 수 있었습니다.

version-2 문제점 인식

처음 이 구조를 도입했을 때는 상당히 만족스러웠으나, 조금 더 깊이 생각해보면 몇 가지 중요한 문제점이 존재했습니다.

먼저 RDS, 백엔드 서버, Jenkins 모두 Public 네트워크를 통해 접근 가능한 상태였습니다. 이는 외부 공격에 취약한 구조이며, 실제 침투가 발생할 경우 어느 지점으로 공격이 이루어졌는지 정확히 파악하기 어렵다는 보안상 취약점을 발견했습니다.

또한 Jenkins의 권장 운영 사양은 최소 4GB 이상의 메모리와 2코어 이상의 CPU입니다. 당시 구축한 환경은 이 권장 사양에 미치지 못했고, Jenkins라는 도구를 위해 EC2 한대를 더 사용해 추가적인 비용이 발생하고 있었습니다.

따라서, 다음 환경에서는 Public 네트워크에서 VPC로 전환 및 CI/CD Agent 변경을 진행하게 되었습니다.


2. AWS 퍼블릭 네트워크에서 VPC로 이동 + Github Actions


모든 서버에서 Public 접근을 차단하기 위해 특정 진입점(entry point)으로만 접근할 수 있도록 구성하고 나머지 서버들은 private 영역에 배치해야 합니다.

AWS의 기본 네트워크 환경은 기본적인 기능들을 제공하지만, 이는 단순히 쉽게 시작할 수 있도록 미리 구성된 구조입니다. 이 환경에서는 서브넷을 세분화하거나 퍼블릭/프라이빗 서브넷을 명확히 구분하여 커스텀 라우팅 테이블을 구성하는 데 제한이 있습니다. NAT 게이트웨이를 통한 프라이빗 서브넷 구성 등 세부적인 라우팅 설정이 어렵다는 한계로 인해, 기본 네트워크 환경에서 VPC 환경으로 변경하기로 결정했습니다.

version-3 Image

VPC(Virtual Private Cloud)는 private IP 주소를 활용해 격리된 네트워크 환경을 구축할 수 있는 서비스입니다. 인프라 설계 시 초기에 IP 주소 대역을 설정해야 하는데, 이번 프로젝트에서는 Class B 규모의 약 65,534개 IP 주소를 수용할 수 있도록 CIDR 블록을 /16으로 설정했습니다. 그 후, 2개의 퍼블릭 서브넷과 프라이빗 서브넷으로 IP 대역을 분할하여 퍼블릭 영역과 프라이빗 영역의 트래픽을 분리하고 라우팅을 제어했습니다.

이 구성은 두 개의 라우팅 테이블을 기반으로 동작합니다. 외부에서 들어오는 요청은 퍼블릭 라우팅 테이블을 통해 오직 퍼블릭 서브넷으로만 도달하도록 설정했습니다. 프라이빗 라우팅 테이블에는 VPC 내부 영역에서 NAT Gateway를 통해 외부 API 요청을 수행할 수 있는 규칙과 함께, VPC 내부 통신만 허용하도록 설정하여 외부에서는 오직 Reverse Proxy 서버를 통해서만 내부 서비스에 접근할 수 있도록 구성했습니다.

이러한 아키텍처 변경을 통해, version-2에서 발생했던 모든 서버가 public하게 노출되는 보안 취약점을 해결할 수 있었습니다. SSH 접속과 같은 관리 작업도 Reverse Proxy 서버를 경유해서만 API 서버나 RDS 서버에 접근할 수 있도록 하여 보안을 강화했습니다.


Jenkins -> Github Actions 변경

Jenkins는 앞서 언급한 바와 같이 최소 사양 요구치가 높고 툴 자체가 무거워 기본적으로 별도의 서버가 추가로 필요했습니다. 이에 대한 대안으로, GitHub Pro 계정 기준 월 2,000분의 빌드 시간을 제공하는 Github Actions를 도입하기로 결정했습니다. 이는 인프라 비용을 절감하면서도 CI/CD 에이전트를 위한 별도 환경 구축 및 유지보수가 필요 없는 최적의 대안이었습니다.


CodeDeploy와 S3를 도입한 이유

Github Actions로 CI 파이프라인을 변경하는 것까지는 성공적이었으나, 배포(CD) 단계에서 보안 문제가 발생했습니다. Bastion Host의 SSH 접근을 허용하는 Security Group 설정에서, GitHub Actions 러너는 동적으로 할당되는 IP를 사용하기 때문에 특정 IP 대역으로 접근을 제한할 수 없었습니다. SSH 포트(22)를 모든 IP에 개방하는 것은 보안 측면에서 위험하며, 이는 기존 ‘보안그룹 + 인증키(pem)’ 방식의 이중 보안에서 ‘인증키(pem)만 보유하면 모든 서버에 접근 가능한’ 취약한 구조로 전락할 위험이 있었습니다.

이러한 문제를 해결하기 위해 AWS 네이티브 서비스를 활용한 CD 파이프라인을 구축했습니다. Github Actions에서 CI 과정까지 완료한 애플리케이션 배포파일을 S3에 업로드한 후, AWS CodeDeploy를 통해 현재 위치 방식(애플리케이션 종료 후 재배포)으로 대상 서버에 배포하는 아키텍처를 구성했습니다.

1
2
3
4
aws deploy create-deployment \
  --application-name waba-bakend-cd \
  --deployment-group-name waba-codedeploy-group-name \
  --s3-location bucket=my-bucket,bundleType=zip,key=my-app.zip

위와 같은 AWS CLI 명령어를 Github Actions 파이프라인에 통합하고, AWS 환경에 CodeDeploy 애플리케이션 및 배포 그룹을 구성하여 S3를 중간 저장소로 활용하는 안전하고 효율적인 CD 파이프라인을 구축했습니다. 이 접근법은 SSH 접근 없이도 배포를 자동화할 수 있어 보안성과 편의성을 모두 확보할 수 있었습니다.


문제점 인식

VPC + Github Actions + CodeDeploy로의 변경을 완료했으나, 이 아키텍처에서도 몇 가지 중요한 고려사항이 남아있었습니다.

가장 우선적인 문제는 서버의 가용성이었습니다. 당시 서비스는 t2.micro 인스턴스 단일 서버로 운영되고 있었는데, 실제 운영 환경에서 이 규모의 인스턴스로는 안정적인 서비스 제공이 어렵다고 판단했습니다. 따라서 서버의 가용성을 판단하기 위해 부하 테스트를 실시하여 서비스의 실제 리소스 요구사항을 정확히 측정하고, 이를 바탕으로 최적의 인스턴스 유형과 규모를 결정해야 했습니다.

두 번째 문제는 비용 효율성이었습니다. 당시 인프라는 모든 리소스가 온디맨드 방식으로 구성되어 있었으며, 특히 NAT Gateway는 불필요하게 높은 비용이 발생하고 있었습니다. 이러한 비용 문제는 스타트업 프로젝트에서 장기적으로 지속 가능한 운영을 위해 반드시 해결해야 할 과제였습니다.

이러한 문제점들을 종합적으로 고려하여, 서비스의 안정적인 가용성을 확보하면서도 비용 효율성을 극대화할 수 있는 인프라 아키텍처로 재구성하기로 결정했습니다.


3. 요금 플랜 변경 및 NAT 인스턴스 도입


우선 AWS의 다양한 요금 플랜을 분석하여 비용 효율적인 구성을 모색했습니다. 자세한 요금 플랜 비교 내용은 AWS 요금플랜 글을 참고하시기 바랍니다. 분석 결과, 온디맨드 방식 외에도 스팟 인스턴스와 절감형 플랜 등의 옵션을 적절히 조합하면 기존 대비 약 3배의 비용 절감이 가능하다고 판단했습니다.

인스턴스 유형 선정을 위해 서비스 요구사항을 분석한 결과, ARM 기반의 t4g.micro가 성능과 비용 측면에서 가장 적합한 옵션으로 판단되어 이를 대상으로 부하테스트를 진행했습니다. 테스트 결과는 다음 세 편의 시리즈로 상세히 정리했습니다:

부하 테스트 1
부하 테스트 2
부하 테스트 3

부하테스트 결과, t4g.micro 인스턴스는 동시 접속 가상 사용자 500명까지 안정적으로 처리할 수 있음을 확인했고, 이는 현재 및 예상 트래픽을 충분히 수용할 수 있는 수준이라고 판단했습니다. 상위 모델인 t4g.xlarge는 vCPU가 2개 더 많지만 비용이 16배 증가하므로 성능-비용 효율성 측면에서 t4g.micro가 최적의 선택이었습니다.

또한, NAT Gateway의 요금 구조를 분석한 결과(처리된 데이터 GB당 USD 0.059 및 시간당 USD 0.059 비용 발생), 서비스 특성상 외부 API 호출이 빈번하지 않은 점을 고려할 때 비용 부담이 과도하다고 판단했습니다. 이에 NAT Gateway 대신 약 10배 저렴한 t3.nano 인스턴스를 NAT 인스턴스로 구성하여 대체 솔루션을 구현했습니다.

이러한 최적화를 통해 인스턴스 비용은 약 3배, NAT 관련 비용은 약 10배 절감하는 효과를 달성했습니다. 최종적으로 구성된 서비스 환경의 인프라 구성은 다음과 같습니다:

1
2
3
t4g.micro 1대 (API 서버)
t3.nano 1대 (Nat 인스턴스)
t3.micro 1대 (Reverse Proxy + Bastion)


개선할 부분 생각해보기


비용 효율화와 서비스 안정성을 위한 기반을 구축했지만, 서비스 운영과 확장성을 고려할 때 추가적인 개선 포인트를 파악해볼 수 있었습니다.

첫째, Load Balancer와 Auto Scaling 도입을 생각해 보았습니다. 현재 구성에서는 가상 사용자 500명까지는 안정적으로 처리 가능하지만, 실제 운영 환경에서는 이 임계값을 초과하는 상황이 충분히 발생할 수 있습니다. 이를 대비하여 Load Balancer와 Auto Scaling을 사용함으로써 갑작스러운 트래픽 증가나 일시적인 부하 집중 상황에 유연하게 대응할 수 있는 인프라를 구축할 수 있을 것입니다.

둘째, CI/CD 파이프라인의 개선이 필요합니다. 현재 사용 중인 현재 위치 배포 방식은 배포 진행 중 서비스 중단이 발생하는 단점이 있습니다. 이 문제는 AutoScailing과 Load Balancer를 도입하게 되면 좀 더 해결하기 쉬울 것이라고 판단하였습니다. 이를 통해 무중단 배포가 가능해지며, 배포 실패 시 신속하게 이전 버전으로 롤백할 수 있는 안전장치도 확보할 수 있습니다.

셋째, 웹 서버 아키텍처의 재검토가 필요합니다. 현재 웹서버로 사용 중인 t3.micro 인스턴스 대신 AWS의 CloudFront와 S3 조합으로 정적 웹 호스팅 구조로 전환하는 것이 유리할 것이라고 판단하였습니다. 이 구조는 사용량 기반 요금 체계를 제공하며, 서버 유지 비용 및 관리 부담을 크게 줄일 수 있습니다. 또한 글로벌 CDN(Content Delivery Network)을 통한 콘텐츠 전송으로 지연 시간 감소와 함께, 트래픽 증가에 따른 자동 확장성까지 확보할 수 있는 장점이 있습니다.

마지막으로, 모니터링 환경의 부재입니다. 서비스 운영 중 문제가 발생하거나 혹은 특정 기간동안 서버의 메트릭을 분석하고 싶어도 방법은 그래프가 불친절한 CloudWatch밖에 없었습니다. 물론 AWS의 Alert 기능은 매우 훌룡하다고 생각하지만 좀 더 직관적이고 사용하기 편한 모니터링 시스템을 도입하여 운영 환경에서 서비스 안정성을 높일 수 있다고판단하였습니다.


4. 최종 인프라 구조

version-4 Image

먼저 로드밸런서와 Auto Scaling을 도입하였습니다. Load Balancer의 경우 ALB(Application Load Balancer)를 사용하였고, 실제 로드밸런서로 서버 2대를 연결하여 부하테스트를 진행했을 때 1,000명의 가상 사용자 테스트에서도 안정적인 성능을 보여주었습니다. Load Balancer 사용 시 타겟 그룹을 Auto Scaling 그룹으로 설정하여, CPU 부하가 90%를 초과할 경우 자동으로 서버를 증설하도록 구성하였습니다. 비용 효율성을 위해 최소 서버 대수는 1대로, 최대 6대까지 확장될 수 있도록 설정하였습니다. 또한 stress 도구를 활용하여 CPU 사용량 증가에 따른 서버 자동 증설 기능이 정상적으로 작동하는지 검증하였습니다.

두 번째로, Load Balancer와 Auto Scaling 기반의 CodeDeploy 배포 방식을 블루/그린 방식으로 변경하였습니다. 이 접근법에서는 새로운 Auto Scaling 그룹을 생성하고 Load Balancer의 타겟 그룹을 새 그룹으로 전환합니다. 배포 후 정상 동작이 확인되면 기존 Auto Scaling 그룹을 삭제하는 방식으로 무중단 배포를 구현하였습니다.

세 번째는 Nginx 웹서버를 CloudFront + S3 조합으로 대체하였습니다. 이 구조에서는 빌드된 정적 리소스 파일들을 S3에 저장하고, CloudFront 배포 생성 시 S3를 원본으로 설정합니다. 배포 후에는 S3의 정적 파일들이 전 세계 엣지 로케이션에 캐싱되어 어디서든 빠른 접근이 가능해집니다. 현재 서비스가 한국 지역에 국한되어 있어 CloudFront의 글로벌 네트워크 이점을 최대한 활용하지는 못하지만, 웹서버 유지보수 비용 절감사용량 기반 과금 체계를 통해 비용 효율성을 높였습니다.

마지막으로, Prometheus와 Grafana 기반의 모니터링 환경을 구축하였습니다. 단일 서버에 Prometheus와 Grafana를 설치하여 Spring Boot 애플리케이션의 메트릭을 수집하도록 구성하였습니다. Spring Boot의 actuator 포트는 VPC 내부에서만 접근 가능하도록 설정하였으며, Prometheus가 이 포트를 통해 메트릭을 수집하고 Grafana에서 실시간 대시보드로 시각화합니다. Grafana 대시보드 접근을 위해 Bastion Server를 통한 SSH 포트 포워딩(SSH 터널링)을 구성하여, localhost:3000으로 안전하게 모니터링할 수 있는 환경을 마련하였습니다.


최종 요금 모델 설정

다음은 실제로 서비스에 사용하는 온디맨드 서버의 수입니다:

1
2
3
t4g.micro 2대 : API 서버 + test 서버
t3.nano 2대 : Bastion Server + NAT 인스턴스
t4g.small 1대 : 모니터링 서버

비용 최적화를 위해 다양한 AWS 요금 옵션을 활용하였습니다. Bastion Server는 스팟 인스턴스로 전환하여 시간당 $0.0006의 저렴한 비용으로 변경하였습니다.

NAT 인스턴스, API 서버, 테스트 서버, 그리고 모니터링 서버의 경우 Savings Plan(절감형 플랜)을 도입하여 약 40%의 비용 절감 효과를 달성하였습니다. 절감형 플랜은 선결제 없이 약정 기간 동안 사용량에 따라 할인된 요금이 적용되는 방식으로, 매월 AWS 요금 청구 시 자동으로 할인이 적용됩니다. 이러한 요금 모델 최적화를 통해 인프라 운영 비용을 효율적으로 관리할 수 있게 되었습니다.

절감형 플랜 구매 과정 Image


5. 더 나아가서…

2025년 2월까지 구축한 인프라 구조는 현재 서비스 규모와 요구사항에 최적화되어 있지만, 서비스 성장과 요구사항 변화에 따라 인프라 아키텍처 역시 진화해야한다고 생각합니다.

향후 대규모 환경 마이그레이션이나 인프라 확장을 고려한다면, Terraform과 같은 IaC(Infrastructure as Code) 도구를 도입하여 일관되고 재현 가능한 인프라 환경을 구축하는 방법을 검토할 계획입니다. 이는 인프라 변경 시 발생할 수 있는 휴먼 에러를 최소화하고 배포 과정을 자동화하는 데 큰 도움이 될 것입니다.

또한, 현재는 API 서버만 다중화되어 있어 DB와 Load Balancer가 여전히 단일 장애점(SPOF)으로 남아 있습니다. 서비스 규모가 확장됨에 따라 이러한 구성 요소들도 다중화하여 고가용성을 확보해야 할 것입니다. 또한 트래픽 증가에 대응하기 위해 Redis와 같은 캐시 서버 도입도 고려해 볼 수 있다고 생각하고, 관리해야 할 서버 대수가 증가하면 Kubernetes 환경으로의 마이그레이션도 검토할 가치가 있을 것입니다.

그럼에도 불구하고, 현 시점에서는 서비스 규모와 요구사항에 가장 적합한 인프라 환경을 구축했다고 자부합니다. 앞으로 인프라를 개선하거나 확장하게 된다면, 기존 아키텍처에서의 학습점과 새로운 요구사항을 고려한 의사결정 과정을 상세히 담은 후속 블로그를 통해 공유해보도록 하겠습니다.

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

© HeechanN. Some rights reserved.