개발이군고구마

[멱살잡고 DevOps] 8. 확장성 고민과 ESC 적용 본문

Cloud Deploy

[멱살잡고 DevOps] 8. 확장성 고민과 ESC 적용

김구황 2025. 7. 29. 11:58
728x90
[Backend Dockerfile] 
1. backend Dockerfile 작성 🎆 
[AWS Front 설정] 
2. S3 버킷 & CloudFront 정적 웹 호스팅 활성화 
[AWS EC2 설정]
3. EC2 인스턴스 프로비저닝
4. EC2 환경 구성 🎆
[CI/CD] 
5. GitHub Actions CI/CD 구축 🎇
[테스트와 확장] 
6.DNS 설정 및 최초 배포 테스트
7. 배포환경 분리 (dev/prod)
[ECS] <- 현 단계
8. ECS 로 확장 🎇
[그외 확장] 
9.  백엔드 dev/prod 나누기 
10. 무중단 배포로 확장 
11. 쿠베네티스 연습

 


1. 언제 ECS/Kubernetes  적용을 고민해야하나? 

컨테이너 오케스트레이션(Container Orchestration) 툴:  여러 컨테이너를 '오케스트라의 지휘자'처럼 조화롭게 관리

 

1. 고가용성(High Availability)이 필요할 때 ✨

  • 고통: "지금은 EC2 서버 한 대가 죽으면 서비스 전체가 멈춥니다. 장애가 나면 개발자가 밤새 복구해야 해요."
  • 해결책: ECS/Kubernetes는 여러 대의 서버(또는 가상 자원)에 동일한 컨테이너를 최소 2개 이상 띄워놓습니다. 그리고 이 중 하나가 비정상 상태가 되면 자동으로 감지하여 제거하고, 건강한 새 컨테이너를 띄워 서비스를 중단 없이 유지합니다(이를 Self-healing이라 합니다).

2. 탄력적인 확장성(Auto Scaling)이 필요할 때

  • 고통: "사용자가 몰리는 특정 시간에 서비스가 너무 느려져요. 그렇다고 평소에 비싼 고사양 서버를 유지하기엔 비용이 부담됩니다."
  • 해결책: CPU 사용량 같은 지표에 따라 컨테이너 개수를 자동으로 2개에서 10개로 늘렸다가, 트래픽이 줄면 다시 2개로 줄여줍니다. 이를 **수평 확장(Horizontal Scaling)**이라 하며, 필요한 만큼만 자원을 사용하므로 비용 효율적입니다. 지금의 '더 좋은 EC2 인스턴스로 교체(Scale-up, 수직 확장)'하는 방식보다 훨씬 유연합니다.

3. 무중단 배포(Zero-Downtime Deployment)가 필요할 때

  • 고통: "새 버전을 배포할 때마다 docker-compose down/up을 하면서 몇 초간 서비스가 중단됩니다. 사용자들이 불편을 겪어요."
  • 해결책: 새로운 버전의 컨테이너를 먼저 띄운 뒤, 트래픽을 서서히 새 버전으로 옮기고, 구 버전 컨테이너를 안전하게 제거하는 블루/그린(Blue/Green) 배포카나리(Canary) 배포 같은 고급 배포 전략을 손쉽게 구현할 수 있습니다.

4. 마이크로서비스(Microservices) 아키텍처의 복잡성이 증가할 때

  • 고통: "기능이 점점 많아져서 백엔드 서비스를 여러 개로 쪼갰더니, 서비스 간의 통신이나 개별 배포 관리가 docker-compose만으로는 너무 복잡해졌어요."
  • 해결책: 수십, 수백 개의 마이크로서비스를 하나의 논리적인 단위로 묶어 관리하고, 서비스 검색(Service Discovery), 내부 네트워킹 등을 효율적으로 처리해줍니다.

 

🎍 ECS와 Kubernetes 공통점  

 

컨테이너를 배포·스케줄링·관리하는 오케스트레이션 솔루션

 

  • 컨테이너 라이프사이클 관리: 배포(Deployment), 스케일 업·다운, 상태 확인(헬스체크)
  • 서비스 디스커버리 & 로드밸런싱
  • 롤링/블루‑그린 배포, 자동 복구(셀프 힐링)

 

🎢  ECS와 Kubernetes 차이점
제어 플레인 완전 관리형(AWS) 오픈소스 → 직접 관리하거나 EKS 사용
러닝 커브 완만 가파름
이식성 AWS 전용 멀티 클라우드·온프레미스 이식 가능
생태계·툴링 AWS 네이티브(CloudWatch, IAM 통합) 방대한 CNCF 생태계(Helm, CRD, Operator 등)
커스터마이징 제한적(주로 AWS 기능 내에서) 무한대(커스텀 리소스·스케줄러·네트워크 정책 등)

 

 

 

 

