[WAFFLE] Connecting the Dots

37 minute read

Published:

In this post, basic concepts of Git, Github, Aws, Poetry, Docker, FastAPI, Sql and their relationship is introduced.

Git & Github

Git은 소스 코드나 문서의 변경 이력(버전)을 관리하는 분산 버전 관리 시스템(Distributed Version Control System, DVCS) 이다. 설치 방법은 아래와 같다. Git을 프로그램이 아닌 시스템이라고 하는 이유는 git은 단순히 git.exe 같은 git 실행 파일 하나를 실행하는 것이 아니라 git-commit, git-init 과 같은 여러 서브 프로그램의 집합으로 이루어져 있고 데이터베이스에서 특별한 방식으로 데이터를 저장하기 때문이다.(다른 이유들도 있다.) 실행 파일이 만들어내는 저장 방식 + 동작 규칙 + 통신 프로토콜**이 통합된 **환경이라는 의미에서 시스템이라고 부른다.

git --version # 설치 여부 확인
brew install git # git 설치

git을 설치하고 나면 처음에 사용자 정보를 입력해야 한다.

git config --global user.name "이호석" # 프로젝트마다 다르게 설정하고 싶다면 --global 제외
git config --global user.email "hslee0324@snu.ac.kr"
git config --list # 확인

Github는 Git 저장소를 인터넷에서 호스팅(hosting)해 주는 서비스이다. 즉, Git으로 관리하는 소스 코드와 프로젝트를 서버에 올려 두고, 여러 사람이 협업할 수 있도록 지원하는 플랫폼이다. 또 다른 표현으로는 나의 로컬 git repository(.git 데이터베이스)를 github 에서 제공하는 원격 서버에 올려두고 이를 인터넷을 통해 원격으로 접근 가능하게 하는 서비스이다.

  • Organization : 여러 명이 협업하는 팀·회사·동아리 전용 GitHub 공간. 개인 Repository 에서 작업하면 협력자를 일일이 초대해야 되고 불편함.
  • 멤버가 되면 해당 Organization 안의 여러 레포지토리에 접근할 수 있고 부여된 권한에 따라 읽고 쓰기 가능

MySQL

MySQL은 데이터베이스 관리 시스템(DBMS), 그중에서도 관계형 데이터베이스 관리 시스템(RDBMS) 이다.

다음과 같이 설치를 한다.

brew install mysql # macOS mysql 설치
sudo apt install -y mysql-server # ubuntu mysql 설치

MySQL 프로그램은 서버 바이너리와 클라이언트 바이너리로 나뉜다. MySQL을 운영체제(OS)에 설치하고 다음의 명령어를 입력하면, mysqld라는 서버 프로세스가 실행된다. 사용자는 클라이언트 프로그램(mysql 명령어, 혹은 Python·Java 같은 애플리케이션에서 드라이버 이용)으로 서버와 통신한다. 여기서 드라이버란, python의 경우 pymysql 과 같이, 코드 라이브러리로써, python 애플리케이션에서 SQL 요청을 보내면 이를 서버에 전달해주는 역할을 한다. MySQL 서버는 클라이언트가 요청한 쿼리에 따라 데이터를 정리하여 클라이언트에게 전달한다.

sudo systemctl start mysql # ubuntu ver. MySQL 데몬(서버)이 켜기 → 백그라운드에서 계속 동작
brew services start mysql # macOS ver.

이후 아래의 명령어를 이용해 서버 클라이언트를 실행하여 이용한다.

mysql -u root # -u: user, root : root 계정 의미

FastAPI

웹 서버 (Apache, Nginx) 는 클라이언트로부터 받은 요청을 웹 애플리케이션에 전달하고, 웹 애플리케이션으로부터 받은 정보를 클라이언트에 전송한다. 웹 서버는 기본적으로 요청(Request)을 받고 응답(Response)을 내보내는 역할 만 한다. 요청 내용에 따라 어떤 계산을 할지, 어떤 데이터베이스를 조회할지, 어떤 HTML을 보여줄지 같은 실제 로직은 웹 서버 혼자 하지 못한다. **그래서 웹 서버는 **웹 애플리케이션을 불러서 실행시키고, 그 결과를 받아서 클라이언트에 응답한다.

FastAPI 는 웹 프레임워크이고 그것으로 만든 프로그램이 웹 애플리케이션이 된다. 즉, FastAPI는 웹 애플리케이션을 쉽게 만들 수 있도록 도와주는 라이브러리 모음이다.

웹 서버가 웹 애플리케이션을 부를 때, 부른다는 것도 소프트웨어 관점에서 코드이고 프로그램이다. 이 때의 규격을 WSGI 라 하고, WSGI를 구현한 프로그램을 WSGI 서버 라고 한다. 즉, WSGI 서버는 웹 애플리케이션을 실행해주는 애플리케이션 실행환경(=프로그램=코드) 이다. 실행환경이란, 웹 애플리케이션을 호출하는 역할(코드)도 포함하고 그 밖에도 여러가지가 있지만 한가지 예를 들자면 앱이 돌려준 결과를 HTTP Response로 만들어 클라이언트에게 반환하는 것 같은 기능의 코드도 포함한다.

여기서 규격이란 무슨 의미일까. 예를 들어 python 으로 (FastAPI 도움 없이) 다음의 웹 애플리케이션을 만들었다고 하자.

# 파일명: app.py

def application(environ, start_response):
    # HTTP 응답 상태
    status = '200 OK'
    # HTTP 응답 헤더
    headers = [('Content-Type', 'text/plain; charset=utf-8')]
    start_response(status, headers)

    # 실제 응답 바디 (반드시 바이트 문자열로 리턴해야 함)
    return [b"Hello, World! This is WSGI."]

이제 WSGI 서버 프로그램(Gunicorn 같은 것)을 이용해 아래와 같이 실행한다.

gunicorn app:application

WSGI는 규격이라서 application(environ, start_response) 같은 인터페이스 형태를 강제한다.

ASGI는 WSGI가 발전한 형태이고, uvicorn 은 ASGI 규약을 따르는 ASGI 서버, FastAPI 도 ASGI 규약을 따르는 웹 애플리케이션으로써 둘을 함께 사용할 수 있다. 참고로 uvicorn 은 AGSI 서버인 동시에 웹 서버의 역할을 동시에 수행한다. 전통적으로 Java 진영에서는 아래와 같은 흐름으로 진행된다.

브라우저 → 웹 서버(Apache, Nginx)
          → 웹 컨테이너(Tomcat)  → 애플리케이션 실행

Python 진영의 흐름은 아래와 같다.

브라우저 → Uvicorn(웹 서버 + ASGI 실행 환경)
          → FastAPI 앱 실행

WAS (Web Application Server) ** 란, **웹 서버 + 웹 애플리케이션 실행환경 + 웹 애플리케이션 으로, Java 진영의 예시에서는 Apache + Tomcat + Java code로 만든 애플리케이션이 될 것이고, Python 진영의 예시에서는 uvicorn + FastAPI로 만든 애플리케이션이 될 것이다.

ASGI 서버 프로그램과 WSGI는 웹 애플리케이션의 함수가 반환할 것으로 (규격에 따라) 기대하는 객체의 타입 등에서 차이가 있고 이 차이로 인해, 웹 애플리케이션의 여러 동작에서 (ex. db 조회) 동작이 마무리 될때까지 웹 애플리케이션 프로세스가 다른 요청(단순한 덧셈 연산 등)을 처리할 수 없거나(동기) 아니면 다른 요청을 받을 수 있거나(비동기)가 결정된다.

Virtual Environment & Poetry

가상 환경이란, 하나의 운영체제(OS) 안에서 독립적으로 격리된 실행 환경을 만드는 것이다. 특정 프로젝트가 다른 프로젝트와 의존성(라이브러리, 설정, 버전 등)이 충돌하지 않도록 분리해 주는 공간을 말한다.

poetry 는, 패키지 관리 + 가상환경 관리 + 배포까지 한 번에 처리해주는 프로그램이다. 다음과 같이 설치를 한다.

brew install poetry # poetry 설치

프로젝트 폴더로 들어가서 다음 명령어를 수행하면 패키지 이름 등을 설정하게 되고 최종적으로 pyproject.toml 파일이 만들어진다.

poetry init

poetry init 을 하면 자동으로 가상환경을 만들어준다.

poetry shell   # 가상환경 진입
exit           # 가상환경 나가기
poetry env info # 가상환경 위치 확인

Poetry는 프로젝트마다 독립적인 가상환경을 만들어 관리한다. 그래서 poetry shell 했을 때 현재 디렉터리가 어느 프로젝트 루트인지에 따라 들어가는 가상환경이 달라진다.

pyproject.toml 에는 필요한 라이브러리, 패키지 의존성들이 작성된다. 직접 pyproject.toml 을 수정하여 특정 의존성을 추가한 경우에는 다음의 명령어를 수행해야 해당 의존성이 가상 환경에 다운 받아진다.

poetry lock # 현재 pyproject.toml 기준으로 새 lock 파일 생성 
poetry install
  • pyproject.toml: 필요한 의존성 범위 선언

  • poetry.lock: 해당 범위에서 실제로 선택된 버전 기록

poetry add 를 하면 Poetry가 실제 설치 가능한 최신 버전을 탐색 (예: 2.31.0) 하여poetry.lock 파일에 requests=2.31.0 확정 기록한다. 그러나, 직접 pyproject.toml 을 수정한 경우에는 이 작업이 자동으로 일어나지 않기 때문에 poetry lock 명령어를 이용하여 pyproject.toml을 읽고 전체 의존성 그래프를 다시 탐색해서 poetry.lock을 새로 만든다.

일반적으로는 아래의 명령어를 이용하면 자동으로 가상환경에 해당 패키지가 설치된다. **단, python 의 경우는 python 패키지가 아니므로 직접 pyproject.toml 파일을 수정해야 한다 (poetry install 할 필요는 없음). 즉, 내가 현재 가상환경에서 사용하려는 python 버전은 로컬에 직접적으로 설치되어 있어야 한다. **

poetry add fastapi # fastapi 설치. 가상환경 진입 여부 상관 없음

poetry 를 이용하여 만들어진 가상환경 쉘에 진입하기 위해선 다음을 한다.

poetry env activate 
	source /Users/arcstone/Library/Caches/pypoetry/virtualenvs/nba-TDolNmsJ-py3.13/bin/activate # 위 명령어의 결과로 활성화 경로를 알려줌
source /Users/arcstone/Library/Caches/pypoetry/virtualenvs/nba-TDolNmsJ-py3.13/bin/activate # 그대로 실행 

추가적으로 Poetry는 pyproject.toml 안에서 이 프로젝트를 하나의 배포 가능한 Python 패키지로 정의한다.

[tool.poetry]
name = "myproject"         # 패키지 이름
version = "0.1.0"          # 버전
description = "Example app"
authors = ["Hoseok Lee <arcstone09@example.com>"]

[tool.poetry.dependencies]
python = "^3.11"
requests = "^2.31"

여기서 name = "myproject"는 PyPI에 올라갈 때 패키지 이름이자pip install myproject 같은 식으로 배포할 수 있는 단위가 된다.

AWS

AWS (Amazon Web Services) 는 아마존이 운영하는 클라우드 컴퓨팅 서비스 플랫폼이다. 즉, 물리적인 서버·스토리지·네트워크·데이터베이스 등을 직접 구매하거나 로컬에 설치하지 않고, 인터넷(클라우드)을 통해 필요한 만큼 빌려 쓰고 비용을 내는 서비스이다. 우리가 사용할 주요 서비스는 아래와 같다.

  • EC2 (Elastic Compute Cloud): 가상 서버 (컴퓨터 대여)

  • RDS (Relational Database Service): 관리형 데이터베이스 (MySQL, PostgreSQL 등)

기본적으로 AWS 를 사용하는 이유는 내 로컬에서 서버를 돌리려면 24시간 켜져있어야 하고 외부에서 접속할 때 포트 포워딩 등 귀찮은 작업들을 해야하기 때문임을 기억하자.

EC2

EC2는 가상 서버 대여 서비스 이름이고 실제 대여 받아 실행 중인 가상 서버 1대. (CPU, 메모리, OS 포함) 를 인스턴스 라 한다. 인스턴스를 생성해보자.

AWS 로그인 → 대시보드에서 EC2 → 인스턴스 시작 → *인스턴스 설정

인스턴스 설정 단계에서 다음의 요소들을 입력해야 한다. 아래는 내가 설정한 값들이다.

  • 이름 : 웹 서버 이름.

    • my-web-server
  • AMI : OS + 기타 설정. 이 때, 프리티어 로 설정해야 과금되지 않는다.

    • ubuntu
  • Inctance Type : 가상 서버의 사양(스펙) 선택. CPU 개수, 메모리 크기, 네트워크 성능, GPU 포함 여부 등이 달라진다. 이 때, 프리티어 로 설정해야 과금되지 않는다.

  • t2.micro

  • Key Pair : 서버에 접속하는 방법은 2가지가 있을 것이다. 하나는 AWS 홈페이지에 로그인하여 인스턴스에 들어가는 것이고 다른 하나는 외부에서(ex. 내 로컬 환경) ssh 명령어를 이용하여 접속하는 것이다. 서버는 보안이 매우 중요하기 때문에 후자의 방법으로 접속할 경우, 아이디-비밀번호 같은 식으로 로그인하지 않는다. RSA key 라는 비대칭 키 방식을 사용하여 로그인한다. 이때, 개인 키(.pem 파일)는 내 컴퓨터에 다운로드하고, AWS는 ec2 인스턴스 내에 공개 키를 보관하여 비밀번호 대신 이 키를 사용해서 서버에 접속. 즉, 여기서는 EC2에 접속할 때 쓰는 암호화 키(SSH 로그인용) 을 만든다. 키 페어 생성을 눌러 아래와 같이 생성한다.

    • my-web-server-key-pair (키 페어 이름), RSA (키 페어 유형), .pem (프라이빗 키 파일 형식) 으로 설정.

    • mv ~/Downloads/my-web-server-key-pair.pem ~/.ssh # 다운받은 key pair를 home directory의 .ssh 폴더로 이동
      sudo chmod 400 my-web-server-key-pair.pem # 다운받았을 때는 악성코드 우려로 OS 차원에서 사용못하게 되어있을 수 있으므로 권한 변경 (.ssh directory 에서 진행, 777하면 permission denied 됨)
      
  • Network Settings : ‘다음에서 SSH 트래픽 허용’ 을 위치 무관으로 설정하면 어디서든 서버로 ssh 접속이 가능하다. 만약, 내 IP로 설정하면 현재 내 위치의 IP에서만 허용하기 때문에 다른 곳으로 이동하면 접속이 안될 것이다. 이럴 경우 트래픽 허용을 나중에 추가해야 한다. VPC (Virtual Private Cloud), Subnet, Auto-assign Public IP, Security Group (방화벽 보안 그룹) 이 무엇인지에 대해서는 다음에 알아보자.

    • 다음에서 SSH 트래픽 허용 : 체크, 위치 무관
    • 인터넷에서 HTTP 트래픽 허용 : 체크 우리가 서버를 웹 서버로 사용할 것이기 때문에 클라이언트 요청을 받아야하므로 허용한다.
  • Storage (EBS, Elastic Block Store) : 인스턴스에 연결할 하드디스크 같은 저장공간.

    • 8gb

기타 고급 설정 옵션은 그대로 유지한 채, 인스턴스 생성을 한다.

