Today I Learned

241231 TIL / Trello 프로젝트, GitHub Action CI/CD

shinelee26 2024. 12. 31. 23:42

오늘 진행한 학습 요약

1. Trello 프로젝트

  • 프로젝트 간단 소개
  • 담당 기능 + 트러블 슈팅
    • 1. 멤버 및 역할관리
    • 2. 워크스페이스
    • 3. 보드
    • 4. 배포와 CI/CD
Kanban-project "Trello" Git-hub 링크: https://github.com/PhoneixJo/kanban-project

 

2. GitHub Action으로 CI/CD 구축하기 (AWS, IntelliJ, Docker, GitHub)

  • 1. 초기 설정
  • 2. 구축 과정

학습 정리

1. Trello 프로젝트

 

프로젝트 간단 소개

  • 프로젝트 이름 : Prello (Trello 오마쥬?..)
  • 한 줄 정리 : Prello는 워크스페이스, 보드, 리스트, 카드 기반의 Kanban 스타일 작업 관리를 제공하는
                          협업 중심 프로젝트 관리 도구
  • 참고 사이트

담당 기능 + 트러블 슈팅

  • 1. 멤버 및 역할관리

      • 🤔 문제
        • 워크스페이스에 멤버가 접근할 때마다 DB에 접근해서 권한값을 가져오는 건 너무 DB에 대한 접근이 많은 것 같았다.
        • 그리고 DB에 접근하는 코드를 작성하면 팀원이 작성한 인가 로직을 잘 활용할 수 없는 아쉬움이 있었다.
     
    • 😎 해결방법
      • 팀에서 사용하기로 한 세션을 사용하기로 했다. 워크스페이스 생성, 삭제나 팀원 추가, 삭제 등이 발생할 경우 세션에 MemberAuth 값을 저장하여 변경되는 로직이 없을 경우 세션값을 계속 조회하도록 수정하였다!
    • 😶 고민
      • 다음번에는 조금 더 효율적인 코드 작성을 위해 Spring Security를 활용해서 인증/인가 로직을 활용해 보는 것이 좋을 것 같다!
    • 🤔 문제
      • 워크스페이스가 여러개인 경우 세션에 값을 어떻게 저장해야 하지? 한 워크스페이스에서 다른 워크스페이스로 이동할 경우에는 세션에서 값이 조회되지 않아 에러가 발생할 텐데..
    • 😎 해결방법
      • 워크스페이스가 여러 개인 경우 여러 워크스페이스의 권한 값을 List로 세션에 저장하도록 작성하였다.
      • 그리고 조회 시 stream을 사용해 매개변수로 입력받은 workspaceId를 통해 해당 workspace에 보유한 권한 값을 비교 및 반환하도록 코드를 작성하였다! 
    • 😶 고민
      • 그렇지만 워크스페이스의 개수가 제한되어있지 않았고 워크스페이스가 늘어날수록 세션의 값이 커져 서버 메모리에 부담이 매우 커지게 된다. 따라서 이번 프로젝트에서는 세션을 사용했지만 다음 프로젝트에서는 조금 더 효율적인 방안을 적용시키는 게 좋을 것 같다.
      • 다른 조에서 JWT 등의 방법을 사용한 것 같아 이런 기능을 활용해 보는 것도 좋을 것 같다.
  • 2. 워크스페이스
    • 🤔 문제
      • 워크스페이스를 소유한 사람, 아닌 사람과 소유 워크스페이스에 따른 접근 권한을 어떻게 예외처리하는 게 좋을까??
    • 😎 해결방법
      • 세션에 저장된 workspacePermission에 대한 값을 workspaceId 매개변수로 받아 해당 워크스페이스에 권한정보를 가지고 있는지 아닌지에 대해 WorkspacePermissionService 클래스를 새로 생성해 검증하였다.
      • 또한 워크스페이스를 생성한(소유한) 사람에 대해서는 일반 workspace권한을 가진 유저와 가질 수 있는 권한에 대해 차이를 둬야 했기 때문에 워크스페이스 소유자에 대한 권한 검증 로직도 추가하였다.
    • 😶 고민
      • 세션을 사용하다 보니 점점 권한을 검증해야 하는 메서드나 클래스 등이 너무나 많아졌다.
      • 팀원과 회의 때 권한검증서비스가 위치할 메서드 등에 대해 이야기를 나눴고 이런 세션을 통한 다양한 권한검증 코드 작성에 대해 고민이 깊어졌던 것 같다.
      • 역시나 추후에는 다른 방법으로 인증/인가를 사용하는 것을 고려하는 게 좋을 것 같으며 만약 세션을 사용해야 하는 경우 권한 검증을 어디서 진행하는 게 좋을지를 고민해 봐야겠다.
  • 3. 보드

    • 🤔 문제
      • 뭐?! 보드를 상세 조회할 때 보드에 속한 Card값과 Card에 속한 Deck(list) 값도 반환해야 한다고!!!?
      • Card에서 단순히 Deck과 Board를 JOIN FETCH로 가져오면 되지 않을까? 했지만 Card가 없으면 조회가 안된다.. ㅠ
        -> (반대로 Board에서 가져오도록 생각했어야 했다..)
      • 그리고 계속 수정에 수정을 거듭하였는데도 Card가 하나만 조회되는 등 문제가 발생했다..
    • 😎 해결방법
      • 1. 보드에 속한 Deck 목록을 DeckRepository를 통해 조회한다.
      • 2. 보드에 속한 Card 목록을 CardRepository를 통해 조회한다.
      • 3. Map을 통해 조회한 카드 목록을 deckId별로 그룹화한다. 그리고 CardResponseDto.toDto를 통해 DTO로 변환한 뒤 저장 한다.
        • 덱 1: [카드 A, 카드 B]
          덱 2: [카드 C, 카드 D]
      • 4. 그리고 decks 리스트를 순환하면서 deck에 속한 card를 조합하여 DeckResponseWithCardsDto를 통해 DTO로 변환 후 deckDtos라는 리스트에 저장한다.
        카드가 없는 덱은 빈 리스트를 사용한다!
        • 예시)
        • {
          1L: [CardResponseDto{id=1, name='Card A'}, CardResponseDto{id=2, name='Card B'}],
          2L: [CardResponseDto{id=3, name='Card C'}, CardResponseDto{id=4, name='Card D'}]
          }

      • 5. 보드 정보와 덱 + 카드 정보들을 합쳐 BoardResponseDto로 반환한다!
        • DeckResponseWithCardsDto{id=1, name='Deck 1', cards=[CardResponseDto{id=1, name='Card A'}, CardResponseDto{id=2, name='Card B'}]}
          DeckResponseWithCardsDto{id=2, name='Deck 2', cards=[CardResponseDto{id=3, name='Card C'}]}
          DeckResponseWithCardsDto{id=3, name='Deck 3', cards=[]}
    • 😶 고민
      • 하위 두 개의 저장된 entity 값을 조회해서 리스트로 반환하는 게 사실 처음엔 쉽지 않았다. 덱만 여러 개 있을 경우, 덱과 카드가 여러개 있을 경우, 덱 하나에 카드가 여러개 있을 경우 등 반환 값이 나오는 결과가 다양했다. 
      • 그리고 덱 또는 카드를 조회했을 시의 응닶값(DTO)을 보드에서 반환해줘야 했기 때문에 Dto로 변환된 값을 사용해야 했다. 
      • 그리고 다시 코드를 보니 card에서 값을 조회하는 게 아니라 해당 보드에 연관된 객체를 boardrepository에서 JOIN FETCH로 가져오고 조회하려는 Board Id값만 있었으면 조금 더 쉽게 코드를 작성할 수 있었다. ㅠㅠ
      • Stream의 편리함은 좋지만 여전히 사용 및 이해에 대해 어려움을 느낀다. 이 부분에 대해서 공부를 더 해야겠다!
  • 4. 배포와 CI/CD
    • 🤔 문제
      • Build가 됐는데 안 됐습니다? 빌드 완료 후 Build가 종료되지 않고 time아웃으로 종료되는 문제가 발생했다. 검색해도 나오지 않고 도대체 이 경우를 어떻게 검색해야 하는지도 감이 잡히지 않았다..
    • 😎 해결방법
      • 튜터님을 찾아가 질문을 하였다!! 역시 고민은 나눌수록 반(?)이 된다..
      • 모종의 이유로 build의 실행 값에 --scan이라는 인수값이 붙을 수 있다고 한다.
      • 이 Sacn 기능은 Gradle Enterprise 서버(또는 Gradle의 기본 서버)로 데이터를 업로드한다고 한다.  모종의 이유로 이 과정이 끝나지 않을 경우 build가 성공되었음에도 종료되지 않을 수 있다고 한다.
      • 따라서 --scan이라는 인수값을 제거하면 build가 성공적으로 이루어진다!
      • 또는./gradlew clean build를 사용하면 좋다고 한다!

 


