Azure DevOps と Microsoft Entra ID 連携で「UPN が重複している」エラーを解消する完全ガイド|見えない重複の洗い出しとPowerShell手順

既存の Azure DevOps 組織を Microsoft Entra ID に接続しようとした際に「UPN が重複している」と判定され、テナント切り替えが失敗する事象は、目視や単純な Graph 取得では見つからない“見えない重複”が原因で起こります。本記事では、再現しやすい落とし穴を回避しつつ、PowerShell と Azure DevOps の両面から重複を洗い出し、確実に解消して接続を完了させるための実践的な手順をまとめます。

目次

Azure DevOps と Entra ID 接続時に表示されるエラーの正体

Azure DevOps の組織を Microsoft Entra ID(旧 Azure AD)に関連付ける操作中、次のようなメッセージで失敗することがあります。

Tenant switch failed ... 2 of 36 total users has multiple active identities with the same UPN

これは Azure DevOps が、同一の UPN(ユーザー プリンシパル名)を持つ「複数のアクティブな ID」を検知したことを意味します。ここでいう「複数の ID」は、以下のような組み合わせで発生します。

  • Entra ID 内に同一 UPN を持つユーザーが複数存在(メンバー+ゲスト、重複招待など)
  • Entra ID の ソフト削除(Soft-Deleted) ユーザーが同一 UPN のまま残存
  • Azure DevOps 側に MSA(Microsoft アカウント)と Entra ID アカウントの 両方 が同じ UPN で登録

重要なのは、Entra ID 側・Azure DevOps 側の両方で重複を解消してから再度テナント接続を行うことです。どちらか一方の整理だけでは同じエラーに戻りがちです。

先に結論:確認すべきポイントの全体像

観点確認場所目的チェックの例対処の要点
UPN の一意性(アクティブ)Entra ID同一 UPN のユーザーが複数いないかPowerShell で全ユーザーを取得して UPN でグルーピング重複ユーザーの削除 or UPN リネーム
ソフト削除Entra ID削除済みだが保留中のユーザーが UPN を確保していないか削除済みディレクトリ オブジェクトを抽出完全削除(ハード デリート)で UPN を解放
ゲスト重複Entra IDゲスト(UserType = Guest)での二重招待ゲストのみ抽出し UPN を確認不要なゲスト削除 or UPN の一意化
MSA と職場/学校アカウントの混在Azure DevOps同じ UPN が MSA と Entra ID の両方で登録されていないかDevOps のユーザー一覧を「origin」で分類MSA 側を除外し Entra ID に統一
オンプレ同期の制約Entra ID(ハイブリッド)UPN リネームの可否(オンプレマスターか)OnPremisesSyncEnabled を確認オンプレ AD で UPN 変更→同期
DevOps 側キャッシュブラウザー / DevOps変更が UI に反映されないサインアウト・キャッシュ削除・再サインイン数分~十数分後に再確認

Entra ID で「見えない重複」を確実に洗い出す(PowerShell 実践)

Microsoft Graph PowerShell SDK を用いると、UI に現れないソフト削除やゲストも含めて網羅的に確認できます。はじめに権限を付与して接続します。

# 必要に応じてインストール
# Install-Module Microsoft.Graph -Scope CurrentUser

# Directory.Read.All / User.Read.All などの権限を要求

Connect-MgGraph -Scopes "User.Read.All","Directory.Read.All"
Select-MgProfile -Name "v1.0"

すべてのユーザーを取得し、UPN 単位で重複を抽出

$users = Get-MgUser -All -Property Id,DisplayName,UserPrincipalName,UserType,AccountEnabled,OnPremisesSyncEnabled

# UPN 正規化(大小文字・空白の揺れを排除)

$normalize = { param($s) if([string]::IsNullOrWhiteSpace($s)){return $null} ($s -replace '\s','').ToLowerInvariant() }

$reportActive = $users | ForEach-Object {
[PSCustomObject]@{
Source = "Active"
Id = $*.Id
DisplayName = $*.DisplayName
UserPrincipalName = $*.UserPrincipalName
NormalizedUpn = &$normalize $*.UserPrincipalName
UserType = $*.UserType
OnPremisesSyncEnabled = $*.OnPremisesSyncEnabled
}
}

$dupActive = $reportActive | Group-Object NormalizedUpn | Where-Object { $*.Name -and $*.Count -gt 1 }
$dupActive | ForEach-Object {
"=== DUP UPN (Active): $($*.Name) ==="
$*.Group | Format-Table Id,UserPrincipalName,DisplayName,UserType,OnPremisesSyncEnabled
}

