Github Actions + CodeDeploy로 CI/CD 구현하기

Mugeon Kim·2023년 10월 2일
0

서론


  • 안녕하세요. 이번에는 Git Action으로 배포 자동화를 수행을 하였습니다.
    이전에는 Jenkins를 통해서 배포 자동화를 하였지만 Git Action으로 배포 자동화를 수행을 하였습니다.
  • Jenkins에서 Git Action으로 변경한 이유는 일단 Jenkins는 설치형입니다. 따라서 2개의 workspace가 필요하며 배포 자동화를 하기 위해서 사용하기에는 불필요한 설정이 많기 때문에 Git Action으로 쉽게 자동화를 하기 위하여 변경을 하였습니다.
  • Jenkins 배포 자동화 정리

본론


배포 방식의 고민

  1. Code Deploy를 통해서 배포 자동화 ( AWS DB 사용 )
  • 위에 방식을 사용하면 GIT ACTION을 사용하여 쉽게 배포가 가능합니다. 하지만 프로젝트에서 MYSQL, REDIS, MONGODB를 사용하기 때문에 비용적인 문제가 있기 때문에 선택을 하지 않았습니다.
  1. Docker, Docker-compose를 통한 자동화
  • CD Deploy를 통하여 compose로 만들어 db의 문제를 쉽게 해결할 수 있지만 이번 프로젝트에서는 AWS CodeDeploy를 사용을 하고 싶어서 선택을 하지 않았습니다.
  1. 2개의 workSpace Code Deploy를 통해서 배포 자동화
  • 이번 프로젝트에서 선택한 방식입니다. 하나의 ec2에 mysql, redis, mongodb를 생성을 하고 다른 하나의 ec2에 배포를 하는 방식을 사용을 하였습니다. 물론 이 방식이 최선의 방식은 아니라고 생각을 했습니다. DB 인스턴스에 문제가 생긴다면 DB의 데이터의 손실의 문제가 발생할 수 있고 확장성에서 떨어지지만 Git Action을 쉽게 학습하고 AWS의 다양한 기능을 사용하기에 적합한 방식이라고 생각하여 이 방식을 선택을 하였습니다.


1. EC2 생성

  • Ubuntu 20.04 버전을 선택을 하였습니다.
  • Ubuntu에서 22 버전이 있지만 명령어가 다르기 때문에 가장 익숙한 20.04 버전을 선택을 하였습니다.
  • 이후 인스턴스 유형은 small로 설정을 하였습니다. 기존에 docker-compose로 배포를 하였을 때 자주 ec2가 다운이 되는 문제가 발생을 하여 small로 변경하고 메모리 swap을 수행을 하였습니다.

메모리 Swap

sudo dd if=/dev/zero of=/swapfile bs=128M count=32

sudo chmod 600 /swapfile

sudo mkswap /swapfile

sudo swapon /swapfile

sudo swapon -s

sudo vi /etc/fstab
--------------------
[ vi에 하단에 추가 ] 
/swapfile swap swap defaults 0 0
----------------------
## 용량 확인
free
  • 이후 EC2 대시보드를 살펴보면 정상적으로 생성이 된 것을 볼 수 있습니다.

  • 이후 탄력적 IP 설정, 태그 관리를 하겠습니다.

  • 사이드에 탄력적 IP를 선택하여 EC2에 할당을 하고 이후 태그를 선택을 합니다.

  • 인스턴스 오른쪽 클릭 -> 인스턴스 설정 -> 태그 관리를 선택을 하여 태그를 생성을 합니다. -> 이때 태그는 나중에 DEPLOY에서 선택을 할 때 사용을 합니다.


2. EC2 접속 && 초기 설정

  • window에서 ec2를 접속하는 방법은 편리함을 주는 mobaxTerm을 자주 사용을 합니다.
  • 여기서 mobaxTerm에서 접속하는 방법은 다음 2개가 있습니다.

1. mobaXterm SSH

  • mobaxTerm에서 session -> ssh를 선택을 하면 다음과 같은 화면이 나옵니다.

  • Remote Host : 탄력적 IP

  • Specify username : aws linux를 사용하면 ec2-user / ubuntu를 사용하면 ubuntu

  • use private key를 선택을 하여 ppm 키를 클릭을 하여 접속을 합니다.


