CICD를 적용하며

byeol·2023년 10월 27일
0

필요성

이전 프로젝트에서 코드가 수정될 때마다 반복되는 배포의 과정(똑같은 행위)이 비효율적이라고 느꼈습니다.
특히 날씨 공공 API의 경우 TimeZone 설정으로 인해서 배포한 후에만 버그를 잡을 수 있어서 빈번한 배포가 계속 발생했었고 그 과정에서 많은 비효율성을 느꼈던 것 같습니다.

그리고 상대방과 협업을 하면서 좋은 품질의 코드가 유지되어져야 했고 내 코드가 추가됨으로써 상대방의 코드에 영향을 끼치는지 여부를 확인하는 과정이 필요했습니다. 모든 테스트 코드가 성공되었을 때 main branch로 push할 수 있다는 명확한 규칙이 필요하다는 것을 느꼈습니다.

그래서 CI/CD를 다음 프로젝트 때는 기필코 구현해보리라 다짐했고 이번 프로젝트에 적용하게 되었습니다.

CD에 대해서 더 찾아보면서
인력거 -> 자전거 -> 자동차 -> 비행기로 발전하기 위해서
사용자들에게 실시간으로 피드백을 받을 수 있는 환경을 구축하기 위함이라는 것을 알게 되었고 이번 프로젝트에서 스프린트가 끝날 때마다 사용자들을 대상으로 피드백을 받아 볼 계획입니다.

다시 CI/CD에 대해서 정의를 내리면 아래와 같습니다.

CI(Continuous Integration)

  • 지속적인 통합입니다.
  • 제가 생각하기에 CI는 하나의 어플리케이션에 대해서 각기 다른 부분을 개발자들이 개발할 때
  • 테스트를 자동화하여 성능이 좋은 코드를 유지할 수 있도록 하는 개념이라고 생각합니다.

CD(Continuous Deploy/Delivery)

  • 지속적인 배포입니다.
  • 제 생각으로 저희가 구현한 것은 Deploy입니다.
  • 지속적으로 클라우드 서버에 어플리케이션을 배포하여 사용자의 피드백을 실시간으로 받을 수 있습니다.

구조

왜 github action인가?

  • 개발자들이 직접 YAML을 만들어 Commit하는 것 만으로 간단하게 CI/CD Workflow를 사용할 수 있습니다.
  • Actions는 재사용이 가능한 형태로 만들어져 있어 Github Marketplace에 있는 Actions를 가져와서 복잡한 작업을 몇 줄의 YAML 작업만으로 간단하게 연동할 수 있다는 점이 매력적이었습니다.
  • 여기서 Action은 무엇일까요?
    • Github Actions에서 빈번하게 필요한 반복 단계를 재사용하기에 용이하도록 제공되는 일종의 작업 공유 메커니즘
    • actions/checkout@v4.1.1이 무엇인지 고민했는 Action을 사용하는 행위라고 볼 수 있습니다.

왜 CodeDeploy인가?

  • 다른 것들에 비해서 적용하는데 드는 비용(배우는 데 드는 비용)이 적다고 생각했습니다.
  • 사실 명확한 사유는 없으나
  • Amazon EC2 인스턴스의 경우, Auto Scaling과 통합 → 트래픽 급증으로 인해 Auto Scaling이 EC2 용량을 자동 조정하면 CodeDeploy는 알림을 받아 새 인스턴스에 자동으로 어플리케이션을 배포하고 Elastic Load Balancing 로드 밸런서에 새 인스턴스를 추가
  • 위 부분이 매력적으로 보였고 무료인 점, 기존 설정 코드를 재사용 할 수 있다는 점이 좋았습니다.

전체적인 폴더 구조


이제부터 CI,CD를 적용한 코드를 분석해 보겠습니다.

.github/workflows/cicd.yml 분석

📌 Github Actions의 Workflow를 명시하고 있는 문서로 이 문서를 읽고 행동합니다.

Workflow가 무엇일까요? Github Repository에 들어가는 작업 단위입니다.

(출처 : https://kotlinworld.com/384)

name: Build and Deploy to EC2

on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]

  workflow_dispatch:

