Github Action을 사용하여 AWS 배포 자동화 구축 - 1

koonlx·2024년 8월 28일

stay_connect

목록 보기
5/10

1. 배포 자동화를 구축한 이유

  • 그 때 당시 진행 중이었던 stay connect 프로젝트는 비전공자 3명, 전공자 1명(본인)으로 4명, 기간은 약 2-3개월이었다. 당장 인력도 실력도 시간도 부족하였기 때문에 push하면 자동배포를 하여 AWS EC2에서 잘 동작하는지 확인하고자 하였다.
  • 개선 사항이 있을 시 빠르게 배포할 수 있도록 하는 것 또한 이유에 포함된다.

2. jenkins가 아닌 github action을 사용한 이유

  • GitHub와의 완벽한 통합: GitHub Actions는 GitHub 리포지토리와 완벽하게 통합되어 있어, GitHub에 대한 깊은 통합 기능(예: 풀 리퀘스트, 이슈, 코드 푸시 등을 트리거로 사용 가능)을 제공한다.
  • 사용의 간편함: YAML 파일을 통해 간단하게 워크플로우를 정의할 수 있다. GitHub 인터페이스에서 직접 설정하고 관리할 수 있어 접근이 용이하다.
  • 자동화된 마켓플레이스: GitHub Actions 마켓플레이스에는 수많은 프리셋 액션이 있어, 필요한 기능을 손쉽게 가져다 사용할 수 있다.
  • 서버 관리 필요 없음: GitHub Actions는 GitHub에서 제공하는 호스티드 러너를 사용하여 실행되기 때문에, 별도의 서버 관리가 필요 없다.(물론 원한다면 self hosting 가능)
  • 비용 효율적: GitHub의 무료 요금제에서도 Actions를 일정 부분 사용할 수 있어, 소규모 프로젝트에 적합하다.(비용이 부담된다면 self hosting을 해도 되기 때문에 문제되지 않는다. 다음에 관련 글을 포스팅 하겠다.)

요는 github 최적화되어 사용이 간편하고, 마켓플레이스에서 프리셋을 사용할 수 있다. 서버를 직접 호스팅하지 않아도 되고, 자원도 넉넉하여 비용부담이 거의 없다.

3. github_action.yml

키워드 순서대로 설명하겠다.

# StayConnect-Action.yml
name: GITHUB ACTION CI/CD WITH AWS

github action
말 그대로 이름이다. github의 action탭에서 왼쪽 사이드바에서 확인할 수 있다.

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main, develop]

on 키워드는 '언제' 실행될지 설정하는 조건을 위한 키워드이다. 해당 설정은 main, develop 브랜치에 push 또는 pull request를 할 때 실행되도록 설정했다.

jobs:
  build: ...
  deploy: ...
  clear: ...
  dependency-submission: ...

jobs 키워드는 on 키워드의 조건이 충족되었을 때 실행될 작업들을 정의하는 곳이다. 하위의 build, deploy, clear는 직접 정의한 jobs의 이름이다. 본인이 원하는 이름으로 작성하면 된다. dependency-submission은 dependency graph를 생성하는 공식 프리셋 기본 이름이다.

