低コストで自作する阿里雲関数計算FC+OSSのイメージホスティング

image

まずは三つだけ:

  1. このイメージホスティングは機能もシンプルで、設定も容易、安全性も十分です。もし足りなくなったら、ソースコードを AI に渡すだけで機能の追加・削除はかんたんにできます。
  2. 本文中の AccessKey、パスワード、ドメイン、バケット名はすべてダミーです。そのまま写さず、自分の値に置き換えてください。
  3. コードは Node.js のシングルファイル(約380行)で、依存は FC のレイヤー(Layer)から注入します。ビジネスコードにはハードコードを一切置かず、設定はすべて環境変数で行います。

ソースコード:https://github.com/ezzty/oss-upload


一、なぜ自前で作るのか

静的ブログには、おおむねどれかのイメージホスティングが必須です。お金を節約したい気持ちもわかりますから、他人の「永久無料」イメージホスティングを使う人も多い。しかし、そんなに偉い善人が永遠に逃げない保証はどこにもありません。しかもデータは他人の手にあり、こちらには何の主導権もない——だからなんとなく心配になります。

いろいろハマった末に、極低成本で自分で運用できるイメージホスティングを作ることにしました。目的はとにかく、安く、容量無制限、そして安全です!

自前でイメージホスティングを作る方法の多くは、自分でサーバーを用意する必要があります。月額数十元のサーバー代がかかるうえ、パッチ当ても自分でやる羽目になる。あるいはローカルのクライアントでしか動かないものだと、パソコンを変えたらアップロードすらできません。結局私は、サーバーレスなクラウドのイメージホスティングを作ることにしました。場所を選ばず、思い立ったときにいつでもアップロードできるように。

阿里雲関数計算 FC + OSS + CDN

FC は従量課金(低トラフィックなら≈0元)+データベース不要+ノーメンテナンスで、デプロイパッケージはわずか 6KB。アップロード頻度が低ければ実質ゼロ円です。

OSS の画像ストレージも従量課金で、数十 GB の画像でも 1 か月で数元程度です。

CDN をかませば、画像の読み込みは飛ぶように速くなります!

個人用のイメージホスティングなので、攻撃される価値もさほどありませんが、多層のセキュリティ設計は入れています:強制 PoW によるブルートフォース対策、マジックナンバー検査による偽装対策、三重のサイズ検査による容量ガード、1 日あたりのクォータによる悪用対策、Token の 2 時間失効による盗用対策。

この方法にも欠点はあります:マルチユーザーはなく、アルバム管理はなく、使用量の統計もない。画像の削除にすら、OSS コンソールに入って手動で削除するしかありません。

でも、小さくて安く、個人で使うには十分です。


二、アーキテクチャとリクエストの流れ

ブラウザ upload.html
   │  ① POST /login   {user, pass}
   │  ← 200 {token}(有効期限 24h)
   │  ② POST /upload  Authorization: Bearer *** + 画像バイナリ
   ▼
阿里雲関数計算 FC(Node.js 18、HTTP トリガー)
   │  認証 / レート制限 / マジックナンバー検査 / サイズ検査
   │  ③ ossClient.put(key, stream)
   ▼
OSS バケット(プライベート読書き、パブリック読み取り禁止)
   │  ④ CDN がオリジンへ(内網、無料)
   ▼
https://img.example.com/m/26j13/ab12cd.jpg   ← 外部アクセス用ドメイン

設計上の判断は三つ:

  • バケットをプライベートに:誰にも直リンクの読み取り権限を与えず、画像には CDN 用ドメインに紐づいた URL 経由でのみアクセスできるようにします。パブリック読み取りにすると、あなたのバケットは公共の資源になり、他人に一覧を取られ、トラフィックを無断で使い放題にされます。
  • アップロードにはログイン必須:そうしないと、あなたの API ドメインが無料のイメージホスティングとして他人に使い放題され、コストもストレージも制御不能になります。
  • ビジネスコードは鍵に触れない:AK/SK は環境変数の中にのみ存在し、コードにもリポジトリにも二度と現れません。

