🌀Full-Stack&Beyond

AWS 서비스를 On-Premise 환경으로 마이그레이션하기

해서미 2025. 2. 22. 23:42

 

기존에 aws에서 동작하던 서비스를 온프레미스로 구축해야할 일이 생겼다. 클라우드 서비스를 사용하지 못하는 기업 환경에서도 서비스를 제공하기 위해, AWS 기반의 서비스를 On-Premise 환경으로 마이그레이션을 하게 되었다.
특이사항은 외부 네트워크 접근이 차단되어있어서 필요한 패키지를 미리 준비해두어야 했다. 
 

참고) 모든 예시 코드는 블로그 포스팅용으로 재구성되었습니다.

 

1. 프로젝트 개요

기존 서비스는 다음과 같은 AWS 서비스들을 활용하고 있었다.

  • 프론트엔드
  • 백엔드
  • S3: 파일 저장소
  • CloudFront: CDN 및 이미지 서빙
  • Lambda server: 마이크로 서버
  • RDS: 데이터베이스
  • CloudWatch: 로깅

이를 On-Premise 환경에서 구현하기 위해 Docker와 Nginx를 활용한 마이그레이션을 진행했다.

 

아키텍쳐 설계

2.1 Docker Compose 기반 마이크로서비스 구성

version: "3.8"

services:
  my-nginx:
    build: ./nginx
    container_name: my-nginx
    restart: always
    depends_on:
      - frontend
      - backend
      - micro-server
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/certs:/etc/nginx/certs
      - ./nginx/logs:/var/log/nginx
      - /data:/data
    networks:
      - app_network

  frontend:
    build: ./frontend
    container_name: frontend
    restart: always
    depends_on:
      - backend
    ports:
      - "3000:3000"
    networks:
      - app_network

  backend:
    build: ./backend
    container_name: backend
    restart: always
    volumes:
      - /data:/data 
    env_file:
      - .env
    environment:
      - DATABASE_URL=비밀
    ports:
      - "8000:8000"
    depends_on:
      - database
    networks:
      - app_network
  
  database:
    image: my-mysql
    container_name: database
    restart: always
    environment:
      비밀비밀
    ports:
      - "3306:3306"
    volumes:
      - mysql_data:/var/lib/mysql
    networks:
      - app_network
  
  micro-server:
    build: ./micro-server
    container_name: micro-server
    restart: always
    ports:
      - "3001:3001"
    networks:
      - app_network
    volumes:
      - /data:/data 
    env_file:
      - .env

networks:
  app_network:
    driver: bridge

volumes:
  mysql_data:

 

 

다음과 같이 4개의 주요 서비스를 컨테이너로 구성했다.

  1. Nginx
  2. Frontend (Next.js)
  3. Backend (FastAPI)
  4. Micro Server (Node.js)
  5. Database (MySQL) - 이건 빌드시 테스트용으로 만든 컨테이너. 실제 온프레미스 환경에서 데이터베이스는 따로 구축했다. 

 

2.2 Nginx를 활용한 서비스 라우팅 구조

1. 외부 nginx (리버스 프록시)
http {

    upstream backend {
        server backend:80;  # FastAPI 컨테이너
    }

    upstream frontend {
        server frontend:3000;  # Next.js 컨테이너
    }

    upstream micro-server {
        server micro-server:80;  # Node.js 컨테이너
    }

    server {
        listen 80;

        # Next.js (프론트엔드)
        location / {
            proxy_pass http://frontend;
        }

         # ✅ FastAPI (백엔드)
        location /api/ {
            proxy_pass http://backend;  # ✅ FastAPI 컨테이너로 프록시
        }

        # Micro Server (Node.js)
        location /micro-server/ {
            proxy_pass http://micro-server/;
        }

        location /data/ {
            alias /data/;
            autoindex off;
            add_header Cache-Control "public, max-age=3600";
            try_files $uri =404;
            
            # 이미지 파일에 대한 설정
            location ~* \.(jpg|jpeg|png|gif|ico|webp)$ {
                expires 1d;
                add_header Cache-Control "public, no-transform";
            }
        }
    }
}

 

