원활한 서비스 제공을 위해서 서버가 다운되는 상황을 최소화해야한다. 시스템 오류나 트래픽 과부하로 서버가 다운될 수 있지만 새로운 버전의 서버를 배포할 때도 서버가 잠시간 다운될 수 있다. 이런 경우 기존 서버를 내리고 새로운 서버가 재가동 될 때까지의 시간이 다운타임이 된다.
서비스를 운영중이라면 이러한 상황은 배포 때마다 서비스를 이용할 수 없기 때문에 치명적이다. 때문에 다운타임 없이 서버 배포가 가능한 무중단 배포 방식이 거의 필수적이라고 볼 수 있다.
이번 포스트에서는 필자가 '개발 한 스푼' 서비스에 무중단 배포를 적용한 방법을 공유한다.
무중단 배포 매커니즘
먼저, 무중단 배포가 어떻게 동작하는지 알아보자. 무중단 배포는 배포 도중에도 서비스 제공이 끊기지 않아야 한다. 새로운 서버가 배포되는데 어떻게 중단이 없을 수 있을까?
보통 클라이언트는 우리가 작성한 코드가 동작하는 웹 어플리케이션 서버(WAS)에 다이렉트로 접근하지 않는다. 그에 앞서 웹 서버나 프로 프록시 서버로 접근하여 이들이 뒷단의 WAS로 요청을 위임하는 방식으로 동작한다. 무중단 배포는 이렇게 배포되는 서버의 앞단에 위치하는 노드에서 라우팅을 컨트롤하는 방식으로 이루어진다.
새로운 버전의 WAS를 실행시키고 실행이 완료되면 웹 서버로 하여금 라우팅을 새로운 버전의 서버로 흐르도록 설정한다. 때문에 기존 서버와 새로운 서버가 함께 운용되는 때가 반드시 존재한다. 새로운 버전의 서버가 정상적으로 작동한다고 판단되면 기존 서버로의 라우팅을 끊고 종료시킴으로써 배포가 마무리된다.
하지만 사용자가 많은 서비스에서 1대의 서버만 동작하지는 않을 것이다. 같은 코드 베이스의 N개의 서버가 동작할 것이다. 때문에 N개의 서버를 대체시키는 방식에 따라 배포 방식이 나뉘어진다.
롤링 업데이트 배포 방식
롤링 업데이트는 N개의 서버를 1개씩 배포하는 방식이다. 새버전의 서버를 1개 띄우고 구버전의 서버를 1개 내린다. 이를 반복하여 N개의 서버를 대체하는 방식이다. 하나씩 교체되기 때문에 배포되는 시간이 오래걸리며 만에하나 중간에 문제 발생시 롤백이 어려울 수 있다는 단점이 있다.
블루/그린 배포 방식
블루/그린은 N개의 서버를 모두 띄우고 한 번에 트래픽을 전환하는 방식이다. 기존 서버들(블루 그룹) 과 같은 수의 서버들(그린 그룹)을 모두 띄운 후 테스트를 진행한다. 테스트가 통과되면 모든 트래픽을 그린그룹을 전환한다. 마무리되면 블루 그룹을 종료시킨다. 배포가 간단하는 장점이 있지만 배포 시에 2 x N 개의 서버가 운용되므로 비용이 크다는 단점이 있다.
까나리 배포 방식
까나리는 트래픽을 점진적으로 새로운 서버그룹으로 전환하는 방식이다. 한 번에 N개의 서버를 띄울수도 전환되는 트래픽 비율에 따라 천천히 띄울 수도 있다. 트래픽을 새로운 서버그룹으로 조금씩 전환하면서 안정성을 확인하는 배포 방식으로 문제 발생시 빠른 롤백이 가능하다는 장점이 있다. 하지만 트래픽 비율을 컨트롤해야하기에 배포 과정이 복잡하고 시간이 오래걸릴 수 있다.
AWS로 블루/그린 배포
'라우팅을 컨트롤한다' 가 말은 쉽지만 직접 구현하기에는 상당히 어려운 방식이라고 한다. 하지만 요즘은 Nginx나 클라우드에서 지원해주기 때문에 구현하기 편리하다.
필자는 AWS를 통해 서버를 운용하고 있었기 때문에 AWS 서비스들을 이용해 무중단 배포를 구현했다. 필자의 서비스는 클라이언트의 요청을 로드밸런서(LB)가 받아 뒷단의 EC2 그룹(AutoScale Group) 내 노드들에게 요청을 분산한다.
AWS의 배포 서비스인 Code Deploy에서는 LB, AutoScale Group과 통합하여 블루/그린 배포 방식을 지원한다. 이들을 이용하여 다음과 같은 모습으로 무중단 배포를 구현하였다.
필자는 기존 인프라를 Terraform으로 관리하고 있어 무중단 배포를 위한 것들도 Terraform을 이용해 구현했다. 더불어 Gitbub Actions를 이용해 특정 github event(PR merge 등) 발생시 자동 배포될 수 있도록 파이프라인을 구성했다.
기존 LB와 AutoScaling Group
유저가 요청을 보내면 `어플리케이션 로드밸런서(ALB)`를 통해 설정된 Target Group 내 EC2 노드에 요청을 분산한다. 때문에 ALB가 요청을 받아들일 통로(listener)와 받은 요청을 분산할 Target Group 설정이 필요하다.
- ALB 생성 (subnet, security group 설정 포함)
- ALB에 연결될 target group 설정 (이후 AutoSacaling Group과 연결할 예정)
- ALB listener 설정 (http 80, https 443 설정 - http 요청시 https로 redirect)
# 1. ALB 생성
resource "aws_lb" "applicaiton" {
name = "${var.name}-alb"
subnets = var.public_subnets
internal = false
security_groups = [ module.alb_sg.id ]
load_balancer_type = "application"
}
# 2. Traget Group 연결 설정
resource "aws_lb_target_group" "application" {
name = "${var.name}-alb-target-group"
port = var.service_port
protocol = "HTTP"
vpc_id = var.vpc_id
slow_start = var.lb_variables.target_group_slow_start
deregistration_delay = var.lb_variables.target_group_deregistration_delay
health_check {
port = var.healthcheck_port
path = var.healthcheck_path
interval = 30
timeout = 5
healthy_threshold = 3
unhealthy_threshold = 3
}
}
# 3. Listener 설정
resource "aws_lb_listener" "listen_80" {
load_balancer_arn = aws_lb.applicaiton.arn
port = 80
protocol = "HTTP"
default_action {
type = "redirect"
redirect {
port = "443"
protocol = "HTTPS"
status_code = "HTTP_301"
}
}
}
resource "aws_lb_listener" "listen_443" {
load_balancer_arn = aws_lb.applicaiton.arn
port = 443
protocol = "HTTPS"
certificate_arn = var.certificate_arn
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.application.arn
}
}
다음은 ALB의 Target Group으로 설정될 `AutoScaling Group`을 만들고 연결한다. AutoScaling Group은 포함된 노드의 CPU/메모리 자원 사용량에 따라 설정된 노드 수 만큼 자동으로 스케일 아웃/인을 처리해 준다.
- 스케일 아웃 시 생성할 EC2의 Launch Template을 설정(AMI, instance profile 등 설정)
- AutoScaling Group 생성(LB의 Traget Group에 연결)
여기서 중요한 점은 AutoScaling Group 내에 생성될 EC2에 CodeDeploy Agent가 설치되어 있어야한다. 필자는 미리 Agent가 설치된 EC2를 AMI로 만들어 두었다. 또한 운영중 EC2에게 필요한 권한들도 Role을 만들어 설정해주자.(S3, SSM 등의 권한을 주었다. 아래 코드에는 생략되어 있다.)
# 1. Launch Template 설정
resource "aws_launch_template" "ec2_template" {
name_prefix = "${var.name}-launch-template"
# AMI 설정
image_id = var.ami_id
instance_type = var.instance_type
key_name = var.key_name
# instance profile 설정
iam_instance_profile {
arn = aws_iam_instance_profile.ec2_profile.arn
}
vpc_security_group_ids = [ aws_security_group.private_ec2_sg.id ]
tag_specifications {
resource_type = "instance"
tags = {
Name = "${var.name}-LaunchTemplate"
}
}
}
# 2. AutoScaling Group 생성
resource "aws_autoscaling_group" "asg" {
launch_template {
id = aws_launch_template.ec2_template.id
version = "$Latest"
}
min_size = var.min_size
max_size = var.max_size
desired_capacity = var.desired_capacity
# Group 내 생성될 EC2 들이 존재할 subnet의 id들
vpc_zone_identifier = var.private_subnets
# LB에서 생성한 Traget Group에 연결
target_group_arns = [ aws_lb_target_group.application.arn ]
}
여기까지가 현재 서비스가 요청을 처리하는 구조이다. 이제 CodeDeploy와 위에서 만든 LB, Target Group(AutoScaling Group)을 이용해서 블루/그린 배포 설정을 진행해보자.
CodeDeploy 설정
CodeDeploy는 AWS 서비스들의 배포 파이프라인을 만드는 것을 지원한다. Commit이나 EventBridge 등 몇몇 Event로 배포 파이프라인이 트리거되도록 설정할 수도 있고 수동으로 실행시킬 수도 있다. 필자는 후에 Github Actions 내 워크플로우에서 cli로 실행시켰다.
CodeDeploy는 ALB와 연결된 TargetGroup을 통해 블루/그린 배포를 실행할 수 있도록 지원한다. 이에 대한 설정만 추가해주면 된다.
resource "aws_codedeploy_app" "app" {
name = var.deployment_app_name
compute_platform = "Server"
}
# 배포 설정
resource "aws_codedeploy_deployment_group" "deploy_group" {
app_name = aws_codedeploy_app.app.name
deployment_group_name = var.deployment_group_name
deployment_config_name = "CodeDeployDefault.OneAtATime"
# CodeDeploy Role 추가
service_role_arn = aws_iam_role.codedeploy.arn
# CodeDeploy가 적용될 LoadBalancer 설정
load_balancer_info {
target_group_info {
name = aws_lb_target_group.application.name
}
}
# 위에서 생성한 AutoScaling Group 연결
autoscaling_groups = [ aws_autoscaling_group.asg.name ]
# 블루/그린 배포 설정
deployment_style {
deployment_type = "BLUE_GREEN"
deployment_option = "WITH_TRAFFIC_CONTROL"
}
blue_green_deployment_config {
deployment_ready_option {
action_on_timeout = "CONTINUE_DEPLOYMENT" # deploy 즉시 라우팅
}
# 배호 성공시 10분 뒤 기존 AutoScalingGroup의 인스턴스 제거
terminate_blue_instances_on_deployment_success {
action = "TERMINATE"
termination_wait_time_in_minutes = 10
}
# 기존 AutoSaclingGroup을 복사해서 새로운 AutoScalingGroup을 생성
green_fleet_provisioning_option {
action = "COPY_AUTO_SCALING_GROUP"
}
}
# 배포 실패시 롤백
auto_rollback_configuration {
enabled = true
events = ["DEPLOYMENT_FAILURE"]
}
}
Code Deploy Role에 필수적으로 추가해야하는 정책들이 있다. Code Deploy가 LB, AutoScaling Group, EC2를 제어하다보니 이와 관련된 정책들을 추가해야 한다. 아래처럼 AWS가 만들어둔 CodeDeploy 정채과 함께 몇가지 추가적인 정책을 Role에 추가했다.
resource "aws_iam_role" "codedeploy" {
name = "${var.name}-codedeploy-role"
assume_role_policy = data.aws_iam_policy_document.codedeploy_assume_role.json
}
# AWS가 만들어 놓은 CodeDeploy 정책 추가
resource "aws_iam_role_policy_attachment" "AWSCodeDeployRole" {
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSCodeDeployRole"
role = aws_iam_role.codedeploy.name
}
# Code Deploy - Launch Template으로 Auto Scaling 그룹 생성시 아래 권한 추가 필요
# 참고 : https://docs.aws.amazon.com/ko_kr/codedeploy/latest/userguide/getting-started-create-service-role.html
resource "aws_iam_policy" "autoscale_policy" {
name = "${var.name}-codedeploy-autoscaling-policy"
path = "/"
policy = data.aws_iam_policy_document.autoscale_policy.json
}
data "aws_iam_policy_document" "autoscale_policy" {
statement {
sid = "CodeDeployTargetAutoScalingGroupPolicy"
actions = [
"iam:PassRole",
"ec2:CreateTags",
"ec2:RunInstances",
]
effect = "Allow"
resources = ["*"]
}
}
resource "aws_iam_role_policy_attachment" "autoscale_policy" {
policy_arn = aws_iam_policy.autoscale_policy.arn
role = aws_iam_role.codedeploy.name
}
이렇게 설정하면 CodeDeploy를 통한 블루/그린 배포 준비가 끝난다.
Github Actions를 통한 배포 자동화
CodeDeploy는 실행시 S3에 존재하는 빌드 결과물(아티팩트)를 활용한다. 따라서, 우리가 작성한 코드를 빌드 후 S3에 업로드해야한다.
또한, CodeDeploy는 실행기일 뿐이므로 아티팩트를 이용해서 서버를 실행시키는 로직을 작성하는 것은 개발자의 몫이다. `appspec.yml` 이라는 파일에 적절한 시기에 서버를 실행시키는 코드를 작성하면서 CodeDeploy가 appspec.yml을 읽고 코드를 실행시켜준다. 정리하자면,
- appsepc.yml에 서버를 실행시키는 스크립트 작성
- (Github Actions) 현재 코드 빌드 후 결과물 S3에 업로드
- (Github Actions) CodeDeploy 실행(S3에서 결과물을 활용, appspec.yml에 명시된대로 실행)
우선 appspec.yml 을 작성한다. 해당 파일은 프로젝트의 최상위에 둔다. 내부에서 script 파일을 사용하는데 마찬가지로 최상위 레이어에 둔다. deploy.sh 실행 스크립트는 필자가 작성한 Spring의 jar 파일을 실행하는 로직이다.
# appspec.yml
version: 0.0
os: linux
# desitnation(아티팩트가 존재) 아래 파일 모두 배포
files:
- source: /
destination: /home/ec2-user/adevspoon
overwrite: yes
# 권한 부여
permissions:
- object: /
owner: ec2-user
group: ec2-user
hooks:
# ApplicationStart Event 시 실행
ApplicationStart:
- location: scripts/deploy.sh
timeout: 180
# deploy.sh
#!/bin/bash
ROOT_PATH="/home/ec2-user/adevspoon"
JAR="$ROOT_PATH/app.jar"
NOW=$(date +%c)
echo "[$NOW] $JAR 복사"
cp $ROOT_PATH/build/libs/adevspoon-api-0.0.1-SNAPSHOT.jar $JAR
echo "[$NOW] > $JAR 실행"
nohup java -Duser.timezone=Asia/Seoul -Dspring.profiles.active=prod -jar $JAR > /dev/null 2> /dev/null < /dev/null &
SERVICE_PID=$(pgrep -f $JAR)
echo "[$NOW] > 서비스 PID: $SERVICE_PID"
이제 작성한 코드를 빌드, S3에 업로드, CodeDeploy 트리거 순으로 실행시키는 워크플로우를 만들어보자. 필자는 tag가 push될때 Github Actions 워크플로우가 실행되도록 하였다.
name: API - Deploy Production Environment
on:
push:
tags:
- Api-v*
workflow_dispatch:
# AWS 정보
env:
AWS_REGION: ---
S3_BUCKET_NAME: ---
CODE_DEPLOY_APPLICATION_NAME: ---
CODE_DEPLOY_DEPLOYMENT_GROUP_NAME: ---
jobs:
api-server-deploy:
runs-on: ubuntu-latest
steps:
# Repo checkout
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.MY_TOKEN }}
submodules: true
# JDK 환경 셋팅
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: '17'
cache: gradle
# Gradle Permission
- name: Grant execute permission for gradlew
run: chmod +x gradlew
# Build App
- name: Gradle build
run: |
./gradlew globalCopyConfig
./gradlew adevspoon-api:build -x test
# AWS Config
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
# S3 Upload
- name: S3 upload
run: |
aws deploy push \
--application-name ${{ env.CODE_DEPLOY_APPLICATION_NAME }} \
--ignore-hidden-files \
--s3-location s3://$S3_BUCKET_NAME/$GITHUB_SHA.zip \
--source ./adevspoon-api
# Code Deploy 실행 (블루/그린 무중단 배포)
- name: CodeDeploy - Blue-Green Deployment
run: |
aws deploy create-deployment \
--application-name ${{ env.CODE_DEPLOY_APPLICATION_NAME }} \
--deployment-config-name CodeDeployDefault.OneAtATime \
--deployment-group-name ${{ env.CODE_DEPLOY_DEPLOYMENT_GROUP_NAME }} \
--s3-location bucket=$S3_BUCKET_NAME,bundleType=zip,key=$GITHUB_SHA.zip
CodeDeploy가 문제없이 실행되면 AWS 콘솔에서 아래 사진과 같은 모습을 볼 수 있다.
마치며
CodeDeploy를 Terraform으로 구현한 코드를 찾아볼 수 없어서 공식문서를 정독하면서 만들어봤다. 콘솔로 만들면 훨씬 편하다. Terraform 코드는 하나씩 다 적어줘야하는 반면 콘솔은 많은 경우 자동으로 기본값들이 설정되는 경우가 많기 때문이다. 그래도 Terraform을 직접 작성하면서 배우는 것들이 많은 것 같다.
마무리로 서비스 링크 안 올리면 섭하지~
GitHub - kids-ground/adevspoon-backend: CS 질문 - adevspoon-backend
CS 질문 - adevspoon-backend. Contribute to kids-ground/adevspoon-backend development by creating an account on GitHub.
github.com
'Server & DevOps' 카테고리의 다른 글
OS, 어플리케이션에서의 데드락 (2) | 2024.07.12 |
---|---|
경쟁상태와 동기화 매커니즘의 활용 (0) | 2024.07.04 |
Covering Index로 쿼리 성능 개선하기 (0) | 2024.06.14 |
Github Actions를 활용한 브랜치 전략 맞춤 자동화 (1) | 2024.06.08 |