목표
Cloud의 편리함은 최대한 누리면서
AWS에는 종속되지 않게
무중단 배포를 구현한다
- 사실 무중단 배포를 구현하는 거 자체는 간단합니다. 새로운 버전의 서버를 미리 띄워 두고, Health Check를 해서 괜찮으면 트래픽을 전환한다... 하지만 이걸 구현하기 위해 내려야 할 여러 기술적인 결정은 단순하지 않습니다.
- 오늘은 진작에 접었어야 했지만 접지 못 해 계속 하고 있는 제 포트폴리오 사이트 책잇아웃의 무중단 배포를 AWS와 Github Actions로 구현하는 과정에서 했던 여러 기술적인 결정들과 배운 것들을 기록해 보겠습니다.
현재상황 (시작점)
🐜 AWS 구조
- 현재 상황은 ELB (Elastic Load Balancing)가 가장 앞단에 있어 트래픽을 받는 구조입니다. ELB에는 Public IP Address가 부여 돼 있고 이 IP 주소에 DNS가 향하고 있습니다.
- 웹 애플리케이션을 운영하는데 필요한 기본적인 리소스들 (EC2, RDS, S3) 등은 모두 VPC 안에 있고 외부 API 호출을 해야 하는 EC2를 제외하고는 NAT Gateway 조차 달려 있지 않아 인터넷에서의 접근은 ELB를 통하지 않고는 못 하게 막아 뒀습니다.
🐜 CI/CD 상황
- 배포를 하게 된다면 Docker를 사용하기 때문에 build나 test를 다 돌리고 Docker Image를 최신으로 갱신해 둡니다. Docker Image는 Private으로 해 뒀기 때문에 secret 등도 다 포함된 이미지입니다.
- Docker Image 갱신이 끝나면 개별 EC2에 접속해...
- 최신 Docker Image를 pull하고
- 현재 실행 중인 Docker Container를 중지시키고
- 새로 pull 한 Docker Image를 기반으로 새로운 Container를 실행시킵니다.
docker pull example_dockerhub_username/example_image_name:latest
docker stop example_container_name
docker run --name example_container_name -it -d -p 80:80 example_dockerhub_username/example_image_name
❌ 후보 1 - [AWS 종속 거의 없게] ELB 없이 EC2 내부에 Nginx 넣고 다른 Port에 서버 띄워서 Nginx로 전환하기
🐜 어떻게 한다고?
현재의 대략적인 구조를 유저하고 비용 절약을 위해 ELB까지 걷어내는 시나리오입니다.
결국 핵심은 개별 EC2 인스턴스는 그대로 유지하고 여기서 Command를 날릴 때 무중단으로 하면 된다는 겁니다. 이를 위해 배포 시에 Docker Container 2개를 띄우고 Niginx를 통해 트래픽을 한 번에 전환하는 겁니다.
여기서 ELB를 없앨 경우 HTTPS를 위한 인증서도 직접 설치하고 관리해야 하는 단점이 있어 ELB를 없애지 않고 EC2 내에서 Nginx로 트래픽 전환만 처리할 수도 있습니다.
🐜 왜 안 하는가?
ELB (Elastic Load Balancing)을 안 쓰는 건 목표에 있는 Cloud의 편리함을 너무 크게 포기하는 느낌이었습니다. 물론 직접 다 할 수 있지만, 직접 다 하기 싫어서 Cloud를 쓰는 건데... 그래서 ELB는 유지한 상태로 어떤 선택지가 있을지 조사하고 고민했습니다.
❌ 후보 2 - [AWS 기능 최대 활용] CodeDeploy를 사용해서
🐜 어떻게 한다고?
CodeDeploy는 AWS에서 제공하는 배포 도구입니다. EC2, Lambda 등을 설정 파일로 배포 자동화를 할 수 있습니다.
CodeDeploy를 여러 배포 전략을 잘 구현할 수 있다고 합니다.
🐜 왜 안 하는가?
CodeDeploy를 쓰기 전에 관련 내용을 배워야 할 거 같아 공식 문서를 찾아봤습니다.
version: 0.0
os: linux
files:
- source: /
destination: /var/www/html
hooks:
BeforeInstall:
- location: scripts/install_dependencies.sh
timeout: 300
runas: root
AfterInstall:
- location: scripts/restart_server.sh
timeout: 300
runas: root
이건 정말 AWS만을 위한 문법이고, 배포 절차 자체가 너무 AWS에 강결합 돼 있는 느낌이었습니다. 처음 정한 목표가 AWS에 결합되는 건 최대한 줄이면서 클라우드의 간편함은 누리자였는데 이건 간편하지도 않고 AWS에 결합도 너무 과하게 되는 느낌이라 포기했습니다.
✅ 후보 3 - [타협형] EC2가 항상 최신 버전으로 재기동 되게 해 놓고 Auto Scale Group을 Instance Refresh 하기
🐜 어떻게 한다고?
위의 방법은 모두 배포 전략을 직접적으로 구성했습니다. (스크립트를 직접 작성하든, 설정 파일을 직접 작성하든) 하지만 제 기존 배포 전략에서 좋았던 점은 최신 Docker Image만 받아오면 그게 바로 최신 버전이 된다는 것이었습니다. 그래서 떠올린 게... EC2의 Auto Scale Group을 설정하고 배포할 때 일시적으로 Desired Capacity를 늘린 뒤 Instance Refresh를 한 뒤, 다시 Desired Capacity를 줄이면 무중단으로 배포를 할 수 있지 않을까 하는 아이디어를 떠올렸습니다.
더 자세히 설명하자면...
- EC2의 Auto Scale Group은 특정 조건에 따라 EC2를 자동으로 늘리고 줄이는 기능입니다. 예를 들어, 갑자기 CPU 사용량이 60%를 넘으면 EC2를 늘린다 식으로 설정할 수 있습니다.
- Auto Scale Group이 새로운 EC2 Instance를 띄우기 위해서 어떻게 해야 하는지를 Launch Template에 명시해 줄 수 있습니다. Launch Template에는 어떤 AMI를 쓰는지, VPC와 Subnet은 어떤 걸 쓰면 되는지, SG는 어떤 걸 하면 되는지 등을 다 명시해 줄 수 있습니다.
- 추가로 Launch Template에는 User Data라는 EC2를 띄울 때 실행할 스크립트를 명시해 줄 수 있습니다.
- 여기서 User Data에 최신 Docker Image로 EC2를 띄운 뒤 서버를 실행하게 하면 (아니면 Github의 파일 등에 Docker 버전을 명시해서 어떤 버전을 띄우기를 원하는지 조정할 수도 있겠쬬?) 최신 버전으로 배포가 완료됩니다.
- 하지만 이렇게 하면 서버가 새로 떠야지만 최신 버전으로 배포가 완료되기 때문에 Instance Refresh라는 기능을 사용할 예정입니다. 이 기능을 사용하면 현재 떠 있는 모든 EC2 Instance를 새롭게 띄우고 다 갈아 끼울 수 있습니다.
- 당연히 AWS의 GUI에서 이걸 수동으로 실행해 무중단으로 배포를 할 수도 있지만... 여기서는 Github Actions를 사용해 CI/CD 파이프라인을 만들어야 하기 때문에 AWS CLI를 사용하겠습니다. AWS CLI를 쓰면 GUI에서 가능한 모든 행동 +α를 CLI로 할 수 있습니다.
최종적인 Github Actions의 스크립트는 아래와 같습니다! (일부 생략 밑 각색)
- name: [AWS] Login in with Credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ env.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ env.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: [AWS] Start EC2 Instance Refresh
run: |
set -euo pipefail
REFRESH_ID=$(aws autoscaling start-instance-refresh \
--auto-scaling-group-name "$ASG_NAME" \
--preferences '{"MinHealthyPercentage":40}' \
--strategy Rolling \
--query 'InstanceRefreshId' \
--output text)
echo "Started Instance Refresh with ID: $REFRESH_ID"
echo "REFRESH_ID=$REFRESH_ID" >> $GITHUB_ENV
- name: [AWS] Wait for Instance Refresh to Complete (with Timeout)
run: |
set -euo pipefail
SLEEP_INTERVAL=30
MAX_WAIT_TIME=$((20 * 60))
TIME_WAITED=0
while :; do
STATUS=$(aws autoscaling describe-instance-refreshes \
--auto-scaling-group-name "$ASG_NAME" \
--instance-refresh-ids "$REFRESH_ID" \
--query 'InstanceRefreshes[0].Status' \
--output text)
echo "Current Instance Refresh Status: $STATUS"
if [[ "$STATUS" == "Pending" || "$STATUS" == "InProgress" ]]; then
if [ "$TIME_WAITED" -ge "$MAX_WAIT_TIME" ]; then
echo "Instance refresh did not complete within 20 minutes."
exit 1
fi
echo "Instance refresh is still in progress. Waiting for $SLEEP_INTERVAL seconds..."
sleep $SLEEP_INTERVAL
TIME_WAITED=$((TIME_WAITED + SLEEP_INTERVAL))
elif [[ "$STATUS" == "Successful" ]]; then
echo "Instance refresh completed successfully."
break
else
echo "Instance refresh failed or was cancelled with status: $STATUS"
exit 1
fi
done
- name: [Slack] Deloy to AWS Done
uses: slackapi/slack-github-action@v1.24.0
if: success()
with:
payload: { "text": ":white_check_mark: Deploy to AWS Done" }
- name: [Slack] Deloy to AWS Failed
uses: slackapi/slack-github-action@v1.24.0
if: failure()
with:
payload: { "text": ":x: Deploy to AWS Failed" }
- aws autoscaling start-instance-refresh 요게 instance refresh를 하는 코드입니다. 결과로 Instance Refresh의 ID를 반환하는데 이걸 환경 변수로 설정해 Instance Refresh의 status를 받아서 Health Check를 해 줄 예정입니다.
- Slack 관련은 배포 결과를 받아보기 위해 추가했습니다.
- echo 하는 부분은 Github Actions의 Console 창에 메시지를 출력하는 부분입니다. 디버깅을 쉽게 하기 위해 추가했습니다.
후기
- 예상대로 프로세스 자체는 굉장히 단순했지만... 여러 옵션들 중에 어떤 게 가장 좋은지 고민하고 조사하는데 대부분의 시간을 썼습니다.
- 현재 상황은 당장 Cloud Provider를 AWS에서 바꾸게 된다면 꽤 큰 작업을 해야 하지만 그래도 Build Script를 처음부터 다 짜야하는 정도는 아니라 본래 목표인 Cloud의 편리함을 누리지만 종속성이 너무 크지는 않은 상황에 도달했습니다. (LB 설정하고, VPC 설정하고, Startup Script 설정하고 등은 Cloud Provider 마다 UI 위치나 부르는 이름만 다르지 모두 공통적으로 제공하는 기능입니다.)
- 그리고 의외로 막혔던 부분은... AWS 자체에 대한 숙련도가 낮아서 그런 게 많았습니다.
- VPC 설정하기
- 각 기능들이 UI의 어디에 위치해 있는지 찾기
- 디버깅하기 (Log가 어디에 나오는지, Public IP Address를 부여 안 하면 개별 Instance에 접속이 불가능한데 이걸 몰라서 헤맸던 거 등)
- 아무쪼록 오랜 숙원이었던 무중단 배포를 구현해 봤습니다! MAU는 2지만 이제 무중단으로 쓸 수 있습니다. 하하 😆
'👨💻 프로그래밍 > Architecture' 카테고리의 다른 글
도메인 모델 풍부하게 만들기 (1) | 2024.05.04 |
---|---|
핵사고날? 클린 아키텍처? DDD? (0) | 2024.01.28 |
CQRS (Command Query Responsibility Segregation) 알아보기 (1) | 2023.12.08 |
Redis, RabbitMQ, Kafka를 각각 Message Queue로 사용할 때의 장단점 (1) | 2023.09.30 |
🔒 분산 Architecture에서 Redlock으로 Lock 걸기 (0) | 2023.09.28 |