프로젝트에 Github Actions 적용해보기 (Java, Spring)

soluinoon·2023년 6월 12일
2

CI/CD

목록 보기
1/1
post-thumbnail

🚨주의🚨 본 글은 미숙한 개발자가 겪은 시행착오가 많이 들어가 있으므로, 해결방안이 필요하신 분은 목차 부분을, 스크립트가 필요하신 분은 맨 아래를 확인해주시면 감사하겠습니다.

손이 많이가는 배포

제 배포방식은 이렇게 진화했습니다.

  1. 로컬에서 빌드한 다음, SCP로 서버로 보낸다음 nohup
  2. 왔다갔다 햇갈리네? 그냥 서버에 클론뜨고 pull 뒤에 nohup
  3. 명령어 너무 귀찮아 서버 들어가서 스크립트만 실행시켜서 재배포

전혀 관계없는 어떤 것들이 비슷한 환경에 맞게 진화했는데, 진화한 모습이 같은 것을 '수렴 진화'라고 합니다.
아마 서버 개발자 분들도 이런 귀찮음을 없애기 위해 위와같은 진화과정을 거쳐 젠킨스나 깃헙액션같은 툴이 탄생하지 않았나 싶습니다.

깃허브 액션 적용해보기


눈물나는 일대기...

시작


처음 깃허브 액션을 활성화 하시고 액션에 가시면 New workflow가 있습니다.

저는 Java with Gradle을 선택했습니다.

이제 여기서 원하는 스크립트를 작성하시면 됩니다.

관문 1. 테스트 오류

간단한 Workflow 입니다.

코드

name: Java CI with Gradle

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

permissions:
  contents: read

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3
    - name: Set up JDK 11
      uses: actions/setup-java@v3
      with:
        java-version: '11'
        distribution: 'temurin'
    - name: Build with Gradle
      uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1
      with:
        arguments: build

오류 발생

> Task :compileTestJava
> Task :processTestResources NO-SOURCE
> Task :testClasses
> Task :test
ApplicationTests > contextLoads() FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:98
Caused by: org.springframework.beans.factory.BeanCreationException at AbstractAutowireCapableBeanFactory.java:1804
Caused by: org.hibernate.service.spi.ServiceException at AbstractServiceRegistryImpl.java:284
Caused by: org.hibernate.HibernateException at DialectFactoryImpl.java:100
1 test completed, 1 failed

분명 로컬에선 잘 작동하는 테스트가 갑자기 동작하지 않습니다.
여기서 Workflow는 Github Action Runner가 설치된 도커 인스턴스(러너)에서 실행됩니다.
오류 메세지를 보시면 hibernate란 단어가 보이므로, 100% DB에서 터진 문제입니다.

저와 같은 문제를 해결하신 분의 게시물을 확인해보니, 아니나 다를까 러너에 MySQL이 세팅되지 않아서 발생하는 문제였습니다. 근데 여기서 근본적인 의문점이 있었습니다.

내 프로젝트는 AWS RDS와 연결해서 진행되는데, 굳이 러너에 MySQL을 설치할 필요가 있나...?

아니나 다를까, 오류는 해결되지 않았습니다.

오류 해결 (임시방편)

따라서 임시 방편으로 테스트를 끄기로 했습니다.

- name: Build with Gradle
#       uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1
#       with:
#         arguments: build
      run : ./gradlew clean build -x test

현재 글을 쓰면서 생각나는 문제는 RDS의 인바운드 룰에 러너의 IP가 허용이 안된 것 입니다. 사실 문제는 알았지만 해결방안을 몰라 진행할 수 없었는데, 아래서 임시로 인바운드룰을 설정할 수 있어서 추후 해결하도록 하겠습니다.

관문 2. 인바운드 룰

이제 빌드된 파일을 전송해주면 되지만, 하나 문제가 있습니다.

Run scp *.jar @:~/cicd
ssh: connect to host *** port 22: Connection timed out
lost connection
Error: Process completed with exit code 1.

