PowerShell DSC実践活用(2)

PowerShell DSC実践活用(2)

本番で使えるPowerShell DSCリソース作成入門

2015年2月16日 (2015/02/17 更新)

Windows Serverの構成管理を自動化できるツール「PowerShell DSC」を使いこなそう。PowerShellでDSCのリソースを自作する方法とは?

株式会社グラニ 吉崎 生
  • このエントリーをはてなブックマークに追加

 PowerShell DSC(Desired State Configuration)では、標準で提供されているリソースだけでなく、独自のリソースも作成できる。本稿では、現在、リリースされているPowerShell v4でリソースを自作する方法を解説する。

リソースとは

 前回の記事では、Configuration構文キーワードを用いて「構成を適用する流れ」を示した。コンフィギュレーション(Configuration)が「どんな構成を適用するかの宣言」だとすれば、リソース(Resource)は「実際に構成を適用するロジック」である。

 コンフィギュレーションは、対応するリソースがあって初めて利用できるため、何かを構成したいときには、その構成を実現可能なリソースを用意する必要がある。逆にいえば、リソースさえあれば、コンフィギュレーションを書くだけで容易に利用可能だ。

既存リソースの利用

 Windows 8.1やWindows Server 2012 R2には、標準でリソースが入っている(ビルトイン・リソース)。

 また、ビルトイン・リソース以外にも、PowerShellチームが追加リソースを公開しておりコミュニティでもリソースを公開している(カスタム・リソース)。それらのカスタム・リソースで事足りる場合は、自作せずに利用することを勧めたい。

 実施したい構成を可能とするリソースがなかったときが、リソースを書くときになるだろう。一見、リソースを書くのは難しそうに見えるかもしれないが、容易に書けるので安心してほしい。今回は、DSCリソースを実際に自作する流れを説明しよう。

リソースについての予備知識

 リソースを書くときにいくつか押さえておくべきことがあるので、先に説明する。

リソースで実装される関数

 リソースには、大きく3つの機能が所定の関数名で実装されている必要がある。

  1. 正しく構成されているかをテスト: Test-TargetResource
  2. 構成を適用: Set-TargetResource
  3. 現在の構成状態を取得: Get-TargetResource

 リソースが公開する関数はこの3つだけで、リソース内部ではあとは自由に関数などを記述可能だ。

冪等性(べき等性)

 「PowerShell DSCは冪等性を備えている」といわれることが多いが、その冪等性はリソースが担保している。DSCが自動的にすでに構成済みかどうかを判断して冪等性を担保するわけではない。リソースを自作するときは、冪等性を担保するように考える必要がある。

 具体的には、Test-TargetResource関数の作りが冪等性に直結するため、リソースを書くときに最も気を遣うべきだろう。

リソースの構成

 リソースは、所定のフォルダー構造である必要がある。またリソースは、MOF(Managed Object Format)といわれるスキーマ構造とPowerShellモジュールを結び付ける必要がある。

 とはいえ、面倒くさいことはなく、PowerShellチームから公開されているxDSCResourceDesignerを使えば容易に出力できるので安心してほしい。本記事でもxDSCResourceDesignerを使った手順を紹介する。

リソースの種類

 ひと言にリソースといっても、(すでに少し説明したものもあるが)大きく3種類存在する。

  • ビルトイン・リソース: Windowsに標準で組み込まれたリソース
  • カスタム・リソース: 標準リソース以外で、構成ロジックを組み上げて作ったリソース
  • コンポジット・リソース: 既存コンフィギュレーションを再利用したリソース

 一般に「リソース」というと、ビルトイン・リソースカスタム・リソースを指す。「では、コンポジット・リソースは?」というと、一度書いたコンフィギュレーションをリソースとして再利用する仕組みだ。

 実際に筆者が作成し、本番環境で利用しているコンポジット・リソースがリスト1だ。ご覧の通り、普通のコンフィギュレーションと何ら変わらない。

PowerShell
configuration cWebPILauncher
{
  param
  (
    [Parameter(Mandatory = $false, helpMessage = "Install and search from Reg by Get-DSCModuleProductId")]
    [string]$ProductId = "4D84C195-86F0-4B34-8FDE-4A17EB41306A", 

    [Parameter(Mandatory = $false)]
    [string]$InstallerUri = "http://go.microsoft.com/fwlink/?LinkId=255386"
  )

  function Get-RedirectedUrl ([String]$URL)
  { 
    $request = [System.Net.WebRequest]::Create($InstallerUri)
    $request.AllowAutoRedirect = $false
    $response = $request.GetResponse()
    $response | where StatusCode -eq "Found" | % {$_.GetResponseHeader("Location")}
  }

  $name = "Web Platform Installer"
  $DownloadPath = Get-RedirectedUrl $InstallerUri

  Package InstallWebPILauncher
  {
    Name       = $name
    Path       = $DownloadPath
    ReturnCode = 0
    ProductId  = $productId
  }
}
リスト1 コンポジット・リソースとして再利用するコンフィギュレーションのサンプル(.psm1ファイル)

 コンポジット・リソースを使うシーンとして、そのコンフィギュレーションがさまざまなサーバーで共通して使われる場合に、「コンポジット・リソース」とすれば同じコンフィギュレーションを何度も書かずに済むため、便利だろう。

 本記事ではカスタム・リソースの作成にのみ触れるが、リソースによってファイル構成が以下のように異なっている。

  • ビルトイン/カスタム・リソース: .schema.mof(スキーマMOF)ファイル+.psm1(モジュール)ファイル
  • コンポジット・リソース: .psd1(モジュールマニフェスト)ファイル+.psm1ファイル(=コンフィギュレーション)