2. 내부 nginx (서비스별)
 
    A[외부 요청] --> B[외부 Nginx :80]
                               B                  -->     C[Frontend :3000]
                               B                  -->     D[내부 Nginx :80 백엔드]        -->   E[FastAPI :8000]
                               B                  -->     F[내부 Nginx :80 micro-server]  -->   G[Node.js :3001]
 
  • Frontend (Next.js)
    • SSR 애플리케이션으로 직접 3000 포트 노출
    • Next 자체로 프로덕션 서버로서 기능을 포함해 내부 Nginx 불필요
  • Backend (FastAPI)
    • 내부 Nginx가 8000 포트의 FastAPI 서버로 프록시
    • 보안 및 로드밸런싱 설정 가능
    • 프로세스 관리로 supervisor 사용
  • Micro Server (Node.js)
    • 내부 Nginx가 3001 포트의 Node.js 서버로 프록시
    • 프로세스 관리로 supervisor 사용
 
 

요청 흐름 예시:

  • API 요청 (/api)
    • 클라이언트 → 외부 Nginx(:80) → Backend 컨테이너(:80) → 내부 Nginx → FastAPI(:8000)
  • micro server 요청 (/micro-server)
    • 클라이언트 → 외부 Nginx(:80) → micro server 컨테이너(:80) → 내부 Nginx → Node.js(:3001)
  • 웹페이지 요청 (/)
    • 클라이언트 → 외부 Nginx(:80) → Frontend 컨테이너(:3000) → Next.js SSR

 

 
 
이렇게  보안, 성능, 확장성을 고려하여 외부 Nginx(리버스 프록시)와 내부 Nginx(서비스별 로드밸런싱)를 분리한 아키텍처를 구성했다. 
 
 

3. AWS 서비스 대체

3.1 파일 시스템 (S3/CloudFront 대체)

AWS S3와 CloudFront를 대체하기 위해 Nginx와 로컬 파일시스템을 활용한 구조를 설계해 온프레미스 환경에서도 동일한 파일 저장 및 서빙 기능을 제공한다. 

 

Nginx 설정을 통한 정적 파일 서빙

http {
    # ... 기존 설정 ...

    server {
        listen 80;

        # 정적 파일 서빙을 위한 location 설정
        location /data/ {
            alias /data/;
            autoindex off;
            add_header Cache-Control "public, max-age=3600";
            try_files $uri =404;
            
            # 이미지 파일에 대한 캐싱 설정
            location ~* \.(jpg|jpeg|png|gif|ico|webp)$ {
                expires 1d;
                add_header Cache-Control "public, no-transform";
            }
        }
    }
}

 

Nginx의 정적 파일 서빙 기능을 활용하여 로컬 파일시스템에 저장된 파일들을 HTTP를 통해 제공한다. /data 디렉토리의 파일들을 웹에서 접근 가능하도록 설정하며, 이는 CloudFront의 CDN 기능을 대체하는 역할을 한다. 

또한 이미지 서빙 시 URL에 불필요한 쿼리 파라미터가 포함되는 경우가 있어서 try_files $uri =404 를 사용해 동일한 이미지에 대해 다른 쿼리 파라미터가 붙더라도 하나의 캐시로 처리했다.

 

Docker 볼륨 마운트

services:
  my-nginx:
    volumes:
      - /data:/data    # 파일 저장소를 nginx 컨테이너에 마운트
    
  backend:
    volumes:
      - /data:/data

파일 저장소를 nginx와 백엔드 서비스가 공유할 수 있도록 Docker 볼륨을 구성해준다. 

 

파일 업로드 및 접근 로직

