배포 개념

배포 개념 (Deployments Concepts)

FastAPI 앱을, 아니 사실 어떤 종류의 웹 API든 배포할 때 신경 써야 할 개념들이 있어요. 이 개념들을 이해하면 가장 적절한 방식으로 앱을 배포할 수 있는 기준이 생겨요.

중요한 개념들은 이 정도예요:

  • 보안(Security) - HTTPS
  • 시작 시 실행(Running on startup)
  • 재시작(Restarts)
  • 복제(Replication - 실행 중인 프로세스 수)
  • 메모리(Memory)
  • 시작 전 사전 단계(Previous steps before starting)

이 개념들이 배포에 어떻게 영향을 주는지 하나씩 볼게요.

궁극적인 목표는 API 클라이언트에게 안전하게 서비스하고, 중단을 피하며, 컴퓨팅 자원(예: 원격 서버/가상 머신)을 최대한 효율적으로 쓰는 거예요. 🚀

여기서 이 개념들에 대해 조금 더 알려드릴게요. 그러면 아주 다양한 환경(심지어 아직 존재하지 않는 미래의 환경일지라도)에서 어떻게 배포할지 결정하는 데 필요한 직감을 얻을 수 있을 거예요. 이 개념들을 고려하면 여러분만의 API를 배포하는 최선의 방식을 평가하고 설계할 수 있게 돼요.

다음 장들에서는 FastAPI 앱을 배포하는 더 구체적인 레시피를 알려줄게요. 지금은 이 중요한 개념적 아이디어들을 살펴볼게요. 이 개념들은 다른 종류의 웹 API에도 동일하게 적용돼요. 💡

출처: 공식문서

보안 - HTTPS

이전의 HTTPS 장에서 HTTPS가 API에 암호화를 제공하는 방법을 배웠어요.

또한 HTTPS는 대개 애플리케이션 서버 외부의 컴포넌트, 즉 **TLS 종료 프록시(TLS Termination Proxy)**에서 제공된다는 것도 봤어요.

그리고 HTTPS 인증서를 갱신하는 책임을 지는 무언가가 있어야 해요. 그게 같은 컴포넌트일 수도, 다른 것일 수도 있어요.

HTTPS를 위한 도구 예시

TLS 종료 프록시로 쓸 수 있는 도구 몇 가지를 보면:

  • Traefik
    • 인증서 갱신을 자동으로 처리해요 ✨
  • Caddy
    • 인증서 갱신을 자동으로 처리해요 ✨
  • Nginx
    • Certbot 같은 외부 컴포넌트로 인증서를 갱신해요
  • HAProxy
    • Certbot 같은 외부 컴포넌트로 인증서를 갱신해요
  • Kubernetes with an Ingress Controller like Nginx
    • cert-manager 같은 외부 컴포넌트로 인증서를 갱신해요
  • 클라우드 제공자가 서비스의 일부로 내부 처리
    • (아래 👇 참고)

또 다른 선택지는, HTTPS 설정을 포함해 더 많은 작업을 해 주는 클라우드 서비스를 쓰는 거예요. 제약이나 추가 비용이 있을 수 있지만, 그 경우 직접 TLS 종료 프록시를 설정하지 않아도 돼요.

구체적인 예시는 다음 장들에서 보여줄게요.


그다음 고려할 개념들은 실제 API를 실행하는 프로그램(예: Uvicorn)에 관한 거예요.

프로그램과 프로세스

앞으로 실행 중인 " 프로세스(process)"에 대해 많이 이야기할 거라서, 그게 무슨 뜻이고 " 프로그램(program)"이라는 단어와 뭐가 다른지 확실히 해 두는 게 좋아요.

프로그램이란

프로그램이라는 단어는 흔히 여러 가지를 가리켜요:

  • 여러분이 작성한 코드, 즉 파이썬 파일들.
  • 운영체제가 실행할 수 있는 파일, 예: python, python.exe, uvicorn.
  • 운영체제에서 실행 중인 특정 프로그램 (CPU를 쓰고 메모리에 데이터를 저장하는 상태). 이걸 프로세스라고도 불러요.

프로세스란