カスタム・リソースのひな型を作成する

 PowerShell v4では、C#かPowerShellのいずれかでDSCリソースを記述できる。ここではPowerShellを用いて簡単なリソースを作ってみよう。

 なお、PowerShell v5が現在、プレビュー版として公開されている。v5では、リソース記述の効率化を主眼にして、クラス構文やDSC用の属性が導入される予定だ。プレビューだけあって、リリースごとに仕様が大きく変わっており、ここでの紹介は避けるが、v5が正式にリリースされた際にはより効率のいいリソースの記述方法を紹介したい。

ベストプラクティス

 指針がない状況でリソースを作成するのは不安も大きいだろう。PowerShellチームからリソースを書くときのベストプラクティスが公開されているので、ぜひ参考にしてほしい。本記事でもこれを注釈に挟みつつ説明する。

作成するリソース

 サンプルとして、非常にシンプルなリソースを作成してみよう。今回は、特定のフォルダーに対象ノードのOS情報をJSONフォーマットのファイルで出力するリソースを作成する。

 あるべきJSONファイルに対しては、

  • CIMクラス名(=CimClass(詳細後述)が「OS」として適切に出力されていること
  • 出力されたJSONに記述されたOSのコンピューターシステム名が、そのノードの名前(=ホスト名)であること

をもって、「あるべき状態(Desired State)で出力されているか」を判定しよう。

 ファイルの生成だけならビルトイン・リソースのFileリソースで容易に作成できるのだが、あえて簡単なサンプルとして作成することとする。

 では、リソースで必要な要素(=プロパティ)を考えてみよう。今回はシンプルに表1に示す2つのプロパティを設定可能にしよう。

プロパティ名一意性必須概要
Ensure System.String なし あり ファイルを存在させる(Present)、させない(Absent)の2種類から選択
Path System.String あり あり ファイルの出力先パスを指定する
表1 リソースに用いるプロパティ

 リソースには、同じリソースを用いても必ず一意に特定できるプロパティ(=「Key属性」と呼ぶ)を1つ定義する必要がある。今回はPathプロパティがKey属性で(=[一意性]列を「あり」としている)、同一リソースを用いてもPathは必ず重複しないことを前提に作成する。

 リソースで利用するプロパティを決めたので、次に「スキーマMOFとモジュール」のひな型を作ってみよう。

xDSCResourceDesignerの導入

 リソースを書くときの手間を大きく軽減してくれるのが、PowerShellチームから公開されているxDSCResourceDesignerモジュール(Module)だ。このモジュールを使うことで、リソースとMOFスキーマの整合性テストや、「スキーマMOFファイル(=.schema.mofファイル)とモジュールファイル(=.psm1ファイル)」のひな型の生成ができる。今回もひな型の生成に用いてみよう。

 まずは、xDSCResourceDesinerモジュールをダウンロードしてほしい。

 ダウンロードした.zipファイルを解凍して、モジュールフォルダーに配置すれば準備は完了だ(図1)。モジュールフォルダーには「%ProgramFiles%\WindowsPowerShell\Modules」と「%UserProfile%\Documents\WindowsPowerShell\Modules」の2種類があり、このどちらに配置しても構わない(筆者は、「%UserProfile%」配下のモジュールフォルダーに配置することで、モジュールの利用者をユーザーに限定している。「%ProgramFiles%」配下のモジュールフォルダーにした場合は全ユーザーで利用できるので、用途に応じて使い分けてほしい)。

図1 ダウンロードした.zipファイルを解凍して「%ProgramFiles%」配下のモジュールフォルダーに配置した様子

 このダウンロードから配置までの処理は、もちろん自動化可能だ。筆者も実際は自動化している。しかし、MicrosoftスクリプトセンターはxDSCResourceDesignerのバージョンが変わるたびに参照先が変わるため、自動化の方法の説明については割愛する。

 さて、v5からは(Microsoftスクリプトセンターではなく)PowerShell Gallery(現在、限定プレビューとして利用可能)に公開されたモジュールは、Linuxのapt-getコマンドのように、Install-Moduleという1つのCmdlet(コマンドレット)で簡単にインストール可能になる。実際に試すと、現在は限定的な利用となるが、モジュールのインストールが各段に容易に可能となっているのが確認できる。もしWindows Management Framework 5.0 Previewの最新版をインストールしているならば、管理者として起動したPowerShellでリスト2のコマンドを試してほしい(執筆時点でNovember Previewが最新)。

PowerShell
Install-Module xDSCResourceDesigner -Scope AllUsers
リスト2 v5で導入される、Install-Moduleコマンドレットを用いてモジュールをインストールする例

 このコマンド1つで、xDSCResourceDesignerが「%ProgramFiles%」配下のモジュールフォルダーにインストールされる(図2)。

図2 Install-Moduleコマンドレットを用いてxDSCResourceDesignerをインストールした様子

xDSCResourceDesignerを利用したひな型の生成

 xDSCResourceDesignerを導入したら、PowerShell ISEを管理者として起動してほしい(図3)。これは、xDSCResourceDesignerが管理者として実行を求めるためだ。

図3 Windows8.1で[Window]+[Q]キーで表示される検索メニューからPowerShell ISEを管理者で起動しようとしている様子
図3 Windows8.1で[Window]+[Q]キーで表示される検索メニューからPowerShell ISEを管理者で起動しようとしている様子

 起動したら、リスト3を実行してモジュールを読み込もう。

PowerShell
Import-Module xDSCResourceDesigner
リスト3 xDSCResourceDesignerモジュールを読み込んでいる様子

 それでは、xDSCResourceDesignerを使って、コンフィギュレーションで利用する各種プロパティを定義して、.schema.mofファイルと.psm1ファイルのひな型を生成する。

 リスト4は、表1で定義したプロパティに基づいてプロパティ名と属性を定義し、モジュール名を「SampleResource」、リソース名を「cOSInfoJson」として、(スキーマMOFとモジュールの)ひな型を出力するコードだ。

PowerShell
Import-Module xDSCResourceDesigner

$property = @()
$property += New-xDscResourceProperty -Name Ensure -Type String -Attribute Required -ValidateSet Present, Absent
$property +=New-xDscResourceProperty -Name Path -Type String -Attribute Key
$param = @{
  ModuleName   = "SampleResource"
  Name         = "cOSInfoJson"
  FriendlyName = "cOSInfoJson"
  Property     = $property
  Path         = "$env:ProgramFiles\WindowsPowerShell\Modules"
}
New-xDscResource @param
リスト4 xDSCResourceDesignerを用いてリソースのひな型を定義・出力するコード例

 コードを簡単に説明しよう。

 New-xDscResourcePropertyコマンドレットは、コンフィギュレーションで利用するプロパティを生成してくれる。-Nameパラメーターでプロパティ名、-Typeパラメーターでプロパティの型、-AttributeパラメーターでスキーマMOFの属性、-ValidateSetパラメーターで利用可能な文字列セットを与える。他にも-DescriptionパラメーターでスキーマMOF中にプロパティの概要を記述することもできる。

 -Attributeパラメーターについてもう少し説明しよう。設定可能な値は、KeyRequiredWriteReadの4種類だ。それぞれ、一意性を持つプロパティにはKey、必須性を持つプロパティにはRequired、コンフィギュレーションで設定可能なプロパティにはWrite、コンフィギュレーションには露出しないが状態取得時にのみ露出するプロパティにはReadを与えてほしい。

 リスト4のコードを実行すると、指定したパスに(.schema.mofファイルと.psm1ファイルの)ひな型が生成される(図4)。今回指定したパス「$env:ProgramFiles\WindowsPowerShell\Modules」は、カスタムDSCリソースを設置するパスでもある。自作、あるいは公開されているリソースは、ノードの当該パスに配置することでリソースとして利用できるようになる。

図4 New-xDSCResourceを実行して(スキーマMOFとモジュールの)ひな型が生成された様子

 生成されたツリー構造を見てみよう(図5)。(paramオブジェクトの)ModuleNameに与えた「SampleResource」と同名のフォルダー配下に、「DSCResources」フォルダーが生成されている。そのフォルダーの配下を見ると、各種リソースは、Nameで与えた「cOSInfoJson」と同名のフォルダーの中に、「cOSInfoJson.schema.mof」ファイルと「cOSInfoJson.psm1」ファイルとして生成されたことが分かるだろう。

図5 リソースのツリー構造

 カスタム・リソースは、必ず「<リソース名>.psm1」ファイルと「<リソース名>.schema.mof」ファイルのセットになっている。

 「<リソース名>.schema.mof」がMOFファイルの定義だが、xDSCResourceDesignerで生成した場合は、直接編集する機会はまずないだろう。リスト6は、今回デザイナーから生成されたcOSInfoJson.schema.mofファイルの内容だ。

MOF
[ClassVersion("1.0.0.0"), FriendlyName("cOSInfoJson")]
class cOSInfoJson : OMI_BaseResource
{
  [Required, ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure;
  [Key] String Path;
};
リスト6 cOSInfoJson.schema.mofファイルの内容

 「<リソース名>.psm1」は、実際にどのように動作させるかのロジックを記述するためのモジュールファイルだ。ここに冪等性を担保するように動作ロジックを記述していく。

リソース内部関数の利用タイミング

 さっそく、生成されたcOSInfoJson.psm1ファイルを開いてみよう。PowerShell ISE上からは、リスト5のようにpsedit関数を使って別タブにファイルを開くことができる。

PowerShell
psedit "$env:ProgramFiles\WindowsPowerShell\Modules\SampleResource\DSCResources\cOSInfoJson\cOSInfoJson.psm1"
リスト6 cOSInfoJson.psm1ファイルをPowerShell ISEで開く

 .psm1ファイルを見てみると、すでにxDSCResourceDesignerで示したプロパティに沿ってリソースのひな型が作成できていることが分かる。

図6 生成されたモジュールのひな型

 ひな型として.psm1ファイルに生成されたGet-TargetResourceSet-TargetResourceTest-TargetResourceは、DSCの動作で利用される関数だ。DSCのライフサイクルの中で、それぞれの関数がどのように利用されるかを説明しよう。

 Get-TargetResource関数は、Get-DSCConfigurationコマンドレットで「ノードの現在の状態を取得する」ために呼び出される。そのため、Get-TargetResource関数は実質的にノード状態のテストを行っていることになる。戻り値は[System.HashTable]型で、スキーマMOFのKeyRequiredに指定したプロパティは必ず返却される必要がある。WriteReadに関しては任意だが、筆者は極力返却するように書いている。

 Set-TargetResource関数は、コンフィギュレーションを適用するときに実行される内容だ。まさに状態をあるべき状態にする操作が記述される。あるべき状態にすることを書けばいいので最も記述しやすいだろう。戻り値は[System.Void]型だ。

 Test-TargetResource関数は、現在のノード状態があるべき状態かどうかを判定するために実行される。戻り値は[System.Boolean]型だ。コンフィギュレーションを適用するときにすでに適用済みかをテストする際と、Test-DSCConfigurationコマンドレットで呼び出されるのがこの関数だ。Get/Set/Testで最も頻繁に呼ばれるため、最も軽い処理であることが望まれる。ベストプラクティスとして、Get-TargetResource関数はテストを内包するため、Test-TargetResource関数の内部ではGet-TargetResource関数を単に呼び出すことが推奨されている。

リソースの記述

 今回、リソースのEnsureプロパティには「PresentかAbsent」のみが許容されるように定義した。リスト7のようにしてenum(列挙体)を生成しておくと、Ensureプロパティが扱いやすくなるので、お勧めだ。

PowerShell
# Enum for Ensure
Add-Type -TypeDefinition @"
  public enum EnsureType
  {
    Present,
    Absent
  }
"@ -ErrorAction SilentlyContinue
リスト7 Ensureプロパティに指定できる値が定まっているため、enumを利用する

2015/02/17 コードの一部を変更しました。詳しくは、本ページ最後の【更新履歴】をご参照ください。)

 こうすることによって、Ensureプロパティに指定すべき値を[EnsureType]::Present.ToString()と呼び出せるため、文字列を直接書くことによる誤字を防ぎ、確実に返すべき値を取得できる。

 現行のPowerShell v4では、enumの生成にC#コードをAdd-Typeコマンドレットでオンザフライにコンパイルする必要がある。v5からはEnumキーワードが追加されてもっとシンプルに書けるようになるので楽しみにしていてほしい。(※2015/02/17追記)

Get-TargetResource関数の記述

 それでは、Get-TargetResource関数から書いていこう。Get-TargetResource関数は、スキーマMOFファイルでKey属性に指定したPathプロパティと、Required属性に指定したEnsureプロパティを返す必要がある。

 今回は、「JSONファイルが指定されたパスに存在しない場合」あるいは「存在していても、そのJSONデータにCIMクラス名OSのコンピューター名が適切に入っていない場合(いずれも詳細は後述する。その判定処理は、リスト9のIsCIMClassValidIsCSNameValid関数で行っている)」は、Ensureプロパティが「Absent」(=存在しない)として扱う。これは、リスト8のように記述できる。

PowerShell
function Get-TargetResource
{
  [CmdletBinding()]
  [OutputType([System.Collections.Hashtable])]
  param
  (
    [parameter(Mandatory = $true)]
    [ValidateSet("Present","Absent")]
    [System.String]$Ensure,

    [parameter(Mandatory = $true)]
    [System.String]$Path
  )

  Write-Debug "Obtain Ensure status"
  $Ensure = if (!(Test-Path -Path $Path))
  {
    Write-Verbose "Path not exist."
    [EnsureType]::Absent.ToString()
  }
  else
  {
    Write-Debug "Read content and convert to JsonObject"
    $jsonObject = GetJsonObject -Path $Path

    Write-Debug "Check content is valid."
    if ((IsCIMClassValid -JsonObject $jsonObject) -and (IsCSNameValid -JsonObject $jsonObject))
    {
      Write-Debug "Content detected valid."
      [EnsureType]::Present.ToString()
    }
    else
    {
      Write-Debug "Content detected invalid."
      [EnsureType]::Absent.ToString()
    }
  }
  
  $returnValue = @{
    Ensure = [System.String]$Ensure
    Path = [System.String]$Path
  }
  return $returnValue
}
リスト8 Get-TargetResource関数のロジック

 続いて、リソースで用いられている定石を簡単に説明する。

 まず、Write-VerboseコマンドレットとWrite-Debugコマンドレットの使い分けだ。

 コンフィギュレーションを使ってリソースを適用するとき、PUSHにおいてはStart-DSCConfigurationコマンドレットに-Verboseパラメーターを付けて途中経過を表示させながら実行することが常套(じょうとう)手段だ。そこで、より細かい状態遷移については、VerboseストリームではなくWrite-Debugコマンドレットを使ってデバッグ実行時にのみ出力するように、PowerShellチームのリソースは組まれている。もちろん、伝えるべき進捗(しんちょく)はWrite-Verboseコマンドレットを使ってVerboseストリームに出力するといいだろうが、Write-Debugコマンドレット実行時により詳細の進捗が分かることは望ましいだろう。

 今回、Get-TargetResource関数で利用する、3つの関数「GetJsonObject」「IsCIMClassValid」「IsCSNameValid」を切り出した(リスト9)。この程度ならば、わざわざ関数として切り出さなくても問題ない。しかし、リソースの規模が大きくなってきたら、関数を分離して、その処理の影響範囲を関数内に封じ込めた方が制御しやすいだろう。今回は「ファイルが必ずJSON形式である」という前提で記述しているが、より正確に制御したければ、リスト9に示すようにGetJsonObject関数で判定すればいいだろう。

PowerShell
function GetJsonObject
{
  [CmdletBinding()]
  [OutputType([PSCustomObject])]
  param
  (
    [parameter(Mandatory = $true)]
    [System.String]$Path
  )

  $json = Get-Content -Path $Path -Encoding UTF8 -Raw | ConvertFrom-Json
  return $json
}

function IsCIMClassValid
{
  [CmdletBinding()]
  [OutputType([System.Boolean])]
  param
  (
    [parameter(Mandatory = $true)]
    [PSCustomObject]$JsonObject
  )

  return $JsonObject.CimClass.CimSuperClassName -eq "CIM_OperatingSystem"
}

function IsCSNameValid
{
  [CmdletBinding()]
  [OutputType([System.Boolean])]
  param
  (
    [parameter(Mandatory = $true)]
    [PSCustomObject]$JsonObject
  )

  return $JsonObject.CSName -eq [System.Net.DNS]::GetHostName()
}
リスト9 切り出した3つの関数

 リスト9のIsCIMClassValid関数では、CIMクラス名(=CimClassオブジェクトのスーパークラス名)が、「CIM_OperatingSystem」(OS)として適切に出力されているかを判定している。

 またIsCSNameValid関数では、ノード名(=そのCIM_OperatingSystemオブジェクトのCSNameプロパティとして取得できるコンピューターシステム名)が、ホスト名(=[System.Net.DNS]::GetHostNameメソッドの戻り値)と一致しているかを判定している。

Test-TargetResource関数の記述

 リソースの3つの関数を書く順番は自由だ。普段筆者は、最初にGet-TargetResource関数を書いて、次にTest-TargetResource関数を書くことにしている。というのも、Get-TargetResource関数で「現在の状態があるべき状態であるかの判定」を行っているので、Test-TargetResource関数はその結果を取得するだけでよいからだ。

 今回の場合、現在の状態がEnsureプロパティに与えた「Present/Absent」と一致しているかで表現できるため、リスト10のように非常にシンプルなコードとなる。

PowerShell
function Test-TargetResource
{
  [CmdletBinding()]
  [OutputType([System.Boolean])]
  param
  (
    [parameter(Mandatory = $true)]
    [ValidateSet("Present","Absent")]
    [System.String]
    $Ensure,

    [parameter(Mandatory = $true)]
    [System.String]
    $Path
  )

  [System.Boolean]$result = (Get-TargetResource -Ensure $Ensure -Path $Path).Ensure -eq $Ensure
  return $result
}
リスト10 Test-TargetResource関数のロジック

 いかがだろうか。先述したようにTest-TargetResource関数は、Get-TargetResource関数を呼び出すようにして作成するのも定石であり、ロジックの分散を招かないための有用な手法である。

Set-TargetResource関数の記述

 最後にSet-TargetResource関数で、あるべき状態にするためのロジックを記述する。普段、PowerShellスクリプト関数を書いている人にとっては、一番書きやすいのではないだろうか。

 今回は、Ensureプロパティが「Absent」の場合は、Pathプロパティに指定したJSONファイルが存在しなければ正常だ。「Present」の場合は、Pathプロパティに指定されたファイルに、Win32_OperatingSystemオブジェクトの内容をJSON形式で出力する。これを記述したのが、リスト11である。

PowerShell
function Set-TargetResource
{
  [CmdletBinding()]
  [OutputType([Void])]
  param
  (
    [parameter(Mandatory = $true)]
    [ValidateSet("Present","Absent")]
    [System.String]
    $Ensure,

    [parameter(Mandatory = $true)]
    [System.String]
    $Path
  )

  #Ensure = "Absent"
  if ($Ensure -eq [EnsureType]::Absent.ToString())
  {
    if (Test-Path -Path $Path)
    {
      Write-Debug ("File found at '{0}'. Removing file." -f $Path)
      Remove-Item -Path $Path -Force > $null
      return;
    }
  }

  # Ensure = "Present"
  $json = Get-CimInstance -ClassName Win32_OperatingSystem | ConvertTo-Json
  $parent = Split-Path -Path $Path -Parent
  if (!(Test-Path -Path $parent))
  {
    New-Item -Path $parent -ItemType Directory -Force > $null
  }
  Write-Debug ("Output Win32_OperatingSystem information to Path '{0}'." -f $Path)
  Out-File -InputObject $json -FilePath $Path -Encoding utf8 -Force
}
リスト11 Set-TargetResource関数のロジック

*-TargetResource関数のエキスポート

 「<リソース名>.psm1」(モジュールファイル)の最後で、Get-TargetResourceSet-TargetResourceTest-TargetResourceの3関数をエキスポートして作成完了だ。

 xDSCResourceDesignerで「<リソース名>.psm1」ファイルを生成すると、リスト12のように「*-TargetResource」とワイルドカード(*)で関数を指定して出力するように書かれているので、特に追記する必要はない。

PowerShell
Export-ModuleMember -Function *-TargetResource
リスト12 Get-TargetResource、Set-TargetResource、Test-TargetResourceの3関数をエキスポート

 最後に、cOSInfoJson.psm1ファイルに記述したコードの全体像をリスト13に示す。

PowerShell
# Enum for Ensure
Add-Type -TypeDefinition @"
  public enum EnsureType
  {
    Present,
    Absent
  }
"@ -ErrorAction SilentlyContinue

function Get-TargetResource
{
  [CmdletBinding()]
  [OutputType([System.Collections.Hashtable])]
  param
  (
    [parameter(Mandatory = $true)]
    [ValidateSet("Present","Absent")]
    [System.String]$Ensure,

    [parameter(Mandatory = $true)]
    [System.String]$Path
  )

  Write-Debug "Obtain Ensure status"
  $Ensure = if (!(Test-Path -Path $Path))
  {
    Write-Verbose "Path not exist."
    [EnsureType]::Absent.ToString()
  }
  else
  {
    Write-Debug "Read content and convert to JsonObject"
    $jsonObject = GetJsonObject -Path $Path

    Write-Debug "Check content is valid."
    if ((IsCIMClassValid -JsonObject $jsonObject) -and (IsCSNameValid -JsonObject $jsonObject))
    {
      Write-Debug "Content detected valid."
      [EnsureType]::Present.ToString()
    }
    else
    {
      Write-Debug "Content detected invalid."
      [EnsureType]::Absent.ToString()
    }
  }
  
  $returnValue = @{
    Ensure = [System.String]$Ensure
    Path = [System.String]$Path
  }
  return $returnValue
}

function Set-TargetResource
{
  [CmdletBinding()]
  [OutputType([Void])]
  param
  (
    [parameter(Mandatory = $true)]
    [ValidateSet("Present","Absent")]
    [System.String]
    $Ensure,

    [parameter(Mandatory = $true)]
    [System.String]
    $Path
  )

  #Ensure = "Absent"
  if ($Ensure -eq [EnsureType]::Absent.ToString())
  {
    if (Test-Path -Path $Path)
    {
      Write-Debug ("File found at '{0}'. Removing file." -f $Path)
      Remove-Item -Path $Path -Force > $null
      return;
    }
  }

  # Ensure = "Present"
  $json = Get-CimInstance -ClassName Win32_OperatingSystem | ConvertTo-Json
  $parent = Split-Path -Path $Path -Parent
  if (!(Test-Path -Path $parent))
  {
    New-Item -Path $parent -ItemType Directory -Force > $null
  }
  Write-Debug ("Output Win32_OperatingSystem information to Path '{0}'." -f $Path)
  Out-File -InputObject $json -FilePath $Path -Encoding utf8 -Force
}

function Test-TargetResource
{
  [CmdletBinding()]
  [OutputType([System.Boolean])]
  param
  (
    [parameter(Mandatory = $true)]
    [ValidateSet("Present","Absent")]
    [System.String]
    $Ensure,

    [parameter(Mandatory = $true)]
    [System.String]
    $Path
  )

  [System.Boolean]$result = (Get-TargetResource -Ensure $Ensure -Path $Path).Ensure -eq $Ensure
  return $result
}

function GetJsonObject
{
  [CmdletBinding()]
  [OutputType([PSCustomObject])]
  param
  (
    [parameter(Mandatory = $true)]
    [System.String]$Path
  )

  $json = Get-Content -Path $Path -Encoding UTF8 -Raw | ConvertFrom-Json
  return $json
}

function IsCIMClassValid
{
  [CmdletBinding()]
  [OutputType([System.Boolean])]
  param
  (
    [parameter(Mandatory = $true)]
    [PSCustomObject]$JsonObject
  )

  return $JsonObject.CimClass.CimSuperClassName -eq "CIM_OperatingSystem"
}

function IsCSNameValid
{
  [CmdletBinding()]
  [OutputType([System.Boolean])]
  param
  (
    [parameter(Mandatory = $true)]
    [PSCustomObject]$JsonObject
  )

  return $JsonObject.CSName -eq [System.Net.DNS]::GetHostName()
}

Export-ModuleMember -Function *-TargetResource
リスト13 作成したcOSInfoJsonリソースの全体像

2015/02/17 コードの一部を変更しました。詳しくは、本ページ最後の【更新履歴】をご参照ください。)

