최근 진행하는 프로젝트가 어느정도 개발이 완료되서 AWS의 EC2를 이용해 배포를 진행하였다.

하지만 서버를 배포한 후에 프론트와 통신을 시작한 후에는 develop 브랜치에서 수행한 커밋들을 거의 바로바로 release브랜치가 pull 해야하는 상황이 되었고

release 브랜치가 바뀔 때마다 서버를 수동으로 재배포 해야했다.

EC2에 접속해서 작동중인 서버를 멈추고 다시 재가동 시키는 일련의 반복작업들이 귀찮아 지기 시작하였고 Github Action를 이용해서 이러한 반복작업들을

자동으로 수행하게끔 진행하였다.

물론 젠킨스라는 툴도 있지만 프로젝트 규모에 비해 오버스펙인 감이 있었고, release브랜치에 일어나는 event중심으로한 작업이 필요했으며,

설정또한 비교적 쉬운  Github Actions이 적당해 보였다.

 

진행하고자하는 프로젝트의 최종 배포 구조는 다음과 같다.

그림을 보면서 대략적인 배포흐름을 얘기하자면 먼저 Github의 release브랜치에서 push 또는 merge 이벤트가 발생하면,

github Actions가 작동하면서 Github에 있는 프로젝트를 build하여 jar파일을 생성한다.

이때 Github Actions에서 생성한 jar파일을 EC2로 직접 보내는 방법이 없기 때문에 AWS 저장소인 S3를 이용한다.

S3에 저장한 jar파일을 EC2로 가져오기 위해서는 CodeDeploy와 CodeDeployAgent가 필요하다.

Github Actions에서 AWS의 CodeDeploy에 요청을 보내면 EC2에 설치한 CodeDeployAgent를 통해 S3에 있는 jar파일을 EC2로 가져온다.

 

먼저 Github Actions 설정방법이다.

Github Actions를 수행할 브랜치를 default 브랜치로 설정한 후 action 탭에서 workflow를 생성하면 자동으로 .github/workflows 경로에 main.yml파일이 생성된다.

해당 main.yml파일의 초기 설정은 다음과 같이 하였다.

name: CI

on:
  push:
    branches: [ release ]
  pull_request:
    branches: [ release ]
  workflow_dispatch:
  
env:
  S3_BUCKET_NAME: fis-police-back-githubaction
  PROJECT_NAME: github-action

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Set up JDK 11
        uses: actions/setup-java@v1
        with:
          java-version: 11

      - name: Grant execute permission for gradlew
        run: chmod +x gradlew
        working-directory: ./fis_police_server
        shell: bash
        
      - name: Build with Gradle
        run: ./gradlew bootJar
        working-directory: ./fis_police_server
        shell: bash
  • name : workflow의 이름을 나타낸다.
  • on : 어떤 이벤트가 일어났을때 Github Actions가 작동할지 정하는 부분이다. 해당 프로젝트에서는 release 브랜치에 push 또는 pull_request가 발생하거나, 수동으로 작동시킬때(workflow_dispatch) 실행시키라고 설정하였다.
  • env: 현재는 쓰이지 않지만 나중에 S3의 버킷 이름과 프로젝트 이름을 사용할때 환경변수를 정의하여 사용하기 위해 설정하였다.
  • jobs, steps : 작업단위로 jobs안에 여러개의 steps를 둘수 있다.
  • runs-on : 해당 workflow를 어떤 환경에서 실행할 것인지 지정한다.
  • checkout : Github의 workflow를 실행할때 준비 동작으로 Github workspace에서 내 저장소로 이동한다고 생각하면 된다.

다음 작업들은 JDK를 setup하고 gradlew의 실행권한을 준 뒤 bootjar하는 과정이다.

이때 bootjar대신 build를 사용할 경우 test과정을 거치기 때문에 test가 실패할 경우 build가 실패할 수 있다.

working-directory는 작업을 실행할 경로를 지정해 주는 것인데,

깃허브에 저장된 프로젝트 구조상 gradlew이 있는 directory를 따로 지정해 주어야 했기 때문에 설정을 해주었다.

일단 main.yml파일을 이정도로 작성하고 github에 push 하면 github Actions가 작동하는것을 actions 탭에서 확인할 수 있다.

 

