Spring boot 3.xxx Docker CI / CD 구축 with Github Actions(2)

2.5*2 하빈·2023년 12월 24일
11

Spring boot 3.xxx Docker CI / CD 구축 with Github Actions(1) 에서는
수동 배포를 했었다.

이번 정리는 자동배포다!!

자동배포 동작 흐름

1. 개발 환경(인텔리제이)에서 github로 commit & push 감지
2. workflows 안에 .yml 파일 실행 (배포 스크립트)
2-1. JDK Setting
2-2. Gradle caching (속도향상)
2-3. application.yml 파일 생성
2-4. Gradle chmod
2-5. gradle build
2-6. docker Login
2-7. docker build & push
2-8. deploy to EC2


1 - Github Actions 생성 (스크립트 파일)


  • github에서 작업 중인 repository 코드로 이동한다.

  • Actions로 이동한다.

  • New workflow를 클릭해서 이동한다.

  • Java with Gradle -> Configure 클릭해서 이동한다.

  • 기본 파일 그대로 생성한다. (나중에 수정 할거임)

  • .github -> workflows -> gradle.yml 파일이 생성된 것을 확인한다.

2 - Gradle.yml (스크립트 파일 수정)


아래는 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: CI/CD github Actions & Docker

on:
  push:
    branches: [ "main", "develop" ]

permissions:
  contents: read

jobs:
  CI-CD:
    runs-on: ubuntu-latest
    steps:
    # JDK setting - github actions에서 사용할 JDK 설정 (aws 과 project의 java 버전과 별도로 관리)
    - uses: actions/checkout@v3
    - name: Set up JDK 17
      uses: actions/setup-java@v3
      with:
        java-version: '17'
        distribution: 'temurin'
        
    ## gradle caching (빌드 시간 줄이기)
    - name: Gradle Caching
      uses: actions/cache@v3
      with:
        path: |
          ~/.gradle/caches
          ~/.gradle/wrapper
        key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
        restore-keys: |
          ${{ runner.os }}-gradle-

    # 환경별 yml 파일 생성(1) - dev
    - name: make application-dev.yml
      if: contains(github.ref, 'develop')
      run: |
        cd ./src/main/resources
        touch ./application.yml
        echo "${{ secrets.YML }}" > ./application.yml
      shell: bash

    # 환경별 yml 파일 생성(2) - prod
    - name: make application-prod.yml
      if: contains(github.ref, 'main')
      run: |
        cd ./src/main/resources
        touch ./application.yml
        echo "${{ secrets.YML }}" > ./application.yml
      shell: bash

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

    # gradle build
    - name: Build with Gradle
      run: ./gradlew clean build -x test

      # docker login
    - name: Docker Hub Login
      uses: docker/login-action@v2
      with:
        username: ${{ secrets.DOCKER_USERNAME }}
        password: ${{ secrets.DOCKER_PASSWORD }}

    # docker build & push to production
    - name: Docker build & push to prod
      if: contains(github.ref, 'main')
      run: |
        docker build -f Dockerfile -t ${{ secrets.DOCKER_REPO }}/agaproject .
        docker push ${{ secrets.DOCKER_REPO }}/agaproject

    # docker build & push to develop
    - name: Docker build & push to dev
      if: contains(github.ref, 'develop')
      run: |
        docker build -f Dockerfile -t ${{ secrets.DOCKER_REPO }}/agaproject .
        docker push ${{ secrets.DOCKER_REPO }}/agaproject
        
    ## deploy to production
    - name: Deploy to prod
      uses: appleboy/ssh-action@master
      id: deploy-prod
      if: contains(github.ref, 'main')
      with:
        host: ${{ secrets.HOST_PROD }} # EC2 퍼블릭 IPv4 DNS
        username: ubuntu
        key: ${{ secrets.PRIVATE_KEY }}
        envs: GITHUB_SHA
        script: |
          sudo docker ps
          sudo docker rm -f $(docker ps -qa)
          sudo docker pull ${{ secrets.DOCKER_REPO }}/agaproject
          sudo docker run -d -p 8080:8080 ${{ secrets.DOCKER_REPO }}/agaproject
          sudo docker image prune -f