リソースのテスト

 先述したPowerShell Teamが公開しているベストプラクティスにもあるが、リソースが書けたら、可能な限りテストを行ってほしい。テスト手法については以下の手順をパスすることが望ましい。

  1. モジュールファイルに記述したGet/Test/Set-TargetResource関数を直接呼び出して意図した動作をすること
  2. 付随する関数が意図した動作をすること
  3. Import-Moduleコマンドレットでリソースのモジュールが正常に読み込めること
  4. Get-DscResourceコマンドレットでリソースが取得できること
  5. コンフィギュレーションを記述してStart-DSCConfigurationコマンドレットで実行したときに、エラーなく、あるべき状態になること
  6. あるべき状態になったときに、Test-DSCConfigurationコマンドレットで状態が「true」として取得できること
  7. あるべき状態でなくなったときに、Test-DSCConfigurationコマンドレットで状態が「false」として取得できること
  8. 構成の適用後、Get-DSCConfigurationコマンドレットでGet-TargetResource関数に記述した通りに現在のノード状態が取得できること

 テストには、テスティングフレームワークの「Pester」を用いることが、PowerShellチームからも推奨されており、デファクトスタンダードといえる。本記事では、リソースのテスト詳細については扱わないが、リソースは公開されて多くの利用者の環境で動作するため、テストを十分に行うことが望ましいだろう。筆者自身、テストが不十分だったために意図しない挙動を招いたことがあったので、心より推奨したい。

