우리는 지난 포스팅에서 Github Actions를 이용해 CI 환경을 구축하는 방법을 알아보았다.
우리는 이전에 EC2 인스턴스를 생성하고 스프링 부트 프로젝트를 띄울 때 scp 명령어를 이용해 로컬의 빌드 파일을 EC2 인스턴스로 복하한 후 ssh 로 접속해서 실행시켰다.
하지만 매 수정사항마다 이렇게 배포 하면 번거롭기 때문에 이러한 작업들을 자동화해보겠다.
1. 배포방법
우리는 main 브랜치로 수정사항을 푸시하면 EC2까지 배포되는 워크플로우를 만들어볼 것이다.
AWS에 배포를 하기 위해서는 소스코드를 압축하여 AWS 스토리지에 저장 후 서버에 전달하여 실행하는 방법을 사용한다.
AWS에서 공식적으로 안내하는 방법은 크게 두 가지가 있다.
- AWS S3 빌드파일 압축해서 업로드 -> AWS EC2 배포 (CodeDeploy 이용)
- AWS ECR에 도커 이미지 업로드 -> -> AWS ECS 배포 (Task Definition 이용)
우리는 EC2 인스턴스를 사용하기 때문에 첫 번째 방법을 사용해 배포할 것이다.
두 방법의 차이를 간단하게 알아보면 AWS ECR은 Docker Image를 저장하는 저장소이고 AWS EC도 일종의 도커 컨테이너 서비스이다.
AWS ECS는 미리 정의한 Task Definition을 기반으로 클러스터에 인스턴스를 생성하고 ECR에 저장된 도커 이미지를 배포하는 등 인스턴스를 관리하며 스케일 인/아웃을 지원한다.
어려 개의 서버 인스턴스를 관리하기 위해선 위 방법이 더 편리할 수 있으나 우리는 EC2 인스턴스를 미리 생성하였고, 이에 배포를 목적으로 하기 때문에 1번 방법을 사용할 것이다.
배포의 과정을 그림으로 설명한 것이다.
간단하게 요약하자면 다음과 같다.
- Github Actions에서 코드 빌드
- AWS 인증
- 코드 압축하여 AWS S3에 업로드
- AWS CodeDeploy 실행 후 S3에 있는 코드를 EC2에 배포
Github 프로젝트 코드를 압축하여 S3에 업로드한 후 EC2에서 끌어다 쓰는 것이 핵심이며 CodeDeploy는 이를 보조해 주기 위한 역할이다.
2. EC2 설정
2.1. Tag 추가
2.1.1. EC2 인스턴스 태그 관리
EC2 인스턴스 > 작업 > 인스턴스 설정 > 태그관리를 선택한다.
2.1.2. 태그 추가
원하는 키를 입력하고 저장한다.
2.1.3. 태그 확인
인스턴스 정보에서 태그가 잘 저장된 것을 볼 수 있다.
2.2 IAM 역할 추가
EC2 인스턴스에서 S3에 업로드된 파일에 접근할 수 있도록 권한을 추가하기 위해 IAM 역할을 추가해줘야 한다.
2.1.1. IAM 역할 관리 메뉴 이동
먼저 다음과 같이 iam을 검색해 iam 메뉴로 이동한 뒤 좌측 카테고리 중 역할을 선택해 역할을 만든다.
2.2.2. 엔티티 선택
먼저 엔티티 유형을 선택한다. EC2에 대한 작업을 수행할 것이기 때문에 EC2를 선택해준다.
2.2.3. S3 접근 권한 추가
EC2 인스턴스가 S3에 접근할 수 있도록 'AmazonS3 FullAccess' 권한을 추가한다.
안 보인다면 검색을 통해 찾아보도록 하자.
2.2.4. 이름 설정
역할의 이름을 지정한 뒤 생성을 완료한다.
추가한 권한도 여기서 확인할 수 있다.
2.2.5. EC2 인스턴스에서 IAM 연결
다시 인스턴스로 돌아와 '작업 > 보안 > IAM 역할 수정'을 선택한다.
여기서 방금 생성한 IAM 역할을 선택한 뒤 역할을 업데이트한다.
2.3 CodeDeploy Agent 설치
이제 EC2 인스턴스에 CodeDeploy Agent를 설치해야 한다.
ssh로 인스턴스에 접근한 뒤 다음 명령어들로 CodeDeploy Agent를 설치한다.
필자의 EC2 환경은 Ubuntu로 다른 환경을 사용하거나 명령어에 대한 자세한 정보가 궁금하다면 공식문서를 참고하자.
$ sudo apt update
$ sudo apt install ruby-full
$ sudo apt install wget
$ wget https://aws-codedeploy-ap-northeast-2.s3.ap-northeast-2.amazonaws.com/latest/install
$ chmod +x ./install
$ sudo ./install auto
$ aws s3 ls s3://aws-codedeploy-ap-northeast-2/releases/ | grep '\.rpm$'
$ sudo ./install auto -v releases/codedeploy-agent-1.6.0-49.noarch.rpm
$ sudo service codedeploy-agent start
$ sudo service codedeploy-agent status
명령어를 모두 따라 하면 다음과 같이 CodeDeploy가 프로세스를 할당받은 것을 확인할 수 있다.
3. AWS S3 생성
AWS S3를 만들어 보자.
S3 버킷은 이미지 또는 zip 파일을 저장하기 위한 스토리지로, 빌드한 프로젝트 코드를 압축하여 S3에 저장한 후 EC2 서버에서 S3에 접근해 해당 파일을 가져오기 위해 사용한다.
3.1. S3 메뉴에서 버킷 생성
이전까지와 마찬가지로 검색을 통해 S3 메뉴로 접근한 뒤 버킷을 생성한다.
3.1.1. 일반 구성 및 객체 소유권 설정
원하는 이름을 입력한 뒤 지역을 선택한다.
이때 위에서 CodeDeploy Agent를 설치할 때 'ap-northeast-2'로 설치해 주었기 때문에 'ap-northeast-2'로 골라준다.
ACL은 권장값을 사용한다.
3.1.2. 액세스, 버킷 버전, 기본 암호화
모두 기본값으로 설정한 뒤 버킷을 생성한다.
4. CodeDeploy 생성
배포를 도와주는 CodeDeploy를 생성해 보자.
4.1. CodeDeploy 전용 IAM 만들기
CodeDeploy를 만들기 전 Codedeploy용 IAM 역할을 먼저 만들어야 한다.
4.1.1. IAM 역할 생성
IAM의 역할 탭에서 새로운 역할을 만들어 준다.
4.1.2. 엔티티 선택
엔티티를 선택해준다. CodeDeploy를 검색하면 손쉽게 찾을 수 있다.
4.1.3. 권한 추가
CodeDeploy에 대한 기본 권한이 추가되어 있다. 이 설정 그대로 넘어가자.
4.1.4. 이름 지정
원하는 이름을 설정하고 역할을 생성하자.
4.2. CodeDeploy 애플리케이션 생성
CodeDeploy 애플리케이션을 생성해보자.
4.2.1. CodeDeploy 애플리케이션 메뉴 이동
검색을 통해 CodeDeploy 애플리케이션 탭으로 이동한 뒤 애플리케이션 생성 버튼을 누른다.
4.2.2. 애플리케이션 생성
원하는 이름과 컴퓨팅 플랫폼을 선택한 뒤 애플리에키션을 생성한다.
4.3. 배포 그룹 생성
방금 생성한 애플리케이션에서 배포 그룹 생성을 클릭해 배포 그룹을 만들 수 있다.
4.3.1. 배포 그룹 이름, 역할, 유형
원하는 이름을 입력한 뒤 역할을 입력한다.
역할은 아까 만든 IAM 역할을 선택하면 된다.
배포 유형은 기본 설정을 사용하자.
4.3.2. 환경 구성
어떤 인스턴스에서 동작할지 선택한다.
이전에 인스턴스에서 태그를 추가하였기 때문에 추가한 태그를 선택한다.
4.3.3. 기타 설정
나머지 설정은 지금 당장 중요한 설정은 아니니 다음과 같이 설정하고 배포 그룹을 생성하자.
5. Githhub Actions에서 사용할 IAM 사용자 추가
Github Actions에서 AWS에 접근하기 위해 권한을 설정해준다.
지금 가지는 IAM 역할만 추가하였지만 이번엔 사용자를 추가해 보자.
5.1. IAM 사용자 메뉴 이동
IAM 사용자 메뉴에 들어와 사용자를 추가한다.
5.2. IAM 사용자 생성
5.2.1. 사용자 정보 입력
원하는 사용자 이름을 입력한 뒤 다음으로 넘어간다.
5.2.2. 권한 설정
필요한 권한 정책을 추가하자. 필요한 권한은 다음 2개이다.
- AWSCodeDeployFullAccess
- AmazonS3 FullAccess
5.2.3. 사용자 생성 완료
올바르게 입력되었는지 확인 뒤 이상이 없다면 사용자를 생성하자.
5.3. ACCESS KEY 생성
5.3.1. 보안 자격 증명 탭 이동
'생성한 사용자 > 보안 자격 증명 탭 > 액세스키 > 액세스 키 만들기'를 통해 access key를 생성할 수 있다.
5.3.2 액세스 키 모범 사례 및 대안 확인
액세스 키를 사용하는 모범 사례와 대안을 확인한다.
우리는 AWS 외부의 Github Actions에서 사용하기 때문에 외부 애플리케이션에 대한 설명을 잘 읽어준다.
5.3.3. 태그 이름 설정
태그 이름을 입력한다.
5.3.4. access key 확인
액세스 키를 확인한다.
'액세스 키 ID'와 '비밀 액세스 키'가 존재하고, 이 두 개의 키 값을 이용해 IAM 권한을 획득할 수 있다.
이 화면을 넘어가면 다시 확인할 수 없기 때문에 따로 저장해 두자.
5.4. Github Repository의 Secrets 추가
방금 만든 액세스 키를 Github Repositoy의 Secrets에 추가해 준다.
'Repository > Setting > Secrets and Variables > Actions'에서 확인한 두 키 값을 적절한 이름으로 저장한다.
이곳에 저장한다 해도 값을 확인할 수 없기 때문에 필요한 경우 따로 저장해 두자.
6. AppSpec 파일 작성
지금까지 EC2, S3, CodeDeploy 총 세 개의 AWS 서비스를 만들었습니다.
이제 CodeDeploy에서 배포를 위해 참조할 AppSpec 파일을 작성하자.
AppSpec 파일을 통해 프로젝트의 어떤 파일을 EC2의 어떤 경로로 복사할지, 배포 프로세스 이후에 수행할 스크립트도 지정해 자동으로 서버를 띄울 수 있다.
AppSpec 파일은 build.gradle과 같은 위치인 루트 디렉터리에 위치해야 한다.
version: 0.0
os: linux
files:
- source: /
destination: /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.yml 파일은 다음과 같다.
각각이 어떤 역할을 하는지 간단하게만 알아보고 넘어가자.
- files : 배포 파일에 대한 설정
- source : 인스턴스에 복사할 디렉터리 경로
- destination : 인스턴스에서 파일이 복사되는 위치
- overwriter : 복사할 위치에 파일이 있는 경우 대체
- permissions : 복사한 파일에 대한 권한 설정
- object : 권한이 지정되는 파일 또는 디렉터리
- pattern (optional) : 매칭되는 패턴에만 권한 부여
- owner (optional) : object의 소유자
- group (optional) : object의 그룹 이름
- hooks : 배포 이후에 수행할 스크립트를 지정한다. 위 코드에서는 파일 설치 후 AfterIstall에서 기존 실행 중이던 애플리케이션을 종료시키고, ApplicationStart에서 새로운 애플리케이션을 실행한다.
- location : hooks에서 실행할 스크립트 위치
- timeout (optional) : 스크립트 실행에 허용되는 최대 시간, 넘으면 배포 실패
- runas (optional) : 스크립트를 실행하는 사용자
더 자세한 내용이 필요하면 공식문서를 참고하자.
7. 배포 스크립트 작성
위에서 stop.sh 와 start.sh를 설정하였기 때문에 이에 맞는 스크립트 파일을 작성하자.
7.1. stop.sh
#!/usr/bin/env bash
PROJECT_ROOT="/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
애플리케이션이 실행 중이라면 종료하는 스크립트이다.
7.2. start.sh
#!/usr/bin/env bash
PROJECT_ROOT="/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
애플리케이션을 실행하는 스크립트이다.
7.3. build.gradle 수정
위 스크립트에서 /build/lbs/*. jar 파일을 $JAR_FILE 파일로 복사한다.
스프링부트 버전 2.5부터는 일반 jar 파일과 -plain.jar 파일을 하나 더 만들기 때문에 이 파일이 생성되지 않도록 build.gradle에 다음 내용을 추가해주자.
jar {
enabled = false
}
8. Github Actions Workflow 작성
이제 사전 작업은 모두 완료되었고 Github Actions 워크플로우만 작성하면 된다.
8.1. Simple Workflow 선택
이전 Github Actions로 CI를 구축할 때는 기본적으로 제공되는 gradle 샘플을 수정하여 사용했지만, 배포 플로우는 거의 다 수정해야 하므로 simple workflow를 선택한다.
8.2. deploy.yml 작성
name: Deploy to Amazon EC2
on:
push:
branches:
- main
# 본인이 설정한 값을 여기서 채워넣습니다.
# 리전, 버킷 이름, CodeDeploy 앱 이름, CodeDeploy 배포 그룹 이름
env:
AWS_REGION: ap-northeast-2
S3_BUCKET_NAME: github-actions-s3-bucket-for-test
CODE_DEPLOY_APPLICATION_NAME: codedeploy-application
CODE_DEPLOY_DEPLOYMENT_GROUP_NAME: codedeploy-deployment-group
permissions:
contents: read
jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
environment: production
steps:
# 기본 체크아웃
- name: Checkout
uses: actions/checkout@v3
# JDK 11 세팅
- name: Set up JDK 11
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '11'
# Gradle build (Test 제외)
- name: Build with Gradle
uses: gradle/gradle-build-action@0d13054264b0bb894ded474f08ebb30921341cee
with:
arguments: clean build -x test
# 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 }}
# 빌드 결과물을 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 .
# 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
다음과 같이 deploy.yml 파일을 작성해준다.
전체적인 흐름은 다음과 같다.
- 프로젝트 빌드
- AWS 인증
- 빌드된. zip 파일을 AWS S3에 업로드
- S3 버킷에 있는 파일을 대상으로 CodeDeploy
이제 코드를 수정하고 main 브랜치에 push 하면 자동으로 배포가 되는 것을 볼 수 있다.
9. 배포 결과 확인
9.1. Gihub Actions에서 확인
먼저 방금 정의한 Github Actions의 Deploy.yml 워크플로우가 잘 수행되는지 확인해보자.
main 브랜치에 push후 깃헙 레포지토리의 Actions 탭에서 워크플로우의 진행 상황과 결과를 확인할 수 있다.
Github Actions CI 편에서 와 마찬가지로 결과를 초록/빨간색으로 나타내어주며 실패할 경우 로그도 확인할 수 있다.
9.2. AWS Codedeploy에서 확인
다음은 Codedeploy가 정상적으로 작동되는지 확인해보자.
AWS Codedeploy의 배포 탭에서 방금 push 한 작업물의 배포 결과를 확인할 수 있다.
배포에 실패했다면 '배포 ID > 배포 수명 주기 이벤트 > Vies events'를 통해 어느 작업에서 실패했는지 확인해 보자.
9.3. ec2 인스턴스에서 확인
마지막으로 ec2 인스턴스에서 프로젝트가 잘 동작하는지 확인하면 된다.
'/ubuntu/app' 경로에 프로젝트가 잘 복사되었다. 이후 'ps -ef | grep java' 명령어를 통해 프로젝트가 인스턴스에 정상적으로 떠있는 것을 볼 수 있다.
curl 명령어를 통해 api를 호출해도 잘 동작한다.