배포(Deploy)

Tomcat + Spring framework + AWS + Docker + GithubAction CICD배포하기

리콜 2024. 10. 17. 13:09

KB IT's Your Life의 마지막 프로젝트를 어제까지 마감하여 끝을 냈다.

프로젝트를 진행하며 CICD 를 플로우를 작성해서 배포를 하였다.

 

CICD를 구축한다면 나중에 마감일에 수정 삭제를 마감일까지 하게 되는데

 

빌드해서 .war 파일만들어서 배포환경의 tomcat에 .war파일 넣고~ 너무 불편하다...

이전에 다른 프로젝트( React + nodeJS 등등 ) 할때에도 CICD 구축의 편리성을 느꼈기 때문에 
이번 프로젝트에도 구축하려고 하였다.

 

먼저 환경은



톰캣 ( Tomcat 9.0.94 ) 
 


Spring Framework ( spring boot 아님!! )
 


Java version 17

 

에서 개발을 진행 하였습니다.

 

 

CICD 프레임 워크로는 

 

 

Github Action 을 사용하였습니다.

 

무료에 Github 에서 바로 설정 할 수 있어 편리합니다.

 

Deploy 환경은 

 

 

AWS EC2 - ubuntu 환경에서 진행하였습니다.

 

 

먼저 제가 생각한 WorkFlow는

 

  1.  백엔드 코드를 작성하고 Github의 기능 branch에 Psuh
  2. Github 의 배포 브랜치로 생각한 mater 브랜치에 배포가 된다면 Github Action에서 이를 감지하고 수행
  3. github action에서 spring 프로젝트를 빌드, tomcat에 넣어 docker 이미지로 빌드
  4. Docker 이미지를 Docker Hub 에 push
  5. ec2에서 image를 pull받아 이미지를 받는다.
  6. 기존의 컨테이너에서 새로 받은 최신 프로젝트 이미지를 run 하여 container를 수행한다.

 

라는 플로우를 생각하여 작성하였습니다.

 

최종 CICD.yml

 

name: Spring Framework CI/CD with Gradle

on:
  push:
    branches: [ master ]

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

    steps:
      - name: Checkout
        uses: actions/checkout@v4

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

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

      - name: application.properties 파일 생성
        run: |
          echo "application.properties 파일 생성"
          echo "src/main/resources 폴더 생성"
          mkdir -p src/main/resources 
          echo "${{ secrets.APPLICATION_PROPERTIES }}" > src/main/resources/application.properties

      - name: Build with Gradle
        run: |
          echo "gradle 빌드 시작"
          chmod +x ./gradlew
          ./gradlew build
          echo "gradle 빌드 완료"

      - name: 도커 로그인
        run: |
          echo "Docker 로그인 중..."
          echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin

      - name: 도커 이미지 빌드
        run: |
          docker build -t ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_IMAGE_NAME }}:${{ github.sha }} .
          docker tag ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_IMAGE_NAME }}:${{ github.sha }} ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_IMAGE_NAME }}:latest

      - name: Docker Hub 에 Push
        run: |
          docker push ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_IMAGE_NAME }}:${{ github.sha }}
          docker push ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_IMAGE_NAME }}:latest

      - name: DEPLOY
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.EC2_PUBLIC_IP }}
          username: ${{ secrets.EC2_USERNAME }}
          key: ${{ secrets.EC2_SSH_KEY }}
          port: ${{ secrets.EC2_SSH_PORT }}
          script: |
            echo "도커 이미지 가져 오는중..."
            docker pull  ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_IMAGE_NAME }}:latest 
            echo "실행중인 컨테이너 확인"
            if [ "$(docker ps -q -f name=${{ secrets.DOCKER_CONTAINER_NAME }})" ]; then
              echo "실행중이던 컨테이너 중지"
              docker stop ${{ secrets.DOCKER_CONTAINER_NAME }}
              echo "실행중이던 컨테이너 삭제"
              docker rm ${{ secrets.DOCKER_CONTAINER_NAME }}
            else
              echo "실행중인 컨테이너 없음"
            fi
            echo "pull과정에서 생긴 none 태그 images 삭제"
            docker rmi $(docker images -f "dangling=true" -q)
            echo "새로운 컨테이너 실행"
            docker run -d --name ${{ secrets.DOCKER_CONTAINER_NAME }} -p 8080:8080 ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_IMAGE_NAME }}:latest

 

 