コンフィギュレーションでリソースを呼び出す

 リソースが書けてテストが全て通ったものとしよう。すでにリソースは「$env:ProgramFiles\WindowsPowerShell\Modules」フォルダーに配置されているので、さっそくコンフィギュレーションを書いてみよう。

 まずは、リソースが正しく配置されて、スキーマMOFも正常に解釈されているかの確認だ。リスト14のGet-DscResourceコマンドレットを実行してほしい。

PowerShell
Get-DscResource -Name cOSInfoJson
リスト14 Get-DscConfigurationコマンドレットの実行

 図7のように、cOSInfoJsonリソースが読み込まれていれば正しい挙動だ。

図7 リソースが正常に読み込めていることを確認する

 読み込めていることが確認できたので、localhost(自分自身)に対してコンフィギュレーションを実施してみる。

EnsureプロパティをPresentとする

 まずは、OS情報のJSONファイルを出力するように、コンフィギュレーションを書いてみよう。Configurationの書き方に関しては、前回の記事を参考にしてほしい。リスト15が、作成したコンフィギュレーションだ。

PowerShell
configuration osInfoPresent
{
  Import-DscResource -ModuleName SampleResource
  cOSInfoJson FilePresent
  {
    Ensure = "Present"
    Path = "c:\hoge\test.json"
  }
}

