노션에서 보기 : https://cabi.oopy.io/b06df004-0449-4aa4-adfc-f15fe2d18f24

개요

이전 글 Github Actions로 CI 구성하기 with Spring Boot, 그리고 EC2로 서버 인스턴스 구성하기 with Spring Boot, Docker를 통해 CI와 서버 인스턴스를 구성해보았다. 이제, CI-CD의 마지막인 CD, 배포 부분을 구성해보려 한다.

AWS EC2를 서버 인스턴스로서 사용하고, Github Actions에서도 사용하기 편리한 것으로 보여서 AWS의 CodeDeploy를 이용해서 CD를 구성해보고자 한다.


CodeDeploy

CodeDeploy는 EC2 인스턴스 등 애플리케이션 배포를 자동화하는 서비스다.

CodeDeploy는 서버에서 실행되고, S3 버킷, GitHub 등에 저장되는 애플리케이션 콘텐츠를 배포할 수 있다.

CodeDeploy Agent

EC2에 CodeDeploy를 이용해 배포하려면 Agent를 설치해주어야 한다.

CodeDeploy 에이전트 설치 - AWS CodeDeploy

sudo yum update
sudo yum install ruby #code deploy를 실행하기 위한 의존성.
sudo yum install wget #http로 다운로드, 상호작용하게끔 해주는 CLI Tool.

wget <https://bucket-name.s3.region-identifier.amazonaws.com/latest/install>
chmod +x ./install #실행권한 설정
sudo ./install auto #최근 버전 다운로드
sudo service codedeploy-agent start #서비스로서 EC2 실행시 실행되도록 설정

또, AWS에서 역할을 생성하고 EC2에 ROLE을 설정해주어야 한다. - RoleForCodeDeploy

상세한 CodeDeploy 구성법과 이후에 서술될 내용들은 CodeDeploy로 Spring 배포 과정이라는 글에도 잘 나타나 있다.

IAM과 role에 대해서 궁금하다면 ’IAM이 도대체 뭘까?’를 읽어보면 좋을 것 같다.

이제 Role을 설정해주었다면 CodeDeploy의 밑바탕은 세팅한 것이다.

S3와 CodeDeploy

CodeDeploy 애플리케이션을 생성하고, 배포 그룹을 생성하여 설정했다면, 이제 S3를 이용해서 CI를 통한 배포 파일들을 관리하고, CodeDeploy를 이용해서 CD를 진행해보자.


S3에 CI 파일 업로드하기

S3는 AWS에서 제공하는 정적 스토리지이다.

이 S3의 버킷(디렉토리)에 우리가 배포하고자 하는 파일들을 전송하고, CodeDeploy에서 이를 이용하도록 지정하면 CI에 따른 CD를 구현할 수 있다!

배포를 위해서 S3 버킷을 생성하고, 해당 버킷에 대한 권한을 가지고 있는 IAM 계정의 ACCESS_KEY_ID와 ACCESS_KEY을 세팅하자. 이 글을 참고하면 좋을 것 같다.

준비되었다면, 해당 KEY와 ID를 github actions의 환경변수로 지정하고, 기존의 CI-CD 워크플로우에 다음과 같은 부분을 추가해보자.