프로세스라는 단어는 보통 더 좁은 의미로, 오직 운영체제에서 실행 중인 것을 가리켜요 (위의 마지막 항목처럼요):

  • 운영체제에서 실행 중인 특정 프로그램.
    • 이건 파일을 가리키는 것도, 코드를 가리키는 것도 아니에요. 오직 실행되고 운영체제가 관리하는 것을 가리켜요.
  • 어떤 프로그램, 어떤 코드든 실행 중일 때만 뭔가를 할 수 있어요. 즉, 프로세스가 실행 중일 때요.
  • 프로세스는 여러분이나 운영체제에 의해 종료(혹은 "kill")될 수 있어요. 그러면 실행이 멈추고 더 이상 아무것도 할 수 없어요.
  • 컴퓨터에서 실행 중인 각 애플리케이션 뒤에는 프로세스가 있어요. 실행 중인 모든 프로그램, 모든 창 등 말이죠. 그리고 컴퓨터가 켜져 있는 동안 보통 동시에 많은 프로세스가 실행돼요.
  • 같은 프로그램여러 프로세스가 동시에 실행될 수 있어요.

운영체제의 "작업 관리자(task manager)"나 "시스템 모니터(system monitor)"(비슷한 도구)를 열면 그런 프로세스들을 많이 볼 수 있어요.

예를 들어 같은 브라우저 프로그램(Firefox, Chrome, Edge 등)을 실행하는 프로세스가 여러 개 있는 걸 볼 거예요. 보통 탭마다 프로세스 하나씩, 거기에 추가 프로세스 몇 개가 더 붙어요.

시작 시 실행 (Running on Startup)

대부분의 경우 웹 API를 만들 때는 클라이언트가 항상 접근할 수 있도록 항상 실행되고 중단되지 않기를 바라요. 특정 상황에서만 실행되길 원하는 이유가 있다면 모르겠지만, 대부분의 경우 계속 실행되고 가용한 상태를 원하죠.

원격 서버에서

원격 서버(클라우드 서버, 가상 머신 등)를 설정할 때 가장 간단한 방법은 로컬에서 개발할 때처럼 fastapi run(Uvicorn을 사용) 같은 것을 수동으로 실행하는 거예요.

그러면 개발 중에는 잘 작동하고 유용해요.

하지만 서버와의 연결이 끊기면 실행 중인 프로세스가 아마 죽을 거예요.

그리고 서버가 재시작되면(예: 업데이트 후, 또는 클라우드 제공자의 마이그레이션 후) 아마 눈치채지 못할 거예요. 그래서 프로세스를 수동으로 재시작해야 한다는 것조차 모르게 되죠. 그러면 API는 그냥 죽은 채로 남아요. 😱

시작 시 자동 실행

일반적으로 서버 프로그램(예: Uvicorn)이 서버 시작 시 자동으로 시작되고, 사람의 개입 없이 여러분의 API를 실행하는 프로세스(예: FastAPI 앱을 실행하는 Uvicorn)가 항상 떠 있기를 원할 거예요.

별도의 프로그램

이를 위해 보통 애플리케이션이 시작 시 실행되게 보장하는 별도의 프로그램이 있어요. 그리고 많은 경우 그 프로그램이 데이터베이스 같은 다른 컴포넌트나 애플리케이션도 실행되게 해요.

시작 시 실행하는 도구 예시

이 일을 할 수 있는 도구의 예시로는:

  • Docker
  • Kubernetes
  • Docker Compose
  • Docker in Swarm Mode
  • Systemd
  • Supervisor
  • 클라우드 제공자가 서비스의 일부로 내부 처리
  • 기타...

구체적인 예시는 다음 장들에서 볼게요.

재시작 (Restarts)

애플리케이션이 시작 시 실행되게 하는 것과 비슷하게, 실패 후 재시작되게 하는 것도 원할 거예요.

우리는 실수를 해요

우리 인간은 언제나 실수를 해요. 소프트웨어는 거의 항상 어딘가 숨은 버그가 있어요. 🐛

그리고 개발자로서 우리는 버그를 찾고 새 기능을 구현하면서(어쩌면 새 버그도 만들면서 😅) 코드를 계속 개선해요.

작은 오류는 자동 처리

