본문 바로가기
Book/스프링 부트와 AWS 웹 서비스

[배포 자동화] Github Actions & S3 & CodeDeploy

by 달의 조각 2023. 2. 26.
이 글은 이동욱 님의 스프링 부트와 AWS로 혼자 구현하는 웹 서비스 책을 읽으며 정리한 글입니다.

 

Github Actions 설정
Github Actions - AWS S3 연동
Github Actions - AWS S3 - CodeDeploy 연동
배포 자동화 구성

 

 

  • CI(Continuous Integration - 지속적 통합): 코드 버전 관리를 하는 VCS 시스템(Git, SVN 등)에 PUSH가 되면 자동으로 테스트와 빌드가 수행되어 안정적인 배포 파일을 만드는 과정
  • CD(Continuous Deployment - 지속적 배포): 빌드된 결과를 자동으로 운영 서버에 무중단 배포까지 진행되는 과정

  여러 개발자가 협업해서 만든 코드를 병합하는 과정을 거치는 이전 방식은 번거롭고 효율이 좋지 않았다. 이후 CI/CD 환경이 개발되면서 개발자들이 보다 개발에만 집중을 할 수 있게 되었다.

책애서 나오는 Travis CI는 깃허브에서 제공하는 무료 CI 서비스이다. 유사 서비스로는 젠킨스가 있는데, 이는 설치형이기 때문에 이를 위한 EC2 인스턴스가 하나 더 필요하다. Travis 유료화로 인해 Github Action을 통해 실습을 진행했다.

 

🍌 Github Actions 설정

.github/workflows 경로에 main.yml 파일이 생성된다

name: Java CI with Gradle # Workflow 이름

on: # Workflow를 자동으로 트리거 하기 위한 이벤트
  release:
    types: [push] # push를 하면 자동으로 실행
  push:
    branches: [master] # master 브랜치에서 동작

jobs: # 병렬로 실행되는 작업 단위, 여러 step으로 이루어짐
  build:

    runs-on: ubuntu-latest # 스크립트를 작동할 OS
    permissions:
      contents: read
      packages: write

    steps:
    - uses: actions/checkout@v3 # 재사용 가능한 워크플로 파일의 위치 및 버전
    - name: Set up JDK 11
      uses: actions/setup-java@v3
      with:
        java-version: '11'
        distribution: 'temurin'
    
    # ./gradlw 실행 권한 부여
    - name: Grant execute permission for gradlew
      run: chmod +x ./gradlew
      shell: bash

    # 프로젝트 build
    - name: Build with Gradle
      run: ./gradlew clean build -x test
      shell: bash

위와 같이 코드를 작성하고 commit 및 push 하면 Actions 탭에서 아래와 같이 진행도를 확인할 수 있다.

 

🍌 Github Actions와 AWS S3 연동하기

  AWS S3는 파일 서버이다. 이미지 파일을 비롯한 정적 파일이나 배포 파일들을 관리하는 등의 기능을 지원한다. 보통 이미지 업로드를 구현한다면 S3를 이용하는 경우가 많다.

Github Actions와 S3를 연동해서 Jar 파일을 전달하도록 하고, 실제 배포는 AWS CodeDeploy를 이용한다. CodeDeploy는 저장 기능이 없으므로 Github Actions가 빌드한 결과를 S3에 저장해서 CodeDeploy가 가져갈 수 있도록 하는 것이다.

CodeDeploy가 깃허브 코드를 가져와서 빌드와 배포를 모두 하도록 할 수도 있지만 이렇게 구성할 경우 빌드 없이 배포만 필요할 때 대응하기가 어려우며 확장성이 떨어진다. 빌드와 배포가 분리되어 있으면 예전에 빌드되어 만들어진 Jar를 재사용하면 된다.

AWS Key 발급