osInfoPresent
Start-DscConfiguration -Wait -Force -Verbose -Path osInfoPresent
リスト15 .jsonファイルを出力するように指定したコンフィギュレーション

 Import-DscResourceキーワードを使って、作成したリソースが含まれるモジュール名「SampleResource」を指定している。これで、作成したリソース「cOSInfoJson」がコンフィギュレーションで利用できるようになる。

 「cOSInfoJson」というリソースのフレンドリ名と定義名(この例では「FilePresent」)を指定したら、スキーマMOFで指定したプロパティ(EnsurePath)を宣言しよう。図8のように、リソース名にカーソルを合わせて、CtrlスペースキーでIntelliSense(=入力候補の一覧ウィンドウ)に利用可能なプロパティが表示される。ここでは、「c:\hoge\test.json」にファイルが存在してほしい「Present」と宣言している。

図8 リソースで利用可能なプロパティがIntelliSenseに表示される

 「osInfoPresent」コンフィギュレーションを実行することで、MOFファイルが現在のディレクトリに生成される。あとは、Start-DscConfigurationコマンドレットで自分自身に適用してみよう。

 図9が実行結果だ。水色の6行目の中ほどにある「開始 テスト」という行が、Test-TargetResource関数が実行されたことを示す。「開始 設定」という行は、Set-TargetResource関数が実行されたことを示す。初めて実行したので、テスト結果(=「Path not exist.」)にある通り、Pathプロパティに指定したファイルが存在しない。そのため、ファイルが生成されている。

