GithubActions 를 이용한 CI/CD ( nginX, redis, springboot )

조대훈·2024년 10월 30일
post-thumbnail

0. CI/CD 사전적 의미

Continuous Intergration Continoues Delivery

CI는 개발자를 위한 자동화 프로세스인 지속적 통합을 의미한다.
CD는 지속적인 서비스 제공 및 지속적 배포를 의미한다.

1. CI/CD 의 단계

1-1 CI/CD 의 사전적 의미


지속적 통합(CI)은 개발 팀이 코드를 지속적으로 통합하고, 이를 자동으로 테스트 하여 통합 버그를 최소화 하는 프로세스를 말한다.

  • git push를 할때마다 자동으로 build & test 를 실행
  • 자동화 배포를 통하여 빈번한 코드 통합이 가능하다.
  • 자동 테스트로 코드가 충돌 되는 현상을 미리 발견할 수 있다.

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

  • 보다 빠르게 기능 개발과 버그 수정 사항이 사용자들에게 제공된다.

1-2 CI/CD의 종류


  1. Jenlkins
  2. GitHubActions
  3. AWS CodePipeline
    ... 등

1-3 Jenkins vs GitHubactions


Jenkins
1. 높은 커스터 마이징
2. 풍부한 플러그인
3. 좀 더 세밀한 파이프라인 제어 가능
4. 설치와 유지 보수 필요
5. 별도의 서버 리소스가 필요
6. 초기 러닝커브 곡선이 있다.

GitHubActions
1. gitHub 와 완벽한 통합
2. 서버 리소스와 유지보수 불필요
3. 비교적 간단한 설정
4. YAML 기반의 직관적인 문법
5. GitHub 종속성
6. 비교적 제한적 커스터마이징

혼자 진행 하게 되는 소규모 1인 프로젝트인 현재 GitHubActions 를 통해 구현 하는 것이 낫다고 판단 했다.

1-4 CI/CD 전략


블루-그린

8080 포트로 연결된 컨테이너


8081 포트로 다른 버전의 컨테이너 생성


