본문 바로가기
개발 잡담

CI/CD ) Github Actions 로 CI/CD 해보기

by 휴일이 2024. 7. 18.

 

 

원래 회사에서

CI -> Jenkins

CD -> Ansible

사용하도록 구축해놨었는데

 

Jenkins 를 사용할 컴퓨팅 리소스가 부족해서..

막상 운영 서버에서는

 

1. 직접 빌드 -> 아티팩트 파일질라로 직접 옮기기

3. 배포는 그래도 ansible-playbook 이용 (휴)

- 그러나 나 말고는.. 종종 그냥 도커 명령어를 직접 사용하기도 하는듯했다.

- ansible-playbook 명령어가 익숙치 않았던듯..

 

 

그래서 github actions 로 기술 변경을 건의해보기로 맘먹었다.

 

github actions 를 사용하는 수많은 이유가 있겠지만 일단 우리가 사용해야하는 이유는..

1. Jenkins 리소스가 부족해서 운영 서버에서는 사용 못함..

- 물론 현재 Test 서버라고 올려져있는 서버에 Jenkins 를 올려놓고 빌드만 여기에서 해도 되지만

- 우린 테스트서버가 없어질 위기에 쳐해진 적이 있어서 빌드 서버를 따로 둔다는 것이 쫌 위험한 것 같음(리소스 상당히 부족한 편,,)

2. 깃허브 액션은 무료 계정도 500MB / 2000분까지는 무료임.

 

그리고 배포 자체도 ansible 이 아니라 github actions 에서 직접 커맨드를 치는 것이 낫다고 판단했음. 왜냐하면..

1. 어차피 깃 액션으로 ssh 접속해서 직접(?) 커맨드 치는 거랑 ansible 설정 파일로 playbook 실행 시키는 거랑 차이가 크게 없을 거 같음
2. Ansible 로 배포하게되면 버전 관리할 때 ansible 설정 파일을 수정하든, 환경 변수를 설정하든 하는 작업을 해줘야하는데 직접 명령어를 쓰면 그런 작업을 안 해도 되어서 작업이 하나 줄어듬
3. 새로운 서버에 작업을 하더라도 ansible 설정을 따로 안 해줘도 됨!! 내가 편해짐..ㅎㅎ (중요)
4. 구성원들이 ansible 에 대한 이해도가 없어서 차라리 gitActions.yml 파일에 직접 커맨드를 써놓으면 이해하기 더 쉬울 것 같음

- 말했듯이 ansible-playbook 쓰라고 알려드렸는데도 도커 명령어를 직접 사용하고 계셔서..

 

 

뭐 어쨌든 편리한 점을 알았으니

건의해보려면 직접 사용해봐야하지 않겠는가?

GitActions 로 더미 프로젝트를 CI/CD 했던 과정을 간단히 적어보려고 한다.

 

실전

  1. 연습용 레포 만들고 레포 기본 페이지로 이동
  2. Actions 클릭

  1. 내 프로젝트에 맞는 설정 클릭, 난 Java with Gradle
  2. 이렇게 기본 구성이 나온다.
# 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" ]

jobs:
  build:

    runs-on: ubuntu-latest
    permissions:
      contents: read

    steps:
    - uses: actions/checkout@v4
    - name: Set up JDK 17
      uses: actions/setup-java@v4
      with:
        java-version: '17'
        # 궁금해서 찾아봤는데 JDK 이미지가 저장된 저장소 이름임..
        distribution: 'temurin'

    # Configure Gradle for optimal use in GitHub Actions, including caching of downloaded dependencies.
    # See: <https://github.com/gradle/actions/blob/main/setup-gradle/README.md>
    - name: Setup Gradle
      uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0

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

    # NOTE: The Gradle Wrapper is the default and recommended way to run Gradle (<https://docs.gradle.org/current/userguide/gradle_wrapper.html>).
    # If your project does not have the Gradle Wrapper configured, you can use the following configuration to run Gradle with a specified version.
    #
    # - name: Setup Gradle
    #   uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0
    #   with:
    #     gradle-version: '8.5'
    #
    # - name: Build with Gradle 8.5
    #   run: gradle build

  dependency-submission:

    runs-on: ubuntu-latest
    permissions:
      contents: write

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

    # Generates and submits a dependency graph, enabling Dependabot Alerts for all project dependencies.
    # See: <https://github.com/gradle/actions/blob/main/dependency-submission/README.md>
    - name: Generate and submit dependency graph
      uses: gradle/actions/dependency-submission@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0

