💭 Minji's Archive

[s2n] CI/CD 파이프라인 + AWS 통합

November 24, 2025

사전 수정 사항

  1. dev.yml에서 빌드할 때 백엔드 경로의 Dockerfile을 명시해서 빌드해야 한다.
  2. Dockerfile 내부에서도 불필요한 파일 전체를 COPY하지 않고 백엔드 앱 관련 파일만 이미지에 포함시키기 => dev.yml, dockerfile 둘 다 수정 완료

현재 프로젝트 구조

AWS-WEB-APP/
 ├── backend/
 │    ├── app.py
 │    ├── config.py
 │    ├── Dockerfile  ← 실제 빌드 대상
 ├── frontend/
 ├── requirements.txt
 └── .github/workflows/
      ├── dev.yml
      └── deploy.yml

이번 CI/CD 파이프라인에서는 백엔드 Dockerfile만 빌드해 EC2에서 실행하는 구조로 구성했다.

CI/CD 파이프라인 + AWS 통합

= 즉 GitHub Actions -> EC2까지 자동 배포되도록 하는 작업

1. EC2 서버 준비 (수동)

1. EC2 접속

ssh ubuntu@<EC2-IP>

완료~!

2. Docker 설치

sudo yum update -y # 시스템 패키지 업데이트
sudo yum install -y docker # 도커 패키지 설치
sudo systemctl start docker # 도커 서비스 시작
sudo systemctl enable docker # 도커 서비스 자동 시작 설정
sudo usermod -aG docker ec2-user # 사용자를 도커 그룹에 추가

설치 완료!

3. GHCR 로그인

echo <DEV_PKG_TOKEN> | docker login ghcr.io -u 504s2n --password-stdin
  • 로그인 성공 메시지 확인 필수 완료~!

2. EC2 GitHub Actions Runner 설치

1. actions-runner 디렉토리 생성

mkdir actions-runner && cd actions-runner

2. GitHub에서 Runner 다운로드

레포 -> Settings -> Actions -> Runners -> New self-hosted runner GitHub가 아래 명령어들을 자동으로 줌:

curl -o actions-runner.tar.gz -L <URL>
tar xzf actions-runner.tar.gz
./config.sh --url https://github.com/504s2n/aws-web-app --token

3. 서비스 등록

sudo ./svc.sh install
sudo ./svc.sh start

4. Runner가 온라인 상태까지 확인

-> GitHub에서 online 표시 뜨면 성공

3. 배포 스크립트 구성

이제 dev.yml은 GHCR에 아이미지 push까지 담당 deploy.yml은 EC2 Runner에서 자동 실행 즉 배포 자동화 시작.

# deploy.yml 예시 구조
name: Deploy to EC2

on:
  push:
    branches: ["dev"]

jobs:
  deploy:
    runs-on: self-hosted

    steps:
      - name: Login to GHCR
        run: echo $ | sudo docker login ghcr.io -u 504s2n --password-stdin
        
      - name: Pull latest image
        run: |
          sudo docker pull ghcr.io/504s2n/aws-web-app:dev

      - name: Stop old container
        run: |
          sudo docker stop chatapp || true

      - name: Remove old container
        run: |
          sudo docker rm chatapp || true

      - name: Run new container
        run: |
          sudo docker run -d \
            --name chatapp \
            --env-file /home/ec2-user/.env \
            -p 5000:5000 \
            ghcr.io/504s2n/aws-web-app:dev

일단 여기까지 하고 dev에 한번 push했다. .env가 없어서 튕겼지만 아직 안 만들었으니까 당연함. 나머지는 다 잘 돌아가는 것 확인. 지금까지 확인한 것: (1) GitHub Actions가 dev push 신호 받음 (2) deploy.yml 실행됨 (3) EC2 self-hosted runner가 job 받아서 실행 (4) docker pull ghcr.io/504s2n/aws-web-app:dev 성공 (5) docker run –env-file /home/ec2-user/.env 실행 여기서 튕김. 즉 CI/CD 연결까지 성공~

4. 환경변수 세팅

EC2 내부에 .env 파일 생성

DB_HOST=xxx
DB_USER=xxx
DB_PASS=xxx
DB_NAME=xxx
SECRET_KEY=xxx
CORS_ORIGINS=*

위 파일을 컨테이너 실행 시 주입: --env-file /home/ubuntu/.env 여기까지 추가 완료해서 dev 브랜치에 push함. -> dev.yml -> Docker image 빌드 후 GHCR에 push -> deploy.yml -> EC2 Runner 실행 EC2에서 도커 확인

5. Nginx + Reverse Proxy + HTTPS

  • Nginx 설치
  • 웹소켓 업그레이드 헤더 설정
  • 443 -> 컨테이너 5000
  • certbot으로 SSL 적용 (팀이랑 논의 필요)