## ## sudo docker run -d -p 8080:8080 ${{ secrets.DOCKER_REPO }}/agaproject

    ## deploy to develop
    - name: Deploy to dev
      uses: appleboy/ssh-action@master
      id: deploy-dev
      if: contains(github.ref, 'develop')
      with:
        host: ${{ secrets.HOST_DEV }} # EC2 퍼블릭 IPv4 DNS
        username: ${{ secrets.USERNAME }} # ubuntu
        password: ${{ secrets.PASSWORD }}
        port: 22
        key: ${{ secrets.PRIVATE_KEY }}
        script: |
          sudo docker ps
          sudo docker rm -f $(docker ps -qa)
          sudo docker pull ${{ secrets.DOCKER_REPO }}/agaproject
          sudo docker run -d -p 8080:8080 ${{ secrets.DOCKER_REPO }}/agaproject
          sudo docker image prune -f
    

2-1. push 명령이 실행되면 스크립트 파일 실행


  • github repository에 코드 내용이 push가 되면 스크립트 파일이 실행된다.

name: CI/CD github Actions & Docker

on:
  push:
    branches: [ "main", "develop" ]

permissions:
  contents: read

2-2. JDK Setting


  • github Actions JDK를 설정하는 명령어이다.

별도로 JDK를 관리하고 실행하는 부분이다.
각자 프로젝트에 맞게 JDK 버전을 맞추는 것을 권장한다.


    # JDK setting - github actions에서 사용할 JDK 설정 (aws 과 project의 java 버전과 별도로 관리)
    - uses: actions/checkout@v3
    - name: Set up JDK 17
      uses: actions/setup-java@v3
      with:
        java-version: '17'
        distribution: 'temurin'

2-2. Gradle caching (속도향상)


  • Gradle을 Caching해서 속도를 향상시켜준다.

    ## gradle caching (빌드 시간 줄이기)
    - name: Gradle Caching
      uses: actions/cache@v3
      with:
        path: |
          ~/.gradle/caches
          ~/.gradle/wrapper
        key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
        restore-keys: |
          ${{ runner.os }}-gradle-

2-3. application.yml 파일 생성


  • 각 실행 환경에 맞게 .yml 파일을 생성해주는 명령어이다.

현재 아래 명령어는 main이든 develop이든 application.yml로 생성하게끔 만들었다.

나중에 따로 생성할 필요가 있을 때 써먹으려고 미리 명령어를 입력해놓았다.


    # 환경별 yml 파일 생성(1) - dev
    - name: make application-dev.yml
      if: contains(github.ref, 'develop')
      run: |
        cd ./src/main/resources
        touch ./application.yml
        echo "${{ secrets.YML }}" > ./application.yml
      shell: bash

    # 환경별 yml 파일 생성(2) - prod
    - name: make application-prod.yml
      if: contains(github.ref, 'main')
      run: |
        cd ./src/main/resources
        touch ./application.yml
        echo "${{ secrets.YML }}" > ./application.yml
      shell: bash

2-4. Gradle chmod


  • Gralde 실행 권한을 바꿔주는 명령어이다.

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

2-5. gradle build


  • gradle build 하는 명령어이다.

다른 자료를 찾다보면 clean이 없는 경우도 있는데
저는 clean이 없으니까 에러가 났습니다.


    # gradle build
    - name: Build with Gradle
      run: ./gradlew clean build -x test

2-6. docker Login


  • docker에 로그인하는 명령어이다.

따로 안 빼주고 docker login -u 명령어를 쓰다보면
가끔 에러가 발생한다.


      # docker login
    - name: Docker Hub Login
      uses: docker/login-action@v2
      with:
        username: ${{ secrets.DOCKER_USERNAME }}
        password: ${{ secrets.DOCKER_PASSWORD }}

2-7. docker build & push


  • docker build 후 push 하는 명령어이다.

