MS Projectリソース管理の闇:AD連携で「メールアドレスの不整合」を根絶するアーキテクチャ
プロジェクトマネージャーの机の引き出しで眠る、古びたExcelマクロや、誰も更新しないリソースリスト。
大規模なプロジェクトにおいて、リソースの「電子メールアドレス」の不整合は、致命的なコミュニケーションロス、ひいてはタスク遅延の温床となる。
特にActive Directory(AD)やAzure AD(Entra ID)環境下において、組織変更や人事異動に伴うメールアドレスの変更がMS Project(Project Desktop / Project Server / Project Online)側に即座に反映されないケースは後を絶たない。
今回は、VBA(Visual Basic for Applications)を用いてADから最新のユーザー情報を抽出し、MS Projectのリソースシートへノータイムで一括同期する「プロダクション品質の堅牢な同期ツール」の設計思想と実装を伝授する。
—
なぜ「愚直なリソース更新」は破綻するのか?
多くのエンジニアが陥る罠が、単なるActive DirectoryのLDAP検索結果をループさせ、Projectのリソースを片っ端から`.Name`や`.EmailAddress`で上書きしていくというアプローチだ。
この「素朴な実装」は、以下の理由で必ず本番環境でクラッシュする。
1. 部分一致の罠: ADの「表示名(DisplayName)」とProjectの「リソース名(Name)」の微妙な表記揺れ(例:「山田 太郎」vs「山田 太郎(スペースの違い)」)により、マッチングミスが多発する。
2. オブジェクトのライフサイクルとパフォーマンス: Projectの `ActiveProject.Resources` コレクションへのアクセスは、COM境界を跨ぐため、非常にコストが高い。不必要なループは実行時間を何倍にも膨れ上がらせる。
3. 例外処理の欠如: 退職者など、ADに存在しないがProjectには残っている「幽霊リソース」に遭遇した瞬間、コードはエラーで停止する。
我々が目指すべきは、「一意なキー(メールアドレスのプレフィックスや社員番号など)を軸にした堅牢な突合ロジック」と、「ADSI(Active Directory Service Interfaces)を安全に叩くモジュール設計」である。
—
アーキテクチャ概要
今回のソリューションは、以下の2つのレイヤーで構成する。
1. ADデータ抽出レイヤー: ADSI (`LDAP://`) を使用して、指定されたOU(組織単位)またはドメインから、アクティブなユーザーの「DisplayName(または社員ID)」と「Mail」のペアを高速に取得する。
2. Projectリソース同期レイヤー: 取得した辞書データ(Dictionary)を元に、MS Projectのリソースを走査し、差分がある場合のみ安全にプロパティを更新する。
—
プロダクションコード:AD連携リソース同期モジュール
以下のコードは、エラーハンドリング、パフォーマンス考慮のコレクション操作、そして実務で耐えうるロギングを網羅した完全版のVBAモジュールである。
Option Explicit
‘ ==============================================================================
‘ Module: modsADResourceSync
‘ Description: Active Directoryから最新のメールアドレスを取得し、
‘ MS Projectのリソースシートを高速かつ安全に同期する
‘ ==============================================================================
Public Sub SyncResourceEmailFromAD()
Dim startTime As Double
startTime = Timer
‘ 1. 事前検証
If ActiveProject.Resources.Count = 0 {
MsgBox “対象プロジェクトにリソースが登録されていません。”, vbExclamation, “同期中断”
Exit Sub
}
‘ 2. ADからユーザー情報を取得(Dictionaryに格納)
Dim adDict As Object
Set adDict = GetActiveDirectoryUsers()
If adDict Is Nothing Or adDict.Count = 0 {
MsgBox “Active Directoryからユーザー情報を取得できませんでした。”, vbCritical, “同期エラー”
Exit Sub
}
‘ 3. Projectリソースとの突合・更新処理
Dim res As Resource
Dim updatedCount As Long
Dim searchKey As String
updatedCount = 0
‘ 画面描画を停止し、パフォーマンスを最大化
App.ScreenUpdating = False
On Error GoTo ErrorHandler
For Each res In ActiveProject.Resources
‘ リソースが有効(Nullでない)かつ、コストリソース等を除外する場合の判定
If Not res Is Nothing Then
‘ ここではリソース名をキーとしてADと突合する設計(必要に応じてメールアドレスの前半やIDに変更可)
searchKey = Trim(res.Name)
If adDict.Exists(searchKey) Then
Dim newEmail As String
newEmail = adDict(searchKey)
‘ メールアドレスが異なっている場合のみ更新(無駄な書き込みを排除)
If res.EmailAddress <> newEmail Then
res.EmailAddress = newEmail
updatedCount = updatedCount + 1
Debug.Print “Updated: ” & searchKey & ” -> ” & newEmail
End If
End If
End If
Next res
App.ScreenUpdating = True
‘ 4. 完了通知
MsgBox “Active Directoryとの同期が完了しました。” & vbCrLf & _
“更新されたリソース数: ” & updatedCount & ” 件” & vbCrLf & _
“処理時間: ” & Format(Timer – startTime, “0.00”) & ” 秒”, _
vbInformation, “同期成功”
Exit Sub
ErrorHandler:
App.ScreenUpdating = True
MsgBox “予期せぬエラーが発生しました: ” & Err.Description, vbCritical, “致命的エラー”
End Sub
‘ ——————————————————————————
‘ Private Function: GetActiveDirectoryUsers
‘ ADSIを使用して、ドメインから「表示名」と「メールアドレス」のペアを抽出
‘ ——————————————————————————
Private Function GetActiveDirectoryUsers() As Object
Dim objRootDSE As Object
Dim strDomain As String
Dim objConnection As Object
Dim objCommand As Object
Dim objRecordSet As Object
Dim dict As Object
Set dict = CreateObject(“Scripting.Dictionary”)
On Error GoTo ADError
‘ ドメインのルートを取得
Set objRootDSE = GetObject(“LDAP://RootDSE”)
strDomain = objRootDSE.Get(“defaultNamingContext”)
Set objConnection = CreateObject(“ADODB.Connection”)
Set objCommand = CreateObject(“ADODB.Command”)
objConnection.Provider = “ADsDSOObject”
objConnection.Open “Active Directory Provider”
Set objCommand.ActiveConnection = objConnection
‘ 抽出クエリ(有効なアカウントかつメールアドレスを持つユーザーに限定)
objCommand.CommandText = _
“SELECT displayName, mail FROM ‘LDAP://” & strDomain & “‘ ” & _
“WHERE objectCategory=’person’ AND objectClass=’user’ ” & _
“AND mail=” AND displayName=””
objCommand.Properties(“Page Size”) = 1000
objCommand.Properties(“Timeout”) = 30
objCommand.Properties(“Cache Results”) = False
Set objRecordSet = objCommand.Execute
Do Until objRecordSet.EOF
Dim dName As String
Dim mailAddr As String
If Not IsNull(objRecordSet.Fields(“displayName”).Value) And _
Not IsNull(objRecordSet.Fields(“mail”).Value) Then
dName = Trim(objRecordSet.Fields(“displayName”).Value)
mailAddr = Trim(objRecordSet.Fields(“mail”).Value)
‘ 辞書に格納(重複キーの競合を防ぐため存在チェック)
If Not dict.Exists(dName) Then
dict.Add dName, mailAddr
End If
End If
objRecordSet.MoveNext
Loop
objRecordSet.Close
objConnection.Close
Set GetActiveDirectoryUsers = dict
Exit Function
ADError:
Debug.Print “AD Error: ” & Err.Description
Set GetActiveDirectoryUsers = Nothing
End Function
—
現場で絶対に押さえておくべき「3つのエンジニアリング知見」
1. `App.ScreenUpdating = False` による高速化
MS Projectで数千件のリソースをループ処理する際、UIの再描画が挟まると実行速度が桁違いに低下する。処理の開始時に画面描画を切り、終了時に戻すお作法は、プロジェクト自動化における鉄則である。
2. ADSIクエリの最適化(条件の絞り込み)
今回のADOクエリでは、`mail=”` と `displayName=”` を指定し、「メールアドレスを持たないサービスアカウントや無効なオブジェクト」を最初からフェッチ対象外にしている。これにより、VBA側のメモリ消費を抑え、ネットワーク帯域の無駄な消費を防いでいる。
3. 「変更がある場合のみ」書き込むデザイン
リソースオブジェクトのプロパティ(`res.EmailAddress`)への書き込みは、裏でトランザクションやダーティフラグの検知が発生するため、コストの高い操作である。
`If res.EmailAddress <> newEmail Then` というガード節を設けることで、変更のないリソースへの無駄な書き込みを完全にバイパスしている。
—
まとめ
リソース管理は地味だが、プロジェクトの成否を握る根幹のデータ基盤である。
「人が手動でコピペする」「誰もメンテしない」という属人化した運用をコードによって駆逐し、インフラストラクチャ(Active Directory)とプロジェクト計画ツール(MS Project)をシームレスに結合すること。
それこそが、現場を支える真の業務自動化エンジニアの仕事である。
