ようへいの日々精進XP

よかろうもん

Ansible や Serverspec で管理することを前提にした Windows Server 2012 で OS 起動時にスクリプトを実行する方法の考察

tl;dr

Windows Server で OS 起動時にとあるスクリプトを起動させたいと思って、調べたり、教えて頂いたりしたことをメモ。そして、それらを Ansible や Serverspec で管理することを前提として最適な方法を検討してみたい。(試した環境は Windows Server 2012 だけど Windows Server 2012 R2 でもイケるはず)


勘違い

当初は...

f:id:inokara:20160401074547p:plain

上記のように

  • C:\Users\Administrator\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup

に起動したいスクリプトを放り込んでいた。

上記の状態だと、リモートデスクトップでログオンした際にスクリプトが起動した。

f:id:inokara:20160401074608p:plain

OS が起動した際にはスクリプトは起動してくれないという事実に気付いたのはだいぶん後だった。


いや、違う

俺がやりたいのは

OS が起動した時には既に起動していて欲しい。


ということで

ローカルグループポリシーを使う場合

起動したいスクリプトを以下のようにバッチスクリプトにしておく。

python.exe C:\Users\Administrator\Documents\python\o-re-no-service03.py
  • ローカルグループポリシーエディタを起動する

以下のように gpedit.msc と入力してローカルグループポリシーエディタを起動する。

f:id:inokara:20160401075919p:plain

以下のように Computer Configuration をクリックして Scripts(Startup/Shutdown) をクリック、そして Startup をダブルクリックする。

f:id:inokara:20160401080119p:plain

以下のように Startup Properties が開くので Add... をクリックして起動したいバッチスクリプトを選択する。

f:id:inokara:20160401080310p:plain

スクリプトの引数まで指定することが出来るが、今回は特に引数無し。

以下のようにスクリプトが登録されたことを確認する。

f:id:inokara:20160401080438p:plain

スクリプトが登録されると以下のように C:\Windows\System32\GroupPolicy\Machine\Scripts に保存されている隠しファイル(script.ini)に設定が書き込まれる。

f:id:inokara:20160401080811p:plain

中身は以下のような内容となっている。

[Startup]
0CmdLine=C:\Users\Administrator\Documents\python\start.bat
0Parameters=

再起動後、スクリプトを確認する場合には以下のように PowerShell スクリプトで確認するか、タスクマネージャで確認することになる。

PS C:\Users\Administrator> Get-WmiObject Win32_Process -Filter "name = 'python.exe'" | select -First 1 CommandLine -Expa
ndProperty CommandLine
python.exe  C:\Users\Administrator\Documents\python\o-re-no-service03.py

以下のようにタスクマネージャで確認することが出来る。

f:id:inokara:20160401082450p:plain

  • ちょっとした罠

以下のように C:\Windows\System32\GroupPolicy\Machine\Scripts\Startupスクリプトを放り込めば...と思ったけど、このフォルダにスクリプトを放り込んでも OS 起動時ではなく、ログオン時にスクリプトが起動されるので期待した動作にはならない。

f:id:inokara:20160401080738p:plain

タスクスケジューラを利用する場合

  • 同僚 O 氏有難うございますmm

同僚の O 氏の教えて頂いたタスクスケジューラを利用する方法。

  • 構成管理ツールで管理する場合には....

個人的にこちらの方法が Ansible 等で管理する場合には良さそうという結論になった。理由については以下の通り。

- 登録したスタートアップタスクを XML で書き出すことが出来る
- 書きだした XML を PowerShell を使ってインポートすることが出来る
- パラメータは XML を修正することで、ある程度は修正可能
- ローカルグループポリシーエディタで書き出される script.ini を弄るのは怖い、敷居が高い

細かいタスク登録手順については以下の記事が詳しい、美しい。

www.atmarkit.co.jp

ウィザードに従って、タスクトリガーにてコンピュータの起動時を選択する。


Ansible + PowerShell を使って OS 起動時にスクリプトを実行させる例

必ず手動で一度は登録しなければいけない...

以下の通り、登録済みのタスクを Export するところから始める。

f:id:inokara:20160401085053p:plain

Export したタスクは以下のような XML フォーマットになっている。

