Skip to main content

왜 무중단 배포가 필요한가

AWS Lightsail + GitHub Actions 무중단 배포 세팅하기

서비스를 운영하다 보면 배포 때마다 수동으로 서버에 접속해서 코드를 당기고 재시작하는 과정이 반복됩니다. 처음엔 괜찮지만, 배포 빈도가 높아질수록 이 과정은 사람 손을 탈수록 실수가 생기고, 서비스 다운타임이 발생합니다. 이 글에서는 GitHub에 push하는 순간 AWS Lightsail 서버에 자동으로 배포되는 CI/CD 파이프라인을 처음부터 구축하는 방법을 실전 위주로 정리합니다. systemd 서비스 관리, 헬스체크, 롤백 전략까지 실무에서 바로 쓸 수 있는 내용을 담았습니다.

전체 아키텍처 개요

파이프라인의 전체 흐름은 다음과 같습니다.

  • 개발자 로컬git push origin main
  • GitHub Actions → 코드 체크아웃 → 테스트 → SSH로 서버 접속 → 배포 스크립트 실행
  • AWS Lightsail → 새 코드 pull → 의존성 설치 → systemd 서비스 재시작 → 헬스체크
  • 헬스체크 실패 시 자동 롤백

이 구조의 핵심은 서버가 절대 직접 GitHub에 접근하지 않는다는 점입니다. Actions Runner가 SSH 터널을 통해 서버에 명령을 내리는 방식이라 보안상 훨씬 안전합니다.

1단계: AWS Lightsail 인스턴스 준비

인스턴스 생성 및 기본 설정

Lightsail 콘솔에서 OS-only Ubuntu 22.04 인스턴스를 생성합니다. FastAPI + Python 기반 서비스라면 $10/월 플랜(2GB RAM)으로 충분히 시작할 수 있습니다. 인스턴스 생성 후 고정 IP(Static IP)를 반드시 붙여주세요. 재시작해도 IP가 바뀌지 않아야 DNS 연결이 안정적입니다.

서버 접속 후 배포 전용 사용자를 만들어 권한을 분리하는 것이 좋습니다.

# 배포 전용 사용자 생성
sudo adduser deploy
sudo usermod -aG sudo deploy

# 앱 디렉토리 생성
sudo mkdir -p /var/www/myapp
sudo chown deploy:deploy /var/www/myapp

# Python 가상환경 초기화
sudo -u deploy python3 -m venv /var/www/myapp/venv

GitHub Actions용 SSH 키 설정

Actions에서 서버로 SSH 접속하려면 비밀번호 없이 인증할 수 있는 키 페어가 필요합니다. 로컬이나 서버에서 키를 생성하고, 공개키는 서버에 등록, 비밀키는 GitHub Secrets에 저장합니다.

# 배포 전용 SSH 키 생성 (로컬에서)
ssh-keygen -t ed25519 -C "github-actions-deploy" -f ~/.ssh/deploy_key -N ""

# 공개키를 서버에 등록
ssh-copy-id -i ~/.ssh/deploy_key.pub deploy@YOUR_LIGHTSAIL_IP

# 비밀키 내용 확인 (GitHub Secrets에 저장할 값)
cat ~/.ssh/deploy_key

GitHub 리포지토리 → Settings → Secrets and variables → Actions에서 다음 시크릿을 등록합니다.

  • SSH_PRIVATE_KEY: 비밀키 전체 내용 (—–BEGIN 포함)
  • SSH_HOST: Lightsail 고정 IP
  • SSH_USER: deploy

2단계: systemd 서비스 설정

FastAPI 앱을 백그라운드에서 안정적으로 실행하려면 systemd 서비스로 등록하는 것이 가장 좋습니다. PM2나 supervisor도 대안이 될 수 있지만, 시스템 기본 도구인 systemd가 재부팅 자동 시작, 충돌 재시작, 로그 관리 면에서 가장 깔끔합니다.

# /etc/systemd/system/myapp.service
[Unit]
Description=MyApp FastAPI Service
After=network.target

[Service]
User=deploy
WorkingDirectory=/var/www/myapp
Environment="PATH=/var/www/myapp/venv/bin"
ExecStart=/var/www/myapp/venv/bin/uvicorn main:app --host 0.0.0.0 --port 8000 --workers 2
Restart=always
RestartSec=3
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

서비스를 등록하고 활성화합니다.

sudo systemctl daemon-reload
sudo systemctl enable myapp
sudo systemctl start myapp
sudo systemctl status myapp

3단계: GitHub Actions 워크플로우 작성

이제 핵심인 GitHub Actions 워크플로우를 작성합니다. 리포지토리 루트에 .github/workflows/deploy.yml 파일을 만듭니다.

name: Deploy to AWS Lightsail

on:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.11"
      - name: Install dependencies
        run: pip install -r requirements.txt
      - name: Run tests
        run: pytest tests/ -v

  deploy:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - name: Deploy via SSH
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.SSH_HOST }}
          username: ${{ secrets.SSH_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            set -e
            cd /var/www/myapp

            # 현재 커밋 저장 (롤백용)
            PREV_COMMIT=$(git rev-parse HEAD)
            echo "Previous commit: $PREV_COMMIT"

            # 코드 업데이트
            git fetch origin main
            git reset --hard origin/main

            # 의존성 설치
            source venv/bin/activate
            pip install -r requirements.txt --quiet

            # 서비스 재시작
            sudo systemctl restart myapp
            sleep 3

            # 헬스체크
            for i in {1..5}; do
              if curl -sf http://localhost:8000/health; then
                echo "Health check passed"
                exit 0
              fi
              sleep 2
            done

            # 헬스체크 실패 -> 롤백
            echo "Health check failed, rolling back..."
            git reset --hard $PREV_COMMIT
            pip install -r requirements.txt --quiet
            sudo systemctl restart myapp
            exit 1

워크플로우의 포인트를 짚어보면:

  • test → deploy 순서로 테스트가 통과해야만 배포가 실행됩니다.
  • PREV_COMMIT에 배포 전 커밋을 저장해두고, 헬스체크 실패 시 해당 커밋으로 즉시 롤백합니다.
  • set -e로 중간에 명령이 실패하면 즉시 스크립트를 중단합니다.
  • 헬스체크는 5회(10초) 재시도하여 앱이 완전히 뜰 시간을 확보합니다.

4단계: 헬스체크 엔드포인트 추가

FastAPI 앱에 헬스체크 엔드포인트를 추가합니다. 단순히 200을 반환하는 것보다 DB 연결 상태까지 확인하는 것이 실용적입니다.

from fastapi import FastAPI
from sqlalchemy import text

app = FastAPI()

@app.get("/health")
async def health_check():
    try:
        # DB 연결 확인
        async with async_session() as session:
            await session.execute(text("SELECT 1"))
        return {"status": "ok", "db": "connected"}
    except Exception as e:
        return JSONResponse(
            status_code=503,
            content={"status": "error", "detail": str(e)}
        )

5단계: Nginx 리버스 프록시 설정

Lightsail에서 외부 접근은 Nginx가 80/443 포트를 받아서 내부 8000 포트로 프록시하는 구조가 표준입니다. Let’s Encrypt SSL과 함께 설정합니다.

# Nginx 설치 및 Certbot
sudo apt install -y nginx certbot python3-certbot-nginx

# /etc/nginx/sites-available/myapp
server {
    server_name yourdomain.com;

    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        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;
    }
}

# SSL 자동 설정
sudo certbot --nginx -d yourdomain.com

배포 실전 팁

환경변수 관리

.env 파일은 절대 리포지토리에 포함하면 안 됩니다. 서버에 직접 /var/www/myapp/.env를 만들어두고 systemd 서비스에서 EnvironmentFile=/var/www/myapp/.env로 읽어들이는 방식이 깔끔합니다. 비밀값은 GitHub Secrets나 AWS Secrets Manager를 활용하세요.

알림 연동

배포 성공/실패를 Slack이나 Telegram으로 받으면 모니터링이 훨씬 쉬워집니다. 워크플로우 마지막에 알림 스텝을 추가하면 됩니다.

- name: Notify Telegram on success
  if: success()
  run: |
    curl -s -X POST "https://api.telegram.org/bot${{ secrets.TG_BOT_TOKEN }}/sendMessage"       -d chat_id="${{ secrets.TG_CHAT_ID }}"       -d text="[배포 완료] ${{ github.repository }} @ $(git rev-parse --short HEAD)"

- name: Notify Telegram on failure
  if: failure()
  run: |
    curl -s -X POST "https://api.telegram.org/bot${{ secrets.TG_BOT_TOKEN }}/sendMessage"       -d chat_id="${{ secrets.TG_CHAT_ID }}"       -d text="[배포 실패] ${{ github.repository }} - 확인 필요!"

zero-downtime 배포 고려

트래픽이 많은 서비스라면 systemd 재시작 시 짧은 다운타임이 발생할 수 있습니다. 이를 해결하려면 uvicorn을 --workers 4로 실행하되, 재시작 대신 systemctl reloadkill -HUP을 활용한 그레이스풀 셧다운을 구현하거나, 아예 Docker + zero-downtime 전략(Blue-Green, Rolling)으로 이전하는 것을 권장합니다. 소규모 서비스는 재시작 시간(2-3초)이 충분히 허용 범위 안에 들어오는 경우가 대부분입니다.

마치며

이 파이프라인을 한 번 구성해두면 이후에는 git push 한 줄로 배포가 끝납니다. 테스트 → 배포 → 헬스체크 → 롤백으로 이어지는 안전망이 갖춰지면, 기능 개발 속도도 자연스럽게 빨라집니다. 배포를 두려워하지 않게 되기 때문입니다.

코드벤터는 FastAPI, SvelteKit, AWS 인프라 설계부터 CI/CD 자동화까지 풀스택 개발 전 과정을 글로벌 협력 네트워크와 함께 진행합니다. 기술 구현과 서비스 성장을 함께 고민하고 싶은 팀이 있다면 언제든 문을 두드려 주세요.

댓글 남기기