Using Azure Data Factory Pipeline(Synapse Pipeline) manage on-premises Active Directory accounts 

# ユースケース
オンプレミスActive Directoryのユーザー管理、メンテナンスのビジネスプロセス(VM上のPowerShell)をAzure上のPaaSサービスで実現したいニーズがあります。
そして、PaaSサービスを使いながら、ユーザーデータを別のシステムに連動し、スケジュールバッチ化にするニーズも多くあると思います。
こちらの記事は、Azure PaaSサービスを使って、上記のニーズの実現する方法と、その実現するための考え方を記述します。

# ソリューションを考える
オンプレミスADに対する操作なので、当然、ADに参加しているサーバー、またADサーバー自身で操作することになります。そして、AD参加していることなので、LDAPと通信できるネットワーク内の環境が必要となります。

どうやってADと会話できるようになるのか、大体、2つパターンになります。

+ AD Powershell(New-ADUserなど)を使って、ユーザーを管理する（ADに参加済みのサーバーVM）
+ LDAP(LDAPアドレス指定)に繋げて、ユーザーを管理する（ADに参加する必要はないものの、ユーザーやパスワード指定して、LDAPと通信できるような環境）

今回やりたいのは、ADF Pipeline(Synapse Pipeline)で、データを連携しつつ、AD操作することなので、LDAP接続(じまいプログラムを組む)の選択肢は考え方として優先度は低くなります。LDAP方法は実現できないわけではないものの、やはりバッチ化できるPowerShellの方法を優先に考えたいと思います。

# ADF Pipeline(Synapse Pipeline)でPowerShellを実行する方法を考える
いくつかのPowerShellをCallする方法があります。

+ Custom Activity(Azure Batch)
+ Azure Function Activity
+ Web Activity
+ WebHook Activity

https://docs.microsoft.com/en-us/azure/data-factory/transform-data-using-custom-activity

https://docs.microsoft.com/en-us/azure/data-factory/control-flow-azure-function-activity

https://docs.microsoft.com/en-us/azure/data-factory/control-flow-web-activity

https://docs.microsoft.com/en-us/azure/data-factory/control-flow-webhook-activity

上記のActivityはPowerShellを操作できるのですが、それぞれの特徴を事前に理解する必要があります。

<dl>
<dt>選択1:Custom Activity(Azure Batch)</dt>
<dd>ユーザーが好みの言語(PowerShell対応)で作成し、‎‎ADF‎‎ がオーケストレーション パイプラインの一部として呼び出すことができる Azure プラットフォーム コンピューティング サービスにラップされた、非常に迅速に行うことができます。並列処理をよく使われるパターンの一つです。</dd>
<dt>➡今回のニーズに基づく評価：</dt>
<dd>ADのユーザーアカウント情報などの更新プロセスにとっては、少しToo Richで、用途からみて、コスト的には合わない可能性があります。</dd>
<dt>選択2:Azure Function Activity</dt>
<dd>Azure Data Factory または Azure Synapse Analytics パイプライン内で Azure Functions を実行できます。</dd>
<dt>➡今回のニーズに基づく評価：</dt>
<dd>Function自体はVNet統合でき、プライベートリンクでADと同じネットワーク内でアクセスできますが、ADに参加しているわけではないので、LDAP方式で、ADユーザーアカウントを管理することになります。そして、Function自体は最大230秒以内で処理を終わらせないとTime Outエラーが発生します。Azure Durable Functionsは別です。ADユーザーの複雑な操作の処理がある場合、向いていない可能性があります。</dd>
<dt>選択3:Web Activity</dt>
<dd>Rest APIで様々なメソッドが対応しており、カスタム REST エンドポイントを呼び出すことができます。</dd>
<dt>➡今回のニーズに基づく評価：</dt>
<dd>オンプレミス側にAPIのようなサーバーを建てるか、Web AppsのVNet統合で、APIプログラムを作ることで、Pipelineで連携できるようになります。また、Automation Runbookをあらかじめに用意し、WebhookのAPIをPOSTすることもできます。ただ、Web Activityは、Syncではなくて、ASyncなので、PipelineからWeb Activityで相手のAPIを呼び出してから、すぐに次のActivityに勝手に進んでしまいます。今回のニーズでは、PowerShellの処理結果も含めてPipelineと連携する（つまり、Sync）必要がありますので、Web Activityは採用できかねることになります。</dd>
<dt>選択4:Webhook Activity</dt>
<dd>Web Activityと同じくRest APIで、POSTできます。Webhook ActivityはPOSTしかできない特徴もあります。Webhook Activityを使用すると、カスタム コードでパイプラインの実行を制御できます。 そして、コードでエンドポイントを呼び出して、callBackUri を渡すことができます。 Pipelineでは、次のActivityに進む前に、callBackが呼び出されるまで実行が待機されます。</dd>
<dt>➡今回のニーズに基づく評価：</dt>
<dd>前後のActivityの実行はSyncできるようになるので、<strong>今回はWebhook Activityを採用し</strong>、オンプレミスADの操作をもう少し深く考えます。</dd>
</dl>

