[Server] Github Actions + Docker를 이용한 CI/CD 배포

김강욱·2024년 6월 15일
0

Project-DoggyWalky

목록 보기
5/5
post-thumbnail

이번 포스팅에서는 Github actionDocker를 이용하여 DoggyWalky를 배포하는 시간을 가져보도록 하겠습니다.

Github ActionsGithub에서 코드 변경이 발생했을 때 변경된 코드를 자동으로 배포 서버에 반영해주는 기능을 제공합니다. 이와 함께 Docker를 통해 컨테이너 이미지화 하여 배포 서버에 해당 컨테이너를 실행시켜 배포해보도록 하겠습니다.


🙌 왜 Github Actions, Docker를 선택하였나요?

Travis CI, Jenkins 등 다양한 CI/CD 도구가 있지만 대부분 유료이거나, 부분적 유료인 경우가 많습니다.

그에 비해 Github Actionspublic repository인 경우 무료이며 Github에 공식적으로 내장된 기능이므로, Github와 함께 관리하기 편하기 때문에 선택하게 되었습니다. Jenkins 같은 경우 동시에 진행중인 EverTrip 프로젝트에서 사용해보기로 하였습니다.

또한 Docker를 사용한 이유는 OS 환경에 종속적이지 않도록 하기 위함이었습니다. 로컬 서버는 윈도우 OS를 사용하였지만 배포 서버는 ubuntu OS였기에 애플리케이션을 OS 환경에 종속적이지 않게 Docker를 활용하여 애플리케이션 이미지를 빌드하고 Docker hub저장소에 올리고 배포 서버에서 해당 이미지를 다운받고 실행하도록 구현하였습니다.


🙌 CI/CD 적용하기

이제 CI/CD를 적용해보도록 하겠습니다. 먼저 Github ActionsWorkflow를 작성하고, 환경 변수를 설정해준 이후 배포 서버에서 확인해보도록 하겠습니다.

🎈 Github Actions Workflow 작성(스크립트 파일 작성)

우선 프로젝트 루트 경로 아래에 .github/workflows/github-actions.yml 파일을 생성합니다.

github-actions.yml 파일에 자동화 배포에 대한 Workflow를 작성하게 됩니다. 해당 Workflow의 순서는 아래와 같습니다.

1. 코드 변경 시 배포 스크립트 실행
2. JDK 설정(github actions에서 사용)
3. gradle caching
4. gradle build 실행
5. Docker 컨테이너 이미지 빌드 및 푸시
6. EC2 서버에 배포하기

📝 github-actions.yml 파일

# github repository actions 페이지에 나타날 이름
name: CI/CD using github actions & docker

# 1. event trigger
# main 브랜치에 push가 되었을 때 실행
on:
  push:
    branches: [ "main"]

permissions:
  contents: read

jobs:
  CI-CD:
    runs-on: ubuntu-latest
    steps:

      # 2. JDK setting - github actions에서 사용할 JDK 설정 (프로젝트나 AWS의 java 버전과 달라도 무방)
      - uses: actions/checkout@v3
      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'temurin'

      # 3. 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-


      # 4. gradle build
      - name: Build Test with Gradle
        env:
          JAVA_TOOL_OPTIONS: "-Djasypt.encryptor.password=${{ secrets.JASYPT_PASSWORD }} -Dcom.amazonaws.sdk.disableEc2Metadata=true"
        run: ./gradlew clean build --info


      # 5. docker build & push to production
      - name: Docker build & push to prod
        if: contains(github.ref, 'main')
        run: |
          docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
          docker build -f Dockerfile --build-arg JASYPT_PASSWORD=${{ secrets.JASYPT_PASSWORD }} -t ${{ secrets.DOCKER_USERNAME }}/docker-doggywalky:latest .
          docker push ${{ secrets.DOCKER_USERNAME }}/docker-doggywalky:latest

      # 6. 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 -q --filter "name=doggywalky-main" | grep -q . && sudo docker stop doggywalky-main && sudo docker rm doggywalky-main || true
            sudo docker pull ${{ secrets.DOCKER_USERNAME }}/docker-doggywalky:latest
            sudo docker run -d -p 8080:8080 --name doggywalky-main -e JASYPT_PASSWORD=${{ secrets.JASYPT_PASSWORD }} -e DISABLE_EC2_METADATA=${{ secrets.DISABLE_EC2_METADATA }} ${{ secrets.DOCKER_USERNAME }}/docker-doggywalky:latest
            sudo docker image prune -f