2. 현재 나에게 무엇이 필요한가 

  • Kubernetes (EKS on AWS)
    • 압도적인 복잡성 소규모 팀이나 1인 개발자가 운영까지 책임지기엔 **운영 부담(Operational Overhead)**이 너무 큼
    • 비용
  • 단기적 해결책 (Scale-Up)
    • t3.small 등으로 **수직 확장(Scale-Up)**
    • 가장 빠르고 간단하게 성능을 개선
    • 근본적인 문제(단일 장애점, 수동 확장)는 해결되지 않음
  • ECS Fargate
    • ECS는 AWS 환경에 완벽하게 통합
    • 서버리스(Serverless) 컨테이너 방식. EC2 인스턴스를 아예 신경 쓸 필요 없이, "이 컨테이너를 CPU 1개, 메모리 2GB로 2개 띄워줘" 라고 명령만 하면 AWS가 알아서 인프라를 관리

 

 

3. ECS 적용

"서버가 죽으면 어떡하지?"라는 걱정에서 해방되고,
트래픽에 따라 자동으로 확장되며,
무중단 배포가 가능한 아키텍처로 발전

 

[개요]

  • **Application Load Balancer(ALB)**를 도입하여 트래픽을 분산
  • Node.js 백엔드와 Nginx를 ECS Fargate 서비스로 전환
  • 최소 컨테이너 수를 2개로 설정하여 고가용성을 확보하고, CPU 사용량에 기반한 자동 확장(Auto Scaling) 규칙을 추가

 

3-1. 소스 코드 수정

1️⃣ 추가 파일

- nginx Dockerfile

  • ECS 에서는 외부에서 설정 파일을 주입(Dockerhub) 하는 대신, 설정이 모두 포함된 '완성품' 이미지를 사용하는 것이 표준 
  • Dockerfile은 공식 Nginx 이미지에 nginx.conf 파일을 복사하여 이미지를 만듦 

2️⃣ 수정 파일 

- nginx.conf

  • 서버 주소를 node-app:5000에서 localhost:5000으로 변경
  • ECS의 'Task(작업)'라는 하나의 단위 안에서 실행되는 컨테이너들은 같은 네트워크 공간을 공유하므로, localhost를 통해 서로 통신할 수 있음

- docker-compose.yml

ㄴ로컬 개발 환경에서만 사용 

ㄴ운영 환경에서는 ECS 배포를 사용함 

  • Nginx 서비스에 build 컨텍스트 추가: nginx/Dockerfile을 이용해 로컬에서도 커스텀 Nginx 이미지를 빌드하도록 하여, 운영 환경과 최대한 유사한 환경.
  • logging 드라이버 제거: CloudWatch 로깅 설정은 운영 환경에만 필요하므로, 로컬 개발용 파일에서는 제거하여 구성을 단순화.

- backend cicd 스크립트 

ㄴ 배포 방식이 **'EC2에 접속해서 명령 실행'**에서 **'ECS에 새 버전 배포 요청'**으로 완전히 바뀜

  • ECR 로그인: Docker Hub 대신 AWS ECR에 로그인.
  • 두 이미지 빌드 & 푸시: node-app과 nginx 이미지를 각각 빌드하여 ECR에 푸시.
  • ECS에 배포: EC2에 명령을 내리던 SSM 액션을 제거하고, 새로운 ECS 배포 액션을 사용. 이 액션은 우리가 지정한 Task Definition 파일을 읽어, 방금 푸시한 새 이미지의 주소로 업데이트한 뒤, ECS 서비스에 이 새 버전을 배포하라고 요청

 

 

3-2. AWS 수정

1️⃣  ECR에 컨테이너 이미지 올리기 🚢

  • 보안: IAM 역할과 연동하여 특정 AWS 서비스(ECS)만 이미지를 내려받도록 권한을 세밀하게 제어
  • 속도와 안정성: AWS 내부망에 위치하므로 ECS가 이미지를 가져오는 속도가 훨씬 빠름. Docker Hub의 다운로드 제한(Rate Limit)으로부터 자유로움.
  • backend / nginx 리파지토리 2개 생성 

1) ECR 레지스트리 backend / nginx 별로 만들어주기 

2) AWS CLI 사용하여 docker 이미지 만들어 push (backend 와 nginx 이미지 별도로) 

이미지를 빌드할 때 폴더 경로를 backend / nginx 로 분명히 하고 빌드해야함 - dockerfile을 읽음

 

 

 

 

