[GitHub Actions] - Docker + Github Action(CI/CD)

CodeByHan·2024년 12월 19일
1

깃허브

목록 보기
4/6

오늘은 깃허브 액션으로 CI/CD 를 구축해보는 것을 정리해볼려고 한다.

CI/CD라는 말은 많이 들어봤는데 정확하게 무슨 뜻인지 잘 몰라 이참에 추가로 정리해볼려고 한다.

CI/CD

CI/CD지속적 통합(Continuous Integration)지속적 배포(Continuous Deployment or Continuous Delivery)를 의미한다.

CI

지속적 통합(CI)은 개발자들이 여러 개발자가 작업한 코드 변경 사항을 정기적으로 병합하고 자동으로 빌드 및 테스트하는 프로세스를 의미한다.

  • 자동화된 빌드 및 테스트
    • 코드가 버전 관리 시스템(Git 등)에 푸시되면 자동으로 빌드와 테스트가 실행
  • 코드 충돌 최소화
    • 개발자들은 자주 코드를 통합할 수 있으며, 코드가 충돌되는 현상(conflict)을 미리 발견
  • 테스트 포함
    • 지속적 테스트(Continuous Testing)를 통해 코드 품질을 보장하며, 단위 테스트(Unit Test), 통합 테스트(Integration Test) 등이 활용

CD

지속적 배포(CD)는 지속적으로 통합된 코드를 자동으로 프로덕션 환경에 배포하는 프로세스

  • 지속적 제공(Continuous Delivery)

    • CI를 거친 후, 배포 준비가 완료된 상태로 유지
  • 지속적 배포(Continuous Deployment)

    • 모든 단계가 완전히 자동화되어 코드는 검증 후 즉시 프로덕션 환경에 배포
  • 코드 변경 사항이 테스트 및 승인(approve)을 거쳐 자동으로 프로덕션 환경에 배포(merge to main)

  • 새로운 기능과 버그 수정 사항이 실제 사용자에게 빠르게 제공

  • 사용자 피드백을 수집하고 제품을 개선하는 속도를 향상 가능

GitHub Action

  • GitHub Actions는 GitHub에서 제공하는 CI/CD(지속적 통합 및 지속적 배포) 플랫폼
  • 소프트웨어 개발 워크플로우를 자동화할 수 있는 도구
  • 코드 푸시, 풀 리퀘스트, 스케줄링 등 다양한 이벤트에 따라 빌드, 테스트, 배포 작업을 자동으로 실행 가능
  • GitHub Actions는 YAML 파일로 정의되며, GitHub 저장소와 긴밀히 통합되어 있어 별도의 외부 CI/CD 도구 없이도 간편하게 사용가능

나는 스프링부트 프로젝트를 도커로 빌드하고 -> Ec2로 배포하는 작업을 해보았다.

일단 도커 이미지 파일을 작성한다.도커 이미지 파일은 최상단 루트 폴더에 작성하면 된다.

FROM openjdk:17-jdk-slim
ARG JAR_FILE=build/libs/your-app.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]

github workflow 만들기

레포지토리에서 Action을 누른후 자신에게 맞는 걸 선택하면 된다.나는 Java with Gradle을 선택했다.

그리고 CI/CD 파이프라인을 구축한다.

파이프라인 개요

파이프라인의 트리거 (Trigger)

on:
  push:
    branches: ["main"]
  pull_request:
    branches: ["main"]
  • push 트리거: main 브랜치에 코드가 푸시되면 파이프라인 실행
  • pull_request 트리거: main 브랜치로 향하는 Pull Request가 열리거나 업데이트될 때 실행
  • 이러한 트리거는 자동화를 통해 개발자 작업 없이도 CI/CD 파이프라인을 활성화

Job 정의

jobs:
  ci-cd:
    runs-on: ubuntu-latest
  • ci-cd Job: 파이프라인의 작업 단위를 정의
  • runs-on: GitHub Actions는 Ubuntu 환경에서 실행

CI 단계 (Continuous Integration)

소스 코드 체크아웃

- uses: actions/checkout@v4
  • GitHub 리포지토리의 코드를 가져온다.
  • 모든 CI/CD 과정은 이 코드를 기반으로 실행

JDK 설정

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

Gradle 설정 및 빌드

- name: Setup Gradle
  uses: gradle/actions/setup-gradle@v4.0.0
