SaaS

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

こんにちは、臼田です。

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

今回はMicrosoft Intuneの設定をコードで管理するという、いわゆるConfiguration as Code(以下CaC)をAzure DevOpsで実現する方法を実際に試してみたいと思います。元となる記事は下記です。

Configuration as Code for Microsoft Intune – Microsoft Community Hub

こちらの元記事では、2つのIntuneテナントを使ってCaCを行うためのコンセプトとパイプラインの組み方のサンプルが上げられていますが、読み解いていくといくつか不明瞭な点があったので、本記事ではそのあたりを補いつつ、具体的に目的を達成する手順を記載します。

元記事のコンセプト

元記事を要約すると以下のようなことが書かれています。

  • Intuneの設定をコードで管理することができる
  • 例えばAzure DevOpsを利用して現在と過去の設定を管理するリポジトリを作成できる
  • パイプラインにより設定の展開を自動化できる
  • このような仕組みを利用することにより、開発用のテナントと本番用のテナントに同じ設定を適用できる

Intuneの設定をコードで管理できることは素晴らしいことです。人の手を介さない安全で正確な展開が可能になり、検証された設定をミスなく本番環境に適用できたり、ポリシーに基づいたチェックのフローを挟むことが可能です。

しかしながら、これを実現することは簡単ではありません。

元記事を読み解いてわかること

元記事を読み解いていくと、大事なことが書かれていないことに気づきました。私が気をつけなければならないと感じたポイントを下記にあげます。

  • Intuneの設定を宣言的に管理する手法は現在存在していない
  • 元記事にはこのパイプラインを実現する完全なコードが提示されていない
  • 一般的に使い回せるIntuneのCaCを実現するスクリプトが存在していない

特に気をつける点は一番最初に書いた宣言的な管理方法が無いことです。例えば、AWSやAzureの多くの構成要素はTerraformを利用して宣言的なInfrastructure as Code(IaC)を実現することが可能です。これは、どのような状態にしたいかをコードで定義して適用でき、現在そのリソースが存在するかしないか、どんな設定かに依存せずにあるべき状態にしていくことが可能であり、冪等性のある処理が可能です。

元記事を最初に確認したときは、そのような宣言的なCaCを期待しましたが、そこに書かれていたのはMicrosoft Graphを経由したAPI実行をPowerShellスクリプトから実行する管理方法であり、スクリプト内では直接作成や変更をリクエストしているため、宣言的ではなく、冪等な処理を担保するものではありませんでした。

本記事で実現すること

とはいえ、PowerShellスクリプトを作り込んでいけば、自社の要件にあったコードによるIntuneの管理を実現することはできなくはありません。(おそらく)

本記事と次回の記事では、元記事で紹介されている下記2つのスクリプトを実際に試してみます。

  • 1つのIntune環境にデバイスコンプライアンスポリシーを作成するスクリプト(本記事)
  • 開発用のIntuneのデバイスコンプライアンスポリシーをエクスポートして本番用のIntuneにインポートする(次回の記事)

どちらも実運用していくためには、本記事に書いていないことを色々考えて適用していく必要があるという点を先に強調させていただきます。その最初の一歩を体験するものだとお考えください。

本記事で実現する内容を簡単に図にするとこんな感じです。

全体のコンセプト上、上記の図にも本番用のテナントとIntuneが描かれていますが、今回はほぼ登場しません。

前提条件

今回用意したのは以下の環境です。

  • 2つのIntune環境(2つのAADテナント)
  • Azure DevOpsを利用できる環境

Intuneについては試用版を利用しています。Azure DevOpsの環境は、必要なライセンスを確保しておいてください。

やってみた

それではやっていきます。ざっくり手順は下記の通りです。

  1. Intuneの準備
  2. アプリ登録
  3. Azure DevOps作成
  4. 設定導入用PS1作成
  5. 変数グループ作成
  6. パイプライン定義
  7. デプロイ

Intuneの準備

まず試用版のIntuneを用意したので、念の為この手順を軽く紹介します。クイック スタート: Microsoft Intune を無料で試す | Microsoft Learnに登録手順と登録のためのリンクが書かれているのでこちらから2つの環境を作成しましょう。(1つは次回使います)

なお、新しいメールアドレスが必要になるので、私はこれも別途作成しておきました。

アプリ登録

Intuneの環境が出来上がったらAzure Active Directory 管理センターで外部から操作するためのアプリを登録します。「Azure Active Directory -> アプリの登録」から「新規登録」を選択します。

適当な名前を入れます。ここでは元記事通りConfiguration as Codeとしています。「サポートされているアカウントの種類」はそのままで、「リダイレクト URI (省略可能)」は右側の値は空でいいのですが、プルダウンリストは選択する必要があったためWebを選択しています。これで「登録」します。