2️⃣ ECS Task Definition (작업 정의) 생성 📝

ㄴ docker-compose.yml의 클라우드 버전으로,

어떤 컨테이너들을 함께 실행할지, 각각의 자원(CPU, 메모리)은 얼마큼 할당할지를 정의 (=설계서)

 

- 1개의 작업에는 2개의 컨테어니가 함께 실행함 (nginx + backend) 

- 컨테이너들의 CPU 0.5 vCPU, 메모리 1 GB

- backend container 에는 docker compose 가 .env 를 읽었듯, env에 등록한 환경변수로 연결해둠 

 

 

 

 

3️⃣ ALB (Application Load Balancer) 생성 🚦

ㄴ 사용자의 모든 요청을 받아 건강한 컨테이너들에게 자동으로 분산

 

1)  고가용성을 위해 최소 두 개 이상의 AZ(Availability Zone)와 각 AZ에 하나씩의 퍼블릭 서브넷을 지정

- 각각 로드밸런서 노드가 배치되어, 트래픽 분산 및 장애 대비가 가능

2) 보안그룹 : HTTPS(443)와 HTTP(80) 포트를 소스 Anywhere (0.0.0.0/0)에 대해 허용

3) 리스닝 : 443 / 80->443

ㄴ ACM 같은 region에 생성한 것으로 연결해줌  (SSL 오프로딩 방식)

cloudflare에 ACM 정보를 CNAME 으로 입력해줘야함
80포트는 443 포트로 리다이렉트

 

 

4) 타켓그룹 : ip주소 - Fargate : 80 포트 

 

사용자 --(① HTTPS)--> CloudFront --(② HTTPS)--> ALB --(③ HTTP)--> ECS(Nginx)

  1. ① 구간 (사용자 ↔ CF): 글로벌 리전의 ACM 인증서로 암호화
  2. ② 구간 (CF ↔ ALB): 서울 리전의 ACM 인증서로 암호화
  3. ③ 구간 (ALB ↔ ECS): ALB가 암호화를 해제(Offloading)하고, 안전한 AWS 내부망을 통해 일반 HTTP로 통신

 

4️⃣ ECS 클러스터 및 서비스 생성 👷

ㄴ클러스터는 서비스들을 담는 논리적인 공간이고,

ㄴ 서비스는 '작업 정의'에 따라 컨테이너를 실제로 실행하고 관리(개수 유지, 자동 복구, 확장)하는 '현장 감독관'

 

1) 클러스터 생성 

2) 서비스 생성 

- 작업 정의 이어주기 

3) 로드 밸런싱

- target group에 container 넣어주기 

  • 인터넷 세상 → alb-sg: 누구나 HTTPS(443)으로 접근 가능
  • alb-sg → ecs-tasks-sg: 오직 ALB만이 HTTP(80)으로 컨테이너에 접근 가능

 

5️⃣ 아키텍처 연결 (CloudFront → ALB)

Cloudfront api 요청 origin / HTTPS 변경

 

 

 


[문제해결]

 

1. Bad Gateway 

 

 

ㄴ 프록시나 게이트웨이가 그 다음 단계의 서버로부터 유효하지 않은 응답을 받았을 때 발생

ㄴ " 연결은 되었는데, 상대방이 이상하거나 응답이 없다"

 

  • API 요청 경로는 [사용자 -> CloudFront -> ALB -> ECS(Nginx) -> ECS(Node.js)]
  • '게이트웨이' 역할을 하는 것은 CloudFront와 ALB
  • ALB가 ECS 컨테이너(Nginx)로부터 비정상적인 응답을 받았다는 의미일 가능성

ECS - Service Logs 를 통해 확인 - nginx의 해당 에러는 문제가 아님 -> 메인 랜딩으로

 

 

1) 우선, 아직 Fargate로 배포하는 스크립트를  push 하지 않은 상황. node.app 을 Fargate로 배포해야함 

  • backend deploy user에 AmazonECS_FullAccess 정책 연결해줌
  • json 파일 및 secret 추가 등 
  • node.app 서버 가동 안하는 문제 파악을 위해 cloudwatch 로그 연결해두는 작업 json 
"logConfiguration": {
    "logDriver": "awslogs",
    "options": {
        "awslogs-group": "/ecs/{작업}",
        "awslogs-create-group": "true",
        "awslogs-region": "ap-northeast-2",
        "awslogs-stream-prefix": "ecs"
    },
    "secretOptions": []
},

 

 

2) 💥 MongoDB ip 등록 문제 -> anywhere 로 변경 