jobs:
   build:
      runs-on: ubuntu-latest 									# hosting server os 명시

      steps:													# 내 repository를 hostign server로 가져온다.
         - name: Checkout repository
           uses: actions/checkout@v4

         - name: Set up JDK 21									# hosting server에 JDK 설치
           uses: actions/setup-java@v4
           with:
              distribution: 'temurin'
              java-version: '21'

         - name: Create environment variables file				# 환경변수 파일 생성. 보안을 위해 github secret 사용
           run: |
              echo "BASE_URL=${{ secrets.BASE_URL }}" >> .env
              echo "SERVER_PORT=${{ secrets.SERVER_PORT }}" >> .env
              echo "JSON_WEB_TOKEN_SECRET_KEY=${{ secrets.JSON_WEB_TOKEN_SECRET_KEY }}" >> .env
              echo "JSON_WEB_TOKEN_EXPIRATION_TIME=${{ secrets.JSON_WEB_TOKEN_EXPIRATION_TIME }}" >> .env
              echo "JSON_WEB_TOKEN_ISSUER=${{ secrets.JSON_WEB_TOKEN_ISSUER }}" >> .env
              echo "MYSQL_SERVER_URL=${{ secrets.MYSQL_SERVER_URL }}" >> .env
              echo "MYSQL_SERVER_USERNAME=${{ secrets.MYSQL_SERVER_USERNAME }}" >> .env
              echo "MYSQL_SERVER_PASSWORD=${{ secrets.MYSQL_SERVER_PASSWORD }}" >> .env
              echo "GOOGLE_CLIENT_ID=${{ secrets.GOOGLE_CLIENT_ID }}" >> .env
              echo "GOOGLE_CLIENT_SECRET=${{ secrets.GOOGLE_CLIENT_SECRET }}" >> .env
              echo "KAKAO_CLIENT_ID=${{ secrets.KAKAO_CLIENT_ID }}" >> .env
              echo "KAKAO_CLIENT_SECRET=${{ secrets.KAKAO_CLIENT_SECRET }}" >> .env
              echo "PORTONE_API_URL=${{ secrets.PORTONE_API_URL }}" >> .env
              echo "PORTONE_API_KEY=${{ secrets.PORTONE_API_KEY }}" >> .env
              echo "PORTONE_SECRET_KEY=${{ secrets.PORTONE_SECRET_KEY }}" >> .env
              echo "PRODUCTION_MYSQL_URL=${{ secrets.PRODUCTION_MYSQL_URL }}" >> .env
              echo "PRODUCTION_MYSQL_ROOT_PASSWORD=${{ secrets.PRODUCTION_MYSQL_ROOT_PASSWORD }}" >> .env
              echo "PRODUCTION_MYSQL_DATABASE=${{ secrets.PRODUCTION_MYSQL_DATABASE }}" >> .env
              echo "PRODUCTION_MYSQL_USERNAME=${{ secrets.PRODUCTION_MYSQL_USERNAME }}" >> .env
              echo "PRODUCTION_MYSQL_PASSWORD=${{ secrets.PRODUCTION_MYSQL_PASSWORD }}" >> .env

         - name: Cache Gradle packages							# gradle caching
           uses: actions/cache@v4
           with:
              path: |
                 ~/.gradle/caches
                 ~/.gradle/wrapper
              key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
              restore-keys: |
                 gradle-${{ runner.os }}-

         - name: Grant execute permission for gradlew 			# gradlew 실행 권한 부여
           run: chmod +x gradlew

         - name: Build with Gradle 								# 배포를 위해 prod profile을 사용해서 jar build
           run: ./gradlew clean build -Dspring.profiles.active=prod -x test

         - name: Upload build artifacts 						# build한 jar 파일을 artifact에 업로드
           uses: actions/upload-artifact@v4
           with:
              name: build-artifacts
              path: build/libs/StayConnect-0.0.2.jar
              retention-days: 1 								# 저장공간을 확보하기 위해 유지기간 1일로 설정

         - name: Upload build artifacts 						# deploy jobs로 넘겨주기 위해 env artifact upload
           uses: actions/upload-artifact@v4
           with:
              name: env-artifacts
              path: .env
              retention-days: 1