env:
  BUCKET_NAME: team09-cicd-bucket
  PROJECT_NAME: studay
  DEPLOYMENT_GROUP_NAME: studay_cicd
  CODE_DEPLOY_APP_NAME: studay_cicd

jobs:
  # 작업의 이름
  build_and_test:
    # GitHub Actions 러너의 운영 체제
    runs-on: ubuntu-latest

    # 순차적으로 실행될 단계들을 정의하는 섹션
    steps:
      - name: Checkout code
        uses: actions/checkout@v4.1.1

      - name: Set up JDK 17
        uses: actions/setup-java@v3.13.0
        with:
          java-version: '17'
          distribution: 'corretto'

      - name: Grant execute permission for gradlew
        run: chmod +x ./gradlew #gradlew 실행

      - name: Build with gradle
        run: ./gradlew build

      - name: Make Zip File 
        run: zip -qq -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_PRIVATE_ACCESS_KEY }}
         aws-region: ap-northeast-1

      - name: Upload to S3
        run: aws s3 cp --region ap-northeast-1 ./$GITHUB_SHA.zip s3://$BUCKET_NAME/$PROJECT_NAME/$GITHUB_SHA.zip

      - name: Code Deploy To EC2 instance
        run: aws deploy create-deployment
         --application-name $CODE_DEPLOY_APP_NAME
         --deployment-config-name CodeDeployDefault.AllAtOnce
         --deployment-group-name $DEPLOYMENT_GROUP_NAME
         --s3-location bucket=$BUCKET_NAME,bundleType=zip,key=$PROJECT_NAME/$GITHUB_SHA.zip

PROJECT_NAME: studay

s3의 하위 폴더명으로 생성된다.

name: Make Zip File

이 코드 스니펫은 GitHub Actions의 워크플로우 파일에 속하는 부분으로 보입니다. 여기서 사용된 내용을 해석해보겠습니다:

  1. name: Make Zip File: 이 부분은 이 액션의 이름을 정의합니다. GitHub Actions 워크플로우에서 각 단계는 이름으로 식별됩니다.
  2. run: zip -qq -r ./$GITHUB_SHA.zip .: 이 부분은 실행될 명령을 정의합니다. 여기서 하는 일은 현재 디렉토리(.)에 있는 파일들을 $GITHUB_SHA.zip이라는 이름의 압축 파일로 만드는 것입니다. $GITHUB_SHA는 GitHub에서 제공하는 환경 변수로, 현재 커밋의 SHA 해시 값을 나타냅니다.
  3. shell: bash: 이 부분은 명령이 실행될 쉘(shell)을 지정합니다. 여기서는 Bash 쉘을 사용하도록 지정되어 있습니다.

이 코드는 주로 GitHub Actions을 사용하여 소스 코드나 다른 파일들을 압축 파일로 만들 때 사용될 것입니다. 이 경우, 워크플로우는 현재 코드가 포함된 커밋의 SHA 해시를 이름으로 하는 ZIP 파일을 생성합니다.

appspect.yml 분석

📌 인스턴스에 배포하기 위해 사용되는 스크립트
AppSpec 파일은 파일에 정의된 일련의 수명 주기 이벤트 후크로 각 배포를 관리하는 데 사용

version: 0.0
os: linux

files:
  - source: /
    destination: /home/ubuntu/studay

permissions:
  - object: /home/ubuntu/studay/
    owner: ubuntu
    group: ubuntu
hooks:
  BeforeInstall:
    - location: scripts/deploy.sh
      timeout: 300
      runas: root

  AfterInstall:
    - location: scripts/deploy.sh
      timeout: 60
      runas: ubuntu
  • files, permissions, hooks의 영역을 섹션이라고 합니다.

files

📌 배포 중 수정의 파일을 인스턴스의 위치로 복사하는 경우에만 필요