일반적으로 AWS 서비스에 외부 서비스가 접근할 수 있으므로 접근 가능하도록 하는 Key를 생성해야 한다. AWS에서의 인증 기능은 IAM(Identity and Access) 서비스가 담당한다. Github Actions가 S3와 CodeDeploy에 접근할 수 있도록 해 보자!

  • AWS → IAM → 사용자 → 사용자 생성
  • 사용자 이름: Apupu
  • 기존 정책 직접 연결: AmazonS3FullAccess, AWSCodeDeployFullAccess 체크 (실제로는 각각 분리해서 권한을 만들기도 한다.)
  • 태그: Name, Apupu-githubactions-deploy
  • 생성한 후에 '보안 자격 증명 → 액세스 키'에서 키를 생성한다.

 

Github Actions에 Key 등록

  • Settings → Security → Actions → New repository secret
  • 액세스 키와 비밀 액세스 키를 각각 AWS_ACCESS_KEY와 AWS_SECRET_ACCESS_KEY라는 환경 변수로 저장
  • main.yml에서 사용할 수 있다.

 

S3 버킷 생성

퍼블릭 액세스를 모두 차단해야 한다. 이 설정을 열어 두면 누구나 Jar 파일을 내려받을 수 있어서 코드나 설정 값, 주요 키 값들이 탈취될 수 있다. 퍼블릭이 아니더라도 IAM 사용자로 발급받은 키를 사용하면 접근할 수 있다.

버킷 이름은 배포할 Zip 파일이 모여 있는 장소임을 의미하도록 짓는다

 

S3로 배포 파일 전달하기

Github Actions에서 빌드 하여 만든 Jar 파일을 S3에 올릴 수 있도록 main.yml에 아래 코드를 추가한다.

name: Java CI with Gradle

on:
  ...
    
env:
  S3_BUCKET_NAME: apupu-build

jobs:
  build:

    ...
    
    # 프로젝트 압축
    - name: Make zip file
      run: zip -r ./practice-deploy.zip .
      shell: bash
    
    # AWS 권한 확인
    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v1
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: ap-northeast-2
    
    # 압축한 프로젝트를 S3로 전송
    - name: Upload to S3
      run: aws s3 cp --region ap-northeast-2 ./practice-deploy.zip s3://$S3_BUCKET_NAME/practice-deploy.zip

위의 코드를 깃허브에 commit 및 push 하면 Github Actions에서 빌드를 진행하고 스크립트 프로세스에 따라 S3 버킷에 Jar 파일을 업로드 한다.

 

🍌 Github Actions와 AWS S3, CodeDeploy 연동

배포 대상인 EC2가 CodeDeploy를 연동받을 수 있도록 IAM 역할 추가

  • IAM의 사용자: AWS 서비스 외에 사용할 수 있는 권한
  • IAM의 역할: AWS 서비스에만 할당할 수 있는 권한

이 권한은 EC2에서 사용할 것이므로 역할로 생성한다!
EC2 인스턴스에 역할을 선택하고 재부팅한다

 

CodeDeploy 에이전트 설치

EC2가 CodeDeploy의 요청을 받을 수 있도록 한다.

aws s3 cp s3://aws-codedeploy-ap-northeast-2/latest/install . --region ap-northeast-2

위의 명령을 내렸을 때 에러가 발생하면 루비라는 언어가 설치되지 않아서이므로 아래 명령을 통해 루비를 설치한다.

sudo yum install ruby

install 파일에 실행 권한을 추가하고, 설치를 진행한다.

chmod +x ./install && sudo ./install auto

설치가 완료되면 아래 명령어로 Agent가 정상적으로 실행되고 있는지 검사해 본다.

sudo service codedeploy-agent status

 

CodeDeploy를 위한 권한 생성

반대로 CodeDeploy가 EC2에 접근하려면 마찬가지로 권한이 필요하다.

 

CodeDeploy 생성

AWS의 배포 삼형제에는 Code Commit, Code Build, CodeDeploy가 있다.

  • Code Commit: 깃허브와 같은 코드 저장소 역할, 거의 사용되지 않는다.
  • Code Build: 빌드용 서비스로, 멀티 모듈을 배포할 때 사용되지만 규모가 있는 서비스에서는 대부분 젠킨스/팀시티 등을 이용한다.
  • CodeDeploy: 대체제가 없으며, 오토 스케일링 그룹 배포, 블루 그린 배포, 롤링 배포, EC2 단독 배포 등을 지원한다.