2. AWS SSH

  • ec2의 연결을 선택을 하면 다음과 같은 화면이 나옵니다. SSH 클라이언트를 선택하고 빨간색의 예를 복사하여 KEY가 있는 디렉토리로 이동을 하여 선택을 합니다.


3. 초기 설정

# 스냅샷 update
sudo apt update && sudo apt upgrade

//서울 시간으로 세팅하기
timedatectl list-timezones | grep Seoul
sudo timedatectl set-timezone Asia/Seoul

# 자바 설치
sudo apt install openjdk-11-jdk

# 자바 버전 확인
java --version

# aws 가이드라인 문서
sudo apt install ruby-full
sudo apt install wget
cd /home/ubuntu
wget https://aws-codedeploy-ap-northeast-2.s3.ap-northeast-2.amazonaws.com/latest/install
chmod +x ./install
sudo ./install auto > /tmp/logfile
sudo service codedeploy-agent status
  • 정상적으로 codeDeploy까지 완료를 하면 다음과 같은 화면이 나옵니다.

Ubuntu Server용 CodeDeploy 에이전트 설치 (공식 문서)
https://docs.aws.amazon.com/ko_kr/codedeploy/latest/userguide/codedeploy-agent-operations-install-ubuntu.html

3. IAM 설정

역할 만들기

  • 역할을 선택하고 역할 만들기를 선택을 합니다.
  • 이후 AWS 서비스를 선택을 하고 EC2를 선택을 합니다.

  • 이후 역할을 추가하는 부분에 2개를 추가를 하고 생성을 합니다.

    AmazonS3FullAccess
    AWSCodeDeployFullAccess


역할 EC2에 연결하기

  • IAM을 만들고 EC2랑 연결한다. EC2 홈에서 EC2 욱클릭 → 보안 → IAM역할 수정


4. S3 & IAM 사용자

S3 만들기

  • 기존에 S3와 만드는 방식과 똑같이 S3를 설정을 하면 됩니다.

IAM 사용자

  • IAM을 눌러 이전의 권한이 아닌 사용자를 누르고 생성을 합니다. 이름을 입력하고 다음을 누르면 역할 설정이 나옵니다.

  • 2개를AmazonS3FullAccess , AWSCodeDeployFullAccess를 추가를 합니다.

  • 이후 완료를 누르고 생성된 사용자를 눌러 보안 자격 증명 -> 엑시스 키 만들기를 선택 -> AWS 컴퓨팅 서비스에서 실행되는 애플리케이션 를 선택을 합니다. 추가 만들어진 키 2개를 따로 저장을 합니다.


5. CodeDeploy

IAM 역할

  • 이전에 작성한 방법처럼 IAM의 역할을 추가를 합니다. 이때 기존의 방식과 다른 점은 사용 사례에서 CodeDeploy를 선택을 합니다.

CodeDeploy 애플리케이션

  • 이후 CodeDeploy를 선택을 하여 애플리케이션 > 애플리케이션 생성을 선택을 합니다.

  • 이후 애플리케이션 CSTUDY_CODE_DEPLOY > 배포 그룹 생성을 선택하여 서비스 역할에 서비스 역할 입력에 이전에 만들었던 IAM을 선택을 합니다.

여기까지 AWS 설정은 끝났습니다.


6. Github

deploy.yaml

해당 프로젝트에서 확인을 할 수 있습니다.

  • 프로젝트에서 root의 위치에 ./github/workflows의 deploy.yaml을 추가를 합니다.

deploy.yaml

name: CI-CD

# Main 브랜치에 push를 하였을 때 
on:
  push:
    branches:
      - main
      
## 이전에 만들었던 S3, DEPLOY_NAME, GROUP_NAME을 ENV로 따로 변수로 설정을 합니다.
## RESOURCE_PATH는 현재 프로젝트가 멀티모듈로 구성이 되어서 모놀리직 구조이면 module-api를 삭제하고
## 적용을 하면 됩니다.
env:
  S3_BUCKET_NAME: s3-cstudy
  RESOURCE_PATH: ./module-api/src/main/resources/application.yml
  CODE_DEPLOY_APPLICATION_NAME: CODE-DEPLOY-CSTUDY
  CODE_DEPLOY_DEPLOYMENT_GROUP_NAME: CODE-DEPLOY-CSTUDY-GROUP

jobs:
  build:
  	# 어떤 OS에 실행이 되는지
    runs-on: ubuntu-latest

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

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