削除済み(Soft-Deleted)ユーザーも含めて確認

削除後 30 日はディレクトリに残るため、アクティブ+削除済みの合算で重複が出ていないかを調べます。

$deleted = Get-MgDirectoryDeletedItem -All | Where-Object {
  $_.AdditionalProperties.'@odata.type' -eq '#microsoft.graph.user'
} | ForEach-Object {
  [PSCustomObject]@{
    Source = "Deleted"
    Id = $_.Id
    DisplayName = $_.AdditionalProperties.displayName
    UserPrincipalName = $_.AdditionalProperties.userPrincipalName
    NormalizedUpn = &$normalize $_.AdditionalProperties.userPrincipalName
    UserType = "Deleted"
  }
}

$combined = @()
$combined += $reportActive
$combined += $deleted

$dupAny = $combined | Group-Object NormalizedUpn | Where-Object { $*.Name -and $*.Count -gt 1 }
$dupAny | ForEach-Object {
"=== DUP UPN (Active+Deleted): $($*.Name) ==="
$*.Group | Sort-Object Source | Format-Table Source,Id,UserPrincipalName,DisplayName,UserType
}

ゲスト(B2B)ユーザーの偏りを可視化

$guests = Get-MgUser -All -Filter "userType eq 'Guest'" -Property Id,DisplayName,UserPrincipalName
$guests | Sort-Object UserPrincipalName | Format-Table DisplayName,UserPrincipalName

CSV レポートを保存(証跡・レビュー用)

$combined | Export-Csv -Encoding UTF8 -NoTypeInformation -Path ".\upn_inventory.csv"

重複を解消する(削除/リネーム)

重複が確認できたら、次のいずれかで一意化します。

  • 不要アカウントの完全削除(アクティブ)
    Remove-MgUser -UserId <ObjectId>
  • 削除済みユーザーのハード デリート
    Remove-MgDirectoryDeletedItem -DirectoryObjectId <ObjectId>
  • UPN のリネーム
    Update-MgUser -UserId <ObjectId> -UserPrincipalName <newUPN>
    ※ OnPremisesSyncEnabled が True の場合は、オンプレ AD で UPN を変更→同期してください。

Azure DevOps 側:MSA/Entra の二重登録や重複を整理

Entra ID を整えただけでは不足です。Azure DevOps 側にも同じ UPN のユーザーが複数(たとえば origin が aad と msa で二重)存在するとエラーの温床になります。

ポータルでの目視確認

  • DevOps ポータル → Organization Settings → Users
  • 同一 UPN(メールアドレス)が 2 行以上ないか、Identity / Origin が異なるものが混在していないか確認
  • 不要な MSA ユーザーを Remove from organization で削除

Azure DevOps REST/CLI で機械的に洗い出す

ユーザー数が多い場合、PAT(個人用アクセストークン)を用いてスクリプトで重複を検知します。

$org = "https://vssps.dev.azure.com/<organization>"
$patPlain = "<your PAT>"
$base64 = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$patPlain"))
$headers = @{ Authorization = "Basic $base64" }

# Graph Users(DevOps の ID ストア)を取得

$resp = Invoke-RestMethod -Headers $headers -Method Get -Uri "$org/_apis/graph/users?api-version=7.1-preview.1"
$rows = $resp.value | Select-Object subjectKind,origin,principalName,mailAddress,displayName,descriptor

# UPN(principalName)で重複抽出

$dupDevOps = $rows | Group-Object principalName | Where-Object { $*.Name -and $*.Count -gt 1 }

"=== DUP in Azure DevOps (by principalName) ==="
$dupDevOps | ForEach-Object {
$_.Group | Sort-Object origin | Format-Table origin,principalName,displayName,descriptor
}

ユーザー除外はポータル操作が安全ですが、CLI を使う場合は次の例が参考になります。

# 例: Azure DevOps CLI(拡張機能)でユーザーを組織から削除
# 事前に: az extension add --name azure-devops
# サインイン: az devops configure --defaults organization=https://dev.azure.com/&lt;organization&gt;
# 実行:
# az devops user remove --user [email protected]

MSA と Entra ID が同じ UPN で並存している場合は、MSA 側を削除し、Entra ID 側のユーザーだけを残すのが原則です。グループ招待を使っている場合は、Entra ID グループに集約して管理負荷を下げます。