- name: Grant execute permission for gradlew
  run: chmod +x gradlew
- name: Build with Gradle Wrapper
  env: ... # 환경 변수 설정
  run: ./gradlew build -x test
  • Gradle은 빌드 도구로 설정
    • gradlew 실행 권한을 부여하고 빌드 명령어(./gradlew build -x test)를 실행합니다.
  • 테스트 제외 옵션 -x test를 사용하여 테스트 실행 없이 빌드
    • 빌드 과정 중 필요한 환경 변수를 GitHub Secrets로부터 가져온다.

CD 단계 (Continuous Deployment)

Docker 이미지 생성

- name: Build Docker Image
  run: docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/gunpo .
  • Dockerfile을 기반으로 애플리케이션 이미지를 생성

Docker Hub에 업로드

- name: Login to Docker Hub
  uses: docker/login-action@v2
  with:
    username: ${{ secrets.DOCKERHUB_USERNAME }}
    password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Push Docker Image to Docker Hub
  run: docker push ${{ secrets.DOCKERHUB_USERNAME }}/gunpo
  • Docker Hub에 로그인한 후 빌드한 이미지를 업로드

EC2 서버에 배포

- name: Deploy to EC2
  uses: appleboy/ssh-action@master
  with:
    host: ${{ secrets.EC2_HOST }}
    username: ec2-user
    key: ${{ secrets.EC2_PRIVATE_KEY }}
    script: |
      # Docker 이미지 가져오기
      sudo docker pull ${{ secrets.DOCKERHUB_USERNAME }}/gunpo:latest

      # 기존 컨테이너 중지 및 삭제
      if [ $(sudo docker ps -a -q -f name=gunpo) ]; then
        sudo docker stop gunpo
        sudo docker rm gunpo
      fi
  • SSH를 사용해 EC2 서버에 접속
  • 최신 Docker 이미지를 EC2에서 가져오고 기존 컨테이너를 중지 및 삭제한 후 새 컨테이너를 실행

환경 변수 설정 및 실행

echo "SPRING_DATASOURCE_URL=${{ secrets.SPRING_DATASOURCE_URL }}" > .env
sudo docker run -d -p 80:8080 \
  --name gunpo \
  --env-file .env \
  ${{ secrets.DOCKERHUB_USERNAME }}/gunpo:latest
  • .env 파일을 생성하여 애플리케이션의 설정값(환경 변수)을 주입
  • Docker 컨테이너를 실행하며 애플리케이션이 EC2 서버에서 동작하도록 기능

Redis 컨테이너 실행

sudo docker run -d --name Gunporedis -p 6379:6379 redis:latest

Docker 리소스 정리

sudo docker system prune -f

최종 Gradle.YAML

name: CI/CD using GitHub Actions & Docker

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

