저비용으로 알리윤 함수 컴퓨팅 FC + OSS 이미지 호스팅 직접 구축하기

image

먼저 세 가지만 짚고 갈게요:

  1. 이 이미지 호스팅은 기능이 단순하고 설정이 쉬우면서도 충분히 안전해요. 뭔가 부족하면 소스를 AI한테 던져주면 기능 추가/삭제는 식은 죽 먹기예요.
  2. 이 글에 나오는 모든 AccessKey, 비밀번호, 도메인, Bucket 이름은 전부 자리표시자예요. 그대로 복사하기 전에 꼭 본인 걸로 바꿔 주세요.
  3. 코드는 단일 파일 Node.js(약 380줄)이고, 의존성은 FC 레이어(Layer)로 주입해요. 비즈니스 코드에 하드코딩된 건 하나도 없고, 설정은 전부 환경 변수로 처리해요.

소스: https://github.com/ezzty/oss-upload


1. 왜 직접 이미지 호스팅을 만드나

정적 블로그라면 거의 다 이미지 호스팅이 필요하죠. 비용 아끼려고 남들이 운영하는 영구 무료 이미지 호스팅을 쓰는 분들도 많은데, 과연 어느 대인이 영원히 서비스를 유지해 줄까요? 게다가 데이터가 남의 손에 있으면 내가 주도권이 없어서 불안하기 마련이에요.

여러 가지 시행착오를 겪고 나서 아주 저렴한 이미지 호스팅을 직접 만들기로 했어요. 목표는 돈 아끼기, 용량 제한 없애기, 그리고 안전이에요!

직접 이미지 호스팅을 만드는 방법은 대부분 서버를 따로 두어야 하고, 월 몇만 원에 패치까지 직접 해야 하죠. 아니면 로컬 클라이언트에서만 돌아가서 컴퓨터만 바꿔도 업로드가 안 되는 것도 있고요. 그래서 결국 Serverless 클라우드 이미지 호스팅으로 정했어요. 언제 어디서든 올리고 싶을 때 올리면 되니까요.

알리윤 함수 컴퓨팅 FC + OSS + CDN

FC는 사용한 만큼만 과금(트래픽 적으면 거의 0원) + 데이터베이스 없음 + 운영 부담 없음, 배포 패키지는 6KB까지 줄었어요. 업로드 빈도가 낮으면 거의 지출이 없어요.

OSS 이미지 저장도 사용한 만큼만 과금이라, 수십 GB 이미지를 한 달에 보관해도 몇 천 원 수준이에요.

여기에 CDN까지 붙이면 이미지 로딩이 날아갈 듯 빨라져요!

개인 이미지 호스팅이라 공격 가치가 크진 않지만, 그래도 여러 겹의 보안 설계를 넣었어요. PoW 강제로 무차별 대입 방지, 매직 넘버 검증으로 위장 파일 차단, 세 겹 용량 제한으로 대량 업로드 방지, 일일 할당량으로 남용 방지, 토큰 2시간 유효로 탈취 방지까지요.

물론 이 방식에 단점이 없는 건 아니에요. 다중 사용자 없음, 앨범 관리 없음, 사용량 통계 없음. 심지어 이미지 삭제도 OSS 콘솔에 들어가서 지워야 해요.

그래도 작잖아요. 비용도 낮고, 개인용으로는 충분해요.


2. 아키텍처와 요청 흐름

브라우저 upload.html
   │  ① POST /login   {user, pass}
   │  ← 200 {token}(유효기간 24h)
   │  ② POST /upload  Authorization: Bearer <token> + 이미지 바이너리
   ▼
알리윤 함수 컴퓨팅 FC(Node.js 18,HTTP 트리거)
   │  인증 / 속도 제한 / 매직 넘버 검증 / 용량 검증
   │  ③ ossClient.put(key, stream)
   ▼
OSS Bucket(비공개 읽기/쓰기,공개 읽기 금지)
   │  ④ CDN 오리진 요청(내부망,무료)
   ▼
https://img.example.com/m/26j13/ab12cd.jpg   ← 외부 접근 도메인

