본문 바로가기
공부/Spring

[Spring] 깃허브 액션으로 CI/CD 구축해보기 (스프링 부트)

by 웅대 2023. 7. 2.
728x90
반응형

CI/CD는 Continuous Integration/Continuous Delivery의 준말로 지속적인 통합과 지속적인 전달을 의미한다.

 

지속적인 통합(CI)은 지속적으로 품질 관리를 적용하는 프로세스를 실행시키는 것이다.

 

작은 단위의 작업을 지속적으로 실행하는데 대표적으로 빌드 및 테스트 과정에 적용한다.

 

지속적인 전달(CD)는 

소프트웨어가 항상 신뢰가능한 수준으로 출시될 수 있도록 해준다.

 

대표적으로 배포 자동화에 적용한다.

 

프로젝트를 진행하다보면 중간중간 변경 사항이 있을테고 이 변경 사항을 배포해야 할 것이다.

 

여기에 CI/CD를 적용하면 개발자는 개발에 집중할 수 있고 생산성을 높일 수 있다.

 

CI/CD 툴에는 젠킨스, 깃허브 액션, 깃랩 등등 다양한 종류가 있는데 이 중 깃허브 액션을 사용해보려고 한다.

 

깃허브 액션 기본 개념

1. workflow

workflow는 하나 이상의 Job을 실행하는 자동화 프로세스이다.

 

레포지토리의 YAML 파일에 의해서 정의되고 특정한 이벤트가 발생하면 실행되도록 할 수 있다.

 

2. Event

workflow 레포지토리에서 발생하는 활동으로 workflow를 실행하도록 할 수 있다.

 

예를 들어 푸쉬가 발생할 때 특정 workflow를 실행하도록 할 수 있다.

 

3. Job

workflow의 일련의 단계를 의미한다.

 

각 Job은 동일한 Runner에서 실행되며 기본적으로 서로 종속성이 없기 때문에 병렬적으로 수행되나 종속성을 구성하여 차례대로 실행하도록 할 수 있다.

 

4. Action

Action은 커스텀 애플리케이션으로 깃허브 액션 플랫폼에서 가져올 수 있다.

 

미리 정의된 유용한 action들이 많기 때문에 원하는 것을 가져오면 생산성이 향상될 수 있다.

 

5.Runner

Runner는 workflow가 트리거되었을 때 동작하는 서버이다.

 

일종의 VM이라 볼 수도 있다.

 

각각의 Runner는 한 번에 하나의 Job을 실행할 수 있다.

https://docs.github.com/ko/actions/using-workflows/about-workflows

 

빌드 및 테스트 자동화 구축 (CI 구축)

먼저 간단한 기능과 테스트 코드를 가지고 있는 스프링 프로젝트를 생성하고 깃허브 레포지토리와 연동한다.

 

그리고 .github 디렉토리를 만들고 그 안에 workflow 디렉토리를 만든 후 그 안에 yaml 파일을 만들어서 원하는 workflow를 생성할 수 있다.

 

직접 만들어도 되지만 다른 사람들이 만든 workflow를 사용해도 좋다.

 

깃허브 상단의 Actions에서 new workflow를 통해 workflow를 가져올 수 있다.

본인 프로젝트에 적절한 workflow를 선택한다.

그러면 다음과 같이 workflow를 추가할 수 있다.

Java With Gradle에서 자바 버전만 바꿨다. 나는 17버전을 사용했다.

 

<./github/workflows/gradle.yml>

# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle

name: Java CI with Gradle

on:
  push:
    branches: [ "master" ]
  pull_request:
    branches: [ "master" ]

permissions:
  contents: read

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3
    - name: Set up JDK 17
      uses: actions/setup-java@v3
      with:
        java-version: '17'
        distribution: 'temurin'
    - name: Run chmod to make gradlew executable
      run: chmod +x ./gradlew
    - name: Build with Gradle
      uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1
      with:
        arguments: build

이제 바뀐 내용을 가지고 커밋을 해보면 성공한 모습을 확인할 수 있다.

완료된 workflow를 클릭하면 자세한 내용을 볼 수 있다. (진행 중일 때도 확인이 가능하다.)

 

이제 한번 테스트 코드에서 에러가 나게 바꾸고 다시 푸쉬를 해보았다.

 

오류가 발생했고 해당 workflow를 자세히 보면 테스트에서 오류가 났다는 메시지를 확인할 수 있다.

이제 커밋을 푸쉬할 때마다 빌드 및 테스트 자동화에 성공했다.

 

