Elastic Beanstalk와 Github Actions를 활용해서 CICD 무중단 배포 구현하기

Elastic Beanstalk란

AWS에서는 인프라에 대한 지식이 없어도 배포 환경을 구축할 수 있는 Elastic Beanstalk(EB)를 제공한다. EB에 어플리케이션을 전달하면 EB에서 용량 프로비저닝, 로드 밸런싱, 조정, 어플리케이션 상태 모니터링을 자동으로 처리한다.

다음은 EB가 어떻게 어플리케이션을 관리하는 지 보여주는 워크플로우다.
EB workflow

Elastic Beanstalk 구조

EB는 여러 종류의 환경을 제공할 수 있다. 웹 서버 환경을 중점적으로 살펴보자.
EB web server Env
Auto Scaling Group에 여러 EC2 인스턴스들이 존재한다. 이 인스턴스에서 어플리케이션이 실행되는데 여기서 HM(호스트 매니저) 이라는 개념이 있다. HM는 어플리케이션의 로그를 S3에 개시하거나 서버 인스턴스의 상태를 보고하는 역할을 한다.

Auto Scaling Group의 EC2들 앞에는 Elastic Load Balancer가 로드 밸런싱을 한다. EB의 ELB는 CNAME(URL)을 가진다. 그리고 EB 환경도 CNAME을 가진다. 이때 환경의 CNAME은 ELB의 CNAME의 별칭으로 지정된다.

Elastic Beanstalk 직접 구성해보기

EB를 생성할 때 중요한 몇가지 위주로 설명하겠다.

플랫폼 설정하기 및 추가 옵션 구성

AWS EB 콘솔에서 새 어플리케이션 만들기를 선택하면 어플리케이션이 어떤 플랫폼인지 그리고 그 플랫폼을 어떤 운영체제에서 실행할지를 정해야 한다.
필자의 경우는 Spring Boot 어플리케이션이므로 Java 플랫폼에 Linux 2를 설정했다. 즉 EB 환경의 EC2가 Linux 2 기반으로 생성된다는 의미이다.

그리고 이때 중요한 건 바로 애플리케이션 생성을 누르지 말고 무중단 배포를 위해 추가 옵션 구성을 선택한다.
플랫폼 설정화면

추가 옵션 구성을 클릭하면 여러가지 설정 가능한 옵션이 나온다. 이때 사전 설정에서 사용자 지정 구성으로 우리 상황에 맞도록 구성할 수 있도록 해보자.
추가 옵션 구성 - 사전 설정

인스턴스 보안그룹 설정하기

Auto Scaling Group의 인스턴스들이 적용되는 보안그룹을 반드시 설정해줘야 한다. 그렇지 않으면 외부에서 인스턴스에 SSH로 접근해서 의도하지 않은 작업을 진행할 수 있다.
인스턴스 - EC2 인스턴스 보안 그룹
F12-PROD 보안그룹은 80 포트와 8080 포트를 모든 IP가 접근할 수 있도록 했고, SSH는 개발자 IP만 접근 가능하도록 인바운드 규칙을 적용했다.
ec2-rds-1 보안그룹은 mysql 포트인 3306 포트를 아웃바운드 규칙으로 적용되어 있다.

Auto Scaling Group 및 로드 밸런싱 설정하기

먼저 Auto Scaling Group에 어떤 인스턴스를 몇 개나 수용할 지를 정해야 한다. 우리는 프리티어 한도 내에서 인프라를 구축해야 하기 때문에 인스턴스를 한 개만 사용하도록 했다.
용량 - Auto Scaling 그룹

다음으로 로드 밸런서를 설정해야 한다. 로드 밸런서는 크게 Classic Load Balancer(ELB)와 Application Load Balancer(ALB)를 선택할 수 있다. ELB가 먼저 출시되어 많은 참고 자료가 존재하지만 갑작스러운 트래픽이 발생할 경우에 대응이 좋지 않다고 한다. 그래서 우리는 ALB를 선택했다.

그리고 추가로 프론트엔드에서 8080포트로 요청하는 상황이라 로드 밸런서의 리스너를 8080포트도 열어줬다.
로드 밸런서

무중단 배포 설정

이제 EB에서 새 버전을 배포하려고 할 때 무중단 배포가 되도록 설정해야 한다. 롤링 업데이트와 배포 수정에 가서 애플리케이션 배포 방식을 정할 수 있다. 우리는 추가 배치를 사용한 롤링 방식을 선택했다. 만약 새 버전이 배포되면 새 EC2 하나를 만들어서 새 버전을 배포하고 배포가 완료되면 로드 밸런서 설정을 바꾸고 기존의 Auto Scaling은 폐기하는 방식으로 운영된다. 다만 이 방식은 배포가 되고나면 롤백하지 못한다.
롤링 업데이트와 배포 수정

