[홈서버 구축 최종] 라즈베리파이와 도커를 이용한 홈서버 구축 - 3. 도커 컨테이너 설정과 github actions CI/CD 파이프라인

오젼·2025년 4월 8일
0

[홈서버 구축]

목록 보기
7/8

이제 github actions를 이용하여 CI/CD 파이프라인을 구축합니다.

권한 설정

github settings에서 Actions->General 페이지를 들어간 후 Workflow permissions에서 Read and write permissions를 체크해주어야 합니다.

organization의 경우 레파지토리의 settings가 아닌 organization의 settings에 들어가야 저 설정을 할 수 있습니다.

.github/workflows/.yml 파일

.github/workflows/ 아래에 yml 파일이 있어야 github actions에서 해당 파일을 바탕으로 배포를 진행합니다.

저는 라즈베리파이에서 도커 컨테이너를 띄워 홈서버로 하였기 때문에 이에 맞춰 yml 파일을 작성했습니다.

제 경우 github actions 러너에서 백엔드(스프링부트) 애플리케이션을 빌드하여 도커 이미지를 만든 후
이를 ghcr(github container registry. 도커 허브.)에 push한 다음

라즈베리파이에서 해당 이미지를 pull 하여 컨테이너를 띄우도록 했습니다.
라즈베리파이에선 성능 문제로 빌드 시간이 오래 걸려 github actions runner에서 빌드를 하도록 한 것입니다.

yml 파일은 각자의 개발 환경에 맞게 수정하면 됩니다.

name: Deploy to Development Server

