SaaS

ポリシー複製編 Microsoft Intuneをコードで管理するConfiguration as CodeをAzure DevOpsを使ってやってみた

こんにちは、臼田です。

みなさん、設定をコードで管理してますか?(挨拶

この記事は下記の続編です。

背景や実施内容、事前準備などは上記をご確認ください。

今回は以下のように、開発用テナントのIntuneからデバイスコンプライアンスポリシーをExportして、本番用テナントのIntuneにImportします。

具体的な処理内容としては、deviceManagement/deviceCompliancePoliciesからデバイスコンプライアンスポリシーをすべて取得し、ポリシーを1つずつdeviceManagement/deviceCompliancePoliciesへPOSTします。

やってみた

手順は下記の通りです。

  1. スクリプト作成
  2. パイプラインの定義
  3. デプロイ

スクリプト作成

前回の続きで作成したAzure DevOpsのConfiguration as Codeプロジェクトで、リポジトリにスクリプトを追加していきます。intune-devicecomplianceフォルダを開き「New -> File」からファイルを追加します。

ファイル名をCompliancePolicy_Export_Import.ps1とし「Create」します。

Export/Importするスクリプトを作成します。元となるコードはCompliancePolicy_Export.ps1CompliancePolicy_Import_FromJSON.ps1です。動作させる完全なコードは下記の通りです。

<#

.COPYRIGHT
Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
See LICENSE in the project root for license information.

#>

####################################################

# Get Environment Params

param(
    [Parameter(Mandatory = $True)]
    [string]$appId_DEV,
    [Parameter(Mandatory = $True)]
    [string]$appSecret_DEV,
    [Parameter(Mandatory = $True)]
    [string]$tenantId_DEV,
    [Parameter(Mandatory = $True)]
    [string]$appId_PROD,
    [Parameter(Mandatory = $True)]
    [string]$appSecret_PROD,
    [Parameter(Mandatory = $True)]
    [string]$tenantId_PROD
)

Install-Module Microsoft.Graph -AllowPrerelease -AllowClobber -Force
# Install-Module Microsoft.Graph -AllowPrerelease

# Add environment variables to be used by Connect-MgGraph.
$Env:AZURE_CLIENT_ID = $appId_DEV #application id of the client app
$Env:AZURE_TENANT_ID = $tenantId_DEV #Id of your tenant
$Env:AZURE_CLIENT_SECRET = $appSecret_DEV #secret of the client app

# Tell Connect-MgGraph to use your environment variables.
Connect-MgGraph -EnvironmentVariable

####################################################

Function Get-DeviceCompliancePolicy() {

    <#
    .SYNOPSIS
    This function is used to get device compliance policies from the Graph API REST interface
    .DESCRIPTION
    The function connects to the Graph API Interface and gets any device compliance policies
    .EXAMPLE
    Get-DeviceCompliancePolicy
    Returns any device compliance policies configured in Intune
    .EXAMPLE
    Get-DeviceCompliancePolicy -Android
    Returns any device compliance policies for Android configured in Intune
    .EXAMPLE
    Get-DeviceCompliancePolicy -iOS
    Returns any device compliance policies for iOS configured in Intune
    .NOTES
    NAME: Get-DeviceCompliancePolicy
    #>

    [cmdletbinding()]

    param
    (
        [switch]$Android,
        [switch]$iOS,
        [switch]$Win10
    )

    $graphApiVersion = "v1.0"
    $Resource = "deviceManagement/deviceCompliancePolicies"
    
    try {

        $Count_Params = 0

        if ($Android.IsPresent) { $Count_Params++ }
        if ($iOS.IsPresent) { $Count_Params++ }
        if ($Win10.IsPresent) { $Count_Params++ }

        if ($Count_Params -gt 1) {
        
            write-host "Multiple parameters set, specify a single parameter -Android -iOS or -Win10 against the function" -f Red
        
        }
        
        elseif ($Android) {
        
            $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)"
        (Invoke-MgGraphRequest -Uri $uri -Method Get).Value | Where-Object { ($_.'@odata.type').contains("android") }
        
        }
        
        elseif ($iOS) {
        
            $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)"
        (Invoke-MgGraphRequest -Uri $uri -Method Get).Value | Where-Object { ($_.'@odata.type').contains("ios") }
        
        }

        elseif ($Win10) {
        
            $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)"
        (Invoke-MgGraphRequest -Uri $uri -Method Get).Value | Where-Object { ($_.'@odata.type').contains("windows10CompliancePolicy") }
        
        }
        
        else {

            $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)"
        (Invoke-MgGraphRequest -Uri $uri -Method Get).Value

        }

    }
    
    catch {

        $ex = $_.Exception
        $errorResponse = $ex.Response.GetResponseStream()
        $reader = New-Object System.IO.StreamReader($errorResponse)
        $reader.BaseStream.Position = 0
        $reader.DiscardBufferedData()
        $responseBody = $reader.ReadToEnd();
        Write-Host "Response content:`n$responseBody" -f Red
        Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)"
        write-host
        break

    }

}

