GitHub Actions 및 AWS EC2를 사용하여 Spring Boot CI/CD를 설정하는 방법

Bien·2024년 5월 27일
0

스프링부트 CI/CD 세팅하기

스프링부트 프로젝트를 시작함에 있어서 이번에 꼭 해보려고 한, 말로만 듣던 ci/cd를 gitHub Actions 과 AWS EC2를 사용해서 세팅을 하며 삽질을 한 과정에 대해 글로 남겨두고자 한다.

### CI/CD란?
1. 지속적인 통합 (Continuous Integration)
- 자동화된 빌드 및 테스트: 개발시 코드를 저장소에 자주 병합하면, CI 도구가 자동으로 빌드하고 테스트를 실행해주고, 이를 통해 코드를 자주 확인하고, 빌드 실패나 버그를 조기에 발견할 수 있는 장점이 있습니다.
- 신속한 피드백: 개발자들은 코드 변경 사항에 대한 피드백을 빠르게 받을 수 있습니다. 문제가 발생하면 즉시 수정할 수 있어, 개발 속도와 품질이 향상됩니다.
- 협업 향상: 여러 개발자가 동시에 작업하는 환경에서 코드 충돌을 줄이고, 원활한 협업을 지원합니다. 코드가 지속적으로 병합되고 테스트되므로, 프로젝트 전체의 코드베이스가 항상 최신 상태로 유지된다는 장점도 있다.

2. 지속적인 배포/전달 (Continuous Deployment/Delivery)
- 자동화된 배포 프로세스: 코드를 수동으로 배포하는 대신, 자동화된 배포 파이프라인을 통해 새로운 기능이나 수정 사항이 신속하게 사용자에게 전달되고, 자동화된 배포 과정은 사람이 배포시 할 수 있는 오류를 줄이고 배포 속도를 높이는데 용이하다.
- 짧은 릴리즈 주기: 작은 변경 사항을 자주 배포함으로써, 제품의 릴리즈 주기를 단축할 수 있다. 이를 통해 사용자 피드백을 빠르게 수집하고 반영할 수 있습니다.
- 안정성 및 신뢰성 향상: 자동화된 테스트와 배포를 통해, 새로운 코드가 항상 일관된 방법으로 배포되므로, 제품의 안정성과 신뢰성이 향상됩니다.
- 롤백 및 복구 용이성: 문제가 발생할 경우, 사람이 직접 롤백하면 미리 백업을 하고 백업한 버전을 적용하는데 시간이 걸리는데, 자동화된 배포 시스템은 이전 안정 버전으로 빠르게 롤백할 수 있어, 서비스 중단 시간을 최소화할 수 있다.

이 두가지의 장점을 찾아 봤을때 나에게 가장 와닿았던거는 안정적으로 배포가 가능하다는 점이였다, 현재 일하는 회사에서는 수동 배포를 해서 변경 파일을 말아서 ftp로 전송하는 방법으로 배포를 하는데 실수할까봐 긴장되기도 하고... 실제로도 초반에는 몇번... 필요한 파일을 빼고 올리는 사고를 친 경험도 있다.

사전 준비

필요한 도구 및 계정으로는 AWS 계정 , gitHub 계정, EC2 인스턴스 (Amazon Linux 2 또는 Ubuntu), SSH 키 생성 및 다운로드 등이 있는데 나는 SSH키를 EC2 인스턴스 생성시 키페어 방식으로 만들고 .pem 파일을 다운 받았다. 그리고 당연하지만 CI/CD를 할 프로젝트가 필요하다. 나는 스프링부트 프로젝트를 gradle 방식으로 간단하게 설정해서 생성했다.

여기서 중요한건 빌드가 잘 되는지 로컬에서 꼭 확인해봐야 한다. 특히 데이터베이스 연결을 잘 확인하자. 우리는 바보처럼 데이터베이스를 만들지 않고 연결을 해서 계속 오류가 났었다...🥺

AWS EC2 설정

인스턴스를 만들고 확인해야하는 부분이 있다.

우선 이렇게 인바운드 규칙에 들어가서 보안그룹 규칙이 포트 22, 80, 443에 대해 열려있는지 확인해야한다. 접근을 허용할 부분에 대해서 잘 입력해주자.

gitHub Actions을 위한 설정

  1. 깃허브 액션에서 워크플로우 파일을 생성한다. 예를들어 ci/cd for Spring boot 라는 이름의 yml 파일을 생성한다.
    나는 처음에 구글링을 통해