FastAPI로 웹 API를 만들 때 코드에 오류가 있으면, FastAPI는 보통 그 오류를 오류를 유발한 단일 요청으로 격리해요. 🛡

그 요청에 대해 클라이언트는 500 Internal Server Error를 받지만, 애플리케이션은 완전히 죽는 대신 다음 요청들을 위해 계속 작동해요.

더 큰 오류 - 크래시

그렇지만 어떤 코드는 전체 애플리케이션을 크래시시켜서 Uvicorn과 Python이 죽는 경우도 있어요. 💥

그래도 한 곳의 오류 때문에 애플리케이션이 죽은 채로 남기를 원하지는 않을 거예요. 적어도 고장 나지 않은 경로 연산들만이라도 계속 실행되길 원하죠.

크래시 후 재시작

하지만 실행 중인 프로세스를 크래시시키는 정말 나쁜 오류의 경우에는, 프로세스를 재시작하는 책임을 지는 외부 컴포넌트가 있길 원할 거예요. 적어도 몇 번은요...

— 다만 전체 애플리케이션이 즉시 크래시한다면 계속 재시작해 봐야 의미 없을 수 있어요. 하지만 그런 경우는 보통 개발 중에, 적어도 배포 직후에 알아차리게 돼요.

그러니 크래시가 완전한 경우에 집중할게요. 미래에 특정 경우에 완전히 크래시해도, 재시작하는 게 여전히 의미 있는 상황이요.

애플리케이션을 재시작하는 책임을 지는 것을 외부 컴포넌트로 두고 싶을 거예요. 그 시점엔 Uvicorn과 Python을 든 같은 애플리케이션이 이미 크래시했으니, 같은 앱의 같은 코드 안에는 아무것도 할 수 있는 게 없으니까요.

자동 재시작 도구 예시

대부분의 경우 시작 시 프로그램을 실행하는 데 쓰는 그 도구가 자동 재시작도 처리해요.

예를 들어 이렇게 처리할 수 있어요:

  • Docker
  • Kubernetes
  • Docker Compose
  • Docker in Swarm Mode
  • Systemd
  • Supervisor
  • 클라우드 제공자가 서비스의 일부로 내부 처리
  • 기타...

복제 - 프로세스와 메모리 (Replication)

FastAPI 앱에서 Uvicorn을 실행하는 fastapi 명령 같은 서버 프로그램을 하나의 프로세스로 한 번 실행하면, 여러 클라이언트를 동시에 서비스할 수 있어요.

하지만 많은 경우 여러 워커 프로세스를 동시에 실행하고 싶어해요.

여러 프로세스 - 워커 (Workers)

단일 프로세스가 감당할 수 있는 것보다 클라이언트가 많고(예: 가상 머신이 그리 크지 않은 경우), 서버 CPU에 코어가 여러 개라면, 같은 애플리케이션으로 여러 프로세스를 동시에 실행하고 요청들을 그 사이에 분산할 수 있어요.

같은 API 프로그램의 여러 프로세스를 실행하면, 이것들을 보통 **워커(workers)**라고 불러요.

워커 프로세스와 포트

HTTPS 문서에서, 서버에서 포트와 IP 주소의 조합 하나에는 오직 하나의 프로세스만 리슨할 수 있다고 배웠던 것 기억나요?

그건 여전히 사실이에요.

그래서 여러 프로세스를 동시에 가지려면, 포트에서 리슨하는 단일 프로세스가 있어서 그 통신을 각 워커 프로세스로 어떤 방식으로든 전달해야 해요.

프로세스당 메모리

이제 프로그램이 메모리에 뭔가를 불러올 때, 예를 들어 머신러닝 모델을 변수에 넣거나 큰 파일 내용을 변수에 넣으면, 그 모든 게 서버의 메모리(RAM)를 조금씩 소비해요.

그리고 여러 프로세스는 보통 메모리를 공유하지 않아요. 즉 각 실행 프로세스는 자기만의 것, 변수, 메모리를 가져요. 코드에서 많은 메모리를 소비한다면 각 프로세스가 그에 상응하는 만큼의 메모리를 소비해요.

서버 메모리