<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
  <RegistrationInfo>
    <Date>2016-03-31T23:40:31.1912347</Date>
    <Author>WIN-XXXXXXXXXXX\Administrator</Author>
  </RegistrationInfo>
  <Triggers>
    <BootTrigger>
      <Enabled>true</Enabled>
    </BootTrigger>
  </Triggers>
  <Principals>
    <Principal id="Author">
      <UserId>S-1-5-18</UserId>
      <RunLevel>LeastPrivilege</RunLevel>
    </Principal>
  </Principals>
  <Settings>
    <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
    <DisallowStartIfOnBatteries>true</DisallowStartIfOnBatteries>
    <StopIfGoingOnBatteries>true</StopIfGoingOnBatteries>
    <AllowHardTerminate>true</AllowHardTerminate>
    <StartWhenAvailable>false</StartWhenAvailable>
    <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
    <IdleSettings>
      <StopOnIdleEnd>true</StopOnIdleEnd>
      <RestartOnIdle>false</RestartOnIdle>
    </IdleSettings>
    <AllowStartOnDemand>true</AllowStartOnDemand>
    <Enabled>true</Enabled>
    <Hidden>false</Hidden>
    <RunOnlyIfIdle>false</RunOnlyIfIdle>
    <WakeToRun>false</WakeToRun>
    <ExecutionTimeLimit>P3D</ExecutionTimeLimit>
    <Priority>7</Priority>
  </Settings>
  <Actions Context="Author">
    <Exec>
      <Command>C:\Users\Administrator\Documents\python\start.bat</Command>
    </Exec>
  </Actions>
</Task>

PowerShell を利用して XML ファイルをインポートしてタスクを登録する

以下のような PowerShell スクリプトを作成して Export した XML ファイルをインポートしてタスクを登録する。

#
# Import Task
#
$XmlFile = "C:\path\to\" +  $args[0] + ".xml"
$Xml = (get-content $XmlFile | out-string)
Register-ScheduledTask `
  -Xml "$Xml" `
  -TaskName $args[0]

Export した XML ファイルを C:\path\to\ 以下に保存してスクリプトを実行するとタスクが登録される。

PS C:\oreno> .\import-task.ps1 Oreno-Script

TaskPath                                       TaskName                          State
--------                                       --------                          -----
\                                              Oreno-Script                      Ready

念のために確認。

PS C:\oreno> Get-ScheduledTask -TaskName Oreno-Script

TaskPath                                       TaskName                          State
--------                                       --------                          -----
\                                              Oreno-Script                      Ready

Ansible の Script モジュールを利用してタスクを登録する

前のステップで利用したタスクをインポートする PowerShell スクリプトを利用して、以下のような Playbook を作成して Ansible 経由でタスクを登録してみる。

#
# - files/scripts/ 以下のファイルをリモートホストの c:/path/to 以下にコピーする
#
- name: スクリプトファイルをアップロードする
  win_copy: src=scripts/ dest=c:/path/to

#
# - check-task.ps1 で登録済みのタスクをチェック
# - import-task.ps1 で XML ファイルをインポートしてタスクを登録
#
- name: 登録済みタスクが無いかをチェックする
  script: files/check-task.ps1 Oreno-Script
  always_run: yes
  failed_when: no
  changed_when: no
  register: task_info

- name: デバッグ出力
  debug: var=task_info.stdout

- name: PowerShell スクリプトと XML を利用してイベントベースのタスクを登録する
  script: files/import-task.ps1 Oreno-Script
  when: task_info.stdout != "Oreno-Script\r\n"
  register: result_info

- name: デバッグ出力
  debug: var=result_info.stdout

冪等性を出来るだけ担保する為に登録済みタスクをチェックするスクリプトも以下のように用意しておく。

#
# 登録済みのタスクをチェックする
#
Get-ScheduledTask -TaskName $args[0] | select -First 1 TaskName -ExpandProperty TaskName

最終的には以下のようなファイル構成となる。

$ tree roles/oreno-script/
roles/oreno-script/
├── files
│   ├── check-task.ps1
│   ├── import-task.ps1
│   └── xml
│       └── Oreno-Script.xml
└── tasks
    └── main.yml

3 directories, 4 files

Playbook を流してみる

早速、Playbook を流してみる。

$ ansible-playbook -i hosts default.yml

PLAY ***************************************************************************

TASK [oreno-script : スクリプトファイルをアップロードする] ***************************************
ok: [ec2-xx-xxx-xx-xx.ap-northeast-1.compute.amazonaws.com]