세 가지 설계 선택:

  • Bucket은 비공개로: 누구에게도 직링크 읽기 권한을 주지 않고, 이미지는 CDN 도메인이 붙은 URL로만 접근하게 해요. 공개 읽기로 열어버리면 내 버킷이 공공재가 되어서 남들이 목록을 훑어보고 트래픽을 무임승차할 수 있어요.
  • 로그인해야만 업로드 가능: 안 그러면 내 API 도메인이 무료 이미지 호스팅처럼 남용당해서 비용과 저장 공간이 순식간에 통제 불능이 돼요.
  • 비즈니스 코드는 키를 건드리지 않음: AK/SK는 환경 변수에만 존재하고, 코드와 저장소에서는 절대 볼 수 없어요.

3. 준비 작업

3.1 제품 개통

함수 컴퓨팅 FC와 오브젝트 스토리지 OSS를 개통해요(둘 다 사용한 만큼 과금, 선구매 없음).

3.2 Bucket 생성

  • 이름: 전역 고유, 예를 들어 my-img
  • 리전: 나 또는 내 사용자와 가까운 곳, 예를 들어 화둥1(항저우) → oss-cn-hangzhou
  • 읽기/쓰기 권한: 비공개(중요)
  • 저장 유형: 표준 저장(자주 접근하는 이미지에는 이걸 써야 해요. 저빈도/아카이브는 최소 보관 기간이 있어서 일찍 지우면 오히려 돈이 나가요)

3.3 RAM 사용자 생성 및 단일 버킷으로 권한 제한(강력 권장)

많은 튜토리얼이 그냥 메인 계정 AccessKey를 쓰라고 하는데, 그러지 마세요. 메인 계정 AK가 유출되면 알리윤 계정 전체가 뚫리는 거예요.

RAM 콘솔 → 권한 정책 → 정책 생성 → JSON 탭:

{
  "Version": "1",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "oss:GetObject",
        "oss:PutObject",
        "oss:DeleteObject",
        "oss:ListObjects",
        "oss:HeadObject"
      ],
      "Resource": [
        "acs:oss:*:*:my-img",
        "acs:oss:*:*:my-img/*"
      ]
    }
  ]
}

정책 이름은 ImageHostSingleBucket. 그다음:

  1. RAM → 사용자 → 사용자 생성, “영구 AccessKey로 접근” 체크
  2. 이 사용자에게 위 정책 하나만 연결
  3. AK/SK 생성 후 즉시 로컬 비밀번호 관리자에 저장

몇 가지 의도적인 선택:

  • oss:ListBuckets는 주지 않음 — 유출돼도 내가 어떤 버킷을 가졌는지조차 못 봐요
  • oss:CopyObject는 주지 않음 — 이미지 호스팅에 복사 권한은 필요 없으니 줄 수 있는 만큼만 줄여요
  • AliyunOSSFullAccess는 연결하지 마세요 — 그러면 이미지 호스팅 AK가 내 모든 데이터 버킷을 읽을 수 있게 돼요

4. 의존성 레이어(Layer) 준비

비즈니스 코드는 ali-oss에만 의존해요. 이걸 레이어로 만들면 코드 패키지를 몇 KB로 유지할 수 있어요.

방법 A: Release 첨부 파일 직접 다운로드(추천)

프로젝트에 Release가 올라가 있어서 layer.zip과 코드 패키지를 바로 받을 수 있어요:

https://github.com/ezzty/oss-upload/releases/latest

첨부 파일 크기 내용
layer.zip 3.25 MB ali-oss@6.23.0 및 의존성, 총 2338개 파일
oss-upload-code.zip 약 6 KB index.js + package.json, 함수 배포 패키지

장점: Node.js 설치도, zip 만들기도 필요 없고, 로컬에서 npm install로 더 최신 SDK를 받아서 “작성자가 검증한 것과 다르다”는 문제도 생기지 않아요. 레이어 안에는 npm 패키지 소스만 있고 키는 없어요.

방법 B: 직접 빌드