예를 들어 코드가 1 GB 크기의 머신러닝 모델을 불러온다면, API로 프로세스 하나를 실행할 때 최소 1 GB의 RAM을 소비해요. 그리고 4개의 프로세스(워커 4개)를 시작하면 각각 1 GB의 RAM을 소비하죠. 그래서 총 API가 4 GB의 RAM을 소비하게 돼요.

그리고 원격 서버나 가상 머신의 RAM이 3 GB뿐이라면, 4 GB 이상의 RAM을 불러오려는 시도는 문제를 일으켜요. 🚨

여러 프로세스 - 예시

이 예시에는 두 개의 워커 프로세스를 시작하고 제어하는 **관리자 프로세스(Manager Process)**가 있어요.

이 관리자 프로세스가 아마 IP의 포트에서 리슨할 거예요. 그리고 모든 통신을 워커 프로세스들로 전달하죠.

워커 프로세스들이 여러분의 애플리케이션을 실행해요. 요청을 받아 응답을 반환하는 주요 계산을 수행하고, 변수에 넣은 어떤 것이든 RAM에 불러와요.

물론 같은 머신에는 여러분의 애플리케이션 말고도 다른 프로세스들이 실행되고 있을 거예요.

흥미로운 세부사항은 각 프로세스가 쓰는 CPU의 비율은 시간에 따라 크게 달라질 수 있지만, **메모리(RAM)**는 보통 어느 정도 안정적이라는 거예요.

매번 비슷한 양의 계산을 하는 API가 있고 클라이언트가 많다면, CPU 사용률도 아마 안정적일 거예요 (계속 급변하기보다는요).

복제 도구와 전략 예시

이를 달성하는 방법은 여러 가지가 있는데, 구체적인 전략에 대해서는 다음 장들(Docker, 컨테이너 이야기)에서 더 알려줄게요.

고려해야 할 핵심 제약은, 공용 IP포트를 처리하는 단일 컴포넌트가 있어야 한다는 거예요. 그리고 복제된 프로세스/워커들로 통신을 전달할 방법이 있어야 하고요.

가능한 조합과 전략 몇 가지를 보면:

  • Uvicorn with --workers
    • 하나의 Uvicorn 프로세스 관리자IP와 포트에서 리슨하고, 여러 Uvicorn 워커 프로세스를 시작해요.
  • Kubernetes 및 기타 분산 컨테이너 시스템
    • Kubernetes 계층의 무언가가 IP와 포트에서 리슨해요. 복제는 여러 컨테이너(각각 Uvicorn 프로세스 하나를 실행)를 두는 방식으로 이뤄져요.
  • 처리를 대신해 주는 클라우드 서비스
    • 클라우드 서비스가 아마 복제를 대신 처리해 줄 거예요. 실행할 프로세스나 쓸 컨테이너 이미지를 정의하게 해 줄 수도 있고, 어쨌든 대부분 단일 Uvicorn 프로세스일 거예요. 클라우드 서비스가 그것을 복제하는 책임을 지고요.

— 컨테이너, Docker, Kubernetes 관련 항목 중 이해가 안 되는 게 있어도 걱정 마세요. 컨테이너 이미지, Docker, Kubernetes 등에 관해서는 다음 장 FastAPI in Containers - Docker에서 더 알려줄게요.

시작 전 사전 단계 (Previous Steps Before Starting)

애플리케이션을 시작하기 전에 몇 가지 단계를 수행하고 싶은 경우가 많아요.

예를 들어 데이터베이스 마이그레이션을 실행하고 싶을 수 있어요.

하지만 대부분의 경우 이런 단계는 한 번만 수행하고 싶어요.

그래서 애플리케이션을 시작하기 전에 그 사전 단계들을 수행할 단일 프로세스가 있길 원할 거예요.

그리고 그 뒤에 애플리케이션 자체를 여러 프로세스(여러 워커)로 시작하더라도 그 사전 단계들은 단일 프로세스가 수행하도록 확실히 해야 해요. 그 단계들을 여러 프로세스가 수행하면 병렬로 실행하면서 작업을 중복하게 되고, 데이터베이스 마이그레이션처럼 민감한 단계라면 서로 충돌을 일으킬 수 있어요.

