왜 무중단 배포가 필요한가

서비스를 운영하다 보면 배포 때마다 수동으로 서버에 접속해서 코드를 당기고 재시작하는 과정이 반복됩니다. 처음엔 괜찮지만, 배포 빈도가 높아질수록 이 과정은 사람 손을 탈수록 실수가 생기고, 서비스 다운타임이 발생합니다. 이 글에서는 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 고정 IPSSH_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 reload와 kill -HUP을 활용한 그레이스풀 셧다운을 구현하거나, 아예 Docker + zero-downtime 전략(Blue-Green, Rolling)으로 이전하는 것을 권장합니다. 소규모 서비스는 재시작 시간(2-3초)이 충분히 허용 범위 안에 들어오는 경우가 대부분입니다.
마치며
이 파이프라인을 한 번 구성해두면 이후에는 git push 한 줄로 배포가 끝납니다. 테스트 → 배포 → 헬스체크 → 롤백으로 이어지는 안전망이 갖춰지면, 기능 개발 속도도 자연스럽게 빨라집니다. 배포를 두려워하지 않게 되기 때문입니다.
코드벤터는 FastAPI, SvelteKit, AWS 인프라 설계부터 CI/CD 자동화까지 풀스택 개발 전 과정을 글로벌 협력 네트워크와 함께 진행합니다. 기술 구현과 서비스 성장을 함께 고민하고 싶은 팀이 있다면 언제든 문을 두드려 주세요.