2. GitHub Action으로 CI/CD 구축하기 (AWS, IntelliJ, Docker, GitHub)

AWS, Docker, Spring Boot, Intellij 활용해서 배포하기 참고

 

1. 초기 설정

  • AWS에 EC2 서버와 RDS 서버를 구성한다.
    • 보안 설정 및 VPC 설정이 제대로 되어 있지 않으면 외부에서 접근 및 서비스에 제대로 접근하기 어려우니 주의!
  • EC2에 Docker를 설치하고 RDS 서버와의 연결을 설정한다.
    • RDS에서 인스턴스를 직접 연결해도 크게 문제가 없었다. 나는 확인을 위해 항상 EC2에 mysql Server를 설치 후 RDS에 접속해 보는 편이다! 복잡한 네트워크 환경이 아니라면 ping check만 해도 크게 문제없을 것 같다. RDS에서 인스턴스를 직접 연결할 경우 알아서 보안그룹을 추가하기 때문이다!!
  • 로컬 컴퓨터에 Docker Desktop을 설치한다. (없을 경우)
  • 배포를 진행할 Spring Boot 환경을 준비한다 (IDE 활용)
  • GitHub에 사용할 Repository와 배포에 사용할 브랜치(ex: release) 만든다.
  • S3도 사용할 경우 연결하면 되는데 이번에는 따로 사용하지 않을 것이다!

