[빙터뷰] CI/CD 파이프라인 구축

impala·2023년 6월 27일
0
post-thumbnail

프론트엔드 개발을 맡은 팀원의 요청으로 개발 중반부에 api서버를 미리 배포하게 되었다. 지금까지 프론트엔드는 api문서를 보고 개발했기 때문에 임시 데이터만으로 개발을 진행하고 있었다. 그러다보니 데이터를 등록하는 부분이나 수정할 때 화면의 변화가 잘 동작하는지 확인하는데 한계가 있어서 임시로 개발서버를 올려달라는 요청을 받았다.

지난번 프로젝트에서는 서버를 배포할 때 로컬에서 깃허브에 코드를 푸쉬하면 서버에서 수동으로 코드를 받아와 실행하는 식으로 배포가 이루어졌는데, 이 과정이 꽤 번거로웠던 기억이 있어서, 이번에는 배포과정을 자동화하여 개발에 조금 더 신경쓸 수 있도록 배포 파이프라인을 구축하였다.

CI/CD 파이프라인 구축을 위해 [스프링 부트와 AWS로 혼자 구현하는 웹 서비스]를 참고하였다.

CI/CD 파이프라인 설계

위 그림은 github action과 aws codedeploy를 활용한 전체적인 CI/CD 파이프라인을 보여준다. 전체적인 배포 순서는 다음과 같다.

  1. 로컬 개발환경에서 코드를 github로 push한다.
  2. github action에서 코드를 테스트한 뒤 빌드한다.
  3. github action에서 빌드 결과물을 압축하여 S3에 업로드하고 Codedeploy에 배포 요청을 보낸다.
  4. Codedeploy가 배포 요청을 받으면 S3에 업로드된 배포파일을 EC2인스턴스로 복사하고 압축을 해제한 뒤, 배포 스크립트를 실행한다.

구현

Github action

name: Spring Boot & Gradle CI/CD

on:
  push:
    branches:
      - main