다음으로 S3에 jar파일을 업로드 하는 과정이다.

먼저 AWS console에서 S3 버킷을 생성한다. 이때 IAM를 사용하여 AWS의 권한을 따로 설정할거기 때문에 '모든 퍼블릭 액세스 차단'를 체크한다.

Github Actions가 현재 생성한 S3에 접근하기 위해 IAM사용자를 추가한다.

이때 중요한것은 Github Actions가 AWS에 접근하여 S3 버킷에 파일을 업로드 할때 액세스 키 ID와 비밀 액세스 키를 통해

접근권한을 받아올 것이기 때문에 프로그래밍 방식 액세스를 체크해야 한다.

부여할 권한은 AmazonS3FullAccess 와 AWSCodeDeployFullAccess이다.

생성을 완료한 후에 보여지는 액세스 키 ID와 비밀 액세스 키는 잘 보관하였다가 나중에 리포지토리 setting에서 secret를 통해 등록해 놓을 것이다.

S3버킷 생성과 IAM 사용자 추가 설정을 마쳤으면 GithubActions를 제어하기 위한 workflows의 main.yml파일을 다음과 같이 수정한다.

name: CI

on:
  push:
    branches: [ release ]
  pull_request:
    branches: [ release ]
  workflow_dispatch:
  
env:
  S3_BUCKET_NAME: fis-police-back-githubaction
  PROJECT_NAME: github-action

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Set up JDK 11
        uses: actions/setup-java@v1
        with:
          java-version: 11

      - name: Grant execute permission for gradlew
        run: chmod +x gradlew
        working-directory: ./fis_police_server
        shell: bash
        
      - name: Build with Gradle
        run: ./gradlew bootJar
        working-directory: ./fis_police_server
        shell: bash
        
      - name: Make zip file
        run: zip -r ./$GITHUB_SHA.zip .
        shell: bash
        
      - 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: ${{ secrets.AWS_REGION }}
          
      - name: Upload to s3
        run: aws s3 cp --region us-east-1 ./$GITHUB_SHA.zip s3://$S3_BUCKET_NAME/$PROJECT_NAME/$GITHUB_SHA.zip

Make zip file에서는 빌드한 파일을 압축하는 작업을 실행한다. S3 버킷에 파일을 올리기 위해서는 해당 작업이 선행되어야 한다.

이때 $GITHUB_SHA는 커밋의 해쉬값으로 해당 작업에서 workflow가 진행하는 커밋의 해쉬값을 파일명으로 하는 압축파일을 만든다는것을 의미한다.

다음으로 Configure AWS credentials는 IAM에서 추가한 사용자의 액세스 키 정보를 통해 S3에 접근권한을 받아오기 위한 작업이다.

이때 ${{SECRETS.~}}는 github의 해당 리포지토리의 setting에서 Secrets설정한 값을 읽어 온다.

따라서 해당 작업을 위해서는 github에 IAM 사용자 추가에서 나온 액세스 키 정보들을 등록해야한다.

다음 설정들을 모두 끝낸후 Github Actions를 돌리면 S3 버킷에 zip파일이 담긴것을 AWS console를 통해 확인할 수 있다.

 

다음으로 CodeDeploy 설정을 해야한다.

CodeDeploy를 작동시키기 위해서는 먼저 Github Actions이 참조하는 workflow의 main.yml파일에 CodeDeploy를 통해 배포하기 위한 명령어를 추가하고,

프로젝트 최상단에 AppSpec.yml파일을 추가한다.

AppSpec.yml은 각 배포단계에서 어떤 스크립트를 실행시킬지 적어놓은 명세서라고 할 수 있다.

GithubActions는 main.yml에 추가한 명령어를 통해 AWS의 CodeDeploy에 배포 요청을 보내고

CodeDeploy는 EC2에 설치되어있는 CodeDeployAgent에게 배포 명령을 내린다. 

 배포명령을 받은 Agent들은 깃허브에 저장되있는 프로젝트 전체를 서버에 내려받고 appspec.yml파일을 읽어 알맞은 스크립트를 실행시켜 배포를 진행한다.

CodeDeployAgent는 배포 성공 여부를 CodeDeploy에 알려주기 때문에 AWS console의 CodeDeploy에서 배포 성공여부를 확인할 수 있다.

 

