Article

バッチファイルで簡単自動化を始める

高田晃太郎
バッチファイルで簡単自動化を始める

MS-DOS時代から続くバッチファイルは、今もWindows 11/Serverで現役の自動化手段として利用できます[1]。驚くのは、追加コストなしに標準機能だけでスケジューリング(タスク スケジューラ)、ログ出力、権限管理まで一通りこなせる点です[2]。運用現場での繰り返し作業は、体感としては小さな雑務の寄せ集めに見えても、積み上がるとチームの集中力とスループットを確実に蝕みます。コンテナやIaCを全面展開する前段として、まずは既存インフラの近くで、リスクの低いユースケースからWindowsバッチファイルによる自動化を進めるのが投資対効果の面でも安全です。Windowsに標準のバッチとタスク スケジューラで、今日から確実に一歩を踏み出すことができます[2]。

最小コストで始める:安全なバッチ設計の基礎

実運用で使えるバッチの要件は、動くことに加えて、失敗時に静かに消えないこと、再実行が安全であること、そして監査に耐える記録が残ることです。加えてパラメータ化(引数で挙動を変えられるようにする)、環境変数の扱い、権限境界の理解、終了コード(プロセスがOSに返す実行結果の数値)の約束事を整えておくと、属人化を防ぎながら横展開できます。前提としては、Windows 10/11 あるいは Windows Server 2016以降、標準の「タスク スケジューラ」を利用できること、対象フォルダや共有へのアクセス権を正しく付与できること、そして管理者権限が必要な操作を区別できることを確認します[9]。

実務テンプレート:ログ、計測、失敗に強い骨格

まずは共通テンプレートを用意し、各ジョブはこの骨格をコピーペーストしてから中身だけを差し替える運用にします。ログはタイムスタンプ付きで標準出力とファイルの二重化、実行時間は開始と終了の両方を記録し、終了コードを明示的に返すことで監視から扱いやすくします。遅延環境変数展開(enabledelayedexpansion)は処理中の値を安全に扱うための定番です。

@echo off
setlocal enabledelayedexpansion
rem === config ===
set SCRIPT_NAME=%~n0
set LOG_DIR=%~dp0logs
set LOG_FILE=%LOG_DIR%\%SCRIPT_NAME%_%%DATE: =_%_%%TIME::=_%_run.log
set EXIT_CODE=0

if not exist "%LOG_DIR%" mkdir "%LOG_DIR%"

call :ts >nul
set START_TS=!TS!

call :log "START %SCRIPT_NAME%"

rem === main ===
call :main || set EXIT_CODE=!ERRORLEVEL!

call :ts >nul
set END_TS=!TS!
call :log "END   %SCRIPT_NAME% exit=!EXIT_CODE! duration_ms=!DUR_MS!"

endlocal & exit /b %EXIT_CODE%

:main
rem ここに本処理を実装
rem 例: 環境検証
where robocopy >nul 2>&1 || (call :log "robocopy not found" & exit /b 2)
rem ダミー成功
exit /b 0

:log
set "msg=%~1"
echo [%DATE% %TIME%] %SCRIPT_NAME% - %msg%
>>"%LOG_FILE%" echo [%DATE% %TIME%] %SCRIPT_NAME% - %msg%
exit /b 0