github-actions.yml 파일을 전체적으로 살펴보도록 하겠습니다.

1. 코드 변경 시 배포 스크립트 실행

# github repository actions 페이지에 나타날 이름
name: CI/CD using github actions & docker

# 1. event trigger
# main 브랜치에 push가 되었을 때 실행
on:
  push:
    branches: [ "main"]

permissions:
  contents: read

main 브랜치에 push가 발생했을 시 배포 스크립트가 실행된다는 것을 의미합니다.

저희 프로젝트는 Git Flow 전략을 사용하여 main 브랜치는 배포 서버용, develop 브랜치는 개발 서버용, feature는 각 기능 개발용으로 분류하였습니다.

feature 브랜치에서 기능을 개발하고 develop에 병합하여 테스트 후 최종적으로 main 브랜치에 푸쉬하게 되고 이때 배포 스크립트가 실행되게 됩니다.

permissions 섹션에서 contents: read 설정을 하여 GitHub Actions가 레포지토리의 콘텐츠에 접근할 수 있도록 허용하도록 하였습니다.

2. JDK 설정(github actions에서 사용)

      # 2. JDK setting - github actions에서 사용할 JDK 설정 (프로젝트나 AWS의 java 버전과 달라도 무방)
      - uses: actions/checkout@v3
      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'temurin'

Github Actions가 사용할 JDK를 설정하고 있습니다.

3. gradle caching

      # 3. 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-

Gradle을 캐싱해주는 작업을 진행합니다.
해당 코드는 없어도 무방하지만, 적용했을 시 빌드 시간을 단축할 수 있습니다.

4. gradle build 실행

      # 4. gradle build
      - name: Build Test with Gradle
        env:
          JAVA_TOOL_OPTIONS: "-Djasypt.encryptor.password=${{ secrets.JASYPT_PASSWORD }} -Dcom.amazonaws.sdk.disableEc2Metadata=true"
        run: ./gradlew clean build --info

Gradle build를 진행하여 테스트 및 빌드 작업을 하게 됩니다. 프로젝트의 환경 변수 값을 설정하여 넣어주고 로그 출력을 위해 ./gradlew clean build --info 명령어를 사용하였습니다.

5. Docker 컨테이너 이미지 빌드 및 푸시

      # 5. docker build & push to production
      - name: Docker build & push to prod
        if: contains(github.ref, 'main')
        run: |
          docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
          docker build -f Dockerfile --build-arg JASYPT_PASSWORD=${{ secrets.JASYPT_PASSWORD }} -t ${{ secrets.DOCKER_USERNAME }}/docker-doggywalky:latest .
          docker push ${{ secrets.DOCKER_USERNAME }}/docker-doggywalky:latest

배포하려는 애플리케이션을 컨테이너 이미지로 빌드하고 Docker Hub 레포지토리에 푸시하는 작업을 진행합니다.

컨테이너 이미지를 빌드할 때 Dockerfile이 필요하게 됩니다. Dockerfile에 작성한 명령어들은 이미지를 빌드하는 과정에서 실제로 실행되며, 그 결과는 이미지에 반영됩니다. 이후 이미지가 나중에 컨테이너를 실행할 때 사용되게 됩니다.

--build-arg JASYPT_PASSWORD=${{ secrets.JASYPT_PASSWORD }} 코드는 컨테이너 이미지 빌드 시 사용할 build argument를 지정합니다. 해당 build argumentDockerfile에서는 ARG argument_name와 같은 형식으로 참조할 수 있습니다.

