기존 배포의 단점을 극복하는 CodeDeploy Blue/Green 방식으로 배포 전환
2026년 8월 3일
기존 "프라이빗 EC2 한 대 + pm2 + SSM Run Command" 배포에서 "Launch Template + Auto Scaling Group + CodeDeploy Blue/Green"으로 옮겼습니다.




1. 기존 배포(v1)
v1의 트래픽 경로와 배포 경로는 이전 게시물에 작성한 것처럼, 다음과 같았습니다.
브라우저
│ https://plick.co.kr https://m.plick.co.kr
▼
퍼블릭 ALB (TLS 종료, 호스트 헤더로 분기)
├─ plick.co.kr → tg-front-web → EC2:3000 (pm2 plick-web)
└─ m.plick.co.kr → tg-front-mobile → EC2:3001 (pm2 plick-mobile)
│
▼ (서버 fetch · /be 프록시)
내부 ALB → BE EC2 (Spring)
GitHub Actions (main 푸시)
├─ web·mobile 각각 빌드 → tar.gz + release.sh를 S3에 업로드
└─ SSM send-command → EC2 한 대가 내려받아 심볼릭 링크 교체 · pm2 재기동
EC2는 프라이빗 서브넷에 있고 퍼블릭 IP도 SSH도 없습니다.
배포와 관리 접속은 전부 SSM입니다.
서버 안에는 releases/<sha> 디렉터리가 쌓이고 current 심볼릭 링크를 갈아끼우는 방식이라 롤백이 링크 되돌리기 한 줄이라 편리합니다.
이 구성 자체도 나름 공들여 만든 것이었고 잘 동작했습니다.
2. v1의 구조적 한계
다만 기존 방식에 네가지정도 한계가 있다고 생각했습니다.
첫째, 서버가 pet입니다. Node, pm2, 디렉터리 구조, .env까지 전부 손으로 세팅했습니다. 인스턴스가 죽으면 문서를 보며 처음부터 다시 세워야 합니다. 재현이 사람과 문서에 의존합니다.
둘째, 단일 인스턴스라 SPOF입니다. AZ 하나가 흔들리거나 인스턴스가 죽으면 서비스 전체가 내려갑니다. 스케일 아웃도 불가능합니다.
셋째, 배포가 in-place입니다. pm2 restart 순간 짧은 순단이 있고, 새 릴리스가 절반쯤 풀린 상태에서 실패하면 스크립트의 자동 롤백에 의존합니다. 검증이 끝나기 전에 이미 기존 프로세스를 죽인 상태라는 게 구조적 약점입니다.
넷째, 배포 대상이 인스턴스 ID 하나에 박혀 있습니다(EC2_INSTANCE_ID 시크릿). 인스턴스가 바뀌면 시크릿을 바꿔야 합니다.
v2는 이 넷을 각각 이렇게 해결합니다.
Launch Template + user data로 서버 셋업이 코드가 됩니다(인스턴스는 cattle).
ASG가 2개 AZ에 인스턴스를 유지하고 죽으면 알아서 다시 띄웁니다.
Blue/Green이라 새 버전을 별도 인스턴스에서 먼저 검증하고 트래픽을 넘기며, 검증 실패 시 기존(Blue)이 그대로 서비스 중이라 롤백이라는 행위 자체가 필요 없습니다.
배포 대상은 "배포 그룹"이라는 논리 이름이 되어 인스턴스가 몇 대든 어느 ID든 워크플로는 모릅니ㄷ.
3. v2 전체 그림
GitHub Actions (main 푸시)
├─ web·mobile 빌드 → appspec.yml + 훅 스크립트 + tar.gz 두 개를 bundle.zip 하나로
├─ S3 업로드 (plick-deploy/frontend/<sha>/bundle.zip)
└─ aws deploy create-deployment (Blue/Green)
│
▼
CodeDeploy
① 현재 ASG를 복제해 Green 인스턴스 기동 (user data가 Node·pm2·에이전트 설치)
② Green에 번들 배포 (훅: 압축 해제 → .env 생성 → pm2 기동)
③ ValidateService — 3000·3001 /api/health 검증
④ 성공 시 Green을 두 대상 그룹에 등록, Blue 제외
⑤ 유예(5분) 후 Blue 종료
트래픽 경로는 v1과 같습니다. ALB, 리스너 규칙, Route 53, ACM은 하나도 손대지 않았습니다.
바뀌는 건 "과녁 뒤의 인스턴스가 어떻게 태어나고 교체되는가"뿐입니다.
설계에서 중요한 결정 하나는
한 인스턴스에 두 앱(web :3000, mobile :3001)이 같이 뜨는 구조를 유지했고, 그래서 배포 단위를 v1의 "앱별 2건"에서 "두 앱을 담은 번들 1건"으로 바꿨습니다.
Blue/Green에서는 인스턴스가 통째로 교체되는데, 앱별로 따로 배포하면 한 ASG를 두 배포가 서로 복제하고 종료하며 싸우기 때문입니다.
배포 그룹 하나, 배포 한 번, 검증은 두 포트 모두다 하는 방식입니다.
핵심 개념 정리
- Launch Template: 인스턴스의 출생 신고서. AMI, 타입, 보안그룹, IAM 프로파일, user data(첫 부팅 때 한 번 도는 셋업 스크립트)를 선언한다
- Auto Scaling Group(ASG): 이 템플릿으로 인스턴스를 N대 유지하는 관리자. 죽으면 다시 띄우고, 여러 AZ에 분산한다
- CodeDeploy Blue/Green: 현재 서비스 중인 세트(Blue)를 그대로 두고, ASG를 복제해 새 세트(Green)를 띄워 배포·검증한 뒤 로드밸런서의 과녁을 Green으로 바꾸는 방식. 검증 실패 시 트래픽은 Blue에 남는다
- appspec.yml: CodeDeploy 에이전트에게 "번들을 어디 풀고, 어느 시점에 어느 스크립트를 누구 권한으로 실행할지" 알려주는 명세. 훅 순서는
ApplicationStop → BeforeInstall → (파일 복사) → AfterInstall → ApplicationStart → ValidateService - SSM Parameter Store: .env의 새 집. 인스턴스가 수시로 태어나고 죽는 세계에서는 서버 안에 손으로 만든 .env가 성립하지 않으므로, 배포 훅이 매번 내려받아 생성한다
- iam:PassRole: "CodeDeploy가 새 인스턴스를 만들 때 그 인스턴스에 IAM 역할을 붙여도 되는가"를 통제하는 별도 권한. 역할을 넘겨주는 행위가 권한 상승 통로가 될 수 있어 AWS가 따로 끊어 뒀다
4. 구현 과정
1~7단계는 기존 서비스에 영향이 전혀 없고, 트래픽이 실제로 옮겨 가는 건 컷오버 한 순간뿐입니다.
v1 인스턴스는 그때까지 계속 서비스하며 진행했습니다.
4-0. 값 수집
시작 전에 콘솔에서 모아 둔 값들. 이후 모든 단계에서 계속 사용합니다.
계정 ID (12자리)
VPC ID
프라이빗 서브넷 ID 2개(2AZ)
현행 EC2 보안그룹 ID
인스턴스 타입
tg-front-web·tg-front-mobile 대상 그룹 ARN
BE 내부 ALB DNS 이름
4-1. Parameter Store — .env 이전
현행 EC2에 SSM 세션으로 들어가 cat /srv/plick-web/shared/.env 출력을 그대로 복사해, 파라미터 두 개를 SecureString(기본 aws/ssm 키)으로 만들었습니다.
손으로 옮겨 적지 않고 복붙한 건 의도적입니다.
HOSTNAME=0.0.0.0 같은 값은 옮기다 놓치기 딱 좋고, 이게 빠지면 Next가 127.0.0.1에만 묶여 대상 그룹이 전부 unhealthy가 됩니다.
/plick/frontend/web/env— web .env 전문/plick/frontend/mobile/env— mobile .env 전문