TASK [oreno-script : デバッグ出力] ***************************************************
ok: [ec2-xx-xxx-xx-xx.ap-northeast-1.compute.amazonaws.com] => {
    "task_info.stdout": "VARIABLE IS NOT DEFINED!"
}

TASK [oreno-script : 登録済みタスクが無いかをチェックする] ***************************************
ok: [ec2-xx-xxx-xx-xx.ap-northeast-1.compute.amazonaws.com]

TASK [oreno-script : デバッグ出力] ***************************************************
ok: [ec2-xx-xxx-xx-xx.ap-northeast-1.compute.amazonaws.com] => {    "task_info.stdout": ""
}

TASK [oreno-script : PowerShell スクリプトと XML を利用してイベントベースのタスクを登録する] **************
changed: [ec2-xx-xxx-xx-xx.ap-northeast-1.compute.amazonaws.com]

TASK [oreno-script : デバッグ出力] ***************************************************
ok: [ec2-xx-xxx-xx-xx.ap-northeast-1.compute.amazonaws.com] => {
    "result_info.stdout": "\r\nTaskPath                                       TaskName                        \r\n--------                                       --------                        \r\n\\                                              Oreno-Script                    \r\n\r\n\r\n"
}

PLAY RECAP *********************************************************************
ec2-xx-xxx-xx-xx.ap-northeast-1.compute.amazonaws.com : ok=6    changed=1    unreachable=0    failed=0   

流し終わったら、OS を再起動する。

登録されたことを確認する...Serverspec で

以下のようにテストを書く。

require 'spec_helper'

context "自動起動スクリプトがタスクスケジューラに登録、有効になっている場合..." do
  describe command("powershell -Command \"Get-ScheduledTask -TaskName 'Oreno-Script' | select -First 1 TaskName -ExpandProperty TaskName\"") do
    its(:stdout) { should match /Oreno-Script/ }
  end

  describe command("powershell -Command \"Get-ScheduledTask -TaskName 'Oreno-Script' | select -First 1 State -ExpandProperty State\"") do
    its(:stdout) { should match /Running/ }
  end
end

context "スクリプトが正常に起動している場合..." do
  describe process("python.exe") do
    it { should be_running }
    its(:CommandLine) { should match /o-re-no-service03.py/ }
  end
end

テストを走らすと以下のように。

$ bundle exec rake serverspec:ec2-xx-xxx-xx-xx
/home/vagrant/.rbenv/versions/2.2.3/bin/ruby -I/home/vagrant/git/sample-ansible-win/vendor/bundle/ruby/2.2.0/gems/rspec-core-3.4.1/lib:/home/vagrant/git/sample-ansible-win/vendor/bundle/ruby/2.2.0/gems/rspec-support-3.4.1/lib /home/vagrant/git/sample-ansible-win/vendor/bundle/ruby/2.2.0/gems/rspec-core-3.4.1/exe/rspec --pattern spec/\{oreno-script\}/\*_spec.rb

自動起動スクリプトがタスクスケジューラに登録、有効になっている場合...
  Command "powershell -Command "Get-ScheduledTask -TaskName 'Oreno-Script' | select -First 1 TaskName -ExpandProperty TaskName""
    stdout
      should match /Oreno-Script/
  Command "powershell -Command "Get-ScheduledTask -TaskName 'Oreno-Script' | select -First 1 State -ExpandProperty State""
    stdout
      should match /Running/

スクリプトが正常に起動している場合...
  Process "python.exe"
    should be running
    CommandLine
      should match /o-re-no-service03.py/

Finished in 7.77 seconds (files took 0.67635 seconds to load)
4 examples, 0 failures

おけけ。


考察

OS 起動時にスクリプトを実行する方法

  • ローカルグループポリシーを使う(実行したいスクリプトを登録する)
  • タスクマネージャを使う(XML をインポートする)

構成管理ツールで手軽に管理出来そうなのは

タスクマネージャを使うパターンが良さそう。理由としては、繰り返しになるけど以下の通り。

  • 登録したスタートアップタスクを XML で書き出すことが出来る
  • 書きだした XMLPowerShell を使ってインポートすることが出来る
  • パラメータは XML を修正することで、ある程度は修正可能
  • タスクスケジューラのコンソールから Status が確認出来る(実行している場合には Running となる)
  • ローカルグループポリシーエディタで書き出される script.ini を弄るのは怖い、敷居が高い

以上。