K

스토리지 연결하기

axhub.yaml에 두 줄만 선언하면 앱 전용 S3 호환 보관함이 생겨요. 표준 S3 도구로 파일을 올리고 내려받는 방법이에요.

이미지나 첨부파일 같은 파일은 데이터베이스가 아니라 앱 전용 보관함(버킷)에 저장해요.
선언 두 줄로 보관함을 만들고, 코드에서 파일을 올리고 내려받는 것까지 해요.

이 보관함은 S3라는 널리 쓰이는 방식과 호환돼요. 그래서 @aws-sdk/client-s3 · boto3 같은 표준 S3 도구를 그대로 쓰면 되고, 파일은 앱과 보관함 사이에서 직접 오가서 용량 제한도 없어요.

준비물

  • 서버가 있는 앱 — 화면만 있는 정적 호스팅 앱에는 못 붙여요 (아래 알아두면 좋은 것 참고)
  • GitHub 저장소axhub.yaml

데이터베이스와 달리 템플릿에는 스토리지 코드가 들어 있지 않아요. 아래 선언과 코드를 직접 넣어요.

선언 두 줄

axhub.yaml에 아래를 추가하고 배포하면 끝이에요.

axhub.yaml
storage:
  enabled: true

다음 배포에서 앱 전용 보관함이 만들어지고, 앱이 실행되는 곳에 접근 정보 4개가 환경변수로 자동 주입돼요.

환경변수
STORAGE_ENDPOINT보관함 접속 주소 (S3 호환 엔드포인트)
STORAGE_BUCKET앱 전용 버킷 이름
STORAGE_ACCESS_KEY액세스 키
STORAGE_SECRET_KEY시크릿 키

터미널에서 켜고 확인하려면 이렇게 해요.

axhub apps storage enable --app demo --execute
axhub apps storage status --app demo

버킷은 앱마다 1개이고 production(실제 서비스) · staging(시험용)이 같은 버킷을 함께 써요. 환경별로 파일을 나누고 싶으면 staging/…처럼 파일 이름 앞에 붙이는 키 접두사(prefix) 로 구분하세요.

올리기

주입된 환경변수를 읽어 S3 클라이언트를 만들면 끝이에요.

체크섬 설정(WHEN_REQUIRED)은 필수예요. 최신 S3 SDK가 기본으로 붙이는 체크섬이 이 보관함과 충돌해서, 빼면 업로드가 SignatureDoesNotMatch로 실패해요. 아래 예제에 이미 들어 있어요.

Node — @aws-sdk/client-s3
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';

const s3 = new S3Client({
  endpoint: process.env.STORAGE_ENDPOINT,
  region: 'auto',
  credentials: {
    accessKeyId: process.env.STORAGE_ACCESS_KEY!,
    secretAccessKey: process.env.STORAGE_SECRET_KEY!,
  },
  // 필수 — 빼면 업로드가 SignatureDoesNotMatch 로 실패해요
  requestChecksumCalculation: 'WHEN_REQUIRED',
  responseChecksumValidation: 'WHEN_REQUIRED',
});

await s3.send(new PutObjectCommand({
  Bucket: process.env.STORAGE_BUCKET,
  Key: 'uploads/photo.png',
  Body: buffer,
  ContentType: 'image/png',
}));
Python — boto3
import os, boto3
from botocore.config import Config

s3 = boto3.client(
    "s3",
    endpoint_url=os.environ["STORAGE_ENDPOINT"],
    region_name="auto",
    aws_access_key_id=os.environ["STORAGE_ACCESS_KEY"],
    aws_secret_access_key=os.environ["STORAGE_SECRET_KEY"],
    # 필수 — 빼면 업로드가 SignatureDoesNotMatch 로 실패해요
    config=Config(
        request_checksum_calculation="when_required",
        response_checksum_validation="when_required",
    ),
)

s3.put_object(
    Bucket=os.environ["STORAGE_BUCKET"],
    Key="uploads/photo.png",
    Body=data,
    ContentType="image/png",
)

사용자에게 보여주기

앱 서버가 파일을 직접 내려주지 말고, presigned URL(정해진 시간만 열리는 임시 주소)을 만들어 브라우저에 넘기세요. 파일 바이트는 브라우저와 보관함이 직접 주고받아서 앱 서버에 부담이 없어요.