## Git Action이 실행이 되면서 동적으로 application.yml에 변수로 주입을 합니다.
## 해당 변수는 Github Setting에서 설정을 할 수 있습니다.
## 밑에 사진으로 설명을 하겠습니다.
      - name: Set yaml file
        uses: microsoft/variable-substitution@v1
        with:
          files: ${{ env.RESOURCE_PATH }}
        env:
          spring.data.mongodb.uri: ${{ secrets.MONGODB_URL }}
          spring.redis.host: ${{ secrets.REDIS_HOST }}

          spring.datasource.url: ${{ secrets.MYSQL_URL }}
          spring.datasource.username: ${{ secrets.MYSQL_USERNAME }}
          spring.datasource.password: ${{ secrets.MYSQL_PASSWD }}

          spring.mail.username: ${{ secrets.MAIL_USERNAME }}
          spring.mail.password: ${{ secrets.MAIL_PASSWORD }}


          spring.security.oauth2.client.registration.google.client-id: ${{ secrets.GOOGLE_CLIENT_ID }}
          spring.security.oauth2.client.registration.google.client-secret: ${{ secrets.GOOGLE_SECRET }}

          spring.security.oauth2.client.registration.naver.client-id: ${{ secrets.NAVER_CLIENT_ID }}
          spring.security.oauth2.client.registration.naver.client-secret: ${{ secrets.NAVER_SECRET }}

          spring.security.oauth2.client.registration.kakao.client-id: ${{ secrets.KAKAO_CLIENT_ID }}

          jwt.secretKey: ${{ secrets.JWT_SECRET_KEY }}
          jwt.refreshKey: ${{ secrets.JWT_REFRESH_KEY }}

          cloud.aws.credentials.accessKey: ${{ secrets.AWS_CREDENTIALS_ACCESS_KEY }}
          cloud.aws.credentials.secretKey: ${{ secrets.AWS_CREDENTIALS_SECRET_KEY }}

          cloud.aws.s3.bucket: ${{ secrets.S3_BUCKET }}
          cloud.aws.region.static: ${{ secrets.AWS_REGION }}

		## 권한을 주는 명령어
      - name: Grant execute permission for gradlew
        run: chmod +x ./gradlew
        shell: bash

		## build를 하는 명령어
        ## 모놀리직 구조는 gradle build -x test
      - name: Build with Gradle
        run: ./gradlew clean :module-api:buildNeeded --stacktrace --info --refresh-dependencies -x test
        shell: bash
		
        ## Zip 파일 생성: 프로젝트를 압축하여 zip 파일 생성.
      - name: Make zip file
        run: zip -r ./$GITHUB_SHA .
        shell: bash
		
        ## AWS 자격 증명 구성: AWS 자격 증명 설정.
      - 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 }}
		
        ## S3에 업로드: 생성된 zip 파일을 S3 버킷에 업로드.
      - name: Upload to S3
        run: aws s3 cp --region ap-northeast-2 ./$GITHUB_SHA.zip s3://$S3_BUCKET_NAME/$GITHUB_SHA.zip
		
        ## Code Deploy: AWS CodeDeploy에 배포 생성.
      - name: Code Deploy
        run: |
          aws deploy create-deployment \
          --deployment-config-name CodeDeployDefault.AllAtOnce \
          --application-name ${{ env.CODE_DEPLOY_APPLICATION_NAME }} \
          --deployment-group-name ${{ env.CODE_DEPLOY_DEPLOYMENT_GROUP_NAME }} \
          --s3-location bucket=$S3_BUCKET_NAME,bundleType=zip,key=$GITHUB_SHA.zip

Application.yml 변수 설정하기

  • 프로젝트의 Repository의 설정 -> Security -> Action에서 New Repository를 선택을 하여 해당 yml 파일에 동적으로 주입을 할 변수를 입력을 합니다.

appspec.yml


## source : 인스턴스 복사 디렉토리
## destination : 인스턴스에서 파일이 복사되는 위치
## overwrite : 복사할 위치에 파일이 있는 경우 대체
version: 0.0
os: linux
files:
  - source: /
    destination: /home/ubuntu/CStudy
    overwrite: yes

## object : 권한이 지정되는 파일 또는 디렉토리
permissions:
  - object: /
    pattern: "**"
    owner: ubuntu
    group: ubuntu