再接続までのランブック(手順書)

  1. 現状のバックアップ
    Entra ID のユーザー在庫(上記 CSV)と、Azure DevOps のユーザー一覧をエクスポートして保存。
  2. アクティブ重複の解消(Entra ID)
    同一 UPN のアクティブ ユーザーを一意化(削除 or リネーム)。オンプレ同期ユーザーはオンプレで UPN を変更。
  3. ソフト削除の排除
    Soft-Deleted に同じ UPN が存在する場合は Remove-MgDirectoryDeletedItem でハード デリート。
  4. ゲストの整理
    不要なゲストを削除、または UPN/招待先メールを一意化。#EXT# 付き UPN のバリエーションに注意。
  5. Azure DevOps の重複除去
    Users 画面で MSA と AAD の二重登録を解消。不要な MSA を除去。
  6. ブラウザー/キャッシュのクリア
    一度サインアウト→キャッシュ削除→再サインイン。別ブラウザー/プライベート ウィンドウで再試行。
  7. テナント接続を再実行
    「Connect to Microsoft Entra ID」を再度実行。
  8. 検証
    組織設定の Users と、プロジェクトのメンバーシップが期待どおりか確認。ビルド サービス アカウント等の権限も再チェック。

よくある原因と具体的対策(まとめ表)

原因症状のヒント対策
ゲストとメンバーで同一 UPN同姓同名の外部招待を繰り返し、#EXT# を含む UPN が二重化不要なゲストを削除、または UPN を [email protected] などに変更
ソフト削除が残存UI に見えないが重複判定に残るRemove-MgDirectoryDeletedItem で完全削除(回収期間の待機を避ける)
MSA と Entra ID の混在DevOps の origin が msa と aad で同じ principalNameDevOps から MSA を削除し、Entra ID に統一
オンプレ同期ユーザーの直接変更Graph で UPN 更新が失敗オンプレ AD で UPN 変更 → 同期を待つ
大小文字や空白の差異目視では気づきにくいスクリプトで正規化(小文字化+空白除去)して比較

検証コマンド:重複ゼロの状態を数式で確認

# アクティブ ユーザーの UPN 一意性を数で担保
$users = Get-MgUser -All -Property UserPrincipalName
$normalize = { param($s) if([string]::IsNullOrWhiteSpace($s)){return $null} ($s -replace '\s','').ToLowerInvariant() }
$normalized = $users | ForEach-Object { &$normalize $_.UserPrincipalName } | Where-Object { $_ }
$total = $normalized.Count
$distinct = ($normalized | Select-Object -Unique).Count
"Active users: $total / Distinct UPN: $distinct / Duplicates: " + ($total - $distinct)

# 削除済みを含めた一意性

$deleted = Get-MgDirectoryDeletedItem -All | Where-Object {
$*.AdditionalProperties.'@odata.type' -eq '#microsoft.graph.user'
} | ForEach-Object {
&$normalize $*.AdditionalProperties.userPrincipalName
} | Where-Object { $_ }

$all = $normalized + $deleted
$totalAll = $all.Count
$distinctAll = ($all | Select-Object -Unique).Count
"Active+Deleted users: $totalAll / Distinct UPN: $distinctAll / Duplicates: " + ($totalAll - $distinctAll)

上記の重複数が 0 になってから Azure DevOps で接続を再試行すると、失敗確率を大幅に下げられます。

運用のコツ:再発防止のために今すぐできること

  • UPN 命名ポリシーの固定化:社外ゲストは接尾辞や接頭辞(例:.guest / _ext)で一意化。
  • 定期健診の自動化:本記事の重複検出スクリプトをスケジュール実行し、レポートを運用チームに配信。
  • DevOps への招待経路を一本化:必ず Entra ID アカウントで招待し、MSA を使わないルールを明文化。
  • 削除のライフサイクル管理:退職・契約終了の際は、Entra ID 側の完全削除までを Runbook 化。
  • ハイブリッド環境ではオンプレ優先:同期ユーザーの属性変更はオンプレ AD で行う運用を徹底。

トラブルに強くなるための追加知識

  • UPN とサインイン ID の違い:B2B ゲストでは #EXT# 付きの内部 UPN が生成され、外部メールと乖離します。内部 UPN の一意性で判定される点に注意。
  • DevOps の ID モデル:origin(例:aad / msa)+principalName で実体を表し、同じ principalName が別 origin に存在すると衝突します。
  • 一時的なレプリケーション遅延:Entra ID の削除・リネームは即時ではありません。作業はまとまった単位で行い、最後に接続を試すのが効率的です。

まとめ