서비스 역할: 좀 전에 생성한 CodeDeploy용 IAM 역할 선택 / 배포 유형: 배포할 서비스가 2개 이상이라면 블루/그린 선택
배포 구성: 한 번 배포할 때 몇 대의 서버에 배포할 것인가? = 두 대 이상이라면 비율을 어떻게 나눌지 선택

 

Github Actions와 AWS S3, CodeDeploy 연동

S3에서 넘겨 줄 zip 파일을 저장할 데릭토리를 생성한다. Github Actions의 빌드가 끝나면 S3에 zip가 파일이 전송되고, 이 zip 파일은 아래 주소로 복사되어 압축을 푼다.

mkdir ~/app/step2 && ~/app/step2/zip

appspec.yml 파일을 만들어서 AWS CodeDeploy 설정을 한다.

# appspec.yml
version: 0.0 # CodeDeploy 버전
os: linux
files:
  - source: / # CodeDeploy에서 전달해 준 파일 중 destination으로 이동시킬 대상 지정: 루트 경로(전체 파일)
    destination: /home/ec2-user/app/step2/zip/ # source에서 지정한 파일을 받을 위치
    overwrite: yes

main.yml에 CodeDeploy 내용을 추가한다.

name: Java CI with Gradle

...
    
env:
  S3_BUCKET_NAME: apupu-build
  CODE_DEPLOY_APP_NAME: Apupu
  CODE_DEPLOY_GROUP_NAME: Apupu-group

jobs:
  build:

    ...

    # 배포 그룹으로 애플리케이션 전송
    - name: Code Deploy
      run: aws deploy create-deployment --application-name $CODE_DEPLOY_APP_NAME --deployment-config-name CodeDeployDefault.AllAtOnce --deployment-group-name $CODE_DEPLOY_GROUP_NAME --s3-location bucket=$S3_BUCKET_NAME,bundleType=zip,key=$PROJECT_NAME.zip

위의 코드를 commit 및 push 하면 CodeDeploy를 통해 배포가 수행된다.

배포 성공!

 

🍌 배포 자동화 구성

연동까지 완료했으니 이제 Jar를 배포해서 실행해 보자!

srcipts/deploy.sh

step2 환경에서 실행될 스크립트이다. dploy.sh와 유사하다. git pull을 통해 직접 빌드했던 부분이 제거되었고, Jar 실행 단계에서 몇 가지 코드가 추가됐다.

#!/bin/bash

REPOSITORY=/home/ec2-user/app/step2
PROJECT_NAME=Apupu

echo "> Build 파일 복사"