의존성 버전을 바꾸고 싶거나 기성 패키지를 신뢰하지 않을 때 직접 만들어요:

mkdir -p layer-build/nodejs
cd layer-build
npm init -y
npm install ali-oss
# 디렉터리는 반드시 이 구조여야 해요:
#   nodejs/
#     node_modules/
#       ali-oss/
#       ...그것의 전이 의존성
cd nodejs && zip -r ../layer.zip node_modules && cd ..

업로드

FC 콘솔 → 레이어 관리 → 레이어 생성:

  • 이름: oss-upload-deps
  • 런타임: Node.js 18
  • layer.zip 업로드

레이어는 한 번만 만들면 돼요. 나중에 ali-oss 버전을 올릴 때 새 버전을 만들어 배포하고 함수가 새 버전을 가리키게만 하면 되고, 비즈니스 코드는 안 바꿔도 돼요.


5. 함수 배포

5.1 비즈니스 코드 받기

Release 첨부 파일을 바로 쓰는 걸 추천해요: https://github.com/ezzty/oss-upload/releases/latest 에서 oss-upload-code.zip(약 6 KB)을 받아 압축을 풀면 표준 배포 패키지가 나와요.

직접 패키징한다면 zip 루트에는 이 두 파일만 있어야 해요(폴더로 감싸면 Handler가 index.handler를 못 찾아요):

code.zip
├── index.js        ← 비즈니스 소스
└── package.json    ← 의존성 선언(참고용, 실제 런타임에서는 레이어에서 로드)
zip code.zip index.js package.json

5.2 함수 생성

  • 런타임: Node.js 18
  • 처리 프로그램(Handler): index.handler
  • 사양: 0.05 vCPU / 128 MB 메모리 / 512 MB 디스크(실측으로 충분한 최소 사양)
  • 레이어 설정: 방금 만든 oss-upload-deps 연결

메모리는 좀 신경 써야 해요. 코드가 body 전체를 메모리로 읽고(bodyBuf), base64 디코딩할 때 복사본이 하나 더 생겨요. 128 MB면 일반 이미지는 문제없지만, 업로드 상한을 20 MB로 올리고 상한에 가까운 큰 이미지를 자주 올린다면 메모리도 같이 올려야 해요.

5.3 환경 변수(필수 8개, 하나라도 빠지면 안 뜸)

함수 설정의 환경 변수는 필수 8개이고, 이미지에 있는 대로 설정하면 돼요.

  • AUTH_USER admin —— 로그인 사용자 이름
  • AUTH_PASS <강력한 비밀번호 설정> —— 로그인 비밀번호, 무작위 긴 문자열로
  • JWT_SECRET <32자 이상 무작위 문자열> —— 토큰 서명 키
  • OSS_ACCESS_KEY_ID LTAIxxxxxxxxxxxxxxxx —— RAM 사용자의 AK
  • OSS_ACCESS_KEY_SECRET <당신의 AccessKey Secret> —— RAM 사용자의 SK
  • OSS_BUCKET my-img —— 버킷 이름
  • OSS_REGION oss-cn-hangzhou —— 버킷 소재 리전 ID
  • CDN_DOMAIN https://img.example.com —— 프로토콜 헤더 필수, 없으면 시작하자마자 실패해요

선택 1개:

  • ALLOWED_ORIGINS example.com,myblog.net —— 허용할 교차 출처, 쉼표 구분. 설정 안 해도 시작은 되고, 코드 레벨 CORS를 켰을 때만 판단에 참여해요(기본 CORS는 플랫폼이 Origin을 그대로 돌려주기 때문에 isValidOrigin이 아예 호출되지 않아요). 코드 레벨 CORS를 켜면 반드시 설정해야 하고, 안 그러면 기본값 example.com이 브라우저 업로드를 전부 막아버려요.

image

5.4 커스텀 도메인 연결

FC 콘솔 → 도메인 관리 → 도메인 추가 → 안내에 따라 CNAME과 인증서를 설정해요. 그래야 https://api.example.com 같은 API 주소가 생겨요.


6. 프론트엔드 페이지

