# Pipeline # # Description: # Deploy Infrastructure as Code to Azure # # References: # YAML: https://aka.ms/yaml # ARM Deploy Task: https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/deploy/azure-resource-group-deployment # Input parameters: https://docs.microsoft.com/en-us/azure/devops/pipelines/process/runtime-parameters # Condotions: https://docs.microsoft.com/en-us/azure/devops/pipelines/process/conditions?view=azure-devops&tabs=yaml # Variables: https://docs.microsoft.com/en-us/azure/devops/pipelines/process/variables?view=azure-devops&tabs=yaml%2Cbatch # - Do not use variables with _ in name, this will break yaml (works fine in classic pipelines) # - Pass variables to options within "" to preserve spaces # VM Password: https://docs.microsoft.com/en-us/azure/virtual-machines/windows/faq#what-are-the-password-requirements-when-creating-a-vm # Secrets: https://devkimchi.com/2019/04/24/6-ways-passing-secrets-to-arm-templates/ # PAT usage: https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate # PAT creation: https://docs.microsoft.com/en-us/azure/devops/cli/log-in-via-pat # Role assignments: https://docs.microsoft.com/en-us/azure/role-based-access-control/role-assignments-cli # parameters: - name: defaultcompanyname displayName: Default Company Name. Will be used as base name for the resource group, vm, etc. Can be 2-8 char, all text. type: string default: # Note. This is not a best practice. For production environments, setup your keyvault before deployment. - name: defaultpassword displayName: Default PassWord. Overwrite with your own value, using Upper and Lower charachters, numbers ans special characters, min, 12. type: string default: skipme - name: designatedowner displayName: Provide the user principal name of the designated owner of the management group in case the Azure DevOps pipeline had read permissions to Azure AD. Otherwise, specify the object ID of the user, for example sjoerd@getshifting_com or aa8346ce-98d3-4223-accf-7ddd2a270388. type: string default: aa8346ce-98d3-4223-accf-7ddd2a270388 - name: softbudget displayName: Set a soft budget. Resources won't stop. Email to owner will be sent out on 90%. 24hour evaluation times may apply. type: string default: 45 # Disable CI/CD trigger: none pool: name: Azure Pipelines vmImage: 'windows-latest' #vmImage: 'ubuntu-latest' steps: # Parameters are required but checked here anyway if it is 0 characters, if so, it fails and exit the pipeline. - ${{ if eq(length(parameters.defaultcompanyname), 0) }}: - script: | echo Default Company Name is empty exit 1 displayName: Check for input parameter # The following task will display all parameters as a separate task when run. - ${{ each parameter in parameters }}: - script: echo ${{ parameter.Key }} ${{ parameter.Value }} displayName: Display all input parameters - task: AzurePowerShell@5 displayName: 'PS: Set Resource Group and Azure ID variables, create and display all system variables. ' inputs: azureSubscription: 'Visual Studio Professional' ScriptType: 'InlineScript' Inline: | $Context = Get-AzContext $SubscriptionId = $Context.Subscription.Id $TenantId = $Context.Subscription.TenantId $firstdayofmonth = get-date -Format "yyyy-MM-01" Write-Host "##vso[task.setvariable variable=rg]rg_${{ parameters.defaultcompanyname }}" Write-Host "##vso[task.setvariable variable=subscriptionid]$SubscriptionId" Write-Host "##vso[task.setvariable variable=tenantid]$TenantId" Write-Host "##vso[task.setvariable variable=firstdayofmonth]$firstdayofmonth" get-childitem -path env:* azurePowerShellVersion: 'LatestVersion' pwsh: true - task: AzureResourceManagerTemplateDeployment@3 displayName: Deploy Network ARM inputs: deploymentScope: 'Resource Group' azureResourceManagerConnection: 'Visual Studio Professional' subscriptionId: '$(subscriptionid)' action: 'Create Or Update Resource Group' resourceGroupName: '$(rg)' location: 'West Europe' templateLocation: 'Linked artifact' csmFile: 'InfraAsCode/aznetwork.json' csmParametersFile: 'InfraAsCode/aznetwork.parameters.json' overrideParameters: '-companyname "${{ parameters.defaultcompanyname }}"' deploymentMode: 'Incremental' deploymentName: 'Network-Deployment' # The following task only runs if the password is NOT set to skipme - ${{ if ne(parameters.defaultpassword, 'skipme') }}: - task: AzureResourceManagerTemplateDeployment@3 displayName: Deploy Key Vault ARM inputs: deploymentScope: 'Resource Group' azureResourceManagerConnection: 'Visual Studio Professional' subscriptionId: '$(subscriptionid)' action: 'Create Or Update Resource Group' resourceGroupName: '$(rg)' location: 'West Europe' templateLocation: 'Linked artifact' csmFile: 'InfraAsCode/keyvault.json' csmParametersFile: 'InfraAsCode/keyvault.parameters.json' overrideParameters: '-companyname "${{ parameters.defaultcompanyname }}" -secretValue "${{ parameters.defaultpassword }}"' deploymentMode: 'Incremental' deploymentName: 'KeyVault-Deployment' deploymentOutputs: 'arm-output' - task: PowerShell@2 displayName: 'PS: Create system variables from ARM Output. ' inputs: filePath: 'Support/parse_arm_deployment_output.ps1' arguments: > -ArmOutputString '$(arm-output)' showWarnings: true pwsh: true - task: PowerShell@2 displayName: 'PS: Display all system variables. ' continueOnError: true inputs: targetType: 'inline' script: | get-childitem -path env:* showWarnings: true pwsh: true - task: AzurePowerShell@5 displayName: 'PS: Set Permission to access Key Vault from pipeline. ' env: kvname: $(KEYVAULTNAME) inputs: azureSubscription: 'Visual Studio Professional' ScriptType: 'InlineScript' Inline: | $Context = Get-AzContext $AzureDevOpsServicePrincipal = Get-AzADServicePrincipal -ApplicationId $Context.Account.Id $ObjectId = $AzureDevOpsServicePrincipal.Id write-host "Test $ObjectId" write-host "Test $env:KVNAME" Set-AzKeyVaultAccessPolicy -VaultName $env:KVNAME -ObjectId $ObjectId -PermissionsToSecrets get,list azurePowerShellVersion: 'LatestVersion' pwsh: true - task: AzureKeyVault@1 displayName: 'Vault: import all secrets from Vault' inputs: azureSubscription: 'Visual Studio Professional' KeyVaultName: $(KEYVAULTNAME) SecretsFilter: '*' RunAsPreJob: false - task: AzureResourceManagerTemplateDeployment@3 displayName: Deploy VM ARM inputs: deploymentScope: 'Resource Group' azureResourceManagerConnection: 'Visual Studio Professional' subscriptionId: '$(subscriptionid)' action: 'Create Or Update Resource Group' resourceGroupName: '$(rg)' location: 'West Europe' templateLocation: 'Linked artifact' csmFile: 'InfraAsCode/vm.json' csmParametersFile: 'InfraAsCode/vm.parameters.json' overrideParameters: '-companyname "${{ parameters.defaultcompanyname }}" -adminPassword "$(vmAdminPassword)"' deploymentMode: 'Incremental' deploymentName: 'VM-Deployment' - task: AzureResourceManagerTemplateDeployment@3 displayName: Deploy Network Security ARM inputs: deploymentScope: 'Resource Group' azureResourceManagerConnection: 'Visual Studio Professional' subscriptionId: '$(subscriptionid)' action: 'Create Or Update Resource Group' resourceGroupName: '$(rg)' location: 'West Europe' templateLocation: 'Linked artifact' csmFile: 'InfraAsCode/networksecurity.json' csmParametersFile: 'InfraAsCode/networksecurity.parameters.json' overrideParameters: '-companyname "${{ parameters.defaultcompanyname }}"' deploymentMode: 'Incremental' deploymentName: 'NetworkSecurity-Deployment' - task: AzureResourceManagerTemplateDeployment@3 displayName: Deploy Recovery Services Vault ARM inputs: deploymentScope: 'Resource Group' azureResourceManagerConnection: 'Visual Studio Professional' subscriptionId: '$(subscriptionid)' action: 'Create Or Update Resource Group' resourceGroupName: '$(rg)' location: 'West Europe' templateLocation: 'Linked artifact' csmFile: 'InfraAsCode/rsvault.json' csmParametersFile: 'InfraAsCode/rsvault.parameters.json' overrideParameters: '-companyname "${{ parameters.defaultcompanyname }}"' deploymentMode: 'Incremental' deploymentName: 'RS-Vault-Deployment' - task: AzureResourceManagerTemplateDeployment@3 displayName: Deploy VM Backup ARM inputs: deploymentScope: 'Resource Group' azureResourceManagerConnection: 'Visual Studio Professional' subscriptionId: '$(subscriptionid)' action: 'Create Or Update Resource Group' resourceGroupName: '$(rg)' location: 'West Europe' templateLocation: 'Linked artifact' csmFile: 'InfraAsCode/vmbackup.json' csmParametersFile: 'InfraAsCode/vmbackup.parameters.json' overrideParameters: '-companyname "${{ parameters.defaultcompanyname }}"' deploymentMode: 'Incremental' deploymentName: 'VM-Backup-Deployment' - task: AzurePowerShell@5 displayName: 'PS: Create Management Group and Assign Owner. ' env: defaultname: "${{ parameters.defaultcompanyname }}" newowner: "${{ parameters.designatedowner }}" inputs: azureSubscription: 'Visual Studio Professional' ScriptType: 'InlineScript' Inline: | # Management Group $managementGroupId = "mg-" + $env:defaultname Write-Host "##vso[task.setvariable variable=mgid]$managementGroupId" $managementGroupDisplayName = $env:defaultname + " Management Group" # Define Object ID Write-Output "This task needs Azure DevOps to have read permissions on Azure Active Directory. " Write-Output "In Azure DevOps -> Project -> Project Settings -> Service Connections -> Find your Service Connection -> Manage Service Principal. " Write-Output "This will open the Azure AD registered Application page of the service principal. " Write-Output "Go to API permissions -> Add a Permission -> Azure Active Directory Graph." Write-Output "Add the Application Permission Directory.Read.All and click Add Permissions. " Write-Output "Click Grant admin consent for ... to apply the new permissions. " if ($env:newowner -match "@"){ $userid = (Get-AzADUser -UserPrincipalName $env:newowner).id Write-Output "$env:newowner object id = $userid" if (([string]::IsNullOrEmpty($userid))){ Write-Host "##vso[task.logissue type=error]User Object ID for $env:newowner cannot be retrieved. Please retrieve the object id from the Cloud Shell: (Get-AzADUser -UserPrincipalName $env:newowner).id " exit 1 } }else{ $userid = $env:newowner } # Start Write-Output "The Object ID $userid will be owner of the new Management Group $managementGroupId" try{ #New-AzManagementGroup -GroupName $managementGroupId -DisplayName $managementGroupDisplayName # Breaking change New-AzManagementGroup -GroupId $managementGroupId -DisplayName $managementGroupDisplayName # Breaking change } catch{ Write-Output "Something failed while creating the management group. Error: " write-output "$($_.Exception.Message)" Write-Output "If the error matches Unable to cast object of type Microsoft.Azure.Management.ManagementGroups.Models.ManagementGroup to type Newtonsoft.Json.Linq.JObject it's because the management group already exists. " } try{ New-AzRoleAssignment -ObjectId $userid -RoleDefinitionName "Owner" -Scope "/providers/Microsoft.Management/managementGroups/$managementGroupId" } catch{ Write-Output "Something failed while assigning permissions. Error: " Write-Output "$($_.Exception.Message)" Write-Output "If the error matches Exception of type Microsoft.Rest.Azure.CloudException was thrown. this can be cautionally ignored. Check if the permissions were assigned correctly, but as far as I can find this is a general error message and a bug. " } azurePowerShellVersion: 'LatestVersion' pwsh: true - task: AzureCLI@2 displayName: Move Subscription to Management Group continueOnError: true env: defaultname: "${{ parameters.defaultcompanyname }}" mgid: "$(mgid)" subid: "$(subscriptionid)" inputs: azureSubscription: 'Visual Studio Professional' scriptType: 'bash' scriptLocation: 'inlineScript' inlineScript: | echo "This all can and should be done in the previous task. But I initially used Azure CLI and wanted to keep some of it in the pipeline for future reference. " managementGroupDisplayName="$defaultname Management Group" echo "##vso[task.setvariable variable=mgname]$managementGroupDisplayName" echo "Note that if the subscription is already in a Management Group the Azure DevOps Service Principal needs (owner?) permissions to the existing management group. Also, the management group will not be visible to users until the subscription is assigned, unless explicit permissions are assigned (as done in the previous task). " echo "If the subscription is in the Tenant Root Group the Azure DevOps Service Principal needs (owner?) permissions on the subscription. " az account management-group list --query "[].{name:name, id:id}" --output tsv az account management-group subscription add --name $mgid --subscription $subid || echo "Moving the subscription failed, probably because of permissions. Please move the subscription manually. " - task: PowerShell@2 displayName: 'PS: Display all system variables. ' continueOnError: true inputs: targetType: 'inline' script: | get-childitem -path env:* showWarnings: true pwsh: true # The next two tasks do not work currently. The first task creates an Azure DevOps Service Connection. The second task uses the service connection to deploy policies on a management group level. # Error if you try it anyway: #There was a resource authorization issue: "The pipeline is not valid. Job Job: Step AzureResourceManagerTemplateDeployment7 input ConnectedServiceName references service connection $(scname) which could not be found. The service connection does not exist or has not been authorized for use. For authorization details, refer to https://aka.ms/yamlauthz." # If you want to use this method, use a fixed service connection name in the second task, and create / update a service connection in the first task with this fixed name. Optionally, you should create an additional task to delete the service connection. # Here I choose to deploy the policy using the AzureCLI. # - task: AzureCLI@2 # displayName: Create Azure DevOps Management Group Service Connection # env: # defaultname: "${{ parameters.defaultcompanyname }}" # subid: "$(subscriptionid)" # tenantid: "$(tenantid)" # mgid: "$(mgid)" # mgname: "$(mgname)" # inputs: # azureSubscription: 'Visual Studio Professional' # scriptType: 'bash' # scriptLocation: 'inlineScript' # inlineScript: | # export SCNAME="SC - MG - "$defaultname # echo "##vso[task.setvariable variable=scname]$SCNAME" # export SCFILE=".\Support\serviceconnection.json" # export AZURE_DEVOPS_EXT_PAT="PATmustbeprovidedfromkeyvault" # cat $SCFILE # # Replace tokens in the template service connection file # sed -i "s/__tenant-id__/$tenantid/g" $SCFILE # sed -i "s/__management-group-id__/$mgid/g" $SCFILE # sed -i "s/__management-group-name__/$mgname/g" $SCFILE # sed -i "s/__service-connection-name__/$SCNAME/g" $SCFILE # cat $SCFILE # az devops configure -d organization=https://dev.azure.com// # az devops service-endpoint create --service-endpoint-configuration $SCFILE --project # az devops service-endpoint list --project -o table - task: AzureCLI@2 displayName: Deploy MG Policy - Allowed Locations env: mgid: "$(mgid)" inputs: azureSubscription: 'Visual Studio Professional' scriptType: 'bash' scriptLocation: 'inlineScript' inlineScript: | az deployment mg create --location westeurope \ --management-group-id "$(mgid)" \ --name "MGP-AllowedLocations" \ --template-file "InfraAsCode/mgp.allowedlocations.json" \ --parameters "InfraAsCode/mgp.allowedlocations.parameters.json" \ --parameters targetMG="$(mgid)" - task: AzureCLI@2 displayName: Deploy MG Policy - Deny RDP with Inbound RDP env: mgid: "$(mgid)" inputs: azureSubscription: 'Visual Studio Professional' scriptType: 'bash' scriptLocation: 'inlineScript' inlineScript: | az deployment mg create --location westeurope \ --management-group-id "$(mgid)" \ --name "MGP-DenyRDPwithInboundInternet" \ --template-file "InfraAsCode/mgp.denyrdpinternet.json" \ --parameters @InfraAsCode/mgp.denyrdpinternet.parameters.json \ --parameters targetMG="$(mgid)" - task: AzureCLI@2 displayName: Deploy MG Policy - Audit VMs that do not use managed disks env: mgid: "$(mgid)" inputs: azureSubscription: 'Visual Studio Professional' scriptType: 'bash' scriptLocation: 'inlineScript' inlineScript: | az deployment mg create --location westeurope \ --management-group-id "$(mgid)" \ --name "MGP-AuditVMsUnmanagedDisks" \ --template-file "InfraAsCode/mgp.auditvmsunmanageddisks.json" \ --parameters @InfraAsCode/mgp.auditvmsunmanageddisks.parameters.json \ --parameters targetMG="$(mgid)" - task: AzureResourceManagerTemplateDeployment@3 displayName: Deploy Budget ARM inputs: deploymentScope: 'Subscription' azureResourceManagerConnection: 'Visual Studio Professional' subscriptionId: '$(subscriptionid)' location: 'West Europe' templateLocation: 'Linked artifact' csmFile: 'InfraAsCode/budget.json' csmParametersFile: 'InfraAsCode/budget.parameters.json' overrideParameters: '-companyname "${{ parameters.defaultcompanyname }}" -amount "${{ parameters.softbudget }}" -startDate "$(firstdayofmonth)"' deploymentMode: 'Incremental' deploymentName: 'Budget-Deployment' - task: AzureResourceManagerTemplateDeployment@3 displayName: Deploy Dashboard ARM inputs: deploymentScope: 'Resource Group' azureResourceManagerConnection: 'Visual Studio Professional' subscriptionId: '$(subscriptionid)' action: 'Create Or Update Resource Group' resourceGroupName: '$(rg)' location: 'West Europe' templateLocation: 'Linked artifact' csmFile: 'InfraAsCode/dashboard.json' csmParametersFile: 'InfraAsCode/dashboard.parameters.json' overrideParameters: '-dashboardName "${{ parameters.defaultcompanyname }}" -targetMG "$(mgid)"' deploymentMode: 'Incremental' deploymentName: 'DashBoard-Deployment'