현재는 main과 develop 환경이 동일하기 때문에
docker build 할 때 docker hub repository 이름이 동일하지만,
나중에 환경이 달라지면 docker hub repository를 다르게 설정해야한다.

즉, 설정 파일(application.yml)이 달라지면(나누면)
docker hub repository도 다르게 설정해야함.


    # docker build & push to production
    - name: Docker build & push to prod
      if: contains(github.ref, 'main')
      run: |
        docker build -f Dockerfile -t ${{ secrets.DOCKER_REPO }}/agaproject .
        docker push ${{ secrets.DOCKER_REPO }}/agaproject

    # docker build & push to develop
    - name: Docker build & push to dev
      if: contains(github.ref, 'develop')
      run: |
        docker build -f Dockerfile -t ${{ secrets.DOCKER_REPO }}/agaproject .
        docker push ${{ secrets.DOCKER_REPO }}/agaproject

2-8. deploy to EC2


  • EC2로 접속해서 pull & run 명령을 실행하는 명령어이다.

여기도 마찬가지로 분리시켰는데,
현재는 설정파일이 동일하기 때문에 상관없지만,
분리되면 여기도 docker repository를 달리해서 설정해줘야함.


    ## deploy to production
    - name: Deploy to prod
      uses: appleboy/ssh-action@master
      id: deploy-prod
      if: contains(github.ref, 'main')
      with:
        host: ${{ secrets.HOST_PROD }} # EC2 퍼블릭 IPv4 DNS
        username: ubuntu
        key: ${{ secrets.PRIVATE_KEY }}
        envs: GITHUB_SHA
        script: |
          sudo docker ps
          sudo docker rm -f $(docker ps -qa)
          sudo docker pull ${{ secrets.DOCKER_REPO }}/agaproject
          sudo docker run -d -p 8080:8080 ${{ secrets.DOCKER_REPO }}/agaproject
          sudo docker image prune -f
## ## sudo docker run -d -p 8080:8080 ${{ secrets.DOCKER_REPO }}/agaproject

    ## deploy to develop
    - name: Deploy to dev
      uses: appleboy/ssh-action@master
      id: deploy-dev
      if: contains(github.ref, 'develop')
      with:
        host: ${{ secrets.HOST_DEV }} # EC2 퍼블릭 IPv4 DNS
        username: ${{ secrets.USERNAME }} # ubuntu
        password: ${{ secrets.PASSWORD }}
        port: 22
        key: ${{ secrets.PRIVATE_KEY }}
        script: |
          sudo docker ps
          sudo docker rm -f $(docker ps -qa)
          sudo docker pull ${{ secrets.DOCKER_REPO }}/agaproject
          sudo docker run -d -p 8080:8080 ${{ secrets.DOCKER_REPO }}/agaproject
          sudo docker image prune -f

3 - secrets 파일 설정


  • 스크립트 파일 중간에 ${{ secrets.??? }} 이렇게 되어있는 것들이 있다.
    이 부분은 개인적으로 설정해서 실행해야한다.

보안이 필요한 부분들이라고 생각하면 된다.

이제 .yml파일을 생성해서 secrets에 정의된 내용들로
yml 파일이 실행되기 때문에 github에 .yml 파일을 올리지 않아도 된다.


  • secrets 파일을 설정하러가자

  • github -> settings -> Secrets and Variables -> Actions
    클릭해서 이동한다.


  • Repository secrets에서 New repository secret 클릭해서
    필요한 secret 파일을 설정(정의)한다.

아래는 secrets 파일 내용이다. (참고해서 생성하면 된다.)

DOCKER_USERNAME : DockerHub ID ( ex : user@gmail.com )

DOCKER_PASSWORD : DockerHub PW ( ex : password )

HOST_PROD : EC2 IPv4
( ex : 12.123.123.123 ( 탄력적 ip )
or
ec2-12-123-123-123.ap-northeast-2.compute.amazonaws.com
( 퍼블릭 ipv4 DNS ) )

PRIVATE_KEY : .pem 파일 복붙(--begin--, --end-- 까지 다 복붙해야함.)