2. 구축 과정

  • 기존에 블로그에서 작성한 AWS, Docker, Spring Boot, Intellij 활용해서 배포하기 와 엄청 크게 다른 내용은 없다.
  • 다만 수동으로 배포하던 내용을 Github Action이라는 것을 사용하여 특정 원격브랜치에 업데이트된 내용을 Push 할 경우 자동으로 EC2 서버에 해당 내용을 배포하게 만들 것이다!
  • 따라서 기존 방법에서 추가된 내용을 중심으로 설명하고자 한다.

 

    •  

    • Spring Boot에서 서비스가 정상으로 Build가 되는지 확인한다.
    •  

    • 정상적으로 Build가 되었으면 Project 최상위 패키지 아래에 docker라는 패키지와 그 아래 Dockerfile을 생성해 준다.
    • 그리고 사진과 같이 Dockerfile을 작성해 준다.
    • 그리고 해당 IDE 터미널에서 docker login 및 docker build를 진행한다. 그리고 이미지가 생성된 것을 확인한다!
    • docker login
      
      docker build -t chews26/prello:latest .
      
      docker images

    • Docker Desktop을 연다! 그리고 본인의 계정 아이콘을 클릭 후 Account Settings에 들어간다.
    •  Security에서 Personal access tokens를 받는다! 이 키는 중요한 키니 잊어버리지 않도록 메모하고 유출되지 않도록 해야 한다.
    • GitHub에 이동하여 해당 레포지토리 Settings로 이동한다. (팀프로젝트 팀원일 경우 권한이 없어 Setting이 불가한 경우가 있으나 이 경우 repository를 생성한 팀원에게 권한부여를 요청하면 된다!)
    • 그리고 만약 프로젝트에 환경변수를 설정한 부분 중 공개되면 안 되는 값이 있으면 Secrets에 이동해 값을 입력해 준다.
    •  아까 dockerhub에서 부여받은 personal access tokens는 DOCKERHUB_TOKEN에 기입한다
      • DB_USERNSME
      • DB_URL
      • DB_PASSWORD
      • DOCKERHUB_TOKEN
      • EC2_HOST
      • EC2_KEY
      • EC2_USER
      • 기타 사용할 환경변수 값 등록!
    • UserName과 프로젝트 명은 variables로 등록해도 된다.
      • DOCKERHUB_USERNAME
      • DOCKER_IMAGE_TAG_NAME
    •  
    •  

  • 해당 레포지토리, Actions에서 New workflow를 클릭한다.
  •  
  • set up a workflow yourself ->라는 파란색 글씨를 클릭한다!
  • 그럼 이런 화면이 뜨면 아래의 값을 입력해 준다.
  • branches 괄호 안에는 어느 브랜치에 푸시했을 때 배포할 건지 작성해야 한다.
  • 그리고 ${} 안에 값들은 위에서 설정해 주었던 변수 값들이다. 외부로 유출하지 말아야 하는 key값 등은 꼭 직접 작성하지 말고 secrets에 작성해야 한다!
  • 본인의 환경에 맞게 아래 파일을 수정하면 된다.
  • 파일 명도 docker-build.yml 등으로 수정한다.
  • 아래 내용을 다 작성했으면 Commit changes를 누른다.
  • name: docker multi-stage build
    
    on:
      push:
        branches: [ release ] # 해당 branch에 push 되었을 경우
    
      # https://github.com/marketplace/actions/build-and-push-docker-images
    jobs:
      docker-build-and-push:
        runs-on: ubuntu-latest
        steps:
          - name: Login to Docker Hub
            uses: docker/login-action@v3
            with:
              username: ${{ vars.DOCKERHUB_USERNAME }}
              password: ${{ secrets.DOCKERHUB_TOKEN }}
          - name: Set up QEMU
            uses: docker/setup-qemu-action@v3
          - name: Set up Docker Buildx
            uses: docker/setup-buildx-action@v3
          - name: Build and push
            uses: docker/build-push-action@v6
            with:
              file: ./docker/Dockerfile
              push: true
              tags: ${{ vars.DOCKERHUB_USERNAME }}/${{ vars.DOCKER_IMAGE_TAG_NAME }}:latest
    
      deploy-to-ec2:
        needs: docker-build-and-push
        runs-on: ubuntu-latest
        steps:
          - name: Deploy to EC2
            uses: appleboy/ssh-action@v1.2.0
            with:
              host: ${{ secrets.EC2_HOST }}
              username: ${{ secrets.EC2_USER }}
              key: ${{ secrets.EC2_KEY }}
              script: |
                CONTAINER_ID=$(sudo docker ps -q --filter "publish=8080-8080")
                if [ ! -z "$CONTAINER_ID" ]; then
                  sudo docker stop $CONTAINER_ID
                  sudo docker rm $CONTAINER_ID
                fi
                sudo docker pull ${{ vars.DOCKERHUB_USERNAME }}/${{ vars.DOCKER_IMAGE_TAG_NAME }}:latest
                sudo docker run -d -p 8080:8080 \
                    -e DB_USERNAME=${{ secrets.DB_USERNAME }} \
                    -e DB_PASSWORD=${{ secrets.DB_PASSWORD }} \
                    -e DB_URL=${{ secrets.DB_URL }} \
                    ${{ vars.DOCKERHUB_USERNAME }}/${{ vars.DOCKER_IMAGE_TAG_NAME }}:latest


  • 그리고 프로젝트를 작성했던 IDE로 돌아와서 아까 yml 파일을 작성한 브랜치(메인으로 설정된 브랜치)를 pull 해온다.
  • 정상적으로 작성한 내용이 확인되면 해당 branch에 test로 커밋 및 push를 진행해 본다.

  • 그리고 GitHub 레포지토리 Actions에 들어와 해당 내용이 잘 Build 및 push가 되는지 EC2에 잘 접근되는지 확인한다.
    • 만약 build가 정상적으로 안되면 코드에서 build가 안되게 하는 요소를 해결해야 하며 환경변수가 잘못됐을 경우에도 올바른 값으로 다시 넣었는지 확인해 본다.
    • EC2에 접근하지 못할 경우 보안그룹 설정이 제대로 되어있는지 EC2에 ssh로도 접근이 가능한지 등을 확인하면 좋다.
  • 해당 ec2에 접근하여 docker ps로 잘 컨테이너가 떠있는지  docker logs {CONTAINER ID}를 했을 경우 Spring boot가 제대로 start 되었는지 확인하면 된다.