# Webhook Activityで、POSTするためのPowerShellの実行環境を考える
Webhook ActivityでPowerShellの実行を呼び出すために、そのPowerShellの実行環境は必ず必要となってきます。ここで、選択肢候補として、Azure Automationです。
Azure Automation は、Azure 環境と非 Azure 環境の全体で一貫性ある管理をサポートするクラウドベースの自動化、オペレーティング システムの更新、および構成サービスを提供します。 プロセスの自動化、構成管理、更新管理、共有機能、および異種環境機能が含まれます。

https://docs.microsoft.com/en-us/azure/automation/quickstarts/create-account-portal

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236646/1eeb6826-f543-10db-57d8-e004298a9110.png)
Azure Automationの中で、Runbookを作れます。そのRunbookには、PowerShellを埋め込めばよいことですが、Runbook自体は、デフォルトではAzure側のリソースで、実行されます。今回のニーズでは、ADに対する操作なので、Azure側のリソースの代わりに、オンプレミス側のリソースでRunbookを実行してもらう必要です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236646/0b815404-368e-4a23-cf77-2079c3a83858.png)
そのため、Hybrid Runbook Workerを構成し、オンプレミス側のリソースの環境上で、RunbookのPowerShellを実行することになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236646/929bcff5-cb88-d217-8452-60fa8b1de5f0.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236646/c86e2e86-4839-af9f-9612-d1f8811d9929.png)

https://docs.microsoft.com/en-us/azure/automation/automation-hybrid-runbook-worker

Hybrid Runbook Workerを構成された後に、後はRunbookに対して、Webhookを作れば、PipelineのWebhook ActivityからPOSTできるようになります。そこで、ADに対する操作（例 New-ADUser）はオンプレミス側のリソース上で、実行され、そのリソース(VM)はADドメインに参加されているようであれば、当然、ADと会話できるようになります。

# PowerShellへ渡すパラメータを考える
Webhook Activityで、AutomationのRunbookをPOSTにする際に、HTTPのBodyの中にパラメータを渡すのは一般的です。場合によって、Headerを使って渡すこともできます。今回はADドメインに対して、OUを指定して、そのOUの中に、ユーザー名を作成していくことを想定します。実際は、もっと汎用的に作れますが、サンプルとして、Create AD Userにしておきます。

Bodyに含まれるパラメータは二つ

+ domainPath
+ domainUser

これらをWebhook ActivityにJSON形式として、POSTします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236646/a95a66f3-0681-c21c-30f5-3ca54c1434db.png)

# Webhookのセキュリティを考える
Webhook のセキュリティは、Webhook の呼び出しを許可するセキュリティ トークンを含む URL のプライバシーに依存します。 Azure Automation は、正しい URL に対して要求が行われている限り、いかなる認証も実行しません。 この理由により、クライアントは、要求を検証する代替方法を使用せずに機密性の高い操作を実行する Runbook には Webhook を使用しないようにする必要があります。

https://docs.microsoft.com/en-us/azure/automation/automation-webhooks?tabs=powershell#webhook-security

そのため、WebhookのURLとトークンが外に漏れたらだれでもPOSTができるようになりますので、ここで、一つコツがあります。Webhook ActivityからPOSTする際に、Header情報として、合言葉(String)を含めるようにします。Runbook側のPowerShellを実行する前に、Header情報の合言葉(String)を照合したうえで、実行継続するかどうか、判断させます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236646/f91356ac-2d2a-e4e5-bc24-95094c8b1491.png)