push 했따 !!

 

오 뭔가 생김

 

 

ㄷㄷㄷ 러너로 빌드 성공 ㄷㄷㄷ

 

 

워크플로 데이터를 아티팩트로 업로드

빌드가 성공했다면 이제 빌드한 아티팩트를 업로드해야겠지 ! → 그래야 Deploy 하지 !

      # 아티팩트 업로드
      - name: Upload build artifacts
        uses: actions/upload-artifact@v4
        with:
          name: artifact
          path: build/libs/*.jar
  • build 단계에서 아티팩트를 업로드해주었다.
  • 만약 도커 레지스트리를 사용한다면 도커 허브로 바로 이미지를 빌드해서 업로드해도 되지 않나 하는 생각은 들었는데
  • 생각만 했고 당장 레지스트리 사용이 어려우니 아티팩트를 업로드해준다.

배포 단계

  deploy:
    # 병렬 작업 ㄴㄴ 직렬로 build 작업이 끝나야지만 배포하고 싶음.
    needs: build

    runs-on: ubuntu-latest
    # 쓰기 작업 허용
    permissions:
      contents: write

    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      # 아까 업로드했던 아티팩트 다운로드해줌.
      - name: Download build artifacts
        uses: actions/download-artifact@v4
        with:
          path: build/libs
      - run: ls -R build/libs

      # 아티팩트 EC2 로 업로드하기
      - name: Upload to EC2
        env:
          SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
          EC2_USER: ${{ secrets.EC2_USER }}
          EC2_HOST: ${{ secrets.EC2_HOST }}
        run: |
          mkdir -p ~/.ssh
          echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
          chmod 600 ~/.ssh/id_rsa
          scp -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa build/libs/artifact/*.jar $EC2_USER@$EC2_HOST:~/my-project

      # 버전 만들어주기
      - name: Extract version
        env:
          MAIN_VERSION: v1.0.
          # GITHUB_RUN_NUMBER -> 리포지토리에 있는 특정 워크플로의 실행마다 고유한 숫자입니다. 이 숫자는 워크플로의 첫 실행 시 1부터 시작하며 새 실행마다 증가합니다. 워크플로 실행을 다시 실행하는 경우 이 숫자는 변경되지 않습니다.(예시: 3)
          GITHUB_RUN_NUMBER: ${{ github.run_number }}
        run: echo "VERSION=$MAIN_VERSION$GITHUB_RUN_NUMBER" >> $GITHUB_ENV

      # 이미지 빌드 & 컨테이너 배포
      - name: Deploy Artifact
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ${{ secrets.EC2_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          port: ${{ secrets.SSH_PORT }}
          script: |
            process_started_at=$(date +%s)
        
            echo "Start Build Docker Image"
            docker build -t cicd_app:${{ env.VERSION }} ~/my-project/.
            build_finished_at=$(date +%s)
            
            docker stop cicd_app

            docker run -d --name cicd_app -p 8000:8000 cicd_app:${{ env.VERSION }}
            
            process_finished_at=$(date +%s)
            image_build_time=$((build_finished_at - process_started_at))
            process_total_time=$((process_finished_at - process_started_at))
            
            echo "Docker build Complete in ${image_build_time} seconds."
            echo "Docker Process Complete in ${process_total_time} seconds."

  • 아래에 묶어서 설명하겠다. 꽤 애먹은 파트가 있어서 중요

아티팩트 업로드, 다운로드, scp (ssh 데이터 복제)

      # 아티팩트 업로드
      - name: Upload build artifacts
        uses: actions/upload-artifact@v4
        with:
          name: artifact
          path: build/libs/*.jar
          
      # 아까 업로드했던 아티팩트 다운로드해줌.
      - name: Download build artifacts
        uses: actions/download-artifact@v4
        with:
          path: build/libs
      - run: ls -R build/libs

      # 아티팩트 EC2 로 업로드하기
      - name: Upload to EC2
        env:
          SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
          EC2_USER: ${{ secrets.EC2_USER }}
          EC2_HOST: ${{ secrets.EC2_HOST }}
        run: |
          mkdir -p ~/.ssh
          echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
          chmod 600 ~/.ssh/id_rsa
          scp -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa build/libs/artifact/*.jar $EC2_USER@$EC2_HOST:~/my-project
  • 아티팩트를 업로드, 다운로드 하는 것은 러너가 아니라 actions/download-artifact@v4 레포에 체크아웃해서 진행한다. 여기에서 주의해야 할 점이 있다.
  • 난 원래 CI/CD 를 러너를 나눠서 진행하려고 했음.
  • 그런데 actions/download(upload)-artifact@v4 가 제공하는 액션은, 해당 러너가 종료되면 내가 업로드한 파일도 삭제함…
  • 그래서 ci 에서 actions/upload-artifact@v4 로 업로드해놓고, cd 에서 actions/download-artifact@v4 로 가져오려고 하면 파일을 가져올 수 없었음 ㅜㅜ

 

 

(사실 이렇게 ci/cd 단계를 아예 나눠버리니 깃 액션에서 보이는 작업도 두 단계로 나뉘어져서, 한 커밋에서 ci/cd 가 제대로 되었는지 명확하게 확인이 어렵다는 것이 제일 커서 한 러너에서 모두 처리하게 변경하긴 했지만…어쨌든 그렇다.)

  • 어쨌든 한 러너에서 job 이 달라도 업로드, 다운로드 하는 것은 문제없어서 그대로 진행했고 !
      # 아티팩트 EC2 로 업로드하기
      - name: Upload to EC2
        env:
          SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
          EC2_USER: ${{ secrets.EC2_USER }}
          EC2_HOST: ${{ secrets.EC2_HOST }}
        run: |
          mkdir -p ~/.ssh
          echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
          chmod 600 ~/.ssh/id_rsa
          scp -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa build/libs/artifact/*.jar $EC2_USER@$EC2_HOST:~/my-project
  • SSH private key 는 git@github.com:깃아이디/레포이름.git 형식으로 만들었고
    • .pub key 는 ec2 인스턴스 ~/.ssh/authorized_keys 에 내용을 추가해준다. 기존 내용에 덧붙여서 복붙해주면 됨.
  • EC2_USER, HOST 는 EC2 접속 유저와 pub IP 주소를 넣었다.
  • 커맨드를 설명하면
    • 러너 컴퓨터에 .ssh 디렉토리에 내가 넣었던 private key 값으로 키를 만들어서
    • 키에 권한을 축소시킴 (본인만 쓸 수 있게)
      • 보통 이런 키들은 오히려 권한이 열려있으면 사용을 못하는 경우가 대부분임
    • scp 명령어로 jar 파일을 my-project 디렉토리에 넣어줌, → 이 때 상대 경로를 사용해줘야해서 ~/my-project 라고 써줘야 함..

secret 환경 변수 설정

  • secrets 환경 변수는 Settings → Secrets and variables → Actions → Repository secrets 에서 설정 가능

 

어쨌든 이런 작업을 거쳤으니 정말 배포를 해봐야겠지?

배포(하기 전에 버전 만들어주기)

      # 버전 만들어주기
      - name: Extract version
        env:
          MAIN_VERSION: v1.0.
          # GITHUB_RUN_NUMBER -> 리포지토리에 있는 특정 워크플로의 실행마다 고유한 숫자입니다. 이 숫자는 워크플로의 첫 실행 시 1부터 시작하며 새 실행마다 증가합니다. 워크플로 실행을 다시 실행하는 경우 이 숫자는 변경되지 않습니다.(예시: 3)
          GITHUB_RUN_NUMBER: ${{ github.run_number }}
        run: echo "VERSION=$MAIN_VERSION$GITHUB_RUN_NUMBER" >> $GITHUB_ENV
  • 그 동안 버전을 직접 올리거나 내려주고 있었는데
  • 마이너 버전 같은 경우는 깃 액션에서 사용하는 GITHUB_RUN_NUMBER 환경변수를 활용해도 될 것 같았다!
  • VERSION 환경 변수를 만들어 $GITHUB_ENV 에 추가해서 사용하기로
    • github actions 에서 사용할 수 있는 환경 변수임

배포

  • 해당 작업은 모든 작업을 ssh 접속이 되어있다는 전제 하에 진행해야해서 appleboy/ssh-action@v1.0.3 를 활용하기로 함!
      # 이미지 빌드 & 컨테이너 배포
      - name: Deploy Artifact
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ${{ secrets.EC2_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          port: ${{ secrets.SSH_PORT }}
          script: |
            process_started_at=$(date +%s%N)
        
            echo "Start Build Docker Image"
            docker build -t cicd_app:${{ env.VERSION }} ~/my-project/.
            build_finished_at=$(date +%s%N)
            
            docker stop cicd_app

            docker run -d --name cicd_app -p 8000:8000 cicd_app:${{ env.VERSION }}
            
            process_finished_at=$(date +%s%N)
            image_build_time=$(echo "scale=3; ($build_finished_at - $process_started_at) / 1000000000" | bc)
            process_total_time=$(echo "scale=3; ($process_finished_at - $process_started_at) / 1000000000" | bc)
            
            echo "Docker build Complete in ${image_build_time} seconds."
            echo "Docker Process Complete in ${process_total_time} seconds."
  • Extract version 단계에서 만들어뒀던 ${{ env.VERSION }} 환경변수를 활용해 도커 이미지를 만들고 → 컨테이너를 중지 후 → 컨테이너를 실행하는 스크립트
  • 도커 이미지 빌드 시간과 총 배포에 걸리는 시간을 계산하고 로그로 남기는 게 중요하다고 생각해서 로그도 남겨보았음.

 

  • 로그는 이런 식으로 찍힌다.

총 프로세스.yaml

name: CI/CD

# master branch 에 push / PR 이 일어날 때를 트리거로 하여 해당 build event 발생
on:
  push:
    branches: [ "develop" ]
  pull_request:
    branches: [ "develop", "master" ]

jobs:
  # 작업 이름 build 정의
  build:
    # ubuntu 최신 이미지 위에서 실행
    runs-on: ubuntu-latest
    # 깃허브의 해당 작업에 대한 권한 범위
    permissions:
      # 해당 커밋에 대한 "읽기" 권한 있음.
      contents: read

    # 빌드 작업의 단계들.. '-' 가 Step 1, 2, 3 으로 관리될거임
    steps:
      # 레포로 체크아웃
      - uses: actions/checkout@v4
      # jdk 17 세팅 작업이라고 이름을 지정하고, uses 를 사용해 setup-java 함
      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          # 궁금해서 찾아봤는데 JDK 이미지가 저장된 저장소 이름임..
          distribution: 'temurin'

      # Gradle 세팅, 아래 링크에 액션의 자세한 내용이 담겨있다.
      # Configure Gradle for optimal use in GitHub Actions, including caching of downloaded dependencies.
      # See: <https://github.com/gradle/actions/blob/main/setup-gradle/README.md>
      - name: Setup Gradle
        uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0

      # build 명령
      - name: Build with Gradle Wrapper
        run: ./gradlew build

      # 아티팩트 업로드
      - name: Upload build artifacts
        uses: actions/upload-artifact@v4
        with:
          name: artifact
          path: build/libs/*.jar

  deploy:
    # 병렬 작업 ㄴㄴ 직렬로 build 작업이 끝나야지만 배포하고 싶음.
    needs: build

    runs-on: ubuntu-latest
    # 쓰기 작업 허용
    permissions:
      contents: write

    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      # 아까 업로드했던 아티팩트 다운로드해줌.
      - name: Download build artifacts
        uses: actions/download-artifact@v4
        with:
          path: build/libs
      - run: ls -R build/libs

      # 아티팩트 EC2 로 업로드하기
      - name: Upload to EC2
        env:
          SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
          EC2_USER: ${{ secrets.EC2_USER }}
          EC2_HOST: ${{ secrets.EC2_HOST }}
        run: |
          mkdir -p ~/.ssh
          echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
          chmod 600 ~/.ssh/id_rsa
          scp -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa build/libs/artifact/*.jar $EC2_USER@$EC2_HOST:~/my-project

      # 버전 만들어주기
      - name: Extract version
        env:
          MAIN_VERSION: v1.0.
          # GITHUB_RUN_NUMBER -> 리포지토리에 있는 특정 워크플로의 실행마다 고유한 숫자입니다. 이 숫자는 워크플로의 첫 실행 시 1부터 시작하며 새 실행마다 증가합니다. 워크플로 실행을 다시 실행하는 경우 이 숫자는 변경되지 않습니다.(예시: 3)
          GITHUB_RUN_NUMBER: ${{ github.run_number }}
        run: echo "VERSION=$MAIN_VERSION$GITHUB_RUN_NUMBER" >> $GITHUB_ENV

      # 이미지 빌드 & 컨테이너 배포
      - name: Deploy Artifact
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ${{ secrets.EC2_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          port: ${{ secrets.SSH_PORT }}
          script: |
            process_started_at=$(date +%s%N)
        
            echo "Start Build Docker Image"
            docker build -t cicd_app:${{ env.VERSION }} ~/my-project/.
            build_finished_at=$(date +%s%N)
            
            docker stop cicd_app

            docker run -d --name cicd_app -p 8000:8000 cicd_app:${{ env.VERSION }}
            
            process_finished_at=$(date +%s%N)
            image_build_time=$(echo "scale=3; ($build_finished_at - $process_started_at) / 1000000000" | bc)
            process_total_time=$(echo "scale=3; ($process_finished_at - $process_started_at) / 1000000000" | bc)
            
            echo "Docker build Complete in ${image_build_time} seconds."
            echo "Docker Process Complete in ${process_total_time} seconds."

  • build 단계에서 살짝 바뀐 부분이 있지만 큰 이상은 없음.

 

 

  • develop 브랜치에 push 이벤트가 일어나면 이렇게 빌드 → 배포 까지 자동으로 진행되는 것을 확인 가능하다 ^0^/)

develop → master PR 올렸을 때

  • master 브랜치에 PR 이 올라갔을 때도 이벤트가 잡혀서 이렇게 워크플로우가 실행되는 것을 확인 가능하다.

 

  • 모든 단계가 성공이 되어 4 successful checks 되었다 (뿌듯)

보완해야할 점

  • 배포 단계에서 해당 커맨드에서 에러가 발생하더라도 → error 취급을 안 함..
  • 그래서 컨테이너가 만들어지지 않더라도 에러로 취급하지 않고 성공으로 간주함.

 

  • 물론 이렇게 도커 이미지 빌드 로그를 err 라고 찍는걸 봐서는 그냥 첫 로그 외에는 다 error 로그로 취급하니까 에러 로그가 발생해도 에러라고 취급 안 하고 넘어가는 것 같은데
  • 커맨드에 에러가 나도 에러 취급하지 않는 것을 이용해
  • 컨테이너 정지 → 삭제 → 생성 단계를 커맨드로 집어넣으면 당장은 괜찮겠지만…
  • 사실 도커 레지스트리를 사용한다면
    1. 레지스트리에 이미지를 푸쉬하는 단계
    2. 풀해와서 컨테이너를 올리는 단계
    • 두 단계로 나누고, 해당 단계가 실패했을 경우 에러를 발생시키고 싶음
    • 사용하지 않아도 나눠도 되긴 하지만…
  • 아직 회사에서 도커 허브를 사용할 수 있을지 아닐지 결정이 안 나서..일단 단계 나누기는 보류

 

 

 

기존에 CI/CD 구축해놨을 때 공부해놨던 것들이 있어서 구조나 단계는 괜찮았는데

역시 ssh 연결에서 살짝 애먹었지만 생각보다 나이스하게 넘어가서 괜찮았당.

좀 더 보완해서 주간회의 때 발표하고 깃허브 액션을 사용할 수 있게 되어서

운영 서버에서도 CI/CD 사용을 할 수 있었음 좋겠다.....ㅜ

 

 

---

주간 회의에서 깃허브 액션 사용 건의했고 사용확정났다.ㅎㅎ

대신 깃허브 액션을 사용하는 대신 도커 허브는 사용하지 않기로... -> 이건 좀 히스토리가 있음 ...ㅎ_ㅎ 머 어쨌든 잘됐지머 CI/CD 는 대니까..

 

이번 회의 때 CDN 이랑 깃헙액션 사용이 동시에 확정나서 !!!!!!! 일단 CDN 먼저 적용하고 그 담에 깃헙액션 적용하면 될듯..

요새 출근하는 게 넘 재미가 없었는데 다시 의욕생김 !!!히히

728x90