GitHub Actions로 간단한 CI/CD 구축하기(Feat. Docker Hub, EC2)

tkdwns414·2023년 10월 9일
2

나는 프로젝트의 꽃이 배포라고 생각한다. 우리가 열심히 준비한 프로젝트를 실제 유저들이 사용하도록 하기 위해서는 배포가 필요하기 때문이다. 하지만 업데이트가 있을 때마다 사람이 직접 배포하는 것은 생각보다 리소스를 잡아먹기도 하고 실제로 귀찮기도 하다. 그래서 이번에는 GitHub Actions를 이용해서 간단하게 무료로 CI/CD 환경을 구축해보는 법에 대해서 작성해보도록 하겠다.

CI/CD

CI/CD는 Continuous Integration/(Continuous Delivery or Continous Deployment)의 약어로 해석하면 지속적 통합/지속적 배포를 의미한다. 개발자들이 작성한 코드 변경 사항을 지속적으로 통합하고 자동으로 배포할 수 있는 프로세스를 의미한다고 생각하면 된다.

위에 CD는 Continuous Delivery or Continuous Deployment라고 표현을 하였는데 두 개념은 약간의 차이가 있다. Delivery와 Deployment의 차이는 Delivery의 마지막 배포 결정 주체가 사용자인 것이고 Deployment는 배포 결정까지 자동화된 것이라고 이해할 수 있다.

Continuous Delivery (지속적 전달):

  • 개발된 소프트웨어가 실제 환경에 배포될 준비가 되어있음을 보장
  • 배포 프로세스 자체는 자동화되어 있지만, 실제로 유저(end-user)가 사용할 수 있도록 배포하는 단계는 수동으로 진행될 수 있음
  • 배포에 더 조심스러운 접근을 필요로 할 때 선택적으로 진행할 수 있음.

Continuous Deployment (지속적 배포):

  • 코드 변경 사항이 자동적으로 테스트되고 실제 환경에 배포까지 됨
  • 추가적인 확인 절차 없이 코드 변경이 곧바로 유저(end-user)에게 제공됨.

자세한 내용은 AWS 문서를 확인해보자

나는 Continuous Deployment를 기준으로 설명하겠다.

CI/CD 환경을 구축함을 통해 간단하게 다음과 같은 장점을 얻을 수 있다.

  1. 한 번 잘 설정해놓으면 배포에 필요한 리소스를 줄일 수 있다.
  2. CI 과정에 테스트를 포함해 수동 배포에서 발생할 수 있는 오류나 지연을 줄일 수 있다.
  3. 빠르게 코드 변경 사항을 프로덕션 환경에 배포할 수 있다.

이외에도 여러 장점이 있지만 위 정도만 설명하고 넘어가겠다.

CI

CI(Continuous Integration, 지속적 통합)는 위에서 “개발자들이 작성한 코드 변경 사항을 지속적으로 통합”하는 프로세스를 의미한다. 이를 통해 코드 변경 사항이 정기적으로 빌드 및 테스트되어 통합된 코드의 안정성을 유지할 수 있다. CI의 과정을 간단하게 보면 아래와 같다.

  1. 코드 변경 사항을 자동으로 가져와서 빌드한다.
  2. 빌드된 코드에 대해 자동으로 테스트를 진행한다.
  3. 테스트 결과를 확인하고, 실패한 경우 해당 사항을 개발자에게 알린다.
  4. 테스트 결과 이상이 없을 경우 CD 단계로 넘어간다.

CD

CD(Continuous Deployment, 지속적 배포)는 CI 단계가 끝난 후 자동으로 개발 환경 혹은 프로덕션 환경에 배포하는 프로세스를 의미한다. CD 단계를 통해 CI를 통해 검증된 변경 사항을 빠르게 배포할 수 있고 이를 통해 사용자들은 최신 기능이나 변경 사항을 즉시 사용할 수 있다

GitHub Actions?