name: CI/CD for Spring Boot

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

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'
        distribution: 'temurin'

    - name: Setup Gradle
      uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0

    - name: Grant execute permission for gradlew
      run: chmod +x gradlew

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

    - name: Run tests
      run: ./gradlew test

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

    steps:
    - uses: actions/checkout@v4

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

    - name: Grant execute permission for gradlew
      run: chmod +x gradlew

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

    - name: Copy files via SCP
      env:
        AWS_EC2_USER: ${{ secrets.AWS_EC2_USER }}
        AWS_EC2_HOST: ${{ secrets.AWS_EC2_HOST }}
        AWS_EC2_KEY: ${{ secrets.AWS_EC2_KEY }}
      run: |
        echo "${{ secrets.AWS_EC2_KEY }}" > ec2_key.pem
        chmod 600 ec2_key.pem
        scp -i ec2_key.pem -o StrictHostKeyChecking=no build/libs/*.jar $AWS_EC2_USER@$AWS_EC2_HOST:~/app/

    - name: SSH and Deploy
      env:
        AWS_EC2_USER: ${{ secrets.AWS_EC2_USER }}
        AWS_EC2_HOST: ${{ secrets.AWS_EC2_HOST }}
        AWS_EC2_KEY: ${{ secrets.AWS_EC2_KEY }}
      run: |
        ssh -i ec2_key.pem -o StrictHostKeyChecking=no $AWS_EC2_USER@$AWS_EC2_HOST << 'EOF'
          pgrep java | xargs kill -9 || true
          nohup java -jar ~/app/*.jar > log.txt 2>&1 &
        EOF

이런 파일을 그냥 냅다 넣었다...여기서 부터였을까요...삽질의 시작이🫠

  1. 시크릿 설정
    위까지 했으면 다음으로 배포를 위해 필요한 시크릿 값을 설정해야한다. 깃허브 리포지토리의 Settings -> Secrets and variables -> Actions 에서 New repository secret을 클릭하면 필요한 시크릿을 추가 할 수 있다.
  • AWS_EC2_USE
  • AWS_EC2_HOST
  • AWS_EC2_KEY
    이부분이고 AWS_EC2_USE는 보통 ec2-user 또는 ubuntu 라는데 ubuntu를 썼고, AWS_EC2_HOST는 AWS EC2에 들어가서 탄력적 IP 주소를 넣어줬다.

    🧐왜 탄력적IP사용이 필요할까?
    EC2의 인스턴스의 퍼블릭IP는 인스턴스 시작/중지시 변경될 수 있다. 탄력적 IP는 고정된 퍼블릭 IP를 제공하기 때문에 인스턴스를 재시작 하더라도 IP 주소가 변경 되지 않는다. 따라서 고정된 IP 주소를 사용하면 gitHub Actions 과 같은 외부 서비스에서 항상 동일한 IP 주소를 사용해 EC2 인스턴스에 안정적이고 일관되게 접근이 가능하다.

그리고 마지막 AWS_EC2_KEY 이 부분이 엄청난 문제였는데, 나는
1. 이걸 인코딩 없이 그냥 넣었고
2. 인코딩 한 후에는 권한이 너무 개방적이라서 문제가 되었다.

따라서 꼭 권한을 주고, 그걸 인코딩해서 넣어줘야 한다!

보통 EC2 인스턴스에 연결하기 위한 키페어 파일을 인스턴스 설정시 받아서 pc에 보관중일텐데, 이 파일의 내용을 GitHub Secrets에 추가해야 한다.

  1. 권한을 변경해준다.(나만 읽고 쓰고 할 수 있다)
chmod 600 ~/Downloads/my-key-pair.pem
  1. Base64 인코딩을 한다.
base64 ~/Downloads/my-key-pair.pem

보통 이렇게들 많이 하던데 나는 이렇게 하니까 오류가 났고 그래서 그냥 cat을 사용해 복사한 내용을 넣어서 문제가 되었다.

base64 -i ~/Downloads/my-key-pair.pem -o ~/Downloads/my-key-pair.pem.base64
// SSH 키 파일을 Base64로 인코딩


cat my-key-pair.pem.base64 // 인코딩된 파일 내용 복사

인코딩시 반드시 이렇게 사용해주자...!

  1. 출력된 문자열을 복사하고 해당 문자열을 붙여준다.

최종으로 생성된 gradle.yml

그래서 여러번의 시도와 실패를 거쳐 완성된 .yml은 이렇다.

name: CI/CD for Spring Boot

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

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'
          distribution: 'temurin'

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

      - name: Setup Gradle
        uses: gradle/gradle-build-action@v2

      - name: Grant execute permission for gradlew
        run: chmod +x gradlew

      - name: Build with Gradle
        run: ./gradlew build --no-daemon --warning-mode all -x test  # 테스트를 생략하는 옵션 추가

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

    steps:
      - uses: actions/checkout@v4

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

      - name: Grant execute permission for gradlew
        run: chmod +x gradlew

      - name: Build with Gradle
        run: ./gradlew build --no-daemon --warning-mode all -x test  # 테스트를 생략하는 옵션 추가

      - name: Decode and save SSH key
        run: echo "${{ secrets.CICD_SECRET_KEY }}" | base64 --decode > ec2_key.pem
        shell: bash

      - name: Set permission for SSH key
        run: chmod 600 ec2_key.pem

      - name: Create target directory on EC2
        env:
          AWS_EC2_USER: ${{ secrets.CICD_ACCESS_USER }}
          AWS_EC2_HOST: ${{ secrets.CICD_ACCESS_HOST }}
        run: |
          ssh -i ec2_key.pem -o StrictHostKeyChecking=no $AWS_EC2_USER@$AWS_EC2_HOST 'mkdir -p ~/app/'

      - name: Copy files via SCP
        env:
          AWS_EC2_USER: ${{ secrets.CICD_ACCESS_USER }}
          AWS_EC2_HOST: ${{ secrets.CICD_ACCESS_HOST }}
        run: |
          scp -i ec2_key.pem -o StrictHostKeyChecking=no build/libs/*.jar $AWS_EC2_USER@$AWS_EC2_HOST:~/app/

      - name: SSH and Deploy
        env:
          AWS_EC2_USER: ${{ secrets.CICD_ACCESS_USER }}
          AWS_EC2_HOST: ${{ secrets.CICD_ACCESS_HOST }}
        run: |
          ssh -i ec2_key.pem -o StrictHostKeyChecking=no $AWS_EC2_USER@$AWS_EC2_HOST << 'EOF'
            pgrep java | xargs kill -9 || true
            nohup java -jar ~/app/*.jar > log.txt 2>&1 &
          EOF

이것은 초기에 아무런 생각없이 작성된 .yml 과 다음과 같은 차이가 있다.

  1. Gradle 패키지 캐시
  • 초기 : 캐시 단계 없음
  • 최종 :
- name: Cache Gradle packages
  uses: actions/cache@v3
  with:
    path: |
      ~/.gradle/caches
      ~/.gradle/wrapper
    key: gradle-home-v1-${{ runner.os }}-build-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
    restore-keys: |
      gradle-home-v1-${{ runner.os }}-build-

Gradle 패키지를 캐시하여 빌드 프로세스를 가속화하고 변경되지 않은 종속성을 재사용하도록 변경하였다.

  1. 빌드 시 테스트 생략
  • 초기 : 빌드 중 테스트를 실행하는 코드가 있었다.
- name: Build with Gradle
  run: ./gradlew build
  • 최종 : 빌드 중 테스트 생략.
- name: Build with Gradle
  run: ./gradlew build --no-daemon --warning-mode all -x test

CI/CD 파이프라인에서 배포에 중점을 두고 테스트 실패로 인해 생기는 문제를 피하기 위해 테스트를 생략했는데, 이부분은 추후 추가할 예정이다.

  1. SSH 키 디코딩 및 사용
  • 초기 : SSH 키를 직접 시크릿에서 echo로 출력함
- name: Copy files via SCP
  env:
    AWS_EC2_USER: ${{ secrets.AWS_EC2_USER }}
    AWS_EC2_HOST: ${{ secrets.AWS_EC2_HOST }}
    AWS_EC2_KEY: ${{ secrets.AWS_EC2_KEY }}
  run: |
    echo "${{ secrets.AWS_EC2_KEY }}" > ec2_key.pem
    chmod 600 ec2_key.pem
  • 최종 : SSH 키를 base64 디코딩하여 사용함
- name: Decode and save SSH key
  run: echo "${{ secrets.CICD_SECRET_KEY }}" | base64 --decode > ec2_key.pem
  shell: bash

- name: Set permission for SSH key
  run: chmod 600 ec2_key.pem

Base64 인코딩/디코딩을 통해 시크릿 관리 시스템 내에서 SSH 키를 안전하고 올바르게 처리한다.

  1. EC2에 디렉토리 생성
  • 초기 : 디렉토리 생성을 명시하지 않은 문제 있었음
  • 최종 : EC2에 타겟 디렉토리를 생성하는 단계 추가
- name: Create directory on EC2
  env:
    AWS_EC2_USER: ${{ secrets.CICD_ACCESS_USER }}
    AWS_EC2_HOST: ${{ secrets.CICD_ACCESS_HOST }}
  run: |
    ssh -i ec2_key.pem -o StrictHostKeyChecking=no $AWS_EC2_USER@$AWS_EC2_HOST "mkdir -p ~/app/"

배포 전 필요한 디렉토리를 생성하여 파일 복사 중 오류를 방지함.

결론

남들은 gitHub Actions 을 사용해서 쉽게 CI/CD를 할 수 있다는데, 나는 생각보다 오래 걸린거 같다. 처음 해봐서 그런것도 있고... 너무 일단 해보고 안되면 돌아가는 과정이 많았던거 같다. 그래도 한번 해봤고 글로 정리도 했으니까 같은 실수를 반복하는 일은 없지 않을까?
혹시 나와 비슷한 경우가 있어서 오류가 있던 사람들도 이 글이 도움이 되면 좋겠다😊

profile
🙀

0개의 댓글