Git Action과 AWS CodeDeploy로 SpringBoot 배포
목적
Git Action과 AWS CodeDeploy 스택이 필요해서 직접 사용해보려고 합니다.
Git Action으로도 CI/CD를 모두 진행할 수 있는데 왜 그렇게 하지 않는지도 생각해보았습니다.
제가 찾은 Git Action 배포 방식은 워크플로우에서 서버에 접속하여 수동으로 배포하는 방법 뿐 입니다.
그런데 워크플로우에서 실행시킨 프로세스는 워크플로우 종료와 함께 모두 종료시키는 것이 워크플로우의 원칙입니다.
실제로 Git Action에서 nohup으로 프로세스를 실행시키는 명령을 주더라도 워크플로우가 종료될 때 프로세스도 함께 종료됩니다.
임의로 뭔가를 건드려서 종료시키지 않을 수도 있지만 그러라고 만든 게.. 아니니까 다른 방법을 쓰는 것이 맞다고 생각합니다.
제가 Git Action을 사용하는 환경을 구성한다면 Git Action으로 테스트 환경과 빌드까지만 하고 배포는 AWS CodeDeploy를 사용할 것 같습니다.
다만 서버에 파일을 전달하는 것까지가 목표라면 거기까진 Git Action으로 구성할 것 같습니다.
EC2에 직접 빌드하기
Git Action을 사용하기 전 EC2 위에서 깃 레포를 클론하여 직접 빌드하고 실행해보았습니다.
직접 실행해보고 자동화 코드를 짜는 것이 틀릴 확률이 적어져 이 방법을 선호하는 편입니다.
EC2를 생성했습니다.
- Ubuntu
- EIP 할당
- t3.micro
인바운드에 http, https, ssh(내 IP에서만 접근 가능), 8080포트를 열어주었습니다.
아웃바운드는 전체 대역으로 두었습니다.
sudo apt-get update
sudo apt-get install -y openjdk-11-jdk
# 설치 확인
java -version
git clone https://github.com/Jeromy0515/cloud-skills-sample-spring-boot-app.git
gradlew clean build
# cloud-skills-sample-spring-boot-app내에서 빌드하면 build/libs/내부에 jar파일 생성
nohup java -jar build/libs/cloud_skills_spring_boot_exam-0.0.1-SNAPSHOT.jar &
사용할 레포지토리에 Java 11 버전으로 명시되어 있습니다.
필요한 툴들을 설치하고 빌드, 실행합니다.
nohup 명령은 서버와 SSH 연결이 끊기더라도 프로그램이 계속 실행됩니다.
& 명령은 프로그램이 백엔드로 실행됩니다.
ubuntu@ip-000-00-0-000:~/cloud-skills-sample-spring-boot-app$ cat nohup.out
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.6.3)
2023-11-24 05:34:28.702 INFO 1620 --- [ main] s.c.CloudSkillsSpringBootExamApplication : Starting CloudSkillsSpringBootExamApplication using Java 11.0.20.1 on ip-000-00-0-000 with PID 1620 (/home/ubuntu/cloud-skills-sample-spring-boot-app/build/libs/cloud_skills_spring_boot_exam-0.0.1-SNAPSHOT.jar started by ubuntu in /home/ubuntu/cloud-skills-sample-spring-boot-app)
nohup 명령 실행 시 nohup.out 파일에 로그가 기록됩니다.
정상 작동하는 것을 확인했습니다.
Git Action 빌드 + AWS CodeDeploy 배포
샘플로 사용할 레포지토리를 클론해 사용했습니다.
https://github.com/su-mmer/cloud-skills-sample-spring-boot-app
레포지토리에서 Action Workflow를 생성합니다.
EC2
AWS IAM instance profile 생성
EC2에서 S3에 접근해 빌드 파일을 가져와야 하므로 S3FullAccess policy를 가진 IAM 역할을 생성하고 EC2에 연결합니다.
CodeDeploy에서 태그로 배포할 EC2를 선택하기 때문에 EC2에 태그를 추가합니다.
AWS CodeDeploy Agent 설치
sudo apt update
sudo apt install ruby-full -y
sudo apt install wget
cd /home/ubuntu
wget https://aws-codedeploy-ap-northeast-2.s3.ap-northeast-2.amazonaws.com/latest/install
sudo chmod +x ./install
sudo ./install auto
sudo service codedeploy-agent status
EC2에 접속하여 CodeDeploy 에이전트를 설치합니다.
agent의 동작을 확인합니다.
S3
빌드한 파일을 저장할 버킷을 생성합니다.
AWS CodeDeploy
IAM Role 생성
CodeDeploy에 연결할 역할을 생성합니다.
Tag를 이용해 배포할 EC2를 정하므로 Tagging을 읽는 권한이 포함됩니다.
AWS CodeDeploy app 생성
앱을 만듭니다.
그룹도 만들어줍니다.
배포할 EC2 인스턴스 세트를 그룹이라고 합니다.
위에서 생성한 CodeDeployRole policy를 가진 IAM Role을 연결해줍니다.
배포할 인스턴스에 추가한 태그로 설정해줍니다.
AllAtOnce를 선택하면 배포 구성의 성공 조건은 최소 0개의 호스트가 동작중일 때 입니다.
GitHub에서 사용할 IAM 사용자 생성
사용자를 생성합니다.
외부에 유출되면 안 되므로 레포지토리에 Secret을 등록합니다.
파일 구성
appspec.yaml
version: 0.0
os: linux
files:
- source: /
destination: /home/ubuntu/app
overwrite: yes
permissions:
- object: /
pattern: "**"
owner: ubuntu
group: ubuntu
hooks:
AfterInstall:
- location: scripts/stop.sh
timeout: 60
runas: ubuntu
ApplicationStart:
- location: scripts/start.sh
timeout: 60
runas: ubuntu
루트 디렉터리 위치에 appspec.yaml을 작성합니다. 파일 이름을 반드시 똑같이 지켜줘야 인식됩니다.
- files
- source: 인스턴스에 복사할 파일 또는 디렉터리 경로
- 디렉터리의 경우 내부 파일만 복사됨
- ‘/’의 경우 파일과 디렉터리 모두 복사됨
- ‘/’의 경우 appspec.yml 포함하여 복사됨
- destination: 인스턴스에서 파일이 복사 되는 위치
- overwrite: 복사할 위치에 파일이 있는 경우 대체 여부
- source: 인스턴스에 복사할 파일 또는 디렉터리 경로
- permissions
- object: 권한을 지정할 파일 또는 디렉터리
- pattern(optional): 매칭되는 패턴에만 권한 부여
- 지정하지 않거나 “**”을 사용하면 권한이 type에 따라 일치하는 모든 파일 또는 디렉터리에 적용됨
- owner(optional): object의 소유자 이름
- group(optional): object의 그룹 이름
- hooks: 파일 설치 후 기존 애플리케이션 종료(AfterInstall), 새 애플리케이션 실행(ApplicationStart)
- location: hooks에서 실행할 스크립트의 위치
- timeout(optional): 스크립트 실행 최대 허용 시간, 넘으면 배포 실패 처리
- runas(optional): 스크립트를 실행하는 사용자
스크립트 파일
scripts 폴더를 만들고 하위에 start.sh와 stop.sh 스크립트 파일을 두었습니다.
#!/usr/bin/env bash
PROJECT_ROOT="/home/ubuntu/app"
JAR_FILE="$PROJECT_ROOT/spring-webapp.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
stop.sh
- 실행 중인 애플리케이션이 있을 경우 종료
- date +%c: 날짜와 시간 출력
- -z $CURRENT_PID: 문자열 길이가 0이면 참
- kill -15: 프로세스 정상 종료
#!/usr/bin/env bash
PROJECT_ROOT="/home/ubuntu/app"
JAR_FILE="$PROJECT_ROOT/spring-webapp.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
start.sh
새 애플리케이션 시작
빌드를 위한 GitActions workflow/build.yaml 파일
name: Deploy to Amazon EC2
on:
push:
branches:
- main
env:
AWS_REGION: ap-northeast-2
S3_BUCKET_NAME: my-cicd-test-bucket-hh
CODE_DEPLOY_APPLICATION_NAME: my-codedeploy-app
CODE_DEPLOY_DEPLOYMENT_GROUP_NAME: my-codedeploy-deployment-group
permissions:
contents: read
jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
environment: production # 작업이 참조하는 환경 정의, 메인페이지에 표시됨
steps:
# (1) 기본 체크아웃
- name: Checkout
uses: actions/checkout@v3
# (2) JDK 11 세팅
- name: Set up JDK 11
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '11'
# (3) Gradle build (Test 제외)
- name: Build with Gradle
uses: gradle/gradle-build-action@0d13054264b0bb894ded474f08ebb30921341cee
with:
arguments: clean build -x test
# (4) AWS 인증 (IAM 사용자 Access Key, Secret Key 활용)
- 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 }}
# (5) 빌드 결과물을 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 .
# (6) S3 버킷에 있는 파일을 대상으로 CodeDeploy 실행
- 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
workflows에 파일을 저장하면 자동으로 액션이 실행되며 빌드가 되는 것을 확인할 수 있습니다.
- push
- application-name: CodeDeploy 애플리케이션 이름
- ignore-hidden-files: 숨김 파일은 빼고 업로드
- s3-location: 업로드할 S3 경로, S3 버킷 이름과 개체 키를 모두 지정해주어야 함
- source: S3에 업로드할 파일들과 AppSpec파일의 위치
- create-deployment
- application-name: CodeDeploy 애플리케이션 이름
- deployment-config-name: 배포 구성, 기본값으로 CodeDeployDefault.AllAtOnce, 최소 정상 호스트값 0
- deployment-group-name: CodeDeploy 배포 그룹 이름
- s3-location bucket=$S3_BUCKET_NAME,key=$GITHUB_SHA.zip,bundleType=zip
- bucket: S3 bucket 이름
- key: S3 객체 키 이름
- bundleType: S3에 저장할 때 포맷
빌드에 성공했습니다.
인스턴스에 접속해서 확인해보면 spring-webapp.jar 파일이 제대로 생성되었습니다.
실행 중인 프로세스 목록에 spring-webapp.jar 파일이 있습니다.
배포된 앱에 접속해보면 제대로 작동합니다.
CodeDeploy 배포 로그 입니다.
CodeDeploy Hooks 로그입니다.
다음엔 테라폼으로 만들어보겠습니다.
참고
블로그
https://bcp0109.tistory.com/356
https://bcp0109.tistory.com/363
샘플 프로젝트
https://github.com/Jeromy0515/cloud-skills-sample-spring-boot-app