물론 사전 단계를 여러 번 실행해도 문제없는 경우도 있어요. 그런 경우엔 훨씬 다루기 쉬워요.

— 설정에 따라 어떤 경우엔 애플리케이션을 시작하기 전에 사전 단계가 아예 필요 없을 수도 있다는 점도 기억해 두세요. 그 경우엔 이런 것들에 대해 걱정할 필요가 없어요. 🤷

사전 단계 전략 예시

이건 시스템을 배포하는 방식크게 달려 있고, 프로그램을 시작하거나 재시작을 다루는 방식과도 연결될 거예요.

가능한 아이디어 몇 가지를 보면:

  • 앱 컨테이너보다 먼저 실행되는 Kubernetes의 "Init Container"
  • 사전 단계를 실행한 다음 애플리케이션을 시작하는 bash 스크립트
    • 여전히 그 bash 스크립트를 시작/재시작하고 오류를 감지할 방법이 필요해요.

— 컨테이너로 이걸 하는 더 구체적인 예시는 다음 장 FastAPI in Containers - Docker에서 볼게요.

자원 사용 (Resource Utilization)

여러분의 서버는 하나의 자원이에요. 프로그램으로 CPU의 계산 시간과 사용 가능한 RAM 메모리를 소비, 즉 활용할 수 있어요.

시스템 자원을 얼마나 소비/활용하고 싶을까요? "별로 안"이라고 생각하기 쉬운데, 실제로는 크래시하지 않는 한 최대한 많이 소비하고 싶을 거예요.

서버 3대를 돈 주고 쓰면서 RAM과 CPU를 조금밖에 안 쓴다면, 아마 돈을 낭비 💸 하고 있고, 아마 서버 전력도 낭비 🌎 하고 있을 거예요.

그 경우엔 서버 2대만 두고 그 자원(CPU, 메모리, 디스크, 네트워크 대역폭 등)의 더 높은 비율을 쓰는 게 나을 수 있어요.

반면에 서버 2대에서 **CPU와 RAM의 100%**를 쓰고 있다면, 어느 시점엔 어떤 프로세스가 더 많은 메모리를 요청해서 서버가 디스크를 "메모리"처럼 써야 할 수도 있는데(수천 배 느릴 수 있어요), 그러다 크래시할 수도 있어요. 또는 어떤 프로세스가 계산을 해야 하는데 CPU가 다시 비워질 때까지 기다려야 할 수도 있고요.

이런 경우엔 서버를 하나 더 두고 그 위에서 프로세스 몇 개를 실행해서 모두 충분한 RAM과 CPU 시간을 갖게 하는 게 낫겠죠.

또한 어떤 이유로 API 사용량에 **급증(spike)**이 있을 가능성도 있어요. 입소문을 타거나 다른 서비스/봇이 쓰기 시작할 수도 있죠. 그런 경우를 대비해 추가 자원을 두고 싶을 수도 있어요.

목표로 삼을 임의의 숫자를 정할 수도 있어요. 예를 들어 자원 사용률 50%에서 90% 사이 같은 거요. 요점은 그런 것들이 배포를 측정하고 조정할 때 주로 보고싶은 것들이라는 거예요.

htop 같은 간단한 도구로 서버에서 쓰는 CPU와 RAM, 또는 각 프로세스가 쓰는 양을 볼 수 있어요. 아니면 서버들에 분산될 수 있는 더 복잡한 모니터링 도구를 쓸 수도 있어요.

정리 (Recap)

지금까지 애플리케이션을 어떻게 배포할지 결정할 때 염두에 두면 좋은 주요 개념 몇 가지를 읽었어요:

  • 보안(Security) - HTTPS
  • 시작 시 실행(Running on startup)
  • 재시작(Restarts)
  • 복제(Replication - 실행 중인 프로세스 수)
  • 메모리(Memory)
  • 시작 전 사전 단계(Previous steps before starting)

이 개념들과 그것을 적용하는 방법을 이해하면, 배포를 구성하고 조정할 때 어떤 결정이든 내리는 데 필요한 직감을 얻을 수 있어요. 🤓

다음 섹션들에서는 따라 할 수 있는 전략의 더 구체적인 예시를 알려줄게요. 🚀

더 알아보기 (Learn more)