이 글은 인프런의 지식 공유자 박재성님의 강의를 듣고 개인적으로 정리하는 글임을 알립니다.


CI/CD는 소프트웨어 개발에서 중요한 개념 중 하나로, “Continuous Integration(지속적 통합)“과 “Continuous Deployment(지속적 배포)” 또는 “Continuous Delivery(지속적 전달)“를 뜻한다.

이 개념은 개발자가 더 효율적으로 코드를 작성하고, 빠르고 안정적으로 사용자에게 소프트웨어를 제공하는 것을 목표로 한다.

 

GitHub Actions는 자동화된 워크플로우를 지원하는 강력한 도구로, 저장소의 빌드, 테스트, 배포 등의 작업을 자동화할 수 있다.

즉, GitHub Actions는 CI/CD 과정에서 빌드, 테스트, 배포에 대한 로직을 실행시키는 서버(컴퓨터)의 역할을 한다.

개발자가 코드를 작성 후 커밋 & 푸시를 하는 순간 GitHub Actions는 빌드 및 테스트를 하고 EC2에 배포까지 자동화 할 수 있다.

물론 테스트 코드에서 오류가 난다면 배포가 중단된다.(서비스가 중단되는 것은 아니다.)

 

기본 문법

GitHub Actions에서 가장 중요한 파일은 deploy.yml인데, 이 파일의 위치는 git으로 관리되는 최상위 루트의 .github/workflows/ 폴더에 저장되어 있어야 한다.

-> 최상위루트/.github/workflows/deploy.yml

# Workflow의 이름
# Workflow : 하나의 yml 파일을 하나의 Workflow라고 부른다. 
name: Github Actions 실행시켜보기

# Event : 실행되는 시점을 설정
# main이라는 브랜치에 push 될 때 아래 Workflow를 실행
on:
  push:
    branches:
      - main

# 하나의 Workflow는 1개 이상의 Job으로 구성된다. 
# 여러 Job은 기본적으로 병렬적으로 수행된다.
jobs: 
  # Job을 식별하기 위한 id
  My-Deploy-Job: 
	  # Github Actions를 실행시킬 서버 종류 선택
    runs-on: ubuntu-latest
    
    # Step : 특정 작업을 수행하는 가장 작은 단위
    # Job은 여러 Step들로 구성되어 있다.
    steps: 
      - name: Hello World 찍기 # Step에 이름 붙이는 기능
        run: echo "Hello World" # 실행시킬 명령어 작성
        
      - name: 여러 명령어 문장 작성하기
        run: |
          echo "Good"
          echo "Morning"
          

			# 참고: https://docs.github.com/en/actions/learn-github-actions/variables
      - name: Github Actions 자체에 저장되어 있는 변수 사용해보기
        run: |
          echo $GITHUB_SHA
          echo $GITHUB_REPOSITORY

			- name: Github Actions Secret 변수 사용해보기
				run: |
					echo ${{ secrets.MY_NAME }}
					echo ${{ secrets.MY_HOBBY }}

 

시크릿 변수는 아래와 같이 지정할 수 있다.

 

 

스프링부트 배포 자동화

EC2에서 빌드하는 방식

이 방식은 깃허브에 저장된 소스코드를 PULL 하고 EC2에서 빌드하는 방식이다.

빌드는 컴퓨터의 자원을 많이 소모하는 작업이다.

 


장점

  • git pull을 활용해서 변경된 부분의 프로젝트 코드에 대해서만 업데이트 하기 때문에 CI/CD 속도가 빠르다.
  • CI/CD 툴로 Github Actions만 사용하기 때문에 인프라 구조가 복잡하지 않고 간단하다.

 

단점

  • 빌드 작업을 EC2에서 직접 진행하기 때문에 운영하고 있는 서버의 성능에 영향을 미칠 수 있다.
  • Github 계정 정보가 해당 EC2에 저장되기 때문에 개인 프로젝트 또는 믿을만한 사람들과 같이 진행하는 토이 프로젝트에서만 사용해야 한다.

 

이 방법은 주로 개인 프로젝트에서 CI/CD를 심플하고 빠르게 적용시키고 싶을 때 적용한다.

 