####################################################

Function Export-JSONData() {

    <#
    .SYNOPSIS
    This function is used to export JSON data returned from Graph
    .DESCRIPTION
    This function is used to export JSON data returned from Graph
    .EXAMPLE
    Export-JSONData -JSON $JSON
    Export the JSON inputted on the function
    .NOTES
    NAME: Export-JSONData
    #>

    param (

        $JSON,
        $ExportPath

    )

    try {

        if ($JSON -eq "" -or $JSON -eq $null) {

            write-host "No JSON specified, please specify valid JSON..." -f Red

        }

        elseif (!$ExportPath) {

            write-host "No export path parameter set, please provide a path to export the file" -f Red

        }

        elseif (!(Test-Path $ExportPath)) {

            write-host "$ExportPath doesn't exist, can't export JSON Data" -f Red

        }

        else {

            $JSON1 = ConvertTo-Json $JSON -Depth 5

            $JSON_Convert = $JSON1 | ConvertFrom-Json

            $displayName = $JSON_Convert.displayName

            # Updating display name to follow file naming conventions - https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx
            $DisplayName = $DisplayName -replace '\<|\>|:|"|/|\\|\||\?|\*', "_"

            $Properties = ($JSON_Convert | Get-Member | ? { $_.MemberType -eq "NoteProperty" }).Name

            $FileName_CSV = "$DisplayName" + "_" + $(get-date -f dd-MM-yyyy-H-mm-ss) + ".csv"
            $FileName_JSON = "$DisplayName" + "_" + $(get-date -f dd-MM-yyyy-H-mm-ss) + ".json"

            $Object = New-Object System.Object

            foreach ($Property in $Properties) {

                $Object | Add-Member -MemberType NoteProperty -Name $Property -Value $JSON_Convert.$Property

            }

            write-host "Export Path:" "$ExportPath"

            $Object | Export-Csv -LiteralPath "$ExportPath\$FileName_CSV" -Delimiter "," -NoTypeInformation -Append
            $JSON1 | Set-Content -LiteralPath "$ExportPath\$FileName_JSON"
            # write-host "CSV created in $ExportPath\$FileName_CSV..." -f cyan
            write-host "JSON created in $ExportPath\$FileName_JSON..." -f cyan
            
        }

    }

    catch {

        $_.Exception

    }

}

####################################################

Function Add-DeviceCompliancePolicy() {

    <#
    .SYNOPSIS
    This function is used to add a device compliance policy using the Graph API REST interface
    .DESCRIPTION
    The function connects to the Graph API Interface and adds a device compliance policy
    .EXAMPLE
    Add-DeviceCompliancePolicy -JSON $JSON
    Adds an Android device compliance policy in Intune
    .NOTES
    NAME: Add-DeviceCompliancePolicy
    #>

    [cmdletbinding()]

    param
    (
        $JSON
    )

    $graphApiVersion = "v1.0"
    $Resource = "deviceManagement/deviceCompliancePolicies"

    try {

        if ($JSON -eq "" -or $JSON -eq $null) {

            write-host "No JSON specified, please specify valid JSON for the Android Policy..." -f Red

        }

        else {

            Test-JSON -JSON $JSON

            $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)"
            # Invoke-MgGraphRequest -Uri $uri -Method Post -Body $JSON

        }

    }

    catch {

        Write-Host
        $ex = $_.Exception
        $errorResponse = $ex.Response.GetResponseStream()
        $reader = New-Object System.IO.StreamReader($errorResponse)
        $reader.BaseStream.Position = 0
        $reader.DiscardBufferedData()
        $responseBody = $reader.ReadToEnd();
        Write-Host "Response content:`n$responseBody" -f Red
        Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)"
        write-host
        break

    }

}

####################################################

$ExportPath = '.'

# If the directory path doesn't exist prompt user to create the directory
$ExportPath = $ExportPath.replace('"', '')

if (!(Test-Path "$ExportPath")) {

    Write-Host
    Write-Host "Path '$ExportPath' doesn't exist" -ForegroundColor Red
    Write-Host
    break

}

Write-Host

####################################################

# Export

$CPs = Get-DeviceCompliancePolicy

####################################################

# Import auth

# Add environment variables to be used by Connect-MgGraph.
$Env:AZURE_CLIENT_ID = $appId_PROD #application id of the client app
$Env:AZURE_TENANT_ID = $tenantId_PROD #Id of your tenant
$Env:AZURE_CLIENT_SECRET = $appSecret_PROD #secret of the client app

# Tell Connect-MgGraph to use your environment variables.
Connect-MgGraph -EnvironmentVariable