files:
  - source: /
    destination: /home/ubuntu/studay
  • source
    if (source == 파일명) { // 단순 파일명만 표기
      지정한 파일만 인스턴스에 복사
    } else if (source == directory) { // directory 표기
      directory 내의 모든 파일이 인스턴스에 복사
    } else if (source == "/") { // = 슬래시 하나인 경우
      수정된 버전의 모든 파일이 인스턴스에 복사
    }
    여기서 잠깐! 수정된 버전 = 그냥 배포될 파일을 이렇게 표현하는 것 같습니다.
  • destination
    • destination 명령은 인스턴스에서 파일이 복사되어야 하는 위치를 식별합니다.
    • /root/destination/directory(Linux, RHEL, Ubuntu) 또는 c:\destination\folder(Windows)와 같은 정규화된 경로여야 합니다.
  • 추가로 file_exists_behavior
    • 선택 사항이며, CodeDeploy가 배포 대상 위치에 이미 존재하지만 이전에 성공한 배포의 일부가 아닌 파일을 처리하는 방식을 지정합니다. 이 설정은 다음 값 중 하나일 수 있습니다.
      • DISALLOW: 배포가 실패합니다. 이는 옵션을 지정하지 않은 경우의 기본 동작입니다.
      • OVERWRITE: 현재 배포 중인 애플리케이션 수정 버전의 파일 버전이 인스턴스에 이미 있는 버전을 대체합니다.
      • RETATE: 인스턴스에 이미 있는 파일의 버전이 유지되고 새 배포의 일부로 사용됩니다
  • 예시
    files:
      - source: Config/config.txt
        destination: /webapps/Config
      - source: source
        destination: /webapps/myApp
    • s3에 있는 Config/config.txt를 인스턴스의 /webapps/Config/config.txt 경로에 복사
    • s3에 있는 source directory에 있는 파일을 인스턴스의 /webapps/myApp directory로 모두 복사

permissions

📌 ‘files’ 섹션에서 정의한 파일이 인스턴스에 복사된 후 해당 파일에 권한이 어떻게 적용되어야 하는지를 지정

permissions:
  - object: /home/ubuntu/studay/
    owner: ubuntu
    group: ubuntu

EC2/온프레미스 배포용으로만 ‘permissions’ 섹션을 사용한다.

AWS Lambda 또는 Amazon ECS 배포에는 resources 섹션이 사용

hooks

📌 EC2/온프레미스 배포에 대한 ‘hooks’ 섹션에는 배포 수명 주기 이벤트 후크를 하나 이상의 스크립트에 연결하는 매핑이 포함되어 있습니다.

  • AppSpec 파일의 ‘hooks’ 섹션 내용은 해당 배포의 컴퓨팅 플랫폼에 따라 다릅니다.
  • Lambda 또는 Amazon ECS 배포에 대한 ‘hooks’ 섹션은 배포 수명 주기 이벤트 중 실행하는 Lambda 확인 함수를 지정합니다.
  • 이벤트 후크가 없는 경우 해당 이벤트에 대해 작업이 실행되지 않습니다.
hooks:
  BeforeInstall:
    - location: scripts/deploy.sh
      timeout: 300
      runas: root

  AfterInstall:
    - location: scripts/deploy.sh
      timeout: 60
      runas: ubuntu

내부 요소

  • location
    • 이벤트가 실행되는 위치
    • root + location
  • timeout
    • 실행되는 이벤트가 timeout에 명시된 시간을 넘어갈 경우 실패
  • runas
    • 선택 사항이다.
    • 기본적으로 인스턴스에서 실행 중인 CodeDeploy 에이전트이다.

scripts/deploy.sh 분석

📌 appspec.yml로 인해 실행되는 shell script

#!/usr/bin/env bash

REPOSITORY=/home/ubuntu/studay
cd $REPOSITORY

APP_NAME=studay
JAR_NAME=$(ls $REPOSITORY/build/libs/ | grep 'SNAPSHOT.jar' | tail -n 1)
JAR_PATH=$REPOSITORY/build/libs/$JAR_NAME

CURRENT_PID=$(pgrep -f $APP_NAME)

if [ -z $CURRENT_PID ]
then
  echo "> 종료할 애플리케이션이 없습니다."
else
  echo "> kill -9 $CURRENT_PID"
  kill -15 $CURRENT_PID
  sleep 5
fi