# ADF Pipeline(Synapse Pipeline)のWebhook Activity仕様を確認する
Web Activityと違うのは2点あります。

+ 他のADF DataSetとLink Serviceは、現在、実行時に渡すことはできません。IR指定もなしで、Internet経由でURLにPOSTのみです。
+ Webhookは、callBackUriの機能を提供します。つまり、APIからの応答を要求して待機してからActicityに戻ります。その点で、Sync処理ができ、Pipelineの制御ができるようになります。

https://docs.microsoft.com/en-us/azure/data-factory/control-flow-webhook-activity

# Automation Runbook PowerShell
PowerShellのデザインは前半と後半を分かれます。
Webhook Activityからは、一つwebhookDataのObjectパラメータを受けることができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236646/07b3b5f5-8217-1c98-3605-d439247d6372.png)

詳細説明はこちらです。

https://docs.microsoft.com/en-us/azure/automation/automation-webhooks?tabs=powershell#parameters-used-when-the-webhook-starts-a-runbook

前半の処理としては、渡されたHeaderパラメータを照合してから、Bodyパラメータを確認し、そして、変数に格納します。
ここで、Headerのパラメータの合言葉を「Microsoft」とします。
callBackUriは、Webhook Activity側で自動作成されますので、それも変数に格納しておきます。後で、エラー処理や正常処理の際に、Pipeline側に返すように、準備しておきます。

<details><summary>前半PowerShellスクリプト</summary>

~~~PowerShell
param(
    [Parameter(Mandatory = $false)]
    [object] $WebhookData
)

$WebhookCallbackHeaders = @{"Content-Type" = "application/json"}
#For Debug(0:Non debug mode,1:Debug mode)
$DebugFlg = 0

if ($DebugFlg -eq 1) {
	write-output "Process Start."
}

if($WebhookData){
	$parameters = (ConvertFrom-Json -InputObject $WebhookData.RequestBody) 
	
	if($parameters.callBackUri) {
		$callBackUri = $parameters.callBackUri
		if ($DebugFlg -eq 1) {
			write-output $callBackUri
		}
	}

	if($parameters.domainPath) {
		$path = $parameters.domainPath
		if ($DebugFlg -eq 1) {
			write-output $path
		}
	}
	
	if($parameters.domainUserName) {
		$username = $parameters.domainUserName
		if ($DebugFlg -eq 1) {
			write-output $username
		}
	}

	$authMessageHeader = $WebhookData.RequestHeader.authMessage
	if($authMessageHeader -ne "Microsoft")
	{
		$body = "Got unauthenticated request."
		if ($DebugFlg -eq 1) {
			write-output $body
		}
		
		$jsonRequestauthError = [ordered]@{
			"Output" = @{
				"Status" = "Failed"
				"Message" = "$body"
    			"StatusCode" = "403"
			}
			"StatusCode" = "403"
		}
		
		if($callBackUri){
			$OutputJson = $jsonRequestauthError | ConvertTo-Json -Depth 10
			Invoke-WebRequest -Uri $callBackUri -UseBasicParsing -Method Post -Body $OutputJson -Header $WebhookCallbackHeaders
			exit
		}
		else
		{
			write-output $body
			exit
		}
	}
	else
	{
		if ($DebugFlg -eq 1) {
			write-output "Checked Header authMessage is $authMessageHeader."
		}
	}
}
~~~

</details>

後半のプロセスでは、サンプルとして、ADに対してユーザーをCreateします。Createしてから、正常処理されたことをPipelineに返すようにします。
こちらのサンプルでは、ループで10個IDを作成するようになっています。

<details><summary>後半PowerShellスクリプト</summary>