위 코드에서 docker-doggywalky:latestDocker Hub에 올라갈 컨테이너 이미지 이름입니다.

6. EC2 서버에 배포하기

      # 6. 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 -q --filter "name=doggywalky-main" | grep -q . && sudo docker stop doggywalky-main && sudo docker rm doggywalky-main || true
            sudo docker pull ${{ secrets.DOCKER_USERNAME }}/docker-doggywalky:latest
            sudo docker run -d -p 8080:8080 --name doggywalky-main -e JASYPT_PASSWORD=${{ secrets.JASYPT_PASSWORD }} -e DISABLE_EC2_METADATA=${{ secrets.DISABLE_EC2_METADATA }} ${{ secrets.DOCKER_USERNAME }}/docker-doggywalky:latest
            sudo docker image prune -f

배포하려는 EC2 서버에 원격 접속한 후 Docker Hub에 푸시했던 프로젝트 컨테이너 이미지를 pull 땡겨서 실행시키는 작업입니다.

EC2 원격 접속에는 appleboy를 사용합니다.
이 때 기입할 정보는 아래와 같습니다.

host: EC2의 IP 주소 혹은 DNS

username: 인스턴스 생성 시 선택한 OS의 기본 사용자 이름

key: EC2 생성 시 받은 pem 파일의 내용


🎈 Dockerfile 작성

Dockerfile도 마찬가지로 프로젝트 루트 경로의 하위에 생성해주시면 됩니다.

# OpenJDK 17 버전의 환경을 구성
FROM eclipse-temurin:17

