현재 진행중인 웹 토이 프로젝트에서 서버 배포를 담당했다.
우리팀은 프론트와 백이 편리하게 개발할 수 있도록 CI/CD를 구축하기로 했다.
CI/CD가 무엇인지는 다른 블로그에서 더 잘 설명해 줄 거에요😀
뭐든 하면 되겠지라는 내 마인드와 달리 배포를 멘땅에 헤딩하는 것은 정말 쉽지 않은 일이었다..
결국 해내긴 했지만 그간 있었던 삽질과 좌절, 절망, 우울, 압박 등을 생각하며, 다신 나와같은 사람이 있지 않았으면 하는 마음에서 이 글을 작성한다.
이 글에서는 내가 진행했던 방법을 처음부터 끝까지 하나하나 설명을 가지고 순서대로 나열할 것이다.
여러가지 방법과 여러가지 시행착오를 겪고나서 느끼고 배운 것들도 작성하고자 한다.
배포 방법에는 여러 가지가 있다. 지금의 나로서는 나열하지는 못하겠지만, 로컬에 있는 프로그램을 클라우드 상으로 배포하는 방법에는 여러 가지가 있다.
이번에 하는 방법도 정말 수많은 방법들 중 하나이다.
처음에 난 배포부터 해보자 해서 단순히 ec2 인스턴스 생성 후, FileZilla와 git bash를 이용한 ssh 접근을 통해 로컬의 jar 파일을 ec2 인스턴스로 넘겨주어 jar 파일을 실행하는 방식으로 했었다.
이렇게 배포는 완료했지만 정작 중요한 것은 CI/CD이므로 복잡한 AWS CodeDeploy를 사용하지 않고 github action으로만 구축하려고 했다.
그리고 막상 전부 수작업으로 배포를 해보니 CI/CD가 필수라고 생각이 들었다.
하지만 찾아보는 모든 글마다 CodeDeploy를 사용했다.
그러다 문득 내가 계속 CodeDeploy를 피해다니는 느낌이 들어 그래! 부딪혀보자! 라는 마인드로 시작하게 되었다.
조금 내용이 길지만 최대한 아는 것을 풀어서 설명할 예정이니 하나하나 잘 따라와주길 바랍니다.
시작하기 전에, 미리 머리속에 어떤 흐름으로 진행할 지 심어두면 훨씬 도움이 되실겁니다.
여러분의 github repository와 github action을 통해서, main에 push할 때마다 build를 진행 후, 여러분의 프로젝트를 zip파일로 압축합니다. 그 zip파일을 AWS의 S3라는 버킷에 담고, CodeDeploy를 통해 EC2 instance에 전달하게 됩니다.
미리 설정해놓은 script를 통해 현재 서버가 진행중이라면 서버를 닫고 현재 가지고 있는 프로젝트의 .jar 파일을 실행시켜 서버를 재가동 시킵니다.
만약 서버가 가동중이 아니라면, 현재 가지고 있는 프로젝트의 .jar 파일을 실행시킵니다.
DB의 경우 AWS RDS를 이용하여 클라우드 상으로 배포하여 이용할 것입니다.
글 형식은, 사진이 있다면 사진 바로 아래에 사진 설명이 있습니다.
참고하여 보시면 되겠습니다.
AWS에 로그인하면, 자신 계정 왼쪽에 지역이 있습니다. 원하는 ec2 사용지역을 눌러줍시다.
참고로 ec2란, AWS에서 제공해주는 임의의 컴퓨터라고 생각하시면 됩니다.
따라서 ec2 인스턴스에 여러분의 프로젝트를 이동시켜 실행파일(.jar)을 실행시키면 서버가 배포가 됩니다.
검색을 통해 EC2 메뉴로 들어가줍시다.
들어가면 인스턴스 시작 버튼이 있을겁니다. 눌러줍시다.
인스턴스의 이름을 지어줍시다.
생성할 인스턴스의 OS를 선택합니다. 여기서는 ubuntu 20.04 를 선택해줍니다.
인스턴스 유형은 그대로 t2.micro로 해주시고,
키 페어는 중요합니다. 생성한 ec2 인스턴스로 연결 및 접속하기 위해 필요한 일종의 키 라고 생각하시면 됩니다.
새 키 페어 생성 을 눌러줍니다.
키 페어 이름엔 본인이 원하는 키의 이름을 적어주고
프라이빗 키 파일 형식은 .pem 으로 선택하여 키 페어 생성합시다.
키 페어는 한 번 저장 후 다시 저장할 수 없으니 꼭 소중하게 여겨주세요.
보안 그룹을 설정하는 곳입니다. 처음이라면 보안 그룹 생성과 기존 설정 값으로 선택해줍시다.
저는 보안그룹 launch-wizard-1이 존재하기에 2로 생겼네요.
참고: 보안 그룹이라는 보안과 관련된 그룹을 만들어, 어느 곳에서 접속을 허락하는지 설정할 수 있습니다.
하나의 보안 그룹이 여러 ec2 인스턴스에 설정될 수 있습니다.
그 외에는 default 값으로 하여 인스턴스를 생성해줍시다.
그리고 인스턴스를 누르면 생성된 인스턴스를 확인할 수 있습니다.
EC2 인스턴스는 재가동 시 퍼블릭 IP가 변경됩니다. 따라서 지속적인 개발환경에서 사용하려면 고정적인 IP주소가 필수겠지요?
현재 IP주소를 고정으로 저장해봅시다.
왼쪽 메뉴바를 내리면, 이러한 그룹이 있습니다. 여기서 탄력적 IP를 눌러줍시다.
들어갔다면 탄력적 IP 주소 할당을 눌러줍니다.
그리고 바로 할당 버튼을 눌러줍시다.
그럼 이렇게 생성이 되었을텐데, 저 IP주소를 클릭하여 작업에서 탄력적 IP 주소 연결을 눌러줍시다.
들어가면
이런 식으로 뜰텐데, 아까 생성한 ec2 인스턴스를 클릭하여 넣어주고, 프라이빗 IP 주소도 클릭하여 넣어줍시다.
그리고 바로 연결을 눌러줍니다.
현재 퍼블릭 IPv4 주소와 탄력적 IP주소가 매칭이 된 것을 확인할 수 있습니다.
CodeDeploy와 EC2 인스턴스를 연계하기 위한 설정입니다.
생성한 ec2 인스턴스를 눌러 다음과 같이 태그 관리를 찾아서 눌러주세요.
다음과 같이 키의 이름과 값을 넣어주세요.
그 다음 EC2 인스턴스에서 S3 버킷에 올려놓은 데이터에 접근할 수 있도록 권한을 줄겁니다.
아래 사진과 같이 IAM 페이지로 들어가주세요.
그리고 역할로 들어가주시고, 역할 만들기 버튼을 찾아 눌러주세요.
여기까지 진행해주시고 완료를 눌러주세요.
이제 EC2 인스턴스에서 IAM을 연결할게요.
EC2 페이지로 들어가서 생성한 EC2 인스턴스를 눌러 아래와 같이 IAM 역할 수정을 눌러주세요.
방금 생성한 IAM 역할을 클릭 후 저장을 눌러줍니다.
이제 여기서 아까 저장했던 my-key.pem을 이용하여 ec2 인스턴스로 접속을 해볼겁니다.
인스턴스 목록에서 생성한 인스턴스를 누르고 연결 버튼을 찾아 눌러주면 아래와 같은 창이 뜹니다.
그리고 git bash를 실행시키고 현재 본인이 my-key.pem 를 저장한 디렉터리로 이동합니다.
그 후 예: ssh ~~~ 에 있는 라인을 복사 후 git bash에 붙여넣어 입력하세요.
그럼 이제 터미널 환경으로 ec2 인스턴스에 접속한 셈입니다.
이제 아까 만들었던 보안 그룹을 설정해봅시다.
마찬가지로 왼쪽 메뉴바에서 쭉 내리다보면 보안 그룹이 있습니다. 들어가주세요.
들어가면 생성된 보안 그룹들이 쭉 있을텐데, 아까 ec2 인스턴스를 생성할 때 만들어진 보안 그룹을 선택해서 (아마 launch-wizard-1)
인바운드 규칙 편집을 눌러줍니다.
보시는 바와 같이 설정해주세요!
인바운드: 외부 -> EC2 인스턴스 내부 허용
아웃바운드: EC2 인스턴스 내부 -> 외부 허용 입니다!
그 다음 다시 EC2 목록으로 가서 생성한 ec2를 눌러 작업 - 네트워킹 - 보안 그룹 변경을 눌러줍니다.
그 다음 만들어진 보안 그룹을 선택해서 적용합니다.
RDS 페이지로 와주세요. 그리고는 데이터베이스 생성을 눌러주세요.
DB 인스턴스 식별자는 사용할 RDS의 이름입니다.
이 부분이 중요한데요, 마스터 사용자 이름은 RDS에 접근하기 위한 사용자 이름(ID)입니다.
마스터 암호는 RDS에 접근하기 위한 암호(PW)입니다.
추후 IntelliJ나 MySQL 워크벤치를 통해 접근하여 확인할 것이니 꼭 기억해주세요!
혹은 본인만의 시그니처 ID/PW를 넣어주세요.
위 그림에서 또 다시 보안 그룹이 나옵니다.
EC2 보안 그룹 설정했듯이 뒤에서 RDS를 위한 보안 그룹 설정을 해줄거에요~
그리고 데이터베이스 생성을 누릅니다. 그러면 생성까지 시간이 조금 걸릴거에요.
이제 RDS 보안 그룹을 설정해볼게요.
생성된 RDS를 클릭해서 보안 그룹을 클릭해줍니다.
현재 저희가 사용중인 RDS와 보안 그룹으로 설정되어 있습니다. 참고해주세요.
들어와서 아까 인바운드 규칙 수정헀듯이 인바운드 규칙 편집을 눌러줍니다.
현재 제 보안 그룹의 설정입니다. 여기서 중요한 것은 가운데입니다.
규칙 추가 - 유형: MYSQL/Aurora - 돋보기: EC2 인스턴스 보안 그룹 으로 설정해줍니다.
EC2 인스턴스 보안 그룹을 정상적으로 만드셨다면 돋보기를 눌렀을 때 알아서 뜰 거예요. 이렇게 설정을 완료해줍니다.
마지막으로 생성한 RDS에 가셔서
저기 밑에 엔드포인트를 복사해줍시다.
방법은 두 가지로 IntelliJ에서 연결해보기, MySQL 워크벤치에서 접속하기가 있습니다.
오른쪽 Gradle 밑에 Database를 눌러줍니다.
이와 같이 MySQL을 찾아 눌러줍니다.
여기서 중요한 것은 다음과 같습니다.
그리고 밑에 Test Connection -> OK을 눌러 연결을 해줍시다.
위에서 했던 것과 매유 유사합니다.
를 설정해주어 연결합시다 !
RDS는 아래 세 가지 설정을 필수로 해줘야 합니다.
이제 이것들을 설정해봅시다.
파라미터 그룹에 들어와 파라미터 그룹 생성 버튼을 눌러줍니다.
이런 식으로 생성할 그룹의 이름과, 그에 대한 설명만 적어주고 생성을 눌러줍니다.
그럼 이렇게 목록에 뜹니다. 방금 생성한 그룹을 클릭해줍니다.
이러한 창이 뜰텐데, 검색창을 통해
utf8mb4는 이모지도 지원해줘 요즘 더 많이 사용되는 추세라고 합니다.
다음과 같이 변경해줍니다.
그 후 RDS 인스턴스로 이동하여 수정을 눌러줍니다.
다음과 같이 파라미터 그룹을 방금 설정한 것으로 바꿔줍니다.
그 다음으로 넘어가면 즉시 적용을 적용하여 DB 인스턴스 수정을 해줍시다.
이렇게 EC2 인스턴스와 RDS 인스턴스를 생성 및 설정하였고, 로컬 환경에서 RDS로 연결도 해보았습니다.
다음부터는 본격적으로 github action을 통해 CI/CD를 구축해봅시다.
$ sudo apt update
$ sudo apt install ruby-full
$ sudo apt install wget
$ cd /home/ubuntu
$ wget https://aws-codedeploy-ap-northeast-2.s3.ap-northeast-2.amazonaws.com/latest/install
$ chmod +x ./install
$ sudo ./install auto > /tmp/logfile
$ sudo service codedeploy-agent status
혹시 오류가 발생한다면 경로 문제 일 수 있습니다.
정상적으로 설치가 완료되면 위와 같은 화면이 뜹니다.
여태 했던 것과 마찬가지로 S3 페이지로 들어가서 버킷 만들기를 눌러주세요.
다음과 같이 진행해주세요.
AWS CodeDeploy를 생성하기 전에, IAM에서 역할을 만들어야 합니다.
IAM - 역할 - 역할 만들기를 눌러줍니다.
그리고 역할 생성을 눌러주세요.
이제 본격적으로 CodeDeploy를 생성해봅시다.
이제 배포 그룹을 만들어볼게요.
여기서 중요한 것은, 키 입니다.
EC2 인스턴스에서 태그를 생성한 것 기억나시나요? 그때의 태그를 넣어주면 됩니다.
하지만 이미 만들어주었기에, 우린 그냥 선택만 하면 됩니다!
여기까지 진행하시면 AWS의 IAM, S3, EC2, CodeDeploy 에 대한 설정이 모두 완료되었습니다.
이제 github action을 통해 CI/CD를 구축해볼게요.
우선 그 전에, github action에서 사용할 IAM 사용자를 추가하겠습니다.
여기서 중요한 것은, AWSCodeDeployFullAccess와 AmazonS3FullAccess도 함께 연결해주세요 !!
이렇게 생성이 완료되면,
위와 같이 액세스 키와 비밀 액세스 키가 주어집니다.
중요합니다!! 꼭 .csv 다운로드해서 로컬 pc에 값을 저장해주세요. 다시 확인 할 수 없어요.
그리고 이 액세스 키들을 가지고 github action에서 접근하는 데에 사용할 거에요.
이제 적용하려는 github의 repository로 이동해서
Settings - Secrets - Actions - New repository secret 버튼 클릭 후
액세스 키와 비밀 액세스 키를 위와 같은 이름으로 저장해주세요.
스펠링 안틀리게 주의해주세요 !
이제 github repository의 루트 디렉터리 위치에, 즉 최상단에 appspec.yml을 위치해주세요.
코드는 다음과 같습니다.
코드에 대한 설명은 주석처리 해놓았습니다.
귀찮더라도 가능한 처음부터 직접 타이핑해서 실수가 안나게 해주세요 ! (주석은 굳이 안적으셔도 됩니다.)
version: 0.0
os: linux
files:
- source: / # 인스턴스에 복사할 디렉터리 경로
destination: /home/ubuntu/app # 인스턴스에서 파일이 복사되는 위치
overwrite: yes # 복사할 위치에 파일이 있는 경우 대체
permissions:
- object: / # 권한이 지정되는 파일 or 디렉터리
pattern: "**" # 매칭되는 패턴에만 권한 부여
owner: ubuntu # object의 소유자
group: ubuntu # object의 그룹 이름
hooks:
AfterInstall: # CodeDeploy의 AfterInstall 단계에서 실행
- location: scripts/stop.sh # hooks에서 실행할 스크립트의 위치
timeout: 60 # 스크립트 실행에 허용되는 최대 시간, 넘으면 배포 실패
runas: ubuntu # 스크립트를 실행하는 사용자
ApplicationStart: # CodeDeploy의 ApplicationStart 단계에서 실행
- location: scripts/start.sh
timeout: 60
runas: ubuntu
방금 위에서의 scripts/stop.sh와 start.sh를 작성해보겠습니다.
github repo 맨 위에 scripts라는 폴더를 만들고, 그 안에 stop.sh와 start.sh를 작성해서 넣어주세요.
코드는 아래와 같습니다.
stop.sh
#!/usr/bin/env bash
PROJECT_ROOT="/home/ubuntu/app"
JAR_FILE="$PROJECT_ROOT/프로젝트 이름.jar" # 여기서 프로젝트 이름은 일단 원하는 프로젝트 이름으로 설정해주세요. 밑에서 추가 설명하겠습니다.
DEPLOY_LOG="$PROJECT_ROOT/deploy.log"
TIME_NOW=$(date +%c)
# 현재 구동 중인 애플리케이션 pid 확인
CURRENT_PID=$(pgrep -f $JAR_FILE)
# 프로세스가 켜져 있으면 종료
if [ -z $CURRENT_PID ]; then
echo "$TIME_NOW > 현재 실행중인 애플리케이션이 없습니다" >> $DEPLOY_LOG
else
echo "$TIME_NOW > 실행중인 $CURRENT_PID 애플리케이션 종료 " >> $DEPLOY_LOG
kill -15 $CURRENT_PID
fi
start.sh
#!/usr/bin/env bash
PROJECT_ROOT="/home/ubuntu/app"
JAR_FILE="$PROJECT_ROOT/프로젝트 이름.jar"
APP_LOG="$PROJECT_ROOT/application.log"
ERROR_LOG="$PROJECT_ROOT/error.log"
DEPLOY_LOG="$PROJECT_ROOT/deploy.log"
TIME_NOW=$(date +%c)
# build 파일 복사
echo "$TIME_NOW > $JAR_FILE 파일 복사" >> $DEPLOY_LOG
cp $PROJECT_ROOT/build/libs/*.jar $JAR_FILE
# jar 파일 실행
echo "$TIME_NOW > $JAR_FILE 파일 실행" >> $DEPLOY_LOG
nohup java -jar $JAR_FILE > $APP_LOG 2> $ERROR_LOG &
CURRENT_PID=$(pgrep -f $JAR_FILE)
echo "$TIME_NOW > 실행된 프로세스 아이디 $CURRENT_PID 입니다." >> $DEPLOY_LOG
여기서 제가 헤맨 경험이 있습니다. 바로 PROJECT_ROOT 떄문인데요.
ec2 인스턴스에 생성된 프로젝트의 경로가/home/ubuntu/app
이 아니라 더 들어갔어야 했는데
이 부분을 수정하지 않다보니 제대로 되지 않았습니다.
따라서 여러분들은 이 부분 잘 설정해주세요.
Spring Boot 2.5 이상부터는 프로젝트 빌드 시 일반 jar 파일과, -plain.jar
파일이 생성됩니다.
그래서 빌드 시 -plain.jar
는 생성되지 않도록 여러분 프로젝트의 build.gradle에 아래의 코드를 추가해주세요.
jar {
enabled = false
}
이제 진짜 마지막입니다.
기존에 잘 나와있는 workflow가 있지만, 가능한 실수를 줄이기 위해 처음부터 생성해보겠습니다.
위와 같이 선택 후 코드를 모두 지우고 아래의 코드를 위에서부터 하나씩 타이핑해볼게요.
이번에도 코드에 대한 설명은 주석으로 달아놓았습니다.
참고로, 실제 제 yml 파일을 그대로 가져와서
name: Java CI with Gradle & Deploy to EC2
# main 브런치에 push가 되면 아래의 flow가 실행됩니다.
on:
push:
branches: [ "main" ]
# flow에서 사용할 변수 같은 느낌입니다.
env:
AWS_REGION: AWS 지역
S3_BUCKET_NAME: AWS S3 버킷 이름
CODE_DEPLOY_APPLICATION_NAME: AWS CodeDeploy App 이름
CODE_DEPLOY_DEPLOYMENT_GROUP_NAME: AWS CodeDeploy 배포 그룹 명
working-directory: ./프로젝트 경로 # 이 코드는 제 프로젝트 경로를 맞춰주기 위해 적어주었습니다. 필요하시면 적으세요 !
permissions:
contents: read
jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
environment: production
# 아래의 flow들이 차례대로 실행됩니다.
steps:
# 1) 기본 체크아웃
- name: Checkout
uses: actions/checkout@v3
# 2) JDK 11 셋팅
- name: Set up JDK 11
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'
# 3) gradlew 권한 설정
- name: Grant execute permission for gradlew
run: chmod +x gradlew
working-directory: ${{ env.working-directory }}
# 4) gradle 빌드
- name: Build with Gradle
run: ./gradlew clean build
working-directory: ${{ env.working-directory }}
# AWS 인증
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
# AWS S3에 업로드
- name: Upload to AWS S3
run: |
aws deploy push \
--application-name ${{ env.CODE_DEPLOY_APPLICATION_NAME }} \
--ignore-hidden-files \
--s3-location s3://$S3_BUCKET_NAME/$GITHUB_SHA.zip \
--source .
# AWS EC2에 Deploy
- name: Deploy to AWS EC2 from S3
run: |
aws deploy create-deployment \
--application-name ${{ env.CODE_DEPLOY_APPLICATION_NAME }} \
--deployment-config-name CodeDeployDefault.AllAtOnce \
--deployment-group-name ${{ env.CODE_DEPLOY_DEPLOYMENT_GROUP_NAME }} \
--s3-location bucket=$S3_BUCKET_NAME,key=$GITHUB_SHA.zip,bundleType=zip
이제 main 브런치에 push 하면
다음과 같이 workflow가 동작하는 것을 확인할 수 있습니다. 여기서 파란불이 들어왔다면
위 사진처럼 배포 결과를 확인할 수 있습니다.
만약 실패했다면 해당 배포ID를 클릭해서 뜨는 페이지에서 맨 밑으로 내리면,
위와 같이 확인할 수 있습니다. 여기서 View events를 눌러서
에러 메시지도 확인하실 수 있고, 어떤 이벤트 단계에서 실패했는지도 확인할 수 있습니다.
저 같은 경우에는 기존 프로젝트 경로에
deploy.log
가 이미 존재한다고 하네요.
만약 모두 정상적으로 됐다면 ec2 인스턴스의 IP 주소로 접속해서 연결을 확인해보세요 !!
여기까지 오시느라 수고하셨습니다. 내용이 길다보니 마무리는 짧게 하겠습니다.
A-Z를 가능한 구체적으로 빠짐없이 보여드리고 싶었는데, 내용이 너무 길다보니 빠트린 내용이 있지 않을까 걱정되네요.
전체 과정이 매우 복잡하고 할 것도 많아서 경로 문제나 버전 충돌이나, 오타와 같은 버그들이 발생할 수 있습니다.
만약 문제가 발생한다면 언제든지 댓글 남겨주세요!!