deploy:
      if: github.ref == 'refs/heads/main' 						# main branch에 변화가 있을 때 동작하도록 조건 설정
      runs-on: ubuntu-latest
      needs: build 												# build jobs가 선행되어야 함.

      steps:
         - name: Checkout repository
           uses: actions/checkout@v4

         - name: Download build artifacts 						# build한 jar 다운로드
           uses: actions/download-artifact@v4
           with:
              name: build-artifacts
              path: build/libs

         - name: Download env artifacts 						# build한 env 다운로드
           uses: actions/download-artifact@v4
           with:
              name: env-artifacts
              path: .

         - name: Set up JDK 21
           uses: actions/setup-java@v4
           with:
              distribution: 'temurin'
              java-version: '21'

         - name: Set lower-case repository owner 				# 소문자만 돼서 owner 이름 소문자로 변경
           id: lowercase
           run: echo "::set-output name=repository_owner::$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]')"


         - name: Set up QEMU 		 							# 다중 플랫폼 빌드를 지원하기 위해 QEMU 에뮬레이터를 설정
           uses: docker/setup-qemu-action@v3

         - name: Set up Docker Buildx           				# 다중 아키텍처 이미지를 빌드할 수 있도록 Docker Buildx를 설정
           uses: docker/setup-buildx-action@v3

         - name: Log in to GitHub Container Registry            # GitHub Container Registry(GHCR)에 로그인
           uses: docker/login-action@v3
           with:
              registry: ghcr.io
              username: ${{ steps.lowercase.outputs.repository_owner }}
              password: ${{ secrets.GITHUB_TOKEN }}

         - name: Build and push Nginx image						# Nginx 이미지를 빌드하고 GHCR로 푸시
           uses: docker/build-push-action@v4
           with:
              context: .
              file: ./docker/dockerfile.nginx
              push: true
              tags: ghcr.io/${{ steps.lowercase.outputs.repository_owner }}/stay_connect_nginx:latest

         - name: Build and push Spring Boot image				# Spring Boot 이미지를 빌드하고 GHCR로 푸시
           uses: docker/build-push-action@v4
           with:
              context: .
              file: ./docker/dockerfile.springboot
              push: true
              tags: ghcr.io/${{ steps.lowercase.outputs.repository_owner }}/stay_connect_app:latest

         - name: Build and push certbot image					# Certbot 이미지를 빌드하고 GHCR로 푸시
           uses: docker/build-push-action@v4
           with:
              context: .
              file: ./docker/dockerfile.certbot
              push: true
              tags: ghcr.io/${{ steps.lowercase.outputs.repository_owner }}/stay_connect_certbot:latest

         - name: Check Directory 								# 오류 발생시 확인 용
           run: ls -al

         - name: Transfer docker-compose-prod.yml to EC2 using SCP # SCP 사용하여 EC2 인스턴스로 파일 전송
           uses: appleboy/scp-action@master
           with:
              host: ${{ secrets.EC2_HOST }}
              username: ${{ secrets.EC2_USER }}
              key: ${{ secrets.EC2_SSH_KEY }}
              port: ${{ secrets.EC2_PORT }}
              source: ./docker/docker-compose-prod.yml
              target: /home/${{ secrets.EC2_USER }}

         - name: Transfer .env to EC2 using SCP
           uses: appleboy/scp-action@master
           with:
              host: ${{ secrets.EC2_HOST }}
              username: ${{ secrets.EC2_USER }}
              key: ${{ secrets.EC2_SSH_KEY }}
              port: ${{ secrets.EC2_PORT }}
              source: .env
              target: /home/${{ secrets.EC2_USER }}

         - name: Transfer init-letsencrypt.sh to EC2 using SCP
           uses: appleboy/scp-action@master
           with:
              host: ${{ secrets.EC2_HOST }}
              username: ${{ secrets.EC2_USER }}
              key: ${{ secrets.EC2_SSH_KEY }}
              port: ${{ secrets.EC2_PORT }}
              source: init-letsencrypt.sh
              target: /home/${{ secrets.EC2_USER }}

         - name: SSH and deploy to EC2								# SSH로 EC2 접속하여 배포 진행
           uses: appleboy/ssh-action@master
           with:
              host: ${{ secrets.EC2_HOST }}
              username: ${{ secrets.EC2_USER }}
              key: ${{ secrets.EC2_SSH_KEY }}
              port: ${{ secrets.EC2_PORT }}
              script: |
                 if [ -f /home/${{ secrets.EC2_USER }}/docker/docker-compose-prod.yml ]; then
                   echo "File exists"
                   chmod 644 /home/${{ secrets.EC2_USER }}/docker/docker-compose-prod.yml
                   chmod +x init-letsencrypt.sh
                 else
                   echo "File does not exist"
                   exit 1
                 fi
                 docker-compose --env-file /home/${{ secrets.EC2_USER }}/.env -f /home/${{ secrets.EC2_USER }}/docker/docker-compose-prod.yml down
                 docker stop $(docker ps -aq)
                 docker rm $(docker ps -aq)
                 docker rmi -f $(docker images -q)
                 docker-compose --env-file /home/${{ secrets.EC2_USER }}/.env -f /home/${{ secrets.EC2_USER }}/docker/docker-compose-prod.yml pull
                 docker-compose --env-file /home/${{ secrets.EC2_USER }}/.env -f /home/${{ secrets.EC2_USER }}/docker/docker-compose-prod.yml up -d

   clear:															# 만들어진 artifact들을 성공여부에 상관없이 삭제하기 위한 jobs
      runs-on: ubuntu-latest
      needs: [build, deploy]										# 모든 jobs가 실행되어야 함.
      if: always()													# 실패 여부와 상관없이 항상 실행되도록 설정.
      steps:
         - name: Delete Artifacts									# Artifact 삭제 (env-artifact는 공개되면 절대 안되기 때문에 작업이 완료되면 바로 삭제해주기 위함이다.)		
           uses: geekyeggo/delete-artifact@v5
           with:
              name: |
                 *

간단하긴 하지만 주석 설명만 봐도 워크플로우를 이해하는데 어렵지 않을것이다.

잘 동작한다면 위 이미지를 확인할 수 있다. deploy의 성공여부나 스킵과 관계없이 clear jobs가 실행완료한 모습을 확인할 수 있다.

글이 너무 길어져서 AWS관련 설명은 다음글에서 이어서 하겠다.

profile
Server Developer

0개의 댓글