name: Deploy To EC2

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: SSH로 EC2에 접속하기
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.EC2_HOST }} # EC2의 주소
          username: ${{ secrets.EC2_USERNAME }} # EC2 접속 username
          key: ${{ secrets.EC2_PRIVATE_KEY }} # EC2접속을 위한 pem 파일 내부 정보
          script_stop: true # 아래 script 중 실패하는 명령이 하나라도 있으면 실패로 처리
          script: |
            cd /home/ubuntu/instagram-server # 여기 경로는 자신의 EC2에 맞는 경로로 재작성하기
            git pull origin main
            ./gradlew clean build
            sudo fuser -k -n tcp 8080 || true # || true를 붙인 이유는 8080에 종료시킬 프로세스가 없더라도 실패로 처리하지 않기 위해서이다. 
            # jar 파일을 실행시키는 명령어이다. 그리고 발생하는 로그들을 ./output.log 파일에 남기는 명령어이다.
            nohup java -jar build/libs/*SNAPSHOT.jar > ./output.log 2>&1 &

이렇게 설정하면 output.log를 통해서 로그 데이터를 읽을 수 있다.

 

 

민감한 값을 따로 application.yml로 분리하는 경우가 많다. 민감한 값이기에 .gitignore에 추가해서 application.yml가 버전관리 되지 않게 세팅한다.

.gitignore

...

application.yml

 

deploy.yml 수정

name: Deploy To EC2

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: SSH로 EC2에 접속하기
        uses: appleboy/ssh-action@v1.0.3
        env:
          APPLICATION_PROPERTIES: ${{ secrets.APPLICATION_PROPERTIES }}
        with:
          host: ${{ secrets.EC2_HOST }} # EC2의 주소
          username: ${{ secrets.EC2_USERNAME }} # EC2 접속 username
          key: ${{ secrets.EC2_PRIVATE_KEY }} # EC2의 Key 파일의 내부 텍스트
          envs: APPLICATION_PROPERTIES
          script_stop: true # 아래 script 중 실패하는 명령이 하나라도 있으면 실패로 처리
          script: |
            cd /home/ubuntu/instagram-server # 여기 경로는 자신의 EC2에 맞는 경로로 재작성하기
            rm -rf src/main/resources/application.yml
            git pull origin main
            echo "$APPLICATION_PROPERTIES" > src/main/resources/application.yml
            ./gradlew clean build
            sudo fuser -k -n tcp 8080 || true # || true를 붙인 이유는 8080에 종료시킬 프로세스가 없더라도 실패로 처리하지 않기 위해서이다. 
            nohup java -jar build/libs/*SNAPSHOT.jar > ./output.log 2>&1 &

이렇게 하면 application.yml이 git에 추적되지 않고, 배포를 자동화 할 수 있다.

 

GitHub Actions에서 빌드하는 방식

이 방식은 빌드를 GitHub Actions에서 하는 방식이다.

 

장점

  • 빌드 작업을 Github Actions에서 하기 때문에 운영하고 있는 서버의 성능에 영향을 거의 주지 않는다.
  • CI/CD 툴로 Github Actions만 사용하기 때문에 인프라 구조가 복잡하지 않고 간단하다.

 

단점

  • 무중단 배포를 구현하거나 여러 EC2 인스턴스에 배포를 해야 하는 상황이라면, 직접 Github Actions에 스크립트를 작성해서 구현해야 한다.
  • 꽤 복잡하다. 

 

name: Deploy To EC2

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Github Repository 파일 불러오기
        uses: actions/checkout@v4

      - name: JDK 17버전 설치
        uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: 17

      - name: application.yml 파일 만들기
        run: echo "${{ secrets.APPLICATION_PROPERTIES }}" > ./src/main/resources/application.yml

      - name: 테스트 및 빌드하기
        run: ./gradlew clean build

      - name: 빌드된 파일 이름 변경하기
        run: mv ./build/libs/*SNAPSHOT.jar ./project.jar

      - name: SCP로 EC2에 빌드된 파일 전송하기
        uses: appleboy/scp-action@v0.1.7
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ${{ secrets.EC2_USERNAME }}
          key: ${{ secrets.EC2_PRIVATE_KEY }}
          source: project.jar
          target: /home/ubuntu/instagram-server/tobe

      - name: SSH로 EC2에 접속하기
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ${{ secrets.EC2_USERNAME }}
          key: ${{ secrets.EC2_PRIVATE_KEY }}
          script_stop: true
          script: |
             rm -rf /home/ubuntu/instagram-server/current
             mkdir /home/ubuntu/instagram-server/current
             mv /home/ubuntu/instagram-server/tobe/project.jar /home/ubuntu/instagram-server/current/project.jar
             cd /home/ubuntu/instagram-server/current
             sudo fuser -k -n tcp 8080 || true
             nohup java -jar project.jar > ./output.log 2>&1 & 
             rm -rf /home/ubuntu/instagram-server/tobe

테스트 및 빌드

  • ./gradlew clean build 명령어를 사용해 Gradle로 프로젝트를 빌드하고, 테스트한다.

 

빌드된 파일 이름 변경

  • 빌드된 JAR 파일의 이름에 SNAPSHOT이 포함된 경우, 이 파일을 project.jar로 이름을 변경한다.

 

SCP로 EC2에 빌드된 파일 전송

  • appleboy/scp-action@v0.1.7 액션을 사용해 EC2 인스턴스로 project.jar 파일을 전송한다.
  • host, username, key는 GitHub Secrets에 저장된 값들을 사용하여 SSH 접속을 설정한다.
  • source는 전송할 파일(project.jar), target은 EC2 인스턴스의 경로이다.

 

SSH로 EC2에 접속하여 명령어 실행

appleboy/ssh-action@v1.0.3 액션을 사용해 SSH로 EC2에 접속한 후, 다음 작업을 수행한다

  1. 현재 실행 중인 서버 파일 삭제: /home/ubuntu/instagram-server/current 디렉터리를 제거하고 새로 생성한다.
  2. 파일 이동: SCP로 전송된 파일을 /tobe 디렉터리에서 /current 디렉터리로 이동시킨다.
  3. 현재 실행 중인 프로세스 종료: 현재 8080 포트에서 실행 중인 프로세스를 강제로 종료한다.
  4. 새로 배포한 파일 실행: nohup 명령어를 사용해 백그라운드에서 project.jar을 실행한다.
  5. 배포 완료 후 불필요한 파일 삭제: /tobe 디렉터리를 삭제한다.

 

Docker를 이용한 컨테이너 기반 CI/CD(중단 후 배포)

이 방식은 Docker 이미지를 빌드하고 Docker Hub에 푸시한 후, 원격 EC2 서버에서 Docker Compose를 이용해 배포하는 방식이다.

 

deploy.yml

name: CI/CD for test

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout the code
      uses: actions/checkout@v3

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

    - name: Create application.yml from secrets
      run: echo "${{ secrets.R2R_APPLICATION_YML }}" > ./SpringBoot/src/main/resources/application.yml

    - name: Build with Gradle
      run: ./gradlew clean build
      working-directory: ./SpringBoot  # SpringBoot 디렉터리에서 빌드 실행

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

    - name: Log in to DockerHub
      run: echo "${{ secrets.DOCKERHUB_TOKEN }}" | docker login -u "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin

    # SpringBoot 애플리케이션 이미지 빌드 및 푸시
    - name: Build and push test image
      run: |
        docker build -t <도커 허브 계정>/test ./SpringBoot/
        docker push <도커 허브 계정>/test

    # MySQL 이미지 빌드 및 푸시
    - name: Build and push MySQL image
      run: |
        docker build -t <도커 허브 계정>/test-db ./mysql/
        docker push <도커 허브 계정>/test-db

  deploy:
    runs-on: ubuntu-latest
    needs: build

    steps:
    - name: SSH to EC2 and Deploy
      uses: appleboy/ssh-action@v1.0.3
      with:
        host: ${{ secrets.R2R_EC2_HOST }}
        username: ${{ secrets.R2R_EC2_USERNAME }}
        key: ${{ secrets.R2R_EC2_PRIVATE_KEY }}
        script: |
          cd /home/ubuntu/test
          docker compose down
          docker compose pull
          docker compose up -d

 

compose.yml

services:
  <서비스 이름>:
    image: <도커 허브 계정>/<이미지 이름>
    platform: linux/amd64
    ports:
      - 80:8080
    depends_on:
      <앱이름-db>:
        condition: service_healthy
    networks:
      - <네트워크 이름>

  <앱이름-db>:
    image: <도커 허브 계정>/<이미지 이름>
    platform: linux/amd64
    environment:
      MYSQL_ROOT_PASSWORD: <루트 패스워드>
      MYSQL_DATABASE: <데이터베이스 이름>
      MYSQL_USER: <유저 이름>
      MYSQL_PASSWORD: <유저 패스워드>
    volumes:
      - <볼륨 이름>:/var/lib/mysql
    ports:
      - 3306:3306
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      retries: 10
    networks:
      - <네트워크 이름>

networks:
  <네트워크 이름>:

volumes:
  <볼륨 이름>: