添付ファイルの保存先(クラウド PaaS での永続化と Blob Storage への移行)
Azure App Service・AWS ECS・Google Cloud Run などの PaaS では、コンテナやインスタンスの再起動・スケールでローカルストレージの内容が消えるため、添付ファイルを永続化する仕組みが必要です。このページでは、添付ファイルの格納方式と各クラウドでの構成、既存の Azure Files マウント構成から AzureBlob プロバイダへの移行手順をまとめます。
- 追加設定なしで済ませるなら
Rds(DB 格納)。ただし DB の容量とコストに直結します。 - Azure では 1.5.7.0 で追加された
AzureBlobが、マウント不要・アカウントキー不要でもっとも扱いやすい構成です。 - それ以外のクラウドでは
Localにして、クラウドのファイルストレージ(EFS・Filestore など)をマウントします。 - Azure Files マウントから
AzureBlobへの移行は「ファイル共有の中身をコンテナのルートへコピー」と「Providerの切り替え」だけで済み、DB の変更は不要です。
INFO
バージョン 1.5.7.0 が対象です。以下のストレージアカウント名・リソースグループ名・App Service 名・ID・IP アドレスなどはすべてダミー値です。実際の環境の値に置き換えてください。
格納方式
BinaryStorage.json
添付ファイルの格納方式は BinaryStorage.json で設定します。
{
"Provider": "Rds",
"AzureBlobStorageAccountUri": null,
"AzureBlobContainerName": null,
"Path": null,
"Attachments": true,
"Images": true,
"UseStorageSelect": false,
"TemporaryBinaryStorageProvider": null
}Provider の比較
Provider に指定できる値は Rds・Local・AzureBlob の 3 種類です。AzureBlob は 1.5.7.0 で追加されたプロバイダで、1.5.6.0 以前は Rds と Local の 2 択でした。
| 項目 | Rds | Local | AzureBlob |
|---|---|---|---|
| 格納先 | データベース(Binaries テーブル) | ファイルシステム | Azure Blob Storage |
| データ形式 | バイナリカラム(Bin) | バイナリファイル(GUID 名) | BLOB(Attachments/{Guid}) |
| DB 負荷 | 大きい(ファイルサイズ分) | 小さい(メタデータのみ) | 小さい(メタデータのみ) |
| バックアップ | DB バックアップに含まれる | 別途ファイルバックアップが必要 | ストレージアカウント側で管理 |
| PaaS 対応 | 追加設定不要 | 永続ストレージのマウントが必要 | マウント不要(HTTPS でアクセス) |
PaaS での選び方
図を読み込み中…
Rds モード
Provider を Rds にすると、添付ファイルはすべてデータベースに格納されるため、ファイルストレージのマウントは不要です。PaaS ではもっともシンプルな構成です。
{
"Provider": "Rds"
}ただし次の点に注意が必要です。
- データベースの容量が添付ファイルの分だけ増えます。
- SQL Server Express を使っている場合、10GB の容量制限があります。
- 大量の添付ファイルを扱う場合、データベースのパフォーマンスに影響する可能性があります。
- データベースのバックアップサイズが大きくなります。
WARNING
大量の添付ファイルを扱う場合や、ファイルサイズが大きい場合は、Local モードや AzureBlob モードを検討してください。
Local モード(ファイルストレージをマウント)
Provider を Local にし、Path にマウント先のパスを指定します。クラウド側のストレージ設定が違うだけで、プリザンターからはどれも同じファイルシステムとして透過的に扱えます。
{
"Provider": "Local",
"Path": "/mnt/pleasanter-files"
}| クラウド | PaaS サービス | ファイルストレージ | マウント方式 |
|---|---|---|---|
| Azure | App Service | Azure Files | パスマッピング |
| Azure | Container Apps | Azure Files | ボリュームマウント |
| AWS | ECS(Fargate) | Amazon EFS | EFS ボリューム |
| AWS | Elastic Beanstalk | Amazon EFS | .ebextensions |
| Google Cloud | Cloud Run | Filestore | NFS ボリューム |
| Google Cloud | GKE | Filestore | PersistentVolume |
| Docker | Docker Compose | ホストディレクトリ | バインドマウント |
以下の各構成で、プリザンター側の設定は上の BinaryStorage.json と同じです。
Azure App Service + Azure Files
Azure Files(SMB ファイル共有)をパスマッピングでマウントします。
図を読み込み中…
ストレージアカウントとファイル共有を作成します。
bash# リソースグループの作成 az group create --name rg-example --location japaneast # ストレージアカウントの作成 az storage account create \ --name stexamplefiles \ --resource-group rg-example \ --location japaneast \ --sku Standard_LRS # ファイル共有の作成 az storage share-rm create \ --storage-account stexamplefiles \ --name pleasanter-attachments \ --quota 100App Service にマウントします。Azure Portal では「構成」>「パス マッピング」から設定します。Azure CLI では次のとおりです。
bash# ストレージアカウントキーの取得 STORAGE_KEY=$(az storage account keys list \ --account-name stexamplefiles \ --resource-group rg-example \ --query '[0].value' -o tsv) # App Service にマウント az webapp config storage-account add \ --resource-group rg-example \ --name app-example \ --custom-id PleasanterFiles \ --storage-type AzureFiles \ --share-name pleasanter-attachments \ --account-name stexamplefiles \ --access-key "$STORAGE_KEY" \ --mount-path /mnt/pleasanter-filesBinaryStorage.jsonをProvider: "Local"、Path: "/mnt/pleasanter-files"にします。
ストレージマウントの詳細は Microsoft のドキュメント「Azure Storage をローカル共有としてマウントする」を参照してください。
TIP
1.5.7.0 以降の Azure App Service では、マウントを使わない AzureBlob モード が推奨です。既存のマウント構成からの移行は Azure Files マウントから AzureBlob へ移行する を参照してください。
Azure Container Apps + Azure Files
Container Apps 環境にストレージを追加し、アプリ定義でボリュームマウントします。
# Container Apps 環境にストレージを追加
az containerapp env storage set \
--name cae-example \
--resource-group rg-example \
--storage-name pleasanterfiles \
--azure-file-account-name stexamplefiles \
--azure-file-account-key "$STORAGE_KEY" \
--azure-file-share-name pleasanter-attachments \
--access-mode ReadWriteproperties:
template:
containers:
- name: pleasanter
image: your-registry/pleasanter:latest
volumeMounts:
- volumeName: pleasanter-files
mountPath: /mnt/pleasanter-files
volumes:
- name: pleasanter-files
storageType: AzureFile
storageName: pleasanterfilesAWS ECS(Fargate)+ Amazon EFS
Amazon EFS(Elastic File System)は NFS v4 プロトコルのマネージドファイルストレージで、ECS タスクから直接マウントできます。
図を読み込み中…
EFS ファイルシステムとマウントターゲットを作成します。
bash# EFS ファイルシステムの作成 aws efs create-file-system \ --performance-mode generalPurpose \ --throughput-mode bursting \ --tags Key=Name,Value=pleasanter-attachments \ --region ap-northeast-1 # マウントターゲットの作成(VPC のサブネットごとに作成) aws efs create-mount-target \ --file-system-id fs-0123456789abcdef0 \ --subnet-id subnet-0123456789abcdef0 \ --security-groups sg-0123456789abcdef0タスク定義でボリュームとマウントポイントを設定します。
json{ "family": "pleasanter", "networkMode": "awsvpc", "requiresCompatibilities": ["FARGATE"], "cpu": "1024", "memory": "2048", "volumes": [ { "name": "pleasanter-files", "efsVolumeConfiguration": { "fileSystemId": "fs-0123456789abcdef0", "rootDirectory": "/pleasanter-attachments", "transitEncryption": "ENABLED" } } ], "containerDefinitions": [ { "name": "pleasanter", "image": "your-registry/pleasanter:latest", "mountPoints": [ { "sourceVolume": "pleasanter-files", "containerPath": "/mnt/pleasanter-files", "readOnly": false } ] } ] }BinaryStorage.jsonをProvider: "Local"、Path: "/mnt/pleasanter-files"にします。
EFS のマウント方法は AWS のドキュメント「Amazon EFS ファイルシステムのマウント」を参照してください。
AWS Elastic Beanstalk + Amazon EFS
.ebextensions で EFS をマウントします。
packages:
yum:
amazon-efs-utils: []
commands:
01_mount_efs:
command: |
mkdir -p /mnt/pleasanter-files
mount -t efs -o tls fs-0123456789abcdef0:/ /mnt/pleasanter-files
chown webapp:webapp /mnt/pleasanter-files
test: "! mountpoint -q /mnt/pleasanter-files"Google Cloud Run + Filestore(NFS)
Filestore(NFS のマネージドサービス)を使います。Cloud Run の第 2 世代実行環境では NFS ボリュームのマウントがサポートされています。
図を読み込み中…
Filestore インスタンスを作成します。
bash# Filestore インスタンスの作成 gcloud filestore instances create pleasanter-files \ --zone=asia-northeast1-a \ --tier=BASIC_HDD \ --file-share=name=pleasanter_attachments,capacity=1TB \ --network=name=defaultCloud Run の YAML 定義で NFS ボリュームをマウントし、デプロイします。
yamlapiVersion: serving.knative.dev/v1 kind: Service metadata: name: pleasanter spec: template: metadata: annotations: run.googleapis.com/execution-environment: gen2 run.googleapis.com/network-interfaces: '[{"network":"default","subnetwork":"default"}]' spec: containers: - image: your-registry/pleasanter:latest volumeMounts: - name: pleasanter-files mountPath: /mnt/pleasanter-files volumes: - name: pleasanter-files nfs: server: 10.0.0.2 path: /pleasanter_attachments readOnly: falsebash# デプロイ gcloud run services replace service.yaml --region=asia-northeast1BinaryStorage.jsonをProvider: "Local"、Path: "/mnt/pleasanter-files"にします。
Cloud Run での NFS マウントは Google Cloud のドキュメント「Cloud Run でネットワーク ファイル システムを使用する」を参照してください。
GKE + Filestore
Filestore CSI ドライバーを使い、PersistentVolume としてマウントします。
apiVersion: v1
kind: PersistentVolume
metadata:
name: pleasanter-pv
spec:
capacity:
storage: 100Gi
accessModes:
- ReadWriteMany
nfs:
server: 10.0.0.2
path: /pleasanter_attachments
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pleasanter-pvc
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 100Gi
volumeName: pleasanter-pvapiVersion: apps/v1
kind: Deployment
metadata:
name: pleasanter
spec:
template:
spec:
containers:
- name: pleasanter
image: your-registry/pleasanter:latest
volumeMounts:
- name: pleasanter-files
mountPath: /mnt/pleasanter-files
volumes:
- name: pleasanter-files
persistentVolumeClaim:
claimName: pleasanter-pvcDocker Compose
ローカル開発や小規模な環境では、ホストのディレクトリをバインドマウントするか、Docker ボリュームを使います。
services:
pleasanter:
image: your-registry/pleasanter:latest
volumes:
- pleasanter-files:/mnt/pleasanter-files
environment:
- ASPNETCORE_URLS=http://+:5000
volumes:
pleasanter-files:
driver: localAzureBlob モード
1.5.7.0 以降で使えます。
1.5.7.0 で追加された AzureBlob プロバイダは、ファイル共有をマウントせずに Azure Blob Storage へ直接読み書きします。認証には DefaultAzureCredential を使うため、App Service のマネージド ID をそのまま利用でき、接続文字列やアカウントキーを保持する必要がありません。
図を読み込み中…
設定手順
ストレージアカウントとコンテナを作成します。
bashaz storage account create \ --name stexampleblob \ --resource-group rg-example \ --location japaneast \ --sku Standard_LRS az storage container create \ --account-name stexampleblob \ --name pleasanter-binaries \ --auth-mode loginApp Service のマネージド ID に「ストレージ BLOB データ共同作成者」ロールを付与します。
bash# システム割り当てマネージド ID を有効化 PRINCIPAL_ID=$(az webapp identity assign \ --resource-group rg-example \ --name app-example \ --query principalId -o tsv) # ストレージ BLOB データ共同作成者ロールを付与 az role assignment create \ --assignee "$PRINCIPAL_ID" \ --role "Storage Blob Data Contributor" \ --scope "/subscriptions/<subscription-id>/resourceGroups/rg-example/providers/Microsoft.Storage/storageAccounts/stexampleblob"BinaryStorage.jsonを設定します。json{ "Provider": "AzureBlob", "AzureBlobStorageAccountUri": "https://stexampleblob.blob.core.windows.net", "AzureBlobContainerName": "pleasanter-binaries" }
AzureBlobStorageAccountUri と AzureBlobContainerName は環境変数でも指定できます。コンテナ名を省略した場合の既定値は pleasanter-binaries です。
| 環境変数 | 対応する設定 |
|---|---|
Implem.Pleasanter_BinaryStorage_AzureBlobStorageAccountUri | AzureBlobStorageAccountUri |
Implem.Pleasanter_BinaryStorage_AzureBlobContainerName | AzureBlobContainerName |
環境変数名の先頭は Service.json の Name(既定は Implem.Pleasanter)です。Service.json の EnvironmentName を設定している場合は、{EnvironmentName}_BinaryStorage_... が先に参照されます。BinaryStorage.json に値があればそちらが優先です(Initializer.cs)。
INFO
AzureBlobBinaryStorageProvider にはサーキットブレーカーが組み込まれており、接続・認証に連続して失敗すると 30 秒 → 2 分 → 10 分と段階的にアクセスを遮断します。遮断中は IOException が返り、失敗の内容は SysLogs に記録されます。回路の状態は静的フィールドで保持されているため、アプリを再起動するとリセットされます。
オブジェクト名とローカルフォルダのパス
ローカルフォルダ側のプロバイダは、objectName の / をディレクトリ区切りに置き換えて BinaryStorage.Path の下に連結しているだけです。Blob 側は objectName をそのままオブジェクト名として使います。
private static string ResolvePath(string objectName)
{
ValidateObjectName(objectName);
var relevantPath = objectName.Replace('/', Path.DirectorySeparatorChar);
return Path.Combine(Directories.BinaryStorage(), relevantPath);
}LocalFolderBinaryStorageProvider.cs#L14-L19
そのため、Blob のオブジェクト名と Local モードでのファイルパスは完全に対応します。
| 種別 | オブジェクト名 | ローカルフォルダ上のパス |
|---|---|---|
| 添付ファイル | Attachments/{Guid} | {Path}/Attachments/{Guid} |
| 本文の画像 | Images/{Guid} | {Path}/Images/{Guid} |
| 画像のサムネイル | Images/{Guid}_thumbnail | {Path}/Images/{Guid}_thumbnail |
| サイト画像 | SiteImage/{ReferenceId}_{SizeType}.png | {Path}/SiteImage/{ReferenceId}_{SizeType}.png |
| テナント画像 | TenantImage/{ReferenceId}_{SizeType}.png | {Path}/TenantImage/{ReferenceId}_{SizeType}.png |
サイト画像・テナント画像のオブジェクト名は次のように組み立てられます。SizeTypes は Regular / Thumbnail / Icon / Logo の 4 種類です。
private string ObjectName(long referenceId, Types type, SizeTypes sizeType)
{
return $"{type}/{referenceId}_{sizeType}.png";
}データベースの Binaries テーブルは Guid を持つだけで、オブジェクト名はそこから組み立てられます。格納先が変わっても参照は壊れません。
UseStorageSelect によるカラム別の格納先
UseStorageSelect を有効にして項目ごとに格納先(DataBase / LocalFolder など)を指定している場合、Provider が AzureBlob なら LocalFolder の指定は自動的に AzureBlob に読み替えられます。
public static string BinaryStorageProvider(Column column = null)
{
if (Parameters.BinaryStorage.UseStorageSelect && column != null)
{
var selected = string.IsNullOrEmpty(column?.BinaryStorageProvider)
? Parameters.BinaryStorage.DefaultBinaryStorageProvider
: column?.BinaryStorageProvider;
if (selected == BinaryStorageProviderNames.LocalFolder
&& Parameters.BinaryStorage.IsAzureBlob())
{
return BinaryStorageProviderNames.AzureBlob;
}
return selected;
}
// ...
}AutoDataBaseOrLocalFolder(サイズによって DB とファイルを使い分ける設定)も同様に、ファイル側の格納先が AzureBlob になります。
LocalFolderLimitSize などのサイズ上限もそのまま効きます。判定に使われる IsStoreExternal() が Local と AzureBlob を同じ「外部ストレージ」として扱うためです。
if (Parameters.BinaryStorage.IsStoreExternal(
BinaryUtilities.BinaryStorageProvider(column, attachment.Size.GetValueOrDefault())))
{
if (attachment.Size > column.LocalFolderLimitSize * 1024 * 1024)
{
return Error.Types.OverLocalFolderLimitSize;
}
}
else
{
if (attachment.Size > column.LimitSize * 1024 * 1024)
{
return Error.Types.OverLimitSize;
}
}DB 格納のファイルとの混在
Provider が Rds の時期に登録されたファイルや、UseStorageSelect で DataBase を指定した項目のファイルは、Binaries テーブルの Bin 列に入ったままです。ダウンロード時は Bin にデータがあればそこから返すフォールバックがあるため、DB 格納のファイルと Blob 格納のファイルが混在していても問題なく動きます。すべてを Blob に寄せたい場合は別途データ移行が必要です。
ロードバランサ配下での一時ファイル
複数インスタンスで運用する場合は、一時ファイルの保存先に注意が必要です。
TemporaryBinaryStorageProvider
TemporaryBinaryStorageProvider を Rds にすると、Web UI からのアップロード時に一時ファイルが Binaries テーブルに BinaryType = 'Temporary' として格納されます。アップロード用のリクエストと保存用のリクエストが別インスタンスに振り分けられても、一時ファイルを見失わないための仕組みです。
1.5.7.0 では、レコード保存時に一時ファイルを DB から外部ストレージへ移し替える処理(MoveBinFromRdsToExternal)が追加されました。Provider が Local や AzureBlob の構成であれば、Bin 列のデータは外部ストレージへアップロードされたうえで NULL に更新されます。
// TemporaryBinaryStorageProvider が "Rds" かつ Web UI の場合
if (Parameters.BinaryStorage.TemporaryBinaryStorageProvider == BinaryStorageProviderNames.Rds
&& context.Api != true)
{
if (provider != null)
{
// 一時ファイルの Bin を外部ストレージへ移送
MoveBinFromRdsToExternal(
context: context,
provider: provider);
}
statements.Add(Rds.UpdateBinaries(
param: Rds.BinariesParam()
.TenantId(context.TenantId)
.ReferenceId(referenceId, _using: referenceId != 0)
.Guid(Guid)
.BinaryType("Attachments")
.Bin(raw: "NULL", _using: provider != null) // 移送済みなら NULL にする
// ...
where: Rds.BinariesWhere().Guid(Guid)));
}MoveBinFromRdsToExternal() は BinaryType = 'Temporary' のレコードを読み出し、Attachments/{Guid} というオブジェクト名で外部ストレージへアップロードします。アップロードに失敗した場合は SysLogs に記録したうえで例外を送出するため、DB 側だけ更新されて実体が消えることはありません。
図を読み込み中…
WARNING
1.5.6.0 以前には移送処理がありません。TemporaryBinaryStorageProvider を Rds にすると、Provider が Local でもファイルが Bin 列に残り続け、Local モードのメリット(DB サイズの削減)が得られません。
推奨構成
| バージョン | 構成 |
|---|---|
| 1.5.7.0 以降 | Provider を Local または AzureBlob にし、TemporaryBinaryStorageProvider を Rds にする。一時ファイルはインスタンス間で共有され、保存時に外部ストレージへ移送される |
| 1.5.6.0 以前 | TemporaryBinaryStorageProvider は Rds にせず、Provider を Local にして Azure Files・Amazon EFS・Filestore などの共有ストレージをマウントする。全インスタンスが同じファイルシステムを見るため、一時ファイルの問題は起きない |
| 容量を気にしない場合 | Provider を Rds にしてすべてを DB に格納する |
{
"Provider": "AzureBlob",
"AzureBlobStorageAccountUri": "https://stexampleblob.blob.core.windows.net",
"AzureBlobContainerName": "pleasanter-binaries",
"TemporaryBinaryStorageProvider": "Rds"
}Azure Files マウントから AzureBlob へ移行する
1.5.7.0 以降で使えます。
すでに Azure App Service で「Azure Files をパスマッピングでマウントし、Provider を Local」にしている環境を、AzureBlob へ移行する手順です。
WARNING
本番環境での作業は必ずメンテナンス時間帯に行い、事前にストレージアカウントとデータベースのバックアップを取得してください。
移行する理由
| 観点 | Azure Files マウント | Blob Storage 直接 |
|---|---|---|
| App Service の設定 | パスマッピングが必要 | 不要 |
| 認証情報 | ストレージアカウントキーを App Service に保持 | マネージド ID(キーの保持なし) |
| プロトコル | SMB | HTTPS |
| コンテナの再作成 | マウント設定の再適用が必要 | 環境変数だけで完結 |
| Linux コンテナ | SMB マウントの制約を受けやすい | 制約なし |
一番大きいのはアカウントキーを持たなくてよくなる点です。マウント方式ではキーを App Service の構成に登録する必要があり、キーのローテーションのたびに再設定が発生します。
移行の考え方
オブジェクト名とローカルフォルダのパス が完全に一致しているため、ファイル共有のルート配下をそのまま Blob コンテナのルートへコピーすれば、パスの読み替えは不要です。Binaries テーブルの変更も要りません。移行作業の実体は「ファイルのコピー」と「設定の切り替え」の 2 つだけです。
また、次の設定は移行後もそのまま使えます。
UseStorageSelectの項目別設定・サイズ上限(UseStorageSelect によるカラム別の格納先 を参照)。サイト設定の変更は不要です。- DB に入っているファイルは移行対象外で、Blob と混在したまま動きます(DB 格納のファイルとの混在)。
TemporaryBinaryStorageProviderがRdsの構成。移送先が Blob になり、「一時ファイルは DB、確定したら Blob」という構成になります。
全体の流れ
図を読み込み中…
1. 移行先の準備
AzureBlob モードの設定手順 の 1〜2(コンテナの作成とマネージド ID へのロール付与)を先に済ませます。
- コンテナは既存の Azure Files と同じストレージアカウントに作ってかまいません。ライフサイクル管理やアクセス制御を分けたい場合だけ別アカウントにします。
- コンテナ名を
pleasanter-binaries(AzureBlobContainerNameの既定値)にしておくと、ストレージアカウント URI の指定だけで動きます。
INFO
ロール割り当ての反映には数分かかることがあります。コピー作業の前に済ませておくと待ち時間を吸収できます。
2. アプリを停止する
コピー中にファイルが追加されると取りこぼしが発生するため、アプリを停止します。
az webapp stop --resource-group rg-example --name app-example3. ファイル共有からコンテナへコピーする
azcopy を使います。
WARNING
AzCopy が Microsoft Entra ID(azcopy login)で認証できるのは Blob と Data Lake Storage だけで、Azure Files には SAS トークンが必要です。azcopy login を済ませていても、ファイル共有の URL に SAS を付けないと認証エラーになります。
まずストレージアカウントキーからファイル共有用の SAS を発行します。
$account = 'stexamplefiles'
$share = 'pleasanter-attachments'
$container = 'pleasanter-binaries'
$expiry = (Get-Date).ToUniversalTime().AddHours(4).ToString('yyyy-MM-ddTHH:mmZ')
$key = az storage account keys list `
--account-name $account `
--resource-group rg-example `
--query '[0].value' -o tsv
# 読み取り + 一覧の SAS
$shareSas = az storage share generate-sas `
--account-name $account `
--account-key $key `
--name $share `
--permissions rl `
--expiry $expiry -o tsvコピー先の Blob コンテナは azcopy login の Entra ID 認証が使えます。ソースは SAS、宛先は OAuth という組み合わせで問題ありません。
azcopy login
azcopy copy `
"https://$account.file.core.windows.net/$share/*?$shareSas" `
"https://$account.blob.core.windows.net/$container/" `
--recursive=true `
--preserve-smb-info=false--recursive=trueにより、Attachments/・Images/・SiteImage/などのサブディレクトリがそのままコンテナ内の階層(オブジェクト名のプレフィックス)になります。- ソース URL の末尾を
/*にしているのは、共有フォルダ自体ではなく中身をコンテナ直下へ展開するためです。 - リネームや階層の作り替えは行いません。
コピー後、azcopy のジョブサマリで Number of Transfers Failed が 0、Final Job Status が Completed であることを確認します。失敗があった場合は azcopy jobs show <ジョブID> で詳細を確認できます。
Number of File Transfers: 12043
Number of Transfers Completed: 12043
Number of Transfers Failed: 0
Final Job Status: CompletedWARNING
azcopy sync で差分だけをコピーすることもできますが、削除の同期(--delete-destination)は使わないでください。コピー元にないファイルがコンテナから消えてしまいます。初回移行では azcopy copy を使うのが安全です。
4. Provider を切り替える
Provider を Local から AzureBlob に変え、接続先を指定します。Path は使われなくなるので削除してかまいません。環境変数で指定することもできます(AzureBlob モードの設定手順 を参照)。
- "Provider": "Local",
- "Path": "/mnt/pleasanter-files"
+ "Provider": "AzureBlob",
+ "AzureBlobStorageAccountUri": "https://stexamplefiles.blob.core.windows.net",
+ "AzureBlobContainerName": "pleasanter-binaries"5. アプリを起動して動作確認する
az webapp start --resource-group rg-example --name app-example次の 4 点を確認します。新規アップロードだけでなく、既存ファイルの参照を必ず確認してください。コピー漏れはここで初めて表面化します。
- 移行前に登録された添付ファイルがダウンロードできる
- 新規に添付ファイルをアップロードして、ダウンロードできる
- 本文に貼り付けた既存画像が表示される
- サイトアイコン(サイト画像)が表示される
失敗する場合は SysLogs を確認します。AzureBlobBinaryStorageProvider は失敗時に次のようなエラーを書き出します。
AzureBlob Download failed. Blob=Attachments/xxxxxxxx Status=403 ErrorCode=AuthorizationPermissionMismatch Message=...| 症状 | 疑うところ |
|---|---|
403 AuthorizationPermissionMismatch | ロール割り当てのスコープ、または反映待ち |
404(特定のファイルだけ) | コピー漏れ。azcopy のログを確認 |
| すべてのファイルで失敗 | ストレージアカウント URI・コンテナ名の指定ミス |
WARNING
移行直後にロール割り当てが未反映だと、連続失敗でサーキットブレーカーが働き、30 秒 → 2 分 → 10 分と段階的にアクセスが遮断されます。権限を直したあとは、遮断時間が明けるのを待つかアプリを再起動してください。
6. マウントを解除する
動作確認が済んだら App Service のパスマッピングを削除します。
az webapp config storage-account delete `
--resource-group rg-example `
--name app-example `
--custom-id PleasanterFilesファイル共有そのものは、切り戻しに備えてしばらく残しておきます。問題がないと判断できてから削除してください。
スクリプトでまとめて実行する
手順 2〜3(アプリ停止・SAS 発行・コピー・結果判定)をまとめた PowerShell 7 以降用のスクリプトです。事前に Azure CLI と AzCopy をインストールし、az login と azcopy login を済ませておきます。
#Requires -Version 7.0
<#
.SYNOPSIS
プリザンターの添付ファイルを Azure Files から Blob Storage へ移行します。
.DESCRIPTION
App Service を停止し、ファイル共有の内容を Blob コンテナへコピーします。
BinaryStorage.json の書き換えとアプリの起動は行いません(動作確認前に
設定を切り替えたくないため)。コピー結果のみを返します。
.EXAMPLE
.\Migrate-PleasanterBinaries.ps1 `
-ResourceGroup rg-example `
-WebAppName app-example `
-StorageAccount stexamplefiles `
-ShareName pleasanter-attachments
#>
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory)][string]$ResourceGroup,
[Parameter(Mandatory)][string]$WebAppName,
[Parameter(Mandatory)][string]$StorageAccount,
[Parameter(Mandatory)][string]$ShareName,
[string]$ContainerName = 'pleasanter-binaries',
[int]$SasExpiryHours = 4,
[switch]$SkipAppStop
)
$ErrorActionPreference = 'Stop'
function Assert-Command {
param([string]$Name)
if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) {
throw "$Name が見つかりません。インストールしてパスを通してください。"
}
}
function Invoke-Az {
param([string[]]$Arguments)
$result = & az @Arguments
if ($LASTEXITCODE -ne 0) {
throw "az $($Arguments -join ' ') が失敗しました(終了コード $LASTEXITCODE)。"
}
return $result
}
# --- 事前チェック ---------------------------------------------------------
Assert-Command -Name 'az'
Assert-Command -Name 'azcopy'
Write-Host '移行対象を確認しています...'
$expiry = (Get-Date).ToUniversalTime().AddHours($SasExpiryHours).ToString('yyyy-MM-ddTHH:mmZ')
# コンテナの存在確認(無ければ先に作成しておく)
$containerExists = Invoke-Az @(
'storage', 'container', 'exists',
'--account-name', $StorageAccount,
'--name', $ContainerName,
'--auth-mode', 'login',
'--query', 'exists', '-o', 'tsv')
if ($containerExists -ne 'true') {
throw "コンテナ $ContainerName が存在しません。先に作成してください。"
}
# --- アプリ停止 -----------------------------------------------------------
if (-not $SkipAppStop) {
if ($PSCmdlet.ShouldProcess($WebAppName, 'App Service を停止')) {
Write-Host "App Service $WebAppName を停止しています..."
Invoke-Az @('webapp', 'stop', '--resource-group', $ResourceGroup, '--name', $WebAppName) | Out-Null
# 実行中のリクエストが落ち着くまで少し待つ
Start-Sleep -Seconds 15
}
}
# --- SAS の発行 -----------------------------------------------------------
# AzCopy は Azure Files に対して Entra ID 認証を使えないため SAS が必須
Write-Host 'ファイル共有の SAS を発行しています...'
$accountKey = Invoke-Az @(
'storage', 'account', 'keys', 'list',
'--account-name', $StorageAccount,
'--resource-group', $ResourceGroup,
'--query', '[0].value', '-o', 'tsv')
$shareSas = Invoke-Az @(
'storage', 'share', 'generate-sas',
'--account-name', $StorageAccount,
'--account-key', $accountKey,
'--name', $ShareName,
'--permissions', 'rl',
'--expiry', $expiry,
'-o', 'tsv')
# --- コピー ---------------------------------------------------------------
$source = "https://$StorageAccount.file.core.windows.net/$ShareName/*?$shareSas"
$destination = "https://$StorageAccount.blob.core.windows.net/$ContainerName/"
if ($PSCmdlet.ShouldProcess($destination, 'azcopy でコピー')) {
Write-Host 'コピーを開始します(件数によっては時間がかかります)...'
$output = & azcopy copy $source $destination `
--recursive=true `
--preserve-smb-info=false 2>&1
$output | Write-Host
# ジョブサマリから結果を判定する
$summary = $output -join "`n"
$failed = [regex]::Match($summary, 'Number of Transfers Failed:\s*(\d+)')
$status = [regex]::Match($summary, 'Final Job Status:\s*(\S+)')
if (-not $status.Success) {
throw 'azcopy のジョブサマリを解析できませんでした。出力を確認してください。'
}
if ($status.Groups[1].Value -ne 'Completed' -or
($failed.Success -and [int]$failed.Groups[1].Value -gt 0)) {
throw "コピーが正常に完了しませんでした(Status=$($status.Groups[1].Value) Failed=$($failed.Groups[1].Value))。" +
' azcopy jobs show <ジョブID> で詳細を確認してください。'
}
$completed = [regex]::Match($summary, 'Number of Transfers Completed:\s*(\d+)')
Write-Host ''
Write-Host "コピー完了: $($completed.Groups[1].Value) 件" -ForegroundColor Green
}
Write-Host ''
Write-Host '次の手順:' -ForegroundColor Cyan
Write-Host ' 1. BinaryStorage.json の Provider を AzureBlob に変更'
Write-Host " 2. az webapp start --resource-group $ResourceGroup --name $WebAppName"
Write-Host ' 3. 既存ファイルの参照と新規アップロードを確認'このスクリプトは意図的に次の 2 つを行いません。
BinaryStorage.jsonの書き換え: 設定の反映方法(ファイル配置・環境変数・デプロイパイプライン)は環境ごとに違うためです。- アプリの起動: 設定を切り替える前に起動すると、まだ
Localを見たままファイルを書き込んでしまいます。停止したまま設定を切り替えてから起動してください。
-WhatIf を付けると、停止とコピーを実行せずに動作を確認できます。
.\Migrate-PleasanterBinaries.ps1 `
-ResourceGroup rg-example `
-WebAppName app-example `
-StorageAccount stexamplefiles `
-ShareName pleasanter-attachments `
-WhatIf所要時間の見積もり
本番のメンテナンス時間を見積もるため、検証環境でコピーの所要時間を測っておくと計画が立てやすくなります。ファイル共有側の件数は次のように確認できます。
$listed = & azcopy list "https://$account.file.core.windows.net/$share/?$shareSas" --machine-readable
$files = $listed | Select-String 'Content Length'
"ファイル数: $($files.Count)"INFO
azcopy list の出力形式はバージョンによって変わることがあります。件数は目安として扱い、コピーの成否はジョブサマリ(Number of Transfers Failed と Final Job Status)で判断してください。
切り戻し手順
- アプリを停止する
BinaryStorage.jsonをProvider: "Local"とPathに戻す- パスマッピングを解除していた場合は再設定する
- アプリを起動する
移行作業ではファイル共有側のファイルを削除していないため、これで元の状態に戻ります。ただし移行後に登録された添付ファイルは Blob 側にしかありません。切り戻す場合は、その分を Blob からファイル共有へコピーし直します。宛先がファイル共有なので、書き込み権限付きの SAS が必要です。
# 移行後に増えた分をファイル共有へ戻す
$shareSas = az storage share generate-sas `
--account-name $account `
--account-key $key `
--name $share `
--permissions rcwl `
--expiry $expiry -o tsv
azcopy copy `
"https://$account.blob.core.windows.net/$container/*" `
"https://$account.file.core.windows.net/$share/?$shareSas" `
--recursive=trueこのリスクを小さくするため、移行はアプリを停止したメンテナンス時間内に完了させ、動作確認まで一気に行うことをおすすめします。