스토리지 연결하기
axhub.yaml에 두 줄만 선언하면 앱 전용 S3 호환 보관함이 생겨요. 표준 S3 도구로 파일을 올리고 내려받는 방법이에요.
이미지나 첨부파일 같은 파일은 데이터베이스가 아니라 앱 전용 보관함(버킷)에 저장해요.
선언 두 줄로 보관함을 만들고, 코드에서 파일을 올리고 내려받는 것까지 해요.
이 보관함은 S3라는 널리 쓰이는 방식과 호환돼요. 그래서 @aws-sdk/client-s3 · boto3 같은 표준 S3 도구를 그대로 쓰면 되고, 파일은 앱과 보관함 사이에서 직접 오가서 용량 제한도 없어요.
준비물
- 서버가 있는 앱 — 화면만 있는 정적 호스팅 앱에는 못 붙여요 (아래 알아두면 좋은 것 참고)
- GitHub 저장소의
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로 실패해요. 아래 예제에 이미 들어 있어요.
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',
}));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(정해진 시간만 열리는 임시 주소)을 만들어 브라우저에 넘기세요. 파일 바이트는 브라우저와 보관함이 직접 주고받아서 앱 서버에 부담이 없어요.
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분
);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 사용자 정보 읽기에서 지금 접속한 사람이 누구인지 앱에서 읽어요.