FARGATE : AWS 가 가진 IP 풀에서 임의로 공인 IP 할당받아 접속

 

  • 기존 EC2 방식 (반려동물):  t2.micro라는 이름의 이 서버는 고유하고 고정된 공인 IP 주소를 가지고 있음.  이  IP를 MongoDB Atlas에 '우리 집 주소'라고 등록해두었고, Atlas는 이 주소에서 오는 요청만 허용
  • 새로운 ECS Fargate 방식 (가축): Fargate는 '서버'라는 개념이 없음. 대신 필요할 때마다 수많은 '가축' 떼(컨테이너 Task) 중 두 마리를 농장(클러스터)에 풀어놓는 것과 같음. 이 가축들은 이름도 없고, 매번 AWS가 가진 거대한 IP 주소 풀에서 임의의 공인 IP를 할당받아 인터넷에 접속. 작업이 중지되고 새로 시작될 때마다 완전히 다른 IP를 갖게 됩니다.

 

3) node.app의 로그도 확인이 안되는 문제 

  • task-definition command를 심어서 service update 버전업 진행 후 -> ECS fargate가 문제 없다는 것을 확인
  • 이후 app.js(node 어플리케이션 코드)에 콘솔로그를 심어서 어디에서 문제가 있는지 확인 

 

4) 인터넷 게이트웨이 :  Fargate에서 container 가 외부 (0.0.0.0/0)로 소통

  • MongoDB Atlas 서버의 주소조차 찾을 수 없거나(DNS 조회 실패), 해당 서버로 통하는 네트워크 경로가 원천적으로 막혀있다면(네트워크 타임아웃), 이는 애플리케이션이 처리할 수 있는 범위를 넘어서는 문제
💡 "컨테이너도 서버다"
  • Fargate에서 실행되는 컨테이너도 결국 VPC(가상 사설 클라우드) 내부에 존재하는, IP 주소를 가진 하나의 '논리적 서버'
  • Fargate Task(작업)가 실행될 때도 AWS는 자동으로 우리 VPC의 서브넷 중 하나에 ENI(Elastic Network Interface)라는 가상 네트워크 카드를 생성하여 할당
  • Task를 지정된 서브넷 중 하나에 랜덤하게 또는 분산하여 배치
  • 외부로 응답값이 빠져나가기 위해서는 서브넷의 라우팅 테이블에 인터넷 게이트웨이가 타겟으로 등록되어야 함 

 

 

 

🌐 502 에러 해결법
단서 : http는 통신하지만 https는 통신하지 못함 
🔥 Cloudfront가 요청한 "ALB( 원본 도메인(Origin domain)):443" 이 ALB에 연결된 ACM의 SSL 인증서와 다름 
  • CloudFront → ALB HTTPS 요청: CloudFront가 .elb.amazonaws.com:443로 HTTPS 요청
  • SSL 인증서 불일치: ALB의 SSL 인증서는  *.도메인 인데, 요청 도메인은 xxx.elb.amazonaws.com
  • SSL Handshake 실패: 도메인 불일치로 SSL 연결 실패
  • 502 Bad Gateway: CloudFront가 Origin에 연결할 수 없어서 발생

CloudFront (검문소 직원): "elb.amazonaws.com 씨, 신분증 보여주시죠" 라고 요청

 

ALB (방문객): (여전히 *. 라는 이름이 적힌 신분증을 보여준다)

 

요청한 주소와 신분증의 이름이 완전히 다릅니다. 통과할 수 없습니다.-> SSL 핸드셰이크 실패 -> 502 Bad Gateway 발생

 

 

 

-- 변경법 "Cloudflare에 별칭주소 생성" 

  • CloudFront: "별칭주소(*.도메인) 씨, 신분증 보여주시죠"
  • ALB: (*.도메인 신분증을 보여준다)
  • CloudFront: "네, 별칭주소(*.도메인)  는 *. 도메인  에 포함되는 이름이 맞군요. 통과하세요." -> SSL 핸드셰이크 성공!

 

Cloudflare 에 별칭 주소 입력 -> 와일드 카드에 해당하는 이름으로 지정해줌
ACM SSL 이 허용되는 도메인 주소로 요청이 들어오게 함

 

 

 

 

5) CORS 오류 

 Nginx와 Express가 동시에 CORS를 책임지면 캐시/헤더 충돌이 빈번 (node 에만 작성)

브라우저가 보낸 Origin 헤더가 CloudFront에 도착했지만, CloudFront는 캐시 효율성과 보안을 위해 기본적으로 대부분의 헤더를 원본 서버(ALB/Nginx)로 전달하지 않음

 

 


[최종 이미지]