[CI/CD] Github Action과 AWS Code Deploy를 활용한 CI/CD 구현 - 3

chrkb1569·2023년 12월 13일
0

개인 프로젝트

목록 보기
20/28

지난 시간에는 Github Action, AWS S3를 통하여 CI 환경을 구성해보았습니다.
즉, 우리가 커밋한 프로젝트가 머지되는 경우, 자동으로 프로젝트가 jar 파일로 변경된 뒤, 해당 파일이 AWS S3에 저장되는 과정까지는 구현한 것입니다.

그런데, codeDeploy를 통하여 우리가 원하는 작업을 수행하기 위해서는 AppSpec.yml파일을 프로젝트에 추가하고, 각 서비스가 다른 서비스에 접근할 수 있도록 AWS IAM을 통하여 권한을 설정해야한다는 것을 알아보았습니다.

오늘은 이러한 설정들을 해줌으로써 codeDeploy를 활용한 CD환경을 구축해보겠습니다.

AppSpec.yml 파일 추가

가장 먼저 codeDeploy가 수행할 작업들을 지정하는 AppSpec.yml 파일부터 프로젝트에 추가해야하는데, 파일을 작성하기 전, 간단하게 codeDeploy의 내부 동작들을 살펴보면,

[Reference : https://docs.aws.amazon.com/codedeploy/latest/userguide/reference-appspec-file-structure-hooks.html#reference-appspec-file-structure-hooks-list]

다음의 과정을 통하여 codeDeploy가 동작하게됩니다.

과정 하나하나를 '훅(hook)'이라고하는데, 각 훅마다 어떠한 동작을 수행하는지 살펴보면 다음과 같습니다.

Start

EC2 내부에 위치한 codeDeploy Agent를 실행하는 작업으로, 배포를 시작하는 단계입니다.

ApplicationStop

이전에 실행되고 있던 애플리케이션을 종료하는 단계입니다.

DownloadBundle

새롭게 실행할 리소스를 가져오는 단계입니다. 저희는 S3에 필요한 파일들을 저장하기 때문에, S3로부터 jar파일과 AppSpec.yml 파일을 가져올 것 같습니다.

BeforeInstall

본격적으로 설치 작업에 들어가기 전, 이전 버전의 설치 정보들을 저장하고 백업하는 과정입니다.

Install

S3로부터 가져온 리소스의 압축을 해제한 뒤, yml 파일에 명시된 작업들을 수행합니다.

AfterInstall

프로그램이 시작되기 전, 프로그램의 구성을 변경하는 단계입니다.

ApplicationStart

새로운 버전의 프로그램을 실행하는 단계입니다.

ValidateService

배포가 성공적으로 완료되었는지 확인하는 단계입니다.

End

성공적으로 배포가 완료되었을경우, 이를 알리고 배포를 종료합니다.

AppSpec.yml 파일 작성을 통하여 특정 훅에서 어떻게 동작할 것인지 설정할 수 있으나, 회색으로 표기되어 있는 Start, DownloadBundle, Install, End 훅들은 별도로 수정할 수 없다는 특징을 가지고 있습니다.

[Reference : https://docs.aws.amazon.com/codedeploy/latest/userguide/reference-appspec-file-structure-hooks.html#reference-appspec-file-structure-hooks-list]

이는 공식 문서에서 확인할 수 있는 yml 파일의 예시이며, 'hooks'라는 옵션 하위에 훅들의 이름을 기입해주고, 이를 통하여 어떠한 작업을 수행할지 설정할 수 있습니다.

https://docs.aws.amazon.com/codedeploy/latest/userguide/application-revisions-appspec-file.html#add-appspec-file-server

다음은 AWS에서 제공하는 공식문서인데, 이를 참고하여 yml 파일을 작성하였습니다.

version: 0.0
os: linux

# 파일을 EC2 인스턴스 내부 어디로 복사할지를 지정
# source는 현재 위치
# destination은 EC2 인스턴스 내부 디렉토리
# 사용하지 않는다면 해당 옵션이 오류를 야기할 수 있기 때문에 없애는 것을 지향한다고함
files:
  - source: /
    destination: /home/ubuntu/chrkb1569/
    overwrite: yes

## 우리가 지정한 경로(/home/ubuntu/chrkb1569/)에
## 실행하고자하는 jar 파일이 존재하는 경우, 오류가 발생할 수 있음
## 이러한 오류를 방지하기 위하여 사용하는 옵션
file_exists_behavior: OVERWRITE

# permissions 옵션의 하위 구성원들은
# files 옵션을 통하여 복사된 파일의 권한을 설정합니다.
# https://docs.aws.amazon.com/ko_kr/codedeploy/latest/userguide/reference-appspec-file-structure-permissions.html
permissions:
  - object: /
    pattern: "**"
    owner: ubuntu
    group: ubuntu

# 각 훅에서 어떠한 동작을 수행할지 추가적으로 명시
hooks:
  BeforeInstall:
    # 추후 EC2 인스턴스의 경로를 기준으로 지정
    - location: ./BeforeInstall.sh
      timeout: 120
      runas: ubuntu

  ApplicationStart:
    - location: ./ApplicationStart.sh
      timeout: 120
      runas: ubuntu

작성한 yml 파일은 이곳에 위치시켰습니다.

.sh 파일 추가

앞에서 작성한 yml 파일을 자세히 살펴보면, 각 훅에 .sh 확장자를 가진 파일의 위치가 명시되어 있음을 확인할 수 있습니다.

이는 리눅스 환경에서 실행되는 리눅스 쉘 스크립트를 의미하는데, 간단하게 설명하면 리눅스 환경에서 실행되는 프로그램을 의미합니다.

즉, 각 훅에서 리눅스 쉘 스크립트에 명시되어있는 동작을 실행한다고 생각하면 좋을 것 같습니다.

BeforeInstall.sh

#!/bin/bash

DIRECTORY=/home/ubuntu/chrkb1569
PROJECT_NAME=CI-CD-Practice-0.0.1-SNAPSHOT

echo "> 현재 실행중인 애플리케이션의 프로세스 ID 확인"
CURRENT_PID=$(pgrep -fl $PROJECT_NAME/*.jar| awk '{print $1}')

echo "> 현재 실행중인 프로세스 ID == $CURRENT_PID"
if [ -z "$CURRENT_PID" ]; then
  echo "> 현재 실행 중인 애플리케이션이 존재하지 않습니다."
else
  echo "실행 중인 애플리케이션을 종료하겠습니다."
  kill -9 $CURRENT_PID
  sleep 10
fi

echo "> 애플리케이션을 종료하였습니다!"

CodeDeploy의 BeforeInstall 단계에서 수행될 작업들을 명시해놓은 파일이라고 생각하면 될 것 같습니다.
본격적으로 애플리케이션의 배포에 들어가기 이전 단계이기 때문에 기존에 실행되고 있던 애플리케이션을 종료하도록 스크립트를 작성하였습니다.

ApplicationStart.sh

#!/bin/bash

DIRECTORY=/home/ubuntu/chrkb1569
PROJECT_NAME=CI-CD-Practice-0.0.1-SNAPSHOT

echo "> 새로운 애플리케이션 실행"
LATEST_PROJECT_NAME=$(ls -tr $DIRECTORY/*.jar | grep jar | tail -n 1)

echo "> 실행 권한 부여"
chmod 700 $LATEST_PROJECT_NAME

echo "> $LATEST_PROJECT_NAME 실행"
nohup java -jar $LATEST_PROJECT_NAME > $DIRECTORY/nohup.out 2>&1 &

본격적으로 배포할 애플리케이션을 실행하는 단계입니다.
/home/ubuntu/chrkb1569/ 경로에 존재하는 jar 파일을 실행하도록 스크립트를 작성하였습니다.

작성한 2개의 쉘 스크립트 파일은 다음에 위치시켰습니다.

AWS IAM 역할 생성

CodeDeploy를 사용하기에 앞서, 생성해야할 역할들이 몇 개 존재하는데, CodeDeploy를 사용하기 위한 역할과, EC2가 S3, CodeDeploy를 사용하기 위한 역할입니다.
일단 CodeDeploy를 위한 역할부터 생성해보겠습니다.

다음처럼 IAM의 역할 메뉴로 들어와 역할 생성을 선택해줍니다.

우리가 생성하는 역할은 AWS의 서비스인 CodeDeploy를 위한 역할이기 때문에, 다음처럼 사용 사례로 CodeDeploy를 선택해줍니다.

마지막으로 다음처럼 역할을 위한 이름을 설정한다면,

다음처럼 역할이 생성된 것을 확인할 수 있습니다.

그럼 이번에는 EC2를 위한 역할을 생성해보겠습니다.

다음처럼 EC2를 위한 역할임을 명시해주고,


CodeDeploy Access, S3 Access 권한을 추가하여 역할을 생성해줍니다.


생성해준 역할 중, EC2와 관련된 역할은 다음처럼 인스턴스 메뉴 - 보안 - IAM 역할 변경을 선택하여 인스턴스에 우리가 생성한 역할을 부여해줍니다.

CodeDeploy 생성

그럼 역할도 생성했으니, CodeDeploy 인스턴스를 생성해보겠습니다.

CodeDeploy - 배포 - 애플리케이션을 선택하면, 다음처럼 애플리케이션을 생성할 수 있는 메뉴를 확인할 수 있습니다.
생성하기를 선택해주겠습니다.

간단하게 애플리케이션의 이름, 플랫폼을 설정해주면 생성할 수 있습니다.

그러나 애플리케이션만으로는 안 될 것같고, 배포 그룹까지 추가해줘야할 것 같습니다.

다음처럼 하단 메뉴에 존재하는 배포 그룹 생성을 선택해주겠습니다.

간단하게 배포 그룹을 위한 이름을 기술하고, 처음에 생성했던 CodeDeploy를 위한 역할을 추가해줍니다.

우린 EC2 인스턴스에 배포할 계획이기 때문에 인스턴스 정보를 추가해줍시다.
여기서 갑자기 키와 같이 나와 당황하였는데,

이 정보는 다음처럼 EC2 상세 정보 - 태그 메뉴에 나와있습니다.
키값을 통하여 인스턴스를 식별하는 것 같습니다.

그렇게 다음처럼 배포 그룹까지 생성할 수 있었습니다.

CodeDeploy 설치

그럼 다음으로는 EC2 인스턴스에 CodeDeploy를 설치해보겠습니다.
EC2 인스턴스에 CodeDeploy를 설치하는 과정은 AWS 공식 문서에 잘 정리되어 있습니다.

[Reference : https://docs.aws.amazon.com/ko_kr/codedeploy/latest/userguide/codedeploy-agent-operations-install-ubuntu.html]

다음처럼 문서에 적혀있는 명령어들을 그대로 입력함으로써 설치할 수 있습니다.

설치가 모두 완료되었다면, 다음 명령어를 통하여 설치한 프로그램이 잘 동작중인지 확인할 수 있습니다.

$ sudo service codedeploy-agent status

다음은 정상적으로 프로그램이 동작하는 경우 출력되는 화면입니다.

Github Action 스크립트 수정

그럼 이제 CD 환경도 얼추 완성되었으니, Github Action 스크립트에 codeDeploy 관련 스크립트를 추가해주겠습니다.

# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle

name: Java CI with Gradle

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

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3
    
    - name: Set up JDK 17
      uses: actions/setup-java@v3
      with:
        distribution: 'temurin'
        java-version: '17'

    - name: write DB_URL
      run: echo "DB_URL=${{secrets.DB_URL}}" >> envyml
      working-directory: ./src/main/resources

    - name: write DB_PASSWORD
      run: echo "DB_PASSWORD=${{secrets.DB_PASSWORD}}" >> env.yml
      working-directory: ./src/main/resources
      
    - name: write DB_USERNAME
      run: echo "DB_USERNAME=${{secrets.DB_USERNAME}}" >> env.yml
      working-directory: ./src/main/resources

    - name: write AWS_REGION
      run: echo "DB_PASSWORD=${{secrets.AWS_REGION}}" >> env.yml
      working-directory: ./src/main/resources
      
    - name: write AWS_BUCKET_NAME
      run: echo "DB_USERNAME=${{secrets.BUCKET_NAME}}" >> env.yml
      working-directory: ./src/main/resources

    - name: write AWS_ACCESS_KEY
      run: echo "DB_USERNAME=${{secrets.AWS_ACCESS_KEY}}" >> env.yml
      working-directory: ./src/main/resources

    - name: write AWS_SECRET_KEY
      run: echo "DB_USERNAME=${{secrets.AWS_SECRET_KEY}}" >> env.yml
      working-directory: ./src/main/resources

    - name: write AWS_CODE_DEPLOY_NAME
      run: echo "DB_USERNAME=${{secrets.AWS_CODE_DEPLOY_NAME}}" >> env.yml
      working-directory: ./src/main/resources

    - name: write AWS_CODE_DEPLOY_GROUP_NAME
      run: echo "DB_USERNAME=${{secrets.AWS_CODE_DEPLOY_GROUP_NAME}}" >> env.yml
      working-directory: ./src/main/resources

    - name: Set up MySQL
      uses: samin/mysql-action@v1.3
      with:
        host port: 3306
        container port: 3306
        mysql host: ${{secrets.DB_URL}}
        mysql database: test
        mysql user: ${{secrets.DB_USERNAME}}
        mysql root password: ${{secrets.DB_PASSWORD}}
        
    - name: Build with Gradle
      uses: gradle/gradle-build-action@bd5760595778326ba7f1441bcf7e88b49de61a25
      with:
        arguments: clean bootJar

    - name: Make Directory for Deploy
      run: mkdir deploy

    - name: Copy Jar File
      run: cp ./build/libs/*.jar ./deploy/

    - name: Copy .sh File
      run: cp -r scripts deploy

    - name: Copy AppSpec.yml File
      run: cp ./appspec.yml ./deploy/

    - name: Move Directory
      run: cd ./deploy/
      
    - name: Make zip file
      run: zip -r -qq -j ./CI-CD.zip ./deploy

    - name: Deliver to AWS S3
      env:
        AWS_ACCESS_KEY_ID: ${{secrets.AWS_ACCESS_KEY}}
        AWS_SECRET_ACCESS_KEY: ${{secrets.AWS_SECRET_KEY}}
      run: aws s3 cp --region ap-northeast-2 --acl private ./CI-CD.zip s3://${{secrets.BUCKET_NAME}}/

    - name: Implement CodeDeploy
      env:
        AWS_ACCESS_KEY_ID: ${{secrets.AWS_ACCESS_KEY}}
        AWS_SECRET_ACCESS_KEY: ${{secrets.AWS_SECRET_KEY}}
        AWS_DEFAULT_REGION: ${{secrets.AWS_REGION}}
      run: aws deploy create-deployment --application-name ${{secrets.AWS_CODE_DEPLOY_NAME}} --deployment-config-name CodeDeployDefault.AllAtOnce --deployment-group-name ${{secrets.AWS_CODE_DEPLOY_GROUP_NAME}} --s3-location bucket=${{secrets.BUCKET_NAME}},key=CI-CD.zip,bundleType=zip

기존에는 jar 파일만을 압축하여 S3로 전송하는 스크립트를 사용하였는데, 기존 스크립트에 codeDeploy가 동작하기 위한 appspec.yml 파일과 쉘 스크립트 파일까지 함께 압축하여 전송하도록 코드를 변경하였습니다.

그리고 마지막으로 codeDeploy를 동작시켜줌으로써 커밋이 발생함과 동시에 배포까지 수행되도록 스크립트를 구성하였습니다.

테스트

그럼 일단 서버를 배포한 상태에서 테스트를 해보겠습니다.

현재 배포되어있는 서버의 Controller 파일입니다.

현재 배포되어 있는 서버는 다음처럼 정상적으로 동작하는 것을 확인할 수 있습니다.

그럼 여기서 v4를 추가하여 커밋하면 어떻게 동작할 지 확인해보도록 하겠습니다.

다음처럼 V4를 추가한 뒤, 커밋해보겠습니다.

github에 커밋할 경우, 다음처럼 github Action에서 코드의 충돌 여부를 확인한 뒤, 스크립트를 실행합니다.

다음처럼 스크립트가 정상적으로 동작하였으면,

codeDeploy가 코드를 배포해주고,

자동으로 배포가 됨을 확인할 수 있었습니다.

Reference

https://saramin.github.io/2022-02-25-hello-aws/
https://goodgid.github.io/Github-Action-CI-CD-CodeDeploy-App-Spec-File/

0개의 댓글