echo "> Deploy - $JAR_PATH "
nohup java -jar $JAR_PATH > /dev/null 2> /dev/null < /dev/null &
  1. Shebang: #!/usr/bin/env bash는 이 스크립트가 Bash 쉘에서 실행됨을 나타냅니다.
  2. REPOSITORY=/home/ubuntu/studay: REPOSITORY 환경 변수에 리포지토리 경로(/home/ubuntu/studay)를 할당합니다.
  3. cd $REPOSITORY: 현재 작업 디렉토리를 REPOSITORY 경로로 변경합니다.
  4. APP_NAME=studay: 애플리케이션 이름을 APP_NAME 변수에 할당합니다.
  5. JAR_NAME=$(ls $REPOSITORY/build/libs/ | grep 'SNAPSHOT.jar' | tail -n 1):
    • ls $REPOSITORY/build/libs/ 명령어로 해당 디렉토리 내의 파일 목록을 가져옵니다.
    • grep 'SNAPSHOT.jar'는 파일 중에서 'SNAPSHOT.jar'이라는 문자열을 포함한 파일을 찾습니다.
    • tail -n 1로 최신 JAR 파일을 선택합니다. 이 파일명을 JAR_NAME 변수에 할당합니다.
  6. JAR_PATH=$REPOSITORY/build/libs/$JAR_NAME: 앞서 찾은 최신의 JAR 파일 이름을 기반으로 JAR 파일의 전체 경로를 JAR_PATH 변수에 할당합니다.
  7. CURRENT_PID=$(pgrep -f $APP_NAME): 실행 중인 프로세스 중에서 애플리케이션 이름에 해당하는 프로세스의 PID(Process ID)를 찾아 CURRENT_PID 변수에 할당합니다.
  8. 프로세스 종료 검사:
    • if [ -z $CURRENT_PID ]는 현재 실행 중인 프로세스가 없는 경우를 확인합니다.
    • 종료할 애플리케이션이 없다면 메시지를 출력합니다.
  9. 프로세스 종료 및 재시작:
    • 만약 현재 실행 중인 프로세스가 있다면, 해당 프로세스를 종료합니다.
    • echo "> kill -9 $CURRENT_PID"는 종료할 프로세스의 PID를 출력합니다.
    • kill -15 $CURRENT_PID로 해당 PID에 해당하는 프로세스를 강제로 종료합니다.
    • sleep 5로 5초 동안 대기합니다.
    • nohup java -jar $JAR_PATH > /dev/null 2> /dev/null < /dev/null &은 새로운 프로세스로 JAR 파일을 실행합니다. nohup은 세션 로그아웃 후에도 프로세스를 계속 실행하도록 합니다. > /dev/null 2> /dev/null < /dev/null은 표준 입력 및 출력을 비활성화하여 백그라운드에서 실행되도록 합니다.

이 스크립트는 주어진 경로에서 최신 SNAPSHOT JAR 파일을 찾아 실행 중인 프로세스를 종료하고, 새로운 JAR 파일로 애플리케이션을 실행시킵니다.

겪었던 이슈

자잘한 오류

  • appspect.yml 이름 잘못되어서 appspec.yml로 수정

굵직한 오류

The deployment failed because a specified file already exists at this location:

  • 원인 : 어떤 위치에 있는 파일이 이미 존재하고 있는 파일이기 때문
  • 해결책 2가지
    • 1) appspec.yml에서 beforeInstall 과정에서 수행할 스크립트를 추가한다. (이미 존재하고 있는 파일 삭제 후 진행)

      hooks:
        BeforeInstall:
          - location: scripts/deploy.sh
            timeout: 300
            runas: root
    • 2) 파일들을 OVERWRITE 한다.

      **file_exists_behavior : OVERWRITE**

출처

https://docs.aws.amazon.com/ko_kr/codedeploy/latest/userguide/reference-appspec-file-structure-files.html

https://goodgid.github.io/Github-Action-CI-CD-CodeDeploy-App-Spec-File/

이슈 해결 : https://kimtaehyun98.tistory.com/136

profile
꾸준하게 Ready, Set, Go!

0개의 댓글