asset-canister について
Internet Computer 上で Web フロントエンドを公開するためのアセットキャニスター設定・アップロードガイドです。SPA のルーティング、認証付きレスポンス、権限、デプロイ時の注意点を説明します。
主な機能
- 01HTML、CSS、JavaScript、画像のホスティング
- 02サブネットによるコンテンツ認証
- 03SPA フォールバックとヘッダーの設定
- 04icp または SDK によるデプロイ
- 05ロール別のアップロード権限管理
- 06圧縮とカスタムドメインの設定
活用シーン
- 01Internet Computer への Web フロントエンド公開
- 02SPA の深いリンクの再読み込み設定
- 03デプロイ工程への最小限のアップロード権限付与
- 04アセットパスやコンテンツタイプの問題解決
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dfinity/ icskills --skill asset-canisterインストール後、Claude Code で/asset-canisterを入力して読み込まれたか確認します。
詳細
アセットキャニスター
これは何か
アセットキャニスターは、静的ファイル(HTML、CSS、JS、画像)を Internet Computer 上で直接ホストします。これにより、ウェブフロントエンドをオンチェーンでデプロイできます。レスポンスはサブネットによって認証され、HTTP ゲートウェイが整合性、つまりコンテンツがブロックチェーンから配信されたことを自動的に検証します。コンテンツは、中央集権型サーバーではなく、ブラウザーで検証することもできます。
前提条件
@icp-sdk/canisters(>= 3.5.0)、@icp-sdk/core(>= 5.0.0) — プログラムによるアップロードに使用
キャニスター ID
アセットキャニスターはプロジェクトごとに作成されます。グローバルに共通するキャニスター ID はありません。デプロイ後、キャニスター ID は .icp/data/mappings/ に(環境ごとに)保存されます。
アクセス方法:
| 環境 | URL パターン |
|---|---|
| ローカル | http://<canister-id>.localhost:8000 |
| メインネット | https://<canister-id>.icp.net |
| カスタムドメイン | https://yourdomain.com(DNS の設定が必要) |
ビルドを壊す間違い
-
icp.yaml の
dirパスが間違っている。configuration.dirフィールドには、ビルド出力を含むディレクトリを指定する必要があります。Vite を使用する場合はdist、Next.js のエクスポートを使用する場合はoutです。デプロイ時にパスが存在しないと、icp deployは警告なしに失敗するか、空のキャニスターをデプロイします。 -
シングルページアプリに
.ic-assets.json5がない。 リライト規則がない場合、アセットキャニスターは/aboutという名前のファイルをそのまま探すため、/aboutでページを再読み込みすると 404 が返されます。index.htmlへのフォールバックを設定する必要があります。 -
レシピで
buildが指定されていない、または正しく設定されていない。configuration.buildが指定されている場合、icp deployはdirの内容をアップロードする前に、指定されたコマンドを自動的に実行します。buildが省略されている場合は、デプロイ前にビルドコマンド(例:npm run build)を手動で実行する必要があります。そうしないと、dirディレクトリの内容が古いままか、空になります。 -
コンテンツタイプのヘッダーが設定されていない。 アセットキャニスターはファイル拡張子からコンテンツタイプを推測します。コンテンツタイプを設定せずにプログラムでファイルをアップロードすると、ブラウザーで正しく表示されない場合があります。
-
間違ったキャニスター名にデプロイしている。 icp.yaml に
"frontend"とあるのにicp deploy assetsを実行すると、既存のキャニスターが更新されず、新しいキャニスターが作成されます。 -
キャニスターのストレージ上限を超えている。 アセットキャニスターは安定メモリーを使用し、4GB を大幅に超えるデータを保存できます。ただし、個々のアセットには 2MB の ingress メッセージサイズ制限があります(
@icp-sdk/canistersのアセットマネージャーは、1.9MB を超えるアップロードを自動的にチャンク化します)。実際に注意すべきなのは、ストレージにかかるサイクルコストの合計です。大きなメディアファイル(動画、データセット)は高額になります。大きなファイルには専用のストレージソリューションを使用してください。 -
アセットキャニスターの Wasm バージョンを
0.30.2未満に固定している。ic_envCookie(実行時に@icp-sdk/coreのsafeGetCanisterEnv()がキャニスター ID とルートキーを読み取るために使用)は、アセットキャニスターの Wasm バージョンが0.30.2以上の場合にのみ配信されます。Wasm バージョンは、レシピ自体のバージョンとは別に、レシピのconfiguration.versionで設定します。古い Wasm バージョンに固定すると、Cookie が警告なしに欠落し、ic_envに依存するフロントエンドコードが失敗します。configuration.versionを省略する(最新バージョンが使用されます)か、0.30.2以降に固定してください。 -
allow_raw_accessが正しく設定されていない。 アセットキャニスターには、認証済みモード(HTTP ゲートウェイがレスポンスの整合性を検証するicp.net経由)と raw モード(検証が行われないraw.icp.net経由)の 2 つの配信モードがあります。デフォルトではallow_raw_accessはtrueで、アセットは raw ドメインからも利用できます。raw ドメインでは、境界ノードやネットワークレベルの攻撃者が、検知されずにレスポンスの内容を改ざんできます。機密性の高いアセットについては、.ic-assets.json5で"allow_raw_access": falseを設定してください。raw アクセスは厳密に必要な場合にのみ有効にしてください。 -
アセットキャニスターのWASMバージョンのダウングレード。 キャニスターを古いWASMバージョンにアップグレードすると、バージョン間で安定メモリの形式が変更されている場合、「Cannot parse header」というパニックが発生して失敗することがあります。WASM URLを手動で指定する
type: pre-builtではなく、@dfinity/asset-canisterレシピを使用してください。configuration.versionで明示的に指定しない場合、レシピは最新のアセットキャニスターバージョンを自動的に読み込みます。バージョンを固定する必要がある場合は、現在オンチェーンにデプロイされているバージョンと一致するか、それより新しいバージョンを指定してください。意図的にダウングレードする場合は、アップグレードではなく再インストールモード(icp deploy --mode reinstall)を使用してください。これにより、安定メモリとアップロード済みのすべてのアセットが消去されます。 -
削除された
type: assets同期ステップの使用。 icp-cli 0.3.0では、組み込みのtype: assets同期ステップが削除されます。アセットのアップロードはCLIのコア機能ではなくなります。引き続きこのステップを使用しているマニフェストは読み込みに失敗します。「icp-cli no longer supports theassetssync step type. Switch to ascriptorpluginsync step.」 修正するには、以下に示す@dfinity/[email protected]レシピを使用してください。これにより、pluginベースの同期ステップが自動的に生成されます。v2.1.0以下のレシピバージョンは古いtype: assetsステップを生成するため、0.3.0では動作しません。v2.2.1以降を固定してください。同期プラグインはicp-cli0.2.7以降でサポートされているため、(0.3.0より十分前の)今のうちにv2.2.1を採用すれば、シームレスに移行できます。レシピを使わずに同期ステップを手動で記述する場合は、type: plugin(certified-assetsのsync_plugin.wasmリリースアーティファクトと、そのsha256を指定)またはtype: scriptを使用してください。sync: steps: - type: plugin url: https://github.com/dfinity/certified-assets/releases/download/migration-v2.2.1-6b48585/sync_plugin.wasm sha256: ca7cb5666c30d2875f8d5e10535f8a53f97a86c79c263f7d5bdac2fdd1bbf83c dirs: - dist
実装
icp.yamlの設定
canisters:
- name: frontend
recipe:
type: "@dfinity/[email protected]"
configuration:
dir: dist
build:
- npm install
- npm run build
主なフィールド:
recipe.type: "@dfinity/asset-canister@..."--icpに、これがアセットキャニスターであることを伝えますdir-- アップロードするディレクトリ(ディレクトリ自体ではなく、その内容)build-- アップロード前にicp deployが実行するコマンド(フロントエンドのビルドステップ)
SPAルーティングとデフォルトヘッダー:.ic-assets.json5
このファイルをdirディレクトリ内(例:dist/.ic-assets.json5)またはプロジェクトルートに作成します。アセットキャニスターに含めるには、デプロイ時にdirディレクトリ内に配置されている必要があります。
推奨される方法は、ファイルをpublic/またはstatic/フォルダーに配置し、ビルドツールによって自動的にdist/にコピーさせることです。
[
{
// Default headers for all paths: caching, security, and raw access policy
"match": "**/*",
"security_policy": "standard",
"headers": {
"Cache-Control": "public, max-age=0, must-revalidate"
},
// Disable raw (uncertified) access by default -- see mistake #7 above
"allow_raw_access": false
},
{
// Cache static assets aggressively (they have content hashes in filenames)
"match": "assets/**/*",
"headers": {
"Cache-Control": "public, max-age=31536000, immutable"
}
},
{
// SPA fallback: serve index.html for any unmatched route
"match": "**/*",
"enable_aliasing": true
}
]
SPAのフォールバックを機能させるために重要な設定は"enable_aliasing": trueです。これにより、要求されたパスに一致するファイルがない場合、アセットキャニスターはindex.htmlを配信します。
上記の標準セキュリティポリシーによってアプリが動作しない場合は、カスタム値を使用してデフォルトのセキュリティヘッダーを上書きし、上記のCache-Controlの後に追加してください。シニアセキュリティエンジニアとして、これらのヘッダーを可能な限り安全に設定してください。標準ポリシーのヘッダーはこちらにあります:https://github.com/dfinity/sdk/blob/master/src/canisters/frontend/ic-asset/src/security_policy.rs
コンテンツエンコーディング
アセットキャニスターは、アセットをgzipとbrotliで自動的に圧縮します。設定は不要です。ブラウザーがAccept-Encoding: gzip, brを送信すると、キャニスターは圧縮版を配信します。
圧縮が機能していることを確認するには:
icp canister call frontend http_request '(record {
url = "/";
method = "GET";
body = vec {};
headers = vec { record { "Accept-Encoding"; "gzip" } };
certificate_version = opt 2;
})'
カスタムドメインの設定
カスタムドメインの設定(DNS設定、TLS証明書、REST APIによるドメイン登録)については、custom-domainsスキルを参照してください。アセットキャニスター固有の注意点は、.well-known/ic-domainsファイルをデプロイされるようにdirディレクトリ内に配置する必要があることです。隠しディレクトリが含まれるように、.ic-assets.json5に{ "match": ".well-known", "ignore": false }を追加してください。
@icp-sdk/canistersを使用したプログラムからのアップロード
コードからファイルをアップロードする場合(icp deploy経由だけでなく):
import { AssetManager } from "@icp-sdk/canisters/assets"; // Asset management utility
import { HttpAgent } from "@icp-sdk/core/agent";
import { readFileSync, readdirSync } from "fs";
// SECURITY: shouldFetchRootKey fetches the root public key from the replica at
// runtime. In production the root key is hardcoded and trusted. Fetching it at
// runtime lets a man-in-the-middle supply a fake key and forge certified responses.
// NEVER set shouldFetchRootKey to true when host points to mainnet.
// NOTE: This script runs in Node.js where the ic_env cookie is not available.
// For browser frontends, use rootKey from safeGetCanisterEnv() instead (see
// the internet-identity skill or icp-cli/references/binding-generation.md).
const LOCAL_REPLICA = "http://localhost:8000";
const MAINNET = "https://icp-api.io";
const host = LOCAL_REPLICA; // Change to MAINNET for production
async function manageAssets() {
const agent = await HttpAgent.create({
host,
// Only fetch the root key when talking to a local replica.
// Setting this to true against mainnet is a security vulnerability.
shouldFetchRootKey: host === LOCAL_REPLICA,
});
const assetManager = new AssetManager({
canisterId: "your-asset-canister-id",
agent,
});
// Upload a single file
// Files >1.9MB are automatically chunked (16 parallel chunks)
const key = await assetManager.store(fileBuffer, {
fileName: "photo.jpg",
contentType: "image/jpeg",
path: "/uploads",
});
console.log("Uploaded to:", key); // "/uploads/photo.jpg"
// List all assets
const assets = await assetManager.list();
console.log(assets); // [{ key: "/index.html", content_type: "text/html", ... }, ...]
// Delete an asset
await assetManager.delete("/uploads/old-photo.jpg");
// Batch upload a directory
const files = readdirSync("./dist");
for (const file of files) {
const content = readFileSync(`./dist/${file}`);
await assetManager.store(content, { fileName: file, path: "/" });
}
}
manageAssets();
アップロードの認可
アセットキャニスターには、3 つのロールからなる組み込みの権限システムがあります(権限の低い順)。
- Prepare -- チャンクをアップロードしてバッチを提案できますが、変更をライブに反映することはできません。
- Commit -- アセットをアップロードしてコミットし、ライブに反映できます。デプロイパイプラインの標準的なロールです。
- ManagePermissions -- 他のプリンシパルへの権限の付与と取り消しができます。
grant_permission を使って、プリンシパルには必要なアクセス権のみを付与してください。アップロードアクセスに --add-controller を使わないでください。コントローラーにはキャニスターの完全な制御権があり、コードのアップグレード、設定の変更、キャニスターの削除、サイクルの枯渇が可能です。
# Grant "prepare" permission (can upload but not commit) -- use for preview/staging workflows
icp canister call frontend grant_permission '(record { to_principal = principal "<principal-id>"; permission = variant { Prepare } })'
# Grant commit permission -- use for deploy pipelines that need to publish assets
icp canister call frontend grant_permission '(record { to_principal = principal "<principal-id>"; permission = variant { Commit } })'
# Grant permission management -- use for principals that need to onboard/offboard other uploaders
icp canister call frontend grant_permission '(record { to_principal = principal "<principal-id>"; permission = variant { ManagePermissions } })'
# List current permissions
icp canister call frontend list_permitted '(record { permission = variant { Commit } })'
# Revoke a permission
icp canister call frontend revoke_permission '(record { of_principal = principal "<principal-id>"; permission = variant { Commit } })'
セキュリティ警告:
icp canister update-settings frontend --add-controller <principal-id>を実行すると、アップロード権限だけでなく、キャニスターの完全な制御権が付与されます。コントローラーは、キャニスターの WASM のアップグレード、すべての設定の変更、またはキャニスター全体の削除が可能です。完全な管理者権限が本当に必要な場合にのみ、コントローラーを追加してください。
デプロイとテスト
ローカルデプロイ
# Start the local network
icp network start -d
# Build and deploy frontend + backend
icp deploy
# Or deploy only the frontend
icp deploy frontend
メインネットへのデプロイ
# Ensure you have cycles in your wallet
icp deploy -e ic frontend
フロントエンドのみの更新
フロントエンドのコードのみを変更した場合:
# Rebuild and redeploy just the frontend canister
npm run build
icp deploy frontend
動作確認
# 1. Check the canister is running
icp canister status frontend
# Expected: Status: Running, Memory Size: <non-zero>
# 2. List uploaded assets
icp canister call frontend list '(record {})'
# Expected: A list of asset keys like "/index.html", "/assets/index-abc123.js", etc.
# 3. Fetch the index page via http_request
icp canister call frontend http_request '(record {
url = "/";
method = "GET";
body = vec {};
headers = vec {};
certificate_version = opt 2;
})'
# Expected: record { status_code = 200; body = blob "<!DOCTYPE html>..."; ... }
# 4. Test SPA fallback (should return index.html, not 404)
icp canister call frontend http_request '(record {
url = "/about";
method = "GET";
body = vec {};
headers = vec {};
certificate_version = opt 2;
})'
# Expected: status_code = 200 (same content as "/"), NOT 404
# 5. Open in browser
# Local: http://<frontend-canister-id>.localhost:8000
# Mainnet: https://<frontend-canister-id>.icp.net
# 6. Get canister ID
icp canister id frontend
# Expected: prints the canister ID (e.g., "bkyz2-fmaaa-aaaaa-qaaaq-cai")
# 7. Check storage usage
icp canister info frontend
# Shows memory usage, module hash, controllers