初めまして、新卒駆け出しIT技術者のIORIです!(来年にはこれ使えんのか...)
第二弾も記事投稿したのでぜひ見てほしいです...!(追記)
今回は研修の一環でアジャイル開発で作成した飲食店マッチングアプリ(記事書いたら会社に怒られるかもしれないので表現変えてます)を、研修用の Azure アカウントから まったく別の Azure アカウント に移行することになりました。
最初は「いい感じに Terraform を動かして Azure SQL のデータをエクスポート/インポートすればいい」と思っていました。
実際には、
- 店舗画像の Blob Storage
- App Service の接続設定
- Terraform の state
- GitHub Actions の OIDC 認証
- Portal で変更されていた SQL スキーマの反映(チームでの変更に関する手順を周知していたのですが...チームメンバとの関わり方はまだ勉強が必要ですね...)
まで手を入れる必要があり、想定の2〜3倍の時間がかかりました。
この記事では、実際の移行手順とハマったポイントをまとめます。「サブスクリプション間の移行」ではなく、移行先の Azure アカウント(テナント)自体が違う ケースを想定しています。
背景と移行のゴール
なぜ移行したか
appName は、社内メンバーがランチの参加者と店舗をランダム抽選できる Web アプリです。
- フロント: React(Azure Static Web Apps)
- API: Node.js(Azure App Service)
- DB: Azure SQL Database
- 画像: Azure Blob Storage
- IaC: Terraform + GitHub Actions(OIDC)
そもそも「なんでそんなことを?」と思われるかと思いますが、研修で使用しているAzure環境は研修用の環境で、研修終了後にリソースグループごと削除されるため、「その前に移行準備をして会社で運用できるようにしよう!」といった背景です。
移行のゴール
| 項目 | ゴール |
|---|---|
| データ | Users / Stores を欠損なく移す |
| 画像 | 店舗画像がフロントで表示される |
| ダウンタイム | 数時間以内(研修時間内で実施) |
| 運用 | 新環境で Terraform CD が動く |
| 旧環境 | 確認後 3 日で削除 |
「別サブスクリプション」と「別テナント」は違う
同一 Microsoft アカウント内のサブスクリプション移動と、別の Microsoft Entra ID テナント への移行では、OIDC や RBAC の作り直しが必要です。本記事は後者を前提にしています。
移行方針の検討
| 手段 | メリット | デメリット | 採用 |
|---|---|---|---|
| BACPAC エクスポート/インポート | スキーマ+データ一括 | 別テナントでは手順が増える | × |
sqlpackage |
自動化しやすい | ツール準備に時間がかかる | × |
| Azure Database Migration Service | 本番向け | 小規模 DB にはオーバースペック | × |
| SQL クライアントで INSERT ダンプ | 小規模なら手軽 | Users が大きいとファイルが膨らむ | ○ |
| Geo レプリケーション | ダウンタイム最小 | 別テナントには使えない | × |
データ量は Stores: 約30件 / Users: 数十件 と小規模だったため、Azure Data Studio で INSERT 文を書き出して新 DB に流し込む 方法を採用しました。
公式ドキュメントは「DB の移行」に寄りがちです。PaaS 構成では Blob・App Service 設定・CI 認証 も移行する必要があります。
移行の全体フロー
1. 新テナントに Terraform でインフラ作成
2. schema.sql でスキーマ適用(Blob URL を新ストレージ名に更新)
3. 旧 DB から INSERT 文をエクスポート → 新 DB に投入
4. 旧 Blob から画像を新 Blob へコピー
5. GitHub Secrets / App Service 環境変数を新環境向けに更新
6. アプリを再デプロイして動作確認
7. DNS・カスタムドメインの確認
8. 旧環境リソースの削除
新環境を先に立ててからデータを流し込むと、切り戻しが楽です。
Step 1: 新環境のインフラを立てる(Terraform)
インフラリポジトリ(appNameInfra)の Terraform で、新サブスクリプションに以下を作成します。
- Azure SQL Server / Database
- App Service(API)
- Static Web Apps(フロント)
- Blob Storage(店舗画像)
- Terraform state 用 Storage
- GitHub Actions 用 Managed Identity(OIDC)
az login
az account set --subscription <新サブスクリプションID>
cd appNameInfra/terraform
Copy-Item terraform.tfvars.example terraform.tfvars
# subscription_id, sql_admin_password 等を新環境用に設定
terraform init "-backend-config=backend.hcl" -reconfigure
terraform plan
terraform apply
| 変数 / 設定 | 内容 |
|---|---|
subscription_id |
新サブスクリプション ID |
sql_admin_password |
新 SQL 管理者パスワード |
backend.hcl |
新 tfstate 用 Storage |
apply 後は terraform output で接続情報を取得します。
terraform output -raw sql_server_fqdn
terraform output -raw storage_account_name
terraform output -raw app_service_name
terraform output -raw github_actions_client_id
GitHub Secrets の更新(Infra リポジトリ)
| Secret | 用途 |
|---|---|
AZURE_CLIENT_ID |
OIDC |
AZURE_TENANT_ID |
新テナント ID |
AZURE_SUBSCRIPTION_ID |
新サブスクリプション ID |
TF_VAR_sql_admin_password |
SQL パスワード |
TFSTATE_* |
新 state Storage |
Terraform は Azure 側の OIDC 設定まで作りますが、GitHub Secrets への登録は手動 です。
Step 2: スキーマの適用
Terraform は DB の中身(テーブル・ストアド)までは作りません。schema.sql をクエリエディタまたは sqlcmd で手動実行します。
Stores テーブルには計算列 ImageBlobUrl があり、Blob の URL がスキーマにハードコードされています。ストレージアカウント名が変わるとスキーマ側も更新が必要で、見落とすと画像だけ 404 になります。
-- 変更前(旧環境)
N'https://appNameimg<旧suffix>.blob.core.windows.net/store-images/'
-- 変更後(新環境)
N'https://appNameimg<新suffix>.blob.core.windows.net/store-images/'
ファイアウォール
Portal からクエリエディタで接続する場合、一時的に自分の IP が ClientIPAddress_* として追加されます。
本番接続用には Terraform の sql_firewall_allowed_ips に永続ルールを入れておきます。
(IPが毎回変わるとめちゃめちゃ面倒くさい)
Step 3: データの移行
旧 DB からのエクスポート
Azure Data Studio で旧 DB に接続し、テーブルごとに INSERT 文としてエクスポートしました。
INSERT INTO appName.dbo.Stores
(StoreName, Address, BusinessHours, BudgetMinYen, BudgetMaxYen,
ImageBlobPath, Latitude, Longitude, CreatedAt, UpdatedAt, StoreCategory)
VALUES (...);
Users テーブルは IconBase64 カラムの影響で、エクスポートファイルが約 1.2 MB になりました。
新 DB への投入
SET IDENTITY_INSERT dbo.Users ON;
-- INSERT 文を実行
SET IDENTITY_INSERT dbo.Users OFF;
SELECT COUNT(*) FROM dbo.Users;
SELECT COUNT(*) FROM dbo.Stores;
DB の照合順序は Japanese_CI_AS です。新 DB 作成時に Terraform で同じ設定になっているか確認してください。
Step 4: Blob Storage(店舗画像)の移行
- コンテナ:
store-images - 対象: 約30ファイル
移行方法
ドラッグ&ドロップです...(ぶっちゃけ件数少ないからそれでいいやとか思ってしまった)
確認
ブラウザで直接 URL を開く。
https://<新ストレージ>.blob.core.windows.net/store-images/<ファイル名>
API 経由で店舗一覧を取得し、ImageBlobUrl が新 URL を返すかも確認します。
Step 5: アプリ・CI の切り替え
App Service(Terraform で管理)
| 環境変数 | 内容 |
|---|---|
AZURE_SQL_SERVER |
SQL Server FQDN |
AZURE_SQL_DATABASE |
DB 名 |
AZURE_SQL_USER / AZURE_SQL_PASSWORD
|
認証情報 |
AZURE_STORAGE_CONNECTION_STRING |
Blob 接続 |
ALLOWED_ORIGIN / ALLOWED_ORIGINS
|
CORS 用 |
SQL 接続・Blob 接続・CORS は terraform apply で更新されます。GitHub Secrets は terraform output 後に手動で更新が必要です。
Back / Front の GitHub Secrets
| リポジトリ | Secret | 取得元 |
|---|---|---|
| appNameBack |
AZURE_CLIENT_ID 等 |
terraform output |
| appNameFront | VITE_BACKEND_HOSTNAME |
新 API URL |
| appNameFront | AZURE_STATIC_WEB_APPS_API_TOKEN |
新 SWA |
再デプロイ
main → production の PR マージ
→ Back deploy.yml → 新 App Service
→ Front deploy.yml → 新 Static Web Apps
VITE_BACKEND_HOSTNAME はビルド時に埋め込まれるため、Secret 変更後は Front の再デプロイが必須です。
カスタムドメイン
DNS の CNAME が新 SWA の default hostname を向いているか確認します。
terraform output -raw frontend_custom_domain_cname_target
Step 6: 動作確認と旧環境の停止
- ログイン(Users テーブル)
- 店舗一覧表示(Stores + 画像 URL)
- 近隣店舗検索(ストアドプロシージャ)
-
Infra の
terraform planがNo changes - Back / Front の deploy が成功
- カスタムドメインで本番確認
新環境で 3 日間問題なければ、旧リソースグループを削除します。
ハマったポイントまとめ
SQL だけ移してもアプリは動かない
接続文字列・OIDC・Blob・CORS がすべて変わります。
ImageBlobUrl のハードコード
スキーマ内のストレージ URL 更新漏れで、画像だけ 404 になります。
フロントの環境変数はビルド時固定
SWA の Application settings を変えても効きません。再デプロイが必要です。
Terraform state は新 Storage に分離
旧 state をそのまま使うと、別テナントのリソース ID と不整合になります。
Portal で変更されていた SQL スキーマ
旧 DB には schema.sql にないカラムが追加されていました。移行前に旧 DB のテーブル定義を確認し、差分を schema.sql に反映してから新 DB に適用しました。DB スキーマは Terraform 管理外なので、Portal で変更した場合は SQL ファイルへの反映が必要です。
GitHub Actions OIDC の Secrets 更新漏れ
AZURE_TENANT_ID を旧テナントのままにしていたため、初回の terraform plan が認証エラーになりました。Infra / Back / Front の 3 リポジトリすべての Secrets を洗い替える必要がありました。
本来ならどうしただろうか
| 今回 | 次回なら |
|---|---|
| 手動 INSERT ダンプ | データ量が増えたら sqlpackage で自動化 |
| Blob URL をスキーマに直書き | アプリ側で URL を組み立てる |
| Portal での SQL 変更が口頭のみ | スキーマ変更は PR 必須にする |
おわりに
Azure SQL の移行は、DB のエクスポート/インポートがゴールではなく入口でした。
App Service + Blob + Terraform CD がある構成では、
- 新インフラを Terraform で立てる
- スキーマとデータを移す
- Blob をコピーする
- CI / Secrets を切り替える
という順番が安全でした。
同じような移行を検討している方の参考になれば幸いです。(誤りがありましたらご指摘ください...)
今後もAzureやら開発やらしたら何かしら発信していこうと思っているので何卒何卒...!