[LLM 경제 뉴스 요약 서비스] # 03. FastAPI 서버 배포 (ECR, ECS)

2026. 1. 7. 22:37Programming/Project

이번 포스팅에서는 저번에 구현한 FastAPI/Postgresql 을 배포해보려합니다.

작업 순서

1. DB EC2 배포
2. Docker Build
3. ECR에 Docker 이미지 Push
4. ECS 배포
4-1. 클러스터 정의
4-2. 태스크 정의 (PostgreSQL, FastAPI)
4-3. 서비스 정의

 

DB EC2 배포

서버 배포 전에 DB 부터 배포해보겠습니다.

DB는 컨테이너처럼 내려갔다 올라갔다 하면 안되니, EC2로 따로 배포했습니다.

1. 인스턴스 생성

  • AMI: Ubuntu 22.04 LTS
  • Instance type: t3.micro
  • Storage: 20GB gp3
  • Key pair: .pem 생성 후 저장
보안 그룹

SSH를 뚫어서, 로컬에서 포트포워딩으로 접속해보겠습니다.
인바운드 규칙에서 22포트를 허용하고
소스(원본)는 보안이 중요하면 '내 IP'로 지정해주시고, 저는 테스트 단계라 0.0.0.0/0으로 전체 허용해줬습니다.
+ 5432 포트는 어디서 여느냐 하면, 이후에 ECS로 정의하는 task의 보안그룹과 연결해 줄 겁니다.

2. DB 설치

인스턴스가 실행되면,

인증 키페어를 저장한 디렉토리에서 ssh로 접근해봅니다.

(base) ➜  chmod 400 postgresql_dev.pem
(base) ➜  project ssh -i postgresql_dev.pem ubuntu@<퍼블릭 IP>

 

 

접속이 되면 postgreql 을 설치/확인 후에는 실행해줍니다.

sudo apt update
sudo apt install -y postgresql postgresql-contrib

psql --version
sudo systemctl status postgresql

sudo -u postgres psql

 

DB 권한 설정

 

-- 유저 생성
CREATE USER dev_user WITH PASSWORD 'strong-password';

-- DB 생성
CREATE DATABASE llm_economy_summary OWNER dev_user;

-- 권한
GRANT ALL PRIVILEGES ON DATABASE llm_economy_summary TO dev_user;

\q

 

그리고 이제 2가지 설정파일을 수정해줘야 합니다.

postgersql.conf
listen_addresess = "*"
port = 5432
ubuntu@ip-***:~$ sudo nano /etc/postgresql/<version>/main/postgresql.conf

 

pg_hba.conf
host    all     all     127.0.0.1/32     scram-sha-256
host    all     all     172.31.0.0/16    scram-sha-256
ubuntu@ip-***:~$ sudo nano /etc/postgresql/<version>/main/pg_hba.conf

 

aws 및 로컬에서 DB 접근 허용을 위한 설정을 추가해줍니다.
+ 처음에 md5로 설정했다가 에러를 경험했으니 주의하세요..

 

이제 DB를 재시작(필) 후 사용자 비밀번호를 설정해 접근을 테스트해봅니다.

ubuntu@ip-***:~$ sudo systemctl restart postgresql
ubuntu@ip-***:~$ sudo -u postgres psql
psql (16.11 (Ubuntu 16.11-0ubuntu0.24.04.1))
Type "help" for help.

postgres=# ALTER USER <사용자> WITH PASSWORD '';
ALTER ROLE
postgres-# \q
ubuntu@ip-***:~$ psql -h localhost -U <사용자> -d llm_economy_summary
Password for user <사용자>:
psql (16.11 (Ubuntu 16.11-0ubuntu0.24.04.1))
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off)
Type "help" for help.

llm_economy_summary=>

 

3. DB 포트포워딩

보안그룹에서 ssh만 열어뒀기 때문에

로컬에서 접근하려면 포트포워딩을 해줘야해요.