배포 자동화 구축 (CD 구축)

이어서 깃허브 액션과 ssh를 활용하여 EC2 인스턴스에 jar 파일을 배포해보려고 한다.

 

EC2 인스턴스는 public subnet에 생성하고 ssh 방식으로 접근하도록 설정했다.

 

우선 로컬에서 rsa 키 쌍을 만든다. 보안을 위해 로컬에 비밀키를 보관하고 원격에는 공개키를 넣어준다.

 

<키 생성>

ssh-keygen -t rsa -b 4096 -C "useremail@example.com"

RSA 알고리즘을 사용하고 4096비트로 설정한다는 뜻이다.

 

<public key 원격 서버에 붙여넣기>

cat ./Users/growth/.ssh/id_rsa.pub | ssh -i "키페어" 사용자 이름@퍼블릭 DNS 주소 'cat >> .ssh/authorized_keys'

우선 cat .ssh/id_rsa.pub을 통해 public key를 가져와서 ssh에 접속한 후 cat >> .ssh/authorized_keys 명령어에 입력으로 넣어준다.

 

즉 로컬에 있는 public key를 원격 서버의 .ssh 디렉토리 안의 authorized_keys에 넣어준다는 뜻이다.

 

이제 로컬에서 생성한 private key를 github actions에 등록해줘야 한다.

 

우선 다음 명령어를 통해 private key를 클립보드로 복사해준다.

clip < .ssh/id_rsa

그리고 이 값을 github actions에 등록해준다.

프로젝트 상단의 settings -> Secrets and variables -> Actions에서 New repository secret을 눌러준다.

 

그 다음 Name을 PRIVATE_KEY로 하고 Secret에 복사한 private key 값을 붙여주고 추가한다.

추가로 USERNAME에는 사용자 이름, HOST에는 public IP 주소, PORT에는 22를 입력해준다.

 

참고로 EC2 인스턴스의 22번 포트를 열어줘야 한다.

 

secrets 다음 4가지가 등록되어야 한다.

 

이제 CI를 구축할 때 사용했던 workflow에 다음 내용을 추가해주면 된다.

- name: executing remote ssh commands using ssh key
  uses: appleboy/ssh-action@v0.1.10
  with:
    host: ${{ secrets.HOST }}
    username: ${{ secrets.USERNAME }}
    key: ${{ secrets.PRIVATE_KEY }}
    port: ${{ secrets.PORT }}
    script: |
      whoami
      ls -al

 

<최종 gradle.yml 코드>

# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle

name: Java CI with Gradle

on:
  push:
    branches: [ "master" ]
  pull_request:
    branches: [ "master" ]

permissions:
  contents: read

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3
    - name: Set up JDK 17
      uses: actions/setup-java@v3
      with:
        java-version: '17'
        distribution: 'temurin'
    - name: Run chmod to make gradlew executable
      run: chmod +x ./gradlew
    - name: Build with Gradle
      uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1
      with:
        arguments: build
    - name: executing remote ssh commands using ssh key
      uses: appleboy/ssh-action@v0.1.10
      with:
        host: ${{ secrets.HOST }}
        username: ${{ secrets.USERNAME }}
        key: ${{ secrets.PRIVATE_KEY }}
        port: ${{ secrets.PORT }}
        script: |
          whoami
          ls -al

이제 커밋을 하면 가장 아래 script에 존재하는 whoami 명령어와 ls -al 명령어가 수행되어어 한다.

명령어가 잘 실행되었다. ssh-action 사용법을 배웠으므로 이를 이용하여 배포를 진행하면 된다.

 

이제 배포 과정을 진행해보려고 한다. 진행 과정은 다음과 같다. 

  1. CI를 마치고 생성된 jar 파일을 scp 명령어로 /home/ec2-user/cicd 디렉토리로 복사한다.
  2. 로컬에 존재하는 deploy.sh 스크립트 파일을 /home/ec2-user/cicd로 복사한다.
  3. deploy.sh를 실행한다. (아래는 deploy.sh 진행 과정)
  4. 실행 중인 jar 파일의 pid를 가져온다.
  5. 만약 실행 중인 jar 파일이 있다면 해당 프로세스를 죽인다.
  6. /home/ec2-user/cicd 안에 있는 jar 파일의 전체 경로를 가져온다.
  7. nohup 명령어를 통해 백그라운드로 실행한다.

진행 전에 EC2에 openjdk와 /home/ec2-user 디렉토리 아래에 cicd 디렉토리를 생성한다.

 