jobs:
  ci-cd:
    runs-on: ubuntu-latest
    permissions:
      contents: read

    steps:
      # 리포지토리 체크아웃
      - uses: actions/checkout@v4

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

      # Gradle 설정
      - name: Setup Gradle
        uses: gradle/actions/setup-gradle@v4.0.0

      # gradlew 실행 권한 부여
      - name: Grant execute permission for gradlew
        run: chmod +x gradlew

      # Gradle 빌드
      - name: Build with Gradle Wrapper
        env:
          SPRING_DATASOURCE_URL: ${{ secrets.SPRING_DATASOURCE_URL }}
          SPRING_DATASOURCE_USERNAME: ${{ secrets.SPRING_DATASOURCE_USERNAME }}
          SPRING_DATASOURCE_PASSWORD: ${{ secrets.SPRING_DATASOURCE_PASSWORD }}
          SERVICE_KEY: ${{ secrets.SERVICE_KEY }}
          JWT_SECRET: ${{ secrets.JWT_SECRET }}
          SPRING_DATA_REDIS_HOST: ${{ secrets.EC2_HOST }}
          REDIS_PASSWORD: ${{ secrets.REDIS_PASSWORD }}
          SPRING_MAIL_USERNAME: ${{ secrets.SPRING_MAIL_USERNAME }}
          SPRING_MAIL_PASSWORD: ${{ secrets.SPRING_MAIL_PASSWORD }}
          UPLOAD_DIR: ${{ secrets.UPLOAD_DIR }}
          GYEONGGI_CURRENCY_DATA_KEY: ${{ secrets.GYEONGGI_CURRENCY_DATA_KEY }}
          KAKAO_CLIENT_ID: ${{ secrets.KAKAO_CLIENT_ID }}
          KAKAO_CLIENT_SECRET: ${{ secrets.KAKAO_CLIENT_SECRET }}
          NAVER_CLIENT_ID: ${{ secrets.NAVER_CLIENT_ID }}
          NAVER_CLIENT_SECRET: ${{ secrets.NAVER_CLIENT_SECRET }}
          GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
          GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
        run: ./gradlew build -x test

      # Docker 이미지 빌드
      - name: Build Docker Image
        run: docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/gunpo .

      # Docker 로그인
      - name: Login to Docker Hub
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_PASSWORD }}

      # Docker 이미지 푸시
      - name: Push Docker Image to Docker Hub
        run: docker push ${{ secrets.DOCKERHUB_USERNAME }}/gunpo

      # EC2에 배포
      - name: Deploy to EC2
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ec2-user
          key: ${{ secrets.EC2_PRIVATE_KEY }}
          script: |
            # 최신 Docker 이미지 가져오기
            sudo docker pull ${{ secrets.DOCKERHUB_USERNAME }}/gunpo:latest

            # 기존 컨테이너 중지 및 삭제
            if [ $(sudo docker ps -a -q -f name=gunpo) ]; then
              sudo docker stop gunpo
              sudo docker rm gunpo
            fi

            if [ $(sudo docker ps -a -q -f name=Gunporedis) ]; then
              sudo docker stop Gunporedis
              sudo docker rm Gunporedis
            fi

            # .env 파일 생성
            echo "SPRING_DATASOURCE_URL=${{ secrets.SPRING_DATASOURCE_URL }}" > .env
            echo "SPRING_DATASOURCE_USERNAME=${{ secrets.SPRING_DATASOURCE_USERNAME }}" >> .env
            echo "SPRING_DATASOURCE_PASSWORD=${{ secrets.SPRING_DATASOURCE_PASSWORD }}" >> .env
            echo "SERVICE_KEY=${{ secrets.SERVICE_KEY }}" >> .env
            echo "JWT_SECRET=${{ secrets.JWT_SECRET }}" >> .env
            echo "SPRING_DATA_REDIS_HOST=${{ secrets.EC2_HOST }}" >> .env
            echo "REDIS_PASSWORD=${{ secrets.REDIS_PASSWORD }}" >> .env
            echo "SPRING_MAIL_USERNAME=${{ secrets.SPRING_MAIL_USERNAME }}" >> .env
            echo "SPRING_MAIL_PASSWORD=${{ secrets.SPRING_MAIL_PASSWORD }}" >> .env
            echo "UPLOAD_DIR=${{ secrets.UPLOAD_DIR }}" >> .env
            echo "GYEONGGI_CURRENCY_DATA_KEY=${{ secrets.GYEONGGI_CURRENCY_DATA_KEY }}" >> .env
            echo "KAKAO_CLIENT_ID=${{ secrets.KAKAO_CLIENT_ID }}" >> .env
            echo "KAKAO_CLIENT_SECRET=${{ secrets.KAKAO_CLIENT_SECRET }}" >> .env
            echo "NAVER_CLIENT_ID=${{ secrets.NAVER_CLIENT_ID }}" >> .env
            echo "NAVER_CLIENT_SECRET=${{ secrets.NAVER_CLIENT_SECRET }}" >> .env
            echo "GOOGLE_CLIENT_ID=${{ secrets.GOOGLE_CLIENT_ID }}" >> .env
            echo "GOOGLE_CLIENT_SECRET=${{ secrets.GOOGLE_CLIENT_SECRET }}" >> .env

            # Redis 컨테이너 실행
            sudo docker run -d --name Gunporedis -p 6379:6379 redis:latest

            # 애플리케이션 컨테이너 실행
            sudo docker run -d -p 80:8080 \
              --name gunpo \
              --env-file .env \
              ${{ secrets.DOCKERHUB_USERNAME }}/gunpo:latest

            # 사용하지 않는 Docker 리소스 정리
            sudo docker system prune -f

참고

커피고래의 노트

[CICD] GitHub Action으로 CI/CD 구축하기

What Is CI/CD?

profile
노력은 배신하지 않아 🔥

0개의 댓글