최종 Dockerfile

# Tomcat 이미지를 기반으로 설정
FROM tomcat:9.0.94-jdk17-temurin

# 컨테이너 내부에서 작업 디렉토리 설정
WORKDIR /usr/local/tomcat/webapps

# 빌드된 WAR 파일을 컨테이너에 복사
COPY build/libs/kb_i_dle_backend-1.0-SNAPSHOT.war /usr/local/tomcat/webapps/ROOT.war

# 애플리케이션 포트 노출
EXPOSE 8080

# Tomcat 실행 명령
CMD ["catalina.sh", "run"]

 

 

먼저 

name: Spring Framework CI/CD with Gradle

on:
  push:
    branches: [ master ]

 

  • name은 이 workflow의 이름이다.
    • 생각하는 이름을 작성
  • on 은 어떤 이벤트때 이 스크립트를 실행 할지 정하면 되는데 mater branch에 merge되어 push되었을때를 하고 싶기 때문에 push 시 라고 작성 했다.
steps:
      - name: Checkout
        uses: actions/checkout@v4

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

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

 

이제 steps는 단계 별로 나누어 작업들을 설정 할 수 있다.

 

  • - name 으로 어떤 작업인지 작성해두고 작성해야 나중에 실행 화면에서 보기 편하기 때문에 -name을 작성하고 작성하는게 좋다.
  • 그리고 use를 이용하여 Action 에서 사용할 수 있는 여러 Open Source, Tool 들을 사용해서 아래의 작업들을 수행한다.
  • Checkout은 " 깃허브의 코드 저장소에 올려둔 코드를 CI 서버로 내려받은 후에 특정 브랜치로 전환하는 행위 " 라는데 쉽게 말하면 github code 들을 cloud 환경에 가져 간다고 생각하면된다. 따라서 action사용할때 첫 번째로, 필수이다.
  • Setup JDK 17은 JDK 17 버전에서 개발했기 때문에 같은 환경을 위해 Action의 JDK 도 17로 맞추어 주었다.
  • Cache Gradle 은 이제 가져간 코드를 gradle을 이용하여 build해야하는데 Build과정은 오래걸리는 작업 중 하나이다.
    • 따라서 Cache를 이용하면 좀 더 빨리 Build할 수 있기에 사용하였다.
      - name: application.properties 파일 생성
        run: |
          echo "application.properties 파일 생성"
          echo "src/main/resources 폴더 생성"
          mkdir -p src/main/resources 
          echo "${{ secrets.APPLICATION_PROPERTIES }}" > src/main/resources/application.properties

      - name: Build with Gradle
        run: |
          echo "gradle 빌드 시작"
          chmod +x ./gradlew
          ./gradlew build
          echo "gradle 빌드 완료"

 

그런데 프로젝트의 환경 변수인 application.properties를 github에 올리지 않았었다!!!

따라서 action에서 빌드를 할려는데 이 파일이 없어 아마 안될 것이다.

그래서 action에서 제공하는 github의 secret을 이용해서 거기에 APPLICATION_PROPERTIES 라는 키값으로 저장해 둔뒤 이 파일을 src/main/resouces/application.properties라는 파일로 생성 해주었다.( 프로젝트와 같은 폴더 )

그뒤 gradle로 빌드를 하였 .war을 생성하였다.

Action

      - name: 도커 로그인
        run: |
          echo "Docker 로그인 중..."
          echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin

      - name: 도커 이미지 빌드
        run: |
          docker build -t ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_IMAGE_NAME }}:${{ github.sha }} .
          docker tag ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_IMAGE_NAME }}:${{ github.sha }} ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_IMAGE_NAME }}:latest

      - name: Docker Hub 에 Push
        run: |
          docker push ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_IMAGE_NAME }}:${{ github.sha }}
          docker push ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_IMAGE_NAME }}:latest

 

 

Dockerfile

# Tomcat 이미지를 기반으로 설정
FROM tomcat:9.0.94-jdk17-temurin

# 컨테이너 내부에서 작업 디렉토리 설정
WORKDIR /usr/local/tomcat/webapps

# 빌드된 WAR 파일을 컨테이너에 복사
COPY build/libs/kb_i_dle_backend-1.0-SNAPSHOT.war /usr/local/tomcat/webapps/ROOT.war

# 애플리케이션 포트 노출
EXPOSE 8080

