나는 프로젝트의 꽃이 배포라고 생각한다. 우리가 열심히 준비한 프로젝트를 실제 유저들이 사용하도록 하기 위해서는 배포가 필요하기 때문이다. 하지만 업데이트가 있을 때마다 사람이 직접 배포하는 것은 생각보다 리소스를 잡아먹기도 하고 실제로 귀찮기도 하다. 그래서 이번에는 GitHub Actions를 이용해서 간단하게 무료로 CI/CD 환경을 구축해보는 법에 대해서 작성해보도록 하겠다.
CI/CD는 Continuous Integration/(Continuous Delivery or Continous Deployment)의 약어로 해석하면 지속적 통합/지속적 배포를 의미한다. 개발자들이 작성한 코드 변경 사항을 지속적으로 통합하고 자동으로 배포할 수 있는 프로세스를 의미한다고 생각하면 된다.
위에 CD는 Continuous Delivery or Continuous Deployment라고 표현을 하였는데 두 개념은 약간의 차이가 있다. Delivery와 Deployment의 차이는 Delivery의 마지막 배포 결정 주체가 사용자인 것이고 Deployment는 배포 결정까지 자동화된 것이라고 이해할 수 있다.
Continuous Delivery (지속적 전달):
Continuous Deployment (지속적 배포):
자세한 내용은 AWS 문서를 확인해보자
나는 Continuous Deployment를 기준으로 설명하겠다.
CI/CD 환경을 구축함을 통해 간단하게 다음과 같은 장점을 얻을 수 있다.
이외에도 여러 장점이 있지만 위 정도만 설명하고 넘어가겠다.
CI(Continuous Integration, 지속적 통합)는 위에서 “개발자들이 작성한 코드 변경 사항을 지속적으로 통합”하는 프로세스를 의미한다. 이를 통해 코드 변경 사항이 정기적으로 빌드 및 테스트되어 통합된 코드의 안정성을 유지할 수 있다. CI의 과정을 간단하게 보면 아래와 같다.
CD(Continuous Deployment, 지속적 배포)는 CI 단계가 끝난 후 자동으로 개발 환경 혹은 프로덕션 환경에 배포하는 프로세스를 의미한다. CD 단계를 통해 CI를 통해 검증된 변경 사항을 빠르게 배포할 수 있고 이를 통해 사용자들은 최신 기능이나 변경 사항을 즉시 사용할 수 있다
CI/CD를 구현하기 위해서 우리는 GitHub Actions를 이용할 것이다. GitHub Actions는 GitHub에서 제공하는 CI/CD 도구로 프로젝트의 변경 사항을 감지하고 원하는 작업을 수행하도록 구성할 수 있는데 이를 사용해 소프트웨어 개발 및 배포 과정을 자동화할 수 있다.
GitHub Actions를 통해 코드 변경 사항이 있을 때 미리 작성해둔 파일을 통해 자동으로 빌드 및 테스트를 실행하거나, 배포를 자동으로 수행할 수 있다. 즉 CI/CD를 구현할 수 있다.
전체적인 플로우를 아주 간단하게만 봤을 때 다음과 같이 생각하면 된다. 배포하는 방법은 여러개가 있겠지만 가장 기본적으로 보통 처음 배우는 EC2를 이용해서 한다고 가정하겠다.
위와 같은 최소한의 플로우를 지킬 경우 쉽게 CI/CD를 구현할 수 있다. 여기에 추가적으로 필요한 것이 있다면 추가 단계(환경 변수 파일 생성 등)를 넣어 필요한 작업을 할 수 있으며 이외에도 PR target 같은 조건을 추가함을 통해 해당 과정들을 안전하게 실행할 수도 있다. (특정 label이 붙은 PR에만 Action이 일어나게 설정 등…)
또한 ssh에 코드를 쉽게 올리기 위해서 도커로 이미지를 빌드하고 Docker Hub에 업로드하는 방식을 사용하겠다.
우선 GitHub Actions를 사용하기 위해 Docker Hub Token과 GitHub Secrets를 등록하는 방법을 알아보도록 하곘다.
Docker hub에 로그인 후 계정 메뉴에서 Account Settings 클릭
좌측 메뉴에서 Security 클릭
Access Tokens 우측의 New Access Token 클릭
필요한 권한 설정 후 토큰 발급 가능
위 과정과 같이 Docker Hub Token과 필요한 GitHub Secrets를 등록해놓으면 이후 GitHub Actions에서 필요할 때 사용할 수 있다.
아래 설명을 할 때 사용하는 프레임워크는 Django와 Spring Boot를 사용할 것이지만 단순 예시로 든 것이 두 프레임워크일뿐 다른 프레임워크(BE 뿐만 아니라 FE)도 개념만 이해하면 직접 구현할 수 있다. 원래 본인이 사용하는 프레임워크를 배포하는 방법을 CI 단계에서 알맞게 적용하기만 하면 된다.
그럼 이제 Django 프레임워크와 Spring Boot를 예시로 CI/CD를 구현하는 방법을 간단하게 확인해보겠다.
전체적으로 아래의 흐름을 기본적으로 따라가되 추가할 필요가 있는 것들에 대해서는 추가해주면 된다.
.env
파일 생성 및 필요한 환경 변수 설정아래는 Django와 Spring Boot 예제이다. 해당 내용에서는 학교 프로젝트 때 썼던 내용들을 조금 바꾼 것으로 테스트코드를 작성할 시간이 없어 테스트코드가 존재하지 않으므로 Test 과정은 포함되지 않았다.
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 }}
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를 구현할 수 있다. 필요에 따라 추가적인 설정이나 조정이 필요할 수 있으니, 항상 공식 문서나 관련 자료를 참고하는 것이 좋다.
추가적으로 주의할 점:
무중단 배포는 소프트웨어를 지속적으로 업데이트하면서도 서비스가 중단되지 않게 변경사항을 제공할 수 있도록 하는 것을 의미한다. 무중단 배포는 CI/CD와 함께 사용되지만 CI/CD만 설정했다고 무중단 배포가 가능한 것은 아니다.
한 대의 서버를 CI/CD로 사용할 경우 도중에 업데이트한 소프트웨어를 다시 실행하기까지 해당 서버를 이용할 수 없게 된다. 그렇기 때문에 무중단 배포에서는 일반적으로 여러대의 서버를 필요로 한다. 간단하게 설명하면 하나의 서버를 업데이트하는 동안 다른 서버들을 사용 가능하게 두고 해당 과정을 순차적으로 진행하여 모든 서버를 업데이트하는 것이다. 이를 위해서 주로 로드 밸런서나 배포 스크립트 등의 기술이 사용된다고 한다.
위의 설명은 EC2와 Docker로 했지만 더 좋은 성능과 관리를 위해서 ECS, ECR을 사용해도 되고 docker 말고 docker-compose가 편하다면 docker-compose를 사용해도 된다. 위의 설명들은 정말 간단하게 CI/CD를 위해 작성한 것이니 더 견고한 서버와 보안을 위해서는 추가적인 방법들을 찾아보길 바란다.