⚙️ 개요
지난 몇 개월간, 여러 사이드 프로젝트의 서버 개발자로 참여하며 대부분의 서버의 배포파이프라인을 글의 제목처럼 Elastic Beanstalk + Github Actions 방식으로 구축했었습니다. ( 자취방 리뷰 서비스, 이룸 / AI 뉴스 리더, 브리핑 많이 구경해주세요!!!🍀) 이번 글에서는 Elastic Beanstalk 환경 설정보다는 스크립트 위주로 작성할 예정입니다. Elastic Beanstalk 관련해서는 다음에 추가적으로 글을 작성하겠습니다.
♻️ 브랜치 전략과 배포 요구사항
이룸이라는 프로젝트 팀에서는 아래와 같은 브랜치 전략을 갖고 있었습니다.
- master ← 운영 서버에 배포되어있는 브랜치
- release ← 다음 릴리즈에 나갈 기능들이 포함되어있는 브랜치
- develop ← 개발 서버에 배포되어있는 브랜치
- feature ← issue/이슈넘버 형태로 생성하여 해당 이슈의 할당된 작업을 하는 브랜치
또한 개발 서버와 운영 서버를 분리하여 관리하고 있었습니다.
- 개발 서버 (e-room.dev)
- develop 브랜치가 push되었을 때 배포가 되어야 함.
- 중단 배포
dev
프로파일로 배포가 되어야 함.
- 운영 서버 (e-room.app)
- master 브랜치로 PR이 합쳐졌을 때 배포가 되어야 함.
- 무중단 배포
prod
프로파일로 배포가 되어야 함
💡 CI/CD를 위한 SpringBoot 프로젝트 구조
Elastic Beanstalk + Github Actions로 배포하기 위한 주요 구성요소는 다음과 같습니다.
- .ebextensions ← Elastic Beanstalk 환경을 구성하거나 커스터마이징 하기 위한 설정 폴더
00-makeFiles.config
- Elastic Beanstalk 환경의 인스턴스에 커맨드를 실행하거나 환경 변수를 설정하고, 패키지를 설치하고, 파일을 생성하는 등의 역할을 하는 설정 파일
- .github ← Github에서 특정 기능을 활용하기 위한 설정 폴더
- workflows ← Github Actions 워크플로우를 정의하는 폴더
deploy.yml
- 수행할 워크플로우를 정의
- workflows ← Github Actions 워크플로우를 정의하는 폴더
Procfile
- 보통 PaaS 서비스에서 애플리케이션의 실행 방법을 지정하는데 사용되는 파일
- .platform ← Elastic Beanstalk 환경을 사용할 때 환경과 관련된 사용자 정의 설정을 저장하기 위한 폴더
nginx.conf
- Nginx 웹 서버의 설정 파일
- 프록시 서버 설정, 리다이렉션, 로깅, SSL 설정, 기타 다양한 웹 서버 관련 설정을 조절 가능
🌐 배포 스크립트 파헤치기
운영 서버를 기준으로 설명하겠습니다. 위에서 설명했듯이 운영서버는 다음과 같은 요구사항을 갖고 있었습니다.
- 운영 서버 (e-room.app)
- master 브랜치로 PR이 합쳐졌을 때 배포가 되어야 함.
- 무중단 배포
prod
프로파일로 배포가 되어야 함
deploy-prod.yml
name: E-room Prod CI/CD # 워크플로우 이름 설정
# 워크플로우 트리거 조건
on:
pull_request: # PR 이벤트에 대한 트리거 설정
branches:
- master # master 브랜치에 대한 PR만 대상
types:
- closed # PR이 닫힐 때만 워크플로우가 실행됨
workflow_dispatch: # 수동으로 워크플로우 실행 가능
jobs: # 워크플로우에서 실행할 작업들 정의
build: # build라는 작업 이름
runs-on: ubuntu-latest # 작업이 실행될 OS 버전 설정
if: github.event.pull_request.merged == true # PR이 병합된 경우에만 작업 실행
steps: # build 작업 내의 실행 단계들
- name: Checkout
uses: actions/checkout@v2 # 현재 리포지토리의 코드 체크아웃
- name: Set up JDK 11
uses: actions/setup-java@v3 # 자바 11 설치
with:
java-version: 11
distribution: 'adopt'
- name: Grant execute permission for gradlew
run: chmod +x ./gradlew # Gradle 래퍼에 실행 권한 부여
shell: bash
- name: Build with Gradle
run: ./gradlew clean build -Dspring.profiles.active=prod -x test # Gradle로 빌드 수행. 테스트는 제외
shell: bash
- name: Get current time
uses: 1466587594/get-current-time@v2 # 현재 시간 정보 획득
id: current-time
with:
format: YYYY-MM-DDTHH-mm-ss
utcOffset: "+09:00"
- name: Show Current Time
run: echo "CurrentTime=$" # 획득한 현재 시간 출력
shell: bash
- name: Generate deployment package
run: | # 배포 패키지 생성
mkdir -p deploy
cp build/libs/*.jar deploy/application.jar
cp Procfile deploy/Procfile
cp -r .ebextensions-prod deploy/.ebextensions
cp -r .platform deploy/.platform
cd deploy && zip -r deploy.zip .
- name: Beanstalk Deploy
uses: einaregilsson/beanstalk-deploy@v20 # AWS Elastic Beanstalk에 배포
with:
aws_access_key: ${{ secrets.AWS_ACTION_ACCESS_KEY_ID }} # AWS 접근 키 (GitHub Secrets에서 가져옴)
aws_secret_key: ${{ secrets.AWS_ACTION_SECRET_ACCESS_KEY }} # AWS 비밀 키 (GitHub Secrets에서 가져옴)
application_name: E-room Prod
environment_name: E-roomProd-env
version_label: github-action-${{ steps.current-time.outputs.formattedTime }} # 배포 버전 라벨 설정
region: ap-northeast-2
deployment_package: deploy/deploy.zip # 배포할 패키지 경로
wait_for_environment_recovery: 60 # 배포 후 환경 복구 대기 시간
- Github Actions는 workflows > jobs > steps 같은 계층 구조를 가지고 있습니다.
- 우리는
E-room Prod CI/CD
라는 workflow를 정의하고 이 workflow는build
라는 job을 수행할 것입니다. build
job은 위와 같은 여러 step들을 가지고 있습니다. 각 step에 대한 설명은 주석으로 적어두었습니다.- 이제 배포 요구사항을 따르기 위해 작성했던 부분들을 설명하겠습니다.
Q. master 브랜치로 병합되었을 때, 배포가 되도록 어떻게 구성할 수 있을까?
workflow의 트리거 조건을 적절히 작성했습니다.
# 워크플로우 트리거 조건
on:
pull_request: # PR 이벤트에 대한 트리거 설정
branches:
- master # master 브랜치에 대한 PR만 대상
types:
- closed # PR이 닫힐 때만 워크플로우가 실행됨
workflow_dispatch: # 수동으로 워크플로우 실행 가능
jobs: # 워크플로우에서 실행할 작업들 정의
build: # build라는 작업 이름
runs-on: ubuntu-latest # 작업이 실행될 OS 버전 설정
if: github.event.pull_request.merged == true # PR이 병합된 경우에만 작업 실행
- workflow가 실행되는 트리거 조건을 위와 같이 작성했습니다.
- Pull Request의 base브랜치가 master이며 PR이 닫혔을 때 실행됩니다.
- 또한, 그냥 PR을 닫는 경우가 아닌 병합이 되었을 경우를 체크해주기 위해 if 분기를 추가했습니다.
Q. 무중단 배포를 어떻게 구성할 수 있을까?
이는 Elastic Beanstalk 환경에서 설정하며 나중에 해당 글을 작성할 때 설명하겠습니다.
Q. prod
와 같은 특정 프로파일로 어떻게 배포할 수 있을까?
주목해야할 부분은 Generate deployment package step입니다.
- name: Generate deployment package
run: | # 배포 패키지 생성
mkdir -p deploy
cp build/libs/*.jar deploy/application.jar
cp Procfile deploy/Procfile
cp -r .ebextensions-prod deploy/.ebextensions
cp -r .platform deploy/.platform
cd deploy && zip -r deploy.zip .
- 이 step에서는 Elastic Beanstalk에 배포하기 위한 형태의 패키지(deploy 폴더)를 만듭니다.
- 여기서는 우리의 배포에 필요한
jar
파일,Procfile
, .ebextensions-prod 폴더, .platform 폴더를 deploy 폴더내로 복사한 후 압축합니다. - Elastic Beanstalk 환경에서는
Procfile
을 보고 시작 스크립트를 실행하는데 시작 스크립트는 .ebextension-prod/00-makeFiles.config 에 정의되어 있습니다. 각 파일을 살펴봅시다.
Procfile
web: appstart
간단히 appstart 스크립트를 실행시키라는 한 줄만 작성되어 있습니다. appstart 스크립트는 00-makeFiles.config
파일에서 정의합니다.
00-makeFiles.config
여기서 appstart 스크립트 파일을 정의했습니다.
files: # Elastic Beanstalk에 필요한 파일들을 생성하거나 수정하는 섹션
"/sbin/appstart" : # /sbin/appstart 라는 파일을 생성하거나 수정
mode: "000755" # 파일의 권한 설정. 여기서는 실행, 읽기 및 쓰기 권한을 소유자에게 부여
owner: webapp # 파일의 소유자를 webapp 사용자로 설정
group: webapp # 파일의 그룹을 webapp 그룹으로 설정
content: | # 아래에 나열된 내용으로 파일을 채운다.
JAR_PATH=/var/app/current/application.jar # JAR_PATH 변수에 애플리케이션 JAR 파일의 경로를 저장
# run app
killall java # 현재 실행 중인 모든 java 프로세스를 종료
java -Dfile.encoding=UTF-8 -Dspring.profiles.active=prod -jar $JAR_PATH # UTF-8 인코딩과 prod 프로파일로 애플리케이션 JAR 파일 실행
prod
와 같은 특정 프로파일로 배포하는 핵심 커맨드는 여기에 있습니다.-Dspring.profiles.active=prod
jar를 실행시킬때 프로파일을 설정해주었습니다.
요약하자면 다음과 같습니다.
- 운영 서버 (e-room.app)
- master 브랜치로 PR이 합쳐졌을 때 배포가 되어야 함. → workflow의 트리거를 적절히 작성합니다.
- 무중단 배포 → Elastic Beanstalk 환경에서 설정합니다.
prod
프로파일로 배포가 되어야 함 → appstart 스크립트에서 jar를 실행할때 프로파일을 포함합니다.
🤐 환경변수와 시크릿 키
지금까지 CI/CD 배포 스크립트를 살펴보았습니다. 그런데 프로젝트를 진행하다보면 DB 접속 계정이나 비밀 키들은 환경변수로 두어 개발하게 됩니다. 따라서 jar를 실행할 때 환경변수가 없으면 에러가 나게 됩니다.
Elastic Beanstalk으로 배포할 때는 Github Actions에서 Elastic Beanstalk으로 접근할 수 있도록 IAM 계정을 생성해주어야 합니다. IAM 계정은 생성해두었다고 가정하고 Beanstalk Deploy step를 살펴봅시다.
- name: Beanstalk Deploy
uses: einaregilsson/beanstalk-deploy@v20 # AWS Elastic Beanstalk에 배포
with:
aws_access_key: ${{ secrets.AWS_ACTION_ACCESS_KEY_ID }} # AWS 접근 키 (GitHub Secrets에서 가져옴)
aws_secret_key: ${{ secrets.AWS_ACTION_SECRET_ACCESS_KEY }} # AWS 비밀 키 (GitHub Secrets에서 가져옴)
application_name: E-room Prod
environment_name: E-roomProd-env
version_label: github-action-${{ steps.current-time.outputs.formattedTime }} # 배포 버전 라벨 설정
region: ap-northeast-2
deployment_package: deploy/deploy.zip # 배포할 패키지 경로
wait_for_environment_recovery: 60 # 배포 후 환경 복구 대기 시간
- einaregilsson/beanstalk-deploy@v20를 사용해 Elastic Beanstalk에 배포합니다.
- IAM의 aws_access_key와 aws_secret_key는 Github Secrets에서 설정하며 위치는 사진과 같습니다.
- Settings > Secretes and variables > New repository secret
다음으로 애플리케이션에서 필요한 환경변수(스프링부트 application.yml에서 사용하는 환경 변수)는 어디에 설정하는지 알아봅시다.
- Elastic Beanstalk > 구성 > 업데이트, 모니터링 및 로깅
- 업데이트 모니터링 및 로깅을 편집하여 Environment properties 에 환경변수를 추가해줄 수 있습니다.
또 다른 방법도 존재합니다. Elastic Beanstalk에 환경변수를 추가하지 않고, Github Actions에 step을 추가하는 방법입니다. 아래 코드는 다른 프로젝트에서 사용한 방법입니다.
- name: Set Environment
uses: microsoft/variable-substitution@v1
with:
files: ./Winey-Common/src/main/resources/application-common.yml
env:
oauth.kakao.base-url: ${{ secrets.KAKAO_BASE_URL }}
oauth.kakao.client-id: ${{ secrets.KAKAO_CLIENT }}
oauth.kakao.client-secret: ${{ secrets.KAKAO_SECRET }}
oauth.kakao.redirect-url: ${{ secrets.KAKAO_REDIRECT }}
oauth.kakao.app-id: ${{ secrets.KAKAO_APP_ID }}
oauth.kakao.admin-key: ${{ secrets.KAKAO_ADMIN_KEY }}
jwt.secret: ${{ secrets.JWT_SECRET_KEY }}
jwt.refresh: ${{ secrets.JWT_REFRESH_KEY }}
cool-sms.api-key: ${{ secrets.COOL_SMS_API_KEY }}
cool-sms.api-secret: ${{ secrets.COOL_SMS_API_SECRET }}
cool-sms.from-number: ${{ secrets.COOL_SMS_FROM_NUMBER }}
cool-sms.domain: ${{ secrets.COOL_SMS_DOMAIN }}
aws.s3.bucket: ${{ secrets.AWS_BUCKET }}
aws.s3.base-url: ${{ secrets.AWS_BASE_URL }}
aws.s3.access-key: ${{ secrets.AWS_S3_ACCESS_KEY }}
aws.s3.secret-key: ${{ secrets.AWS_S3_SECRET_KEY }}
- application.yml에 Github Secrets의 값을 끼워 넣어주는 step을 추가함으로써 jar가 환경변수를 가진 채 빌드되도록 방법입니다.
- 체감상 두번째 방법이 배포 속도가 더 빨랐는데, 검증된 정보는 아닙니다!
요약하자면 Elastic Beanstalk 환경에 환경변수를 추가하는 방법과 build job에 step을 추가하는 방법이 있습니다.
🤔 개발서버는?..
그러면 이제 개발서버의 배포 요구사항을 다시 살펴봅시다.
- 개발 서버 (e-room.dev)
- develop 브랜치가 push되었을 때 배포가 되어야 함.
- 중단 배포
- dev 프로파일로 배포가 되어야 함.
지금까지 살펴본 내용으로 충분히 해결할 수 있는 내용입니다.
Q. develop 브랜치가 push되었을 때 배포가 되도록 어떻게 할까?
workflow의 트리거를 develop 브랜치가 push 되었을 때 배포하도록 작성합니다.
- deploy-dev.yml
on:
push:
branches:
- develop
workflow_dispatch:
Q. 중단 배포를 어떻게 구성할 수 있을까?
이는 Elastic Beanstalk 환경에서 설정하며 나중에 해당 글을 작성할 때 설명하겠습니다.
Q. dev 와 같은 특정 프로파일로 어떻게 배포할 수 있을까?
appstart 스크립트에서 jar를 실행할때 프로파일을 포함합니다.
- .ebextensions-dev/00-makeFiles.config
files:
"/sbin/appstart" :
mode: "000755"
owner: webapp
group: webapp
content: |
JAR_PATH=/var/app/current/application.jar
# run app
killall java
java -Dfile.encoding=UTF-8 -Dspring.profiles.active=dev -jar $JAR_PATH
🖥️ 기타 설정 파일
💡 CI/CD를 위한 SpringBoot 프로젝트 구조 섹션에서 적어두었지만 설명하지 않은 .platform/nginx.conf 파일에 대해 알아봅시다.
Elastic Beanstalk는 nginx를 리버스 프록시로 사용하여 애플리케이션을 포트 80의 ELB 로드 밸런서에 매핑합니다.
기본적으로 Elastic Beanstalk은 요청을 포트 5000의 애플리케이션에 전달하도록 nginx 프록시를 구성합니다. 그래서 PORT 환경 변수를 기본 애플리케이션이 수신 대기하는 포트로 설정하여 기본 포트를 재정의할 수 있습니다. (스프링부트의 경우 보통 8080 재정의)
- 기본 포트를 재정의 하는 방법
- 환경 속성 중에 사진상에 맨 아래보이는 PORT 값을 설정해주면 됩니다.
nginx의 구성은 nginx.conf 파일을 통해 재정의할 수도 있습니다.
- .platform/nginx.conf
# Nginx process settings
user nginx; # Nginx 프로세스를 'nginx' 사용자로 실행
error_log /var/log/nginx/error.log warn; # 에러 로그의 위치와 경고 수준을 지정
pid /var/run/nginx.pid; # Nginx의 PID 파일 위치
worker_processes auto; # 자동으로 워커 프로세스 수를 결정
worker_rlimit_nofile 33282; # 각 워커 프로세스가 열 수 있는 파일의 최대 수
# Events module settings
events {
use epoll; # 사용할 이벤트 모델 (Linux에서의 high-performance 방법)
worker_connections 1024; # 한 워커 당 연결 수 제한
multi_accept on; # 한 번에 여러 연결을 수락
}
# HTTP module settings
http {
include /etc/nginx/mime.types; # MIME 타입 설정 포함
default_type application/octet-stream; # 기본 MIME 타입 설정
# 로그 포맷 정의
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; # 추가적인 설정 포함
# Websocket 지원을 위한 설정
map $http_upgrade $connection_upgrade {
default "upgrade";
}
# Spring Boot 애플리케이션으로의 프록시 설정
upstream springboot {
server 127.0.0.1:8080; # 로컬의 8080 포트로 연결
keepalive 1024; # keep-alive 연결 수
}
# 기본 서버 설정
server {
listen 80 default_server; # IPv4에서 80 포트 리스닝
listen [::]:80 default_server; # IPv6에서 80 포트 리스닝
# 모든 요청에 대한 설정
location / {
proxy_pass <http://springboot>; # 요청을 springboot 업스트림에 전달
proxy_http_version 1.1; # 프록시 요청의 HTTP 버전
proxy_set_header Connection $connection_upgrade; # 헤더 설정
proxy_set_header Upgrade $http_upgrade; # WebSocket 헤더 설정
proxy_set_header Host $host; # 원래 요청의 Host 헤더 유지
proxy_set_header X-Real-IP $remote_addr; # 실제 클라이언트 IP 전달
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 프록시된 IP를 전달
}
access_log /var/log/nginx/access.log main; # 접근 로그 위치 및 포맷
# 타임아웃 및 압축 관련 설정
client_header_timeout 60;
client_body_timeout 60;
keepalive_timeout 60;
gzip off; # gzip 압축 비활성화
gzip_comp_level 4; # gzip 압축 레벨 (사용되지 않음, gzip이 off)
# Elastic Beanstalk의 health check 설정 포함
include conf.d/elasticbeanstalk/healthd.conf;
}
}
- nginx에 대한 자세한 설명은 생략하겠습니다!
🔥 Slack으로 Github Actions 알림 받기
사실 배포가 잘 시작했는지, 잘 완료되었는지 여부는 Github의 Actions 탭에 들어가면 확인할 수 있지만 이를 슬랙 알림으로 받으면 편할 것 같아서 설정했습니다.
우선 프로젝트 워크스페이스에 Github 앱이 설치되어있어야합니다.
다음으로, 배포 알림을 받을 채널을 생성합니다.
- 저희는 tech-deploy라는 채널을 만들어 사용하고 있습니다.
마지막으로, 해당 채널에 메시지를 전송해서 특정 workflow를 구독하도록 만듭니다.
/github subscribe <organization>/<repository> workflows:{name: "<workflow 이름>"}
- 이렇게 설정하면 해당 채널은 이제 해당 workflow가 트리거될 때마다 알림을 받게 됩니다!
📚 마치며..
확실히 CI/CD 파이프라인을 한 번 구축해두면 정말 편리한 것 같습니다. 현재 재직중인 회사에서는 프로젝트 초기 단계라 아직 구축해두지 않아 일일이 서버에 접속해서 소스를 가져오고 커맨드를 실행하는 것을 반복하고 있는데 너무 번거롭습니다..😅
이번에 작성한 글은 기억에 의존해 작성하였기때문에 틀린 부분이나 최신화되지 않은 정보가 있을 수 있습니다. 틀린 부분이 있다면 계속 수정하겠습니다!
'🧑🏻💻 Develop > Devops' 카테고리의 다른 글
[Jenkins] GitLab과 연동하기 (1) | 2024.04.28 |
---|---|
[Jenkins] 설치 및 실행하기 (0) | 2024.04.28 |