アプリの作成が完了すると「アプリケーション(クライアント)ID」が生成されるのでこれを控えておきます。ついでにAADのディレクトリ(テナント)IDも利用するためここで控えます。

次に作成したアプリにアクセス許可を設定します。「APIのアクセス許可」から「アクセス許可の追加」を選択し、「Microsoft Graph」を選択します。

「アプリケーションの許可」を選択し、検索欄にDeviceManagementと入力すると「DeviceManagementConfiguration」の2つのアクセス許可が表示されるので、2つ共選択して「アクセス許可の追加」をします。

許可を追加した後は管理者の同意が必要になるため、「xxxに管理者の同意を与えます」を押してポップアップで「はい」を押します。

状態が緑になれば完了です。

続いて、外部からこのアプリを利用するために必要なクライアントシークレットを作成します。「証明書とシークレット -> クライアントシークレット」から「新しいクライアントシークレット」を押し適当な名前(今回は元記事通りconfigascode)をつけて「追加」します。

追加できたら、クライアントシークレットの値を控えておきます。

以上でアプリの登録は完了です。次回の記事ではもう1つIntuneのテナントとアプリを利用するため、ここまでの手順をもう一度実行しておきます。

Azure DevOps作成

Intuneが準備できたらAzure DevOpsの準備に取り掛かります。今回は2つのIntuneとは別のテナントで用意していますが、同じテナントでも問題ないでしょう。

Azure DevOpsのポータルにアクセスし、「新しい組織の作成」をします。

開始のウィザードが始まるので「Continue」を押します。

組織の名前とどこのホストを利用するかが聞かれるので適当に入力し、画面の指示に従って登録を進めます。

組織が作成できたら、続いてプロジェクトを作成します。名前を適当に入力し(今回はConfiguration as Code)、「Private」を選択して作成します。

プロジェクトを作成したら、Gitリポジトリを作成します。元記事ではCloneしてローカルで実行する方法も紹介されていますが、今回は簡単な手順しか実施しないためWeb上で直接リポジトリの操作を行います。「Repos -> Files」を開き、下の方の「Initialize」を押してファーストコミットします。

設定導入用PS1作成

続いて、実際のIntuneを操作するロジックとなるPowerShellスクリプトを作成していきます。リポジトリの三点リーダーから「New -> Folder」を選択します。

フォルダ構成などは任意で構いませんが、今回は元記事通りintune-devicecomplianceフォルダを作成しその配下にCompliancePolicy_Add.ps1ファイルを作成します。

CompliancePolicy_Add.ps1のコードのオリジナルはこちらにあります。しかしこのコードはCaCを行うためのコードではなく、Microsoft Graph APIを利用した操作のサンプルでしかありません。元記事ではこれをアレンジするための参考案がいくつか上げられていますが、完全なコードは記述されていないので、私がうまく動かせたコードを以下に記載します。なお、コードのクオリティは元々高くないところに、私もあまり詳しいわけではないのでとりあえず動くレベルであることをご容赦ください。ツッコミどころはめちゃくちゃ多いです。

<#

.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
)

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

# 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 Test-JSON() {

    <#
    .SYNOPSIS
    This function is used to test if the JSON passed to a REST Post request is valid
    .DESCRIPTION
    The function tests if the JSON passed to the REST Post is valid
    .EXAMPLE
    Test-JSON -JSON $JSON
    Test if the JSON is valid before calling the Graph REST interface
    .NOTES
    NAME: Test-JSON
    #>

    param (

        $JSON

    )

    try {

        $TestJSON = ConvertFrom-Json $JSON -ErrorAction Stop
        $validJson = $true

    }

    catch {

        $validJson = $false
        $_.Exception

    }

    if (!$validJson) {

        Write-Host "Provided JSON isn't in valid JSON format" -f Red
        break

    }

}

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

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

    }

}

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

$JSON_Android = @"

    {
    "passwordExpirationDays": null,
    "requireAppVerify":  true,
    "securityPreventInstallAppsFromUnknownSources":  true,
    "@odata.type":  "microsoft.graph.androidCompliancePolicy",
    "scheduledActionsForRule":[{"ruleName":"PasswordRequired","scheduledActionConfigurations":[{"actionType":"block","gracePeriodHours":0,"notificationTemplateId":""}]}],
    "passwordRequiredType":  "numeric",
    "storageRequireEncryption":  true,
    "storageRequireRemovableStorageEncryption":  true,
    "passwordMinutesOfInactivityBeforeLock":  15,
    "passwordPreviousPasswordBlockCount":  null,
    "passwordRequired":  true,
    "description":  "Android Compliance Policy",
    "passwordMinimumLength":  4,
    "displayName":  "Android Compliance Policy",
    "securityBlockJailbrokenDevices":  true,
    "deviceThreatProtectionRequiredSecurityLevel":  "low",
    "deviceThreatProtectionEnabled":  true,
    "securityDisableUsbDebugging":  true
    }