## 파일 설치 뒤, AfterInstall에서 기존에 실행되던 애플리케이션을 종료시키고, ApplicationStart에서 새로운 애플리케이션을 실행시킨다.
hooks:
  ApplicationStart:
    - location: scripts/gh_deploy.sh
      timeout: 60
      runas: ubuntu

gh_deploy.sh

#!/bin/bash
PROJECT_NAME="CStudy"
JAR_PATH="/home/ubuntu/CStudy/module-api/build/libs/*.jar"
DEPLOY_PATH=/home/ubuntu/$PROJECT_NAME/
DEPLOY_LOG_PATH="/home/ubuntu/$PROJECT_NAME/deploy.log"
DEPLOY_ERR_LOG_PATH="/home/ubuntu/$PROJECT_NAME/deploy_err.log"
APPLICATION_LOG_PATH="/home/ubuntu/$PROJECT_NAME/application.log"
BUILD_JAR=$(ls $JAR_PATH)
JAR_NAME=$(basename $BUILD_JAR)

echo "===== 배포 시작 : $(date +%c) =====" >> $DEPLOY_LOG_PATH

echo "> build 파일명: $JAR_NAME" >> $DEPLOY_LOG_PATH
echo "> build 파일 복사" >> $DEPLOY_LOG_PATH
cp $BUILD_JAR $DEPLOY_PATH

echo "> 현재 동작중인 어플리케이션 pid 체크" >> $DEPLOY_LOG_PATH
CURRENT_PID=$(pgrep -f $JAR_NAME)

if [ -z $CURRENT_PID ]
then
  echo "> 현재 동작중인 어플리케이션 존재 X" >> $DEPLOY_LOG_PATH
else
  echo "> 현재 동작중인 어플리케이션 존재 O" >> $DEPLOY_LOG_PATH
  echo "> 현재 동작중인 어플리케이션 강제 종료 진행" >> $DEPLOY_LOG_PATH
  echo "> kill -9 $CURRENT_PID" >> $DEPLOY_LOG_PATH
  kill -9 $CURRENT_PID
fi

DEPLOY_JAR=$DEPLOY_PATH$JAR_NAME
echo "> DEPLOY_JAR 배포" >> $DEPLOY_LOG_PATH
# 만약에 실제 서비스면
nohup java -jar $DEPLOY_JAR >> $APPLICATION_LOG_PATH 2> $DEPLOY_ERR_LOG_PATH &

sleep 3

echo "> 배포 종료 : $(date +%c)" >> $DEPLOY_LOG_PATH

Git Action 확인하기


7. AWS 확인하기

  • 빌드가 정상적으로 동작을 하였으면 AWS의 S3에 ZIP 파일이 정상적으로 로딩을 확인할 수 있습니다.

  • 이후 Git Action에서 배포 요청을 보내고 S3에서 codeDeploy에게 zip 파일을 전달하여 EC2에 배포하는 방식으로 배포 자동화를 하였습니다.

  • 실제 프로젝트에서 이걸 한번 확인하기 위해서 lsof -i:8080을 통해서 PORT가 정상적으로 동작이 되었는지 확인하고

ubuntu@ip-172-XX-15-XXX:~/CStudy$ lsof -i:8080
COMMAND   PID   USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
java    32787 ubuntu   32u  IPv6 100746      0t0  TCP *:http-alt (LISTEN)
  • cat deploy.log를 통해서 배포를 확인할 수 있고 cat application.log를 통하여 오류를 확인을 할 수 있습니다.

  • 이후 배포의 테스트를 위해서 임시로 String을 반환하는 Test Controller를 만들어서 CURL 요청을 통하여 확인하면 정상적으로 배포가 되었는지 알 수 있습니다.

결론


  • 이전에 Jenkins를 통해서 배포 자동화를 하였을 때 현업자와 함께 스터디를 통해서 학습을 하다보니 막히는 부분에서 도움을 많이 받았다. 하지만 이번에는 혼자서 학습하고 배포 자동화를 해보니 많은 실패가 있었지만 자료가 많아서 해결할 수 있었다.

  • 현재는 배포 자동화만 하였지만 다음에는 VPC 설정, 오토 스케일링, Red-Green 무중단 배포를 학습하여 블로깅을 하겠다.

profile
빠르게 실패하고 자세하게 학습하기

0개의 댓글