- name: Configure AWS credentials
				# 해당 브랜치가 main이거나 dev인 경우에 실행되는 step이다.
        if: ${{ github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main' }}
        uses: aws-actions/configure-aws-credentials@v1
        with:
				#환경변수로 지정한 accesskey와 해당 버킷의 region을 설정해준다.
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ap-northeast-2
      - name: Upload build file to S3 and trigger CodeDeploy
        if: ${{ github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main' }}
        run: |
            mkdir -p before-deploy # 임시 디렉토리를 만든다.
            cp backend/build/libs/pet-*.jar before-deploy/ # 현재 컨테이너에서 체크아웃한 리포지터리의 CI 빌드된 파일을 옮긴다.
            cp backend/scripts/deploy.sh before-deploy/deploy.sh # 리포지토리에 미리 업로드한 deploy.sh를 옮긴다.
            cp backend/scripts/appspec.yml before-deploy/appspec.yml # 미리 업로드한 appspec.yml을 옮긴다.
            cd before-deploy && zip -r before-deploy * # 옮겨놓은 파일들을 압축한다.
            cd ../ && mkdir -p deploy # deploy 폴더를 생성한다.
            mv before-deploy/before-deploy.zip deploy/deploy.zip # before-deploy.zip을 deploy.zip으로 바꿔서 폴더로 옮긴다.
            aws s3 cp deploy/deploy.zip s3://42paw-deploy/deploy.zip # 컨테이너의 deploy.zip 파일을 s3://[버킷이름]/deploy.zip으로 업로드 한다.

이제 S3에 deploy.zip을 기준으로, CodeDeploy가 해당 파일들을 이용해 배포할 수 있도록 trigger 해주어야 한다.

위 yaml에 아래와 같이 더 작성해준다.

- name: Configure AWS credentials
				# 해당 브랜치가 main이거나 dev인 경우에 실행되는 step이다.
        if: ${{ github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main' }}
        uses: aws-actions/configure-aws-credentials@v1
        with:
				#환경변수로 지정한 accesskey와 해당 버킷의 region을 설정해준다.
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ap-northeast-2
      - name: Upload build file to S3 and trigger CodeDeploy
        if: ${{ github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main' }}
        run: |
            mkdir -p before-deploy
            cp backend/build/libs/pet-*.jar before-deploy/
            cp backend/scripts/deploy.sh before-deploy/deploy.sh
            cp backend/scripts/appspec.yml before-deploy/appspec.yml
            cd before-deploy && zip -r before-deploy *
            cd ../ && mkdir -p deploy
            mv before-deploy/before-deploy.zip deploy/deploy.zip
            aws s3 cp deploy/deploy.zip s3://42paw-deploy/deploy.zip

            aws deploy create-deployment \\ # 배포 생성
            --application-name paw-prod-deploy \\ # CodeDeploy 애플리케이션 이름
            --deployment-config-name CodeDeployDefault.OneAtATime \\ # 한 번에 한 대의 서버에만 배포를 진행하도록 하는 설정
            --deployment-group-name paw \\ # 배포 타겟 그룹 이름 - 이전에 설정했다면, 배포하고자 하는 EC2가 이 그룹에 설정되어 있어야 한다.
            --description "Deploying from GitHub Actions" \\ # 설명
            --file-exists-behavior OVERWRITE \\ # 이미 파일이 존재할 시 덮어쓴다.
            --s3-location bucket=42paw-deploy,bundleType=zip,key=deploy.zip # 버킷의 위치를 지정해주고, 파일 타입, 배포하려는 파일을 지정해준다.

이렇게 설정하면, S3 버킷에 deploy.zip이 업로드되고, 이후에 CodeDeploy가 트리거된다.

그리고 해당 zip 파일을 타겟 그룹에 배포한다. 우리의 경우 이전에 EC2에 설치한 CodeDeploy Agent가 이를 수행, 압축을 해제하고 appspec.yml에 따라 동작을 수행한다.

이제 appspec.yml을 잘 작성하고, 필요한 후 작업들을 구성하면 CI-CD를 완성할 수 있다!


appspec.yml

CodeDeploy를 트리거하고, 원하는 대로 동작하게 하고 싶다면 배포 방법에 대한 설정파일인 appspec.yml을 작성해주어야 한다.

S3에 전달해주면, CodeDeploy가 트리거될 때 해당 yml을 읽고 이정표로서 사용할 수 있다.

version: 0.0
os: linux
files: #CodeDeploy에게 애플리케이션의 파일 및 디렉토리를 배포할 위치를 알려준다.
  - source: / #배포할 파일의 위치를 지정한다. 이 경우, 루트 디렉토리다.
    destination: /home/ec2-user/deploy/ #해당 파일들이 어디에 위치해있는지 지정한다.
    overwrite: yes #덮어쓰기

permissions: #배포 후 해당 파일이나 디렉토리의 권한을 설정한다.
  - object: / #권한을 변경하려는 대상을 지정한. 이 경우 루트 디렉토리를 지정한다.
    pattern: "**" #권한을 변경하려는 파일이나 디렉토리의 패턴을 지정한다. 모든 파일과 디렉토리(**)를 대상으로 설정했다.
    owner: ec2-user 
    group: ec2-user # 소유자 및 그룹을 ec2-user로 설정한다.

hooks: # 배포의 단계별로 실행할 스크립트를 지정한다.
  ApplicationStart:
    - location: deploy.sh
      timeout: 60 #deploy.sh가 완료될 때까지 기다린다.
      runas: ec2-user #해당 스크립트를 실행할 사용자를 지정한다.

deploy.sh

결국 배포를 통해 가져오는 핵심적인 파일은 [프로젝트]-[버전].jar 파일이다.

배포 후에 진행되어야 하는 작업은 다음과 같다.

  • 기존에 구동 중이던 서버 프로세스를 종료한다.
  • 새로운 버전의 빌드 파일로 서버 프로세스를 실행한다.

기존에 돌아가고 있는 프로세스를 냅다 찾아서 kill하는 방법으로 처음에 구현했었다.

#!/bin/bash
USER_HOME=/home/ec2-user
PROJECT_NAME=pet
SPRING_BOOT_PROCESS_ID= $(lsof | grep '.jar' | awk '{print $2}' | head -n 1)

kill $SPRING_BOOT_PROCESS_ID
java -jar -Dspring.profiles.active=prod $USER_HOME/deploy/$PROJECT_NAME-*.jar &

하지만 위 방식은 잘 적용되지도 않을 뿐더러 상당히 불안정하다(정확하게 딱 해당하는 프로세스를 종료하고, 종료 뒤에 구동한다는 보장이 없다).

좀 더 낫게 만드는 방법을 찾아보았을 때, spring boot 프로세스 자체를 해당 EC2의 service로서 동작시키는 방법을 알아냈다.

service가 무엇인지, 어떻게 등록하는지는 이 글을 참고하면 좋을 것 같다.

deploy.sh - 배포 후 실행하고자 하는 script

  • 기존에 있는 jar 배포 파일을 삭제하고, 현재 배포된 jar 파일을 /usr/local 디렉토리에 원하는 이름으로 복사한다.
  • 실행중인 service 프로세스를 종료하고, 새로운 빌드 파일로 실행한다.
#!/bin/bash

SERVICE_NAME=[서비스 이름] # petboot
SOURCE_DIR_PATH=/home/ec2-user/deploy
TARGET_PATH=/usr/local/[서비스에서 실행하고자 하는 파일 이름].jar # /usr/local/pet_boot.jar

echo "jar 파일이 있는지 확인합니다"
if [ -e ${TARGET_PATH} ] ; then
	echo "기존의 jar파일 삭제합니다"
	sudo rm $TARGET_PATH
	STATUS=`echo $?`
	if [ ${STATUS} -ne 0 ] ; then
		echo "기존 jar파일 삭제를 실패했습니다"
		exit 1
	fi
fi

echo "build된 jar파일이 있는지 확인합니다"
SOURCE_PATH=`find $SOURCE_DIR_PATH -type f -name "*jar"`
if [ ${TARGET_PATH} -ef ${SOURCE_PATH} ] ; then
	echo "build된 jar파일을 찾지 못했습니다"
	exit 1
fi

echo "$SOURCE_PATH 에서 jar파일을 찾았습니다"
echo "build된 jar파일을 옮깁니다"

sudo cp ${SOURCE_PATH} ${TARGET_PATH}
STATUS=`echo $?`

if [ ${STATUS} -ne 0 ] ; then
	echo "jar파일 옮기기를 실패했습니다"
	exit 1
fi

echo "실행되고 있는 spring service가 있는지 확인합니다"
sudo systemctl is-active $SERVICE_NAME
SERVICE_STATUS=`echo $?`

echo "실행 되는 서비스 상태는 $SERVICE_STATUS 입니다"
if [ ${SERVICE_STATUS} -eq 0 ] ; then
	echo "실행중인 서비스를 종료합니다"
	sudo systemctl stop $SERVICE_NAME
	STATUS=`echo $?`
	if [ ${STATUS} -ne 0 ] ; then
		echo "서비스 종료에 실패했습니다"
		exit 1
	fi
fi

echo "서비스를 실행합니다."
sudo systemctl start $SERVICE_NAME
STATUS=`echo $?`
if [ ${STATUS} -ne 0 ] ; then
	echo "서비스 실행에 실패했습니다"
	exit 1
fi

sudo systemctl is-active $SERVICE_NAME
SERVICE_STATUS=`echo $?`
if [ ${SERVICE_STATUS} -ne 0 ] ; then
	echo "서비스 실행에 실패했습니다"
	exit 1
fi

exit 0

/etc/systemd/system/[서비스 이름].service

위 deploy.sh이 잘 동작하기 위해서는 우리가 원하는 service로 동작하게끔 설정해주어야한다.

/etc/systemd/system 디렉토리에 .service로 우리가 실행하고자하는 서비스를 구성해주자.

[Unit]
Description=[설명] # Pet Spring Boot Server
After=network.target

[Service]
ExecStart=/bin/bash -c "exec java -Dspring.profiles.active=prod -jar  /usr/local/[서비스에서 실행하고자 하는 파일 이름].jar" > ~/deploy/deploy.log 2>>~/deploy/deploy_err.log
User=ec2-user
Group=ec2-user

[Install]
WantedBy=multi-user.target

위 설정을 통해 다음과 같이 사용할 수 있다.

  • systemctl start [서비스 이름]
  • systemctl stop [서비스 이름]
  • systemctl enable [서비스 이름]
  • systemctl disable [서비스 이름]

자세한 용법은 이 글을 참고하면 좋을 것 같다.

이제 service를 구동하고 상태를 체크해보면 아래와 같이 나타난다.

deploy.sh을 변경해주면 Github Actions → CodeDeploy → EC2에 압축해제 후 실행의 과정을 거쳐서 새 빌드파일로 교체, service를 재시작하는 프로세스로 진행될 것이다.

special thanks to dongglee


정리

최종 흐름은 위와 같다.

이제 변경사항에 대한 Commit, PR && Merge만으로도 빌드, 테스트, 배포까지 자동으로 진행된다!

한편, CD에 있어서 의도적으로 프로덕션 서버의 DB와 관련한 커맨드를 구성하지 않았다.

그 이유는, 로컬이나 dev 환경에서는 자동적으로 DB를 조작해도 되지만, 프로덕션 서버는 혹시 모를 문제가 발생할 수 있으므로, 임의로 조작하면 안 될 것 같다고 생각했기 때문이다.

CI-CD의 진가는 반복되는 작업을 줄이고, 테스팅을 해본다는 점에서 배포의 부담과 협업의 효율성과 효과성을 높여주는 매우 중요한 부분이라고 생각이 들었다. 아직 완벽하다고는 말할 수 없겠지만, 필요한 경우에 대해서 어떠한 흐름에서 조정하고, 더 고도화해야할 지 파악한 것 같다.

참고자료

CodeDeploy로 Spring 배포 과정

AWS 계정 및 액세스 키 - AWS Tools for PowerShell

Amazon Linux 또는 RHEL용 CodeDeploy 에이전트 설치 - AWS CodeDeploy

리눅스에서 Service 등록하기

복사했습니다!