[AWS] 프리티어 - Spring 프로젝트 배포 (2)

이전 게시글에서 이어지는 내용입니다.

Github Actions 설정

Repository secret 설정

Secret의 경우, deploy.yml에서 사용되는 정보들이다. 이는 유출되면 위험하기 때문에, Github secrets에서 관리를 하고, deploy.yml에서 변수 이름으로 가져와 사용하게 된다. 배포할 리포지토리의 Settings → Secrets and variables → Actions에 들어가서, 다음 항목들을 New repository secret을 눌러 입력해준다.

이름
EC2_HOST EC2 퍼블릭 IPv4 주소 (예: 13.125.xxx.xxx)
EC2_USER 보통 ec2-user (Amazon Linux), ubuntu (Ubuntu)
EC2_SSH_KEY 이전 게시글에서 만든 private key 전체 (줄바꿈 포함)
DOCKER_USERNAME Docker hub 아이디
DOCKER_PASSWORD Docker hub 비밀번호

Workflow 생성

이제 deploy.yml을 만들어주자. 이 파일을 만드는 방법은 여러 가지가 있는데, 이 중에 편한걸로 하면 된다.

yml, yaml 모두 동일한 포맷이기에 상관없다.

  • 프로젝트 내부에 .github/workflows/deploy.yml을 생성
  • Github Repository → Code → Add file → .github/workflows/deploy.yml 생성
  • Github Repository → Actions → New workflow → set up a workflow yourself → deploy.yml

결국에는 위 3가지 방법 모두 다음과 같은 위치에 파일이 생기기 때문에 뭘 하든 똑같다.

CI 구성하기

CI의 목적은 코드 변경을 자동으로 빌드 및 검증해서 배포 가능한 산출물을 만드는 것이다.

name: Deploy to EC2  

on:  
  push:  
    branches: [ master ]  