요약

  1. EC2 서버 세팅 및 도커 환경 구축 -> EC2에서 Docker 환경이 정상 구동되는지 검증
    • EC2 접속
    • 도커 설치/서비스 등록
    • GHCR 로그인
    • 실제로 도커 이미지 pull 테스트 성공
  2. Self-Hosted GitHub Actions Runner 설치 -> EC2가 GitHub Actions 배포 Job을 직접 실행하는 구조가 됨
    • 깃허브에서 Runner 설치 스크립트 다운로드
    • Runner 등록 (linux-x64)
    • Runner를 서비스로 등록해서 always-on 상태 유지
  3. CI/CD 파이프라인 분리 구성: dev 브랜치에 푸시 -> 이미지 재빌드 -> EC2에서 자동 재배포
    • dev.yml: Docker 이미지 빌드 & GHCR에 push
    • deploy.yml: EC2 Runner가 자동으로 pull & run
  4. Dockerfile 구조 개선
    • 백엔드만 포함되는 도커파일로 정리
  5. EC2에서 .env 구성
    • RDS 접속 정보 등
  6. 트러블슈팅
    • 컨테이너 실행은 되는데 MySQL 인증 실패 오류 발생. MySQL에서 특정 사용자의 host 기반 권한이 안 맞아서 Reject 되고 있음.

백엔드 앱 잘 떠있는지 확인

.env에 오타가 있었고, 수정해서 드디어 성공…… 하………

EC2 -> 외부 네트워크 통신 OK 확인 (nc -vz 명령어로 EC2의 5001 포트가 외부에서 접근이 가능한 것을 확인함 -> 보안 그룹/VPC/서브넷/라우팅 모두 정상) curl 명령어로 백엔드 Flask 서버 정상 동작 중임을 확인 (Flask API(=REST endpoint)가 정상 응답 -> Docker 컨테이너 안에서 백엔드 서버가 잘 실행되고 있다는 뜻 -> 컨테이너 포트 (5000) - 호스트 포트 (5001) 매핑도 정상)

현재까지

  • EC2 -> Docker 백엔드 서버 실행
  • 외부 -> EC2 5001 포트 접근
  • REST API 응답 정상 확인. 이제 백엔드 -> RDS 연결을 확인해야 함.

백엔드-EC2-RDS 전체 연결 확인

REST API는 잘 동작했지만, 웹소켓 연결은 별도의 흐름이라 독립적으로 확인해야 했다. 간단한, 최소한의 핸드셰이크만 포함한 test_ws.py 파일을 만들어서 테스트를 진행했다.이렇게 계속 400이 뜨면서 잘 작동하지 않았다. 원인은 .env의 CORS_ORIGNS 환경 변수였다. Flask-SocketIO는 Origin 체크를 엄격하게 수행하고, 문자열 한 덩어리를 리스트처럼 인식하지 않는다. .env를 수정하고, 컨테이너를 재실행한 후 다시 테스트해 보니

  • 0{…}: Socker.IO handshake 성공
  • 2: WebSocket 연결 성공 + ping/pong 유지됨
  • Message Sent!: 서버로 메시지 emit 성공 -> EC2 내부에서 WebSocket 통신이 정상적으로 작동함을 확인

이제 websocket이 잘 작동하는 걸 확인했으므로 진짜 RDS를 확인할 차례다. REST API POST를 테스트하려고 했는데 지금 백엔드의 app.py에는 POST 라우트가 없고 GET 라우트만 있다. 그래서 curl로 POST를 보내면 405 Method Not Allowed가 뜬다. app.py의 메시지 저장은 WebSocket 이벤트로만 동작한다 (socket.io의 send_message 이벤트를 통해서만 동작).

그래서 WebSocket 이벤트로 메시지를 보내고 실제로 RDS에 저장되었는지를 확인하는 방식으로 테스트를 진행하려고 한다. ① test_ws.py에서 실제 “send_message” 이벤트로 메시지 보내기 ② /api/messages GET으로 응답 확인 ③ MySQL 접속해서 테이블에 메시지 들어갔는지 SELECT로 확인

실제 메시지 저장까지 테스트하는 test_ws_full.py를 만들었다. socket.io의 ping 응답만 오고 성공적으로 들어가지 않고 있다. 다시 디버깅 시작. /backend/app.py와 실제 RDS의 테이블명이 달라서 일어나는 문제였다. 팀원에게 알려 주고 팀원이 PR 올려서 다시 진행.

config.py 수정하고, test 코드도 수정하고 와 끝!! 지금까지 검증된 플로우는 EC2 -> Docker 백엔드 -> RDS 전체 연결 성공이 완전 검증된 상태. WebSocker 핸드셰이크 성공 -> Socker.IO 이벤트 수신 성공 (send_message) -> nickname-content 파싱 -> IP 해싱 -> db_handler.save_message() 실행 -> MySQLCOnnectionPool -> RDS TCP 연결 성공 -> INSERT 정상 수행 -> auto_increment(id) 정상 증가 -> created_at 자동 저장 -> SELECT로 다시 조회 시 데이터 정상 노출 이 흐름 전체를 확인 성공함.


내가 한 일 총정리

1) EC2–Docker 백엔드 서버 배포 성공

  • Dockerfile/Multi-stage build 적용
  • GitHub Actions에서 GHCR 자동 빌드 성공
  • EC2에서 env 파일로 민감 정보 주입해 컨테이너 실행 성공
  • 포트 매핑(5001 → 5000) 정상 작동

2) 배포된 백엔드 서버가 정상적으로 REST API 동작

  • curl http://localhost:5001/api/messages
  • 빈 배열이지만 정상 response 확인됨

3) WebSocket 통신 성공

  • test_ws_full.py로 connect → message send 까지 성공
  • CORS_ORIGINS 환경변수와 config.py 우선순위 문제 해결

4) 백엔드 → RDS 연결 성공

  • RDS 연결 오류 해결
  • 올바른 table(chat_messages)에 메시지 insert 성공
  • DB에서 방금 보낸 메시지 row 확인됨