Windows Server 2016でタスク スケジューラからPowerShellを実行すると、履歴は完了しているのにAD削除などの変更が反映されないことがあります。原因は権限だけでなく、実行ユーザー・パス・TLS/モジュール・ファイルのブロックなど手動実行との差分に潜みます。戻り値0x80070001(2147942401)を手掛かりに、切り分けと解決手順をまとめます。
結論:原因は1つとは限らないため「実行環境差分」を潰すのが最短ルート
タスク スケジューラ実行で「成功っぽく見えるのに何も起きない」現象は、スクリプトのロジック不具合よりも、実行環境の違いで処理が途中停止しているケースが大半です。特に次の要素は、手動実行と差が出やすく、しかも表面化しにくいので優先して疑います。
- 実行ユーザー(ログオンユーザー vs サービスアカウント/SYSTEM)
- モジュール(インストール場所、バージョン、Importの成否)
- TLS 1.2(PowerShellGet/PowerShell Gallery/Graph接続で詰まる)
- パス(作業フォルダー、相対パス、ネットワークドライブ、32/64bit)
- ブロック/実行ポリシー(Unblock-File、署名要件、AppLocker)
今回のケースでは、TLS 1.2 を使えない状態+古い PowerShellGet(などの依存モジュール)が重なり、必要モジュールの導入・読み込みが失敗していたことが本命でした。結果として、Entra ID(旧Azure AD)側の取得や、関連コマンドレットが動かず、削除処理まで到達していない、という流れです。
質問概要:Windows Server 2016で「Entra ID抽出→オンプレAD削除」を定期実行したい
やりたいことは、次のような「クラウド→ファイル→オンプレ変更」のパイプラインです。どこか1点でも崩れると、最終的な削除が走らない(または対象ゼロ扱いになる)ため、“完了したのに何も変わらない”ように見えます。
| ステップ | 目的 | スケジュール実行で起きがちな失敗 | 結果の見え方 |
|---|---|---|---|
| Entra IDから抽出 | 90日以上チェックインしていないデバイスを取得し、ファイルへ書き出す | Graph/Entra接続に失敗(TLS/モジュール/認証)、プロキシ差、証明書ストア差 | 出力ファイルが空・作られない/以降の処理がスキップ |
| ファイルを読み込み | DeviceID一覧を読み込む | パスが違う(作業フォルダー/相対パス)、ネットワークドライブが存在しない | 対象ゼロ扱いになり「何も起きない」 |
| オンプレADを変更 | 該当デバイス(Computerオブジェクト)を削除または無効化する | 権限不足、RSAT/ActiveDirectoryモジュール未導入、Confirm待ち、実行ポリシー | 変更されない/途中で止まるがタスクUIでは気づきにくい |
戻り値 2147942401 / 0x80070001 と Last Run Result 0x1 の関係
この手のトラブルで混乱しやすいのが、「タスクは完了」と表示される一方で、Last Run Result が 0x1だったり、ログ上は 2147942401(0x80070001)が出たりする点です。まずは関係を整理します。
| 表示/値 | 意味合い | 補足 |
|---|---|---|
| Last Run Result: 0x0 | タスクのプロセス終了コードが0(成功) | PowerShellの内部で例外が出ても、握りつぶして0で終了すると「成功」に見えます |
| Last Run Result: 0x1 | 終了コード1(一般的な失敗) | 履歴イベントは「アクション完了」と出ることがあり、見落としやすいポイントです |
| 2147942401 | 10進数表記のHRESULT | 16進にすると 0x80070001 で、下位は「1」を示します |
| 0x80070001 | ERROR_INVALID_FUNCTION(機能が無効/不正)系 | 原因を特定するには「何が失敗したか」をログで掘る必要があります |
重要なのは、これらのコードは「どこで何が失敗したか」までは教えてくれないという点です。だからこそ、次章の「実行主体の一致」と「ログの可視化」が近道になります。
まず「手動実行」と「スケジュール実行」を同じ条件に揃える
手動実行が成功しているのに、スケジュール実行だけ失敗する場合、最優先で確認すべきは実行アカウントです。PowerShellは「同じサーバー上」でも、実行ユーザーが違うだけで挙動が大きく変わります。
| 差分ポイント | ログオンユーザーでの手動実行 | タスク(サービスアカウント/SYSTEM) | 典型的な影響 |
|---|---|---|---|
| 権限(AD操作/ファイル/レジストリ) | 管理者として実行しているつもりでも、昇格していないことがある | 「最上位の特権」未チェックだと権限不足になりやすい | 削除やモジュール導入で失敗 |
| 証明書ストア | CurrentUserに証明書/秘密鍵がある | 別ユーザーなので参照できない | Graphの証明書認証が失敗 |
| PSModulePath | ユーザー単位で入れたモジュールが見える | 見えない(モジュール不足) | Import-Module失敗→コマンドレット未定義 |
| ネットワーク到達性/プロキシ | ユーザーのプロキシ設定が有効 | WinHTTPプロキシが未設定など | モジュール取得/API通信が失敗 |
| トークン/キャッシュ | 手動ログインのキャッシュが残る | 非対話実行では対話ログインできない | Connect系コマンドが失敗・停止 |
サービスアカウントでログオンして手動実行し、同じ失敗が再現できるかをまず確認してください。ここで再現するなら、タスク設定ではなく「アカウント/環境」の問題です。逆に成功するなら、タスク設定(引数・作業フォルダー・32/64bit)に絞れます。
失敗点を可視化する:スクリプトに“必ず”ログを残す
タスク スケジューラ実行は、画面にエラーが出ないぶん、失敗が埋もれます。どこで止まっているかさえ分かれば、原因特定は急に簡単になります。運用スクリプトとしては、最低限次の3点を入れるのがおすすめです。
- トランスクリプト(Start-Transcript / Stop-Transcript)
- 例外を握りつぶさない($ErrorActionPreference = ‘Stop’)
- 終了コードを明確に返す(成功は0、失敗は1以上)
例として、先頭に以下を入れるだけでも「モジュールが無い」「TLSで接続できない」「ファイルが読めない」などがログに残り、原因が見えるようになります。
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
Set-StrictMode -Version Latest
$logRoot = 'C:\\Logs\\DeviceCleanup'
New-Item -ItemType Directory -Force -Path $logRoot | Out-Null
$ts = Get-Date -Format 'yyyyMMdd_HHmmss'
$logPath = Join-Path $logRoot ("run_{0}.log" -f $ts)
Start-Transcript -Path $logPath
$exitCode = 0
try {
Write-Output ("WHOAMI: {0}" -f (whoami))
Write-Output ("PSVersion: {0}" -f $PSVersionTable.PSVersion)
Write-Output ("Bitness: {0}" -f ([IntPtr]::Size * 8))
Write-Output ("Script: {0}" -f $PSCommandPath)
Write-Output 'STEP: start'
# ここに本処理(Import-Module / Entra取得 / ファイル出力 / AD削除)
Write-Output 'STEP: end'
}
catch {
$exitCode = 1
Write-Error $_
}
finally {
Stop-Transcript
}
exit $exitCode
ログはまずローカルディスク(例:C:\\Logs)に書き出すのが安全です。ネットワーク共有に直接出すと、共有権限や到達性の問題が混ざって、さらに切り分けが難しくなります。
タスク スケジューラ側の設定チェック:ここがズレると必ずハマる
次は、タスク定義の基本項目です。PowerShellスクリプトを安定運用したいなら、「実行ファイルの場所」「引数」「開始(作業)フォルダー」の3点は必ず押さえます。
| 設定項目 | 推奨 | 理由 | チェックのコツ |
|---|---|---|---|
| プログラム/スクリプト | C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe | 64bitで動かし、モジュール参照ズレを防ぐ | SysWOW64を指定していないか確認 |
| 引数の追加 | -NoProfile -ExecutionPolicy Bypass -File “C:\\Scripts\\DeviceCleanup.ps1” | プロファイル依存を排除し、実行ポリシーで止まるのを回避 | パスに空白があるなら必ずダブルクォーテーション |
| 開始(作業)フォルダー | C:\\Scripts | 相対パスやログ出力の基準がズレるのを防ぐ | スクリプト内で$PSScriptRootを使うのも有効 |
| 最上位の特権で実行する | チェック | AD削除やモジュール導入などで権限不足になりやすい | サービスアカウントでも必要 |
| ユーザーがログオンしているかどうかにかかわらず実行 | 用途に応じて選択(定期実行なら推奨) | 非対話で回す前提を作る | 対話ログイン必須の処理が混ざっていないか確認 |
さらに確実にするなら、まずは「スクリプトではなくPowerShell自体が起動できているか」を切り分けます。例えば、次のような“超短いコマンド”を一度タスクで実行し、ファイルが作られるかを確認します。
-NoProfile -Command "whoami | Out-File C:\\Logs\\task_whoami.txt -Force"
これでファイルが作られないなら、スクリプト以前にタスク設定(実行権限、パス、作業フォルダー)が原因です。作られるなら、次はスクリプト内部(モジュール、TLS、ファイルパス)に絞れます。
原因別の早見表:一番よくある“ハマり所”と対策
| よくある原因 | 起きる症状 | 確認ポイント | 対策 |
|---|---|---|---|
| モジュールが不足/別ユーザーにだけ入っている | コマンドレットが見つからない、途中で止まる | タスク実行ユーザーでGet-Module -ListAvailable | -Scope AllUsersで導入、Import-Moduleを明示 |
| TLS 1.2が使えない/使われていない | Install-ModuleやAPI接続が失敗 | ログに接続エラーが出るか | [Net.ServicePointManager]::SecurityProtocolをTls12に設定、必要に応じて更新 |
| 対話ログイン前提の認証 | タスクではサインイン画面が出ず失敗 | Connect系がDeviceCode/Interactiveになっていないか | 証明書/アプリ登録など非対話認証に切り替える |
| ネットワークドライブ/相対パス | ファイルが読めず対象ゼロ | 入力/出力ファイルの実体をログで確認 | UNC/フルパス、開始(作業)フォルダー固定 |
| SYSTEM実行によるネットワーク権限差 | 共有にアクセスできない | 共有側ログで拒否されていないか | 専用サービスアカウントに変更、共有権限を付与 |
本命パターン:TLS 1.2 未設定+古い PowerShellGet でモジュール導入/読み込みが失敗する
Entra IDやMicrosoft Graph関連の処理は、モジュールやWeb通信に依存します。ここでTLS 1.2が使えない(またはPowerShellがTLS 1.0で通信しようとする)状態だと、次のような連鎖で「何も起きない」になります。
- 必要なモジュール(例:Microsoft.Graph / AzureAD / ActiveDirectory など)が入っていない
- 入れようとしても PowerShellGet/PackageManagement が古くて取得に失敗する
- Import-Module が失敗し、以降のコマンドレットが存在しない
- エラーがログに出ず、対象ゼロ扱い・スキップで終わったように見える
モジュールを「明示的にImport」して沈黙をなくす
スクリプトの先頭で、依存モジュールを明示的にImportし、失敗したら即座に落とすようにすると、沈黙がなくなります。
# 例:依存モジュールを明示的に読み込む(失敗したら止める)
Import-Module ActiveDirectory -ErrorAction Stop
Import-Module Microsoft.Graph -ErrorAction Stop
TLS 1.2 を強制して「取得/接続できる状態」を作る
PowerShell 5.1環境では、通信の既定がTLS 1.0寄りになっているケースがあります。まずはスクリプトの先頭でTLS 1.2を明示するだけでも改善することがあります。
# 例:PowerShellセッションでTLS 1.2を使用
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
PowerShellGet/PackageManagementを更新してから必要モジュールを揃える
「タスクで毎回Install-Module」は避け、事前にサーバーへ必要モジュールを揃えておくのが運用上のセオリーです(インターネット到達性やプロキシで毎回ブレるため)。更新・導入の一例は次の通りです。
# NuGetプロバイダーを準備(未導入の場合)
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force
# PowerShellGetを更新(PowerShell 5.1想定)
Install-Module PowerShellGet -Force -AllowClobber
# 必要モジュールを「全ユーザー」で使えるように入れる(例)
Install-Module Microsoft.Graph -Scope AllUsers -Force
ポイントは、タスク実行のアカウントがモジュールにアクセスできるよう、-Scope AllUsersを基本にすることです。ユーザー単位(CurrentUser)で入っていると、手動では動くのにタスクでは見えない、が発生します。
Entra ID(Microsoft Graph)をタスクで回すときの認証ポイント
手動実行で「とりあえずサインインして動いた」スクリプトは、タスクにすると失敗しがちです。なぜなら、タスクは非対話実行になりやすく、サインイン画面やデバイスコード入力ができないためです。定期実行するなら、非対話で成立する認証に寄せるのが安全です。
- 推奨:アプリ登録(サービスプリンシパル)+証明書(またはシークレット)で接続
- 避けたい:手動サインイン前提(Interactive/DeviceCode)の接続方法
例として、証明書の拇印を使って接続する形にすると、タスクでも安定しやすくなります(証明書はタスク実行ユーザーが参照できるストアに配置します)。
# 例:証明書認証でGraphへ接続(値は環境に合わせて設定)
$tenantId = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
$clientId = 'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy'
$thumbprint = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
Connect-MgGraph -TenantId $tenantId -ClientId $clientId -CertificateThumbprint $thumbprint -NoWelcome
「どの証明書ストアに入れるか」「秘密鍵のアクセス権がタスク実行ユーザーに付いているか」で失敗しやすいので、ここもログで確認できるようにしておくと安心です。
ネットワークドライブは避ける:ファイル入出力はUNCパスが安全
タスク実行では、エクスプローラーで見えているドライブレター(例:Z:)が存在しないことがよくあります。ファイル出力・入力で詰まっていると、対象ゼロ扱いになり、やはり「何も起きない」に見えます。
| やりがち | なぜダメになりやすいか | 推奨 |
|---|---|---|
| Z:\\Share\\devices.csv | 別アカウント/非対話実行ではドライブが割り当てられていない | \\\\server\\share\\devices.csv(UNCパス) |
| 相対パス(.\\output\\devices.csv) | 開始(作業)フォルダーが違うと参照先が変わる | C:\\Scripts\\output\\devices.csv または $PSScriptRoot基準 |
さらに安定させたい場合は、スクリプト冒頭で作業ディレクトリを固定します。
# 作業ディレクトリをスクリプトの場所に固定
Set-Location -Path $PSScriptRoot
スクリプトファイルのブロック解除:SYSTEM/非対話で効くことがある
ブラウザーやメール添付から取得したps1は、Zone情報(いわゆる「ブロック」)が付いていることがあります。手動実行では気づかないのに、タスク(特にSYSTEM)で実行すると挙動が不安定になることがあるため、運用前に解除しておくと安心です。
# 例:ダウンロード由来のブロックを解除
Unblock-File -Path "C:\\Scripts\\DeviceCleanup.ps1"
「成功っぽい」を潰す:運用スクリプトに必要な堅牢化ポイント
タスク スケジューラ運用では、失敗を失敗として返すことが最重要です。次のような“運用向けの作法”を入れておくと、再発時の調査コストが激減します。
| 堅牢化ポイント | 具体例 | 得られる効果 |
|---|---|---|
| 例外を止める | $ErrorActionPreference = ‘Stop’ | 途中で壊れても先へ進まず、原因をログに残せる |
| 重要ステップごとに目印 | Write-Output ‘STEP: …’ | どこまで進んだか一目で分かる |
| 入力/出力の検証 | ファイル存在、行数、対象件数をチェック | 対象ゼロを「正常」と誤認しにくい |
| 終了コードを返す | 成功 exit 0 / 失敗 exit 1 | タスクのLast Run Resultが信頼できる指標になる |
| モジュール依存を固定 | Import-Module -ErrorAction Stop | 環境差でコマンドレットが無い問題を即検知 |
削除を安全に運用するための設計案:いきなり削除しない
90日未チェックイン端末を「即削除」は、運用上はリスクが高めです。トラブルを減らすなら、次のような2段階運用がおすすめです。
- 第1段階:対象端末を無効化(Disable)し、隔離OUへ移動。レポート(CSV/ログ)を必ず保存。
- 第2段階:さらに一定期間(例:30日)動きがなければ削除。例外端末(役員PC、共用端末、検証機)はホワイトリストで除外。
この方式にしておくと、抽出ロジックのミスや、Entra IDとオンプレADの紐づきズレがあっても「即消し」の事故を避けられます。また、手動検証(-WhatIf)と本番挙動の差が出ても、影響を小さくできます。
切り分けのおすすめ手順:再現しやすい順に潰す
最後に、現場で迷いにくいように、切り分けを「やる順番」でまとめます。上から順に実施すると、遠回りになりにくいです。
| 順番 | 確認内容 | 成功/失敗で分かること |
|---|---|---|
| 1 | タスクの「プログラム」「引数」「開始(作業)フォルダー」を見直す | そもそもPowerShellが想定通り起動できているか |
| 2 | タスクと同じアカウントでログオンし、手動実行する | 環境差(権限/証明書/プロキシ/モジュール)か、タスク設定差かを切り分け |
| 3 | Start-Transcriptでログを取り、停止箇所を特定する | 「どの行/どのステップで落ちたか」が分かる |
| 4 | Import-Moduleを明示し、TLS 1.2を設定してから接続/取得を試す | Entra/Graph/ギャラリー接続やモジュール依存の問題を炙り出す |
| 5 | ファイルパスをUNC/フルパスにし、出力・入力ファイルの存在を検証する | 対象ゼロ扱い、パス違い、共有権限違いを排除できる |
まとめ
タスク スケジューラでPowerShellを回すときに「成功なのに反映されない」場合、原因はスクリプト本体よりも実行環境差分にあることが多いです。特に Windows Server 2016 では、TLS 1.2 と PowerShellGet/依存モジュールの古さが絡むと、必要モジュールの導入や読み込みに失敗しやすく、結果として処理が途中停止します。
まずは同一アカウントでの手動実行とトランスクリプトによるログ可視化で、失敗点を特定してください。そこから、TLS 1.2 の適用、モジュールの更新・全ユーザー導入、UNCパス化、作業フォルダー固定といった「運用の基本」を揃えると、0x80070001(2147942401)系の“成功っぽい失敗”を着実に潰せます。

コメント