EC2 키 페어 설정

EB에서 생성된 EC2 인스턴스에 접근할 수 있는 PEM 키를 설정해 줄 수 있다. 우리는 기존에 만들어놨던 PEM 키를 활용했다.
보안 수정

이제 EB 설정은 어느정도 끝났으니 EB를 생성하면 된다.

EB 어플리케이션 구성

EB 어플리케이션을 실행할 때 인스턴스에서 설정해줘야 하는 것들이 있을 수 있다. 예를 들어 EC2의 타임존 설정이나 리버스 프록시 설정이 필요할 수 있다. 그 설정을 EB에서는 .ebextensions 디렉토리, .platform 디렉토리로 설정해줄 수 있다.

EC2 리버스 프록시 설정하기

로드 밸런서가 보내는 트래픽을 EC2에서 애플리케이션으로 전달 할 때 리버시 프록시를 설정해줄 수 있다. 우리가 사용하는 Linux 2 플랫폼은 Nginx로 리버스 프록시를 구현할 수 있다. 이때 .platform/nginx/nginx.conf 파일을 통해서 EC2의 Nginx의 설정을 오버라이딩 할 수 있다. .platform/nginx/conf.d/{설정파일이름}.conf에 파일을 만들면 기존의 Nginx 설정 파일에 설정을 추가하는 방식으로 구현할 수 있다.

우리는 .platform/nginx/nginx.conf 파일을 프로젝트에 추가해서 Nginx 설정을 오버라이딩 하는 방식으로 구현했다. /로 오는 모든 요청은 로컬의 8080포트로 보내도록 구현했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
user                    nginx;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
worker_processes auto;
worker_rlimit_nofile 33282;

events {
use epoll;
worker_connections 1024;
}