어디서 많이 봤던 에러코드죠? 22번 포트가 닫혀있다... 즉 서버에서 인바운드 룰에 ssh로 ip추가를 하지 않았다는 의미입니다. 여기서 전 관문 1에서부터 품고있던, 어쩌면 이번 프로젝트 내내 생각했던 문제를 떠올립니다.

명령어나 코드로 특정 IP에 대해 임시로 포트를 허용하고, 닫을 수 있나?

💡 러너는 고정된 IP가 아닙니다. 왜냐하면 진행될 때 마다 다른 도커 컨테이너를 사용하니까요.

찾아보니까 예전에 S3 업로더를 사용할 때 사용했던 IAM을 사용해서 진행할 수 있더라구요.

AWS IAM 사용해서 인바운드 룰 명령어로 수정하기

코드

- name: Get Github action IP # 액션 IP 얻어오기
  id: ip
  uses: haythem/public-ip@v1.2
        
- name: Setting environment variables # 환경변수 설정
  run: |
    echo "AWS_DEFAULT_REGION=ap-northeast-2" >> $GITHUB_ENV
    echo "AWS_SG_NAME=launch-wizard-2" >> $GITHUB_ENV

- name: Configure AWS credentials
  uses: aws-actions/configure-aws-credentials@v1
  with:
  
     aws-access-key-id: ${{ secrets.AWS_IAM_ACCESS_KEY_ID }} # IAM 엑세스키
     aws-secret-access-key: ${{ secrets.AWS_IAM_SECRET_KEY }} ## IAM 시크릿 키
     aws-region: ap-northeast-2

- name: Add Github Actions IP to Security group
  run: | # 명령어로 시큐리티 그룹 인바운드 임시 설정
    aws ec2 authorize-security-group-ingress --group-name ${{ env.AWS_SG_NAME }} --protocol tcp --port 22 --cidr ${{ steps.ip.outputs.ipv4 }}/32 
    
- name: Remove Github Actions IP from security group
  run: | # 작업이 끝났으니 다시 인바운드 룰에서 제거
   aws ec2 revoke-security-group-ingress --group-name ${{ env.AWS_SG_NAME }} --protocol tcp --port 22 --cidr ${{ steps.ip.outputs.ipv4 }}/32
  env:
    AWS_ACCESS_KEY_ID: ${{ secrets.AWS_IAM_ACCESS_KEY_ID }}
    AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_IAM_SECRET_KEY }}
    AWS_DEFAULT_REGION: ap-northeast-2

💡 여기서 secrets.~~~는 깃허브 시크릿으로 등록한 값 입니다.

이렇게 설정하시면 되시는 분도 있겠지만 저는 안됐는데요, 그 이유는 IAM에서 권한설정을 해주지 않았기 때문입니다.

전 다음과 같은 오류가 발생했습니다.

An error occurred (UnauthorizedOperation) when calling the AuthorizeSecurityGroupIngress operation: You are not authorized to perform this operation.

해결방안을 찾았는데요, 저 권한을 인라인 정책으로 설정해도 똑같은 오류가 발생해서 그냥 EC2 모든 권한을 정책으로 등록했더니 해결됐습니다.

관문 3. SCP

이제 빌드를 완료했고, 아티팩트를 다운받고 배포만 해주면 됩니다.

코드

 ## 빌드했던거 받기
  - name: Download artifact
    uses: actions/download-artifact@v2
    with:
      name: cicdsample
          
  # SCP로 서버로 전송
  - name: SCP transfer
    uses: appleboy/scp-action@master
    with:
      username: ${{ secrets.SSH_USER }}
      host: ${{ secrets.SSH_ADDR }}
      key: ${{ secrets.SSH_KEY }}
      rm: true
      source: ${{ secrets.SOURCE_PATH }}
      target: ${{ secrets.DIST_PATH }}
      ## 작업에 사용했던 dist 디렉토리를 경로상에서 제거
      strip_components: 1

💡 깃허브 액션의 장점으로 사람들이 미리 만들어놓은 액션들을 사용할 수 있습니다.

