Repository navigation
Expand file tree
/
Copy pathCreate-TestDomainController.ps1
More file actions
1778 lines (1491 loc) · 76.2 KB
/
Copy pathCreate-TestDomainController.ps1
File metadata and controls
1778 lines (1491 loc) · 76.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# =============================================================================
# Script: Create-TestDomainController.ps1
# Created: 2025-02-14 21:37:49 UTC
# Author: maxdaylight
# Last Updated: 2025-05-27 14:45:00 UTC
# Updated By: maxdaylight
# Version: 4.9.5
# Additional Info: Improved password security and removed credential display
# =============================================================================
<#
.SYNOPSIS
Creates and configures a Domain Controller in Azure from a Windows 11 Pro workstation.
.DESCRIPTION
This script deploys and configures an Azure VM as a Domain Controller, ensuring full
compatibility with Windows 11 Pro workstations and PowerShell 7.5.0. No Server or Core
dependencies required.
Key actions:
- Validates Windows 11 Pro environment
- Creates Azure resources using PowerShell 7.5.0
- Configures Domain Controller through PowerShell remoting
- Creates test users and configures basic AD structure
- Automatically creates resources without prompting
- Enables Trusted Launch security features
- Configures static IP addressing for network stability
- Implements security hardening for domain services
- Provides RDP access through public IP address
Dependencies:
- Windows 11 Pro workstation
- PowerShell 7.5.0
- Az PowerShell modules
- Active Azure subscription
- Administrative privileges
Security considerations:
- Uses secure credential handling
- Implements least privilege access
- Encrypts all sensitive data
- Trusted Launch enabled with vTPM and Secure Boot
- Disables legacy cryptography protocols
- Enforces static IP configuration
- Hardens domain security settings
Performance impact:
- Average deployment time: 30-45 minutes
- Resource usage: Moderate CPU and memory during deployment
Known limitations:
- DNS delegation warnings can be ignored for test environments
- Default cryptography settings are explicitly configured
- Not intended for production use without DNS infrastructure integration
.PARAMETER ResourceGroupName
The name of the Azure Resource Group where all resources will be created.
Default value: "MD-TEST-RG2"
.PARAMETER Location
The Azure region where all resources will be created.
Default value: "westus2"
.PARAMETER VMSize
The size/SKU of the virtual machine to be created.
Default value: "Standard_D2s_v3"
.PARAMETER VMName
The name of the virtual machine that will become the Domain Controller.
Default value: "MD-TEST-DC01"
.PARAMETER VnetName
The name of the Virtual Network to be created.
Default value: "MD-TEST-VNET"
.PARAMETER SubnetName
The name of the subnet to be created within the Virtual Network.
Default value: "MD-TEST-SUBNET"
.PARAMETER StorageAccountName
The name of the storage account for boot diagnostics and scripts.
Default value: "MDteststorage0"
.PARAMETER ContainerName
The name of the container within the storage account for scripts.
Default value: "scripts"
.PARAMETER DomainName
The FQDN of the Active Directory domain to be created.
Default value: "MDtest.local"
.PARAMETER AdminUsername
The administrator username for the Domain Controller.
Default value: "MDadmin"
.PARAMETER TestUserPrefix
The prefix used for creating test user accounts in the domain.
Default value: "TestUser"
.PARAMETER OSDiskName
The name of the OS disk resource for the virtual machine.
Default value: "MD-TEST-DC01-OSDISK"
.EXAMPLE
.\Create-TestDomainController.ps1
Creates a new Domain Controller using all default values with hardened security settings
.EXAMPLE
.\Create-TestDomainController.ps1 -ResourceGroupName "MyRG" -Location "westus2"
Creates a new Domain Controller in the specified resource group and location with security hardening
.NOTES
Security Level: High
Required Permissions: Azure Subscription Contributor, Local Administrator
Validation Requirements:
- Windows 11 Pro environment
- Azure PowerShell modules
- Network connectivity to Azure
- Static IP address configuration
- Domain security hardening
Expected Warnings:
- DNS delegation warnings are normal and can be ignored in test environments
- Cryptography settings warnings are addressed through explicit configuration
#>
[CmdletBinding(SupportsShouldProcess=$false)]
param (
[Parameter()]
[string]$ResourceGroupName = "MD-TEST-RG2",
[Parameter()]
[string]$Location = "westus2",
[Parameter()]
[string]$VMSize = "Standard_D2s_v3",
[Parameter()]
[string]$VMName = "MD-TEST-DC01",
[Parameter()]
[string]$VnetName = "MD-TEST-VNET",
[Parameter()]
[string]$SubnetName = "MD-TEST-SUBNET",
[Parameter()]
[string]$StorageAccountName = "MDteststorage0",
[Parameter()]
[string]$ContainerName = "scripts",
[Parameter()]
[string]$DomainName = "MDtest.local",
[Parameter()]
[string]$AdminUsername = "MDadmin",
[Parameter()]
[string]$TestUserPrefix = "TestUser",
[Parameter()]
[string]$OSDiskName = "MD-TEST-DC01-OSDISK",
[Parameter()]
[string]$ShutdownTime = "2100",
[Parameter()]
[string]$TimeZone = "Mountain Standard Time"
)
# Set strict error handling
$ErrorActionPreference = 'Stop'
$PSDefaultParameterValues['*:ErrorAction'] = 'Stop'
Set-StrictMode -Version Latest
# Enable extended error details
$FormatEnumerationLimit = -1
$InformationPreference = 'Continue'
# Add error handling function
function Write-ErrorDetails {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[System.Management.Automation.ErrorRecord]$ErrorRecord
)
Write-Host "=== Detailed Error Information ===" -ForegroundColor Red
Write-Host "Error Message: $($ErrorRecord.Exception.Message)" -ForegroundColor Red
Write-Host "Error Type: $($ErrorRecord.Exception.GetType().FullName)" -ForegroundColor Red
Write-Host "Command: $($ErrorRecord.InvocationInfo.MyCommand)" -ForegroundColor Red
Write-Host "Line Number: $($ErrorRecord.InvocationInfo.ScriptLineNumber)" -ForegroundColor Red
Write-Host "Script Name: $($ErrorRecord.InvocationInfo.ScriptName)" -ForegroundColor Red
Write-Host "Statement: $($ErrorRecord.InvocationInfo.Line)" -ForegroundColor Red
if ($ErrorRecord.Exception.StackTrace) {
Write-Host "Stack Trace: $($ErrorRecord.Exception.StackTrace)" -ForegroundColor Red
}
Write-Host "=== End Detailed Error Information ===" -ForegroundColor Red
}
# Suppress confirmation prompts globally for the entire script
$global:ConfirmPreference = 'None'
# Global transcript tracking
$script:TranscriptStarted = $false
# Start logging only if not already started
if (-not $script:TranscriptStarted) {
$ScriptPath = $PSScriptRoot
if (-not $ScriptPath) {
$ScriptPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
}
$LogDir = Join-Path $ScriptPath "Logs"
$LogFile = Join-Path $LogDir "Create-TestDomainController_$(Get-Date -Format 'yyyyMMdd_HHmmss').log"
try {
# Create logs directory if it doesn't exist
if (-not (Test-Path $LogDir)) {
New-Item -ItemType Directory -Path $LogDir -Force | Out-Null
}
# Remove all existing log files
Get-ChildItem -Path $LogDir -Filter "Create-TestDomainController_*.log" | Remove-Item -Force
# Start new transcript
Start-Transcript -Path $LogFile -Force
$script:TranscriptStarted = $true
}
catch {
Write-Error "Failed to initialize logging: $_"
throw
}
}
function Initialize-RemotingConfiguration {
[CmdletBinding()]
param()
try {
Write-Host "Configuring PowerShell remoting..." -ForegroundColor Cyan
# Temporarily suppress verbose output just for remoting commands
$originalVerbosePreference = $VerbosePreference
$VerbosePreference = 'SilentlyContinue'
try {
# Execute remoting commands with suppressed verbose output
$winPSPath = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe"
$configScript = {
$VerbosePreference = 'SilentlyContinue'
Enable-PSRemoting -Force -SkipNetworkProfileCheck
Set-Item -Path WSMan:\localhost\Client\TrustedHosts -Value "*" -Force
Restart-Service WinRM -Force
}
$encodedCommand = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($configScript))
Start-Process -FilePath $winPSPath -ArgumentList "-EncodedCommand $encodedCommand" -Wait -NoNewWindow
# Other remoting configuration commands
$null = Enable-PSRemoting -Force -SkipNetworkProfileCheck
# Verify configuration
$trustedHosts = (Get-Item WSMan:\localhost\Client\TrustedHosts).Value
if ($trustedHosts -ne "*") {
Set-Item WSMan:\localhost\Client\TrustedHosts -Value "*" -Force
}
}
finally {
# Restore original verbose preference
$VerbosePreference = $originalVerbosePreference
}
Write-Host "PowerShell remoting configured successfully" -ForegroundColor Green
return $true
}
catch {
Write-Error "Failed to initialize remoting configuration: $_"
return $false
}
}
function Initialize-WinRMConfiguration {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[string]$HostName
)
try {
Write-Host "Configuring WinRM Service..." -ForegroundColor Cyan
# Configure WinRM Service
Set-WSManQuickConfig -Force
Enable-PSRemoting -Force -SkipNetworkProfileCheck
# Configure WinRM HTTPS listener
$cert = New-SelfSignedCertificate -DnsName $HostName -CertStoreLocation "Cert:\LocalMachine\My"
$thumbprint = $cert.Thumbprint
$command = "winrm create winrm/config/Listener?Address=*+Transport=HTTPS '@{Hostname=`"$HostName`";CertificateThumbprint=`"$thumbprint`"}'"
cmd.exe /C $command
# Configure firewall rules
New-NetFirewallRule -DisplayName "Windows Remote Management (HTTPS-In)" -Name "Windows Remote Management (HTTPS-In)" -Profile Any -LocalPort 5986 -Protocol TCP
New-NetFirewallRule -DisplayName "Windows Remote Management (HTTP-In)" -Name "Windows Remote Management (HTTP-In)" -Profile Any -LocalPort 5985 -Protocol TCP
# Set basic authentication
Set-Item -Path WSMan:\localhost\Service\Auth\Basic -Value $true
# Restart WinRM service
Restart-Service WinRM -Force
Write-Host "WinRM configuration completed successfully" -ForegroundColor Green
return $true
}
catch {
Write-Error "Failed to configure WinRM: $_"
return $false
}
}
# Enhanced session creation for PowerShell 7.5.0
function New-EnhancedPSSession {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[string]$ComputerName,
[Parameter(Mandatory=$true)]
[System.Management.Automation.PSCredential]$Credential,
[Parameter()]
[int]$MaxAttempts = 5,
[Parameter()]
[int]$InitialRetryIntervalSeconds = 10
)
$attempt = 0
$session = $null
$retryInterval = $InitialRetryIntervalSeconds
do {
$attempt++
try {
Write-Host "Creating PowerShell session (Attempt $attempt of $MaxAttempts)..." -ForegroundColor Cyan
# Simplified session options compatible with PS 7.5.0
$sessionOption = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck `
-OpenTimeout 180000 `
-OperationTimeout 180000 `
-IdleTimeout 172800000 `
-MaximumReceivedDataSizePerCommand 209715200
# Verify WinRM service is running on local machine
if ((Get-Service -Name "WinRM").Status -ne "Running") {
Start-Service -Name "WinRM"
Start-Sleep -Seconds 5
}
# Create and validate session with enhanced error handling
$session = New-PSSession -ComputerName $ComputerName `
-Credential $Credential `
-SessionOption $sessionOption `
-ErrorAction Stop `
-UseSSL:$false `
-Port 5985 `
-Authentication Negotiate
# Test session state with improved validation
if ($session.State -ne "Opened") {
throw "Session created but not in Opened state. Current state: $($session.State)"
}
# Simple connectivity test
$testResult = Invoke-Command -Session $session -ScriptBlock { $env:COMPUTERNAME } -ErrorAction Stop
if (-not $testResult) {
throw "Session test command failed"
}
Write-Host "Successfully created and validated PowerShell session" -ForegroundColor Green
return $session
}
catch {
Write-Warning "Session creation attempt $attempt failed: $_"
if ($session) {
Remove-PSSession -Session $session -ErrorAction SilentlyContinue
}
if ($attempt -lt $MaxAttempts) {
$jitter = Get-Random -Minimum 1 -Maximum 10
$retryInterval = [Math]::Min($retryInterval * 2 + $jitter, 300)
Write-Host "Waiting $retryInterval seconds before retry..." -ForegroundColor Yellow
Start-Sleep -Seconds $retryInterval
}
else {
Write-Error "Failed to create PowerShell session after $MaxAttempts attempts"
throw
}
}
} while ($attempt -lt $MaxAttempts)
return $null
}
# Modified New-DomainControllerVM function with enhanced PS 7.5.0 support
function New-DomainControllerVM {
[CmdletBinding(SupportsShouldProcess=$false)]
[OutputType([PSCustomObject])] # Explicitly declare output type
param (
[Parameter(Mandatory=$true)]
[string]$ResourceGroupName,
[Parameter(Mandatory=$true)]
[string]$Location,
[Parameter(Mandatory=$true)]
[string]$VMSize,
[Parameter(Mandatory=$true)]
[string]$VnetName,
[Parameter(Mandatory=$true)]
[string]$SubnetName
)
# Ensure confirmation suppression in function scope
$ConfirmPreference = 'None'
try {
# Initialize remoting configuration
if (-not (Initialize-RemotingConfiguration)) {
throw "Failed to initialize remoting configuration"
}
# Create Virtual Network with enhanced error handling
Write-Host "Creating Virtual Network..." -ForegroundColor Cyan
$subnetConfig = New-AzVirtualNetworkSubnetConfig -Name $SubnetName `
-AddressPrefix "10.0.0.0/24"
$vnet = New-AzVirtualNetwork -Name $VnetName -ResourceGroupName $ResourceGroupName `
-Location $Location -AddressPrefix "10.0.0.0/16" -Subnet $subnetConfig
Write-Host "Virtual Network created successfully" -ForegroundColor Green
# Create Public IP with enhanced Static allocation
Write-Host "Creating Public IP..." -ForegroundColor Cyan
$publicIp = New-AzPublicIpAddress -Name "$VMName-IP" `
-ResourceGroupName $ResourceGroupName -Location $Location `
-AllocationMethod Static -Sku Standard
Write-Host "Public IP created successfully" -ForegroundColor Green
# Create Network Security Group with enhanced rules
Write-Host "Creating Network Security Group..." -ForegroundColor Cyan
$nsgRuleRDP = New-AzNetworkSecurityRuleConfig -Name "Allow-RDP" -Description "Allow RDP" `
-Access Allow -Protocol Tcp -Direction Inbound -Priority 102 `
-SourceAddressPrefix * -SourcePortRange * `
-DestinationAddressPrefix * -DestinationPortRange 3389
# Modified WinRM rules to be more specific
$nsgRuleWinRMHttp = New-AzNetworkSecurityRuleConfig -Name "Allow-WinRM-HTTP" -Description "Allow WinRM HTTP" `
-Access Allow -Protocol Tcp -Direction Inbound -Priority 101 `
-SourceAddressPrefix * -SourcePortRange * `
-DestinationAddressPrefix * -DestinationPortRange 5985
$nsgRuleWinRMHttps = New-AzNetworkSecurityRuleConfig -Name "Allow-WinRM-HTTPS" -Description "Allow WinRM HTTPS" `
-Access Allow -Protocol Tcp -Direction Inbound -Priority 100 `
-SourceAddressPrefix * -SourcePortRange * `
-DestinationAddressPrefix * -DestinationPortRange 5986
$nsgRuleDomain = New-AzNetworkSecurityRuleConfig -Name "Allow-Domain" -Description "Allow Domain Services" `
-Access Allow -Protocol * -Direction Inbound -Priority 103 `
-SourceAddressPrefix * -SourcePortRange * `
-DestinationAddressPrefix * -DestinationPortRange 53,88,135,389,445,464,636,3268,3269
$nsg = New-AzNetworkSecurityGroup -ResourceGroupName $ResourceGroupName -Location $Location `
-Name "MD-TEST-NSG" -SecurityRules @($nsgRuleRDP, $nsgRuleWinRMHttp, $nsgRuleWinRMHttps, $nsgRuleDomain)
Write-Host "Network Security Group created successfully" -ForegroundColor Green
# Create Network Interface with enhanced validation
Write-Host "Creating Network Interface..." -ForegroundColor Cyan
$subnet = Get-AzVirtualNetworkSubnetConfig -VirtualNetwork $vnet -Name $SubnetName
$nic = New-AzNetworkInterface -Name "$VMName-NIC" -ResourceGroupName $ResourceGroupName `
-Location $Location -SubnetId $subnet.Id -PublicIpAddressId $publicIp.Id `
-NetworkSecurityGroupId $nsg.Id
# Create VM Configuration with enhanced Trusted Launch
Write-Host "Creating VM Configuration with Trusted Launch..." -ForegroundColor Cyan
# Get admin password from .env file with no default value - as secure string
$scriptPath = $PSScriptRoot
if (-not $scriptPath) {
$scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
}
$envFilePath = Join-Path -Path $scriptPath -ChildPath '.env'
$securePassword = Get-EnvVariable -Name 'ADMIN_PASSWORD' -EnvFilePath $envFilePath -AsSecureString
$cred = New-Object System.Management.Automation.PSCredential ($AdminUsername, $securePassword)
# Configure storage account for boot diagnostics BEFORE VM creation
Write-Host "Setting up boot diagnostics storage..." -ForegroundColor Cyan
$maxStorageRetries = 5
$storageRetryCount = 0
$storageAccount = $null
$timeout = New-TimeSpan -Minutes 5
# Create and start stopwatch with a more compatible approach
$sw = New-Object System.Diagnostics.Stopwatch
$sw.Start()
while ($storageRetryCount -lt $maxStorageRetries -and $sw.Elapsed -lt $timeout) {
try {
Write-Host "Attempt $($storageRetryCount + 1) of $maxStorageRetries to configure storage..." -ForegroundColor Yellow
# Check if storage account exists
Write-Host "Checking for existing storage account..." -ForegroundColor Cyan
$storageAccount = Get-AzStorageAccount -ResourceGroupName $ResourceGroupName `
-Name $StorageAccountName -ErrorAction SilentlyContinue
if (-not $storageAccount) {
Write-Host "Creating new storage account '$StorageAccountName'..." -ForegroundColor Yellow
$storageAccount = New-AzStorageAccount -ResourceGroupName $ResourceGroupName `
-Name $StorageAccountName `
-Location $Location `
-SkuName Standard_LRS `
-ErrorAction Stop
if ($storageAccount) {
Write-Host "Storage account created successfully" -ForegroundColor Green
# Verify storage account is fully provisioned
$provisioningState = (Get-AzStorageAccount -ResourceGroupName $ResourceGroupName `
-Name $StorageAccountName).ProvisioningState
if ($provisioningState -eq "Succeeded") {
Write-Host "Storage account provisioning confirmed" -ForegroundColor Green
break
} else {
throw "Storage account not fully provisioned. State: $provisioningState"
}
}
} else {
Write-Host "Using existing storage account" -ForegroundColor Green
break
}
}
catch {
$storageRetryCount++
Write-Warning "Storage operation failed: $_"
if ($storageRetryCount -eq $maxStorageRetries -or $sw.Elapsed -ge $timeout) {
throw "Failed to configure storage after $storageRetryCount attempts or timeout reached ($($sw.Elapsed.TotalMinutes) minutes): $_"
}
$delay = [math]::Min(30, [math]::Pow(2, $storageRetryCount)) # Exponential backoff
Write-Host "Waiting $delay seconds before retry..." -ForegroundColor Yellow
Start-Sleep -Seconds $delay
}
}
if (-not $storageAccount) {
throw "Failed to create or retrieve storage account within timeout period of $($timeout.TotalMinutes) minutes"
}
# Try creating VM with Trusted Launch first, then fall back if needed
$vmCreated = $false
$retries = 0
$maxRetries = 3
$vm = $null
$vmCreationError = $null # Initialize error variable
while (-not $vmCreated -and $retries -lt $maxRetries) {
try {
$retries++
$securityType = if ($retries -eq 1) { "TrustedLaunch" } else { "Standard" }
$vmCreationError = $null # Reset error variable for each attempt
Write-Host "VM creation attempt $retries of $maxRetries with security type: $securityType" -ForegroundColor Yellow
# Create VM configuration based on current security type
if ($securityType -eq "TrustedLaunch") {
Write-Host "Attempting to create VM with Trusted Launch security..." -ForegroundColor Cyan
$vmConfig = New-AzVMConfig -VMName $VMName -VMSize $VMSize -SecurityType TrustedLaunch |
Set-AzVMOperatingSystem -Windows -ComputerName $VMName `
-Credential $cred -ProvisionVMAgent -EnableAutoUpdate |
Set-AzVMSourceImage -PublisherName "MicrosoftWindowsServer" `
-Offer "WindowsServer" -Skus "2022-Datacenter-g2" -Version "latest" |
Set-AzVMOSDisk -Name $OSDiskName -CreateOption FromImage |
Add-AzVMNetworkInterface -Id $nic.Id
# Configure Trusted Launch security features with enhanced settings
$vmConfig = Set-AzVMSecurityProfile -VM $vmConfig -SecurityType TrustedLaunch
$vmConfig = Set-AzVMUefi -VM $vmConfig -EnableVtpm $true -EnableSecureBoot $true
} else {
Write-Host "Falling back to standard VM configuration without Trusted Launch..." -ForegroundColor Yellow
$vmConfig = New-AzVMConfig -VMName $VMName -VMSize $VMSize |
Set-AzVMOperatingSystem -Windows -ComputerName $VMName `
-Credential $cred -ProvisionVMAgent -EnableAutoUpdate |
Set-AzVMSourceImage -PublisherName "MicrosoftWindowsServer" `
-Offer "WindowsServer" -Skus "2022-Datacenter" -Version "latest" |
Set-AzVMOSDisk -Name $OSDiskName -CreateOption FromImage |
Add-AzVMNetworkInterface -Id $nic.Id
}
# Configure boot diagnostics with the storage account created earlier
Write-Host "Configuring boot diagnostics..." -ForegroundColor Cyan
$vmConfig = Set-AzVMBootDiagnostic -VM $vmConfig -Enable `
-ResourceGroupName $ResourceGroupName `
-StorageAccountName $storageAccount.StorageAccountName
# Create the VM with detailed error capturing
Write-Host "Creating Virtual Machine: $VMName (Attempt $retries)..." -ForegroundColor Cyan
$vm = New-AzVM -ResourceGroupName $ResourceGroupName -Location $Location -VM $vmConfig -ErrorVariable vmCreationError
if ($vm) {
$vmCreated = $true
Write-Host "Virtual Machine created successfully with security type: $securityType" -ForegroundColor Green
} else {
throw "VM creation returned null result"
}
}
catch {
if ($retries -ge $maxRetries) {
Write-Host "Failed to create VM after $maxRetries attempts. Last error: $_" -ForegroundColor Red
throw
}
# Log the specific error details - with check to ensure vmCreationError is set
Write-Warning "VM creation attempt $retries failed: $_"
if ($vmCreationError) {
Write-Warning "VM Creation Error Details: $($vmCreationError | Out-String)"
}
# Add a delay before retry with exponential backoff
$delay = [Math]::Pow(2, $retries) * 5
Write-Host "Waiting $delay seconds before next attempt..." -ForegroundColor Yellow
Start-Sleep -Seconds $delay
}
}
if (-not $vm) {
throw "VM creation returned null result after $maxRetries attempts"
}
Write-Host "Configuring boot diagnostics..." -ForegroundColor Cyan
$vmConfig = Set-AzVMBootDiagnostic -VM $vmConfig -Enable `
-ResourceGroupName $ResourceGroupName `
-StorageAccountName $storageAccount.StorageAccountName
# Create the VM
Write-Host "Creating Virtual Machine: $VMName..." -ForegroundColor Cyan
$vm = New-AzVM -ResourceGroupName $ResourceGroupName -Location $Location -VM $vmConfig
if (-not $vm) {
throw "VM creation returned null result"
}
Write-Host "Virtual Machine created successfully" -ForegroundColor Green
# Configure auto-shutdown for 7 PM Mountain Standard Time
Write-Host "Configuring automatic shutdown schedule..." -ForegroundColor Cyan
try {
# Get the current subscription ID
$subscriptionId = (Get-AzContext).Subscription.Id
if (-not $subscriptionId) {
throw "Could not retrieve subscription ID"
}
# Get the VM resource ID
$vm = Get-AzVM -ResourceGroupName $ResourceGroupName -Name $VMName
if (-not $vm) {
throw "Could not retrieve VM details"
}
$vmId = $vm.Id
# Create the auto-shutdown schedule
$scheduleName = "shutdown-computevm-$VMName"
$scheduleId = "/subscriptions/$subscriptionId/resourceGroups/$ResourceGroupName/providers/microsoft.devtestlab/schedules/$scheduleName"
$scheduleConfig = @{
location = $vm.Location
properties = @{
status = "Enabled"
taskType = "ComputeVmShutdownTask"
dailyRecurrence = @{time = $ShutdownTime}
timeZoneId = $TimeZone
notificationSettings = @{
status = "Disabled"
timeInMinutes = 30
}
targetResourceId = $vmId
}
}
# Log the shutdown configuration details for debugging
Write-Host "Auto-shutdown details:" -ForegroundColor Cyan
Write-Host " Schedule Name: $scheduleName" -ForegroundColor Cyan
Write-Host " VM ID: $vmId" -ForegroundColor Cyan
Write-Host " Shutdown Time: $ShutdownTime" -ForegroundColor Cyan
Write-Host " Time Zone: $TimeZone" -ForegroundColor Cyan
# Create the auto-shutdown resource with correct parameter set (remove ResourceType when using ResourceId)
try {
$null = New-AzResource -ResourceId $scheduleId `
-Properties $scheduleConfig.properties `
-Location $vm.Location `
-ApiVersion "2018-09-15" `
-Force
Write-Host "Auto-shutdown schedule created using ResourceId parameter set" -ForegroundColor Green
}
catch {
Write-Warning "Failed with ResourceId parameter set: $($_.Exception.Message)"
# Try alternative parameter set if first method fails
try {
$null = New-AzResource -ResourceGroupName $ResourceGroupName `
-ResourceType "microsoft.devtestlab/schedules" `
-ResourceName $scheduleName `
-Properties $scheduleConfig.properties `
-Location $vm.Location `
-ApiVersion "2018-09-15" `
-Force
Write-Host "Auto-shutdown schedule created using ResourceGroupName parameter set" -ForegroundColor Green
}
catch {
Write-Warning "Failed with ResourceGroupName parameter set: $($_.Exception.Message)"
throw $_
}
}
# Try both naming conventions for verification
$schedule = Get-AzResource -ResourceId $scheduleId -ErrorAction SilentlyContinue
# Also check using the auto-shutdown naming convention that the Get-AzResource command uses
$autoShutdownId = "/subscriptions/$subscriptionId/resourceGroups/$ResourceGroupName/providers/microsoft.devtestlab/schedules/$VMName/auto-shutdown"
$autoShutdown = Get-AzResource -ResourceId $autoShutdownId -ErrorAction SilentlyContinue
if ($schedule -or $autoShutdown) {
Write-Host "Automatic shutdown scheduled successfully for $ShutdownTime $TimeZone" -ForegroundColor Green
} else {
# If verification failed, try the alternative naming convention
$altScheduleName = "$VMName/auto-shutdown"
$altScheduleId = "/subscriptions/$subscriptionId/resourceGroups/$ResourceGroupName/providers/microsoft.devtestlab/schedules/$altScheduleName"
Write-Host "Attempting alternative auto-shutdown naming convention..." -ForegroundColor Yellow
# Create schedule with alternative naming
$altScheduleConfig = @{
location = $vm.Location
properties = @{
status = "Enabled"
taskType = "ComputeVmShutdownTask"
dailyRecurrence = @{time = $ShutdownTime}
timeZoneId = $TimeZone
notificationSettings = @{
status = "Disabled"
timeInMinutes = 30
}
targetResourceId = $vmId
}
}
try {
$null = New-AzResource -ResourceId $altScheduleId `
-Properties $altScheduleConfig.properties `
-Location $vm.Location `
-ResourceType "microsoft.devtestlab/schedules" `
-ApiVersion "2018-09-15" `
-Force
# Verify the alternative schedule was created
$altSchedule = Get-AzResource -ResourceId $altScheduleId -ErrorAction SilentlyContinue
if ($altSchedule) {
Write-Host "Automatic shutdown scheduled successfully using alternative naming convention" -ForegroundColor Green
} else {
Write-Warning "Auto-shutdown schedule could not be verified with either naming convention"
}
} catch {
Write-Warning "Failed to create auto-shutdown with alternative naming: $_"
}
}
}
catch {
Write-Warning "Failed to configure auto-shutdown: $_"
Write-Warning "Error details: $($_.Exception.Message)"
if ($_.Exception.InnerException) {
Write-Warning "Inner error: $($_.Exception.InnerException.Message)"
}
# Use Az CLI as fallback method
try {
Write-Host "Attempting fallback method using Az CLI..." -ForegroundColor Yellow
$azCliCommand = "az vm auto-shutdown -g $ResourceGroupName -n $VMName --time $ShutdownTime --time-zone `"$TimeZone`""
Write-Host "Executing: $azCliCommand" -ForegroundColor Cyan
Invoke-Expression $azCliCommand
Write-Host "Auto-shutdown scheduled via Az CLI" -ForegroundColor Green
}
catch {
Write-Warning "Az CLI fallback method also failed: $_"
}
}
# Wait for VM to be fully ready
Write-Host "Waiting for VM to be fully ready..." -ForegroundColor Cyan
$maxRetries = 30
$retryCount = 0
$vmReady = $false
while (-not $vmReady -and $retryCount -lt $maxRetries) {
try {
$vmStatus = Get-AzVM -ResourceGroupName $ResourceGroupName -Name $VMName -Status
if ($vmStatus.Statuses[-1].Code -eq 'PowerState/running') {
$vmReady = $true
Write-Host "VM is ready and running" -ForegroundColor Green
break
}
}
catch {
Write-Warning "Retry ${retryCount}: Checking VM status failed: $_"
}
$retryCount++
if (-not $vmReady) {
Start-Sleep -Seconds 10
}
}
if (-not $vmReady) {
throw "VM failed to reach ready state after $maxRetries attempts"
}
Write-Host "Configuring WinRM on remote VM..." -ForegroundColor Cyan
$publicIpAddress = (Get-AzPublicIpAddress -ResourceGroupName $ResourceGroupName -Name "$VMName-IP").IpAddress
# Configure WinRM using custom script extension
$scriptContent = @'
winrm quickconfig -force
winrm set winrm/config/service/auth '@{Basic="true"}'
winrm set winrm/config/service '@{AllowUnencrypted="true"}'
New-NetFirewallRule -DisplayName "Windows Remote Management (HTTP-In)" -Name "Windows Remote Management (HTTP-In)" -Profile Any -LocalPort 5985 -Protocol TCP
Enable-PSRemoting -Force -SkipNetworkProfileCheck
'@
# Save script content to a temporary file
$tempFile = [System.IO.Path]::GetTempFileName() + ".ps1"
$scriptContent | Out-File -FilePath $tempFile -Encoding ASCII
# Upload script to storage account
$storageContext = $storageAccount.Context
$container = Get-AzStorageContainer -Name $ContainerName -Context $storageContext -ErrorAction SilentlyContinue
if (-not $container) {
# Create container with private access
$container = New-AzStorageContainer -Name $ContainerName -Context $storageContext -Permission Off
}
$blobName = "ConfigureWinRM.ps1"
Set-AzStorageBlobContent -File $tempFile -Container $ContainerName -Blob $blobName -Context $storageContext -Force
# Verify blob exists and generate SAS token
$blob = Get-AzStorageBlob -Container $ContainerName -Blob $blobName -Context $storageContext
if (-not $blob) {
throw "Failed to upload script to storage blob"
}
# Generate SAS token with read permission that expires in 1 hour
$sasToken = New-AzStorageBlobSASToken -Container $ContainerName `
-Blob $blobName `
-Context $storageContext `
-Permission r `
-ExpiryTime (Get-Date).AddHours(1) `
-FullUri
# Use SAS token URL for the custom script extension
$vm = Set-AzVMCustomScriptExtension `
-ResourceGroupName $ResourceGroupName `
-VMName $VMName `
-Name "ConfigureWinRM" `
-Location $Location `
-FileUri @($sasToken) `
-Run "ConfigureWinRM.ps1"
# Clean up temporary file
Remove-Item -Path $tempFile -Force
Write-Host "Configuring PowerShell Remoting..." -ForegroundColor Cyan
$publicIpAddress = (Get-AzPublicIpAddress -ResourceGroupName $ResourceGroupName `
-Name "$VMName-IP").IpAddress
# Initialize remoting using enhanced PS 7.5.0 configuration
Initialize-RemotingConfiguration
# Get a fresh reference to the VM to ensure we have valid data
$freshVM = Get-AzVM -ResourceGroupName $ResourceGroupName -Name $VMName -ErrorAction Stop
if (-not $freshVM) {
throw "Failed to get VM reference after deployment"
}
# Create the result object with explicit validation for each property
$result = [ordered]@{}
# Add each property with validation
if ($freshVM) {
$result['VM'] = $freshVM
} else {
throw "VM reference is null"
}
if ($publicIpAddress) {
$result['PublicIP'] = $publicIpAddress
} else {
throw "Public IP address is null"
}
if ($ResourceGroupName) {
$result['ResourceGroupName'] = $ResourceGroupName
} else {
throw "Resource group name is null"
}
if ($cred) {
$result['Credentials'] = $cred
} else {
throw "Credentials object is null"
}
# Final validation of the complete object
$requiredProps = @('VM', 'PublicIP', 'ResourceGroupName', 'Credentials')
$missingProps = $requiredProps | Where-Object { -not $result.Contains($_) }
if ($missingProps) {
throw "Result object missing required properties: $($missingProps -join ', ')"
}
Write-Host "Successfully created VM deployment object" -ForegroundColor Green
# Return directly to avoid array creation - don't assign to variable first
return [PSCustomObject]$result
}
catch {
Write-Error "Error in New-DomainControllerVM: $_"
throw
}
}
function Get-VMConnectionInfo {
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)]
[string]$ResourceGroupName,
[Parameter(Mandatory=$true)]
[string]$VMName
)
try {
Write-Host "Retrieving connection information..." -ForegroundColor Cyan
# Remove unused VM object retrieval
$publicIp = Get-AzPublicIpAddress -ResourceGroupName $ResourceGroupName -Name "$VMName-IP"
Write-Host "`nRDP Connection Information:" -ForegroundColor Green
Write-Host "------------------------" -ForegroundColor Green
Write-Host "Computer: $($publicIp.IpAddress)" -ForegroundColor Yellow
Write-Host "Username: $AdminUsername" -ForegroundColor Yellow
Write-Host "Password: Password can be found in .env file" -ForegroundColor Yellow
Write-Host "Domain: $DomainName" -ForegroundColor Yellow
Write-Host "`nTo connect:" -ForegroundColor Green
Write-Host "1. Open Remote Desktop Connection (mstsc.exe)" -ForegroundColor Cyan
Write-Host "2. Enter the IP address: $($publicIp.IpAddress)" -ForegroundColor Cyan
Write-Host "3. Use the credentials above" -ForegroundColor Cyan
Write-Host "Note: Domain join may take 5-10 minutes after script completion" -ForegroundColor Yellow
}
catch {
Write-Error "Failed to retrieve connection information: $_"
}
}
function Test-AutoShutdownSchedule {
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)]
[string]$ResourceGroupName,
[Parameter(Mandatory=$true)]
[string]$VMName
)
try {
Write-Host "Verifying auto-shutdown configuration..." -ForegroundColor Cyan
# Standard naming convention check
$autoShutdown = Get-AzResource -ResourceGroupName $ResourceGroupName `