http {
include /etc/nginx/mime.types;
default_type application/octet-stream;

log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';

include conf.d/*.conf;

map $http_upgrade $connection_upgrade {
default "upgrade";
}

upstream springboot {
server 127.0.0.1:8080;
keepalive 1024;
}

server {
listen 80 default_server;

location / {
proxy_pass http://springboot;
proxy_http_version 1.1;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Upgrade $http_upgrade;

proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

access_log /var/log/nginx/access.log main;

client_header_timeout 60;
client_body_timeout 60;
keepalive_timeout 60;
gzip off;
gzip_comp_level 4;

# Include the Elastic Beanstalk generated locations
include conf.d/elasticbeanstalk/healthd.conf;
}
}

EB 환경 커스텀하기

EB에서 가동 중인 EC2에 파일을 추가한다던지 커맨드를 실행하고 싶은 경우 .ebextensions 디렉토리에 config 확장자로 파일을 추가하면 된다.

우리는 JAR 배포 스크립트와 로깅 설정파일을 생성하고 EC2 타임존 설정 커맨드를 실행하도록 구현해보자
.ebextension/00-makeFiles.config

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
files:
"/sbin/appstart" :
mode: "000755"
owner: webapp
group: webapp
content: |
#!/usr/bin/env bash
JAR_PATH=/var/app/current/application.jar

# run app
killall java
java -Dfile.encoding=UTF-8 -Dspring.profiles.active=main -jar $JAR_PATH

"/opt/elasticbeanstalk/tasks/taillogs.d/applogs.conf":
mode: "000755"
owner: webapp
group: webapp
content: |
/var/app/current/logs/log/*.log

"/opt/elasticbeanstalk/tasks/bundlelogs.d/applogs.conf":
mode: "000755"
owner: webapp
group: webapp
content: |
/var/app/current/logs/log/*.log

"/opt/elasticbeanstalk/tasks/taillogs.d/dblogs.conf":
mode: "000755"
owner: webapp
group: webapp
content: |
/var/app/current/logs/db/*.log

"/opt/elasticbeanstalk/tasks/bundlelogs.d/dblogs.conf":
mode: "000755"
owner: webapp
group: webapp
content: |
/var/app/current/logs/db/*.log

.ebextension/01-setupKSTTimezone.config

1
2
3
commands:
set_time_zone:
command: ln -f -s /usr/share/zoneinfo/Asia/Seoul /etc/localtime

Procfile을 사용해서 애플리케이션 프로세스 구성

애플리케이션이 실행하기 위한 커맨드를 모은 파일이다. Procfile의 프로세스는 계속 실행 될 것으로 기대하고 EB에서 해당 프로세스를 모니터링하고 종료된 프로세스는 재시작한다.

Procfile

1
web: appstart

Github Actions로 CICD 구축하기

CI 스크립트 작성

사실 CI 스크립트는 매우 간단하다. Github Actions에 대한 지식이 있으면 구현할 수 있다.

.github/workflows/backend.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
name: backend

on:
pull_request:
branches:
- main
- release
paths:
- 'backend/**'

defaults:
run:
working-directory: backend

jobs:
build:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v3

- name: set up JDK
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'

- name: grant execute permission for gradlew
run: chmod +x gradlew

- name: gradle build
run: ./gradlew build

- name: add comments to a pull request
uses: mikepenz/action-junit-report@v3
if: always()
with:
report_paths: backend/build/test-results/test/TEST-*.xml

IAM 인증키 발급받아 Github Actions에서 사용하기

외부에서 AWS 리소스에 접근하려면 IAM을 통해 액세스 키가 필요하다. AWS IAM에 가서 사용자를 추가해주자.
IAM 사용자 추가

해당 IAM 사용자에게 EB에 접근할 수 있는 권한을 설정해주면 된다.
IAM 권한 추가

이렇게 IAM 사용자를 만들면 액세스 키와 시크릿키가 생성된다. 이 두 키를 깃허브 레포지토리에 액션 시크릿으로 추가해준다. 필자는 액세스 키는 AWS_ACCESS_KEY_ID, 시크릿 키는 AWS_SECRET_ACCESS_KEY로 설정했다.
github Action secrets

CD 스크립트 작성

우리는 beanstalk-deploy라는 Github Actions 플러그인을 사용해서 배포 스크립트를 작성해본다.
스크립트에 주석으로 각 단계가 어떤 의미 인지 기록해두었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
name: deploy eb backend
on:
workflow_dispatch:
push:
branches:
- main

defaults:
run:
working-directory: backend

jobs:
build:
runs-on: ubuntu-latest
steps:

- name: Set up JDK
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'

- name: Checkout source code
uses: actions/checkout@v2
with:
submodules: 'true' # 서브 모듈이 존재하는 경우 반드시 넣어준다.
token: ${{ secrets.GH_ACCESS_TOKEN }}

- name: Setup Gradle
uses: gradle/gradle-build-action@v2
with:
cache-read-only: ${{ github.ref != 'refs/heads/main' }}

- name: Grant execute permission for gradlew
run: chmod +x gradlew

- name: Execute Gradle build
run: ./gradlew bootJar

- name: Get current time # 현재 시각을 기록해둔다.
uses: 1466587594/get-current-time@v2.0.2
id: current-time
with:
format: YYYY-MM-DDTHH-mm-ss
utcOffset: "+09:00"

- name: Generate deployment package # JAR 파일과 함께 이전에 만들었던 설정파일을 함께 zip파일로 압축한다.
run: |
mkdir -p deploy
cp build/libs/f12-0.0.1-SNAPSHOT.jar deploy/application.jar
cp Procfile deploy/Procfile
cp -r .ebextensions deploy/.ebextensions
cp -r .platform deploy/.platform
cd deploy && zip -r deploy.zip .

- name: Deploy to EB
uses: einaregilsson/beanstalk-deploy@v21
with:
aws_access_key: ${{ secrets.AWS_ACCESS_KEY_ID }} # IAM
aws_secret_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} # IAM
application_name: f12-prod-backend
environment_name: F12prodbackend-env-2
version_label: github-action-${{steps.current-time.outputs.formattedTime}} # 버전명을 아까 기록한 시간을 토대로 구분하게 했다.
region: ap-northeast-2
deployment_package: backend/deploy/deploy.zip
wait_for_environment_recovery: 180 # 배포가 Green으로 돌아오는 시간을 고려해 환경 회복 시간을 180초로 하였다.

- name: add comments to a pull request
uses: mikepenz/action-junit-report@v3
if: always()
with:
report_paths: backend/build/test-results/test/TEST-*.xml

참고

https://docs.aws.amazon.com/ko_kr/elasticbeanstalk/latest/dg/Welcome.html
https://docs.aws.amazon.com/ko_kr/elasticbeanstalk/latest/dg/concepts-webserver.html
https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/platforms-linux-extend.html
https://docs.aws.amazon.com/ko_kr/elasticbeanstalk/latest/dg/java-se-procfile.html
https://techblog.woowahan.com/2539/

Share