"@

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

$JSON_iOS = @"

  {
  "@odata.type": "microsoft.graph.iosCompliancePolicy",
  "description": "iOS Compliance Policy",
  "displayName": "iOS Compliance Policy",
  "scheduledActionsForRule":[{"ruleName":"PasswordRequired","scheduledActionConfigurations":[{"actionType":"block","gracePeriodHours":0,"notificationTemplateId":""}]}],
  "passcodeBlockSimple": true,
  "passcodeExpirationDays": null,
  "passcodeMinimumLength": 4,
  "passcodeMinutesOfInactivityBeforeLock": 15,
  "passcodePreviousPasscodeBlockCount": null,
  "passcodeMinimumCharacterSetCount": null,
  "passcodeRequiredType": "numeric",
  "passcodeRequired": true,
  "securityBlockJailbrokenDevices": true,
  "deviceThreatProtectionEnabled": true,
  "deviceThreatProtectionRequiredSecurityLevel": "low"
  }

"@

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

Write-Host "Adding Android Compliance Policy from JSON..." -ForegroundColor Yellow

Add-DeviceCompliancePolicy -JSON $JSON_Android

Write-Host "Adding iOS Compliance Policy from JSON..." -ForegroundColor Yellow
Write-Host

Add-DeviceCompliancePolicy -JSON $JSON_iOS

特に変更している箇所はハイライトしています。詳細は元記事を見ていただければわかりますが、大きく2つの変更をしています。1つは認証部分が大きく変わるためGet-AuthToken関数を削除して、外部パラメータから環境変数を設定していること、もう1つがMicrosoft.Graphのモジュールを利用していることです。

このスクリプトを実行すると、スクリプト内に直接定義されているiOSとAndroidのデバイスコンプライアンスポリシーが作成されます。

それでは操作の説明に戻りまして、上記コードをファイルの編集画面で貼り付け、「Commit」を行います。

コミットメッセージを適当に入力し(今回はデフォルトのまま)、「Commit」します。

変数グループ作成

続いてデプロイの準備にかかります。上記のスクリプトでは操作するためのアプリの認証情報を、スクリプト実行時の引数から取るように設計されています。これをAzure DevOpsのプロジェクトで管理するために変数グループを作成します。「Pipelines -> Library」から「Variable group」を作成します。

適当な名前(今回はconfiguration-as-code)を入力し、Valiablesに2つのIntune環境のテナントID、アプリID、クライアントシークレットを登録します。変数の名前は下記の通りです。

  • tenantId_DEV
  • appId_DEV
  • appSecret_DEV
  • tenantId_PROD
  • appId_PROD
  • appSecret_PROD

それぞれ値を入力したら、右側の鍵マークを押して値を非表示にします。

パイプライン定義

いよいよパイプラインを作ります。「Pipelines -> Pipelines」から「Create Pipeline」します。

どのリポジトリを利用するか聞かれるので「Azure Repos Git」を選択します。

「Configuration as Code」を選択します。

パイプラインの定義は未作成のため「Starter pipeline」を選択します。

パイプラインの定義ファイルの設置場所は任意で問題ないですが、今回は元記事通りintune-devicecompliance/add-compliancepolicy-pipeline.ymlとします。定義は下記のようにしました。

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

trigger:
- none

pool:
  vmImage: $(vmImageName)

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

これを入力して「Save and run」を押します。

定義ファイルをコミットするためコミットメッセージを適当に(今回はデフォルトのまま)入力し、「Save and run」で保存・パイプラインの実行をします。

デプロイ

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

初回は設定した変数グループをパイプラインから利用することを承認するように要求されるので、「Permit」します。問題なければしばらくするとJobsがSuccessします。今回は1分40秒程度で完了しました。うまく行かない場合はエラーの内容が表示されるので、そこから切り分けを行いましょう。

デプロイされた結果を確認します。Microsoft Endpoint Management admin centerへアクセスし、デバイスコンプライアンスポリシーを開きます。iOSとAndroidのポリシーが追加されていることが確認できます。(下記は別途手動で設定したWindows10のポリシーも表示されています)

詳細画面を確認し、スクリプト内の定義と同じ設定が入っていることが確認できました。

まとめ

今回はAzure DevOpsを利用してIntuneの設定を適用するところまで実施しました。

ひとまずこれで一連の処理内容や実現方法を理解することができました。実際にはスクリプト作成とパイプライン定義にはかなり苦労しました。

次回は元記事でも紹介されている、開発用のテナントの設定を本番用のテナントにインポートする方法を試してみたいと思います。

臼田 佳祐

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