HOST_DEV : EC2 IPv4
( ex : 12.123.123.123 )

USERNAME : EC2 사용자 계정 ID ( ex : ubuntu )

PASSWORD : EC2 사용자 계정 PW ( ex : 1234 )
비밀번호 설정 참고 블로그

YML : 프로젝트 .yml 파일에 설정한 내용

  • HOST_DEV와 HOST_PROD를 나눈 이유도 서버 여러 개 실행을 위해서
    미리 나눈 것이다. ( 현재는 동일한 IPv4 주소가 설정되어있음 )

이제 main 이나 develop에서 push 하고
gitActions 동작이 잘되는지 확인하면 된다.


  • EC2 서버에서도 잘 돌아가고 있는지 확인하자.

docker ps 명령어로 확인 가능하다.

sudo docker logs ${Container ID} 입력하면 log도 뜬다.

참고로 docker 명령어는 해당 velog를 참조하였다.


마무리😎

이렇게 자동 배포까지 해보았다.
정말 많은 삽질을 했지만, 그 만큼 또 많은 깨달음을 얻을 수 있었다.
하다가 진짜 에러 하나 해결하면 또 다른 에러가 뜨고
디버깅 파티였다....
하다하다 지쳐서 "에러 해결 못하겠다" 싶을 때가 있었는데,
이때 포기하고 싶었지만,
이것도 못하면 무슨 개발자를 하겠는가...라는 생각으로 8시간 넘게 삽질하고 고치고 했던 것 같다.

이 글을 읽고 있는 분들은 편하게 자동 배포를 하시기를 바라면서
트러블 슈팅으로 글을 마치도록 하겠습니다.

👏👏👏 감사합니다 👏👏👏

#참고 : 카카오 기술 블로그에 docker 속도향상(도커 캐시 적용)에 대해서
정리가 되어있는 곳도 링크를 걸고 가도록 하겠다!#


🧨트러블 슈팅🧨


🍀 1 - 폴더 구조 문제 🍀


Error: Error: Cannot locate a Gradle wrapper properties file at 

  • 자꾸 wrapper를 찾을 수 없다고 에러가 나타났는데
    원래 소스 코드 구조가 .idea, gralde/wrapper, src 등 모두 폴더 다른 폴더 안에 한번 더 감싸서 존재했는데

build 할 때 wrapper를 못찾는거 같아서 폴더를 벗겼습니다.
폴더구조는 아래와 같음


🍀 2 - chmod와 gradle init 추가 🍀


Error: Error: Cannot locate a Gradle wrapper properties file at 
  • 폴더 구조를 바꿔도 wrapper를 못찾아서
    gradle init과 chmod로 graldew 권한을 변경함.

참고 : init 추가(1), init 추가(2)

아래 명령어를 추가했음. (전체 파일 내용에는 추가한 내용으로 들어가있다.)

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

결국, 두 개를 추가하니 해결되었는데,
나중에 init을 빼고 빌드가 되서 그냥 빼버렸다.


🍀 3 - push 감지못함 🍀


  • gitActions에서 push를 해도 아무리 상태가 안뜨길래 이상해서 명령어를 쭉 훓었는데 branches["main, develop"] 이렇게 같이 묶어서 정의를 했었다.

  • 아래와 같이 "main", "develop" 정확하게 분류하니 해결이 되었다.

- on:
  push:
    branches: [ "main", "develop" ]
branches ["main, develoop"] 으로 해놓은거 위 처럼 변경

🍀 4 - JDK 버전 비호환 🍀


  • 여러 블로그를 보면서 참고했는데 JDK Setting에서 대부분 11을 쓰길래 아무생각없이 11버전으로 했는데,

  • spring boot 3.xxx JDK 머시기 버전이 안 맞다고 해서
    프로젝트도 3.xxx버전이고 해서 모든(로컬환경, 우분투)곳에 JDK 17로 맞췄다.