인스턴스가 생성되고 인스턴스 요약을 가보면 퍼블릭 IPv4 주소 와 퍼블릭 IPv4 DNS 주소 가 나온다.

3.80.105.171 # 퍼블릭 IPv4
ec2-3-80-105-171.compute-1.amazonaws.com # 퍼블릭 DNS

서버에 접속하기 위해 둘 중 하나의 주소를 이용하여 다음의 명령어로 ssh 접속한다. 이 때, ubuntu 는 서버의 계정 이름으로 기본으로 이렇게 설정되어 있다.

ssh ubuntu@3.80.105.171 -i ~/.ssh/my-web-server-key-pair.pem

하지만, 매번 위 명령어를 사용하여 접속하기 귀찮으므로 ~/.ssh/config 파일에 다음의 내용을 추가한다.

# ~/.ssh/config 에 추가
Host my-web-server
  HostName 3.80.105.171
  User ubuntu
  IdentityFile ~/.ssh/my-web-server-key-pair.pem

앞으로는 다음의 명령어만으로 서버에 접속할 수 있게 되었다.

ssh my-web-server

이제, 서버에 접속한 상태에서 다음 몇 가지 기본 세팅을 한다.

sudo apt update # package manager update

서버에 python3 가 설치되어있는지 확인하고 (위 설정대로라면 기본으로 존재) 없다면 설치한다. 이후 poetry 설치 및 세팅 과정을 위 Virtual Environment & Poetry 섹션에서 설명했듯이 그대로 서버에서도 진행한다.

RDS

AWS 에서 MySQL 을 사용할 수 있는 방법은 크게 두 가지이다.

  • EC2 인스턴스에 직접 MySQL 설치
  • AWS RDS (Relational Database Service)에서 MySQL 인스턴스 생성

RDS 란, AWS가 제공하는 매니지드(Managed) 관계형 데이터베이스 서비스로, MySQL, PostgreSQL, MariaDB, Oracle, SQL Server, Amazon Aurora 같은 DB 엔진을 선택해서 쓸 수 있다. 핵심은내가 직접 EC2에 설치/관리하지 않아도, AWS가 데이터베이스 서버를 대신 운영해 준다는 점이다.

다음의 방법으로 데이터베이스 서버를 생성한다. 이 과정에서 연결할 ec2 컴퓨팅 리소스를 설정하는 옵션에서 현재 내 인스턴스를 선택한다.

AWS 로그인 → 대시보드에서 RDS → 데이터베이스 생성 → *데이터베이스 설정

이제 우리가 해야 하는 일은 EC2 인스턴에서 RDS(MySQL)에 접속해, 데이터베이스(DB)를 만들어야 한다.

sudo apt install -y mysql-client # ec2 인스턴스에 클라이언트 설치 

이제 ec2에서 다음의 형식으로 RDS MySQL 서버와 연결한다.

mysql -h <RDS엔드포인트주소> -P 3306 -u <마스터사용자이름> -p
mysql -h my-web-server-database.cneqoay20wx1.us-east-1.rds.amazonaws.com -P 3306 -u admin -p
ALTER USER 'admin'@'%' IDENTIFIED WITH mysql_native_password BY 'YourPassword';
FLUSH PRIVILEGES; # 인증 방식에 관한 설정.

바로 위 명령어는 일단 보류.

그리고 다음의 MySQL 쿼리를 작성하여 database를 생성하고 테이블을 만든다.

CREATE DATABASE testdb;
USE testdb;

CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(50) NOT NULL,
    email VARCHAR(100) UNIQUE
);

INSERT INTO users (name, email) VALUES
('Alice', 'alice@example.com'),
('Bob', 'bob@example.com');

이렇게 하고 ec2 인스턴스에서 프로젝트의 main.py 에서 다음과 같이 작성한다.

from fastapi import FastAPI
import pymysql

app = FastAPI()

# 데이터베이스 연결 함수
def get_connection():
    return pymysql.connect(
        host="my-web-server-database.cneqoay20wx1.us-east-1.rds.amazonaws.com",
        user="admin",          
        password="H**********",     
        database="testdb",    
        cursorclass=pymysql.cursors.DictCursor
    )

@app.get("/")
def read_root():
    return {"Hello": "MySQL"}

@app.get("/users")
def read_users():
    conn = get_connection()
    cursor = conn.cursor()
    cursor.execute("SELECT id, name, email FROM users;")
    rows = cursor.fetchall()
    conn.close()
    return rows
  • host : database 서버 주소. 생성되고 난 후 확인 가능.
  • user : 기본은 “admin”
  • password : database 서버 생성 과정에서 입력함.
  • database : 하나의 MySQL 서버 안에는 여러 개의 데이터베이스가 있을 수 있는데, 이 중 어느 것을 사용할 것인지 설정.

AWS 로그인 방식과 관련된 세부사항은 다음과 같다.

  • IAM (Identity and Access Management) = AWS에서 사용자와 권한을 관리하는 서비스

    IAM 계정 = 루트 계정(=AWS 가입한 메인 계정)에서 만들어서 팀원·개발자에게 주는 개별 사용자 계정

  • Root 계정은 AWS 가입 시 처음만든 (메일주소, 비밀번호) 로 로그인한다.

    • 고유한 Account ID 12 자리가 부여된다.
    • Account Alias (계정 별칭) 이란, 이 Account ID 대신 쓸 수 있는 이름이다. 즉, 메일=Root 계정 당 1개씩 존재한다.
  • IAM 계정은 (접속하려는 Root 계정의 ID or 별칭, IAM 사용자 이름, 비밀번호) 로 접속한다.

AWS 는 사용한 만큼 비용이 청구되는 On-Demand 요금제가 적용되기 때문에 해커에게 계정이 유출되어 대형 서버가 띄어지면 큰 비용을 물게 될 수도 있다. 따라서, MFA 라는 2단계 인증을 사용하도록 설정해주어야 한다. (자세한 방법은 생략)

A to Z

로컬에서 웹 애플리케이션 서버를 구동하고 이를 ec2를 통해 배포하는 전 작업을 직접 해보자.

Step 0. Install

brew install git
git config --global user.name "이호석"
git config --global user.email "hslee0324@snu.ac.kr"
brew install mysql
brew services start mysql
brew install poetry

Step 1. Git Init

mkdir NBA ; cd NBA # local repository 생성 및 이동
git init # 현재 디렉토리를 git 저장소로 초기화

Step 2. Remote Repo 생성

다음으로, github.com 으로 가 remote repisitory 를 생성한다. 이름은 똑같이 NBA 로 만들었다.

Step 3. Local-Remote Repo 연결

이제 로컬의 repository 와 remote repository 를 연결한다.

git remote add origin https://github.com/arcstone09/NBA.git
  • git remote add : 로컬 저장소에 원격(remote) 저장소 주소를 등록. “내 로컬 프로젝트에, GitHub 저장소를 origin이라는 이름으로 연결해둔다” 는 의미이다.

  • origin : 원격 저장소의 이름(별명). 보통 기본값으로 origin을 쓴다.

  • 참고로, 이 과정에서 원격 저장소의 주소를 잘못 등록했을 때에는 다음과 같이 수정할 수 있다.

    git remote set-url origin https://github.com/arcstone09/NBA.git
    

한 번 등록을 해두고 나면, 다음의 명령어를 이용할 수 있다.

git push origin main   # 로컬 main 브랜치를 GitHub에 업로드
git pull origin main   # GitHub main 브랜치를 로컬로 가져오기

Step 4. Poetry Init

poetry init 

Step 5. FastAPI, uvicorn, python Install

poetry add "fastapi^0.115.0"
poetry add "uvicorn^0.30.6"
poetry add pymysql
poetry add cryptography # mysql 서버 접속시 인증 플러그인

Step 6. MySQL database 생성(로컬)

mysql -u root
CREATE DATABASE testdb;
USE testdb;

CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(50) NOT NULL,
    email VARCHAR(100) UNIQUE
);