CI/CD를 구현하기 위해서 우리는 GitHub Actions를 이용할 것이다. GitHub Actions는 GitHub에서 제공하는 CI/CD 도구로 프로젝트의 변경 사항을 감지하고 원하는 작업을 수행하도록 구성할 수 있는데 이를 사용해 소프트웨어 개발 및 배포 과정을 자동화할 수 있다.

GitHub Actions를 통해 코드 변경 사항이 있을 때 미리 작성해둔 파일을 통해 자동으로 빌드 및 테스트를 실행하거나, 배포를 자동으로 수행할 수 있다. 즉 CI/CD를 구현할 수 있다.

Before Exercise

전체적인 플로우를 아주 간단하게만 봤을 때 다음과 같이 생각하면 된다. 배포하는 방법은 여러개가 있겠지만 가장 기본적으로 보통 처음 배우는 EC2를 이용해서 한다고 가정하겠다.

  1. Github에 PR 생성
  2. repository에 PR merge
  3. CI 단계 실행
    1. test
    2. docker build
    3. Docker Hub에 Docker Image 업로드
  4. CD 단계 실행
    1. EC2의 ssh에 접속
      • docker stop & pull & run

위와 같은 최소한의 플로우를 지킬 경우 쉽게 CI/CD를 구현할 수 있다. 여기에 추가적으로 필요한 것이 있다면 추가 단계(환경 변수 파일 생성 등)를 넣어 필요한 작업을 할 수 있으며 이외에도 PR target 같은 조건을 추가함을 통해 해당 과정들을 안전하게 실행할 수도 있다. (특정 label이 붙은 PR에만 Action이 일어나게 설정 등…)

또한 ssh에 코드를 쉽게 올리기 위해서 도커로 이미지를 빌드하고 Docker Hub에 업로드하는 방식을 사용하겠다.

우선 GitHub Actions를 사용하기 위해 Docker Hub Token과 GitHub Secrets를 등록하는 방법을 알아보도록 하곘다.

Docker Hub Token

  1. Docker hub에 로그인 후 계정 메뉴에서 Account Settings 클릭

  2. 좌측 메뉴에서 Security 클릭

  3. Access Tokens 우측의 New Access Token 클릭

  4. 필요한 권한 설정 후 토큰 발급 가능

Github Secrets

  1. GitHub Repository의 settings 접속
  2. 좌측 메뉴 중 Secrets and variables > Actions 클릭
  3. New repository secret에 key, value 값 등록
  4. 추가, 수정, 삭제는 가능하지만 현재 어떤 값이 들어가있는지는 확인이 불가능함

위 과정과 같이 Docker Hub Token과 필요한 GitHub Secrets를 등록해놓으면 이후 GitHub Actions에서 필요할 때 사용할 수 있다.

Exercise

아래 설명을 할 때 사용하는 프레임워크는 Django와 Spring Boot를 사용할 것이지만 단순 예시로 든 것이 두 프레임워크일뿐 다른 프레임워크(BE 뿐만 아니라 FE)도 개념만 이해하면 직접 구현할 수 있다. 원래 본인이 사용하는 프레임워크를 배포하는 방법을 CI 단계에서 알맞게 적용하기만 하면 된다.

그럼 이제 Django 프레임워크와 Spring Boot를 예시로 CI/CD를 구현하는 방법을 간단하게 확인해보겠다.

전체적으로 아래의 흐름을 기본적으로 따라가되 추가할 필요가 있는 것들에 대해서는 추가해주면 된다.

  1. 코드 체크아웃: GitHub Actions를 사용하여 레포지토리의 코드를 가져오기
  2. 환경 변수 설정: .env 파일 생성 및 필요한 환경 변수 설정
  3. 테스트: 코드 품질을 보장하기 위해 필요한 테스트들을 실행
  4. Docker 이미지 생성 및 push: Dockerfile을 기반으로 도커 이미지를 빌드하고, 이를 Docker Hub에 push
  5. 배포: 서버에 SSH 접근하여 기존 컨테이너를 중지 및 제거 후, 새 이미지를 기반으로 새 컨테이너를 실행