図9 初めてコンフィギュレーションを実行した結果

 生成されたファイルも、意図した通り、JSON形式で出力されている。

図10 生成されたJSONファイルを読み込んでオブジェクトに変換した様子

再実行でテストが通り、設定がスキップされることの確認

 一度、ファイルが生成されたら、正しくCimClassオブジェクトが「OS」で、そのCSNameプロパティが「ホスト名」と合致していれば、「それは望んだファイル」と判定するようにGet-TargetResource関数で組んだ。

 図11は、実際にコンフィギュレーションを再実行して、今度は設定がスキップされることを確認した様子だ。

図11 すでに設定が完了しているため、設定がスキップされた様子

 Test-DscConfigurationコマンドレットを使うと(図12)、あるべき状態かをテストした結果、「true」となっていることが分かる。

図12 すでに「あるべき状態」であるため、Test-DscConfigurationコマンドレットの実行結果はtrueとなっている
図12 すでに「あるべき状態」であるため、Test-DscConfigurationコマンドレットの実行結果はtrueとなっている

 また、Get-DscConfigurationコマンドレットを使うと(図13)、現在の状態が取得でき、ファイルパスと共にEnsureプロパティが「Present」(存在する)と評価されていることが分かる。

図13 Get-DscConfigurationコマンドレットの実行結果で「現在の状態」を取得した様子