INSERT INTO users (name, email) VALUES
('Alice', 'alice@example.com'),
('Bob', 'bob@example.com');

Step 7. Write main.py

from fastapi import FastAPI
import pymysql

app = FastAPI()

# 데이터베이스 연결 함수
def get_connection():
    return pymysql.connect(
        host="localhost",     # MySQL 서버 주소
        user="root",          # 사용자
        password="",      # 비밀번호
        database="testdb",    # 사용할 DB 이름
        cursorclass=pymysql.cursors.DictCursor
    )

@app.get("/")
def read_root():
    return {"Hello": "MySQL"}

@app.get("/users")
def read_users():
    conn = get_connection()
    cursor = conn.cursor()
    cursor.execute("SELECT id, name, email FROM users;")
    rows = cursor.fetchall()
    conn.close()
    return rows

Step 8. 웹 서버 실행(로컬)

poetry run uvicorn main:app --reload
# main = main.py 파일 이름
# app = main.py 안의 FastAPI 객체 변수명
# --reload : 코드 수정 시 자동으로 서버 재시작 (개발할 때 필수)
  • 웹 브라우저로 http://127.0.0.1:8000 접속.

Step 9. Git push (local → github)

로컬 master brach 에서 작업한 레포지토리를 github의 원격 레포지토리 develop brach로 git push 한다. git push 에 대한 내용(인증과정에서 유저이름 대신 토큰 사용 등)은 여기 참고.

git add .
git commit -m 
git remote -v 
		origin  https://github.com/username/myrepo.git (push) # 위 명령어 결과 예시
git push origin master:main

Step 10. Ec2 instance 생성

인스턴스 생성 과정은 위 AWS/EC2 섹션 참조.

  1. 로컬에서 인스턴스로 ssh 접속

  2. pip(필수는 아님), poetry 설치, (python3 도 없다면 설치)

    sudo apt install pip 
    curl -sSL https://install.python-poetry.org | python3 - # poetry 설치
    export PATH="$HOME/.local/bin:$PATH" 
    

PATH는 쉘 환경변수 중 하나로, 터미널에서 어떤 명령어(ls, python, poetry 등)를 실행할 때 어디서 실행파일을 찾을지 경로 목록을 담고 있다. 위 명령어를 설정하지 않으면 poetry 를 실행했을 때 command not found error 가 날 수 있다. 위 명령어의 $PATH 부분은 기존의 환경 변수값을 유지한 채로 거기에 더해 위 내용을 추가하겠다는 뜻이다.

Step 11. Git clone (github → aws ec2 instance)

서버에서 github repository의 main brach 를 git clone

git clone -b main https://github.com/arcstone09/NBA.git

Git clone 후, poetry init 을 새로 할 필요 없다. pyproject.toml 과 poetry.lock 파일이 이미 있다면 그 프로젝트는 이미 poetry로 초기화된 상태이기 때문이다.

단, poetry install 을 이용해서 의존성을 설치해야 한다.

poetry install

mysql 도 설치한다.

sudo apt install -y mysql-server

Step 12. MySQL database 생성(AWS)

RDS 를 이용하자. AWS/RDS 섹션에서 설명했듯이 다음의 과정들을 거친다.

AWS 로그인 → 대시보드에서 RDS → 데이터베이스 생성 → *데이터베이스 설정
sudo apt install -y mysql-client # ec2 인스턴스에 클라이언트 설치 
mysql -h my-web-server-database.cneqoay20wx1.us-east-1.rds.amazonaws.com -P 3306 -u admin -p # ec2 sql-client 를 이용하여 sql-server와 연결
ALTER USER 'admin'@'%' IDENTIFIED WITH mysql_native_password BY 'YourPassword';
FLUSH PRIVILEGES; # 인증 방식에 관한 설정.
# rds database 인스턴스에 database 생성
CREATE DATABASE testdb;
USE testdb;

CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(50) NOT NULL,
    email VARCHAR(100) UNIQUE
);

INSERT INTO users (name, email) VALUES
('Alice', 'alice@example.com'),
('Bob', 'bob@example.com');
# local의 main.py 에서와 db connection 부분 다르게 수정.
def get_connection():
    return pymysql.connect(
        host="my-web-server-database.cneqoay20wx1.us-east-1.rds.amazonaws.com",
        user="admin",          
        password="H**********",     
        database="testdb",    
        cursorclass=pymysql.cursors.DictCursor
    )

Step 13. 웹 서버 실행(ec2)

현재 ec2 인스턴스의 인바운드 규칙에서 8000 port 가 외부에서 접근 불가능하기 때문에 새로운 규칙을 추가해야 한다.

image-20250917164116007

위 두 규칙은 인스턴스 생성시에 설정한 것이고 가장 아래 것을 추가하면 된다. 유형 HTTP = Custom TCP (Port 80) 과 완전히 동일하다.

poetry run uvicorn main:app --host 0.0.0.0 --port 8000 --reload

--host 0.0.0.0 을 설정하지 않으면 기본 실행 설정이 로컬 호스트(127.0.0.1) 만 바인딩하기 때문에, 외부에서 접근 할 수 없다. 즉, 같은 EC2 안에서는 접속 가능하지만, 외부(내 PC 브라우저)에서는 접근할 수 없다.

위와 같이 설정하면 웹 브라우저에서 다음과 같이 웹 서버에 요청을 보내고 응답을 받을 수 있다.

http://3.80.105.171:8000
http://3.80.105.171:8000/users

Furthermore 1 - some problems

여기까지 기본적인 세팅을 마쳤다. 이제 이후에 코드를 개발해가는 flow 가 어떻게 되는지 살펴보자.

local에서 코드 수정, local mysql server로 database 수정 → git push → (local에서 ssh로 ec2에 접속하여) git pull → ec2 인스턴스로 서버 실행시에는 rds database instance 이용됨.

이 때, 우리는 다음 네 가지의 불편함을 생각할 수 있다. 이 중 첫 세가지 불편함은 local 과 ec2 인스턴스에 사용하는 database server가 다른데에서 기인한다.

  1. local과 ec2 인스턴스에서 사용하는 database server가 다르므로 위 과정 중 step 7 과 step 12 에서 db_connection 함수를 다르게 짜야 한다. 즉, ec2 인스턴스에서 git pull 한 이후에 코드를 직접 수정해야 하는 일이 발생한다.

  2. local과 ec2 인스턴스에서 사용하는 database server가 다르므로 local mysql server로 database 수정한 내용 (CREATE Table 과 같이 스키마 즉, 테이블 구조 변경)을 ec2 인스턴스에서 rds database server 를 통해 반복 작업해야 한다.

  3. 2번의 작업을 할때 local 터미널에서 시작하여 rds database 인스턴스에 연결되기 까지 두 번의 연결이 필요하다.

    ssh my-web-server # local → ec2 인스턴스
    mysql -h my-web-server-database.cneqoay20wx1.us-east-1.rds.amazonaws.com -P 3306 -u admin -p
    

​ 이를 단일화하고 싶다.

  1. pymysql 을 이용할 때, 결국 파이썬 코드 상에서 cursor.execute("SELECT id, name, email FROM users;") 와 같은 실제 mysql 쿼리를 적어야한다. 조금 더 쉽게 다루고 싶다.

이제 각 문제를 해결하여 보자.

우선, 문제 4는 SQLAlchemy 를 이용하여 해결할 수 있다. SQLAlchemy는 Python에서 가장 널리 쓰이는 ORM(Object Relational Mapper) 이다. ORM 은 Python 클래스 <-> DB 테이블을 자동 매핑해주는 기술이다. SQLAlchemy는 내부적으로 PyMySQL 같은 드라이버를 사용해서 실제 DB와 통신한다. 아래 코드와 같은 식으로 활용할 수 있다.

from sqlalchemy import Column, Integer, String, create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

