첫 해커톤에서 CI/CD 파이프 라인을 구축하느라 애썼던 기억이 있어서 참고용으로 UMC 6th 워크북을 다시 보면서 정리해두려고 한다. AWS에 대한 자세한 내용은 나중에 따로 공부하면서 추가해야겠다.
CI/CD
CI(Continuous Integration) - 지속적 통합
CI는 코드가 수정될 때마다 지속적으로 편하게 통합되어 빌드, 테스트를 하는 과정을 말한다.
- UMC에선 테스트까진 다루지 않고 지속적으로 빌드가 되는 과정까지만 다룬다.
CI를 도와주는 툴로는 GitHub Actions와 AWS Code Pipeline 등이 존재한다.
- UMC는 한 계정 당 달에 1000시간을 무료로 사용할 수 있는 GitHub Actions를 사용한다.
- GitHub Actions는 처음엔 무료지만 유료로 전환이 되면 AWS Code Pipeline보다 비싸진다.
CD(Continuous Deploy/Delivery) - 지속적 배포
CD의 핵심은 사용자가 직접 인프라(EC2 등)를 만드는 게 아니라 툴을 통해 만드는 것이다.
CD에 사용되는 툴에는 AWS Elastic Beanstalk과 AWS Elastic Container Service 등이 존재한다.
- ECS의 경우는 난이도가 높고, 많은 부분에 대한 설정을 사용자가 직접 하는 느낌이라면, EB는 비교적 쉽고 툴에서 설정을 대신해 주는 부분이 많기 때문에 UMC에선 EB를 사용한다.
- 단, EB는 내부적으로 Docker를 사용해서 컨테이너 단위로 배포하는 경우, ECS보다 불편해진다.
- 따라서, 단일 플랫폼을 이용한 EB 배포를 다룬다.
CI/CD 파이프 라인 구축하기 - Git Flow 정하기
우선, CI/CD 파이프 라인을 구축하기 전에 브랜치 전략부터 정해야 한다.
- 정해진 답은 없지만 보통은 기능들을 각 브랜치(ex. feature)로 분리해 동시에 여러 사람이 개발하고, 이를 하나의 브랜치(ex. develop)에 합친 후 통합적으로 버그를 찾아내고 수정하는 과정을 채택하는 게 좋다.
- 이후 버그를 모두 수정하고 기능 개발까지 끝난 코드를 하나의 브랜치(ex. main)에 모아 배포하면 된다.
- 추가로, develop 브랜치에 모여 있는 코드를 외부에서 접근할 수 있도록 배포하여, Client 측에서 API를 호출하고 잘 동작하는지 확인할 수 있는 과정이 필요하다.
런칭까지 하진 않는 간단한 프로젝트를 진행할 때는 아래 방식을 추천한다고 한다. 따라서, 적어도 2개의 CI/CD 파이프 라인이 있어야 한다.
- develop 브랜치에 팀원들의 코드를 모으고, 이를 테스트할 수 있도록 배포
- 데모데이 전, 실제 배포를 위한 release 브랜치에 테스트가 완료된 코드를 merge하고 배포
주의!
팀원이 새로 브랜치를 팔 때(ex. 새로운 기능 구현), 반드시 develop 브랜치에서 뻗어 나가 새 브랜치를 만들어야 한다.
CI/CD 파이프 라인 구축하기 - GitHub Action
워크북 예제에서는 develop 브랜치에 새로 merge가 되면, GitHub Actions를 통해 AWS Elastic Beanstalk에 자동으로 배포가 되도록 CI/CD 파이프 라인을 구축해 본다.
- 절대 사람 손으로 하지 않고 트리거를 통해 이벤트 기반으로 돌아가게 한다.
1. 프로젝트를 위한 리포지토리 생성
2. main 브랜치에 초기 README나 아무것도 없는 Spring Boot 코드 push
3. 이후 main 브랜치에서 바로 develop 브랜치 만들기
4. 개발용 CI/CD 파이프 라인 구축하기
1. GitHub Actions 스크립트 작성하기
GitHub Actions는 GitHub가 Action의 대상이 되는 리포지토리에서 .github 폴더를 찾아서 그 내부의 yml 파일을 읽어 들여 진행된다.
- 이때, 반드시 리포지토리의 root 경로에 .github 폴더가 있어야 한다. 다른 곳에 존재한다면 GitHub Actions이 진행되지 않는다.
프로젝트 디렉터리 구조에서 root 경로에 .github 폴더를 추가하고 그 내부에 CI/CD용 배포 스크립트를 만들어야 한다.
스크립트는 아래와 같다. GitHub Actions는 jobs 내부의 여러 step을 통해 동작한다. step마다 원하는 동작을 하도록 작성할 수 있고, CI/CD 파이프 라인 말고도 AWS를 통해 이메일을 보내는 등의 작업도 포함할 수 있다.
- if 부분을 살펴보면, PR merge에 성공하고 PR의 base 브랜치 이름이 develop이면 작동하도록 되어 있다.
- 각각 step의 name은 마음대로 지어도 된다. 다만 GitHub에 박제되기 때문에 알아보기 쉽게 적는 것이 좋다.
- Checkout
- Action이 돌아가는 대상인 develop 브랜치의 코드를 가져온다.
- 원한다면 다른 Public 리포지토리의 특정 브랜치 코드도 가져올 수 있다. Private 리포지토리라면 추가적인 인증 과정을 거쳐야 한다.
- Set up JDK 21 & Grant execute permission for gradlew & Build with Gradle
- GitHub Actions를 이용해 Jar 파일을 만들기 위해, 자체적으로 Java JDK를 설치하고 gradle이 들어갈 대상에 권한을 부여한다.
- Java 버전을 알맞게 고치면 된다.
- Get current time & Show current time
- 타임스탬프를 기록한다.
- Generate deployment package
- GitHub Actions를 통해 GitHub가 자체적으로 리눅스 가상 환경을 만들어 배포에 필요한 빌드 과정을 진행한다.
- Beanstalk Deploy
- AWS Elastic Beanstalk에 배포를 진행한다.
- Checkout
name: UMC Dev CI/CD # 아무 이름이나 짓기
on:
pull_request:
types: [closed]
workflow_dispatch: # (2).수동 실행도 가능하도록
jobs:
build:
runs-on: ubuntu-latest # (3).OS환경
if: github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'develop'
steps:
- name: Checkout
uses: actions/checkout@v2 # (4).코드 check out -> 위에 적은 develop 브랜치에서 코드 가져오기
- name: Set up JDK 21
uses: actions/setup-java@v3
with:
java-version: 21 # (5).자바 설치
distribution: 'adopt'
- name: Grant execute permission for gradlew
run: chmod +x ./gradlew
shell: bash # (6).권한 부여 -> 새로 들어 갈 컴퓨터(E2C 등)의 gradlew에 대한 실행 권한
- name: Build with Gradle
run: ./gradlew clean build -x test
shell: bash # (7).build시작
- name: Get current time
uses: 1466587594/get-current-time@v2
id: current-time
with:
format: YYYY-MM-DDTHH-mm-ss
utcOffset: "+09:00" # (8).build시점의 시간확보
- name: Show Current Time
run: echo "CurrentTime=${{ steps.current-time.outputs.formattedTime }}"
shell: bash # (9).확보한 시간 보여주기
- name: Generate deployment package
run: |
mkdir -p deploy
cp build/libs/*.jar deploy/application.jar
cp Procfile deploy/Procfile
cp -r .ebextensions-dev deploy/.ebextensions
cp -r .platform deploy/.platform
cd deploy && zip -r deploy.zip .
# build/libs/*: 깃험 액션이 빌드한 파일을 컴퓨터에 올리기 전 임시적으로 저장해 두는 디렉토리
# Procfile: 리눅스의 Makefile과 비슷한 것
# .ebextensions-dev: beanstalk에게 추가적인 기능을 요청하기 위한 설정
# .platform: 추가적인 플랫폼 설정(웹 서버인 nginx 등)
# 마지막은 압축!
- name: Beanstalk Deploy
uses: einaregilsson/beanstalk-deploy@v20
with:
aws_access_key: ${{ secrets.AWS_ACTION_ACCESS_KEY_ID }}
aws_secret_key: ${{ secrets.AWS_ACTION_SECRET_ACCESS_KEY }}
application_name: {application_name}
environment_name: {environment_name}
version_label: github-action-${{ steps.current-time.outputs.formattedTime }}
region: {region}
deployment_package: deploy/deploy.zip
wait_for_deployment: false # true면 GitHub Actions에서 deploy 로그 확인 가능
wait_for_recovery: 180 # 기본값 30
2. ebextensions 작성
위에 스크립트를 미리 작성해 두고 그에 맞춰 다른 파일들을 작성해야 한다.
가장 먼저 Docker 없이 단일 플랫폼으로 Elastic Beanstalk에 배포를 할 경우, 배포 대상이 되는 파일이 EB가 생성할 EC2의 어디에 위치해야 하는지를 알려줘야 한다.
- 이를 포함한 EB에 추가적인 설정을 해주는 파일을 .ebextensions 폴더에 모아둔다. 이 폴더도 root 경로에 생성해야 한다.
00-makeFiles.config
- 앞에 붙은 00번은 EB에서 EC2를 만들고 WAS를 설치할 때 순서대로 파일을 적용하기 때문에 붙는 것이다.
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 -jar $JAR_PATH
- 스프링의 application.yml에 profile 설정을 할 경우 아래 파일의 마지막 라인을 아래처럼 수정해서 사용해야 한다.
java -Dfile.encoding=UTF-8 -Dspring.profiles.active=dev -jar $JAR_PATH
01-set-timezone.config
- Elastic Beanstalk에 배포를 할 경우, 서버 시간이 영국 시간이 된다. 따라서 한국 시간이 되도록 설정을 해줘야 하며 01번 파일에 설정한다.
commands:
set_time_zone:
command: ln -f -s /usr/share/zoneinfo/Asia/Seoul /etc/localtime
CI/CD 파이프 라인 구축하기 - Elastic Beanstalk
서울 리전에서 진행한다.
1. VPC 생성
서브넷을 Public 2개와 Private 2개를 만들 예정이다. RDS에 Private 서브넷 2개를 주기 위해 추가로 만든다.
참고
디폴트 VPC의 리소스 맵을 살펴보면, 모든 서브넷이 인터넷 게이트웨이에 연결돼 있는 것을 확인할 수 있다.
(디폴트 VPC = Public)
우선 [VPC 설정]에서 이름 태그와 IPv4 CIDR을 작성하고 VPC를 생성한다.
이후 Public 서브넷 2개와 Private 서브넷 2개를 만든다.
- 프리 티어에서 제공해 주는 건 가용 영역 a와 c를 지원하기 때문에 서브넷도 거기에 맞춰서 생성해야 한다.
다음으로 Public 서브넷을 위해 인터넷 게이트웨이를 만들고, VPC에 연결한 뒤 라우팅 테이블을 생성한다.
- [라우팅 편집]에서 아웃바운드 설정을 추가한다.
이후 명시적 서브넷 연결을 편집한다.
- 이때 Public 서브넷 2개만 선택해야 한다.
생성한 VPC에 RDS를 연결하려면 VPC 설정 편집에서 아래 설정을 꼭 해야 한다. 하지 않으면 아래 에러가 발생하면서 RDS를 생성할 수 없다고 뜬다.
Cannot create a publicly accessible DBInstance. The specified VPC does not support DNS resolution, DNS hostnames, or both. Update the VPC and then try again
마지막으로 보안 그룹을 생성한다.
2. IAM 생성
AWS는 DevOps 업체이기 때문에 가용성 / (기밀성 / 무결성 → 보안)을 중요하게 생각한다. IAM은 보안을 위한 기능이라고 보면 된다. 예를 들어, 루트 계정에서 IAM 계정을 만들어 EC2 조회 권한만 넣어주면 해당 계정은 EC2를 조회할 수만 있다.
IAM으로 만들 수 있는 건 총 세 가지가 있다.
- 사용자(계정)
- 일일 작업을 수행하는 계정 내 사용자
- 역할
- AWS Elastic Beanstalk이 배포를 진행할 때, 새로운 EC2를 직접 만들고 기존의 EC2를 대체한다. → Cloud Formation
- 그러나 EB에 해당 권한이 없기 때문에 IAM의 역할을 통해 EC2 생성 권한(정책)을 넣어줘야 한다.
- AWS Elastic Beanstalk이 배포를 진행할 때, 새로운 EC2를 직접 만들고 기존의 EC2를 대체한다. → Cloud Formation
- 정책(권한)
먼저 EB 자체를 위한 역할을 생성하자.
보통은 아래의 정해진 이름을 주로 사용한다.
위에서 사용 사례에 EC2를 선택했기 때문에 해당 역할을 EB에 넣어주더라도 EB를 신뢰하지 못한다. 따라서 신뢰 정책을 편집해 EB를 신뢰할 수 있도록 해야 한다.
다음으로 EC2 자체에 대한 역할을 만든다.
보통은 아래의 정해진 이름을 주로 사용한다.
3. Elastic Beanstalk 생성
아래 버튼을 누르면 EB를 만들 수 있다.
애플리케이션 이름을 입력하면, 환경 이름도 자동으로 입력된다.
플랫폼은 자바로 선택한다.
무중단 CI/CD
배포가 진행될 때, 기존의 EC2를 날리거나 Spring Boot만 날려버릴 수 있다. 따라서 새로운 EC2가 빌드되는 동안 길면 5분 정도의 공백이 생겨 서버가 죽게 된다. 게임 업데이트 때문에 게임에 접속할 수 없는 상황을 생각해 보면 이해가 잘 될 거다. 이런 문제를 해결하기 위해 무중단 CI/CD가 생겼다.
무중단 CI/CD로 만들기 위해 [사용자 지정 구성]을 선택한다.
위에서 만든 IAM 역할을 선택한다.
내가 만든 VPC를 선택한다.
EB가 EC2를 만들고 나서 EC2가 잘 만들어졌는지 확인할 수 있어야 한다. 이때 EC2와 EB가 네트워크로 통신하면서 확인을 하게 된다. 따라서 퍼블릭 IP 주소를 꼭 활성화해야 한다.
- EB는 가용성을 생각하기 우선으로 생각하기 때문에 서브넷을 2개 이상 연결해야 만족한다. 이때 꼭 Public 서브넷을 선택하자.
위에서 만든 보안 그룹을 선택한다.
오토 스케일링은 자동적으로 인프라의 크기를 결정하는 것이다. EB가 배포하는 과정에서 EC2 생성과 삭제를 해야 하는데 최댓값이 1개라면 무중단 배포가 아닌, 단일 배포가 된다.
- 따라서 EB 1개를 배포할 거라면, [밸런싱 된 로드]를 선택하고 인스턴스의 최솟값을 1로, 최댓값을 2로 설정해야 무중단 배포가 된다.
- 아래에서 t3.small을 빼지 않으면 과금이 발생한다.
EC2가 생성되고, EC2 안의 WAS도 정상적으로 작동하는지 확인하기 위해 GET 요청을 보내 응답이 오는지 확인한다.
- 아래 경로를 변경해 GET 요청을 보낼 경로를 바꿀 수 있다.
정해진 시간에 업데이트될 필요가 있다면 활성화하면 된다.
무중단 CI/CD를 위해선 [추가 배치를 사용한 롤링]을 선택해야 한다.
로드 밸런싱
외부 요청이 올 때 뒤에 있는 여러 가지 서버 중에서 어떤 서버에 갈지 매핑을 해야 한다. 이때 EB 로드 밸런서가 EB의 WAS가 5000 포트에 있을 거라고 기본 설정을 해둔다. 그런데 Spring 서버는 8080 포트에 있기 때문에 환경 속성에 {PORT}:{8080}으로 넣어 기본 설정을 바꿔줘야 한다.
4. Action IAM 추가 생성
EB에 접근하고, EB를 만드는 등 모든 행위를 할 수 있는 사용자를 생성한다. GitHub Actions에게 권한을 주기 위해 만든 거라고 보면 된다.
- 콘솔 로그인 링크를 통해 해당 사용자로 접속할 수 있다.
- AWS에 접근할 수 있는 액세스 키를 만든다. GitHub는 AWS 밖에서 실행되는 애플리케이션이다.
만든 액세스 키를 GitHub 리포지토리에 넣어줘야 한다.
EB 구성에서 [인스턴스 트래픽 및 크기 조정]에서 리스너에 8080 포트를 추가한다.
5. RDS 생성
DB 서브넷 그룹부터 만든다.
- dev용은 public 서브넷 2개로 만든다.
이후 DB를 생성한다.
- MySQL & 프리 티어 선택
- 활성화 시 과금 주의
- dev용 db 서브넷 그룹 선택
- 기존에 만든 보안그룹 선택 및 아래 선택 시 과금 주의
- release 전용 db도 비슷하게 만들면 된다. (release 서브넷 그룹 선택 O & 퍼블릭 액세스 선택 X)
마무리
1. 기타 필요한 파일 작성
EB를 만들 때 프록시 서버 설정을 NGINX로 해서 생생했고, GitHub Action 스크립트의 .platform이 그에 대한 설정인 것을 확인할 수 있다.
- 따라서 3개의 파일을 추가적으로 작성해줘야 한다.
.platform
- 이 폴더도 root 경로에 만들어야 한다.
- nginx.conf 파일 내용은 아래처럼 작성한다.
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;
multi_accept on;
}
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;
listen [::]:80 default_server;
location / {
proxy_pass http://springboot;
# CORS 관련 헤더 추가
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type';
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;
}
}
Procfile
- 이 파일도 root 경로에 작성해야 한다.
web: appstart
build.gradle
- 맨 밑에 아래 코드를 추가한다.
jar {
enabled = false
}
마지막으로 .github 내부 yml 파일의 Beanstalk Deploy step을 내가 만든 EB 환경과 application 이름으로 수정한다.
2. Health 체크용 API 구현
배포된 WAS가 잘 동작한다는 것을 확인할 수 있도록 Health 체크용 API를 만들어 주는 게 좋다.
@RestController
public class RootController {
@GetMapping("/health")
public String healthCheck() {
return "I am healthy";
}
}
위에서 만든 EB 주소 뒤에 /health 경로를 입력했을 때 "I am healthy"가 뜬다면 정상적으로 동작한다는 뜻이다.