on:
  push:
    branches: [develop]
  pull_request:
    branches: [develop]

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    environment: development

    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          submodules: true
          token: ${{ secrets.GH_PAT }}
      
      # Gradle 캐시 설정
      - name: Setup Gradle cache
        uses: actions/cache@v3
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
          restore-keys: ${{ runner.os }}-gradle-

      # 서브모듈 설정
      - name: Setup submodule
        run: |
          cd src/main/resources/config
          git pull
          cd ../../../../

      # GitHub Container Registry 로그인
      - name: Login to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GH_PAT }}
      
      # 도커 빌더 설정
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      # Backend 이미지 빌드 및 푸시
      - name: Build and push Backend image
        uses: docker/build-push-action@v5
        with:
          context: .
          file: docker-files/backend/Dockerfile
          push: true
          tags: ${{ secrets.BACKEND_IMAGE }}
          platforms: linux/arm64
          cache-from: type=gha
          cache-to: type=gha,mode=max

      # 필요한 파일만 서버로 전송
      - name: Copy deployment files
        uses: appleboy/scp-action@v0.1.7
        with:
          host: ${{ secrets.HOST }}
          port: ${{ secrets.PORT }}
          username: ${{ secrets.USERNAME }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          source: "docker-compose.yml,docker-files/nginx/"
          target: "${{ secrets.DEPLOY_PATH }}"
          strip_components: 0

      # 백엔드 배포
      - name: Deploy backend
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.HOST }}
          port: ${{ secrets.PORT }}
          username: ${{ secrets.USERNAME }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            cd ${{ secrets.DEPLOY_PATH }}

            # GitHub Container Registry 로그인 추가
            echo ${{ secrets.GH_PAT }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin
            
            # 환경 변수 파일 생성
            cat > .env << EOL
            DB_ROOT_PASSWORD=${{ secrets.DB_ROOT_PASSWORD }}
            DB_HOST=${{ secrets.DB_HOST }}
            DB_NAME=${{ secrets.DB_NAME }}
            DB_USER=${{ secrets.DB_USER }}
            DB_PASSWORD=${{ secrets.DB_PASSWORD }}
            DOMAIN_NAME=${{ secrets.DOMAIN_NAME }}
            BACKEND_PORT=${{ secrets.BACKEND_PORT }}
            NGINX_PORT=${{ secrets.NGINX_PORT }}
            DB_PORT=${{ secrets.DB_PORT }}
            BACKEND_IMAGE=${{ secrets.BACKEND_IMAGE }}
            SEOUL_API_KEY=${{ secrets.SEOUL_API_KEY }}
            EOL

            # 백엔드만 재배포
            docker-compose pull backend
            docker-compose up -d --no-deps backend
            
            # 상태 확인
            docker-compose ps
            
            # 미사용 이미지 정리
            docker image prune -f --filter "until=168h"

환경변수 설정

deploy.yml을 보면 알 수 있듯이 github secrets를 사용하여 민감한 변수들을 관리해 주었습니다.

원래는 github settings 페이지에서 secrets를 일일이 등록해주어야 합니다.

하지만 변수가 많아지면 일일이 등록하기가 힘들어 변수를 한 번에 등록할 수 있게 스크립트를 만들어 주었습니다.

참고로 GH_PAT는 github personal access token입니다. 깃허브 settings에서 발급 받으면 됩니다.

setup-ssh-key.sh

먼저 ssh key를 생성해주는 스크립트입니다. 깃허브 actions에서 배포 서버에 원격으로 접속할 때 ssh 연결을 하게 됩니다.

따라서 ssh key가 필요합니다.

#!/bin/bash

# ANSI 색상 코드
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
NC='\033[0m'

# SSH 디렉토리 및 파일 확인
echo "SSH 설정을 확인합니다..."

# .ssh 디렉토리가 없으면 생성
if [ ! -d ~/.ssh ]; then
    echo ".ssh 디렉토리를 생성합니다..."
    mkdir -p ~/.ssh
    chmod 700 ~/.ssh
fi

# id_ed25519 파일이 이미 존재하는지 확인
if [ -f ~/.ssh/id_ed25519 ]; then
    echo "${YELLOW}SSH 키가 이미 존재합니다.${NC}"
    echo "기존 키를 사용하시겠습니까? (y/n)"
    read -r response
    if [ "$response" = "y" ]; then
        echo "${GREEN}기존 SSH 키를 사용합니다.${NC}"
    else
        echo "새로운 키로 대체하시겠습니까? 이 작업은 되돌릴 수 없습니다. (y/n)"
        read -r replace
        if [ "$replace" = "y" ]; then
            echo "${RED}Warning: 필요한 경우 기존 키를 먼저 백업하는 것을 추천합니다.${NC}"
            echo "계속하시겠습니까? (y/n)"
            read -r confirm
            if [ "$confirm" = "y" ]; then
                echo "새로운 SSH 키를 생성합니다..."
                echo "이메일 주소를 입력해주세요:"
                read -r email
                rm ~/.ssh/id_ed25519*
                ssh-keygen -t ed25519 -b 4096 -C "$email"
            else
                echo "${RED}스크립트를 종료합니다.${NC}"
                exit 1
            fi
        else
            echo "${RED}스크립트를 종료합니다.${NC}"
            exit 1
        fi
    fi
else
    # SSH 키 생성
    echo "새로운 SSH 키를 생성합니다..."
    echo "이메일 주소를 입력해주세요:"
    read -r email
    ssh-keygen -t ed25519 -b 4096 -C "$email"
fi

# authorized_keys 설정
echo "authorized_keys 파일을 설정합니다..."
cat ~/.ssh/id_ed25519.pub >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys

echo "${GREEN}SSH 키 설정이 완료되었습니다!${NC}"

set-github-secrets.sh

.env 파일에 있는 환경변수들을 github secrets에 등록해주는 스크립트입니다. 이때 .env 파일은 스크립트를 실행시키는 위치에 있어야 하며

DB_HOST=host
DB_NAME=db
DB_USER=user
DB_PASSWORD=1234
DOMAIN_NAME=domain
BACKEND_PORT=1234
NGINX_PORT=5678
DB_PORT=9012
BACKEND_IMAGE=ghcr.io/user/project:tag

이렇게 환경변수명=값 형식으로 입력 되어 있어야 합니다.

#!/bin/bash

# ANSI 색상 코드
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
NC='\033[0m' # No Color

# 사용법 체크
if [ -z "$1" ]; then
   echo "${RED}Usage: ./set-github-env-secrets.sh <environment_name>${NC}"
   echo "${YELLOW}Example: ./set-github-env-secrets.sh development${NC}"
   exit 1
fi

ENVIRONMENT=$1

# Environment secrets 설정
echo "⚙️ ${GREEN}GitHub ${ENVIRONMENT} environment secrets 설정을 시작합니다...${NC}"

# .env 파일이 존재하는지 확인
if [ ! -f .env ]; then
   echo "${RED}Error: .env 파일을 찾을 수 없습니다.${NC}"
   exit 1
fi

# .env 파일을 읽어서 secrets 설정
while IFS='=' read -r key value
do
   if [ ! -z "$key" ] && [ ! -z "$value" ]; then
       gh secret set "$key" -b"$value" --env $ENVIRONMENT || exit 1
   fi
done < .env

# SSH key 설정 (선택적)
if [ -f ~/.ssh/id_ed25519 ]; then
   gh secret set SSH_PRIVATE_KEY -b"$(cat ~/.ssh/id_ed25519)" --env $ENVIRONMENT || exit 1
fi

echo "✅ ${GREEN}GitHub ${ENVIRONMENT} environment secrets 설정이 완료되었습니다!${NC}"

docker-compose.yml

services:
  nginx:
    image: nginx:alpine
    container_name: mb-nginx
    depends_on:
      backend:
        condition: service_started
    ports:
      - "${NGINX_PORT}:80"
    volumes:
      - ./docker-files/nginx/nginx.conf:/etc/nginx/conf.d/default.conf
    networks:
      - app-network
    restart: unless-stopped

  data-init:
    build:
      context: ./docker-files/data-init
      dockerfile: Dockerfile
    container_name: mb-data-init
    depends_on:
      mariadb:
        condition: service_healthy
    environment:
      DB_HOST: ${DB_HOST}
      DB_USER: ${DB_USER}
      DB_PASSWORD: ${DB_PASSWORD}
      DB_NAME: ${DB_NAME}
      SEOUL_API_KEY: ${SEOUL_API_KEY}
    networks:
      - app-network
    restart: unless-stopped

  mariadb:
    image: mariadb:latest
    container_name: mb-mariadb
    healthcheck:
      test: ["CMD", "mariadb", "-h", "localhost", "-u${DB_USER}", "-p${DB_PASSWORD}", "-e", "SELECT 1"]
      start_period: 30s
      interval: 10s
      timeout: 5s
      retries: 5
    environment:
      MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
      MARIADB_DATABASE: ${DB_NAME}
      MARIADB_USER: ${DB_USER}
      MARIADB_PASSWORD: ${DB_PASSWORD}
    ports:
      - "${DB_PORT}:3306"
    volumes:
      - mariadb-data:/var/lib/mysql
    networks:
      - app-network
    restart: unless-stopped

  backend:
    image: ${BACKEND_IMAGE}
    platform: linux/arm64
    container_name: mb-backend
    depends_on:
      mariadb:
        condition: service_healthy
    environment:
      - DB_HOST=${DB_HOST}
      - DB_NAME=${DB_NAME}
      - DB_USER=${DB_USER}
      - DB_PASSWORD=${DB_PASSWORD}
    ports:
      - "${BACKEND_PORT}:8080"
    networks:
      - app-network
    restart: unless-stopped

networks:
  app-network:
    driver: bridge

volumes:
  mariadb-data:

docker-files/nginx/nginx.conf

server {
    listen 80;
    server_name ${DOMAIN_NAME};

    location / {
        proxy_pass http://backend:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

docker-files/backend/Dockerfile

# 빌드 스테이지
FROM amazoncorretto:17-alpine as build

WORKDIR /workspace/app

COPY gradle gradle
COPY build.gradle settings.gradle gradlew ./
COPY src src

RUN chmod +x ./gradlew
RUN ./gradlew bootJar
RUN mkdir -p build/libs

# 실행 스테이지
FROM amazoncorretto:17-alpine

VOLUME /tmp

COPY --from=build /workspace/app/build/libs/*.jar app.jar

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "/app.jar"]

docker-files/data-init/Dockerfile

FROM python:3.11-slim

WORKDIR /app

RUN apt-get update && apt-get install -y bash

COPY requirements.txt .
COPY get_cultural_events.py .
RUN pip install -r requirements.txt

COPY . .

CMD ["tail", "-f", "/dev/null"]

정리

이렇게 되면 nginx가 들어오는 모든 요청을 backend 컨테이너로 프록시하는 구조가 됩니다. 전체 시스템은 다음과 같이 동작합니다:

  1. 외부에서 들어오는 모든 웹 요청은 nginx 컨테이너의 ${NGINX_PORT}번 포트로 들어옵니다.
  2. nginx는 이 요청을 backend 컨테이너의 8080 포트로 프록시합니다.
  3. backend 컨테이너는 스프링부트 애플리케이션을 실행하며, 필요한 경우 mariadb 컨테이너에 연결하여 데이터를 조회하거나 저장합니다.
  4. mariadb 컨테이너는 데이터베이스 서비스를 제공하며, 볼륨을 통해 데이터가 영구적으로 보존됩니다.
  5. data-init 컨테이너는 초기 데이터 설정을 담당하며, 서울 API를 통해 문화 이벤트 데이터를 가져오는 역할을 합니다. 필요 시 컨테이너에 접속해 파이썬 스크립트를 실행할 수 있도록 CMD ["tail", "-f", "/dev/null"]를 사용해 컨테이너가 종료되지 않게 하였습니다.

이 구조의 장점은 다음과 같습니다:

  • 각 서비스가 독립적인 컨테이너로 분리되어 있어 관리가 용이합니다.
  • docker-compose를 통해 전체 서비스를 한 번에 시작하거나 중지할 수 있습니다.
  • GitHub Actions를 통해 코드 변경 시 자동으로 빌드 및 배포가 이루어집니다.
  • 환경 변수를 통해 설정을 외부화하여 보안을 강화하고 유연성을 확보했습니다.
  • nginx를 프론트 프록시로 사용함으로써 로드 밸런싱이나 SSL 설정 등 추가 기능을 쉽게 구현할 수 있습니다.

이 CI/CD 파이프라인은 GitHub의 develop 브랜치에 코드가 푸시되거나 PR이 생성될 때마다 자동으로 실행됩니다. 빌드된 이미지는 GitHub Container Registry(ghcr.io)에 저장되고, 라즈베리파이 서버에서 이 이미지를 가져와 컨테이너를 실행합니다.

특히 라즈베리파이의 성능 한계로 인해 빌드 과정은 GitHub Actions에서 처리하고, 배포만 라즈베리파이에서 진행하는 방식을 채택했습니다.

전체 시스템이 제대로 동작하려면 GitHub Secrets에 필요한 환경 변수를 모두 설정해야 하며, 이를 위한 스크립트도 함께 제공하여 설정 과정을 간소화했습니다.

0개의 댓글