Base = declarative_base()

class User(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True)
    name = Column(String(50))

engine = create_engine("mysql+pymysql://root:pw@localhost/test")
Session = sessionmaker(bind=engine)
session = Session()

user = session.query(User).filter(User.id == 1).first()
print(user.name)

SQLAlchemy 를 사용하기 위해서 다음과 같이 추가한다.

poetry add sqlalchemy

다음으로, 문제 3은 SSH 터널링 (포트포워딩) 을 이용하여 해결할 수 있다. 문제 3을 다른말로 바꾸면 내 로컬의 mysql client를 이용해 rds database instance의 mysql server와 소통하고 싶다 이다. 결론부터 말하면 다음의 명령어를 입력하면된다.

ssh -i ~/.ssh/my-web-server-key-pair.pem \
    -L 3307:my-web-server-database.cneqoay20wx1.us-east-1.rds.amazonaws.com:3306 \
    ubuntu@3.80.105.171

이렇게 되면 기본적으로 ssh 세션이기 때문에 ec2 인스턴스에 접속하게 된다. 그러나 우리의 목적은 로컬의 mysql-client 를 이용하는 것이므로

새로운 터미널을 열어 로컬에서 다음의 명령어를 입력하고 하던대로 MySQL 쿼리를 입력하면 된다.

mysql -h 127.0.0.1 -P 3307 -u admin -p

**위 과정에서 오류남!! 이따가 해결 **

내가 바로 위 명령어를 통해 로컬의mysql-client 를 실행하면, mysql-client 가 127.0.0.1:3307 으로 TCP 연결을 시도한다. 한 단계 더 위 명령어에서 3307 은 로컬에서 열어둔 포트로, ssh-client 는 해당 포트로 들어온 연결을 그 뒤에 인자로 들어온 서버로 전달한다. mysql-client 는 기존에는 로컬의 mysql-server 와 통신했겠지만, 이제는 3307 포트가 ssh 터널 입구가 되어 있으므로 3307 로 들어온 트래픽(MySQL 클라이언트가 보내는 SQL 접속 요청, 인증정보, 쿼리 전부 트래픽) 이 ssh-client 에 의해 ec2 인스턴스로 보내진다. EC2는 SSH 세션 안에서 이 데이터를 꺼내, 다시 my-web-server-database.rds.amazonaws.com:3306 으로 보낸다. 3306은 RDS database 에서 열어둔 포트이다. RDS MySQL 서버가 응답하면, 반대 경로로 돌아온다. 기본적으로 -L 옵션(포트포워딩)은 SSH 세션을 맺는 동시에 내 로컬에 포트(3307)를 열어둔다.

포트 포워딩 이란, A의 특정 포트 ↔ B의 특정 포트 를 연결해주는 것인데 위 경우처럼 로컬의 3306 포트와 RDS의 3307 포트를 연결하는 것을 의미한다. 직접 연결하는 경우 (예를 들어, ec2에서 RDS 접속) 에는 포트 포워딩이 필요 없이 접속하고자 하는 서버의 포트 번호만 알면되지만 이렇게 중간단계 (ec2)가 있는 경우 포트 포워딩이 필요하고 SSH -L 옵션이 그 기능을 제공한다.

다음으로, 문제 2는 alembic 을 이용하여 해결할 수 있다.

DB가 변경되었을 때는 크게 두 가지가 있다. 스키마(테이블 구조)의 변경과 데이터(레코드)의 변화이다. 문제 2에 관해 전자는 alembic 을 이용하여 해결할 수 있고, 후자는 보통 운영 데이터는 직접 입력/마이그레이션한다.

poetry add alembic 
poetry env activate
source ~~~... # 위에서 나온 경로 사용하여 가상환경 진입
alembic init alembic # 프로젝트 루트에서 실행

이제 alembic.ini 안의 데이터베이스 url을 수정한다.

sqlalchemy.url = mysql+pymysql://admin:H**********@my-web-server-database.cneqoay20wx1.us-east-1.rds.amazonaws.com:3306/test_db # ec2에 있는 alembic.ini 파일
sqlalchemy.url = mysql+pymysql://root:localpassword@localhost:3306/local_db # 로컬에 있는 alembic.ini 파일. 

Alembic은 내부적으로 SQLAlchemy → PyMySQL 드라이버를 이용해서 DB에 직접 연결한다. 즉, ec2의 경우 내가 따로 mysql -h ... 명령어를 쳐서 ec2 필요 없이,

alembic upgrade head

만 하면 sqlalchemy.url 에 적힌 RDS에 접속해서 SQL을 실행한다.

alembic 은 sqlalchemy 팀에서 만든 것이기 때문에 Alembic이 DB랑 통신하거나, 테이블 구조를 읽을 때 SQLAlchemy를 기반으로 동작한다. Alembic은 ORM이 없어도 동작할 수 있지만, 자동으로 마이그레이션 스크립트를 생성하는 기능을 사용하지 못한다.

alembic revision --autogenerate -m "create users table" # migration script 생성
alembic upgrade head # git pull 한 경우 위 script가 있을테니 이를 실행하여 migration
alembic downgrade -1

마지막으로, 문제 1은 환경변수 파일과 python-dotenv 를 이용해 해결할 수 있다. 문제 2를 해결하는 과정에서 문제 1에 관련하여 alembic.ini 파일의 sqlalchemy.url 값도 로컬과 ec2에서 다르게 적어야 하는 불편함이 발생한다.

이를 위해 python-dotenv 를 먼저 설치한다.

poetry add python-dotenv

그리고 우리는 .env 파일을 만들어 필요한 환경변수들을 입력한다.

# local의 .env
DB_DIALECT=mysql
DB_DRIVER=localhost
DB_HOST=mydb.xxxxxx.ap-northeast-2.rds.amazonaws.com
DB_PORT=3306
DB_USER=root
DB_PASSWORD=""
DB_DATABASE=testdb

당연하게도 .env 는 로컬과 ec2에서 각자의 내용으로 다르게 적어야 하고 github에 올려서도 안된다. 따라서, .gitignore 파일에 .env 를 추가한다.

그리고 python 파일에서 다음과 같이 사용할 수 있다.

import os
from dotenv import load_dotenv
import pymysql

# .env 불러오기
load_dotenv()

# 환경변수 읽기
host = os.getenv("DB_HOST")
user = os.getenv("DB_USER")
password = os.getenv("DB_PASSWORD")
database = os.getenv("DB_DATABASE")

# DB 연결
conn = pymysql.connect(
    host=host,
    user=user,
    password=password,
    database=database,
    port=int(os.getenv("DB_PORT", 3306))
)

print("DB 연결 성공!")

Furthermore 2 - Frontend server

이제 위에서 했던 것과 마찬가지로 front-end의 repository 도 만들고 이 둘을 연결시켜 보자.

NBA_front 라는 레포지토리를 만들었고 다음 내용의 index.html 을 작성했다.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>My Frontend</title>
</head>
<body>
  <h1>Hello, Frontend!</h1>
  <p>This is a minimal static site.</p>
</body>
</html>

웹 서버가 필요하다. 전통적인 웹 서버 (Nginx/Apache) 인 Nginx(정적 파일(HTML, CSS, JS) 서빙에 최적화) 를 설치한다.

brew install nginx

설치 확인 방법은 다음과 같다. 브라우저로 다음 주소로 이동하면 특정 화면이 보인다.

nignx # 실행 case1
sudo nginx -c /opt/homebrew/etc/nginx/nginx.conf # 또는 conf 파일 명시적으로 지정 case2(root권한으로 실행)
brew services start nginx # 또는 Homebrew가 macOS 서비스 (launchpd)로 관리 case3