jobs:  
  build:  
    runs-on: self-hosted  

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

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

      - name: Grant execute permission for Gradle wrapper  
        run: chmod +x ./gradlew  

      - name: Build with Gradle  
        run: ./gradlew clean build  

      - name: Upload JAR artifact  
        uses: actions/upload-artifact@v4  
        with:  
          name: app-jar  
          path: build/libs/*.jar  

  dockerize:  
    runs-on: self-hosted  
    needs: build  

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

      - name: Download JAR artifact  
        uses: actions/download-artifact@v4  
        with:  
          name: app-jar  

      - name: Move JAR to expected path  
        run: |  
          mkdir -p build/libs  
          mv *.jar build/libs/  

      - name: Set up Docker Buildx  
        uses: docker/setup-buildx-action@v3  

      - name: Log in to Docker Hub  
        uses: docker/login-action@v3  
        with:  
          username: ${{ secrets.DOCKER_USERNAME }}  
          password: ${{ secrets.DOCKER_PASSWORD }}  

      - name: Build and push Docker image  
        uses: docker/build-push-action@v5  
        with:  
          context: .  
          push: true  
          tags: ${{ secrets.DOCKER_USERNAME }}/hits-backend:latest  
          platforms: linux/amd64,linux/arm64  

  deploy:  
    runs-on: self-hosted  
    needs: dockerize  

    steps:  
      - name: Setup SSH  
        uses: webfactory/ssh-agent@v0.7.0  
        with:  
          ssh-private-key: ${{ secrets.EC2_SSH_KEY }}  

      - name: Deploy to EC2  
        run: ssh -v -o StrictHostKeyChecking=no ${{ secrets.EC2_USER }}@${{ secrets.EC2_HOST }} './deploy.sh'

CD 구성하기

CD의 목적은 CI에서 만든 산출물을 환경(테스트/스테이징/프로덕션)에 실제 배포하는 것이다.

  deploy:  
    runs-on: self-hosted  
    needs: dockerize  

    steps:  
      - name: Setup SSH  
        uses: webfactory/ssh-agent@v0.7.0  
        with:  
          ssh-private-key: ${{ secrets.EC2_SSH_KEY }}  

      - name: Deploy to EC2  
        run: ssh -v -o StrictHostKeyChecking=no ${{ secrets.EC2_USER }}@${{ secrets.EC2_HOST }} './deploy.sh'

최종 파일

name: Deploy to EC2  

on:  
  push:  
    branches: [ master ]  

jobs:  
  build:  
    runs-on: self-hosted  

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

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

      - name: Grant execute permission for Gradle wrapper  
        run: chmod +x ./gradlew  

      - name: Build with Gradle  
        run: ./gradlew clean build  

      - name: Upload JAR artifact  
        uses: actions/upload-artifact@v4  
        with:  
          name: app-jar  
          path: build/libs/*.jar  

  dockerize:  
    runs-on: self-hosted  
    needs: build  

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

      - name: Download JAR artifact  
        uses: actions/download-artifact@v4  
        with:  
          name: app-jar  

      - name: Move JAR to expected path  
        run: |  
          mkdir -p build/libs  
          mv *.jar build/libs/  

      - name: Set up Docker Buildx  
        uses: docker/setup-buildx-action@v3  

      - name: Log in to Docker Hub  
        uses: docker/login-action@v3  
        with:  
          username: ${{ secrets.DOCKER_USERNAME }}  
          password: ${{ secrets.DOCKER_PASSWORD }}  

      - name: Build and push Docker image  
        uses: docker/build-push-action@v5  
        with:  
          context: .  
          push: true  
          tags: ${{ secrets.DOCKER_USERNAME }}/hits-backend:latest  
          platforms: linux/amd64,linux/arm64  

  deploy:  
    runs-on: self-hosted  
    needs: dockerize  

    steps:  
      - name: Setup SSH  
        uses: webfactory/ssh-agent@v0.7.0  
        with:  
          ssh-private-key: ${{ secrets.EC2_SSH_KEY }}  

      - name: Deploy to EC2  
        run: ssh -v -o StrictHostKeyChecking=no ${{ secrets.EC2_USER }}@${{ secrets.EC2_HOST }} './deploy.sh'

EC2 배포 스크립트 생성

배포 스크립트 생성

CI를 통해 만들어진 산출물을 CD 과정에서 EC2에서 실행할 수 있도록 Deploy to EC2 하는 부분이 있다. 이 때, ./deploy.sh를 실행하기 때문에, EC2에 만들어주자.

vim ~/deploy.sh

deploy.sh 파일명을 다른 것으로 할 경우, 추후에 생성할 deploy.yml 파일에는 수정사항을 반영해야 한다.

Docker Hub 관련 변수 디렉토리 경로 및 branch는 본인 환경에 맞게 설정하면 된다.

${docker-usename}/${docker-project-name}는 Docker Hub 아이디 및 프로젝트 이름과 Docker에 표시될 컨테이너 이름을 입력해주면 된다.

#!/bin/bash

IMAGE_NAME=${docker-usename}/${docker-project-name}
CONTAINER_NAME=${docker-container-name}

echo "Stopping existing container..."
docker stop $CONTAINER_NAME || true
docker rm $CONTAINER_NAME || true

echo "Pulling latest image..."
docker pull $IMAGE_NAME:latest

echo "Starting new container..."
docker run -d \
  --name $CONTAINER_NAME \
  -p 8080:8080 \
  --restart always \
  $IMAGE_NAME:latest

예를 들면 다음과 같이 설정하면 된다.

#!/bin/bash

IMAGE_NAME=jwhy/service-backend
CONTAINER_NAME=service

echo "Stopping existing container..."
docker stop $CONTAINER_NAME || true
docker rm $CONTAINER_NAME || true

echo "Pulling latest image..."
docker pull $IMAGE_NAME:latest

echo "Starting new container..."
docker run -d \
  --name $CONTAINER_NAME \
  -p 8080:8080 \
  --restart always \
  $IMAGE_NAME:latest

마무리

이 과정까지 했다면, master 혹은 main 브랜치에 push를 하거나, Pull Request가 merge 되었을 경우, actions가 잘 동작할 것이다. 만약 actions가 잘 동작하지 않는다면, self-hosted를 사용해보는 것도 좋다.

 

New self-hosted runner를 눌러서 나오는대로 하면 된다. 인텔 맥의 경우 x64를 사용하면 되고, Apple silicon의 경우 ARM64를 선택하면 된다.