이전에 다른 SCP 액션을 사용하면 오류가 발생해 appleboy라는 분이 만드신 액션을 사용하도록 변경했습니다.
자세한 옵션 설명
여기서 몇몇 시크릿에 대해 설명드리겠습니다.

  • SSH_USER : 터미널로 ec2 접근하실 때 사용하는 id 입니다.

  • SSH_ADDR : 터미널로 ec2 접근하실 때 사용하는 주소 입니다. IP도 되고 DNS도 되는 것 같습니다. (현재 DNS 사용중)

  • SSH_KEY : ec2를 기준으로 받으셨던 .pem키 파일을 메모장이나 Vim으로 열어서 안에 있는 값 입니다.

  • SOURCE_PATH : 다운받은 아티팩트 경로입니다. 저는 /home/runner/work/프로젝트/프로젝트/.jar 이런식으로 작성했습니다. 액션 결과를 확인해보시면 됩니다!!

  • DIST_PATH : 서버에 저장될 경로입니다.

관문 4. 스크립트 실행 시키기

마지막으로 서버에 들어가게 해서 제가 만든 스크립트를 실행시켜주고 깃허브 러너 ip를 인바운드 룰에서 제거해주면 끝 입니다.

# 내가 만든 서버 스크립트 실행
- name: Execute Server Init Script
  uses: appleboy/ssh-action@master
  with:
    username: ${{ secrets.SSH_USER }}
    host: ${{ secrets.SSH_ADDR }}
    key: ${{ secrets.SSH_KEY }}
    script_stop: true
    script: |
      sh action_deploy.sh 

# 깃허브 러너 아이피를 인바운드 룰에서 제거
- name: Remove Github Actions IP from security group
  run: |
   aws ec2 revoke-security-group-ingress --group-name ${{ env.AWS_SG_NAME }} --protocol tcp --port 22 --cidr ${{ steps.ip.outputs.ipv4 }}/32
  env:
    AWS_ACCESS_KEY_ID: ${{ secrets.AWS_IAM_ACCESS_KEY_ID }}
    AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_IAM_SECRET_KEY }}
    AWS_DEFAULT_REGION: ap-northeast-2

제가 만든 스크립트는 서버에 들어갔을 때 경로에 만들었고, 다음과 같습니다.


#!/bin/bash

echo “ACTION DEPLOY START”

# 현재 프로세스 가져오기
CURRENT_PID=$(pgrep -f 프로세스 이름)

# 있으면 종료
echo$CURRENT_PIDif [ -z $CURRENT_PID ]; then
        echo “no such app didn’t find.”
else
        kill -9 $CURRENT_PID
        echo “killed current pid”
        sleep 3
fi
echo “deploy started”

cd /home/ubuntu/cicd/workspace
JAR_NAME=$(ls | grep '자르 이름' | tail -n 1)
echo "> JAR Name: $JAR_NAME"
# 일반 자르로 하면 동작이 안돼서 출력과 오류를 따로 폴더를 만들어 텍스트 파일로 저장했습니다.
nohup java -jar -Duser.timezone=Asia/Seoul $JAR_NAME 1>nohup/stdout.txt 2>nohup/stderr.txt &
sleep 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/CD with Gradle

# 메인에 푸쉬나 피알되면 이벤트 발생
on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]

permissions:
  contents: read

## 
jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3
    
    - name: Set up JDK 11
      uses: actions/setup-java@v3
      with:
        java-version: '11'
        distribution: 'temurin'
        
    - name: Build with Gradle