case 1,2 와 homebrew를 사용하는 것의 차이는 다음과 같다.

  • 그냥 실행 (nginx) = “앱 아이콘 더블클릭해서 실행” → 내가 직접 켜고 직접 꺼야 함. 재부팅하면 자동으로 안켜짐.
  • brew services start nginx = “로그인하면 자동 실행되는 백그라운드 앱” → 운영체제가 알아서 관리. 재부팅해도 자동으로 다시 켜짐.
  • ❗️brew services start nginx 로 실행하면 유저로 실행되는데 이를 user mode nginx라고 한다. root 권한이 없어서 80/443(특권 포트) listen 불가 하고 그래서 자동으로 8080에서 listen **한다. sudo nginx 로 실행하면 **root로 실행 8080 자동 listen 없다. (nginx.conf 읽은 그대로 listen)
  • ❗️nginx는 항상 2개의 프로세세를 통해 포트를 listen한다. master process는 설정 파일을 읽고 worker 생성/관리하는 역할을 한다. worker process는 실제 http 요청을 처리한다. sudo로 nginx를 실행하면 master process는 root 유저가 되고 worker process는 nobody 유저가 되기 때문에 80번 포트로 브라우저가 요청을 보냈을 때, nobody가 index.html에 접근할 권한이 없다면 403에러가 발생할 수 있다. 따라서, 해당 파일이 있는 폴더와 주변 폴더들의 권한을 조정할 필요가 있다. sudo로 실행하지 않는다면 master, worker 모두 해당 유저의 권한으로 실행된다.
http://localhost:8080 # 브라우저로 요청

nginx 종료 방법은 다음과 같다.

nginx -s quit # case 1, 2로 실행한 경우
brew services stop nginx

nginx의 기본 문서 루트는 보통 아래와 같다.

  • macOS(Homebrew): /usr/local/var/www
  • Ubuntu: /var/www/html homebrew 로 설치한 경우에는 /opt/homebrew/var/www

내 index.html을 위 디렉터리에 복사한다.

cp ~/Desktop/NBA_front/index.html /opt/homebrew/var/www/index.html

nginx 재시작한다.

nginx -s reload # 일반 경우
brew services restart nginx # homebrew 로 설치한 경우

이제, http://localhost:8080 로 다시 접속하면 index.html 이 보인다. 혹시 안보이는 경우에는 브라우저의 문제일 수 있으므로 command+R 을 통해 브라우저 강제 새로고침을 하면된다.

이렇게 했는데도 보이지 않는다면, nginx.conf 파일의 위치를 찾아 파일을 열고 다음 작업을 한다.

cd /opt/homebrew/etc/nginx # nginx.conf
vim nginx.conf