# Tomcat 실행 명령
CMD ["catalina.sh", "run"]

 

  • 그뒤 이제 Gradle로 빌드를 한 .war 파일을 가지고 Dockerfile을 수행한다!
  • 먼저 Action의 컴퓨터에서 docker 이미지를 빌드 하면 Docker Hub에 올릴때 로그인이 되어 있어야 하기에 로그인을 해준다.
  • 그리고 Dockerfile을 이용해 이미지를 빌드한다.
    • Dockerfile은 우리가 Tomcat 9.0.94 버전을 사용했기에 DockerHub 에서 9.0.94 버전을 찾았고 jdk 17 버전이 여러개 있었는데 그중 아까 JDK 깔때 와 같은 temurin 버전을 사용했다.
    • 그뒤 빌드한 war 파일을 Tomcat 이미지의 소스 안에 넣어준다.
      • 이때 ROOT.war로 하는게 좋다!!! ROOT.war 파일로 해야 배포시 주소가 " 원격주소/ " 로 된다.
      • 만약 ROOT 가 아니라 backend.war로 하면 " 원격주소/backend " 가 기본 주소가 된다.
    • 그뒤 tomcat의 기본 port인 8080을 설정해주고
    • tomcat 실행 명령어를 통해 컨테이너를 실행 시켜 줄 것이다.
  • 이때 이미지를 두개 만드는데 ${{ github.sha }} 는 이 action을 사용할때 생성되는 랜덤 코드라 생각하면 된다.
  • 코드로 만들어진 이미지와 latest 버전 이미지를 만드는데
    • 이렇게 만들면 dockerhub에서 코드를 통해 이미지 버전 관리
    • latest 를 통해 ec2 에서 이미지를 pull 받아 컨테이너를 실행할때 이미지 관리가 용이 할 것이다. 
      • 처음에는 그냥 github.sha 버전으로 실행했는데 ec2 에 이미지가 계속 쌓이는 문제를 해결하기 위해 이렇게 작성했다.
  • 이렇게 만든 도커 이미지를 Docker hub에 Push한다.
      - name: DEPLOY
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.EC2_PUBLIC_IP }}
          username: ${{ secrets.EC2_USERNAME }}
          key: ${{ secrets.EC2_SSH_KEY }}
          port: ${{ secrets.EC2_SSH_PORT }}
          script: |
            echo "도커 이미지 가져 오는중..."
            docker pull  ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_IMAGE_NAME }}:latest 
            echo "실행중인 컨테이너 확인"
            if [ "$(docker ps -q -f name=${{ secrets.DOCKER_CONTAINER_NAME }})" ]; then
              echo "실행중이던 컨테이너 중지"
              docker stop ${{ secrets.DOCKER_CONTAINER_NAME }}
              echo "실행중이던 컨테이너 삭제"
              docker rm ${{ secrets.DOCKER_CONTAINER_NAME }}
            else
              echo "실행중인 컨테이너 없음"
            fi
            echo "pull과정에서 생긴 none 태그 images 삭제"
            docker rmi $(docker images -f "dangling=true" -q)
            echo "새로운 컨테이너 실행"
            docker run -d --name ${{ secrets.DOCKER_CONTAINER_NAME }} -p 8080:8080 ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_IMAGE_NAME }}:latest

 

그리고 appleboy라는 오픈 소스를 이용해 ec2 에 접속한다.

action에서 run으로 ssh 접속 명령어를 작성해 접속해도 된다는데 나는 그렇게 하니 자꾸 오류나서

appleboy를 사용하니 바로 해결 되었다.

 

이렇게 ssh 로 ec2 에 접속해 ec2에서 명령어를 실행해 주었다.

 

  • 이미지를 가져오는데 이때 미리 ec2에 docker hub 로그인을 해두어야한다.
  • 그리고 이미지를 run하면 되는데 처음에는 괜찮지만 CICD를 계속하면 이미 컨테이너가 실행중이다.
    • 그래서 실행중인지 확인하고 각 상황에 맞게 컨테이너를 삭제해준다.
  • 또한 이미지를 pull하는 과정에서 none 태그 이미지가 생기게 되는데 이를 관리하기 위해
  • none태그 이미지들을 삭제하여 ec2 용량을 관리하였다.
  • 그뒤 컨테이너를 실행하였다.

 

 

아주 잘 된다!!!!

 

 

반응형