Node
import { GetObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';

const url = await getSignedUrl(
  s3,
  new GetObjectCommand({ Bucket: process.env.STORAGE_BUCKET, Key: 'uploads/photo.png' }),
  { expiresIn: 900 },  // 15분
);
Python
url = s3.generate_presigned_url(
    "get_object",
    Params={"Bucket": os.environ["STORAGE_BUCKET"], "Key": "uploads/photo.png"},
    ExpiresIn=900,
)

버킷은 비공개라서 이 주소 없이는 외부에서 직접 접근할 수 없어요.

콘솔 · 터미널에서 파일 보기

앱 콘솔 → 리소스 탭 → 스토리지에서 올라간 파일을 폴더처럼 탐색하고, 다운로드 링크(15분 유효)를 받거나 파일을 삭제할 수 있어요. 접속 정보 4개도 여기서 복사해요. 업로드는 콘솔이 아니라 앱 코드에서 해요.

앱 소유자가 열람 정책으로 잠가 둔 앱이라면, 관리자라도 콘솔에서 파일 열람이 제한될 수 있어요.

터미널에서는 이렇게 봐요.

axhub apps storage ls --app demo                          # 파일 목록
axhub apps storage get-url uploads/photo.png --app demo   # 다운로드 링크 발급
axhub apps storage rm uploads/photo.png --app demo --execute

제한 · 에러

항목
앱당 버킷1개 (production · staging 공유)
파일 크기 · 업로드 제한플랫폼 제한 없음 (보관함과 직접 통신)
presigned URL 유효기간15분 (고정)
플랜 스토리지 쿼터좌석제(플랜) 기준 Free 1 GB · Pro 50 GB · Business 200 GB · Enterprise 500 GB — DB 용량과 합산, 초과해도 차단 없음. 종량제는 플랜 쿼터 없이 사용량만큼 과금돼요

자주 만나는 에러예요.

상황응답
체크섬 설정 없이 업로드SignatureDoesNotMatch — 위 WHEN_REQUIRED 설정을 넣으세요
버킷이 만들어지기 전에 파일 API 호출404 — "먼저 배포해 주세요"
화면만 있는 앱(정적 앱)에 켜기 시도409 unsupported_for_static_app
정적 앱이 선언한 채 배포배포 실패 prepare.storage_unsupported_for_static
버킷 만들기(프로비저닝) 실패배포 실패 prepare.storage_provision_failed — 재배포로 재시도

aws CLI(v2.31 이상)로 업로드할 땐 같은 이유로 AWS_REQUEST_CHECKSUM_CALCULATION=when_required 환경변수를 함께 주세요.

알아두면 좋은 것

  • 화면만 있는 앱에는 못 붙여요 — 정적 호스팅 앱은 계속 돌고 있는 서버가 없어서 접근 정보를 넣을 곳이 없어요. 선언한 채 배포하면 배포가 명확히 실패해요.
  • 선언을 지워도 파일은 사라지지 않아요 — 버킷 · 파일 · 접근 정보 모두 유지돼요. 앱을 영구 삭제할 때만 함께 삭제돼요. 일시정지 · 보관 처리해도 파일은 보존돼요.
  • 이미 떠 있는 앱에는 소급 주입되지 않아요 — 켠 뒤 첫 배포부터 환경변수가 들어가요.
  • STORAGE_*는 직접 덮어쓸 수 없어요 — 같은 이름으로 저장한 환경변수는 무시돼요.
  • 격리는 클라우드 IAM(접근 권한 관리)으로 강제돼요 — 앱의 키는 자기 버킷에만 동작하고, 버킷은 공개 접근이 차단돼 presigned URL 없이는 외부에서 읽을 수 없어요.
  • 켠 직후 첫 쓰기는 드물게 한 번 실패할 수 있어요(권한이 퍼지는 데 수십 초 걸려요) — 재시도하면 돼요.

이렇게 보이면 성공

선언 후 배포가 끝나고, 앱 코드로 올린 파일이 앱 콘솔 → 리소스 탭 → 스토리지에 나타나면 성공이에요.

다음은 SSO 사용자 정보 읽기에서 지금 접속한 사람이 누구인지 앱에서 읽어요.