server {
        listen       8080;
        server_name  localhost;

        #charset koi8-r;

        #access_log  logs/host.access.log  main;

        location / {
            #root   html;
            root /Users/arcstone/Desktop/NBA_front; # 위 부분을 이것으로 변경!
            index index.html;
        }

보통 nginx.conf 파일의 root html 에서 html 은 /opt/homebrew/var/www을 의미하는데 그렇지 않을 경우가 있을 수 있으므로 아래와 같이 명시적으로 디렉터리를 설정해준다. 이렇게 하면 아까 index.html 을 복사한 작업은 굳이 필요 없었던 작업이 된다.

자 이제, backend 에서의 위 step 9~11 를 frontend 에서도 동일하게 진행한다. git push , ec2 접속 후 git clone , nginx 설치 등의 과정을 진행한다. 로컬에서와 마찬가지로, nginx.conf 을 수정하려고 하는데 파일을 봤더니 server block 이 없고 다음이 있다.

include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;

즉, 실제 웹사이트 root 설정은 conf.d 또는 sites-enabled 안에 별도 파일로 두는 게 일반적이다. 따라서, etc/nginx/sites-available/default를 수정한다.

# /etc/nginx/sites-available/default 아래 내용 수정
sudo vim default

#root /var/www/html; # 주석 처리
root /home/ubuntu/NBA_front; # 추가
index index.html # 추가

sudo chmod 755 /home/ubuntu
sudo chmod 755 /home/ubuntu/NBA_front
sudo chmod 644 /home/ubuntu/NBA_front/index.html

reload 하고 실행한 다음, http:http://3.80.105.171 로 접속하면 index.html 내용이 보인다.

sudo nginx -t
sudo systemctl reload nginx

다음은 ubuntu 시스템에서 실행, 중지, 재시작 문법이다

sudo systemctl start nginx     # 실행
sudo systemctl stop nginx      # 중지
sudo systemctl restart nginx   # 재시작
sudo systemctl reload nginx    # 설정만 다시 읽기
sudo systemctl status nginx    # 상태 확인

Furthermore 3 - connecting front & back

자, 이제 프런트와 백이 상호작용 하도록 만들어보자.

이제, Nginx 는 FastAPI 앞에, 리버스 프록시 역할을 하게 된다.

프록시 란, 중간에 대신 요청을 전달해 주는 서버를 의미한다. 흔히 프록시 서버 라고 하면 클라이언트 쪽에 가까이 있는 Forward Proxy 를 의미한다. 반면 Reverse Proxy 는 서버 쪽에 가까이 있다. 클라이언트는 오직 **리버스 프록시 (Nginx) ** 와만 통신한다. 리버스 프록시는 내부의 여러 서버로 요청을 분배하고, 다시 클라이언트에 응답한다.

브라우저 → Nginx(Reverse Proxy) → FastAPI 서버

/etc/nginx/sites-available/front.conf 같은 곳에 아래 처럼 작성한다.

server {
    listen 80;
    server_name _;

    # 정적 파일
    root /home/ubuntu/NBA_front;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }

    # API 요청 → FastAPI로 프록시
    location /api {
        proxy_pass http://127.0.0.1:8000; # uvicorn 서버가 대기중
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

그러면 다음과 같은 역할 분담이 이루어지게 된다.

  • Nginx:
    • 80포트에서 들어오는 모든 요청을 받음
    • 정적 파일(/index.html, /style.css 등)은 직접 서비스
    • /api 같은 경로는 FastAPI로 전달
  • FastAPI (uvicorn):
    • 오직 비즈니스 로직(API 처리)만 담당
    • 외부에 직접 노출되지 않고 Nginx 뒤에서 동작

이렇게 분리하면 정적 파일은 고성능으로 빠르게 서빙, API는 Python 애플리케이션 서버가 처리하여 안정적이고 확장성 있는 구조가 된다.

참고로 여기서 nginx+uvicorn+fastapi 가 아닌, nginx+fastapi 만으로 실행할 수 없는 이유는 파이썬 실행환경을 만들어주는 역할을 nginx 가 할 수 없기 때문이다.

development flow

실제 개발 과정에서는 dev (개발용) ec2 서버와, prod (배포용) ec2 서버 두 대를 이용하여 backend 에서 작업한 결과를 dev 에 push 하면, frontend 에서 로컬에서 작업 환경에서 로컬 프론트와 dev 의 백엔드를 바로 위에서 본것처럼 연결하여 테스트한다. 이후 프론트도 dev에 push 하고 다시 API 를 테스트한다. 최종적으로 prod 에 push 하여 배포를 한다.

Furthermore 4 - Docker & CI/CD

이렇게 코드를 작성하는 과정에서 다음 과정들이 로컬에서 수행한 작업인데 ec2 인스턴스에서 반복적으로 다시 수행하게 된다.

  1. 백엔드 ec2에서 python 설치. poetry 로 변경된 파이썬 패키지 설치.
  2. ec2 my-sql 클라이언트 설치
  3. ec2 .env 파일 생성
  4. 프론트 ec2에서 nginx 설치 및 nginx.conf 수정.

Docker 를 이용하면 이러한 문제들(1, 2, 4번)을 해결할 수 있다. Docker 에 앞서 가상머신(VM) 의 개념을 알아보자. 가상머신을 이용하면 원래는 한 컴퓨터 = 한 운영체제였는데 가상화 기술 덕분에 운영체제를 여러 개 동시에 실행할 수 있다. 각 운영체제는 독립적인 컴퓨터라고 믿고 돌아간다. (자기가 쓰는 CPU, 메모리, 디스크가 전부 실제인 줄 안다). 사실은 하이퍼바이저가 물리 자원을 나눠 쓰게 해주는 것.

즉, 가상머신은 소프트웨어로 하드웨어 자원(cpu, ram, disk)을 구현한 것인데, 컴퓨터 구조 시간에 “소프트웨어 규칙이 단순해지면(ex. 어레이 생성하는 머신코드가 명령어 한줄이면됨) 하드웨어가 복잡해지고(이를 구현하기 위한 회로 설계가 복잡해짐), 하드웨어 규칙이 단순해지면 소프트웨어 규칙이 복잡해진다(같은 동작을 하더라도 cpu가 할 수 있는 단위들로 쪼개져 명령어가 많고 복잡해짐) “ 관계를 생각하면 하드웨어와 소프트웨어가 상호 전환적이라는 것을 알 수 있다.

CPU를 소프트웨어로 구현하는 예를 보자. CPU는 명령어를 받아 실행한다. 예를 들어, x86 CPU의 명령어 ADD R1, R2 = “레지스터 R1과 R2를 더해 R1에 저장”한다는 의미이다.

  • 물리 CPU: 전자회로(가산기)가 바로 계산.

  • 소프트웨어 CPU(에뮬레이터): 프로그램 코드로 처리.

    # 가상의 레지스터 집합
    registers = {"R1": 5, "R2": 7}
      
    # 가상 CPU가 "ADD R1, R2" 명령어를 받음
    def emulate_add(r1, r2):
        registers[r1] = registers[r1] + registers[r2]
      
    emulate_add("R1", "R2")
    print(registers["R1"])  # 결과: 12
    

    여기서는 실제 CPU 회로가 없는데도, “ADD”라는 명령어의 결과를 똑같이 소프트웨어로 흉내 낸 것이다. 이게 바로 가상 CPU = 소프트웨어로 만든 하드웨어이다. 이런 원리로 가상머신 소프트웨어가 동작한다.

가상머신의 격리 수준은 하드웨어(물리 자원)까지 가상화 → 운영체제를 VM마다 새로 올려야 하는 반면에 도커의 격리 수준 OS 커널은 공유하고, 프로세스만 격리하는 것이다.

이제 Docker 에 대해 알아보자. 도커의 사용이유와 핵심적인 기능을 크게 컨테이너 격리 와 이미지를 이용한 배포 측면에서 깨달을 수 있다.

우선 이미지를 이용한 배포 측면을 알아보자. 프로그램은 호스트 OS + 언어 런타임 + 라이브러리 + 설정파일 위에서 실행된다. 예를 들어, 내가 로컬에서 작업한 파이썬 프로그램은 macOS (OS) + python3.11 (runtime) + numpy1.11 (라이브러리) 에 의해 돌아간다. 이 코드를 서버에서 pull 하여 사용하니 서버에 설치된 환경은 ubuntu (OS) + python3.13 (runtime) + numpy1.12 (라이브러리) 이다. 이렇게 되면 내가 올린 코드가 상황에 따라 동작하지 않을 수 있다. OS가 달라도 pure Python/Java 같은 코드라면 런타임 + 라이브러리만 있으면 돌아간다. 하지만 런타임 자체나 라이브러리가 커널 기능에 의존하면, OS 차이 때문에 동작이 달라지거나 아예 안 될 수 있다. Docker가 유용한 이유는 라이브러리/런타임을 패키징해서 환경 차이 줄이고 리눅스 커널 기반 표준화를 한다는 점이다.

Docker Image 는 작은 운영체제 + 필요한 프로그램들 이 들어있다.

  • 예를 들어 python:3.11 이미지를 쓰면, 그 안에는
    • 리눅스 배포판(예: Debian, Alpine 등 최소 OS 파일)
    • Python 3.11 실행 파일 + 표준 라이브러리가 이미 들어있다.

Dockekr Container 는 이미지를 기반으로 실행 중인 인스턴스(프로세스) 를 의미한다. 컨테이너를 실행하면, 호스트의 Python을 쓰는 게 아니라, 이미지 안에 포함된 Python 실행 파일을 쓴다. 그래서 호스트에 Python이 설치되어 있지 않아도 python 이 돌아간다. 도커는 이미지를 기반으로 컨테이너를 만들어 이 컨테이너들 간에 OS 커널은 공유 하고, 프로세스만 격리(런타임, 라이브러리 등) 하는 기술이다. 하지만 여기서 의문이 드는 것은, 윗 문단에서 OS의 차의에 의해 로컬에서 동작하는 코드가 서버에서는 동작하지 않을 수도 있다고 했다. 하지만 도커에서는 OS 가 그대로 사용되므로 OS의 차이가 그대로 유지되는 것 아닌가 하는 생각을 할 수 있다. 하지만 그렇지 않다. macOS에서 Docker 실행하면 사실은 Linux VM(가상머신) 위에서 돌아가는 구조 (Docker Desktop이 내부적으로 작은 Linux VM을 띄움)라서, 결국 linux OS 기반의 환경이 만들어진다. 다음으로, 컨테이너 격리 의 측면을 알아보자. 도커의 또 다른 핵심은 “하나의 호스트(OS 커널) 위에서 여러 개의 컨테이너가 동시에 실행되더라도, 각 컨테이너가 마치 자기만의 독립된 시스템(OS)에서 동작하는 것처럼 보이게 해준다”는 점이다. 그렇게 되면 여러 프로그램을 안전하게 동시에 실행할 수 있다. 실제로는 호스트 OS 커널을 공유하는데, 컨테이너 안에서는 Namespace 덕분에 자기만의 PID 공간 (프로세스 목록), 파일시스템, 네트워크 인터페이스, 사용자/호스트 이름을 가지는 것처럼 보인다. 도커를 사용하지 않은 경우와 사용한 경우, 컨테이너 격리의 이점 측면에서는 다음의 차이가 난다.

  • 만약 하나의 호스트 OS 위에 프로그램 A, B, C를 그냥 돌린다면?
    • 라이브러리 버전 충돌 (A는 numpy 1.11 필요, B는 numpy 1.21 필요)
    • 포트 충돌 (A가 5000번 포트 쓰는데 B도 5000번 포트 쓰려고 함)
    • 한 프로그램이 메모리 다 먹으면 다른 프로그램도 죽음
  • 컨테이너 격리 덕분에 각 컨테이너는 자기만의 세계에서 동작 → 충돌 없이 공존 가능.

Dockerfile은 간단히 말해 도커 이미지(Docker Image)를 만들기 위한 레시피(설계도)이다.

  • Dockerfile은 텍스트 파일이고, 안에는 이미지를 어떻게 만들지에 대한 명령어들이 적혀 있다.
  • docker build 명령어를 실행하면 Dockerfile을 읽고 그대로 실행해서 새로운 이미지를 만들어낸다. 이후, 이 이미지를 이용해 docker run 명령어를 실행하면 컨테이너가 실행된다.

도커 실행 흐름은 다음과 같다. 우선 아래와 같은 파이썬 소스파일이 있다.

# hello.py
import platform

print("Hello, Docker!")
print("Python version:", platform.python_version())

도커 파일을 만든다.

# Dockerfile
FROM python:3.11-slim   # 항상 Python 3.11 환경
WORKDIR /app            # 작업 디렉토리
COPY hello.py .         # 코드 복사
CMD ["python", "hello.py"]  # 컨테이너 실행시 명령

로컬에서 다음과 같이 소스코드를 실행할 수 있다.

# 이미지 빌드 (현재 디렉토리에 Dockerfile이 있다고 가정)
docker build -t mypythonapp .

# 컨테이너 실행
docker run --rm mypythonapp

Hello, Docker! # 출력
Python version: 3.11.9

ec2 에서도 마찬가지로 다음과 같이 소스코드를 실행할 수 있다.

docker build -t mypythonapp .
docker run --rm mypythonapp

이렇게 하면 우리가 제기한 문제 중, 1, 2, 4번에서 프로그램 설치는 중복해서 할 필요가 없어졌다. 도커 빌드만 중복해서 하면 된다. 그러나 4번에서 conf 같이 설정값이 다른 경우는 도커로는 해결할 수 없고 .env 파일에 모아두는 수밖에 없다. 즉, 3번 문제는 근본적으로 해결하기 어렵다.

이제 CI/CD 에 대해서 알아보자. 위에서 우리가 프론트와 백의 협업 과정 순서를 생각하면 아래와 같았다.

  1. 백엔드 개발자 → dev EC2에 local 코드 반영 (실제로는 github 다른 brach에 push하여 main으로 pull request를 먼저 보내고 이후 팀원들의 승인에 따라 merge 됨. 그리고 ec2에서 pull)

  2. 프론트 개발자 → 로컬 프론트 + dev 백엔드 연결 → 테스트

  3. 프론트 개발자도 dev EC2에 push (실제로는 pull request를 먼저 보내고 merge 됨)→ 통합 테스트

  4. 최종 검증 후 prod EC2에 push → 실제 서비스 배포

이 때, 1~4 번의 과정 전반에 도커를 사용하면 반복되는 작업을 없애고 동일한 환경을 구축할 수 있음을 배웠다. 하지만 여전히 몇가지 비효율이 남아있다.

  • 문제1: 1, 3번 과정에서 git push 는 로컬에서 개발자가 준비된 때에 하는 것이 당연하다. 하지만 ec2에서 git pull 과 docker build 과정을 자동화할 수 없을까?
  • **문제2: **2번 과정에서 로컬 프론트 ↔ dev 백엔드 직접 연결하는데 이를 자동화할 수 없을까? 또한, dev 현재 상태가 최신인지 프론트 사람들이 항상 확인하고 테스트해야하는 불편함을 해결할 수 없을까?
  • **문제3: **1~4번 과정에서 사람이 dev → prod 순서로 수동 반영하는데 dev 배포 → 테스트 → prod 배포가 자동화 될 수 없을까?
  • 문제4: **1번 과정에서 보통 merge를 하고 나서야 dev 에서 pull을 하고 테스트가 가능한데 PR을 열자마자 dev 서버에 자동 배포 → 팀원들이 **dev 서버 주소로 들어가서 실제 동작을 검증 가능하게 할 수 없을까?

CI/CD 는 이에 대한 해답이다. 우선 정의를 보자.

  • CI (Continuous Integration, 지속적 통합)
    • 개발자들이 작성한 코드를 자주 병합(merge)하고 자동으로 빌드·테스트하는 과정
    • 목적: 코드가 합쳐질 때마다 문제가 없는지 빠르게 확인
    • 예: 내가 push → 자동으로 test 실행 → 에러 있으면 알려줌
    • 즉, 코드 검증 자동화
  • CD (Continuous Deployment, 지속적 배포)
    • CI 이후, 테스트 통과된 코드를 자동으로 서버에 배포하는 과정
    • 목적: 코드를 수정하면 곧바로 사용자 환경에 반영
    • 예: main 브랜치에 push → 자동으로 EC2에 배포 → 서비스 업데이트 완료
    • 즉, 배포 자동화

이러한 CI/CD 의 개념을 가능하게 해주는 도구 중 하나가 바로 GitHub Actions 이다.

GitHub Actions 는 GitHub에서 제공하는 자동화 도구이다.

  • 코드 저장소에 event (예: git push, pull request)가 발생하면, 정해둔 workflow(자동화 시나리오)를 실행한다.
  • CI/CD 작업을 자동으로 처리할 수 있다.

사용하는 법은 다음과 같다.

  • GitHub Actions는 .github/workflows/ 폴더 안에 있는 YAML 파일을 찾아서 push/pull request 시 자동으로 실행한다. 따라서, 레포지토리에 이 폴더/파일이 없으면 GitHub Actions는 아무 일도 하지 않는다.

  • GitHub에서 Actions 탭 → 새로운 Workflow 설정을 클릭하면, GitHub이 자동으로 .github/workflows/ci.yml 같은 샘플 파일을 생성해 준다. 또는 직접 로컬에서 .github/workflows/ 폴더 만들고, 그 안에 ci.yml, deploy.yml 같은 걸 작성해도 된다.

이제 YAML 파일 예시를 보며 위에서 제기한 문제를 어떻게 해결하고 있는지 알아보자.

name: CI/CD Pipeline

on:
  push:
    branches:
      - main    # main에 push되면 실행

jobs:
  build-and-push-image:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      # Docker 이미지 빌드
      - name: Build Docker image
        run: docker build -t myapp .

      # Docker Hub 로그인
      - name: Login to DockerHub
        run: echo "$" | docker login -u "$" --password-stdin

      # Docker Hub로 이미지 push
      - name: Push image
        run: |
          docker tag myapp mydockerhubuser/myapp:latest
          docker push mydockerhubuser/myapp:latest
  
  # 문제1, 2, 4 해결! (docker build 자동화 하고 서버 실행, ec2 코드 자동 업데이트)
  deploy-to-dev: 
    needs: build-and-push-image
    runs-on: ubuntu-latest
    steps:
      - name: Deploy to dev EC2
        uses: appleboy/ssh-action@v0.1.10
        with:
          host: $
          username: ubuntu
          key: $
          script: |
            docker pull mydockerhubuser/myapp:latest
            docker stop myapp || true
            docker rm myapp || true
            docker run -d --name myapp -p 8080:8080 mydockerhubuser/myapp:latest

  deploy-to-prod:
    needs: deploy-to-dev # 문제 3 해결!
    runs-on: ubuntu-latest
    steps:
      - name: Deploy to prod EC2
        uses: appleboy/ssh-action@v0.1.10
        with:
          host: $
          username: ubuntu
          key: $
          script: |
            docker pull mydockerhubuser/myapp:latest
            docker stop myapp || true
            docker rm myapp || true
            docker run -d --name myapp -p 80:80 mydockerhubuser/myapp:latest

문제1 (EC2 git pull + docker build 수동 작업 제거)

개발자가 GitHub에 push → GitHub Actions 실행

  • 이벤트(push) 감지 → Runner(가상머신)에서 Workflow 실행
  • actions/checkout으로 레포지토리의 코드 내려받기
  • docker build 로 새 이미지 생성 (업데이트 된 레포지토리의 코드 + 의존성 포함)
  • 이 이미지를 Docker Hub(또는 AWS ECR) 에 push

  • EC2에서 반영 GitHub Actions Workflow 안에 SSH 배포 단계 있음:

    docker pull mydockerhubuser/myapp:latest
    docker stop myapp || true
    docker rm myapp || true
    docker run -d --name myapp -p 80:80 mydockerhubuser/myapp:latest
    
  • 즉, EC2는 git repo를 몰라도 되고 단지 Docker Hub에서 최신 이미지를 받아 실행만 하면 됨.

2번 문제 (로컬 프론트 ↔ dev 백엔드 연결) 프론트 개발자는 “dev EC2 주소”만 열면 된다. GitHub Actions가 main에 push되면 dev EC2에 자동 배포하므로, 항상 최신 백엔드가 준비된다.프론트 코드에서 API 주소를 바꾸는 건 여전히 필요하지만, “서버에 최신 코드 올라갔는지 확인 → 다시 빌드/실행” 같은 수동 작업이 없어진다.

3번 문제 (dev → prod 순서 수동 반영)

  • needs: deploy-to-dev → dev 배포 후에만 prod 실행됨 (순차적)

  • environment → prod 배포 전에 GitHub Actions에서 “승인(Approve)” 버튼을 눌러야 진행됨.

Leave a Comment