여기서 네이밍 고민이 하나 있었습니. 콘솔을 열어 보니 BE 팀 파라미터가 /plick/prod/DB_URL처럼 "변수 하나 = 파라미터 하나"로 이미 잔뜩 있었습니다.
맞출까 하다가 "앱당 파라미터 하나, .env 통째"로 갔습니다.
변수별로 쪼개면 파라미터 16개에, 배포 훅이 받아서 KEY=VALUE로 조립하는 파싱 로직이 생깁니다.
지금 방식은 get-parameter 한 번에 파일 저장으로 끝납니다.
그리고 경로를 /plick/prod/ 아래 넣으면 /plick/prod/* 읽기 권한을 가진 BE 인스턴스 롤이 프론트 파라미터까지 읽게 됩니다. /plick/frontend/*로 분리하면 팀별 IAM 경계가 깔끔하게 유지됩니다.
4-2. IAM 역할 3개
① plick-frontend-ec2-role (인스턴스용, 신규)
Green 인스턴스가 태어날 때마다 붙는 역할.
관리형 정책 AmazonSSMManagedInstanceCore(SSM 접속)와 AmazonEC2RoleforAWSCodeDeploy(에이전트의 S3 번들 읽기), 그리고 인라인 정책으로 파라미터 읽기:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": ["ssm:GetParameter", "ssm:GetParametersByPath"], "Resource": "arn:aws:ssm:ap-northeast-2:<계정ID>:parameter/plick/frontend/*" } ] }
파라미터 ARN의 서비스가 iam이 아니라 ssm인 것에 주의해야합니다.
EC2 사용 사례로 역할을 만들면 같은 이름의 인스턴스 프로파일이 자동으로 생기고, Launch Template에서 그걸 고릅니다.
② plick-frontend-codedeploy-role (CodeDeploy 서비스용, 신규)
CodeDeploy가 ASG를 복제하고 인스턴스를 만들고 지우는 권한.
AWSCodeDeployRole + AmazonEC2FullAccess + 인라인 PassRole:
{ "Effect": "Allow", "Action": "iam:PassRole", "Resource": "arn:aws:iam::<계정ID>:role/plick-frontend-ec2-role" }
이 PassRole이 없으면 Green 기동이 소리 없이 실패합니다.
참고로 원래 계획의 이름은 plick-codedeploy-role이었는데 BE도 곧 CodeDeploy를 쓸 예정이라 frontend를 붙여 팀별로 분리했습니다.
단순한 이름 문제가 아니라, 롤을 공유하면 나중에 BE의 PassRole까지 한 롤에 얹으면서 "프론트 배포가 BE 인스턴스 롤을 넘길 수 있는" 경계 붕괴가 생기기 때문입니다.
③ 기존 OIDC 롤(plick-frontend-deploy) 수정
GitHub Actions가 쓰는 롤.
신뢰 정책은 그대로 두고 인라인 정책에 codedeploy 권한(CreateDeployment, GetDeployment 등)을 추가했습니다.
기존 SSM 권한(ssm:SendCommand 등)은 지우지 않고 일단은 남겼습니다.
전환이 틀어지면 v1로 돌아가야 하니, 제거는 마지막 정리 단계에 하는것으로..

4-3. Launch Template
이름 plick-frontend-lt
AMI는 Ubuntu Server 24.04 LTS x86_64.
여기서 arm(Graviton)을 고르면 안 됩니다.
GitHub 러너(ubuntu x86_64)에서 빌드한 산출물에 sharp 같은 플랫폼 종속 네이티브 바이너리가 딸려 들어가서, 아키텍처가 다르면 서버에서 깨질수 있다고 합니다.
인스턴스 타입은 현행과 동일(t3.small),
키 페어 없음(SSH를 아예 안 쓴다),
서브넷은 템플릿에 지정하지 않음(ASG가 정함),
보안그룹은 현행 Next EC2 것 재사용
스토리지는 현행에 맞춰 30GB gp3, 종료 시 삭제 "예" (인스턴스가 수시로 종료되는 구조라 "아니요"면 고아 볼륨이 쌓입니다)
IAM 인스턴스 프로파일: plick-frontend-ec2-role
user data 전문.
v1에서 "SSM 세션 들어가서 서버 준비"라고 문서 한 절을 차지하던 손 세팅이 통째로 이 스크립트가 됐습니다.
#!/bin/bash set -euxo pipefail # Node 22 + pm2 (NodeSource는 인터넷이 필요하다 — NAT 경유) curl -fsSL https://deb.nodesource.com/setup_22.x | bash - apt-get install -y nodejs ruby-full npm install -g pm2 # CodeDeploy 에이전트 (리전 버킷은 S3 게이트웨이 엔드포인트로 닿는다) cd /tmp curl -O https://aws-codedeploy-ap-northeast-2.s3.ap-northeast-2.amazonaws.com/latest/install chmod +x ./install ./install auto systemctl enable codedeploy-agent # 배포 훅이 Parameter Store를 읽을 때 쓰는 aws CLI snap install aws-cli --classic # 앱 루트. 배포 훅이 ubuntu로 실행되므로 소유자를 맞춘다 mkdir -p /srv/plick chown -R ubuntu:ubuntu /srv/plick # 재부팅 시 pm2 자동 기동 env PATH=$PATH:/usr/bin pm2 startup systemd -u ubuntu --hp /home/ubuntu
user data는 첫 부팅에 한 번만 돕니다.
고치려면 템플릿 새 버전을 만들고 ASG의 참조 버전을 올려야 합니다.
ASG가 참조할 버전은 Latest가 아니라 Default로 두고, 검증된 버전만 Default로 승격하는 방침으로 갔습니다.

4-4. Auto Scaling Group — 사고 발생
-
이름
plick-frontend-asg, 템플릿plick-frontend-lt(버전 Default) -
기존 VPC의 프라이빗 서브넷 2개(2AZ)
-
로드 밸런싱: 기존 대상 그룹
tg-front-web·tg-front-mobile둘 다 연결 -
상태 확인: EC2 (ELB 아님), 유예 300초
-
Desired 2 / Min 2 / Max 4
상태 확인을 EC2로 두는 이유가 이 구조의 포인트입니다.
갓 태어난 인스턴스에는 앱이 없습니다(앱은 CodeDeploy가 나중에 얹습니다).
ELB 헬스체크 기준으로 하면 ASG가 "unhealthy네" 하고 인스턴스를 죽이고 새로 만들기를 무한 반복합니다.
콘솔이 "권장"이라며 ELB 체크를 켜라고 유혹하는데 여기서는 함정입니다.

그리고 여기서 첫 사고가 생겼습니다.
ASG가 띄운 첫 2대에 퍼블릭 IP가 붙어 나왔습니다.
프라이빗 서브넷 인스턴스에 있으면 안 되는 값입니다.
확인해 보니 서브넷 자체는 프라이빗이 맞는데(라우팅 0.0.0.0/0 → NAT)
서브넷의 "퍼블릭 IPv4 주소 자동 할당" 설정이 켜져 있었습니다.
Launch Template에서 네트워크 인터페이스 설정을 비워 두면 이 서브넷 설정을 따라갑니다.
라우팅에 IGW가 없어서 그 IP로 실제 인바운드는 성립하지 않아 보안 실해는 없을것같지만,
서브넷 2개의 자동 할당을 끄고 인스턴스를 종료해 ASG가 다시 만들게 했습니다. 빈 인스턴스 시점이라 부담 없이 갈아치울 수 있었습니다.

또 하나, ASG를 만드는 순간부터 빈 인스턴스 2대가 대상 그룹에 unhealthy로 등록됩니다.
기존 EC2가 healthy라 ALB는 그쪽으로만 보내니 서비스 영향은 없지만, 이 상태에서 기존 EC2까지 unhealthy가 되면 ALB가 fail-open으로 전 대상에 트래픽을 뿌립니다. 그래서 ASG 생성부터 첫 배포까지는 같은 날 몰아서 하기로 정했습니다.
4-5. CodeDeploy 애플리케이션 + 배포 그룹
-
애플리케이션
plick-frontend(EC2/온프레미스) -
배포 그룹
plick-frontend-dg, 서비스 역할plick-frontend-codedeploy-role -
배포 유형 블루/그린, 환경 구성 "Auto Scaling 그룹을 자동으로 복사" →
plick-frontend-asg -
배포 설정
CodeDeployDefault.AllAtOnce, 트래픽 재라우팅 즉시, Blue 종료 대기 5분 -
로드 밸런서: 대상 그룹에
tg-front-web·tg-front-mobile둘 다 지정
대상 그룹 둘 다 지정을 반드시 해줘야 합니다.
하나만 걸면 배포는 성공하는데, Blue가 종료된 뒤 나머지 도메인의 과녁이 비어 503이 납니다.
4-6. 리포지토리 변경 — 배포 명세가 코드로
plick/
├── .github/workflows/deploy.yml (전면 교체)
├── appspec.yml (신규 — 번들 루트에 있어야 해서 리포 루트)
└── scripts/deploy/
├── application_stop.sh
├── before_install.sh
├── after_install.sh
├── application_start.sh
└── validate_service.sh
appspec.yml:
version: 0.0 os: linux files: - source: / destination: /srv/plick/bundle file_exists_behavior: OVERWRITE hooks: ApplicationStop: - location: scripts/deploy/application_stop.sh timeout: 60 runas: ubuntu BeforeInstall: - location: scripts/deploy/before_install.sh timeout: 60 runas: ubuntu AfterInstall: - location: scripts/deploy/after_install.sh timeout: 300 runas: ubuntu ApplicationStart: - location: scripts/deploy/application_start.sh timeout: 120 runas: ubuntu ValidateService: - location: scripts/deploy/validate_service.sh timeout: 180 runas: ubuntu
runas: ubuntu가 v1의 sudo -u ubuntu -H 를 대체합니다.
에이전트는 root로 돌지만 훅을 ubuntu로 내려 주므로, root로 pm2를 건드려 데몬이 유저별로 두 개 뜨는 v1 시절의 사고가 원천 차단됩니다.
훅 다섯 개는 v1 release.sh 하나가 하던 일을 시점별로 쪼갠 것입니다.
-
application_stop: pm2 프로세스가 있으면 stop. Green은 매번 새 인스턴스라 사실상 보험입니다 (이전 리비전이 없으면 이 훅 자체를 건너뛴다)
-
before_install:
/srv/plick/{web,mobile}을 비우고 다시 생성. 멱등하게 -
after_install (핵심): tarball을 앱 디렉터리에 풀고, Parameter Store에서 .env 생성
tar -xzf /srv/plick/bundle/web.tar.gz -C /srv/plick/web tar -xzf /srv/plick/bundle/mobile.tar.gz -C /srv/plick/mobile aws ssm get-parameter --region ap-northeast-2 \ --name /plick/frontend/web/env --with-decryption \ --query Parameter.Value --output text > /srv/plick/web/.env # (mobile 동일) chmod 600 /srv/plick/web/.env /srv/plick/mobile/.env
-
application_start: 앱별로 .env를 소싱하고 pm2 기동. 소싱은
set -a; . .env; set +a순서가 중요합니다.set -a가 이후 변수를 전부 export로 만들어야 pm2가 띄우는 자식 프로세스(server.js)에 값이 넘어갑니다. 그리고 앱마다 서브셸( ... )로 감쌌습니다. web의 환경변수가 셸에 남은 채 mobile을 기동하면 값이 샐 수 있습니다 -
validate_service: 3000·3001 각각
/api/health가 200을 줄 때까지 최대 60초. 실패하면 exit 1 → CodeDeploy가 배포를 Failed 처리하고 트래픽은 Blue에 남습니다. v1처럼 스크립트가 직접 심볼릭 링크를 되돌리는 롤백 코드가 사라졌습니다. 실패의 뒷정리를 스크립트가 아니라 배포 시스템이 하는 구조가 된 것입니다
v1 release.sh에서 안 가져간 것들:
flock 직렬화(배포가 번들 1건이 됐고 에이전트는 인스턴스당 배포를 하나씩만 돌려 경합이 없습니다),
releases/ 심볼릭 링크와 릴리스 보관(롤백 단위가 릴리스 디렉터리에서 인스턴스로 바뀌었습니다),
S3 다운로드(에이전트 몫).
deploy.yml은 matrix 2잡에서 단일 잡이 됐습니다.
핵심 부분:
# 번들 조립 — appspec.yml이 zip "루트"에 있어야 한다 - name: Assemble bundle run: | mkdir -p bundle/scripts/deploy cp appspec.yml bundle/ cp scripts/deploy/application_stop.sh ... bundle/scripts/deploy/ cp web.tar.gz mobile.tar.gz bundle/ (cd bundle && zip -r ../bundle.zip .) - name: Create deployment run: | DEPLOYMENT_ID=$(aws deploy create-deployment \ --application-name plick-frontend \ --deployment-group-name plick-frontend-dg \ --s3-location bucket=plick-deploy,key=frontend/$SHA/bundle.zip,bundleType=zip \ --query deploymentId --output text) aws deploy wait deployment-successful --deployment-id "$DEPLOYMENT_ID"
zip을 (cd bundle && zip -r ../bundle.zip .)로 안에 들어가서 만듭니다.
바깥에서 디렉터리째 zip하면 bundle/appspec.yml로 한 겹 싸여 에이전트가 appspec을 못 찾습니다.
그리고 wait deployment-successful 한 줄이 v1의 send-command 폴링 루프 40줄을 대체합니다.
15초 간격 최대 30분 기다리다 실패로 끝나면 non-zero로 빠져 잡이 빨간불이 됩니다.
훅 스크립트의 stdout이 러너로 안 오는 건 v1 대비 후퇴라, 실패 시 get-deployment로 어느 훅에서 죽었는지 요약을 찍는 스텝을 뒤에 달았습니다.
4-7. 첫 배포 — 16분 57초
develop → main 병합으로 새 deploy.yml이 돌았고 한 번에 성공했습니다. 빌드부터 S3 업로드까지 5분쯤, 나머지가 Create deployment였습니다. 이 시간 안에서 Green 2대의 부팅, user data(Node·pm2·에이전트 설치), 훅 실행, 두 포트 헬스체크, 재라우팅이 전부 벌어집니다. v1이 1분대였던 것과 비교하면 길지만, 서버를 새로 만드는 비용입니다.
배포 중 EC2 목록에는 잠깐 4대(빈 Blue 2 + Green 2)가 공존했고, 재라우팅 후 유예가 지나자 빈 Blue가 자동 종료됐습니다. 첫 Blue/Green에서 Blue는 "앱 없는 빈 인스턴스"라 이들이 죽는 게 정상 동작입니다.


4-8. 컷오버 — 두 번째 사고 (청크 404)
배포 성공 후 대상 그룹에는 기존 EC2와 Green 2대가 같이 healthy로 떠 있었습니다.
그리고 브라우저에서 직접 확인해보는데 콘솔에 _next/static/chunks/....js 404가 났습니다.
메커니즘은 이렇습니다.
브라우저가 페이지를 열면 HTML 1개 + 청크 수십 개 + CSS + 이미지, 전부 별개의 HTTP 요청이고 ALB는 이들을 각각 독립적으로 분배합니다.
Next는 빌드마다 청크 파일명에 콘텐츠 해시를 붙이는데, 코드가 같아도 빌드가 다르면 해시가 다릅니다.
HTML을 신버전 인스턴스가 줬는데 그 HTML이 참조하는 청크 요청이 구버전 인스턴스에 떨어지면, 거기엔 그 이름의 파일이 없어서 404가 납니다.
같은 빌드를 풀어 놓은 Green 2대끼리는 이 문제가 없습니다.
기존 EC2를 두 대상 그룹에서 등록 취소(Deregister)하니 404가 사라졌습니다 일단은.
draining이 끝난 뒤 두 도메인 전 화면(홈·피드·릴스·상세·마이페이지), 서버 컴포넌트 데이터, 클라 fetch, 카카오·구글 OAuth까지 확인하고, 기존 EC2는 일단 중지해 안전핀으로 뒀습니다.

4-9. 두 번째 배포 — 무중단 실측
첫 배포는 "빈 Blue → Green"이라 반쪽 검증입니다.
문서 수정 PR을 main에 태워 진짜 Blue/Green(서비스 중인 Blue 2대를 교체)을 검증했습니다.
배포가 도는 내내 두 도메인의 /api/health를 1초 간격으로 찔렀습니다.
while true; do for d in plick.co.kr m.plick.co.kr; do code=$(curl -s -o /dev/null -m 5 -w '%{http_code}' "https://$d/api/health") [ "$code" != "200" ] && echo "$(date +%H:%M:%S) $d -> $code" done sleep 1 done
결과는 비정상 응답 0건.
서비스 중인 인스턴스 2대를 통째로 갈아치우면서 순단이 없었습니다.
v1의 pm2 restart 순단이 실제로 사라진 것을 숫자로 확인했습니다
이 배포 중에 청크 404가 잠깐 다시 보였습니다.
CodeDeploy의 "트래픽 재라우팅"은 원자적 스위치가 아니라
Green 등록 → ALB 헬스체크 통과 → Blue 제거(draining) 순서로 진행되고,
그 1~2분은 구·신 세대가 과녁에 같이 떠 있습니다.
그 창에서 같은 세대 불일치 404가 납니다.


4-10. 구 경로 정리
무중단 실측까지 끝나 v1 복귀 시나리오가 사실상 사라졌다고 판단하고 기존 ec2를 삭제합니다.
-
기존 EC2 종료 (중지 → 종료)
-
OIDC 롤 인라인 정책에서
ssm:SendCommand·ssm:GetCommandInvocation제거 -
GitHub 시크릿
EC2_INSTANCE_ID삭제 -
리포에서
release.sh삭제
5. 장단점
장점
-
무중단 배포. 실측 0건으로 확인했습니다. 검증 실패 시 트래픽이 Blue에 남아 "롤백"이라는 행위 자체가 필요 없습니다.
-
서버 셋업의 코드화. user data가 곧 서버 재현 절차라 문서 보고 손으로 세울 일이 없습니다(사실 이게 젤 큰 이득 같습니다)
-
2AZ 이중화 + 자가 복구. 인스턴스가 죽으면 ASG가 다시 띄웁니다
-
배포 대상의 논리화. 인스턴스 ID 시크릿이 사라졌습니다
-
워크플로 단순화. 폴링 루프 40줄 →
wait한 줄, flock·자체 롤백 코드 삭제
단점
-
인스턴스 상시 2대(v1은 1대), 배포 순간에는 최대 4대 - 단점이라기 보다는, 비용이 많이 나갈 수 있습니다
-
배포 시간 1분 → 17분. 매 배포가 서버 생성을 포함하므로
-
훅 로그가 러너에 안 옵니다. 상세 디버깅은 CodeDeploy 콘솔의 이벤트 로그로.
-
콘솔에 CodeDeploy가 만든
CodeDeploy_plick-frontend-dg_<배포ID>ASG가 세대마다 생겨 목록이 낯설어집니. 원본 ASG는 첫 배포 후 빈 껍데기로 남는데 지우면 안 됩니다
6. 지금 방식의 한계
-
1.재라우팅 겹침 구간의 청크 404. 무중단 배포는 정의상 두 버전이 동시에 트래픽을 받는 순간을 가지는데, 정적 자산이 인스턴스에 묶여 있는 한 그 순간의 세대 불일치 404는 원리적으로 못 없앱니다. 일단은 배포당 1~2분, Next가 청크 로드 실패 시 전체 리로드로 복구하므로 개선사항으로 남겨둡니다.
-
2.ASG 자가 복구의 반쪽짜리 구멍. 인스턴스가 죽어 ASG가 새로 띄우면 그 인스턴스는 user data만 돌고 앱이 없는 빈 상태로 unhealthy 대기입니다. 배포를 다시 돌려야 서비스에 낍니다.
-
3.pm2 로그가 인스턴스 로컬이라 교체되면 사라집니다
7. 개선 예정
-
1.정적 자산 S3 + CloudFront 분리(
assetPrefix). 모든 세대의 청크가 한 저장소에 쌓이므로 겹침 404와 배포 직후 캐시된 HTML 문제가 함께 사라집니다. 부수적으로 이미지 한 장까지 Node를 거치는 현재 구조의 CPU 낭비도 줄 수 있습니다 -
2.CloudWatch Logs 에이전트로 로그 수집
-
3.ASG 자가 복구 구멍의 근본 해결: AMI에 앱 굽기(golden AMI) 또는 EventBridge로 인스턴스 기동 이벤트에 자동 재배포 연결
8. 마치며
정적 자산 이슈와 ASG 자가 복구에 구멍이 아직 존재한다는게 가장 큰 현재의 이슈입니다. 이를 해결하기 위한 방안을 강구하고, 다시 발전시키고자 합니다.
끝이없는 인프라 구축의 길...
그래도 프론트면 인프라 몰라도 됨 이라는 말은 참 어리석은 말이라는 것을 하면서 깨닫습니다.