permissions:
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    
    - name: Set up JDK 17
      uses: actions/setup-java@v3
      with:
        java-version: '17'
        distribution: 'adopt'
    
    - name: Change application.yml to deploy version
      run: |
        cd ./src/main/resources
        rm ./application.yml
        mv ./application-deploy.yml ./application.yml
        cat application.yml
          
    - name: Make application-secret.yml
      env:
        GOOGLE_OAUTH_CLIENT_ID: ${{ secrets.GOOGLE_OAUTH_CLIENT_ID }}
        GOOGLE_OAUTH_CLIENT_SECRET: ${{ secrets.GOOGLE_OAUTH_CLIENT_SECRET }}
        JWT_TOKEN_SECRET: ${{ secrets.JWT_TOKEN_SECRET }}
        AWS_S3_ACCESS_KEY: ${{ secrets.AWS_S3_ACCESS_KEY }}
        AWS_S3_KEY_SECRET: ${{ secrets.AWS_S3_KEY_SECRET }}
        AGORA_APP_ID: ${{ secrets.AGORA_APP_ID }}
        AGORA_APP_CERTIFICATE: ${{ secrets.AGORA_APP_CERTIFICATE }}
      run: |
        cd ./src/main/resources
        echo -e \
        "
        spring:
          security:
            oauth2:
              client:
                registration:
                  google:
                    client-id: $GOOGLE_OAUTH_CLIENT_ID
                    client-secret: $GOOGLE_OAUTH_CLIENT_SECRET
                    scope:
                      - profile
                      - email

        jwt:
          secret: $JWT_TOKEN_SECRET
          
        cloud:
          aws:
            credentials:
              accessKey: $AWS_S3_ACCESS_KEY
              secretKey: $AWS_S3_KEY_SECRET
        agora:
          appId: $AGORA_APP_ID
          appCertificate: $AGORA_APP_CERTIFICATE
        " > application-security.yml
        cat application-security.yml

    - name: Grant execute permission for gradlew
      run: chmod +x gradlew

    - name: Build with Gradle
      run: ./gradlew clean build
    
    - name: Make Directory for deliver
      run: mkdir deploy
    
    - name: Copy Jar
      run: cp ./build/libs/*.jar ./deploy/
      
    - name: Copy AppSpec
      run: cp ./appspec.yml ./deploy/
    
    - name: Copy deploy.sh
      run: cp ./scripts/* ./deploy/
    
    - name : Make zip file
      run: zip -r -qq -j ./vingterview-build.zip ./deploy
      
    - name: Deliver to AWS S3
      env:
        AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
        AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_ACCESS_KEY_SECRET }}
      run: |
        aws s3 cp \
        --region ap-northeast-2 \
        --acl private \
        ./vingterview-build.zip s3://deploy-vingterview/
    
    - name: Deploy
      env:
        AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
        AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_ACCESS_KEY_SECRET }}
      run: |
        aws deploy create-deployment \
        --application-name vingterview \
        --deployment-group-name vingterview-group \
        --file-exists-behavior OVERWRITE \
        --s3-location bucket=deploy-vingterview,bundleType=zip,key=vingterview-build.zip \
        --region ap-northeast-2

우리는 backend 브랜치를 통해 개발을 진행했기 때문에 backend 브랜치에서 어느정도 코드가 정리되면 main 브랜치로 pull request를 보내 merge하는 방식으로 깃허브를 관리하기로 했다. main브랜치에 push이벤트가 감지되면 job을 수행하도록 설정했다.

  1. 먼저 테스트를 위한 Runner를 초기화하기 위해 우분투 머신에 JDK17을 설치한다.

  2. 다음으로 서버의 설정파일을 배포버전으로 변경하고 각종 비밀 키값을 담은 application-security.yml파일을 작성한다. 이 파일은 공개된 레포지토리에 올라가면 키가 유출될 수 있으므로 서버에 직접 환경변수로 등록하거나 따로 관리해야 하는데, 전자는 추후에 서버를 증축하게 되면 각 서버마다 직접 환경변수를 설정해주어야 하는 불편함이 있기 때문에 github secret을 통해 비밀키값을 관리하고 배포 직전에 Runner에서 관련 설정파일을 직접 작성하도록 구현했다.

  3. security파일을 정상적으로 만들었으면 greadew에 권한을 주고 빌드를 수행한다. 이때 gradle은 모든 테스트코드를 통과하면 빌드를 진행하므로 테스트코드가 잘 작성되었다면 문제없이 빌드파일이 만들어진다.

  4. 이후 배포파일을 담을 디렉토리를 만들고 빌드 산출물인 jar파일과 codedeploy를 위한 appspec.yml파일, 자동 배포를 위한 deploy.sh파일을 디렉토리에 복사한 후 압축한다.

  5. 마지막으로 압축된 배포파일을 AWS S3에 업로드하고 Codedeploy에 배포요청을 보내 실제 서버에 코드를 배포한다.

Aws CodeDeploy

# appspec.yml

version: 0.0
os: linux
files:
  - source: /
    destination: /home/ubuntu/app/deploy

permissions:
  - object: /
    pattern: "**"
    owner: ubuntu
    group: ubuntu

hooks:
  ApplicationStart:
    - location: deploy.sh
      timeout: 60
      runas: ubuntu

appspec.yml에는 EC2에 설치된 Codedeploy 에이전트가 수행할 작업을 설정한다.

os와 files에서 배포환경과 배포파일의 위치를 설정하고 permissions를 통해 권한을 부여한다. hooks에서는 배포 라이프사이클에 맞춰 지정된 작업을 수행하도록 설정한다.

여기에서는 간단하게 ApplicationStart 단계에서 배포 스크립트인 deploy.sh를 실행하도록 설정했다.

# deploy.sh

#! /bin/bash
REPOSITORY=/home/ubuntu/app/deploy
PROJECT_NAME=vingterview

echo "> 현재 구동중인 어플리케이션 확인"
CURRENT_PID=$(pgrep -f "vingterview.*\.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

echo ">$JAR_NAME 실행"
nohup java -jar \
  $JAR_NAME > $REPOSITORY/nohup.out 2>&1 &

배포 스크립트에서는 현재 어플리케이션이 실행중이면 종료하고 새로운 어플리케이션을 데몬으로 실행한다. 이때 로그파일을 남기기 위해 nohup.out에 콘솔 출력을 기록한다.

이 외에도 CI/CD를 위해서는 AWS에서 여러 설정들이 필요한데, 그 부분은 책을 참고하여 진행했다. 간단하게 설명하자면

  • EC2에서 S3의 배포파일을 가져오기 위해 S3에 대한 접근권한을 가진 IAM Role을 EC2에 추가함
  • Codedeploy에 적절한 IAM Role을 추가함

0개의 댓글