nginX.conf 수정 후 reload(업스트림 8081 수정


8080 컨테이너 제거

  • 두 개의 별도 컨테이너를 사용해서 애플리케이션을 업데이트 하는 방식
  • 새로운 업데이트를 반영한 애플리케이션 그린에 배포 -> 기존 웹서버, Gateway 등의 설정을 변경하여 그린으로 트래픽을 몰도록 한다 -> 그린 환경에서 서비스 운영
  • 추후 업데이트는 반대로 블루 업데이트 -> 블루 트래픽 설정 변경 -> 블루 환경에서 서비스 운영
  • 롤백이 쉽고 테스트, 검증 시간을 충분히 확보 할수 있다.
  • 인프라 소스가 두 배 가량 필요 하다.
  • 배포 시간이 다소 늘어난다.

Rolling

  • 새로 배포 되어야 하는 버전을 하나씩 순차적으로 적용 시키면서 배포하는 방식. 옛날 버전과 새로운 버전이 공존해 잘못하면 호환성 문제를 야기하기 쉽다.
  • 롤백이 어렵다.

카나리

  • 지정한 서버 또는 특정 User 에게 서비스를 시범해 운영하다가, 버그가 없고 정상적이라 판단 되면 전체적으로 배포하는 방식.

2.CI/CD 적용

2-1 배포에 앞서 기술적 의사 결정


가급적이면 비용 문제상 aws의 부수적인 서비스들은 이용하지 않는 방향으로 진행 했다. (elastic cache(redis), aws elastic load balancing, aws route53)

본 진행하는 프로젝트는 소규모 프로젝트로 비교적 설정이 간단하고 세밀한 workflow만 구현하면 되는 조건이라, jenkins가 아닌 githubactions으로 docker image 를 build 후 ec2 인스턴스에 배포 하기로 결정 했다. DB는 따로 ec2 에 올리지 않고 RDS를 이용 했다.SSL 설정은 route53을 이용하지 않고 가비아에서 도메인을 직접 구매 자체 DNS 서비스를 이용해 포팅 하기로 결정 했다. 인증서는 certbot을 이용해 Let's Encrypt 인증서를 받고 nginX 를 이용해 연동 해주었다. 수정이 잦은 conf 파일을 포함해 docker-compose.yml 파일은 workflow.yml 에 포함시켜 배포 했다. 다루기 민감한 값들과 application.yml은 base64로 인코딩 후 Repository secrets 에 담아줬다.

2-2 보완이 필요해 보이는 부분


application.yml 수정이 생길 때 마다 github secrets 에 수동으로 수정하는 작업을 거쳐야 하는데 추후 이 과정도 수정이 필요해 보인다. code Deploy를 추가하지 않고 단일 컨테이너 CI/CD 파이프라인이다. 빌드 후 2분 가량 멈추는 시간이 발생한다. 즉 무중단 배포는 아니다. 추후 기존 활용하고 있는 nginX의 로드밸런싱 기능과 codeDeploy 를 추가해 blue/green 배포 전략을 활용할 예정이다. master dev productuon 등 실무환경 처럼 branch를 별도로 나누지 않고 master branch로 push 시에 파이프라인이 진행되게 설정 했다.

2-3 workflow 흐름


  1. 소스 코드 체크아웃
  2. JAVA 개발 환경 설정
  3. 설정 파일 생성
  4. gradle 빌드
  5. Docker 이미지 생성 및 푸시
  6. EC2 인스턴스 배포

ec2 인스턴스에 443 포트(ssl), 80 포트(nginx), 8080 포트를 열고 인증서 발급 과정, ec2 생성과 rds 연결, IAM 생성 및 권한 설정, 도메인 구입 과정과 DNS 연결 과정, docker hub 연결 등의 과정들은 다루지 않을 예정.

workflow.yml

# 워크플로우의 이름 설정
name: Java CI/CD with Gradle

# 워크플로우 실행 조건 설정
# master 브랜치에 push나 pull request가 발생할 때 실행
on:
  push:
    branches:
      - master
  pull_request:
    branches:
      - master

# GitHub Actions의 기본 권한 설정
# contents: read는 저장소 내용을 읽을 수 있는 권한만 부여
permissions:
  contents: read

# 전역 환경 변수 설정
# Docker 이미지명, API 도메인, 이메일 등 워크플로우에서 사용할 변수들
env:
  DOCKER_IMAGE: ${{ secrets.DOCKER_USERNAME }}/spring
  API_DOMAIN: api.togerun.shop
  EMAIL: ${{ secrets.EMAIL }}

# 실제 작업(job) 정의
jobs:
  # build-and-deploy라는 이름의 작업 정의
  build-and-deploy:
    runs-on: ubuntu-latest    # Ubuntu 최신 버전에서 실행
    permissions:
      contents: read          # 이 작업의 권한 설정

    # 작업의 각 단계(step) 정의
    steps:
      # Step 1: GitHub 저장소 코드를 가져오기
      - name: Checkout Repository
        uses: actions/checkout@v4   # GitHub 제공 액션 사용

      # Step 2: Java 개발 환경 구성
      # JDK 17 설치 및 설정
      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: '17'        # Java 버전 지정
          distribution: 'temurin'    # JDK 배포판 지정

      # Step 3: application.yml 파일 생성
      # GitHub Secrets에 저장된 설정 파일을 디코딩하여 생성
      - name: Create and verify application.yml
        run: |
          mkdir -p ./src/main/resources
          cd ./src/main/resources
          echo "${{ secrets.APPLICATION_YML }}" | base64 -d > application.yml
          echo "Created application.yml:"
          cat application.yml

      # Step 4: Gradle 설정
      # Gradle 빌드 도구 설정
      - name: Setup Gradle
        uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5

      # Step 5: Gradle 실행 권한 부여
      # gradlew 파일에 실행 권한 부여
      - name: Grant Execute Permission For Gradlew
        run: chmod +x gradlew

      # Step 6: Gradle을 사용하여 프로젝트 빌드
      - name: Build with Gradle
        run: ./gradlew build --info

      # Step 7: Docker 빌드 컨텍스트 생성
      # JAR 파일과 Dockerfile을 준비
      - name: Create build context
        run: |
          mkdir -p docker-build
          EXEC_JAR=$(find build/libs/ -name "*.jar" -not -name "*plain.jar" -type f)
          
          if [ -z "$EXEC_JAR" ]; then
            echo "Error: No executable JAR file found"
            exit 1
          fi
          
          echo "Found executable JAR: $EXEC_JAR"
          cp "$EXEC_JAR" docker-build/app.jar
          cp Dockerfile docker-build/

      # Step 8: DockerHub 로그인
      # Docker Hub 인증 처리
      - name: Login to DockerHub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      # Step 9: Docker 이미지 빌드 및 푸시
      # 애플리케이션 Docker 이미지 생성 및 Docker Hub에 업로드
      - name: Build and push Docker image
        uses: docker/build-push-action@v5
        with:
          context: docker-build
          push: true
          tags: ${{ secrets.DOCKER_USERNAME }}/spring:latest

      # Step 10: EC2 인스턴스에 배포
      # SSH를 통해 EC2 인스턴스에 접속하여 배포 수행
      - name: Deploy to EC2
        uses: appleboy/ssh-action@master
        env:
          DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
          PUBLIC_IP_V4: ${{ secrets.PUBLIC_IP_V4 }}
          EMAIL: ${{ secrets.EMAIL }}
          REDIS_PASSWORD: ${{ secrets.REDIS_PASSWORD }}
        with:
          host: ${{ secrets.HOST_DEV }}          # EC2 호스트 주소
          username: ${{ secrets.USERNAME }}      # EC2 접속 사용자명
          key: ${{ secrets.PRIVATE_KEY }}        # EC2 접속 키
          port: 22                               # SSH 포트
          envs: DOCKER_USERNAME,PUBLIC_IP_V4,EMAIL,REDIS_PASSWORD  # 환경변수 전달
          script: |
            # SSL 인증서 디렉토리 구성
            # Let's Encrypt SSL 인증서 관련 디렉토리 생성
            mkdir -p certbot/conf
            mkdir -p certbot/www
            mkdir -p ssl
            
            # docker-compose.yml 파일 생성
            # 컨테이너 구성 정의: nginx, certbot, spring-boot, redis
            cat << EOF > docker-compose.yml
            # ... [docker-compose.yml 내용] ...
            EOF
            
            # nginx.conf 파일 생성
            # Nginx 웹 서버 설정: SSL, CORS, 프록시 등 설정
            cat << EOF > nginx.conf
            # ... [nginx.conf 내용] ...
            EOF
            
            # 기존 Docker 컨테이너 정리
            # 이전 컨테이너 종료 및 이미지 정리
            docker-compose down --remove-orphans
            docker image prune -f
            
            # Docker 네트워크 생성
            # 컨테이너 간 통신을 위한 네트워크 구성
            docker network create ubuntu_this_network || true
            
            # 최신 이미지 배포 및 서비스 시작
            # 새 버전 배포 및 컨테이너 실행
            docker-compose pull
            docker-compose up -d

      # Step 11: 배포 시간 기록
      # 배포 완료 시간 기록 (한국 시간 기준)
      - name: Get Current Time
        uses: 1466587594/get-current-time@v2
        id: current-time
        with:
          format: YYYY-MM-DDTHH:mm:ss
          utcOffset: "+09:00"

      # 배포 시간 출력
      - name: Print Current Time
        run: echo "Current Time=${{steps.current-time.outputs.formattedTime}}"
        shell: bash

각 단계의 수행 역할 정리

  • 빌드 준비 단계(Step1-3)
    • 소스코드 체크 아웃
    • JDK 설정
    • 설정 파일 생성
  • 빌드 단계 (Step4-7)
    • Gradle 설정
    • 프로젝트 빌드
    • Docker 빌드 준비
  • 컨테이너화 단계 (Step 8-9)
    • DockerHub 로그인
    • image 빌드 푸시
  • 배포 단계 (Step 10)
    • EC2 접속
    • 인프라 구성
    • 컨테이너 배포
  • 완료 단계 (Step 11)
    • 배포 시간 기록

2-4현재 파이프라인 모식도 ( 이후엔 code deploy 를 이용해 8081, 8082 포트를 나눠 blue/green 방식으로 리팩토링 하는 방식을 포스팅할 예정이다. )


  • Clidet 부분은 s3 정적 배포를 하지 않고 vercel을 이용했다.
    • 프로젝트 규모, ec2 비용 부담 등의 사유
    • 구매한 도메인과 연동 가능
    • cicd 파이프라인 자동 제공
    • 유지 보수 불필요

참고 : https://velog.io/@leejungho9/CICD-%EB%9E%80,
https://minsu20.tistory.com/27
https://wikidocs.net/197261

profile
백엔드 개발자를 꿈꾸고 있습니다.

0개의 댓글