아래는 Django와 Spring Boot 예제이다. 해당 내용에서는 학교 프로젝트 때 썼던 내용들을 조금 바꾼 것으로 테스트코드를 작성할 시간이 없어 테스트코드가 존재하지 않으므로 Test 과정은 포함되지 않았다.

Django

Dockerfile (프로젝트 루트/Dockerfile):

# Python 3.8 사용
FROM python:3.8
# 컨테이너 내 코드 경로 설정
WORKDIR /app
# Project 코드를 /app으로 복사
COPY . .
# requirements.txt에 있는 필요한 라이브러리들 설치
RUN pip install -r requirements.txt
# staticfile들 수집 / 양이 많다면 생각보다 오래 걸리는 작업이라 yml에서 조건 달고 해도 됨
RUN python3 manage.py collectstatic --noinput
# 실행
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:8000", "project.wsgi:application"]

GitHub Actions 설정 (프로젝트 루트/.github/workflows/cicd.yml):

name: Django CI/CD

on:
  push:
    branches: [ "develop" ]

jobs:
  CI:
    runs-on: ubuntu-20.04

    steps:
    - uses: actions/checkout@v3

    - name: Create .env file
      run: |
        touch .env
        echo SECRET_KEY=${{ secrets.DJANGO_SECRET_KEY }} >> .env
        echo DEBUG=${{ secrets.DJANGO_DEBUG }} >> .env
        echo API_URL=${{ secrets.DJANGO_API_URL }} >> .env
        echo WEB_URL=${{ secrets.DJANGO_WEB_URL }} >> .env
 
    - name: Login to Docker Hub
      uses: docker/login-action@v2
      with:
        username: ${{ secrets.DOCKERHUB_USERNAME }}
        password: ${{ secrets.DOCKERHUB_TOKEN }}

    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v2

    - name: Build and push
      uses: docker/build-push-action@v4
      with:
        context: .
        file: ./Dockerfile
        push: true
        tags: ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPONAME }}

  CD:
    needs: [ CI ]
    runs-on: ubuntu-20.04

    steps:
    - name: Docker Image Pull and Container Run
      uses: appleboy/ssh-action@1.0.0
      with:
        host: ${{ secrets.DEPLOYMENT_HOST }}
        username: ${{ secrets.DEPLOYMENT_USERNAME }}
        password: ${{ secrets.DEPLOYMENT_PASSWORD }}
        port: ${{ secrets.DEPLOYMENT_PORT }}
        script: |
          docker stop my-was
          docker rm my-was
          docker image rm ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPONAME }}
          docker run -d -p 8000:8000 \
          -v /root/my-proj/resources/images:/app/media \
          -v /root/my-proj/resources/static:/app/static \
          --name my-was ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPONAME }}

Spring Boot

Dockerfile (프로젝트 루트/Dockerfile):

FROM gradle:8.5-jdk17 AS builder
COPY . /usr/src
WORKDIR /usr/src
RUN gradle wrapper --gradle-version 8.5
RUN ./gradlew clean build -x test

FROM openjdk:17-jdk-alpine
COPY --from=builder /usr/src/build/libs/cgv-0.0.1-SNAPSHOT.jar /usr/app/app.jar
ENTRYPOINT ["java", "-jar", "/usr/app/app.jar"]

GitHub Actions 설정 (프로젝트 루트/.github/workflows/cicd.yml):

name: Spring CI/CD

on:
  push:
    branches: [ "dev" ]