三、準備

3.1 サービスの開通

関数計算 FC と オブジェクトストレージ OSS を開通します(どちらも従量課金、前払いなし)。

3.2 バケットの作成

  • 名前:グローバル一意。例: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 であなたの全データバケットを読めるようになってしまう

四、依存レイヤー(Layer)の準備

ビジネスコードの依存は ali-oss のみ。これを Layer にすれば、コードパッケージは数 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 をアップロード

レイヤーは一度作れば OK です。今後 ali-oss のバージョンを上げるときは、新しいバージョンを再パッケージングして公開し、関数を新しいバージョンに向けるだけ。ビジネスコードは触りません。


五、関数のデプロイ

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
  • ハンドラー: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 を有効にしたときだけ判定に関わります(デフォルトの CORS はプラットフォームが Origin をそのまま返すだけなので、isValidOrigin は根本的に呼ばれません)。コードレベルの CORS を有効にするなら必ず設定を。さもないとフォールバック値の example.com がブラウザからのアップロードを全部遮断します。

image

5.4 カスタムドメインの紐づけ

FC コンソール → ドメイン管理 → ドメインの追加 → 案内に従って CNAME と証明書を設定。こうして初めて https://api.example.com のような API アドレスが持てます。


六、フロントエンドページ

upload.html はシングルファイルのフロントエンドなので、自分のサイトのディレクトリに置くだけです。

必ず直す場所が一つ、_FC_PARTS を探してください:

// あなたの関数計算のカスタムドメインに変更
const _FC_PARTS = ['https://', 'api.example.com'];

サイズ上限はバックエンドと揃えること(デフォルトはどちらも 20MB):

if (file.size > 20 * 1024 * 1024) { /* 遮断 */ }

前後にそれぞれ制限があり、小さい方を基準にします。フロントだけ直してバックエンドを触らないと、大きなファイルはサーバー側で 413 になります。バックエンドだけ直すと、ブラウザ側で先に遮断されてそもそも送信できません。


七、深く知りたいなら、核心はソースの README に

以上が自前イメージホスティングを動かす一連の流れです。コードがなぜこう書かれているのか、各層の防御の実装詳細、完全なチェックリストはすべてソースリポジトリの README にまとめてあるので、必要なときに見比べてください:

https://github.com/ezzty/oss-upload → README.md の「核心実装(コードレベル)」と「リリース前検収」

ブログ本文では「なぜ自前で、どう動かすか、何に気をつけるか」だけを分かりやすく伝えたければ十分。実際にデプロイするなら、README がそのまま完全な技術ドキュメントです。


八、コスト

従量課金で、大きく三つ:

項目 課金方式 目安
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 ヶ月に数十枚アップロードした場合の費用はおよそ 1 元程度でした。


九、保守と更新

ビジネスコードを変えるとき(画像リンク形式など):

  1. ローカルの index.js を修正
  2. zip code.zip index.js package.json
  3. コンソール → コードデプロイ → アップロード → デプロイ
  4. README の検収プローブを実行して、新しいバージョンが効いていることを確認

フロントを変えるとき:

  1. upload.html を修正
  2. サイトへアップロード。Cache-Control: no-cache を設定することを推奨(さもないとユーザーが古い版を受け取る可能性がある)

依存を更新するとき:

  1. ali-oss を再 npm install して layer.zip をパッケージング
  2. コンソールで新しいレイヤーバージョンを公開
  3. 関数を新しいレイヤーバージョンへ切り替え

鍵のローテーション(定期的に):

  1. RAM で新しい AK/SK を生成
  2. FC の環境変数を更新 → デプロイ
  3. サービスが正常に動いていることを確認した後、RAM コンソールで旧 AK を無効化

十、参考