株式会社Good Labでエンジニアをしている コータロー です。
日々、Java・SQL・Gitなどの技術情報や、新人エンジニア向けの学習ノウハウ、
AI活用についての情報を発信しています。
Good Labについて気になった方は、コーポレートサイトもぜひご覧ください。
▶コーポレートサイト
本記事は 「PowerShell 業務自動化レシピ」 シリーズの第10回です。このシリーズは、文法解説ではなく 「1記事=1つの業務課題」を、コピペで動く検証済みスクリプトで解決することを目的とした実務レシピ集です(全12回想定)。SES・受託・社内運用の現場で、いまだに手作業でやっている地味な作業を片っ端から自動化していきます。
第9回 では「REST API連携(Invoke-RestMethod でデータ取得・投稿)」を扱いました。ここまでで、ログ削除(第2回)・台帳出力(第3回)・点検レポート・API取得など、現場で使えるスクリプトがいくつも手元に揃ったはずです。
第10回のテーマは 「毎朝決まった時刻に自動実行(タスクスケジューラ登録)」。せっかく作ったスクリプトも、毎回手で実行していたら属人化し、いつか実行を忘れます。*-ScheduledTask* コマンドでスクリプトをWindowsのタスクスケジューラに登録し、無人で定期実行する仕組みを作ります。
💡 第2回でも「タスクスケジューラに登録する例」を数行だけ紹介しましたが、あれはあくまで触りです。今回は本格版・専用回として、トリガー設計・実行ユーザーと権限・起動引数の定石・ログと失敗検知・「手動では動くのにタスクだと動かない」定番トラブルまで、運用に乗せるために必要なことを一通り扱います。
本記事は PowerShell 7(pwsh、クロスプラットフォーム) を基準にしています。ただし タスクスケジューラはWindows専用機能で、
*-ScheduledTask*コマンドは Windowsでのみ利用可能です(ScheduledTasksモジュール)。Windows標準の Windows PowerShell 5.1 での差異は、各所と最後の章で明記します。
⚠️ 破壊的操作ではありませんが、無人実行は誤動作が気づかれにくいのが最大の落とし穴です。本記事では「登録する前に、必ず手動でスクリプトを通しで実行して検証する」を徹底します(第2回と同じ原則)。誤った対象を消すスクリプトを無人で回すと、誰も気づかないまま被害が広がります。
Before:手作業のつらいシーン
シーン1:「毎朝、自分が出社したら手で叩く」運用
第3回で作った「共有フォルダの台帳CSVを出力するスクリプト」を、毎朝出社後に手で実行してチームに共有している。
- 自分が休んだ日・直行直帰の日は台帳が更新されない
- 「あの人しか回し方を知らない」状態になり、完全に属人化
- 実行を忘れた日があっても、誰も気づかない
シーン2:深夜・休日に動かしたい処理を手で回せない
第2回のログ削除や、夜間バッチの後処理を「業務時間外に動かしたい」のに、そのためだけに残業・休日出社している。
- 深夜2時のログ掃除のために起きていられない
- 結局「平日の朝にまとめて」になり、ディスクが一時的に逼迫する
これ、タスクスケジューラに一度登録すれば、PCが起動していれば無人で毎朝(毎晩)動きます。しかも登録自体もPowerShellのコマンドで再現可能・他PCへ展開可能にできます。
After:スクリプトをタスクとして登録する
タスクは大きく 4つの部品(Action / Trigger / Settings / Principal)を組み立てて Register-ScheduledTask で登録します。
| 部品 | コマンド | 役割 |
|---|---|---|
| Action | New-ScheduledTaskAction |
何を実行するか(pwsh.exe + 引数) |
| Trigger | New-ScheduledTaskTrigger |
いつ実行するか(毎日・毎週・ログオン時…) |
| Settings | New-ScheduledTaskSettingsSet |
失敗時の再試行・実行時間制限などの挙動 |
| Principal | New-ScheduledTaskPrincipal |
どのユーザー・権限で実行するか |
⚠️ タスクの登録(
Register-ScheduledTask)は、基本的に管理者権限のPowerShellで実行してください(特に後述の「最上位の特権」「ユーザーがログオンしていなくても実行」を使う場合は必須)。PowerShellを「管理者として実行」で起動して進めます。
レシピ1:基本登録(毎朝9時にスクリプトを実行)
まずは一番シンプルな形。C:\scripts\Export-Inventory.ps1(第3回の台帳出力スクリプトを想定)を、毎朝9:00に実行するタスクを登録します。
# ===== 設定 =====
$taskName = "Export-Inventory-Daily" # タスク名(管理しやすい名前を)
$scriptPath = "C:\scripts\Export-Inventory.ps1" # 実行するスクリプト
$runAt = "9:00AM" # 実行時刻
# ===== 部品を組み立てる =====
# (1) 何を実行するか:pwsh.exe に起動引数を渡す(後述「起動引数の定石」参照)
$action = New-ScheduledTaskAction -Execute "pwsh.exe" `
-Argument "-NoProfile -ExecutionPolicy Bypass -File `"$scriptPath`"" `
-WorkingDirectory "C:\scripts"
# (2) いつ実行するか:毎日 9:00
$trigger = New-ScheduledTaskTrigger -Daily -At $runAt
# (3) 挙動:時刻を過ぎていたら可能になった時点で実行 + 実行は最大1時間で打ち切り
$settings = New-ScheduledTaskSettingsSet `
-StartWhenAvailable `
-ExecutionTimeLimit (New-TimeSpan -Hours 1)
# ===== 登録 =====
Register-ScheduledTask -TaskName $taskName `
-Action $action -Trigger $trigger -Settings $settings `
-Description "共有フォルダの台帳CSVを毎朝出力する"
登録に成功すると、タスクスケジューラ(taskschd.msc)の「タスク スケジューラ ライブラリ」に Export-Inventory-Daily が現れます。
この例では
Principal(実行ユーザー)を指定していません。その場合、タスクを登録したユーザーのアカウント で、かつ「ユーザーがログオンしているときのみ実行」として登録されます。サーバーで無人運用したい・ログオフ中も動かしたい場合は、レシピ4のPrincipal指定が必要です。
レシピ2:トリガーいろいろ(毎週・ログオン時・繰り返し間隔)
New-ScheduledTaskTrigger を差し替えるだけで、実行タイミングを変えられます。
# 毎日 深夜2:00(ログ掃除など業務時間外向け)
$trigger = New-ScheduledTaskTrigger -Daily -At 2:00AM
# 毎週 月曜の朝8:30(週次レポートなど)
$trigger = New-ScheduledTaskTrigger -Weekly `
-DaysOfWeek Monday -At 8:30AM
# 平日(月〜金)の朝9:00 ※DaysOfWeek は複数指定できる
$trigger = New-ScheduledTaskTrigger -Weekly `
-DaysOfWeek Monday,Tuesday,Wednesday,Thursday,Friday -At 9:00AM
# 自分がWindowsにログオンしたタイミングでX実行(出社トリガー)
$trigger = New-ScheduledTaskTrigger -AtLogOn
# 1日1回の朝9:00を起点に、以降15分おきに丸1日繰り返す(営業時間中の定期巡回など)
$trigger = New-ScheduledTaskTrigger -Once -At 9:00AM `
-RepetitionInterval (New-TimeSpan -Minutes 15) `
-RepetitionDuration (New-TimeSpan -Hours 24)
-DaysOfWeek に渡せるのは Sunday〜Saturday の曜日名で、カンマ区切りで複数指定できます。繰り返し(-RepetitionInterval)は「N分おきに動かしたい」監視系の処理で便利です。
レシピ3:起動引数の定石(ここが一番ハマる)
タスクで PowerShell スクリプトを動かすときの -Argument の組み立て が、トラブルの大半の原因です。定石を覚えてしまいましょう。
$scriptPath = "C:\scripts\Export-Inventory.ps1"
$action = New-ScheduledTaskAction -Execute "pwsh.exe" `
-Argument "-NoProfile -ExecutionPolicy Bypass -File `"$scriptPath`"" `
-WorkingDirectory "C:\scripts"
引数の意味は次の通りです。
| 引数 | なぜ付けるか |
|---|---|
-NoProfile |
プロファイル($PROFILE)を読み込まない。ユーザー環境に依存せず毎回同じ条件で動かすため。起動も速い |
-ExecutionPolicy Bypass |
スクリプト実行ポリシーで止められないように。そのプロセス限定で緩和するので、システム全体のポリシーは変えない |
-File "..." |
実行するスクリプトファイルを指定。パスは必ずダブルクォートで囲む(スペースや日本語を含むパス対策) |
-WorkingDirectory |
スクリプトの「カレントディレクトリ」を固定。相対パスを使っているスクリプトはこれが無いと動かない(後述の定番トラブル) |
PowerShell内の文字列で
"を埋め込むため、`"$scriptPath`"のように バッククォート(`)でエスケープしています。これで Task Scheduler 側には-File "C:\scripts\Export-Inventory.ps1"という1引数として正しく渡ります。
レシピ4:実行ユーザーと権限(無人・ログオフ中も動かす)
「自分がログオフしていても動かしたい」「管理者権限が要る処理を動かしたい」場合は、Principal を指定します。
# ===== 設定 =====
$taskName = "Cleanup-OldLogs-Nightly"
$scriptPath = "C:\scripts\Cleanup-OldLogs.ps1" # 第2回のログ削除スクリプト
$action = New-ScheduledTaskAction -Execute "pwsh.exe" `
-Argument "-NoProfile -ExecutionPolicy Bypass -File `"$scriptPath`"" `
-WorkingDirectory "C:\scripts"
$trigger = New-ScheduledTaskTrigger -Daily -At 2:00AM
$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable `
-ExecutionTimeLimit (New-TimeSpan -Hours 1)
# ===== 実行ユーザー・権限 =====
# 実行アカウントを SYSTEM にし、最上位の特権(管理者相当)で実行する
# -UserId "SYSTEM" … ローカルSYSTEMアカウント。常駐サービス的に無人実行できる
# -LogonType ServiceAccount … パスワード不要・ログオン状態に依存しない
# -RunLevel Highest … 「最上位の特権で実行する」(管理者権限が要る処理向け)
$principal = New-ScheduledTaskPrincipal `
-UserId "SYSTEM" `
-LogonType ServiceAccount `
-RunLevel Highest
# ===== 登録 =====
Register-ScheduledTask -TaskName $taskName `
-Action $action -Trigger $trigger -Settings $settings -Principal $principal `
-Description "30日より古いログを退避・削除(無人)"
Principal の主要な選択肢は次の通りです。用途に合わせて選びます。
| やりたいこと | 指定 |
|---|---|
| 無人で動かす(ログオフ中もOK・パスワード管理不要) | -UserId "SYSTEM" -LogonType ServiceAccount |
| 特定ドメインユーザーで、ログオフ中も動かす |
-UserId "DOMAIN\user" -LogonType Password(登録時にパスワードを要求/要対話) |
| ログオン中のときだけ動けばよい | -UserId "DOMAIN\user" -LogonType Interactive |
| 管理者権限が必要な処理 | 上記に -RunLevel Highest を追加 |
⚠️
SYSTEMで動かすと「ユーザーのマップされたネットワークドライブ(Z:等)」「ユーザープロファイル配下のパス」が見えません。共有フォルダを触る場合は、後述のとおり UNCパス(\\server\share\...)で書くか、ネットワーク資源にアクセスできるサービスアカウントを使ってください。これが「手動では動くのに、タスクだと共有フォルダが見つからない」の典型原因です。
レシピ5:失敗検知(再試行設定 + スクリプト側のログ)
無人実行の生命線は 「失敗したことに気づける」 ことです。2段構えで備えます。
① タスク側:失敗時に自動で再試行する
# 失敗したら10分おきに最大3回まで再試行する
$settings = New-ScheduledTaskSettingsSet `
-StartWhenAvailable `
-ExecutionTimeLimit (New-TimeSpan -Hours 1) `
-RestartCount 3 `
-RestartInterval (New-TimeSpan -Minutes 10)
② スクリプト側:必ずログを残し、失敗を終了コードで伝える
タスクスケジューラは画面に何も出しません。スクリプト自身がログを書かないと、後から原因を追えません。.ps1 の中身を、必ずこの形にしておきます(次回・第11回の予告テーマでもあります)。
# ===== Export-Inventory.ps1(抜粋・冒頭にログ基盤を仕込む)=====
$logDir = "C:\scripts\logs"
if (-not (Test-Path -LiteralPath $logDir)) {
New-Item -ItemType Directory -Path $logDir | Out-Null
}
$logFile = Join-Path $logDir ("inventory_{0}.log" -f (Get-Date -Format "yyyyMMdd"))
function Write-Log {
param([string]$Message, [string]$Level = "INFO")
$line = "{0} [{1}] {2}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss"), $Level, $Message
# コンソールとログファイルの両方へ
$line | Tee-Object -FilePath $logFile -Append
}
try {
Write-Log "処理開始"
# ===== ここに本来の処理(台帳出力など)を書く =====
Get-ChildItem -Path "C:\share" -Recurse -File |
Select-Object FullName, Length, LastWriteTime |
Export-Csv -Path "C:\share\_inventory.csv" -NoTypeInformation -Encoding utf8
Write-Log "処理正常終了"
exit 0 # 成功は終了コード 0
}
catch {
Write-Log "エラー発生: $($_.Exception.Message)" "ERROR"
exit 1 # 失敗は 0 以外で終了 → タスクの「前回の実行結果」に反映される
}
これで、タスクの実行結果(成功/失敗)がタスクスケジューラに記録され、かつ 何が起きたかは C:\scripts\logs\ のログを見れば分かる状態になります。exit 1(0以外)で終了すれば、タスク側の -RestartCount による再試行も発動します。
レシピ6:登録済みタスクの確認・手動実行・無効化・削除
登録したら終わりではありません。運用で使う一通りのコマンドをまとめておきます。
# 一覧(自分が作ったタスクを名前で絞り込み)
Get-ScheduledTask -TaskName "Export-Inventory-Daily"
# 次回実行時刻・前回結果などの詳細を確認
Get-ScheduledTaskInfo -TaskName "Export-Inventory-Daily"
# その場で手動実行(★登録後の動作確認に必須)
Start-ScheduledTask -TaskName "Export-Inventory-Daily"
# 一時的に無効化(定義は残したまま動かなくする)
Disable-ScheduledTask -TaskName "Export-Inventory-Daily"
# 再び有効化
Enable-ScheduledTask -TaskName "Export-Inventory-Daily"
# 完全に削除(-Confirm:$false で確認なし。慎重に)
Unregister-ScheduledTask -TaskName "Export-Inventory-Daily" -Confirm:$false
特に Start-ScheduledTask での手動キックは、登録直後に必ずやる動作確認です。「タスクとして・登録した実行ユーザーで・指定した起動引数で」本当に動くかは、ここで初めて分かります。手動でのスクリプト直接実行が通っても、タスク経由だと落ちることはよくあります(次章)。
コードの要点解説(なぜこの書き方か)
文法そのものではなく、現場で事故らないための選択を中心に解説します。
1. なぜ部品を4つに分けて組み立てるのか
Register-ScheduledTask に直接ずらずら書くこともできますが、Action / Trigger / Settings / Principal を変数で分けて組むと、トリガーだけ差し替える・権限だけ変える、が簡単になります。レシピ2でトリガーだけ入れ替えていたのがその恩恵です。
2. -NoProfile -ExecutionPolicy Bypass -File はワンセット
この3点は無人実行スクリプトの定番です。-NoProfile でユーザー環境への依存を断ち、-ExecutionPolicy Bypass でポリシーによる停止を回避(そのプロセス限定なのでシステムには影響しない)、-File でスクリプトを指定。手動では動くのにタスクだと止まる原因の上位が「実行ポリシー」なので、ここで先に潰します。
3. -File と -Command の違い
-
-File "script.ps1"… スクリプトファイルを実行。引数はスクリプトの param() に渡る。タスクで.ps1を回すなら基本こちら。 -
-Command "..."… 文字列をその場でコマンドとして解釈。クォートのエスケープが複雑になりがちで、タスク登録では事故りやすい。
短い1行処理ならともかく、.ps1 ファイルを動かすなら -File が素直で安全です。
4. ログを「必ず」残す(無人実行の鉄則)
タスクは画面を持ちません。Write-Host の出力はどこにも残らず消えます。Tee-Object -Append でファイルにも書く、try/catch でエラーをログに落とす、exit で成否を終了コードに反映する——この3点が無人運用の最低ラインです。
5. exit 0 / exit 1 で成否をタスクに伝える
スクリプトが exit 0 なら成功、0以外 なら失敗として、タスクスケジューラの「前回の実行結果」に記録されます。catch の中で exit 1 を返しておくと、Get-ScheduledTaskInfo の LastTaskResult で異常を機械的に検知でき、-RestartCount の再試行も効きます。
応用・注意点
① 「手動では動くのに、タスクだと動かない」定番原因チェックリスト
無人実行のトラブルはほぼこの中にあります。登録前・トラブル時に上から確認してください。
| 症状の原因 | 対策 |
|---|---|
| 作業ディレクトリが違う(相対パスが解決できない) |
-WorkingDirectory を指定。またはスクリプト内のパスをすべて絶対パスにする
|
| 実行ユーザーの環境/権限が違う(手動は自分、タスクはSYSTEM等) | 想定する実行ユーザーで Start-ScheduledTask して再現確認。権限が要るなら -RunLevel Highest
|
| 実行ポリシーで止まる | 起動引数に -ExecutionPolicy Bypass
|
ネットワークドライブ(Z: 等)が見えない |
UNCパス(\\server\share)で書く。SYSTEMはマップ済みドライブを引き継がない |
対話的UIが出る処理が混ざっている(Read-Host・GUIダイアログ・-Confirm) |
無人実行では入力待ちで永久停止する。対話要素を全部排除する |
| 文字コードで日本語が化ける/失敗する | スクリプトを UTF-8(5.1ならBOM付き)で保存。出力も -Encoding utf8 を明示 |
| タスク登録が権限不足で失敗 | PowerShellを管理者として実行して Register-ScheduledTask
|
② 引数のクォートに注意
パスにスペースや日本語が含まれると、クォートが崩れて「ファイルが見つからない」になります。レシピ3のように -File `"$scriptPath`" で必ずダブルクォートで囲むのが定石です。登録後は Get-ScheduledTask <名前> | Select-Object -ExpandProperty Actions で、実際に登録された引数文字列を確認できます。
③ 登録前に「必ず手動で通し検証」(第2回と同じ原則)
無人タスクは、間違っていても誰も見ていません。登録の直前に、
- スクリプト単体を手動で最後まで実行して期待通りの結果になることを確認
-
Start-ScheduledTaskでタスク経由でも正しく動くことを確認(実行ユーザー・引数込みで) -
Get-ScheduledTaskInfoでLastTaskResultが0(成功)になっていることを確認
この3段を必ず通してから、スケジュールに任せます。特に第2回のような削除を伴うスクリプトを無人化するときは、検証なしで本番登録は厳禁です。
④ ログの肥大化に注意(自己言及)
ログを残すのは正義ですが、そのログ自体が溜まります。第2回(古いログの自動削除)の出番です。C:\scripts\logs を対象に、N日より古いログを消すタスクも併せて登録しておくと、運用がきれいに閉じます。
⑤ Windows PowerShell 5.1 との差異
*-ScheduledTask* コマンド群(ScheduledTasks モジュール)は 5.1 でもそのまま使えます。主な差は実行ファイル名です。
| 項目 | PowerShell 7(pwsh) | Windows PowerShell 5.1 |
|---|---|---|
| Action の実行ファイル | -Execute "pwsh.exe" |
-Execute "powershell.exe" |
pwsh の導入 |
別途インストールが必要 | 標準搭載(追加導入不要) |
*-ScheduledTask* コマンド |
利用可(Windows上) | 利用可 |
| 既定の文字コード | UTF-8 | レガシー(Shift_JIS/ANSI系)。スクリプト/ログ名の日本語に注意 |
| スクリプト保存の推奨 | UTF-8 | UTF-8 (BOM付き) だと文字化けを避けやすい |
素のWindows環境(pwsh未導入)で配布・運用するなら、
-Execute "powershell.exe"にしておくと「実行先が無い」事故を避けられます。pwsh を前提にするなら、対象PCに PowerShell 7 が入っていることを確認してください。
まとめ
- タスク登録は Action / Trigger / Settings / Principal を組み立てて
Register-ScheduledTask - 起動引数の定石は
-NoProfile -ExecutionPolicy Bypass -File "..."+-WorkingDirectory - 無人・ログオフ中も動かすなら
Principalで-UserId "SYSTEM" -LogonType ServiceAccount、管理者権限が要るなら-RunLevel Highest - 無人実行の生命線はログと終了コード。
try/catch+Tee-Object+exit 0/1をスクリプトに必ず仕込む - 「手動では動くのにタスクだと動かない」の定番は作業ディレクトリ・相対パス・実行ユーザー・実行ポリシー・ネットワークドライブ・対話的UI
- 登録前に 手動実行 →
Start-ScheduledTask→Get-ScheduledTaskInfoの3段検証を必ず通す
これで、毎朝手で叩いていた作業がPCに任せられ、属人化も実行忘れも解消されます。
次回 第11回は「配るスクリプトを『壊れにくく』する(引数化・try/catch・ログ出力)」。今回チラ見せした「失敗に強いスクリプトの作法」を専用回として深掘りし、他人に渡しても・別PCでも壊れない .ps1 の書き方を扱います。お楽しみに。
参考
- Register-ScheduledTask (ScheduledTasks) - Microsoft Learn
- New-ScheduledTaskAction (ScheduledTasks) - Microsoft Learn
- New-ScheduledTaskTrigger (ScheduledTasks) - Microsoft Learn
- New-ScheduledTaskSettingsSet (ScheduledTasks) - Microsoft Learn
- New-ScheduledTaskPrincipal (ScheduledTasks) - Microsoft Learn
- Get-ScheduledTaskInfo (ScheduledTasks) - Microsoft Learn
- Start-ScheduledTask / Unregister-ScheduledTask (ScheduledTasks) - Microsoft Learn
- about_PowerShell_exe / pwsh コマンドラインオプション - Microsoft Learn
@kotaro_ai_lab
AI活用や開発効率化について発信しています。フォローお気軽にどうぞ!