「UPN が重複している」エラーは、Entra ID(アクティブ/ソフト削除/ゲスト)とAzure DevOps(MSA 混在)の双方にまたがる重複が原因であることがほとんどです。本記事のスクリプトとチェックリストで見えない重複を可視化し、不要な ID の完全削除や UPN の一意化、MSA の排除を徹底すれば、テナント接続は正常に完了します。最後に、ブラウザー キャッシュの影響を避けるためのサインアウト/再サインインも忘れずに実施してください。

付録:クイック・ステップ(要点だけ読みたい方向け)

  1. Graph PowerShell でアクティブ ユーザーを取得 → UPN 正規化で重複抽出
  2. 削除済みユーザー(Soft-Deleted)を取得 → 同じ UPN が残っていないか確認
  3. ゲスト(UserType=Guest)をリストアップ → 重複/不要招待を削除
  4. オンプレ同期ユーザーの UPN はオンプレ AD で変更 → 同期完了まで待機
  5. Azure DevOps で MSA を除去し、Entra ID に統一
  6. サインアウト/キャッシュ削除後、テナント接続を再実行

参考スニペット:Entra ID & DevOps をまとめて監査

# 1) Entra ID の UPN インベントリ(アクティブ+削除済み)
Connect-MgGraph -Scopes "User.Read.All","Directory.Read.All"
$normalize = { param($s) if([string]::IsNullOrWhiteSpace($s)){return $null} ($s -replace '\s','').ToLowerInvariant() }

$active = Get-MgUser -All -Property Id,DisplayName,UserPrincipalName,UserType |
Select-Object @{N='Source';E={'Active'}},Id,DisplayName,UserPrincipalName,UserType,@{N='NormalizedUpn';E={&$normalize $_.UserPrincipalName}}

$deleted = Get-MgDirectoryDeletedItem -All | Where-Object {
$*.AdditionalProperties.'@odata.type' -eq '#microsoft.graph.user'
} | ForEach-Object {
[PSCustomObject]@{
Source = 'Deleted'
Id = $*.Id
DisplayName = $*.AdditionalProperties.displayName
UserPrincipalName = $*.AdditionalProperties.userPrincipalName
UserType = 'Deleted'
NormalizedUpn = &$normalize $_.AdditionalProperties.userPrincipalName
}
}

$allUpn = $active + $deleted
$dupUpn = $allUpn | Group-Object NormalizedUpn | Where-Object { $*.Name -and $*.Count -gt 1 }
$dupUpn | ForEach-Object {
"UPN DUP: $($*.Name)"
$*.Group | Format-Table Source,UserPrincipalName,DisplayName,UserType
}

# 2) Azure DevOps の Graph Users をチェック(PAT 必要)

$org = "[https://vssps.dev.azure.com/<organization](https://vssps.dev.azure.com/<organization)>"
$pat = ""
$auth = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$pat"))
$headers = @{ Authorization = "Basic $auth" }
$gu = Invoke-RestMethod -Headers $headers -Method Get -Uri "$org/*apis/graph/users?api-version=7.1-preview.1"
$dupDevOps = $gu.value | Group-Object principalName | Where-Object { $*.Name -and $*.Count -gt 1 }
$dupDevOps | ForEach-Object {
"DevOps DUP principalName: $($*.Name)"
$_.Group | Sort-Object origin | Format-Table origin,principalName,displayName,descriptor
}

注意事項(安全に作業するために)

  • 削除操作は元に戻せません。必ず事前にエクスポートし、対象と影響範囲をレビューしてください。
  • UPN の変更はサインインに直結します。実施時間帯・関係者への周知・切替手順を事前に準備してください。
  • CI/CD サービス接続やビルド/リリース パイプラインの実行 ID に影響しないかを確認してください。

この手順を実施しても解決しない場合

UPN に重複がなく、DevOps 側の MSA も除外済みなのに失敗する場合は、一時的なディレクトリ整合性や古い招待のメタデータが原因のことがあります。すべての変更を反映させるためにサインアウト/キャッシュ削除/別ブラウザーでの再実行を行い、それでも再現する場合は、アクティブ+削除済み+DevOps の 3 点で再度 UPN 衝突がないかをレポートで突き合わせてください。

最終チェックリスト(接続直前)

  • Entra ID のアクティブ ユーザー:同一 UPN なし
  • Entra ID のソフト削除:同一 UPN なし(必要に応じて完全削除済み)
  • ゲスト:不要招待なし、UPN 一意
  • Azure DevOps:MSA と AAD の二重登録なし
  • オンプレ同期ユーザーの UPN 変更はオンプレで完了 → 同期済み
  • 管理者アカウントで DevOps 組織の所有者権限を保持
  • ブラウザー キャッシュ/セッションのクリア済み

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次