#       uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1
#       with:
#         arguments: build
      run : ./gradlew clean build -x test
      
      # 아티팩트 업로드
    - name: Upload artifact
      uses: actions/upload-artifact@v2
      with:
        name: cicdsample
        path: build/libs/*.jar
 
  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      ## 빌드했던거 받기
      - name: Download artifact
        uses: actions/download-artifact@v2
        with:
          name: cicdsample
          
#       - name: Setup SSH
#         uses: webfactory/ssh-agent@v0.5.4
#         with:
#           ssh-private-key: ${{ secrets.SSH_KEY }}
      # 깃허브 액션 러너의 아이피를 얻어온다.
      - name: Get Github action IP
        id: ip
        uses: haythem/public-ip@v1.2
      
      # 환경변수 설정
      - name: Setting environment variables
        run: |
          echo "AWS_DEFAULT_REGION=ap-northeast-2" >> $GITHUB_ENV
          echo "AWS_SG_NAME=launch-wizard-2" >> $GITHUB_ENV
      
      # AWS 설정
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          # 아이엠 키 설정
           aws-access-key-id: ${{ secrets.AWS_IAM_ACCESS_KEY_ID }} 
           aws-secret-access-key: ${{ secrets.AWS_IAM_SECRET_KEY }} 
           aws-region: ap-northeast-2
      
      # 깃허브 액션의 아이피를 인바운드 룰에 임시 등록
      - name: Add Github Actions IP to Security group
        run: |
          aws ec2 authorize-security-group-ingress --group-name ${{ env.AWS_SG_NAME }} --protocol tcp --port 22 --cidr ${{ steps.ip.outputs.ipv4 }}/32    
      
      # SCP로 서버로 전송
      - name: SCP transfer
        uses: appleboy/scp-action@master
        with:
          username: ${{ secrets.SSH_USER }}
          host: ${{ secrets.SSH_ADDR }}
          key: ${{ secrets.SSH_KEY }}
          rm: true
          source: ${{ secrets.SOURCE_PATH }}
          target: ${{ secrets.DIST_PATH }}
          ## 작업에 사용했던 dist 디렉토리를 경로상에서 제거
          strip_components: 1

#       - name: Execute remote commands
#         run: |
#           ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_ADDR }} "sudo fuser -k 8080/tcp"
#           ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_ADDR }} "
      
      # 내가 만든 서버 스크립트 실행
      - name: Execute Server Init Script
        uses: appleboy/ssh-action@master
        with:
          username: ${{ secrets.SSH_USER }}
          host: ${{ secrets.SSH_ADDR }}
          key: ${{ secrets.SSH_KEY }}
          script_stop: true
          script: | 
            sh action_deploy.sh
            
      # 깃허브 러너 아이피를 인바운드 룰에서 
      - name: Remove Github Actions IP from security group
        run: |
         aws ec2 revoke-security-group-ingress --group-name ${{ env.AWS_SG_NAME }} --protocol tcp --port 22 --cidr ${{ steps.ip.outputs.ipv4 }}/32
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_IAM_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_IAM_SECRET_KEY }}
          AWS_DEFAULT_REGION: ap-northeast-2
    

마치며

계속 미루고 미루던 CI/CD를 만들어서 기쁩니다. 졸업작품 때 팀원이 젠킨스를 쓰면서 정말 고생했던 기억이 있는데, 깃허브 액션은 그에 비해 쉬운 것 같아서 나름 재밌었습니다 ㅎㅎ.
코드 저장소에서 직접 제공하는 CI/CD 툴이니 직관적이고 이해, 비교가 쉽습니다. 여러분들도 꼭 써보셨으면 좋겠습니다.
다음에는 실제 배포에서 사용되는 blue/green 무제한 배포를 구현해 실제 배포환경을 구성해보려 합니다.
긴 글 읽어주셔서 감사드리고 좋은 액션 제공해주신 appleboy님 감사합니다.

References

https://zzsza.github.io/development/2020/06/06/github-action/
https://hoestory.tistory.com/35
https://makethree.tistory.com/19
https://stackoverflow.com/questions/62275740/what-role-allows-an-ec2-user-to-run-aws-ec2-authorize-security-group-egress
https://github.com/appleboy/ssh-action
https://github.com/appleboy/scp-action
https://docs.github.com/ko/actions

profile
수박개 입니다.

3개의 댓글

comment-user-thumbnail
2023년 9월 3일

안녕하세요
궁금하게 있어 질문 남깁니다!
${{ steps.ip.outputs.ipv4 }} 요건 어디서 주입되는걸까요?

1개의 답글
comment-user-thumbnail
2024년 1월 29일

좋은 포스팅 감사합니다.

답글 달기