이제 본격적으로 CodeDeploy설정을 해보겠다.

먼저 EC2에 CodeDeployAgent를 설치하여한다. EC2에 자바를 설치되어있다고 가정하고 다음 명령어들을 치면 된다.

# 패키지 매니저 업데이트, ruby설치
sudo apt-get update
sudo apt-get install ruby
sudo apt-get install wget

# 만약 서울리전에 있는 CodeDeploy 리소스 키트 파일을 다운받고 싶다면
# wget https://aws-codedeploy-ap-northeast-2.s3.ap-northeast-2.amazonaws.com/latest/install
cd /home/ec2-user
wget https://bucket-name.s3.region-identifier.amazonaws.com/latest/install

# 해당 파일에 실행 권한 부여
chmod +x ./install

# 설치 진행 및 Agnet 상태확인
sudo ./install auto
sudo service codedeploy-agent status

공식문서에서는 CodeDeployAgent를 설치하는 과정에서 apt-get대신 yum를 사용하는데 이것은 Linux가 어떤 계열인지에 따라 달라진다.

현재 사용중인 Linux가 레드헷 계열이라면 yum을 사용하고 우분투, 데비안계열이라면 apt-get를 사용해야한다.

나는 우분투 계열의 Linux를 사용중이기 때문에 yum대신 apt-get를 사용하였다.

 

이제는 EC2가 S3와 CodeDeploy에 대한 접근권한을 얻기 위해 IAM역할을 부여해야 한다.

위에서 GithubActions가 AWS의 S3에 접근하기위해 IAM사용자를 추가했었다. 

IAM에대해 부연설명 하자면, GithubActions같이 외부에서 AWS의 접근 권한을 얻기 위해서는 IAM사용자를 추가해야하고,

EC2처럼 AWS에서 제공하는 서비스가 AWS내부 서비스에 접근하기 위해서는 IAM역할을 추가해줘야 한다.

역할 만들기에 들어가 사용 사례에서 EC2를 선택하고, AmazonS3FullAccess와 AWSCodeDeployFullAccess 정책을 추가한다.

역할을 성공적으로 생성했다면 EC2대시보드에 들어가 해당 EC2에 생성한 IAM역할을 부여한다.

 

다음으로 CodeDeploy에도 IAM역할을 생성해서 부여해야 한다.

IAM 역할만들기에서 CodeDeploy를 선택하고 사용사례에서 CodeDeploy를 선택하여 넘어간다.

 

이제 CodeDeploy 애플리케이션을 생성해야한다. 

AWS console에서 CodeDeploy-애플리케이션-애플리케이션생성 에서 EC2를 선택하고 진행한다.

배포그룹 생성에서는 서비스역할에는 방금전 생성한 CodeDeploy용 IAM역할을 입력하고 배포유형은 현재 위치를 선택한다.

환경구성에서는 Amazon EC2 인스턴스를 선택하고 EC2 인스턴스의 태그를 알맞게 입력한다.

배포설정에서는 EC2가 한대이기 때문에 CodeDeployDefault.AllAtOnce를 선택한다.

로드밸런서는 없기 때문에 로드 밸런싱 활성화 체크는 해체하고 배포그룹을 생성한다.

프로젝트 최상단에 appspec.yml를 놓아야하는데 형식은 아래 코드와 같다.

# appspec.yml

version: 0.0
os: linux
files:
  - source: /
    destination: /home/ec2-user/github-action/
    overwrite: yes
    
permission:
  - object: /
    pattern: "**"
    owner: ec2-user
    group: ec2-user

hooks:
  ApplicationStart:
    - location: fis_police_server/scripts/deploy.sh
      timeout: 180
      runs: ec2-user

files.destination은 S3에 있는 zip파일을 EC2의 어느경로로 가져올지를 나타낸다.

permission은 인스턴스에 복사된 파일에 특수권한(있는 경우)이 어떻게 적용되어야 하는지를 지정하는 것이다.

ApplicationStart는 ApplicationStart단계에서 해당 쉘 스크립트를 실행하겠다는 내용이다.

이 경우 deploy.sh의 내용은 다음과 같다.