# 빌드 시 환경 변수를 설정하기 위해 ARG 사용
ARG JAR_FILE=build/libs/*.jar

# JAR_FILE을 app.jar로 복사
COPY ${JAR_FILE} /app/app.jar

# 런타임에 사용할 환경 변수를 설정하기 위해 ENV 사용
ARG JASYPT_PASSWORD
ENV JASYPT_PASSWORD_ENV=${JASYPT_PASSWORD}

# 환경 변수를 사용하여 애플리케이션 실행
ENTRYPOINT ["sh", "-c", "java -jar -Djasypt.encryptor.password=${JASYPT_PASSWORD_ENV} -Dcom.amazonaws.sdk.disableEc2Metadata=true /app/app.jar"]

컨테이너 이미지를 빌드할 때 위의 명령어들이 차례로 실행되게 됩니다.

FROM eclipse-temurin:17

eclipse-temurin:17 이미지를 기반으로 새로운 이미지를 생성합니다.

ARG JAR_FILE=build/libs/*.jar

빌드 인수(build argument)로 JAR_FILE을 설정합니다. 기본값은 build/libs/*.jar입니다. 애플리케이션의 JAR 파일 경로를 지정합니다.

COPY ${JAR_FILE} /app/app.jar

ARG로 지정된 JAR_FILE 경로에 있는 파일을 도커 이미지의 /app/app.jar 위치로 복사합니다.

ARG JASYPT_PASSWORD
ENV JASYPT_PASSWORD_ENV=${JASYPT_PASSWORD}

빌드 인수 JASYPT_PASSWORD를 설정하고
환경 변수 JASYPT_PASSWORD_ENV를 JASYPT_PASSWORD 값으로 설정합니다. 이 환경 변수는 애플리케이션 실행 시 사용됩니다.

ENTRYPOINT ["sh", "-c", "java -jar -Djasypt.encryptor.password=${JASYPT_PASSWORD_ENV} -Dcom.amazonaws.sdk.disableEc2Metadata=true /app/app.jar"]

컨테이너가 시작될 때 실행할 명령을 정의합니다. 환경 변수로 JASYPT_PASSWORD_ENV를 사용하여 애플리케이션을 실행하게 됩니다.


🎈 Github Actions 환경 변수 설정(Secrets)

GithubSecret Key를 설정하여 환경 변수를 세팅할 수 있습니다.

레포지토리의 Settings 태그에서

다음과 같이 Secret Key를 설정할 수 있습니다. 설정한 Secret Key들은 github-actions.yml 파일에서 ${{ secrets.VARIABLE_NAME }} 다음과 같이 사용할 수 있습니다.


🎈 배포 서버 로그 출력해보기

이제 main 브랜치에 푸쉬하여 CI/CD를 적용해보도록 하겠습니다.

Githun ActionsWorkflow을 확인하려면 아래로 들어가시면 됩니다. 현재 동작하고 있는 Workflow를 클릭하시면 순차적으로 동작하고 있는 Job들을 확인하실 수 있습니다.

위 사진과 같이 CI/CD가 정상적으로 완료된 것을 확인하였고, 배포 서버에 가서 컨테이너 실행 여부를 확인해보도록 하겠습니다.

docker ps 명령어 실행 결과 doggywalky-main 이름의 컨테이너가 실행 중인 것을 확인할 수 있습니다.

Spring Rest Docs를 사용하여 만든 api.html 페이지를 확인해보도록 하겠습니다.

배포 서버의 api.html 역시 정상적으로 접속이 가능한 것을 확인할 수 있었습니다.


🙌 참고 : ChatServer 배포

DoggyWalky 프로젝트는 Main ServerChatServer로 분류되어 있기 때문에 ChatServer도 배포해주었습니다.

아직 로드 밸런싱을 적용하기 전이라 같은 EC2 서버에 8081 포트로 배포해주었습니다. 추후에 로드 밸런싱을 적용해볼 예정입니다.

ChatServer 배포는 github-actions.yml 파일의 deploy to production 부분만 수정하였습니다.

      # 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 -q --filter "name=doggywalky-chat" | grep -q . && sudo docker stop doggywalky-chat && sudo docker rm doggywalky-chat || true
            sudo docker pull ${{ secrets.DOCKER_USERNAME }}/docker-doggywalky-chat:latest
            sudo docker run -d --network host --name doggywalky-chat -e JASYPT_PASSWORD=${{ secrets.JASYPT_PASSWORD }} -e DISABLE_EC2_METADATA=${{ secrets.DISABLE_EC2_METADATA }} ${{ secrets.DOCKER_USERNAME }}/docker-doggywalky-chat:latest
            sudo docker image prune -f

sudo docker run -d --network host ...에서 네트워크 옵션을 사용하여 도커 컨테이너와 호스트가 동일한 IP 주소를 사용하게 되며, 모든 네트워크 인터페이스와 포트를 공유하도록 설정해주었습니다.

Github ActionsWorkflow 확인을 해보겠습니다.

정상적으로 CI/CD가 완료되었음을 확인할 수 있습니다. 이제 배포 서버로 가서 컨테이너 실행 여부를 확인해보겠습니다.

현재 실행되고 있는 컨테이너는 doggywalky-maindoggywalky-chat이 있음을 확인할 수 있습니다. 마지막으로 배포 서버의 8081 포트로 접속하여 api.html을 확인해보도록 하겠습니다.

ChatServer에 대한 API 명세가 제대로 나오는 것을 확인할 수 있습니다.


☕ 마치며..

이번 포스팅에서는 DockerGithub Actions를 활용하여 CI/CD를 적용해보는 시간을 가졌습니다. 해당 과정을 거치면서 로컬에서 작성한 개발 코드들의 배포 과정을 자동화하여 효율성을 높일 수 있었습니다.

CI/CD 파이프라인을 설정하는 데 다소 시간이 걸렸지만, 일단 완성되고 나니 자동화된 CI/CD가 수동 배포와 비교하여 훨씬 간편하고 효율적이라는 것을 직접 깨달을 수 있었습니다.

📌 참고

짱제이님의 Github Actions과 Docker을 활용한 CI/CD 구축 포스팅

profile
TO BE DEVELOPER

0개의 댓글

관련 채용 정보