upload.html은 단일 파일 프론트엔드라서 내 웹사이트 디렉터리에 그냥 던져 넣으면 돼요.

한 군데는 반드시 고쳐야 해요, _FC_PARTS를 찾아서:

// 함수 컴퓨팅 커스텀 도메인으로 변경
const _FC_PARTS = ['https://', 'api.example.com'];

용량 상한은 백엔드와 맞춰야 해요(기본은 둘 다 20MB):

if (file.size > 20 * 1024 * 1024) { /* 차단 */ }

프론트와 백엔드에 각각 제한이 있고, 작은 쪽이 기준이 돼요. 프론트만 고치고 백엔드를 안 고치면 큰 파일은 서버에서 413으로 튕기고, 백엔드만 고치고 프론트를 안 고치면 사용자가 브라우저에서 막혀서 아예 못 보내요.


7. 더 깊이 파고 싶다면? 핵심 디테일은 소스 README에

지금까지가 이미지 호스팅을 직접 구축해서 돌리는 전체 흐름이었어요. 코드를 왜 이렇게 썼는지, 각 방어 계층의 구현 디테일, 전체 검수 체크리스트는 소스 저장소 README에 정리해 뒀으니 필요할 때 보면 돼요:

https://github.com/ezzty/oss-upload → README.md의 「핵심 구현(코드 레벨)」과 「출시 전 검수」

블로그 본문에서는 「왜 직접 만드는지, 어떻게 돌리는지, 뭘 주의해야 하는지」만 명확히 다루고 싶었어요. 실제로 배포할 때는 README가 완전한 기술 문서예요.


8. 비용

사용한 만큼 과금, 세 가지:

항목 과금 방식 규모 참고
FC 컴퓨팅 컴퓨팅 CU(vCPU+메모리×시간) + 호출 횟수 무료 할당량 있음, 개인 이미지 호스팅은 보통 할당량 근처
OSS 저장 위안/GB/월 표준 저장 약 0.09~0.12 위안/GB/월
OSS 요청 건당 매우 낮음, 무시 가능
CDN 트래픽 위안/GB 아래 참고

트래픽이 청구서의 큰 부분이고, 가장 최적화하기 쉬운 부분이에요:

  • OSS로 업로드: 무료
  • OSS 내부망(FC ↔ OSS 같은 리전): 무료
  • OSS 외부망 유출: 유료(약 0.5 위안/GB 수준, 시간대와 리전에 따라 다름)
  • CDN 경유: CDN 오리진 요청은 내부망이라 오리진 트래픽 무료, CDN 트래픽만 과금

결론: 이미지 호스팅에는 반드시 CDN을 붙이세요. 속도도 빨라지고 OSS 외부망 유출 비용도 피할 수 있어요. 그리고 FC와 OSS는 반드시 같은 리전이어야 해요. 리전이 다르면 공인망을 타서 느려지고 추가 트래픽 비용이 생길 수 있어요.

블로거 실측으로, 한 달에 이미지 수십 장을 올린다면 한 달 비용은 대략 1위안 정도예요.


9. 유지보수 및 업데이트

비즈니스 코드 수정(예: 이미지 링크 형식):

  1. 로컬 index.js 수정
  2. zip code.zip index.js package.json
  3. 콘솔 → 코드 배포 → 업로드 → 배포
  4. README의 검수 프로브를 돌려서 새 버전이 적용됐는지 확인

프론트엔드 수정:

  1. upload.html 수정
  2. 웹사이트에 업로드, Cache-Control: no-cache 설정 권장, 안 그러면 사용자가 구버전을 받을 수 있어요

의존성 업그레이드:

  1. 다시 npm install ali-oss 후 layer.zip 패키징
  2. 콘솔에서 새 레이어 버전 배포
  3. 함수를 새 레이어 버전으로 전환

키 교체(주기적으로):

  1. RAM에서 새 AK/SK 생성
  2. FC 환경 변수 업데이트 → 배포
  3. 서비스가 정상인 걸 확인한 후 RAM 콘솔에서 이전 AK 비활성화

10. 참고