# Import

foreach ($CP in $CPs) {

    write-host "Device Compliance Policy:"$CP.displayName -f Yellow
    # Export-JSONData -JSON $CP -ExportPath "$ExportPath"
    Write-Host
    
    # Excluding entries that are not required - id,createdDateTime,lastModifiedDateTime,version
    $JSON_Convert = $CP | Select-Object -Property * -ExcludeProperty id, createdDateTime, lastModifiedDateTime, version
    
    $DisplayName = $JSON_Convert.displayName
    
    $JSON_Output = $JSON_Convert | ConvertTo-Json -Depth 5
    
    # Adding Scheduled Actions Rule to JSON
    $scheduledActionsForRule = '"scheduledActionsForRule":[{"ruleName":"PasswordRequired","scheduledActionConfigurations":[{"actionType":"block","gracePeriodHours":0,"notificationTemplateId":"","notificationMessageCCList":[]}]}]'        
    
    $JSON_Output = $JSON_Output.trimend("}")
    
    $JSON_Output = $JSON_Output.TrimEnd() + "," + "`r`n"
    
    # Joining the JSON together
    $JSON_Output = $JSON_Output + $scheduledActionsForRule + "`r`n" + "}"
    
    # write-host
    # write-host "Compliance Policy '$DisplayName' Found..." -ForegroundColor Yellow
    # write-host
    $JSON_Output
    write-host
    Write-Host "Adding Compliance Policy '$DisplayName'" -ForegroundColor Yellow
    Add-DeviceCompliancePolicy -JSON $JSON_Output

}

雑に2つのスクリプトをくっつけています。Exportした$CPsを1つずつ取り出し、Importするための前処理としてSelect-Objectで邪魔なパラメータを除去し、何やらよくわからない値を付け加えてImportを実施しています。

前回の記事で説明しましたが、この処理は冪等な処理ではありません。本来であれば既存環境の設定状況を確認したり、その状態を管理する必要があります。今回はあくまでコンセプトを動かすにとどめます。

コードを入力したら「Commit」していきます。

適当なメッセージを入れて「Commit」します。

パイプラインの定義

続いて新しいパイプラインを作成します。前回と同じ手順で新規のパイプラインを作成していき、パイプラインの定義を入れます。下記の定義です。

variables:
- name: vmImageName
  value: windows-latest
- group: configuration-as-code

trigger:
- none

pool:
  vmImage: $(vmImageName)

steps:
- task: PowerShell@2
  displayName: 'Run CompliancePolicy_Export_Import.ps1'
  inputs:
    pwsh: true
    targetType: filePath
    filePath: '$(Build.Repository.LocalPath)\\intune-devicecompliance\CompliancePolicy_Export_Import.ps1'
    arguments: -tenantId_DEV '$(tenantId_DEV)' -appId_DEV '$(appId_DEV)' -appSecret_DEV '$(appSecret_DEV)' -tenantId_PROD '$(tenantId_PROD)' -appId_PROD '$(appId_PROD)' -appSecret_PROD '$(appSecret_PROD)'
    ignoreLASTEXITCODE: true

パスはintune-devicecompliance/sync-compliancepolicy-pipeline.ymlを指定します。

今回はpwsh: trueを入れていますが、これによりPowerShellのバージョンが7になるようです。前回はこれを指定しなくても通りましたが、元記事の想定ではPowerShell7を利用するとしているので、こちらを設定したほうが良さそうです。今回のスクリプトでは無いと適切に動作しませんでした。

「Save and run」でコミットして進めます。

適当なメッセージを入れて開始します。

デプロイ

「Save and run」するとデプロイが開始されます。

初回は設定した変数グループをパイプラインから利用することを承認するように要求されるので、「Permit」します。今回は1分半ぐらいで完了しました。

それでは結果を確認していきます。本番用のIntuneでデバイスコンプライアンスポリシーが3つとも複製されていることが確認できました。

ちなみに、もう一度実行すると先述の通り、もう3個追加されます。既存で設定が存在するかを確認したり、既存の設定に上書きするなどは各環境に合わせてロジックを検討する必要があるでしょう。

まとめ

Microsoft IntuneをAzure DevOpsでConfiguration as Codeしていくシリーズ2本を書きました。コンセプトとしては面白いですが、実運用に持っていくための作り込みがまだまだ必要そうであることと、宣言的に定義して冪等な設定をしていくには、仕組みが足りていないのが現状です。例えばTerraformでこれを実現するようなIssueも検索するとあったりしましたが、あまりアクティブではないようだったので、そういった仕組みが待ち望まれますね。

臼田 佳祐

AWSとセキュリティやってます。普段はクラスメソッドで働いてます。クラウドネイティブでは副業としてセキュリティサービスの検証とかやってます。