gitActions는 JDK를 별도로 관리한다는데
나는 버전을 맞춰야 실행이됐다.

    # JDK setting - github actions에서 사용할 JDK 설정 (aws 과 project의 java 버전과 별도로 관리)
    - uses: actions/checkout@v3
    - name: Set up JDK 17
      uses: actions/setup-java@v3
      with:
        java-version: '17'
        distribution: 'temurin'

🍀 5 - Dockerfile 찾지 못함 🍀


Run docker build -f Dockerfile -t ***/agaproject .
#0 building with "default" instance using docker driver

  • 이렇게 Dockerfile을 찾지 못한다고해서 찾아봤는데,
    main이랑 develop 브랜치에서 왔다갔다 작업하다 보니
    main에서 빌드하는데 dockerfile이 없는 것을 확인하고,
    Dockerfile생성해서, 빌드했더니 해결되었다.

이 문제는 폴더구조에도 연관되어있으니 참고하시길!!


🍀 6 - EC2 서버 접속 못함 🍀


======CMD======
sudo docker ps
sudo docker pull /agaproject
sudo docker run -d -p 8081:8081
/agaproject
sudo docker image prune -f

======END======
2023/12/23 13:24:08 dial tcp ***:22: i/o timeout


  • 자꾸 timeout이 떠서 .yml을 잘못정의했나 계속 찾았는데 문제가 없었다.

  • 저번에 친구 EC2 서버 접속 도와주다가 인바운드 규칙 안 열었을때랑 에러가 같아서 aws ec2 서버 인바운드 규칙을 열어줬더니 해결되었다.


ssh로 접속하는 모든 ip를 허용하게끔 만들었음.
어차피 .pem 파일이 없거나,
EC2 서버 ID, PASSWORD를 모르면 접속이 안되니까 상관 없었다.

ID, PASSWORD 설정은 꼭 해주자!


🍀 7 - EC2에서 spring boot DB연결 안됨 🍀


com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure

  • AWS RDS를 사용해 DB를 쓰고있는데 로컬에서는 연결 잘 하더니,
    갑자기 docker 활용해서 EC2 서버에서 실행하니 DB 연결이 안 된다고 한다.

  • 이것도 AWS 인바운드 규칙 문제인가 싶어서 보안 규칙을 봤는데 EC2 서버랑 규칙 규칙을 동일하게 설정해줬다.

많은 자료를 찾아보다가 ssl 문제라는 것을 깨달았고,
application.yml 파일에 DB 연결 url부분 맨 뒤에
&useSSL=false 이거를 추가하면 된다.

spring:
  datasource:
    url: jdbc:mysql://엔드포인트:3306/alarmdb?serverTimezone=Asia/Seoul&useSSL=false
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: 
    password: 

🍀 8 - swagger 안 열림 🍀


  • 실행 다 되었는데 Swagger가 안 열린다...

  • AWS 인바운드 규칙에 8080 포트를 열어주니 해결되었다.



🍀 9 - Ubuntu Password 설정 🍀


  • 우분투 Password 설정을 해주어야한다.
  • ##deploy to develop 2개 나뉘어져 있는 부분에
    한 개는 .pem 으로 접속하고 한 개는 ID, PW로 접속하기 때문

Password 설정 방법은 아래와 같다.

EC2서버(우분투)에서 입력하자

# 1. sshd config 파일 들어가기
sudo vim /etc/ssh/sshd_config

# 2. PasswordAuthentication부분을 no에서 yes로 변경해주기

# 3. 시스템 재부팅 (위에 수정사항 적용하기)
sudo systemctl restart sshd

# 4. 패스워드 설정하기 (ubuntu 자리에 자기 ec2-user 이름 들어가면 된다.)
sudo passwd ubuntu

# 5. 로컬 (개발환경)에서 접속해보기
#ssh ec2-user@<ec2 instance-ip> -p <port 번호>
ssh ubuntu@00.00.00.0 -p 22

길고 긴 여정이 마무리되었다...

profile
끝내주는 남자

2개의 댓글

comment-user-thumbnail
2023년 12월 29일

도움이 되었습니다.

1개의 답글