async def upload_file(file: UploadFile, file_path: str) -> dict:
    try:
        content = await file.read()

        if STAGE == 'on-premise':
            # URL 형태의 경로를 파일 시스템 경로로 변환
            if file_path.startswith(IMAGE_DOMAIN_URL):
                file_path = file_path.replace(IMAGE_DOMAIN_URL, '')
            
            # 온프레미스 환경: 로컬 파일시스템에 저장
            DATA_DIR = os.environ.get('FILE_DIR')  # /data 디렉토리 경로
            full_path = os.path.join(DATA_DIR, file_path)
            os.makedirs(os.path.dirname(full_path), exist_ok=True)
            
            with open(full_path, 'wb') as f:
                f.write(content)

            # nginx를 통한 파일 접근 URL 생성
            file_url = f"{IMAGE_DOMAIN_URL}/{file_path}"
        else:
            # AWS 환경: S3에 업로드 후 CloudFront URL 반환
            s3_client.put_object(
                Bucket=aws_bucket_name,
                Key=file_path,
                Body=content
            )
            file_url = f"{CLOUDFRONT_URL}/{file_path}"

        return {
            "filename": file.filename,
            "location": file_url  # 브라우저에서 접근 가능한 URL 반환
        }

 

파일 업로드 로직에서는 환경(AWS/온프레미스)에 따라 저장소를 분기처리하면서도, 일관된 URL 구조를 반환하도록 구현했다.

온프레미스 환경에서는 IMAGE_DOMAIN_URL을 기반으로 한 파일 경로를 생성하고, AWS 환경에서는 CloudFront URL을 사용한다. 이를 통해 프론트엔드에서는 환경과 관계없이 동일한 방식으로 파일에 접근할 수 있다.

예를 들어 동일한 이미지 파일이

와 같이 서로 다른 환경에서도 일관된 구조로 접근 가능하게 했다.

 

3.2 Micro Server (Lambda 대체)

// Lambda 핸들러를 Express 엔드포인트로 변환
app.post('/micro-server', async (req, res) => {
    try {
        const result = await handler(req.body);
        
        if (result.statusCode === 200) {
            res.setHeader('Content-Type', result.headers['Content-Type']);
            // 로직로직
            res.send(결과);
        }
    } catch (error) {
        res.status(500).json({ error: 'Internal server error' });
    }
});

 

AWS Lambda로 구현되어 있던 기능을 독립적인 Node.js 서비스로 마이그레이션했다. Docker 컨테이너로 실행되며, REST API 엔드포인트를 통해 기존 Lambda 함수와 동일한 기능을 제공한다.

 

 

4. 배포 프로세스

4.1 빌드 프로세스 (온프레미스 환경으로 가져가기 전, 도커 이미지 빌드 과정)

빌드 과정

  1. 도커 rpm 설치 파일 다운로드
  2. 각 서비스(Frontend, Backend, Micro Server)를 Docker 이미지로 빌드
  3. 온프레미스 환경 배포를 위해 이미지를 tar 파일로 저장 (외부 네트워크 접근 차단된 환경)

이렇게 스크립트화 해서 한번에 배포에 필요한 파일들을 만들게 해두었다. 

 

4.2 배포 자동화

1. 데이터베이스 배포 스크립트

미리 저장된 docker rpm 패키지로 도커 설치
Docker 서비스 활성화
Docker 그룹 생성 및 사용자 권한 설정
.tar파일로부터 도커 이미지 로드
MySQL 컨테이너 실행

 

2. 어플리케이션 서비스 배포 스크립트

미리 저장된 docker rpm 패키지로 도커 설치
Docker 서비스 활성화
Docker 그룹 생성 및 사용자 권한 설정
docker compose 설정
.tar파일로부터 도커 이미지 로드
docker compose를 통해 서비스 시작

 

대강 이런식으로 온프레미스 각 서버에서 스크립트를 실행하면 환경설정과 서비스가 실행되도록 스크립트를 구성했다. 

 

 

마무리아무말

끝난것같지만 아직 끝이 아니다. 크롬익스텐션, electron 윈도우 어플리케이션도 이 환경에서 되게 테스트해봐야함. 

로깅도 수정해야함. 테스트도 더 해봐야함

이러다 글 또 평생 못올릴까봐 생각난김에 일단 된거 올려버리기