~~~PowerShell
#Add AD User
try
{
	if (($path) -and ($username)){
		$count = 1..10
		foreach ($i in $count){
			New-AdUser -Name $username$i -Path $path -Enabled $True -ChangePasswordAtLogon $true -AccountPassword (ConvertTo-SecureString "P@ssw0rd" -AsPlainText -force) -passThru
		}

		$body = "AD users have been added."
		if ($DebugFlg -eq 1) {
			write-output $body
		}

		$jsonRequestAddUser = [ordered]@{
			"Output" = @{
				"Status" = "Success"
				"Message" = "$body"
    			"StatusCode" = "200"
			}
			"StatusCode" = "200"
		}
		
		$OutputJson = $jsonRequestAddUser | ConvertTo-Json -Depth 10
		if($callBackUri){
			Invoke-WebRequest -Uri $callBackUri -UseBasicParsing -Method Post -Body $OutputJson -Header $WebhookCallbackHeaders
			exit
		}
	}
}
catch
{
	$body = $_.Exception

	$jsonRequestAddUserError = [ordered]@{
		"Output" = @{
			"Status" = "Failed"
			"Message" = "$body"
			"StatusCode" = "500"
		}
		"StatusCode" = "500"
	}

	if ($DebugFlg -eq 1) {
		write-output $body
	}
	
	$OutputJson = $jsonRequestAddUserError | ConvertTo-Json -Depth 10
	if($callBackUri){
		Invoke-WebRequest -Uri $callBackUri -UseBasicParsing -Method Post -Body $OutputJson -Header $WebhookCallbackHeaders
		exit
	}
}
~~~~

</details>

ポイントとしては、Webhook ActivityのcallBakUriに対して、返すBodyの中に、StatusCodeを設定することによって、Webhook Activity自体をエラーにするか、成功にするかコントロールすることができます。基本的に、400以上を指定すると、Pipeline側としてエラーになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236646/eff04e80-dea9-d529-bd43-7d649698afc4.png)

下記はPowerShellの全体です。

~~~PowerShell
param(
    [Parameter(Mandatory = $false)]
    [object] $WebhookData
)

$WebhookCallbackHeaders = @{"Content-Type" = "application/json"}
#For Debug(0:Non debug mode,1:Debug mode)
$DebugFlg = 0

if ($DebugFlg -eq 1) {
	write-output "Process Start."
}

if($WebhookData){
	$parameters = (ConvertFrom-Json -InputObject $WebhookData.RequestBody) 
	
	if($parameters.callBackUri) {
		$callBackUri = $parameters.callBackUri
		if ($DebugFlg -eq 1) {
			write-output $callBackUri
		}
	}

	if($parameters.domainPath) {
		$path = $parameters.domainPath
		if ($DebugFlg -eq 1) {
			write-output $path
		}
	}
	
	if($parameters.domainUserName) {
		$username = $parameters.domainUserName
		if ($DebugFlg -eq 1) {
			write-output $username
		}
	}

	$authMessageHeader = $WebhookData.RequestHeader.authMessage
	if($authMessageHeader -ne "Microsoft")
	{
		$body = "Got unauthenticated request."
		if ($DebugFlg -eq 1) {
			write-output $body
		}
		
		$jsonRequestauthError = [ordered]@{
			"Output" = @{
				"Status" = "Failed"
				"Message" = "$body"
    			"StatusCode" = "403"
			}
			"StatusCode" = "403"
		}
		
		if($callBackUri){
			$OutputJson = $jsonRequestauthError | ConvertTo-Json -Depth 10
			Invoke-WebRequest -Uri $callBackUri -UseBasicParsing -Method Post -Body $OutputJson -Header $WebhookCallbackHeaders
			exit
		}
		else
		{
			write-output $body
			exit
		}
	}
	else
	{
		if ($DebugFlg -eq 1) {
			write-output "Checked Header authMessage is $authMessageHeader."
		}
	}
}

#Add AD User
try
{
	if (($path) -and ($username)){
		$count = 1..10
		foreach ($i in $count){
			New-AdUser -Name $username$i -Path $path -Enabled $True -ChangePasswordAtLogon $true -AccountPassword (ConvertTo-SecureString "P@ssw0rd" -AsPlainText -force) -passThru
		}

		$body = "AD users have been added."
		if ($DebugFlg -eq 1) {
			write-output $body
		}

		$jsonRequestAddUser = [ordered]@{
			"Output" = @{
				"Status" = "Success"
				"Message" = "$body"
    			"StatusCode" = "200"
			}
			"StatusCode" = "200"
		}
		
		$OutputJson = $jsonRequestAddUser | ConvertTo-Json -Depth 10
		if($callBackUri){
			Invoke-WebRequest -Uri $callBackUri -UseBasicParsing -Method Post -Body $OutputJson -Header $WebhookCallbackHeaders
			exit
		}
	}
}
catch
{
	$body = $_.Exception

	$jsonRequestAddUserError = [ordered]@{
		"Output" = @{
			"Status" = "Failed"
			"Message" = "$body"
			"StatusCode" = "500"
		}
		"StatusCode" = "500"
	}

	if ($DebugFlg -eq 1) {
		write-output $body
	}
	
	$OutputJson = $jsonRequestAddUserError | ConvertTo-Json -Depth 10
	if($callBackUri){
		Invoke-WebRequest -Uri $callBackUri -UseBasicParsing -Method Post -Body $OutputJson -Header $WebhookCallbackHeaders
		exit
	}
}
~~~