jobs:
  CI:
    runs-on: ubuntu-20.04

    steps:
    - name: Checkout
      uses: actions/checkout@v3
        
    # Set runner application java
    - name: Set up JDK 17
      uses: actions/setup-java@v3
      with:
        java-version: '17'
        distribution: 'temurin'
        
    # Grant execute permission for gradlew
    - name: Grant execute permission for gradlew
      run: chmod +x gradlew

    # Project build
    - name: Build with Gradle
      run: ./gradlew clean build

    # Docker Image Build and Push
    - name: Login to Docker Hub
      uses: docker/login-action@v2
      with:
        username: ${{ secrets.DOCKERHUB_USERNAME }}
        password: ${{ secrets.DOCKERHUB_TOKEN }}
    
    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v2
        
    - name: Build and push
      uses: docker/build-push-action@v4
      with:
        context: .
        file: ./Dockerfile
        push: true
        tags: ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPONAME }}

  CD:
    needs: [CI]
    
    runs-on: ubuntu-20.04

    steps:
    # SSH Connect and Docker Image Pull and Container Run
    - name: Docker Image Pull and Container Run
      uses: appleboy/ssh-action@v1.0.0
      with:
        host: ${{ secrets.DEPLOYMENT_HOST }}
        username: ${{ secrets.DEPLOYMENT_USERNAME }}
        password: ${{ secrets.DEPLOYMENT_PASSWORD }}
        port: ${{ secrets.DEPLOYMENT_PORT }}
        script: |
          docker stop my-was
          docker rm my-was
          docker image rm ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPONAME }}
          docker run -d -p 8080:8080 \
          -v /root/my-proj/resources:/app/resources \
          --name my-was ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPONAME }}

위와 같이 간단하게 CI/CD를 구현할 수 있다. 필요에 따라 추가적인 설정이나 조정이 필요할 수 있으니, 항상 공식 문서나 관련 자료를 참고하는 것이 좋다.

추가적으로 주의할 점:

  • Spring boot의 경우 빌드 시 테스트 진행하고 싶지 않을 시 "-x test" 이용하기
  • 위의 Django는 .env file을 만드는 과정을 포함하고 Spring boot는 secret file을 만드는 과정을 포함하고 있지 않음. 해당 부분은 본인의 상황에 맞게 수정/삭제/추가가 필요함
    환경변수로 사용하게 docker 자체에 지정할 수도 있으며 docker build 전에 secret 파일을 직접 만들 수도 있고 docker volume을 이용해 EC2 내부에 직접 파일을 만들어 도커 컨테이너와 공유할 수도 있음
  • docker run my-was는 실행할 도커 컨테이너의 이를 포함한 다른 step에서 사용되는 명령어의 버전이나 JDK, gradle, ubuntu 등의 버전은 모두 자신의 상황에 맞는 방식으로 수정할 수 있음

CI/CD와 무중단 배포

무중단 배포는 소프트웨어를 지속적으로 업데이트하면서도 서비스가 중단되지 않게 변경사항을 제공할 수 있도록 하는 것을 의미한다. 무중단 배포는 CI/CD와 함께 사용되지만 CI/CD만 설정했다고 무중단 배포가 가능한 것은 아니다.

한 대의 서버를 CI/CD로 사용할 경우 도중에 업데이트한 소프트웨어를 다시 실행하기까지 해당 서버를 이용할 수 없게 된다. 그렇기 때문에 무중단 배포에서는 일반적으로 여러대의 서버를 필요로 한다. 간단하게 설명하면 하나의 서버를 업데이트하는 동안 다른 서버들을 사용 가능하게 두고 해당 과정을 순차적으로 진행하여 모든 서버를 업데이트하는 것이다. 이를 위해서 주로 로드 밸런서나 배포 스크립트 등의 기술이 사용된다고 한다.

마무리

위의 설명은 EC2와 Docker로 했지만 더 좋은 성능과 관리를 위해서 ECS, ECR을 사용해도 되고 docker 말고 docker-compose가 편하다면 docker-compose를 사용해도 된다. 위의 설명들은 정말 간단하게 CI/CD를 위해 작성한 것이니 더 견고한 서버와 보안을 위해서는 추가적인 방법들을 찾아보길 바란다.

0개의 댓글