:ts
for /f "tokens=2 delims==." %%A in ('wmic os get localdatetime /value') do set TS=%%A
rem 上記TSはYYYYMMDDhhmmss形式。開始と終了から概算ミリ秒差分をPowerShellで計算
for /f %%X in ('powershell -NoProfile -Command ^
  "$s=[datetime]::ParseExact('%START_TS%','yyyyMMddHHmmss',$null); ^
   $e=[datetime]::ParseExact((Get-Date).ToString('yyyyMMddHHmmss'),'yyyyMMddHHmmss',$null); ^
   [int]($e-$s).TotalMilliseconds"') do set DUR_MS=%%X
exit /b 0

テンプレートでは、開始と終了を明確に記録し、exit /bで終了コードを返しています。wmicの日時取得は互換性が高く(新しい環境でwmicが無効な場合は PowerShell の Get-Date -Format ‘yyyyMMddHHmmss’ でも代替可能)、差分計算そのものはPowerShellを呼び出して行うことで桁落ちやロケールの罠を避けています。Windowsの混在環境ではこうした小さな堅牢性が長く効きます。

パラメータ化と再実行の設計

ジョブの引数は%1のような位置引数で受け取り、デフォルト値を用意しておくと失敗を減らせます。再実行可能性(冪等:何度実行しても同じ結果になる性質)を担保するには、ターゲット側で冪等に書き込む、作業用の一時ディレクトリをトランザクション的に扱う、そして中間ファイルのクリーンアップを最後ではなく開始時に行う方針が有効です。

@echo off
setlocal
set SRC=%~1
set DST=%~2
if "%SRC%"=="" set SRC=C:\data\daily
if "%DST%"=="" set DST=\\fileserver\backup\daily

if not exist "%SRC%" (echo source missing & exit /b 10)
if not exist "%DST%" (echo creating dst & mkdir "%DST%")

set TMP=%DST%\.tmp_copy
if exist "%TMP%" rmdir /s /q "%TMP%"
mkdir "%TMP%"

robocopy "%SRC%" "%TMP%" /MIR /R:2 /W:5 /COPY:DAT /NFL /NDL /NP
if errorlevel 8 (echo robocopy failed & exit /b %ERRORLEVEL%)

robocopy "%TMP%" "%DST%" /MIR /MOVE /R:2 /W:5 /NFL /NDL /NP
if errorlevel 8 (echo promote failed & exit /b %ERRORLEVEL%)

echo OK & exit /b 0

robocopyの戻り値は0だけが成功ではないことに注意が必要です。エラー判定をerrorlevel 8以上に設定するのは運用の常套です[3]。/MIR(ミラーリング)は削除も伴うため、バックアップ先の取り扱いには十分注意します。二段階コピーで冪等性と部分失敗時の巻き戻し容易性を両立しています。

具体例で学ぶ:現場で役立つバッチ5選

バックアップとログローテーション

バックアップは最初に効果が見えやすい題材です。コピーと同じくらい重要なのが古いログの整理で、容量ひっ迫と検索性の低下を避けます。

@echo off
setlocal
set LOG_ROOT=C:\apps\myapp\logs
forfiles /p "%LOG_ROOT%" /s /m *.log /d -14 /c "cmd /c del /q @path" 1>nul 2>&1
if errorlevel 1 (echo prune failed & exit /b 1)
echo pruned logs older than 14 days & exit /b 0

forfilesは日付条件(/d -14 で14日より古い)での一括削除に適しており、古いファイルだけを安全に消せます[4]。監査観点では保持期間の根拠をコメントに残すことも忘れないようにします。

サービスの健全性回復と通知

長期稼働のWindowsサービスは時にハングします。サービスの状態を確認し、停止と開始を試み、失敗時は即座に通知します。

@echo off
setlocal
set SVC=MyService
sc query "%SVC%" | findstr /i RUNNING >nul
if %ERRORLEVEL%==0 (echo %SVC% OK & exit /b 0)

call :log "%SVC% not RUNNING; try restart"
net stop "%SVC%" /y
net start "%SVC%"
if errorlevel 1 (
  powershell -NoProfile -Command ^
   "Send-MailMessage -SmtpServer 'smtp.local' -To 'ops@local' -From 'bot@local' ^
    -Subject '[ALERT] %SVC% restart failed' -Body 'check host'" 2>NUL
  exit /b 2
)
exit /b 0

:log
echo [%DATE% %TIME%] %~1
exit /b 0

メール送信はPowerShellに委譲しています[5]。単純なHTTPのWebhookでPagerDutyやTeamsへ通知する構成も選べます。権限起因の失敗が多いタスクなので、実行ユーザーのサービス制御権限を事前に検証しておきます。

HTTPヘルスチェックとJSON解析

Webアプリの健全性確認は、cmd単体では扱いづらいHTTP/JSONの処理をPowerShellに任せるのが合理的です。バッチはスケジュールと退出コードの統合に徹します。

@echo off
setlocal
set URL=https://api.example.com/health
for /f %%X in ('powershell -NoProfile -Command ^
  "$r=Invoke-RestMethod -Uri '%URL%' -TimeoutSec 5; ^
   if($r.status -ne 'ok'){ exit 2 } else { exit 0 }" ^
') do set VOID=%%X
if errorlevel 1 (echo health check failed & exit /b 2)
echo healthy & exit /b 0

Invoke-RestMethodはJSONを自動でオブジェクト化し、条件分岐が容易です[6]。ネットワーク障害や証明書の問題を想定して、タイムアウトを短めに設定し、リトライ戦略はタスク スケジューラ側の再実行ポリシーで補完します。

Python連携でハッシュ検証を自動化

配布物の整合性チェックはハッシュの一致で機械化できます。PowerShellでも可能ですが、チームでPythonを標準化しているなら、importを含む短いスクリプトを呼び出すのが移植性の点で便利です。

#!/usr/bin/env python3
import sys
import hashlib
from pathlib import Path

def sha256sum(path: Path) -> str:
    h = hashlib.sha256()
    with path.open('rb') as f:
        for chunk in iter(lambda: f.read(1024 * 1024), b''):
            h.update(chunk)
    return h.hexdigest()

if __name__ == '__main__':
    if len(sys.argv) != 3:
        print('usage: verify.py <file> <expected_sha256>')
        sys.exit(2)
    p = Path(sys.argv[1])
    expected = sys.argv[2].lower()
    actual = sha256sum(p)
    print(actual)
    sys.exit(0 if actual == expected else 1)
@echo off
setlocal
python C:\ops\verify.py C:\drop\app.zip 0123abcd... || (echo hash mismatch & exit /b 3)
echo verified & exit /b 0

インポートを明示したPythonスクリプトはレビューと再利用が容易です。バッチは結果の成否だけを取り込み、上位の監視やジョブ連鎖に繋げます。

スケジューリングと権限:タスク スケジューラの勘所

スケジュール作成はGUIでも可能ですが、再現性の観点ではコマンドで定義します[9][7]。専用のサービスアカウントを用意し、最小権限の原則で運用します。

schtasks /Create ^
 /TN \ops\daily_backup ^
 /TR "C:\ops\backup.bat" ^
 /SC DAILY /ST 01:30 ^
 /RU "DOMAIN\\svc_ops" /RP "*****" ^
 /RL HIGHEST /F

証跡が必要ならXMLエクスポートして管理リポジトリでレビュー対象にします。変更はPull Requestで合意形成し、リリースノートに残すと監査負荷が軽くなります[7]。

品質を上げる運用:計測・監視・ROI

ログ標準化とメトリクス化

バッチはテキスト出力が基本ですが、標準フォーマットを決めると他のツールと連携しやすくなります。日時、スクリプト名、レベル、メッセージを一行に収め、終了コードや所要時間を最後に集約します。集計はPowerShellでCSV化するとダッシュボードに取り込みやすく、日次の成功率、95パーセンタイルの処理時間、リトライ回数といった指標が見える化されます。

# Parse batch logs to CSV
param(
  [string]$Path = 'C:\ops\logs')
Get-ChildItem $Path -Filter *.log -Recurse | ForEach-Object {
  Select-String -Path $_.FullName -Pattern '^\[(.+?)\] (\S+) - (.+)$' | ForEach-Object {
    $ts,$name,$msg = $Matches[1],$Matches[2],$Matches[3]
    [pscustomobject]@{ Timestamp = [datetime]$ts; Script = $name; Message = $msg }
  }
} | Export-Csv "$Path\summary.csv" -NoTypeInformation

形式を決めるだけで「集計できるログ」に変わります。失敗の早期検知は単純な閾値から始め、徐々に移動平均や曜日別傾向に広げると過検知を抑えられます。

ミニ・ベンチマークの設計と結果の読み方

バッチでのベンチマークは、大きく二つに分けるとI/O中心タスクとAPI呼び出し中心タスクです。robocopyのようなI/O主体では、ファイル数と合計サイズを変化させた複数ケースを用意し、所要時間とスループットを記録します。API中心では平均レイテンシ、タイムアウト率、再試行の有無を記録します。以下は計測用の簡易ハーネスです。

@echo off
setlocal
set SRC=C:\data\bulk
set DST=\\fileserver\bench
set CASE=10k_small
set START=%TIME%
robocopy "%SRC%\%CASE%" "%DST%\%CASE%" /MIR /R:1 /W:2 /MT:16 /NFL /NDL /NP
set RC=%ERRORLEVEL%
set END=%TIME%
powershell -NoProfile -Command ^
  "$d=([datetime]'1/1/0001 ' + (New-TimeSpan -Start '%START%' -End '%END%')); ^
   Write-Host ('CASE=%CASE% rc=%RC% duration='+$d.ToString('HH:mm:ss.fff'))"
exit /b %RC%

出力例として、CASE、戻り値、所要時間が一行で残る形にすると、過去との比較やパラメータ調整の効果検証が容易になります。チューニングでは/MTでの並列数、/Rと/Wのリトライ戦略、ネットワーク負荷とのバランスを意識します[8]。

コストと効果:ROIの考え方

ROIは「自動化で削減できる定常工数」と「導入・保守コスト」の比較で見積もります。例えば、毎朝のログ整理に10〜20分、バックアップ検証に5〜15分、サービス再起動対応に平均5〜10分かかっていたとします。平日20日前後で合計7〜15時間の削減になり、時給換算で一般的なエンジニア単価(数千円台)の前提でも、月数万円規模、年で数十万円規模の回収余地が見込めます。スクリプト作成とレビューに初期で数時間、月次の微修正に1時間前後であれば、早期にペイしやすいケースが多い、というのが一般的な見立てです。単純な足し算ですが、数字に落とすことで優先度付けがしやすくなります。

スケールとガバナンス:アンチパターンを避ける

スクリプトの乱立とハードコードの罠

現場でありがちなのは、担当者ごとに微妙に違うスクリプトが散在し、ハードコードされたパスや資格情報が長く放置される状態です。対策はシンプルで、共通テンプレートと設定ファイルの分離、共通ライブラリの配置、そして秘密情報はWindowsの資格情報マネージャーやDPAPIで暗号化して扱う姿勢を徹底します。変更管理はGitのリポジトリでPull Requestを通し、実行体制の権限は最小化します。

失敗の見落としをゼロに近づける

失敗が監視に載らない時点で自動化は逆効果になります。終了コードを正しく返し、タスク スケジューラ側で「失敗時に再試行」を設定し、連続失敗のしきい値を超えたらWebhookで即通知するルートを用意します。バッチ側では成功・失敗の定義を関数化し、第三者が読んでも同じ解釈になるようにメッセージを標準化します。人が読むログと機械が読むメトリクスの両方を意識すると、後方互換を壊さずに改善を続けられます。

Windows以外への拡張と撤退戦略

バッチはWindowsに近い領域で強力ですが、複雑なデータ変換や並列オーケストレーションではPowerShell、Python、もしくはジョブスケジューラ製品の領域です。移行を前提に、バッチはランチャーとして割り切り、外部ツールに仕事を委譲する設計にしておけば、将来KubernetesやSaaSに載せ替える際も痛みが少なく済みます。撤退のしやすさも十分な設計品質の一部だと考えると意思決定がぶれません。

まとめ:今日の10分が明日の1時間を生む

標準機能だけで、バッチは十分に実用的な自動化の土台になります。失敗に強いテンプレートを用意し、ログと計測で可視化し、リスクの低い領域から着手すれば、早い段階で効果が見えるはずです。やがてPowerShellやPythonとの連携に進み、スケールが必要になった時には専門ツールに移ればよいのです。まずは一つ、明日の自分を助けるジョブを選び、テンプレートを複製して、運用に載せてみてください。小さな自動化が積み重なった先に、チームの時間は確実に取り戻せます。あなたの環境で最初に救うべき10分は、どの作業でしょうか。

参考文献

  1. kopenguin.com. バッチファイルはMS-DOS時代から現代のWindowsまで活用可能である旨の解説. https://kopenguin.com/post-107101/
  2. @IT(atmarkit.itmedia.co.jp). バッチファイルの基本/タスクスケジューラと組み合わせた自動化の解説. https://atmarkit.itmedia.co.jp/ait/articles/2112/10/news031.html
  3. Microsoft Learn. Robocopy の終了コード(8以上は失敗を示す). https://learn.microsoft.com/ja-jp/windows-server/administration/windows-commands/robocopy
  4. Microsoft Learn. Forfiles コマンドの使い方(日時条件での一括処理). https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2012-R2-and-2012/cc753551(v=ws.11)
  5. Microsoft Learn. Send-MailMessage(PowerShell からのメール送信). https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/send-mailmessage
  6. Microsoft Learn. Invoke-RestMethod(JSON レスポンスは PSObject として返却). https://learn.microsoft.com/ja-jp/powershell/module/microsoft.powershell.utility/invoke-restmethod
  7. Microsoft Learn. schtasks /Create(タスク スケジューラのコマンド登録). https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/schtasks-create
  8. Microsoft Learn. Robocopy オプション(/MT でマルチスレッドコピー). https://learn.microsoft.com/ja-jp/windows-server/administration/windows-commands/robocopy#remarks
  9. @IT(atmarkit.itmedia.co.jp). 【Windows 10/11】タスクスケジューラで定期作業を自動化する:Tech TIPS. https://atmarkit.itmedia.co.jp/ait/articles/1305/31/news049.html