EnsureプロパティをAbsentとする

 リスト15では、コンフィギュレーション適用時にEnsureプロパティを「Present」(存在する)として実行した。

 リスト16は、Ensureプロパティを「Absent」(存在しない)として適用するコンフィギュレーションだ。リソースが正しく動作すれば、Pathプロパティに指定したJSONファイルが削除されるはずだ。

PowerShell
configuration osInfoAbsent
{
  Import-DscResource -ModuleName SampleResource
  cOSInfoJson FileAbsent
  {
    Ensure = "Absent"
    Path = "c:\hoge\test.json"
  }
}

osInfoAbsent
Start-DscConfiguration -Wait -Force -Verbose -Path osInfoAbsent
リスト16 .jsonファイルが存在しないように指定したコンフィギュレーション

 図14が、その実行結果だ。指定したパスにすでにJSONファイルが存在したため、「開始 設定」として削除処理が起動したことが分かる。

図14 .jsonファイルが存在しないように指定したコンフィギュレーションの実行結果

 図15は、再度実行した結果だ。すでにパスに存在したJSONファイルは削除済みのため、「スキップ 設定」と、処理がスキップされたことが分かる。

図15 すでに設定が完了しているため、設定がスキップされた様子

 以上が、リソースを0から作成して、コンフィギュレーションで実際に適用するまでの流れだ。

 いかがだっただろうか。すでにPowerShellを利用したことがあり、モジュールを書いたことがある人にとっては、新しく覚える必要のある概念はごく少なかったのではないだろうか。

 多くの人にとって、本記事がリソースを書くときの参考となることを願っている。

【更新履歴】2015/02/17

 リスト7とリスト13を以下の理由で修正しました。

 Add-Typeコマンドレットでは、同じ型を追加すると、その(2度目以降の)追加時に「既に読み込み済み」のエラーが発生します。これを抑制するために、try{...}catch{...}を使って回避していました。このコードをより自然に回避する手段として、-ErrorAction SilentlyContinueによるコードに変更しました。

1. PowerShell DSCで導入された新しい構文キーワード

Windowsインフラ環境の構築を自動化できる「PowerShell DSC」とは? その使い方を紹介。DSCの構文をコードで示しながら、基本的な実践手順を説明する。

2. 【現在、表示中】≫ 本番で使えるPowerShell DSCリソース作成入門

Windows Serverの構成管理を自動化できるツール「PowerShell DSC」を使いこなそう。PowerShellでDSCのリソースを自作する方法とは?

3. PowerShell v5の新機能と、実戦で使ってほしい機能

Windows 10に標準搭載され、Windows 7/8.1/Server 2008/2012向けにもリリースされたWMF 5.0に同梱されるPowerShell 5.0の新機能と、PowerShellユーザーに特にお勧めの機能を紹介する。

サイトからのお知らせ

Twitterでつぶやこう!