cp $REPOSITORY/zip/*.jar $REPOSITORY/

echo "> 현재 구동중인 애플리케이션 pid 확인"

# 수행 중인 애플리케이션 프로세스 ID => 구동 중이면 종료하기 위함
CURRENT_PID=$(pgrep -fl $PROJECT_NAME | grep jar | awk '{print $1}')

echo "현재 구동중인 어플리케이션 pid: $CURRENT_PID"

if [ -z "$CURRENT_PID" ]; then
    echo "> 현재 구동중인 애플리케이션이 없으므로 종료하지 않습니다."
else
    echo "> kill -15 $CURRENT_PID"
    kill -15 $CURRENT_PID
    sleep 5
fi

echo "> 새 어플리케이션 배포"

JAR_NAME=$(ls -tr $REPOSITORY/*.jar | tail -n 1)

echo "> JAR Name: $JAR_NAME"

echo "> $JAR_NAME 에 실행권한 추가"

chmod +x $JAR_NAME # Jar 파일은 실행 권한이 없는 상태이므로 권한 부여

echo "> $JAR_NAME 실행"

nohup java -jar \
    -Dspring.config.location=classpath:/application.properties,classpath:/application-real.properties,/home/ec2-user/app/application-oauth.properties,/home/ec2-user/app/application-real-db.properties \
    -Dspring.profiles.active=real \
    $JAR_NAME > $REPOSITORY/nohup.out 2>&1 &
# nohup 실행 시 CodeDeploy는 무한 대기한다. 이를 해결하기 위해 nohup.out 파일을 표준 입출력용으로 별도로 사용한다.
# 이렇게 하지 않으면 nohup.out 파일이 생성되지 않고 CodeDeploy 로그에 표준 입출력이 출력된다.

 

Github Actions 설정 파일 main.yml 수정

현재 프로젝트의 모든 파일을 압축하는데, 실제로 필요한 파일은 Jar, appspec.yml, 배포를 위한 스크립트이다. 나머지는 포함하지 않도록 수정한다. before-deploy는 zip 파일에 포함시킬 파일을 저장하고, zip -r 명령어로 전체 파일을 압축한다.

name: Java CI with Gradle

...

jobs:
  build:

    ...

    # 실제 필요한 파일만 압축
    - name: Generate deployment package
      run: |
        mkdir -p before-deploy
        cp scripts/*.sh before-deploy/
        cp appspec.yml before-deploy/
        cp build/libs/*.jar before-deploy/
        cd before-deploy && zip -r before-deploy *
        cd ../ && mkdir -p deploy
        mv before-deploy/before-deploy.zip deploy/$PROJECT_NAME.zip
      shell: bash
    
    ...

 

CodeDeploy 설정 파일 appspec.yml 수정

permissions를 통해 CodeDeploy에서 EC2 서버로 넘겨 준 파일들을 모두 ec2-user 권한을 갖도록 하고, hooks를 통해 CodeDeploy 배포 단계에서 실행할 명령어를 지정한다.

version: 0.0 # CodeDeploy 버전
os: linux
files:
  - source: / # CodeDeploy에서 전달해 준 파일 중 destination으로 이동시킬 대상 지정: 루트 경로(전체 파일)
    destination: /home/ec2-user/app/step3/zip/ # source에서 지정한 파일을 받을 위치
    overwrite: yes

permissions: # CodeDeploy에서 EC2 서버로 넘겨 준 파일들은 모두 ec2-user 권한을 가진다
  - object: /
    pattern: "**"
    owner: ec2-user
    group: ec2-user

hooks: # CodeDeploy 배포 단계에서 실행할 명령어
  AfterInstall:
    - location: stop.sh # 엔진엑스와 연결되어 있지 않은 스프링 부트를 종료
      timeout: 60 # 스크립트 실행 60초 이상 수행되면 실패 (무한 대기할 수는 없으니 설정)
      runas: ec2-user

 

배포 자동화 테스트

실제로 push를 하면 배포가 자동으로 되는지 확인한다. Upupu로 설정되었던 문구를 Apupu로 변경했다. 여기서 배포가 자동으로 되지 않는 문제를 겪었었는데, 해당 문제는 이 글(배포 자동화가 되지 않는 문제)에서 정리했다.

push를 하니 자동으로 변경사항이 배포되었다!

 

CodeDeploy 로그 확인

cd opt/codedeploy-agent/deployment-root

위의 경로로 들어가서 파일 목록을 확인하면 아래와 같다. 첫 번째 라인의 디렉토리명은 CodeDeploy ID이다. 이 디렉토리로 들어가면 배포한 단위별로 배포 파일이 있다.

deployment.logs 파일은 CodeDeploy 로그 파일이다. CodeDeploy로 이루어지는 배포 내용 중 표준 입/출력 내용은 모두 여기에 담기며, 작성한 echo 내용도 모두 표기된다.

drwxr-xr-x 7 root root 101 Mar  2 12:12 759aa3b8-☆-☆-☆-☆
drwxr-xr-x 2 root root 247 Mar  2 12:12 deployment-instructions
drwxr-xr-x 2 root root  46 Mar  1 13:12 deployment-logs
drwxr-xr-x 2 root root   6 Mar  2 12:12 ongoing-deployment

댓글