(base) ➜  project ssh -i postgresql_dev.pem -L 5432:localhost:5432 ubuntu@<퍼블릭 IP>
Welcome to Ubuntu 24.04.3 LTS (GNU/Linux 6.14.0-1015-aws x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/pro
...

 

로컬에서는 localhost:5432로 진입해주면 됩니다.

 

 

Docker Build

1. Dockerfile

프로젝트 내, Dockerfile 생성 후

패키지 설치와 실행 플로우를 구축합니다.

# builder(패키지 설치) 스테이지
FROM python:3.12-slim-bookworm as builder # 파이썬 버전 확인 후 설정

RUN pip install --upgrade pip && \ # poetry 설치
    pip install poetry==2.2.*

ENV POETRY_NO_INTERACTION=1 \
    POETRY_VIRTUALENVS_IN_PROJECT=1 \
    POETRY_CACHE_DIR=/tmp/poetry_cache

WORKDIR /app

COPY pyproject.toml poetry.lock ./ # 패키지 lock 복사

RUN --mount=type=cache,target=$POETRY_CACHE_DIR \  # 패키지 설치
    poetry install --no-root

FROM python:3.12-slim-bookworm # 실행 스테이지

WORKDIR /app

ENV VIRTUAL_ENV=/app/.venv \
    PATH="/app/.venv/bin:$PATH"

COPY --from=builder ${VIRTUAL_ENV} ${VIRTUAL_ENV}

COPY . .

EXPOSE 8000

CMD ["uvicorn", "src.user_interface.restapi:app", "--host", "0.0.0.0", "--port", "8000", "--no-access-log", "--no-use-colors", "--log-level", "warning"]

 

2. Build / Run

이제 로컬에서 빌드해보고 8000포트로 실행까지 테스트해봅시다.

+ 저는 ecs를 linux/amd64 환경으로 설정했기에, 이미지도 동일하게 맞춰주지 않으면 exec format error가 납니다

이렇게요..

# build
docker buildx build -f Dockerfile --platform linux/amd64 -t <IMAGE_NAME>:latest .
# run
docker run --rm -d --name <CONTAINER_NAME> -p 8000:8000 <IMAGE_NAME>:latest

 

localhost에서 확인해보면 잘 작동하는 것을 볼 수 있습니다.

error >

초반 빌드 과정에서 pyproject.toml 내에 [tool.poetry] 가 없다는 에러가 났습니다.

poetry의 초기 설정을 그대로 따랐었는데,
확인해보니 프로젝트내 pyproject.toml은 [project]로 기재되어있고
이는 PEP-621 형식으로, 25년 1월 poetry 2.0.0 이상부터 지원되기 시작했다고 하네요.

# 이전 코드
RUN pip install --upgrade pip && \ # poetry 설치
    pip install poetry==1.8.*
 => ERROR [builder 5/5] RUN --mount=type=cache,target=/t  0.4s
------
 > [builder 5/5] RUN --mount=type=cache,target=/tmp/poetry_cache poetry install --without dev,test --no-root:
0.284 
0.284 [tool.poetry] section not found in /app/pyproject.toml
------

 

ECR Push

Elastic Contaienr Registry(ECR) 이란?
Docker hub와 같이 AWS에서 지원하는 애플리케이션 이미지 및 아티팩트 배포를 위한 컨테이너 레지스트리 입니다.
간단하게, 도커 이미지 저장소인거죠-

 

ECR 서비스 > 리포지토리 생성

이름만 설정해서 생성해줍니다.

저는 서비스명/backend 형식으로 해줬습니다.

 

이제 생성한 리포지토리에 1번에서 빌드한 이미지를 push 해줄게요.

1. aws 인증

IAM 계정으로 발급받은

Access Key ID/ Scret Access Key 와 작업할 region을 입력해주세요

(저는 저렴한 오래곤에서 했습니다. us-west-2)

(base) ➜  ~ which aws
/usr/local/bin/aws
(base) ➜  ~ aws configure
AWS Access Key ID []: 액세스 키 ID
AWS Secret Access Key []: 액세스 키
Default region name []: region
Default output format [None]:

2. 이미지 PUSH

이제 aws ecr 서비스에 로그인을 해줍니다.

+ <ECR URI> 는 <Account>.dkr.ecr.us-west-2.amazonaws.com/llm-economy/backend 형식입니다.

(base) ➜  ~ aws ecr get-login-password --region us-west-2 \
| docker login --username AWS \
  --password-stdin <ECR URI>
Login Succeeded

 

로그인에 성공하면 로컬에 빌드한 이미지를 조회한 뒤,

태그를 달아주고 PUSH 해주면 됩니다.

(base) ➜  ~ docker images
REPOSITORY                             TAG           IMAGE ID       CREATED          SIZE
llm-economy-summary                    latest        93ae8dbf421c   55 minutes ago   208MB
(base) ➜  ~ docker tag llm-economy-summary <ECR URI>
(base) ➜  ~ docker push <ECR URI>
Using default tag: latest
The push refers to repository [<ECR URI>]
5a7d7c3a6a8e: Pushed
58ab4c0bf0cf: Pushed
ecf8d02cefcf: Pushed
0b6e3d9b19ad: Pushed
571f0ddaefd7: Pushed
6658a93512ed: Pushed
a213e6585473: Pushed
latest: digest: sha256:3db19b8453dba15c00be817d4792b66a184e4e13ac6b4dbd4c6cc1bb1a865824 size: 1787
(base) ➜  ~

 

레포지토리에 이미지가 들어간 걸 확인할 수 있어요.

 

https://junior-datalist.tistory.com/379 

 

[AWS] ECR 이미지 푸시

기존 NCloud 플랫폼에서 운영중인 서버를 AWS 로 이관하게 됐다. 단일 compute server 에서 운영중인 서버를 이번 기회에 AWS ECS 를 통해 배포하려고 한다. 마이그레이션 과정을 기록해본다. 우선 ECR에 Pr

junior-datalist.tistory.com

 

 

ECS Fargate 배포

이제 이미지를 컨테이너로 배포하고 호스팅하는 단계입니다.

Elastic Container Service(ECS)란?
AWS의 컨테이너 오케스트레이션 서비스입니다.

ECS vs EKS
우선 ECS는 AWS 내부에서 설계된 서비스로 설정이 간소화되어 러닝커브가 낮은 서비스 입니다.
또한 스케일링 속도에 강점이 있다고 하네요.
그래서 저처럼 소규모 서비스에 빠르게 도입하고 AWS 서비스들과의 친화성을 고려하면 ECS가 적합하다고 생각됩니다.

EKS는 쿠버네티스(Kubernetes) 서비스로 기존 쿠버네티스를 AWS가 Control plane을 직접 관리해 관리 포인트를 줄일 수 있습니다.
또한 ECS보다 확장성과 유연성 그리고 이식성이 강점이나 그만큼 직접 설정해야할 요소가 많다고 느껴집니다.
(비용도 훨씬 비싸고요..)

그래서 서비스가 MVP수준을 넘어서고 확장되면 EKS로 마이그레이션 해보려합니다.

 

ECS는 Task / Task Definition / Service / Cluster 로 구성됩니다.

  • Task : 컨테이너를 실행하는 최소 단위
  • Task Definition
    : Task Definition은 설계도, Task는 인스턴스 (like Docker image-container)
  • Service
    : Task의 배포를 관리하는 주체 (Service : Task Defintion = 1 : 1)
  • Cluster
    : 여러 Cluster instance가 실행될 수 있는 가상의 공간,
    같은 Cluster 내부의 태스크와 서비스는 같은 네트워킹 정보와 인프라 설정 (fargate or EC2)를 가지게 됩니다.

1. Cluster 생성

클러스터 이름 입력후 Fargate 전용으로 생성해줍니다.


2. 태스크 정의 (FastAPI)

저는 JSON으로 생성해줬습니다.

 

그 전에 DB password를 어디에 저장할까 고민하다가,

AWS의 Secret Manager 서비스에 저장하고 Task 정의에서 넣어줬습니다.

 

다른 필요 변수들은 환경변수에 넣어주고,

Secret Manager 변수는 secrets에 다음과 같이 넣어줍니다.

다만, valueFrom에는 <ARN>:<key>:: 형식으로 넣어줘야합니다.

 

그리고 DB 인스턴스의 보안그룹 / 인바운드규칙에 해당 task의 보안그룹으로 연결해주고,
db_host에 프라이빗 IP를 넣어주면 됩니다.

"secrets": [
    {
        "name": "db_password",
        "valueFrom": "<ARN>:db_password::"
    }
],
{
    "family": "llm-economy-task", # task 이름
    "containerDefinitions": [
        {
            "cpu": 0,
            # 환경 변수
            "environment": [
                {
                    "name": "APP_ENV",
                    "value": "development"
                },
                {
                    "name": "db_port",
                    "value": "5432"
                },
                {
                    "name": "db_host",
                    "value": ""
                },
                {
                    "name": "db_database_name",
                    "value": ""
                },
                {
                    "name": "db_user",
                    "value": ""
                }
            ],
            "essential": true,
            "image": "<ECR URI>:latest",
            # Cloud watch 그룹 연결
            "logConfiguration": {
                "logDriver": "awslogs",
                "options": {
                    "awslogs-group": "/ecs/llm-economy",
                    "awslogs-region": "us-west-2",
                    "awslogs-stream-prefix": "fastapi"
                }
            },
            "mountPoints": [],
            "name": "fastapi",
            # fastapi 포트 연결
            "portMappings": [
                {
                    "containerPort": 8000,
                    "hostPort": 8000,
                    "protocol": "tcp"
                }
            ],
            # DB password 관리
            "secrets": [
                {
                    "name": "db_password",
                    "valueFrom": "<ARN>:db_password::"
                }
            ],
            "systemControls": [],
            "volumesFrom": []
        }
    ],
    "executionRoleArn": "arn:aws:iam::<Account>:role/service-role/ecsTaskExecutionRole",
    "networkMode": "awsvpc",
    "volumes": [],
    "placementConstraints": [],
    "requiresCompatibilities": [
        "FARGATE"
    ],
    "cpu": "256",
    "memory": "512"
}


3. Service 생성

태스크 정의 패밀리에는 2번에서 생성한 패밀리로 설정하고,
이외에는 기본 설정을 따라가도 됩니다.
 
다만, 호스팅을 위해 로드밸런싱(ALB)을 활성화해줍니다.

+ ALB는 실제 서버 주소는 외부 접속을 막고, Target group으로 연결해주는 Entry Point 역할을 합니다.

[간단한 아키텍쳐]
Client
 ↓
ALB
 ↓
Target Group
  ↓
ECS Service (FastAPI Task)
  ↓
FastAPI Router (/summary, /health, ...)
대상 그룹은 접속해야하는 8000포트로 정의해주고, 서비스의 상태 확인을 위한 경로를 추가해줬습니다.
+ 저는 /health 라우터를 따로 만들어놨지만, docs로도 가능할 것 같아요.

 

이제 서비스 > 구성 및 네트워킹 > 네트워크 구성 > DNS의 링크로 접속 하면 

성공적으로 배포된 걸 확인할 수 있어요-!

 

참고 자료

https://aws.amazon.com/ko/ecr/

https://aws.amazon.com/ko/secrets-manager/

https://junior-datalist.tistory.com/379

 

 

간단 회고

솔직히.. 재밌었어요.
네트워크, 보안그룹, ALB 개념들을 이번에 제대로 터득한 느낌이고
이후에 배포 자동화까지 수행해보니까 어플리케이션 전체 플로우를 익힌 거 같아서 유익했습니다.
다음 포스팅은 CI/CD로 돌아올게요!
반응형