#!/bin/bash
BUILD_JAR=$(ls /home/ec2-user/github-action/fis_police_server/build/libs/*.jar)
JAR_NAME=$(basename $BUILD_JAR)
echo "> build 파일명: $JAR_NAME" >> /home/ec2-user/github-action/deploy.log

echo "> 현재 실행중인 애플리케이션 pid 확인" >> /home/ec2-user/github-action/deploy.log
CURRENT_PID=$(pgrep -f $JAR_NAME)

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

echo "> build 파일 복사" >> /home/ec2-user/github-action/deploy.log
DEPLOY_PATH=/home/ec2-user/github-action/
cp $BUILD_JAR $DEPLOY_PATH

DEPLOY_JAR=$DEPLOY_PATH$JAR_NAME
echo "> DEPLOY_JAR 배포" >> /home/ec2-user/github-action/deploy.log
nohup java -jar $DEPLOY_JAR >> /home/ec2-user/deploy.log 2>/home/ec2-user/github-action/deploy_err.log &

이때 주의할점은 현재 가동중인 애플리케이션을 kill한 후에 build한 jar파일을 $DEPLOY_PATH로 복사해야 한다는것이다.

만약 jar파일을 먼저 복사한 후 가동중인 애플리케이션을 kill하게 되면 다음과 같은 에러가 간헐적으로 터질 수 있다.

Failed to stop bean 'webServerGracefulShutdown'

해당 에러는 jar가 실행중일때 해당 jar파일을 바꾸게 될경우 jar내부 클래스 참조가 엉켜서 생기는 에러이다.

따라서 jar파일을 실행할 경로로 복사해오기 전에 꼭 현재 실행중인 jar를 먼저 kill한 후 복사해오자.

참조 : https://stackoverflow.com/questions/65090165/graceful-shutdown-fails

 

CodeDeploy가 작동하도록 workflow의 main.yml에 명령어를 추가한 후의 main.yml의 내용은 다음과 같다.

name: CI

on:
  push:
    branches: [ release ]
  pull_request:
    branches: [ release ]
  workflow_dispatch:
  
env:
  S3_BUCKET_NAME: fis-police-back-githubaction
  PROJECT_NAME: github-action

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Set up JDK 11
        uses: actions/setup-java@v1
        with:
          java-version: 11

      - name: Grant execute permission for gradlew
        run: chmod +x gradlew
        working-directory: ./fis_police_server
        shell: bash
        
      - name: Build with Gradle
        run: ./gradlew bootJar
        working-directory: ./fis_police_server
        shell: bash
        
      - name: Make zip file
        run: zip -r ./$GITHUB_SHA.zip .
        shell: bash
        
      - 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: ${{ secrets.AWS_REGION }}
          
      - name: Upload to s3
        run: aws s3 cp --region us-east-1 ./$GITHUB_SHA.zip s3://$S3_BUCKET_NAME/$PROJECT_NAME/$GITHUB_SHA.zip

      - name: Code Deploy
        run: aws deploy create-deployment --application-name fis-police-back-githubaction --deployment-config-name CodeDeployDefault.AllAtOnce --deployment-group-name release --s3-location bucket=$S3_BUCKET_NAME,bundleType=zip,key=$PROJECT_NAME/$GITHUB_SHA.zip

위의 코드가 main.yml의 최종형태이다.

  • application-name은 CodeDeploy 애플리케이션의 이름을 지정한다.
  • deployment-config-name은 배포그룹 설정에서 선택했던 배포 방식을 지정한다.
  • deployment-group-name은 배포 그룹의 이름이다.
  • s3-location은 jar를 S3에서 가지고 오기 위해 차례로 bucket이름, 파일 타입, 파일 경로를 입력한다.

마지막으로 CodeDeploy Agent를 시작한 후에 IAM Role를 EC2에 부여했기 때문에 Agent가 IAM Role을 가지고 있지 않아 배포가 실패할 것이다.

따라서 다음 명령어로 CodeDeployAgent를 재가동 시켜줘야 한다.

sudo service codedeploy-agent restart

위에 내용들을 정확하게 수행하고 필요한 yml파일들을 정상적으로 작성해 release브랜치에 push했다면,

이제 release브랜치에 push또는 pull_request이벤트가 일어났을때 해당 내용으로 자동 배포가 이뤄지게 된다.

 

 

참고: https://wbluke.tistory.com/40?category=418851

+ Recent posts