이 cicd 디렉토리 안에 jar 파일, script 파일, nohup.out 파일이 생성될 것이다.

 

<배포 과정>

      - name: Copy jar file to remote
        uses: appleboy/scp-action@master
        with:
          username: ec2-user
          host: ${{ secrets.HOST }}
          key: ${{ secrets.PRIVATE_KEY }}
          source: "./build/libs/*.jar"
          target: "/home/ec2-user/cicd"
          strip_components: 2
      - name: Copy deploy script file to remote
        uses: appleboy/scp-action@master
        with:
          username: ec2-user
          host: ${{ secrets.HOST }}
          key: ${{ secrets.PRIVATE_KEY }}
          source: "deploy.sh"
          target: "/home/ec2-user/cicd"
      - name: Execute deploy script
        uses: appleboy/ssh-action@master
        with:
          username: ec2-user
          host: ${{ secrets.HOST }}
          key: ${{ secrets.PRIVATE_KEY }}
          script_stop: true
          script: |
            chmod +x /home/ec2-user/cicd/deploy.sh
            sh /home/ec2-user/cicd/deploy.sh

 

<최종 코드>

name: Java CI with Gradle2141352

on:
  push:
    branches: [ "master" ]
  pull_request:
    branches: [ "master" ]

permissions:
  contents: read

jobs:
  build2:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'temurin'
      - name: Run chmod to make gradlew executable
        run: chmod +x ./gradlew
      - name: Build with Gradle
        uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1
        with:
          arguments: build
      - name: Upload artifact
        uses: actions/upload-artifact@v2
        with:
          name: cicdsample
          path: build/libs/*.jar

      - name: Copy jar file to remote
        uses: appleboy/scp-action@master
        with:
          username: ec2-user
          host: ${{ secrets.HOST }}
          key: ${{ secrets.PRIVATE_KEY }}
          source: "./build/libs/*.jar"
          target: "/home/ec2-user/cicd"
          strip_components: 2
      - name: Copy deploy script file to remote
        uses: appleboy/scp-action@master
        with:
          username: ec2-user
          host: ${{ secrets.HOST }}
          key: ${{ secrets.PRIVATE_KEY }}
          source: "deploy.sh"
          target: "/home/ec2-user/cicd"
      - name: Execute deploy script
        uses: appleboy/ssh-action@master
        with:
          username: ec2-user
          host: ${{ secrets.HOST }}
          key: ${{ secrets.PRIVATE_KEY }}
          script_stop: true
          script: |
            chmod +x /home/ec2-user/cicd/deploy.sh
            sh /home/ec2-user/cicd/deploy.sh

다음은 deploy.sh이다. 실질적으로 배포를 진행하는 코드이다.

 

<deploy.sh>

#!/bin/bash
 CURRENT_PID=$(pgrep -f .jar)
 echo "$CURRENT_PID"
 if [ -z $CURRENT_PID ]; then
         echo "no process"
 else
         echo "kill $CURRENT_PID"
         kill -9 $CURRENT_PID
         sleep 3
 fi

 JAR_PATH="/home/ec2-user/cicd/*.jar"
 echo "jar path : $JAR_PATH"
 chmod +x $JAR_PATH
 nohup java -jar $JAR_PATH > /dev/null 2> /dev/null < /dev/null &
 echo "jar fild deploy success"

급히 작성한 스크립트 파일이라 좋은 코드는 아니다.

 

적절히 변형해서 사용하면 좋을 것 같다.

 

이제 변경 사항을 커밋을 해보고 배포에 반영이 되는지 확인하자.

 

나는 "/hello" 경로로 GET 요청을 보내면 "hello ci-cd!"라는 메시지를 출력하도록 하였다.

잘 출력이 되는 모습을 확인할 수 있다.

 

이제 복잡한 배폭 과정을 거칠 필요가 없다. 깃허브에 푸쉬만 하면 자동으로 테스트코드를 실행하고 배포까지 진행한다.  

 

다음에는 AWS code deploy를 사용하여 배포 자동화를 구현해보려고 한다.

 

 

참고

https://ko.wikipedia.org/wiki/CI/CD

https://docs.github.com/ko/actions/learn-github-actions/understanding-github-actions

https://www.youtube.com/watch?v=iLqGzEkusIw 

https://dev.to/tarohida/error-on-appleboyssh-action-dial-tcp-lookup-exampleexample-on-19202153-io-timeout-6ng

https://github.com/appleboy/ssh-action

 

728x90
반응형

댓글