# ADF Pipeline(Synapse Pipeline)
RunbookのPowrShellを準備が整えてから、いよいよWebhook Activityを配置し、HeaderとBoby、そして、WebhookのURLを指定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236646/266db339-2641-1e8d-a824-e109624e5f40.png)
Pipeline実行することで、Hybrid Runbook Worker環境のRunbook PowerShellの処理結果が返してくれます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236646/a328ba79-21fb-39bc-f8a3-6d5aee1411d1.png)
そして、Runbookの実行履歴も確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236646/daab0e78-76e6-2748-4612-2a786b6924e4.png)
そして、Active DirectoryのOUの中を確認したところ、実際に、10アカウントが追加されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236646/c5239043-1291-84ac-bc95-c6946efdb0aa.png)
試しに、Headerの部分のauthMessageに、合言葉以外のStringを指定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236646/d891e2ac-9b87-d3e5-07c0-3f658e3188e2.png)
その実行結果はエラーとして返してくれます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236646/6c4996d1-9f9b-1f06-b1b2-5a6761b7226c.png)

# PowerShellスクリプトをVM上に置いたままの実行の考え方
Runbookは、VM上のフォルダの中のPS1を呼び出すこともできますが、問題は、そのPS1の処理をうまくRunbook側が受け取って、そしてADFやSynapseのPipelineに返すのに、結構大変なになります。
そのため、PS1実行ではなく、そのPowerShellをRunbookに埋め込むことをお勧めします。

# ADF Pipeline(Synapse Pipeline)からRunbook実行するための通信

+ その前にも書いたとおりに、Webhook ActivityはLinked Serviceを使わないため、そのままInternetで、Automation Accountにアクセスしますので、Automation Account側のNetwork設定は、Public Accessを許可する必要です。Private Linkを作っても問題ないです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236646/96a69758-a19f-86e2-f4b8-1f571a83ef12.png)
+ ADFやSynapse workspaceのManaged VNet、Private Linkでも対応可能です。

# まとめ

+ ADF Pipeline(Synapse Pipeline)で、Webhook Activityを使えば、そのWebhook先のRunbookに、PowerShellを組めることができます。
+ PowerShellの実行環境はAzureリソースか、Hybrid Runbook Workerを使って、オンプレミスリソースで、両方対応しています。
+ Webhookのパラメータは[object] $WebhookDataが一つあり、このパラメータには、BodyやHeaderの情報が含まれています。
+ Webhook ActivityはcallBackUriをRunbook側に渡して、Runbookの処理の次第で、callBackUriに対して、Invoke-WebRequestを行います。そうすると、呼び出し側で、Runbookの実行進捗や状況などをSyncできます。
+ ADF Pipeline(Synapse Pipeline)のWebhook Activityの実行結果（成功、失敗）は、Runbook側で、StatusCodeで定義し、Outputのメッセージとともに、callBackUriに返します。

# 汎用性
今回は、ADに対するシナリオでしたが、別にADに限らずに、ほかの仕組みにも適用できます。重要なのは、ADF Pipeline(Synapse Pipeline)のWebhook ActivityとAutomationのRunbookのやり取りの仕方を理解すれば、汎用できます。

# 最後に
Webhook Activityを利用しようとするエンジニアの方に対して、この記事が少しでも役に立てればうれしいです。

Azure Data Platformの勉強会は毎月に開催しています。ぜひ、Communityもアクセスしてみてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236646/fd1cf6ef-7449-486c-3c60-c471856de9d3.png)

https://aka.ms/jssug