├── .gitattributes ├── .gitignore ├── README.md ├── ase-agent ├── DSC │ ├── ConfigureASEBuildAgent.ps1 │ └── ConfigureASEBuildAgent.ps1.zip ├── README.md └── azuredeploy.json ├── ase-devops ├── README.md ├── ase_devops.png └── azuredeploy.json ├── ase ├── README.md ├── azuredeploy.json └── nested-templates │ └── ase-ilb-certificate.json ├── core-network ├── DSC │ ├── ConfigureADBDC.ps1 │ ├── ConfigureADBDC.ps1.zip │ ├── CreateADPDC.ps1 │ └── CreateADPDC.ps1.zip ├── README.md ├── azuredeploy.json ├── core-network.png └── nested-templates │ └── update-vnet-dns.json ├── devnet-tfs-ha ├── README.md ├── azuredeploy.json └── private_ha_devops.png ├── devnet-tfs ├── README.md ├── azuredeploy.json └── private_devops.png ├── gadgetron-workstation ├── README.md ├── azuredeploy.json └── scripts │ ├── bootstrap_gadgetron.sh │ └── setup_gadgetron_workstation.sh ├── iaas-web ├── DSC │ ├── ConfigureWeb.ps1 │ └── ConfigureWeb.ps1.zip ├── README.md └── azuredeploy.json ├── primitives ├── encryptvm.json ├── functionapp.json ├── linuxvm.json ├── publicip.json ├── vnet.json └── windowsvm.json ├── scripts ├── CreateKeyVault.ps1 ├── PrepareAseDeployment.ps1 └── PrepareDevnetTfsDeployment.ps1 ├── sql-alwayson ├── DSC │ ├── AddDatabaseAG.ps1 │ ├── AddDatabaseAG.ps1.zip │ ├── PrepareSQLServer.ps1 │ └── PrepareSQLServer.ps1.zip ├── README.md ├── adddbtoag.json └── azuredeploy.json ├── tfs-ha ├── DSC │ ├── InstallTFS.ps1 │ ├── InstallTFS.ps1.zip │ ├── InstallVSTSAgent.ps1 │ └── InstallVSTSAgent.ps1.zip ├── README.md ├── azuredeploy.json └── deployagents.json └── tfs ├── DSC ├── TFSSQLServerConfig.ps1 └── TFSSQLServerConfig.ps1.zip ├── README.md └── azuredeploy.json /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.jfm 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | 217 | # Business Intelligence projects 218 | *.rdl.data 219 | *.bim.layout 220 | *.bim_*.settings 221 | 222 | # Microsoft Fakes 223 | FakesAssemblies/ 224 | 225 | # GhostDoc plugin setting file 226 | *.GhostDoc.xml 227 | 228 | # Node.js Tools for Visual Studio 229 | .ntvs_analysis.dat 230 | 231 | # Visual Studio 6 build log 232 | *.plg 233 | 234 | # Visual Studio 6 workspace options file 235 | *.opt 236 | 237 | # Visual Studio LightSwitch build output 238 | **/*.HTMLClient/GeneratedArtifacts 239 | **/*.DesktopClient/GeneratedArtifacts 240 | **/*.DesktopClient/ModelManifest.xml 241 | **/*.Server/GeneratedArtifacts 242 | **/*.Server/ModelManifest.xml 243 | _Pvt_Extensions 244 | 245 | # Paket dependency manager 246 | .paket/paket.exe 247 | paket-files/ 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # JetBrains Rider 253 | .idea/ 254 | *.sln.iml 255 | 256 | # CodeRush 257 | .cr/ 258 | 259 | # Python Tools for Visual Studio (PTVS) 260 | __pycache__/ 261 | *.pyc -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Infrastructure as Code (IaC) 2 | ---------------------------- 3 | 4 | This repository contains a number of templates for deploying infrastructure in Microsoft [Azure](https://azure.microsoft.com/en-us/). 5 | 6 | * [core-network](core-network/) is a basic virtual network with two domain controllers in an availability set and a jump box for connecting to the VNET. 7 | * [tfs](tfs/) is a template for deploying [Team Foundation Server](https://www.visualstudio.com/tfs/) (TFS) into an existing virtual network with domain controllers already deployed. 8 | * [devnet-tfs](devnet-tfs) is a combination of core-network and tfs mentioned above. First the core network is deployed and then the tfs server with database and build agent. 9 | * [sql-alwayson](sql-alwayson) is a template automation for deploying SQL Server (2016 or 2017) with "Always On", high-availability configuration configuration. 10 | * [tfs-ha] is a template for deploying a high availability version of [Team Foundation Server](https://www.visualstudio.com/tfs/) (TFS). It is recommended to use the [SQL Always On](sql-alwayson) configuration on the backend for a true high availability configuration. * [devnet-tfs-ha](devnet-tfs-ha) is a combination of [core-network](core-network), [sql-alwayson](sql-alwayson), and [tfs-ha](tfs-ha) mentioned above (including build agents) for a complete end-to-end high-availability DevOps environment base on TFS. 11 | * [ase](ase/) is a template for deploy Azure App Service Environment (ASE) into an existing or new Virtual Network. 12 | * [ase-agent](ase-agent/) is a template for deploying a VSTS/TFS build agent into a Virtual Network and let it deploy to a Web App in an App Service Environment (ASE). 13 | * [ase-devops](ase-devops/) is a combination of [ase](ase/) and [ase-agent](ase-agent/) to demonstrate the complete ASE VSTS/TFS DevOps experience. 14 | -------------------------------------------------------------------------------- /ase-agent/DSC/ConfigureASEBuildAgent.ps1: -------------------------------------------------------------------------------- 1 | configuration ConfigureASEBuildAgentDsc 2 | { 3 | param 4 | ( 5 | [Parameter(Mandatory)] 6 | [String]$TSUrl, 7 | 8 | [Parameter(Mandatory)] 9 | [String]$AgentPool, 10 | 11 | [Parameter(Mandatory)] 12 | [String]$PAToken, 13 | 14 | [Parameter(Mandatory)] 15 | [String]$AseIp, 16 | 17 | [Parameter(Mandatory)] 18 | [String]$AppDns, 19 | 20 | [Parameter(Mandatory=$false)] 21 | [String]$VSTSAgentUrl = "https://vstsagentpackage.azureedge.net/agent/2.127.0/vsts-agent-win-x64-2.127.0.zip" 22 | ) 23 | 24 | Import-DscResource -ModuleName xNetworking, 'PSDesiredStateConfiguration' 25 | 26 | $tmp = $AppDns.Split('.') 27 | $OFS='.' 28 | $AppScmDns = $tmp[0] + ".scm." + [String]$tmp[1..$tmp.Count] 29 | 30 | Node localhost 31 | { 32 | Script DownloadAgent 33 | { 34 | GetScript = { 35 | return @{ 'Result' = $true } 36 | } 37 | SetScript = { 38 | $agentUrl = $using:VSTSAgentUrl 39 | $agentZip = "$env:TEMP" + "\vsts_agent.zip" 40 | Write-Host "Downloading TFS: $agentUrl" 41 | Invoke-WebRequest -Uri $agentUrl -OutFile $agentZip 42 | } 43 | TestScript = { 44 | $agentZip = "$env:TEMP" + "\vsts_agent.zip" 45 | Test-Path $agentZip 46 | } 47 | } 48 | 49 | 50 | Script UnzipAgent 51 | { 52 | GetScript = { 53 | return @{ 'Result' = $true } 54 | } 55 | SetScript = { 56 | $agentZip = "$env:TEMP" + "\vsts_agent.zip" 57 | $agentPath = "C:\agent" 58 | If(!(Test-Path $agentPath)) 59 | { 60 | New-Item -ItemType Directory -Force -Path $agentPath 61 | } 62 | 63 | $shell = New-Object -com shell.application 64 | $zip = $shell.NameSpace($agentZip) 65 | Foreach($item in $zip.items()) 66 | { 67 | $shell.Namespace($agentPath).copyhere($item) 68 | } 69 | } 70 | TestScript = { 71 | Test-Path "C:\agent\config.cmd" 72 | } 73 | DependsOn = "[Script]DownloadAgent" 74 | } 75 | 76 | 77 | Script ConfigAgent 78 | { 79 | GetScript = { return @{ 'Result' = $true }} 80 | SetScript = { 81 | Set-Location "C:\agent" 82 | $tsurl = $using:TSUrl 83 | $tok = $using:PAToken 84 | $pool = $using:AgentPool 85 | $cmd = ".\config.cmd --unattended --runAsService --work _work --url $tsurl --auth pat --token $tok --pool $pool --agent $env:COMPUTERNAME" 86 | Invoke-Expression $cmd | Write-Verbose 87 | } 88 | TestScript = { 89 | Test-Path "C:\agent\.credentials" 90 | } 91 | DependsOn = "[Script]UnzipAgent" 92 | } 93 | 94 | xHostsFile HostEntry 95 | { 96 | HostName = $AppDns 97 | IPAddress = $AseIp 98 | Ensure = 'Present' 99 | } 100 | 101 | xHostsFile HostScmEntry 102 | { 103 | HostName = $AppScmDns 104 | IPAddress = $AseIp 105 | Ensure = 'Present' 106 | } 107 | 108 | Registry StrongCrypto1 109 | { 110 | Ensure = "Present" 111 | Key = "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework\v4.0.30319" 112 | ValueName = "SchUseStrongCrypto" 113 | ValueType = "Dword" 114 | ValueData = "00000001" 115 | } 116 | 117 | Registry StrongCrypto2 118 | { 119 | Ensure = "Present" 120 | Key = "HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\.NETFramework\v4.0.30319" 121 | ValueName = "SchUseStrongCrypto" 122 | ValueType = "Dword" 123 | ValueData = "00000001" 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /ase-agent/DSC/ConfigureASEBuildAgent.ps1.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hansenms/iac/277b41b9f9a0ce395f577d8a74640000c91c5ce1/ase-agent/DSC/ConfigureASEBuildAgent.ps1.zip -------------------------------------------------------------------------------- /ase-agent/README.md: -------------------------------------------------------------------------------- 1 | VSTS Build Agent in Private Virtual Network for ASE 2 | --------------------------------------------------- 3 | 4 | The Azure [App Service Environment (ASE)](https://docs.microsoft.com/en-us/azure/app-service/environment/intro) allows you to deploy Azure Web Apps into a private environment for enhanced security and access control. One challenge with this configuration is how to orchestrate Continuous Integration and Continuous Deployment (CI/CD) with [Visual Studio Team Services](https://www.visualstudio.com/team-services/) or [Team Foundation Server](https://www.visualstudio.com/tfs/) into such environments. 5 | 6 | This template deploys a VSTS/TFS build agent into the Virtual Network where the ASE is deployed and connects this agent to a VSTS or TFS instance. It also adds appropriate `hosts` file entries to the agent to allow it to deploy to a specific Web App in an ASE. 7 | 8 | To ensure that the configuration of the agen is correct, you need to supply: 9 | 10 | * `TSServerUrl`: Url of your VSTS/TFS instance 11 | * `AgentPool`: Name of the agent pool in the VSTS/TFS instance (needs to be created in advance) 12 | * `PAToken`: Personal Access Token for agent to register with the VSTS/TFS instance 13 | * `AseIp`: The IP address of the ASE environment. 14 | * `AppDns`: The DNS name of the app, e.g. myapp.contoso-internal.us 15 | 16 | 17 | 18 | 19 | 20 | 21 | 23 | -------------------------------------------------------------------------------- /ase-agent/azuredeploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "subnetId": { 6 | "type": "string" 7 | }, 8 | "vmSize": { 9 | "type": "string", 10 | "allowedValues": [ 11 | "Standard_DS1_v2", 12 | "Standard_DS2_v2", 13 | "Standard_DS3_v2", 14 | "Standard_DS4_v2", 15 | "Standard_DS5_v2", 16 | "Standard_DS11_v2", 17 | "Standard_DS12_v2", 18 | "Standard_DS13_v2", 19 | "Standard_DS14_v2", 20 | "Standard_DS15_v2" 21 | ], 22 | "defaultValue": "Standard_DS2_v2" 23 | }, 24 | "adminUsername": { 25 | "type": "string", 26 | "defaultValue": "EnterpriseAdmin" 27 | }, 28 | "adminPassword": { 29 | "type": "securestring" 30 | }, 31 | "vmName": { 32 | "type": "string", 33 | "defaultValue": "ase-build-agent" 34 | }, 35 | "ImagePublisher": { 36 | "type": "string", 37 | "defaultValue": "MicrosoftVisualStudio" 38 | }, 39 | "ImageOffer": { 40 | "type": "string", 41 | "defaultValue": "VisualStudio" 42 | }, 43 | "ImageSku": { 44 | "type": "string", 45 | "defaultValue": "VS-2017-Ent-Latest-WS2016" 46 | }, 47 | "TSServerUrl": { 48 | "type": "string" 49 | }, 50 | "AgentPool": { 51 | "type": "string" 52 | }, 53 | "PAToken": { 54 | "type": "string" 55 | }, 56 | "AseIp": { 57 | "type": "string" 58 | }, 59 | "AppDns": { 60 | "type": "string" 61 | } 62 | }, 63 | "variables": { 64 | "baseUri": "[deployment().properties.templateLink.uri]", 65 | "windowsVmTemplateURL": "[uri(variables('baseUri'),'../primitives/windowsvm.json')]", 66 | "AgentConfigureModuleURL": "[uri(variables('baseUri'), 'DSC/ConfigureASEBuildAgent.ps1.zip')]", 67 | "AgentConfigureFunction": "ConfigureASEBuildAgent.ps1\\ConfigureASEBuildAgentDsc", 68 | "agentDownloadUrl": "https://vstsagentpackage.azureedge.net/agent/2.127.0/vsts-agent-win-x64-2.127.0.zip", 69 | 70 | }, 71 | "resources": [ 72 | { 73 | "name": "AgentVM", 74 | "type": "Microsoft.Resources/deployments", 75 | "apiVersion": "2017-05-10", 76 | "properties": { 77 | "mode": "Incremental", 78 | "templateLink": { 79 | "uri": "[variables('windowsVmTemplateURL')]", 80 | "contentVersion": "1.0.0.0" 81 | }, 82 | "parameters": { 83 | "vmName": { 84 | "value": "[parameters('vmName')]" 85 | }, 86 | "vmSize": { 87 | "value": "[parameters('vmSize')]" 88 | }, 89 | "subnetId": { 90 | "value": "[parameters('subnetId')]" 91 | }, 92 | "adminUsername": { 93 | "value": "[parameters('adminUsername')]" 94 | }, 95 | "adminPassword": { 96 | "value": "[parameters('adminPassword')]" 97 | }, 98 | "assignPublicIP": { 99 | "value": false 100 | }, 101 | "imagePublisher": { 102 | "value": "[parameters('ImagePublisher')]" 103 | }, 104 | "imageOffer": { 105 | "value": "[parameters('ImageOffer')]" 106 | }, 107 | "imageSku": { 108 | "value": "[parameters('ImageSku')]" 109 | } 110 | } 111 | } 112 | }, 113 | { 114 | "type": "Microsoft.Compute/virtualMachines/extensions", 115 | "name": "[concat(parameters('vmName'), '/configureagent')]", 116 | "dependsOn": [ 117 | "[resourceId('Microsoft.Resources/deployments', 'AgentVM')]" 118 | ], 119 | "apiVersion": "2016-03-30", 120 | "location": "[resourceGroup().location]", 121 | "properties": { 122 | "publisher": "Microsoft.Powershell", 123 | "type": "DSC", 124 | "typeHandlerVersion": "2.21", 125 | "autoUpgradeMinorVersion": true, 126 | "settings": { 127 | "modulesURL": "[variables('AgentConfigureModuleURL')]", 128 | "configurationFunction": "[variables('AgentConfigureFunction')]", 129 | "properties": { 130 | "TSUrl": "[parameters('TSServerUrl')]", 131 | "VSTSAgentUrl": "[variables('agentDownloadUrl')]", 132 | "AgentPool": "[parameters('AgentPool')]", 133 | "PAToken": "[parameters('PAToken')]", 134 | "AseIp": "[parameters('AseIp')]", 135 | "AppDns": "[parameters('AppDns')]" 136 | } 137 | } 138 | } 139 | } 140 | ] 141 | } -------------------------------------------------------------------------------- /ase-devops/README.md: -------------------------------------------------------------------------------- 1 | DevOps Enabled App Service Environment 2 | -------------------------------------- 3 | 4 | The Azure [App Service Environment (ASE)](https://docs.microsoft.com/en-us/azure/app-service/environment/intro) allows you to deploy Azure Web Apps into a private environment for enhanced security and access control. One challenge with this configuration is how to orchestrate Continuous Integration and Continuous Deployment (CI/CD) with [Visual Studio Team Services](https://www.visualstudio.com/team-services/) or [Team Foundation Server](https://www.visualstudio.com/tfs/) into such environments. 5 | 6 | The pattern in this template deployment illustrates how to combine ASE with CI/CD using VSTS or TFS. It uses the [ase](../ase) template to establish an App Service Environment with a Web App. It also uses the [ase-agent](../ase-agent) template to deploy a VSTS/TFS agent in the Virtual Network and connect this agent with a VSTS or TFS instance. Finally, a jump-box is also deploy in this case to allow access to the Virtual Network for testing purposes. 7 | 8 | The final deployment looks like this: 9 | 10 | ![ase-devops](ase_devops.png) 11 | 12 | A number of parameters (including SSL certificate) are needed for proper deployment of this pattern. To assist with preparing these parameters, there is a [convenience script](https://github.com/hansenms/iac/blob/master/scripts/PrepareAseDeployment.ps1). You need an SSL certificate to support the DNS names `*.domainname` and `*.scm.domainname`, if you don't have one, the script with create a self-signed certificate for you. You can call the script with: 13 | 14 | ``` 15 | iac\scripts\PrepareAseDeployment.ps1 -DomainName mydomain-internal.us ` 16 | -TSServerUrl "https://.visualstudio.com" -AdminUsername EnterpriseAdmin` 17 | -AgentPool -PAToken ` 18 | -OutFile C:\temp\myase-devops.parameters.json 19 | ``` 20 | 21 | The `AgentPool` and `PAToken` parameters need to be established in your VSTS or TFS instance in advance. The script will save all the parameters in a template parameter json file that you can copy/paste from or use directly with CLI or Powershell commands to deploy the template. 22 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | -------------------------------------------------------------------------------- /ase-devops/ase_devops.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hansenms/iac/277b41b9f9a0ce395f577d8a74640000c91c5ce1/ase-devops/ase_devops.png -------------------------------------------------------------------------------- /ase-devops/azuredeploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "jumpvmSize": { 6 | "type": "string", 7 | "allowedValues": [ 8 | "Standard_DS1_v2", 9 | "Standard_DS2_v2", 10 | "Standard_DS3_v2", 11 | "Standard_DS4_v2", 12 | "Standard_DS5_v2", 13 | "Standard_DS11_v2", 14 | "Standard_DS12_v2", 15 | "Standard_DS13_v2", 16 | "Standard_DS14_v2", 17 | "Standard_DS15_v2" 18 | ], 19 | "defaultValue": "Standard_DS2_v2" 20 | }, 21 | "agentVmSize": { 22 | "type": "string", 23 | "allowedValues": [ 24 | "Standard_DS1_v2", 25 | "Standard_DS2_v2", 26 | "Standard_DS3_v2", 27 | "Standard_DS4_v2", 28 | "Standard_DS5_v2", 29 | "Standard_DS11_v2", 30 | "Standard_DS12_v2", 31 | "Standard_DS13_v2", 32 | "Standard_DS14_v2", 33 | "Standard_DS15_v2" 34 | ], 35 | "defaultValue": "Standard_DS2_v2" 36 | }, 37 | "adminUsername": { 38 | "type": "string", 39 | "defaultValue": "EnterpriseAdmin" 40 | }, 41 | "adminPassword": { 42 | "type": "securestring" 43 | }, 44 | "domainName": { 45 | "type": "string", 46 | "defaultValue": "contoso-internal.us" 47 | }, 48 | "aseName": { 49 | "type": "string", 50 | "defaultValue": "GENERATE" 51 | }, 52 | "siteName": { 53 | "type": "string", 54 | "defaultValue": "ase-site" 55 | }, 56 | "TSServerUrl": { 57 | "type": "string" 58 | }, 59 | "AgentPool": { 60 | "type": "string" 61 | }, 62 | "PAToken": { 63 | "type": "string" 64 | }, 65 | "pfxBlobString": { 66 | "type": "string" 67 | }, 68 | "certificatePassword": { 69 | "type": "securestring" 70 | }, 71 | "certificateThumbprint": { 72 | "type": "string" 73 | } 74 | }, 75 | "variables": { 76 | "baseUri": "[deployment().properties.templateLink.uri]", 77 | "windowsVmTemplateURL": "[uri(variables('baseUri'),'../primitives/windowsvm.json')]", 78 | "agentTemplateURL": "[uri(variables('baseUri'),'../ase-agent/azuredeploy.json')]", 79 | "aseTemplateURL": "[uri(variables('baseUri'),'../ase/azuredeploy.json')]", 80 | "vnetName": "asevnet", 81 | "subnetName1": "subnet-1", 82 | "subnetName2": "subnet-2", 83 | "subnetId1": "[concat(resourceId('Microsoft.Network/virtualNetworks', variables('vnetName')), '/subnets/', variables('subnetName1'))]", 84 | "subnetId2": "[concat(resourceId('Microsoft.Network/virtualNetworks', variables('vnetName')), '/subnets/', variables('subnetName2'))]", 85 | "jumpVmName": "JumpBox", 86 | "buildAgentVmName": "build-agent", 87 | "AppDns": "[concat(parameters('siteName'),'.',parameters('domainName'))]", 88 | "AseIp": "10.0.1.11" 89 | }, 90 | "resources": [ 91 | { 92 | "name": "[variables('vnetName')]", 93 | "type": "Microsoft.Network/virtualNetworks", 94 | "location": "[resourceGroup().location]", 95 | "apiVersion": "2016-03-30", 96 | "dependsOn": [], 97 | "properties": { 98 | "addressSpace": { 99 | "addressPrefixes": [ 100 | "10.0.0.0/16" 101 | ] 102 | }, 103 | "subnets": [ 104 | { 105 | "name": "[variables('subnetName1')]", 106 | "properties": { 107 | "addressPrefix": "10.0.0.0/24" 108 | } 109 | }, 110 | { 111 | "name": "[variables('subnetName2')]", 112 | "properties": { 113 | "addressPrefix": "10.0.1.0/25" 114 | } 115 | } 116 | ] 117 | } 118 | }, 119 | { 120 | "name": "[concat('jump',resourceGroup().name)]", 121 | "type": "Microsoft.Resources/deployments", 122 | "apiVersion": "2017-05-10", 123 | "dependsOn": [ 124 | "[resourceId('Microsoft.Network/virtualNetworks',variables('vnetName'))]" 125 | ], 126 | "properties": { 127 | "mode": "Incremental", 128 | "templateLink": { 129 | "uri": "[variables('windowsVmTemplateURL')]", 130 | "contentVersion": "1.0.0.0" 131 | }, 132 | "parameters": { 133 | "vmName": { 134 | "value": "[variables('jumpVmName')]" 135 | }, 136 | "vmSize": { 137 | "value": "[parameters('jumpVmSize')]" 138 | }, 139 | "subnetId": { 140 | "value": "[variables('subnetId1')]" 141 | }, 142 | "adminUsername": { 143 | "value": "[parameters('adminUsername')]" 144 | }, 145 | "adminPassword": { 146 | "value": "[parameters('adminPassword')]" 147 | }, 148 | "assignPublicIP": { 149 | "value": true 150 | } 151 | } 152 | } 153 | }, 154 | { 155 | "name": "[concat('agent',resourceGroup().name)]", 156 | "type": "Microsoft.Resources/deployments", 157 | "apiVersion": "2017-05-10", 158 | "dependsOn": [ 159 | "[resourceId('Microsoft.Network/virtualNetworks',variables('vnetName'))]" 160 | ], 161 | "properties": { 162 | "mode": "Incremental", 163 | "templateLink": { 164 | "uri": "[variables('agentTemplateURL')]", 165 | "contentVersion": "1.0.0.0" 166 | }, 167 | "parameters": { 168 | "TSServerUrl": { 169 | "value": "[parameters('TSServerUrl')]" 170 | }, 171 | "AgentPool": { 172 | "value": "[parameters('AgentPool')]" 173 | }, 174 | "PAToken": { 175 | "value": "[parameters('PAToken')]" 176 | }, 177 | "AseIp": { 178 | "value": "[variables('AseIp')]" 179 | }, 180 | "AppDns": { 181 | "value": "[variables('AppDns')]" 182 | }, 183 | "vmName": { 184 | "value": "[variables('buildAgentVmName')]" 185 | }, 186 | "vmSize": { 187 | "value": "[parameters('agentVmSize')]" 188 | }, 189 | "subnetId": { 190 | "value": "[variables('subnetId1')]" 191 | }, 192 | "adminUsername": { 193 | "value": "[parameters('adminUsername')]" 194 | }, 195 | "adminPassword": { 196 | "value": "[parameters('adminPassword')]" 197 | } 198 | } 199 | } 200 | }, 201 | { 202 | "name": "[concat('ase',resourceGroup().name)]", 203 | "type": "Microsoft.Resources/deployments", 204 | "apiVersion": "2017-05-10", 205 | "dependsOn": [ 206 | "[resourceId('Microsoft.Network/virtualNetworks',variables('vnetName'))]" 207 | ], 208 | "properties": { 209 | "mode": "Incremental", 210 | "templateLink": { 211 | "uri": "[variables('aseTemplateURL')]", 212 | "contentVersion": "1.0.0.0" 213 | }, 214 | "parameters": { 215 | "subnetId": { 216 | "value": "[variables('subnetId2')]" 217 | }, 218 | "domainName": { 219 | "value": "[parameters('domainName')]" 220 | }, 221 | "aseName": { 222 | "value": "[parameters('aseName')]" 223 | }, 224 | "aseSiteName": { 225 | "value": "[parameters('siteName')]" 226 | }, 227 | "pfxBlobString": { 228 | "value": "[parameters('pfxBlobString')]" 229 | }, 230 | "certificatePassword": { 231 | "value": "[parameters('certificatePassword')]" 232 | }, 233 | "certificateThumbprint": { 234 | "value": "[parameters('certificateThumbprint')]" 235 | } 236 | } 237 | } 238 | } 239 | ] 240 | } -------------------------------------------------------------------------------- /ase/README.md: -------------------------------------------------------------------------------- 1 | App Service Environment with Website 2 | ------------------------------------ 3 | 4 | This template illustrates how to deploy [Azure App Service Environment](https://docs.microsoft.com/en-us/azure/app-service/environment/intro) a.k.a. ASEv2 with ILB configuration. 5 | 6 | The template allows you to supply an existing Subnet for ASE deployment (there should be no resources in this Subnet). If you do not supply a Subnet, i.e. leave that parameter as `null`, a new Virtual Network will be created and the ASE deployed in it. 7 | 8 | The ASE needs a certificate, which supports the `*.domainname`, and `*.scm.domainname` DNS names. If you do not have a certificate, you can generate one with the `New-SelfSignedCertificate` command. The repository also includes a [convenience script](https://github.com/hansenms/iac/blob/master/scripts/PrepareAseDeployment.ps1), that prepares the details needed for this deployment. To use the script for this deployment: 9 | 10 | ``` 11 | iac\scripts\PrepareAseDeployment.ps1 -DomainName mydomain-internal.us -OutFile C:\temp\myase.parameters.json 12 | ``` 13 | 14 | This will generate a parameter file that you can use to either copy/paste from or with a CLI or Powershell command to deploy the template. 15 | 16 | Note: It would be possible (and better) to deploy the certificate using Key Vault, but this workflow is not supported in all clouds (including sovereign clouds); for compatibility reasons, the certificate is passed as a string blob in this template. 17 | 18 | 19 | 20 | 21 | 22 | 23 | 25 | -------------------------------------------------------------------------------- /ase/azuredeploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "subnetId": { 6 | "type": "string", 7 | "defaultValue": "null" 8 | }, 9 | "domainName": { 10 | "type": "string", 11 | "defaultValue": "contoso-internal.us" 12 | }, 13 | "internalLoadBalancingMode": { 14 | "type": "int", 15 | "defaultValue": 3, 16 | "allowedValues": [ 17 | 0, 18 | 1, 19 | 2, 20 | 3 21 | ], 22 | "metadata": { 23 | "description": "0 = public VIP only, 1 = only ports 80/443 are mapped to ILB VIP, 2 = only FTP ports are mapped to ILB VIP, 3 = both ports 80/443 and FTP ports are mapped to an ILB VIP." 24 | } 25 | }, 26 | "pfxBlobString": { 27 | "type": "string" 28 | }, 29 | "certificatePassword": { 30 | "type": "securestring" 31 | }, 32 | "certificateThumbprint": { 33 | "type": "string" 34 | }, 35 | "aseName": { 36 | "type": "string", 37 | "defaultValue": "GENERATE" 38 | }, 39 | "aseSiteName": { 40 | "type": "string", 41 | "defaultValue": "ase-site" 42 | }, 43 | "pricingTier": { 44 | "type": "string", 45 | "allowedValues": [ 46 | "1", 47 | "2", 48 | "3" 49 | ], 50 | "defaultValue": "1", 51 | "metadata": { 52 | "description": "Defines pricing tier for workers: 1 = Isolated 1, 2 = Isolated 2, 3 = Isolated 3." 53 | } 54 | }, 55 | "capacity": { 56 | "type": "int", 57 | "defaultValue": 1, 58 | "metadata": { 59 | "description": "Defines the number of instances that will be allocated to the app service plan." 60 | } 61 | } 62 | }, 63 | "variables": { 64 | "baseUri": "[deployment().properties.templateLink.uri]", 65 | "ilbCertTemplateURL": "[uri(variables('baseUri'),'nested-templates/ase-ilb-certificate.json')]", 66 | "aseName": "[if(equals(parameters('aseName'),'GENERATE'),concat('ASE', uniqueString(resourceGroup().id)), parameters('aseName'))]", 67 | "genVnetName": "[concat('ase-vnet',uniqueString(resourceGroup().id))]", 68 | "genVnetid": "[resourceId('Microsoft.Network/virtualNetworks', variables('genVnetName'))]", 69 | "genVnetSubnetName1": "default", 70 | "genVnetSubnetName2": "ase-subnet", 71 | "genSubnetId1": "[concat(variables('genVnetid'), '/subnets/', variables('genVnetSubnetName1'))]", 72 | "genSubnetId2": "[concat(variables('genVnetid'), '/subnets/', variables('genVnetSubnetName2'))]", 73 | "subnetId": "[if(equals(parameters('subnetId'),'null'), variables('genSubnetId2'), parameters('subnetId'))]", 74 | "lastSlash": "[lastIndexOf(variables('subnetId'),'/')]", 75 | "subnetName": "[substring(variables('subnetId'),add(variables('lastSlash'),1))]", 76 | "vnetId": "[substring(variables('subnetId'),0,sub(variables('lastSlash'),8))]", 77 | "certName": "[concat('DefaultCertificateFor_', variables('aseName'), '_InternalLoadBalancingASE')]", 78 | "appServicePlanName": "[concat(variables('aseName'),'-asp')]", 79 | "locationLUT": { 80 | "usgovvirginia": "USGov Virginia", 81 | "usgoviowa": "USGov Iowa", 82 | "usdodeast": "USDoD East", 83 | "usdodcentral": "USDoD Central", 84 | "usgovtexas": "USGov Texas", 85 | "usgovarizona": "USGov Arizona", 86 | "eastasia": "East Asia", 87 | "southeastasia": "Southeast Asia", 88 | "centralus": "Central US", 89 | "eastus": "East US", 90 | "eastus2": "East US 2", 91 | "westus": "West US", 92 | "northcentralus": "North Central US", 93 | "southcentralus": "South Central US", 94 | "northeurope": "North Europe", 95 | "westeurope": "West Europe", 96 | "japanwest": "Japan West", 97 | "japaneast": "Japan East", 98 | "brazilsouth": "Brazil South", 99 | "australiaeast": "Australia East", 100 | "australiasoutheast": "Australia Southeast", 101 | "southindia": "South India", 102 | "centralindia": "Central India", 103 | "westindia": "West India", 104 | "canadacentral": "Canada Central", 105 | "canadaeast": "Canada East", 106 | "uksouth": "UK South", 107 | "ukwest": "UK West", 108 | "westcentralus": "West Central US", 109 | "westus2": "West US 2", 110 | "koreacentral": "Korea Central", 111 | "koreasouth": "Korea South" 112 | } 113 | }, 114 | "resources": [ 115 | { 116 | "name": "[variables('genVnetName')]", 117 | "type": "Microsoft.Network/virtualNetworks", 118 | "location": "[resourceGroup().location]", 119 | "apiVersion": "2016-03-30", 120 | "condition": "[equals(parameters('subnetId'),'null')]", 121 | "dependsOn": [], 122 | "properties": { 123 | "addressSpace": { 124 | "addressPrefixes": [ 125 | "10.0.0.0/16" 126 | ] 127 | }, 128 | "subnets": [ 129 | { 130 | "name": "[variables('genVnetSubnetName1')]", 131 | "properties": { 132 | "addressPrefix": "10.0.0.0/24" 133 | } 134 | }, 135 | { 136 | "name": "[variables('genVnetSubnetName2')]", 137 | "properties": { 138 | "addressPrefix": "10.0.1.0/25" 139 | } 140 | } 141 | ] 142 | } 143 | }, 144 | { 145 | "name": "[variables('aseName')]", 146 | "type": "Microsoft.Web/hostingEnvironments", 147 | "apiVersion": "2016-09-01", 148 | "dependsOn": [ 149 | "[variables('genVnetId')]" 150 | ], 151 | "kind": "ASEV2", 152 | "location": "[variables('locationLUT')[resourceGroup().location]]", 153 | "properties": { 154 | "name": "[variables('aseName')]", 155 | "location": "[variables('locationLUT')[resourceGroup().location]]", 156 | "virtualNetwork": { 157 | "id": "[variables('vnetId')]", 158 | "subnet": "[variables('subnetName')]" 159 | }, 160 | "ipSslAddressCount": 0, 161 | "internalLoadBalancingMode": "[parameters('internalLoadBalancingMode')]", 162 | "dnsSuffix": "[parameters('domainName')]" 163 | } 164 | }, 165 | { 166 | "name": "SetILBCert", 167 | "type": "Microsoft.Resources/deployments", 168 | "apiVersion": "2017-05-10", 169 | "dependsOn": [ 170 | "[concat('Microsoft.Web/hostingEnvironments/', variables('aseName'))]" 171 | ], 172 | "properties": { 173 | "mode": "Incremental", 174 | "templateLink": { 175 | "uri": "[variables('ilbCertTemplateURL')]", 176 | "contentVersion": "1.0.0.0" 177 | }, 178 | "parameters": { 179 | "appServiceEnvironmentName": { 180 | "value": "[variables('aseName')]" 181 | }, 182 | "existingAseLocation": { 183 | "value": "[variables('locationLUT')[resourceGroup().location]]" 184 | }, 185 | "pfxBlobString": { 186 | "value": "[parameters('pfxBlobString')]" 187 | }, 188 | "password": { 189 | "value": "[parameters('certificatePassword')]" 190 | }, 191 | "certificateThumbprint": { 192 | "value": "[parameters('certificateThumbprint')]" 193 | }, 194 | "certificateName": { 195 | "value": "[variables('certName')]" 196 | } 197 | } 198 | } 199 | }, 200 | { 201 | "apiVersion": "2016-09-01", 202 | "name": "[variables('appServicePlanName')]", 203 | "type": "Microsoft.Web/serverfarms", 204 | "location": "[variables('locationLUT')[resourceGroup().location]]", 205 | "dependsOn": [ 206 | "[resourceId('Microsoft.Web/hostingEnvironments', variables('aseName'))]", 207 | "[resourceId('Microsoft.Resources/deployments', 'SetILBCert')]" 208 | ], 209 | "properties": { 210 | "name": "[variables('appServicePlanName')]", 211 | "hostingEnvironmentProfile": { 212 | "id": "[resourceId('Microsoft.Web/hostingEnvironments', variables('aseName'))]" 213 | } 214 | }, 215 | "sku": { 216 | "name": "[concat('I',parameters('pricingTier'))]", 217 | "tier": "Isolated", 218 | "size": "[concat('I',parameters('pricingTier'))]", 219 | "family": "I", 220 | "capacity": "[parameters('capacity')]" 221 | } 222 | }, 223 | { 224 | "apiVersion": "2016-08-01", 225 | "name": "[parameters('aseSiteName')]", 226 | "type": "Microsoft.Web/sites", 227 | "location": "[variables('locationLUT')[resourceGroup().location]]", 228 | "dependsOn": [ 229 | "[concat('Microsoft.Web/serverFarms/', variables('appServicePlanName'))]" 230 | ], 231 | "properties": { 232 | "name": "[parameters('aseSiteName')]", 233 | "serverFarmId": "[resourceId('Microsoft.Web/serverFarms', variables('appServicePlanName'))]", 234 | "hostingEnvironmentProfile": { 235 | "id": "[resourceId('Microsoft.Web/hostingEnvironments', variables('aseName'))]" 236 | } 237 | } 238 | } 239 | ] 240 | } -------------------------------------------------------------------------------- /ase/nested-templates/ase-ilb-certificate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "appServiceEnvironmentName": { 6 | "type": "string", 7 | "metadata": { 8 | "description": "The default SSL certificate will be configured for this App Service Environment." 9 | } 10 | }, 11 | "existingAseLocation": { 12 | "type": "string", 13 | "metadata": { 14 | "description": "Set this to the same location as the App Service Environment defined in appServiceEnvironmentName." 15 | } 16 | }, 17 | "pfxBlobString": { 18 | "type": "string", 19 | "metadata": { 20 | "description": "A pfx file encoded as a base-64 string. The pfx contains the SSL certificate that will be configured as the default SSL certificate for the ASE." 21 | } 22 | }, 23 | "password": { 24 | "type": "string", 25 | "metadata": { 26 | "description": "The password for the pfx filed contained in pfxBlobString." 27 | } 28 | }, 29 | "certificateThumbprint": { 30 | "type": "string", 31 | "metadata": { 32 | "description": "The hexadecimal certificate thumbprint of the certificate contained in pfxBlobString. All spaces need to be removed from the hex string." 33 | } 34 | }, 35 | "certificateName": { 36 | "type": "string", 37 | "metadata": { 38 | "description": "Name of the certificate. This can be any name you want to use to identify the certificate." 39 | } 40 | } 41 | }, 42 | "resources": [ 43 | { 44 | "apiVersion": "2015-08-01", 45 | "type": "Microsoft.Web/certificates", 46 | "name": "[parameters('certificateName')]", 47 | "location": "[parameters('existingAseLocation')]", 48 | "properties": { 49 | "pfxBlob": "[parameters('pfxBlobString')]", 50 | "password": "[parameters('password')]", 51 | "hostingEnvironmentProfile": { 52 | "id": "[resourceId('Microsoft.Web/hostingEnvironments',parameters('appServiceEnvironmentName'))]" 53 | } 54 | } 55 | }, 56 | { 57 | "apiVersion": "2016-09-01", 58 | "type": "Microsoft.Web/hostingEnvironments", 59 | "name": "[parameters('appServiceEnvironmentName')]", 60 | "location": "[parameters('existingAseLocation')]", 61 | "properties": { 62 | "clusterSettings": [ 63 | { 64 | "name": "DefaultSslCertificateThumbprint", 65 | "value": "[parameters('certificateThumbprint')]" 66 | }, 67 | { 68 | "name": "DisableTls1.0", 69 | "value": "1" 70 | } 71 | ] 72 | }, 73 | "dependsOn": [ 74 | "[concat('Microsoft.Web/certificates/',parameters('certificateName'))]" 75 | ] 76 | } 77 | ] 78 | } -------------------------------------------------------------------------------- /core-network/DSC/ConfigureADBDC.ps1: -------------------------------------------------------------------------------- 1 | configuration ConfigureADBDC 2 | { 3 | 4 | param 5 | ( 6 | [Parameter(Mandatory)] 7 | [String]$DomainName, 8 | 9 | [Parameter(Mandatory)] 10 | [System.Management.Automation.PSCredential]$Admincreds, 11 | 12 | [Int]$RetryCount=20, 13 | [Int]$RetryIntervalSec=30 14 | ) 15 | 16 | Import-DscResource -ModuleName xActiveDirectory, xStorage, xPendingReboot 17 | 18 | [System.Management.Automation.PSCredential ]$DomainCreds = New-Object System.Management.Automation.PSCredential ("${DomainName}\$($Admincreds.UserName)", $Admincreds.Password) 19 | 20 | Node localhost 21 | { 22 | LocalConfigurationManager 23 | { 24 | RebootNodeIfNeeded = $true 25 | } 26 | 27 | xWaitforDisk Disk2 28 | { 29 | DiskId = 2 30 | RetryIntervalSec =$RetryIntervalSec 31 | RetryCount = $RetryCount 32 | } 33 | 34 | xDisk ADDataDisk 35 | { 36 | DiskId = 2 37 | DriveLetter = "F" 38 | DependsOn = "[xWaitForDisk]Disk2" 39 | } 40 | 41 | WindowsFeature ADDSInstall 42 | { 43 | Ensure = "Present" 44 | Name = "AD-Domain-Services" 45 | } 46 | 47 | WindowsFeature ADDSTools 48 | { 49 | Ensure = "Present" 50 | Name = "RSAT-ADDS-Tools" 51 | DependsOn = "[WindowsFeature]ADDSInstall" 52 | } 53 | 54 | WindowsFeature ADAdminCenter 55 | { 56 | Ensure = "Present" 57 | Name = "RSAT-AD-AdminCenter" 58 | DependsOn = "[WindowsFeature]ADDSTools" 59 | } 60 | 61 | xWaitForADDomain DscForestWait 62 | { 63 | DomainName = $DomainName 64 | DomainUserCredential= $DomainCreds 65 | RetryCount = $RetryCount 66 | RetryIntervalSec = $RetryIntervalSec 67 | } 68 | xADDomainController BDC 69 | { 70 | DomainName = $DomainName 71 | DomainAdministratorCredential = $DomainCreds 72 | SafemodeAdministratorPassword = $DomainCreds 73 | DatabasePath = "F:\NTDS" 74 | LogPath = "F:\NTDS" 75 | SysvolPath = "F:\SYSVOL" 76 | DependsOn = "[xWaitForADDomain]DscForestWait" 77 | } 78 | 79 | xPendingReboot RebootAfterPromotion { 80 | Name = "RebootAfterDCPromotion" 81 | DependsOn = "[xADDomainController]BDC" 82 | } 83 | 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /core-network/DSC/ConfigureADBDC.ps1.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hansenms/iac/277b41b9f9a0ce395f577d8a74640000c91c5ce1/core-network/DSC/ConfigureADBDC.ps1.zip -------------------------------------------------------------------------------- /core-network/DSC/CreateADPDC.ps1: -------------------------------------------------------------------------------- 1 | configuration CreateADPDC 2 | { 3 | param 4 | ( 5 | [Parameter(Mandatory)] 6 | [String]$DomainName, 7 | 8 | [Parameter(Mandatory)] 9 | [System.Management.Automation.PSCredential]$Admincreds, 10 | 11 | [Int]$RetryCount=20, 12 | [Int]$RetryIntervalSec=30 13 | ) 14 | 15 | Import-DscResource -ModuleName xActiveDirectory, xStorage, xNetworking, PSDesiredStateConfiguration, xPendingReboot 16 | [System.Management.Automation.PSCredential ]$DomainCreds = New-Object System.Management.Automation.PSCredential ("${DomainName}\$($Admincreds.UserName)", $Admincreds.Password) 17 | $Interface=Get-NetAdapter|Where Name -Like "Ethernet*"|Select-Object -First 1 18 | $InterfaceAlias=$($Interface.Name) 19 | 20 | Node localhost 21 | { 22 | LocalConfigurationManager 23 | { 24 | RebootNodeIfNeeded = $true 25 | } 26 | 27 | WindowsFeature DNS 28 | { 29 | Ensure = "Present" 30 | Name = "DNS" 31 | } 32 | 33 | Script EnableDNSDiags 34 | { 35 | SetScript = { 36 | Set-DnsServerDiagnostics -All $true 37 | Write-Verbose -Verbose "Enabling DNS client diagnostics" 38 | } 39 | GetScript = { @{} } 40 | TestScript = { $false } 41 | DependsOn = "[WindowsFeature]DNS" 42 | } 43 | 44 | WindowsFeature DnsTools 45 | { 46 | Ensure = "Present" 47 | Name = "RSAT-DNS-Server" 48 | DependsOn = "[WindowsFeature]DNS" 49 | } 50 | 51 | xDnsServerAddress DnsServerAddress 52 | { 53 | Address = '127.0.0.1' 54 | InterfaceAlias = $InterfaceAlias 55 | AddressFamily = 'IPv4' 56 | DependsOn = "[WindowsFeature]DNS" 57 | } 58 | 59 | xWaitforDisk Disk2 60 | { 61 | DiskId = 2 62 | RetryIntervalSec =$RetryIntervalSec 63 | RetryCount = $RetryCount 64 | } 65 | 66 | xDisk ADDataDisk { 67 | DiskId = 2 68 | DriveLetter = "F" 69 | DependsOn = "[xWaitForDisk]Disk2" 70 | } 71 | 72 | WindowsFeature ADDSInstall 73 | { 74 | Ensure = "Present" 75 | Name = "AD-Domain-Services" 76 | DependsOn="[WindowsFeature]DNS" 77 | } 78 | 79 | WindowsFeature ADDSTools 80 | { 81 | Ensure = "Present" 82 | Name = "RSAT-ADDS-Tools" 83 | DependsOn = "[WindowsFeature]ADDSInstall" 84 | } 85 | 86 | WindowsFeature ADAdminCenter 87 | { 88 | Ensure = "Present" 89 | Name = "RSAT-AD-AdminCenter" 90 | DependsOn = "[WindowsFeature]ADDSTools" 91 | } 92 | 93 | xADDomain FirstDS 94 | { 95 | DomainName = $DomainName 96 | DomainAdministratorCredential = $DomainCreds 97 | SafemodeAdministratorPassword = $DomainCreds 98 | DatabasePath = "F:\NTDS" 99 | LogPath = "F:\NTDS" 100 | SysvolPath = "F:\SYSVOL" 101 | DependsOn = @("[WindowsFeature]ADDSInstall", "[xDisk]ADDataDisk") 102 | } 103 | 104 | xPendingReboot RebootAfterPromotion{ 105 | Name = "RebootAfterPromotion" 106 | DependsOn = "[xADDomain]FirstDS" 107 | } 108 | 109 | } 110 | } -------------------------------------------------------------------------------- /core-network/DSC/CreateADPDC.ps1.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hansenms/iac/277b41b9f9a0ce395f577d8a74640000c91c5ce1/core-network/DSC/CreateADPDC.ps1.zip -------------------------------------------------------------------------------- /core-network/README.md: -------------------------------------------------------------------------------- 1 | Core Virtual Network with Domain Controllers 2 | --------------------------------------------- 3 | 4 | This template deploys a virtual network with two private domain controllers in an availbility set and a jump box for accessing the network: 5 | 6 | ![core-network](core-network.png) 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 16 | -------------------------------------------------------------------------------- /core-network/azuredeploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "adminUsername": { 6 | "type": "string", 7 | "defaultValue": "EnterpriseAdmin" 8 | }, 9 | "adminPassword": { 10 | "type": "securestring" 11 | }, 12 | "domainName": { 13 | "type": "string", 14 | "defaultValue": "devnet.contoso.us" 15 | }, 16 | "jumpBoxVmSize": { 17 | "type": "string", 18 | "allowedValues": [ 19 | "Standard_D1_v2", 20 | "Standard_D2_v2", 21 | "Standard_D3_v2", 22 | "Standard_D4_v2", 23 | "Standard_D5_v2", 24 | "Standard_D11_v2", 25 | "Standard_D12_v2", 26 | "Standard_D13_v2", 27 | "Standard_D14_v2", 28 | "Standard_D15_v2", 29 | "Standard_DS1_v2", 30 | "Standard_DS2_v2", 31 | "Standard_DS3_v2", 32 | "Standard_DS4_v2", 33 | "Standard_DS5_v2", 34 | "Standard_DS11_v2", 35 | "Standard_DS12_v2", 36 | "Standard_DS13_v2", 37 | "Standard_DS14_v2", 38 | "Standard_DS15_v2" 39 | ], 40 | "defaultValue": "Standard_D2_v2" 41 | }, 42 | "DCVmSize": { 43 | "type": "string", 44 | "allowedValues": [ 45 | "Standard_D1_v2", 46 | "Standard_D2_v2", 47 | "Standard_D3_v2", 48 | "Standard_D4_v2", 49 | "Standard_D5_v2", 50 | "Standard_D11_v2", 51 | "Standard_D12_v2", 52 | "Standard_D13_v2", 53 | "Standard_D14_v2", 54 | "Standard_D15_v2", 55 | "Standard_DS1_v2", 56 | "Standard_DS2_v2", 57 | "Standard_DS3_v2", 58 | "Standard_DS4_v2", 59 | "Standard_DS5_v2", 60 | "Standard_DS11_v2", 61 | "Standard_DS12_v2", 62 | "Standard_DS13_v2", 63 | "Standard_DS14_v2", 64 | "Standard_DS15_v2" 65 | ], 66 | "defaultValue": "Standard_D1_v2" 67 | } 68 | }, 69 | "variables": { 70 | "diagStorageAccountName": "[concat(uniquestring(resourceGroup().id), 'vmdiag')]", 71 | "coreVnetName": "corevnet", 72 | "corevnetPrefix": "10.0.0.0/16", 73 | "corevnetDCSubnetName": "Subnet-1", 74 | "corevnetDCSubnetPrefix": "10.0.0.0/24", 75 | "corevnetDevOpsSubnetName": "Subnet-2", 76 | "corevnetDevOpsSubnetPrefix": "10.0.1.0/24", 77 | "jumpNICVnetID": "[resourceId('Microsoft.Network/virtualNetworks', variables('coreVnetName'))]", 78 | "jumpNICSubnetRef": "[concat(variables('jumpNICVnetID'), '/subnets/', variables('corevnetDevOpsSubnetName'))]", 79 | "ADSubnetRef": "[concat(variables('jumpNICVnetID'), '/subnets/', variables('corevnetDCSubnetName'))]", 80 | "jumpVMName": "JumpBox", 81 | "adPDCVMName": "DC1", 82 | "adBDCVMName": "DC2", 83 | "DCHASetName": "DCHASet", 84 | "baseUri": "[deployment().properties.templateLink.uri]", 85 | "windowsVmTemplateURL": "[uri(variables('baseUri'),'../primitives/windowsvm.json')]", 86 | "adPDCModulesURL": "[uri(variables('baseUri'),'DSC/CreateADPDC.ps1.zip')]", 87 | "adPDCConfigurationFunction": "CreateADPDC.ps1\\CreateADPDC", 88 | "adBDCModulesURL": "[uri(variables('baseUri'),'DSC/ConfigureADBDC.ps1.zip')]", 89 | "adBDCConfigurationFunction": "ConfigureADBDC.ps1\\ConfigureADBDC", 90 | "vnetwithDNSTemplateUri": "[uri(variables('baseUri'),'nested-templates/update-vnet-dns.json')]" 91 | }, 92 | "resources": [ 93 | { 94 | "type": "Microsoft.Storage/storageAccounts", 95 | "name": "[variables('diagStorageAccountName')]", 96 | "apiVersion": "2016-01-01", 97 | "location": "[resourceGroup().location]", 98 | "sku": { 99 | "name": "Standard_LRS" 100 | }, 101 | "kind": "Storage", 102 | "properties": {} 103 | }, 104 | { 105 | "name": "[variables('coreVnetName')]", 106 | "type": "Microsoft.Network/virtualNetworks", 107 | "location": "[resourceGroup().location]", 108 | "apiVersion": "2016-03-30", 109 | "dependsOn": [], 110 | "properties": { 111 | "addressSpace": { 112 | "addressPrefixes": [ 113 | "[variables('corevnetPrefix')]" 114 | ] 115 | }, 116 | "subnets": [ 117 | { 118 | "name": "[variables('corevnetDCSubnetName')]", 119 | "properties": { 120 | "addressPrefix": "[variables('corevnetDCSubnetPrefix')]" 121 | } 122 | }, 123 | { 124 | "name": "[variables('corevnetDevOpsSubnetName')]", 125 | "properties": { 126 | "addressPrefix": "[variables('corevnetDevOpsSubnetPrefix')]" 127 | } 128 | } 129 | ] 130 | } 131 | }, 132 | { 133 | "name": "[variables('DCHASetName')]", 134 | "type": "Microsoft.Compute/availabilitySets", 135 | "location": "[resourceGroup().location]", 136 | "apiVersion": "2017-03-30", 137 | "dependsOn": [], 138 | "tags": { 139 | "displayName": "DCHASet" 140 | }, 141 | "properties": { 142 | "platformUpdateDomainCount": 2, 143 | "platformFaultDomainCount": 2 144 | }, 145 | "sku": { 146 | "name": "Aligned" 147 | } 148 | }, 149 | { 150 | "name": "DC1Deploy", 151 | "type": "Microsoft.Resources/deployments", 152 | "apiVersion": "2017-05-10", 153 | "dependsOn": [ 154 | "[resourceId('Microsoft.Network/virtualNetworks', variables('coreVnetName'))]", 155 | "[resourceId('Microsoft.Storage/storageAccounts', variables('diagStorageAccountName'))]" 156 | ], 157 | "properties": { 158 | "mode": "Incremental", 159 | "templateLink": { 160 | "uri": "[variables('windowsVmTemplateURL')]", 161 | "contentVersion": "1.0.0.0" 162 | }, 163 | "parameters": { 164 | "vmName": { 165 | "value": "[variables('adPDCVMName')]" 166 | }, 167 | "vmSize": { 168 | "value": "[parameters('DCVmSize')]" 169 | }, 170 | "subnetId": { 171 | "value": "[variables('ADSubnetRef')]" 172 | }, 173 | "fixedPrivateIp": { 174 | "value": "10.0.0.4" 175 | }, 176 | "adminUsername": { 177 | "value": "[parameters('adminUsername')]" 178 | }, 179 | "adminPassword": { 180 | "value": "[parameters('adminPassword')]" 181 | }, 182 | "diagStorageAccountId": { 183 | "value": "[resourceId('Microsoft.Storage/storageAccounts', variables('diagStorageAccountName'))]" 184 | }, 185 | "availabilitySetId": { 186 | "value": "[resourceId('Microsoft.Compute/availabilitySets', variables('DCHASetName'))]" 187 | } 188 | } 189 | } 190 | }, 191 | { 192 | "name": "DC2Deploy", 193 | "type": "Microsoft.Resources/deployments", 194 | "apiVersion": "2017-05-10", 195 | "dependsOn": [ 196 | "[resourceId('Microsoft.Network/virtualNetworks', variables('coreVnetName'))]", 197 | "[resourceId('Microsoft.Storage/storageAccounts', variables('diagStorageAccountName'))]", 198 | "[resourceId('Microsoft.Resources/deployments', 'UpdateVNetDNS')]" 199 | ], 200 | "properties": { 201 | "mode": "Incremental", 202 | "templateLink": { 203 | "uri": "[variables('windowsVmTemplateURL')]", 204 | "contentVersion": "1.0.0.0" 205 | }, 206 | "parameters": { 207 | "vmName": { 208 | "value": "[variables('adBDCVMName')]" 209 | }, 210 | "vmSize": { 211 | "value": "[parameters('DCVmSize')]" 212 | }, 213 | "subnetId": { 214 | "value": "[variables('ADSubnetRef')]" 215 | }, 216 | "fixedPrivateIp": { 217 | "value": "10.0.0.5" 218 | }, 219 | "adminUsername": { 220 | "value": "[parameters('adminUsername')]" 221 | }, 222 | "adminPassword": { 223 | "value": "[parameters('adminPassword')]" 224 | }, 225 | "diagStorageAccountId": { 226 | "value": "[resourceId('Microsoft.Storage/storageAccounts', variables('diagStorageAccountName'))]" 227 | }, 228 | "availabilitySetId": { 229 | "value": "[resourceId('Microsoft.Compute/availabilitySets', variables('DCHASetName'))]" 230 | } 231 | } 232 | } 233 | }, 234 | { 235 | "name": "JumpDeploy", 236 | "type": "Microsoft.Resources/deployments", 237 | "apiVersion": "2017-05-10", 238 | "dependsOn": [ 239 | "[resourceId('Microsoft.Network/virtualNetworks', variables('coreVnetName'))]", 240 | "[resourceId('Microsoft.Storage/storageAccounts', variables('diagStorageAccountName'))]", 241 | "[resourceId('Microsoft.Resources/deployments', 'UpdateVNetDNS')]" 242 | ], 243 | "properties": { 244 | "mode": "Incremental", 245 | "templateLink": { 246 | "uri": "[variables('windowsVmTemplateURL')]", 247 | "contentVersion": "1.0.0.0" 248 | }, 249 | "parameters": { 250 | "vmName": { 251 | "value": "[variables('jumpVMName')]" 252 | }, 253 | "vmSize": { 254 | "value": "[parameters('jumpBoxVmSize')]" 255 | }, 256 | "subnetId": { 257 | "value": "[variables('jumpNICSubnetRef')]" 258 | }, 259 | "adminUsername": { 260 | "value": "[parameters('adminUsername')]" 261 | }, 262 | "adminPassword": { 263 | "value": "[parameters('adminPassword')]" 264 | }, 265 | "diagStorageAccountId": { 266 | "value": "[resourceId('Microsoft.Storage/storageAccounts', variables('diagStorageAccountName'))]" 267 | }, 268 | "assignPublicIP": { 269 | "value": true 270 | } 271 | } 272 | } 273 | }, 274 | { 275 | "apiVersion": "2016-03-30", 276 | "type": "Microsoft.Compute/virtualMachines/extensions", 277 | "name": "[concat(variables('adPDCVMName'),'/CreateADForest')]", 278 | "location": "[resourceGroup().location]", 279 | "dependsOn": [ 280 | "[resourceId('Microsoft.Resources/deployments', 'DC1Deploy')]" 281 | ], 282 | "properties": { 283 | "publisher": "Microsoft.Powershell", 284 | "type": "DSC", 285 | "typeHandlerVersion": "2.19", 286 | "autoUpgradeMinorVersion": true, 287 | "settings": { 288 | "ModulesUrl": "[variables('adPDCModulesURL')]", 289 | "ConfigurationFunction": "[variables('adPDCConfigurationFunction')]", 290 | "Properties": { 291 | "DomainName": "[parameters('domainName')]", 292 | "AdminCreds": { 293 | "UserName": "[parameters('adminUserName')]", 294 | "Password": "PrivateSettingsRef:AdminPassword" 295 | } 296 | } 297 | }, 298 | "protectedSettings": { 299 | "Items": { 300 | "AdminPassword": "[parameters('adminPassword')]" 301 | } 302 | } 303 | } 304 | }, 305 | { 306 | "apiVersion": "2016-03-30", 307 | "type": "Microsoft.Compute/virtualMachines/extensions", 308 | "name": "[concat(variables('adBDCVMName'),'/ConfigureADBDC')]", 309 | "location": "[resourceGroup().location]", 310 | "dependsOn": [ 311 | "[resourceId('Microsoft.Resources/deployments', 'DC2Deploy')]", 312 | "[resourceId('Microsoft.Resources/deployments', 'UpdateVNetDNS')]" 313 | ], 314 | "properties": { 315 | "publisher": "Microsoft.Powershell", 316 | "type": "DSC", 317 | "typeHandlerVersion": "2.19", 318 | "autoUpgradeMinorVersion": true, 319 | "settings": { 320 | "ModulesUrl": "[variables('adBDCModulesURL')]", 321 | "ConfigurationFunction": "[variables('adBDCConfigurationFunction')]", 322 | "Properties": { 323 | "DomainName": "[parameters('domainName')]", 324 | "AdminCreds": { 325 | "UserName": "[parameters('adminUserName')]", 326 | "Password": "PrivateSettingsRef:AdminPassword" 327 | } 328 | } 329 | }, 330 | "protectedSettings": { 331 | "Items": { 332 | "AdminPassword": "[parameters('adminPassword')]" 333 | } 334 | } 335 | } 336 | }, 337 | { 338 | "apiVersion": "2015-06-15", 339 | "type": "Microsoft.Compute/virtualMachines/extensions", 340 | "name": "[concat(variables('jumpVMName'),'/joindomain')]", 341 | "location": "[resourceGroup().location]", 342 | "dependsOn": [ 343 | "[resourceId('Microsoft.Resources/deployments', 'JumpDeploy')]" 344 | ], 345 | "properties": { 346 | "publisher": "Microsoft.Compute", 347 | "type": "JsonADDomainExtension", 348 | "typeHandlerVersion": "1.3", 349 | "autoUpgradeMinorVersion": true, 350 | "settings": { 351 | "Name": "[parameters('domainName')]", 352 | "OUPath": "", 353 | "User": "[concat(parameters('domainName'), '\\', parameters('adminUserName'))]", 354 | "Restart": "true", 355 | "Options": "3" 356 | }, 357 | "protectedSettings": { 358 | "Password": "[parameters('adminPassword')]" 359 | } 360 | } 361 | }, 362 | { 363 | "name": "UpdateVNetDNS", 364 | "type": "Microsoft.Resources/deployments", 365 | "apiVersion": "2017-05-10", 366 | "dependsOn": [ 367 | "[concat('Microsoft.Compute/virtualMachines/', variables('adPDCVMName'),'/extensions/CreateADForest')]" 368 | ], 369 | "properties": { 370 | "mode": "Incremental", 371 | "templateLink": { 372 | "uri": "[variables('vnetwithDNSTemplateUri')]", 373 | "contentVersion": "1.0.0.0" 374 | }, 375 | "parameters": { 376 | "vnetName": { 377 | "value": "[variables('coreVnetName')]" 378 | }, 379 | "dnsServers": { 380 | "value": [ 381 | "10.0.0.4", 382 | "10.0.0.5" 383 | ] 384 | }, 385 | "addressSpace": { 386 | "value": "[reference(concat('Microsoft.Network/virtualNetworks/', variables('coreVnetName'))).addressSpace]" 387 | }, 388 | "subnets": { 389 | "value": "[reference(concat('Microsoft.Network/virtualNetworks/', variables('coreVnetName'))).subnets]" 390 | } 391 | } 392 | } 393 | } 394 | ], 395 | "outputs": {} 396 | } 397 | -------------------------------------------------------------------------------- /core-network/core-network.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hansenms/iac/277b41b9f9a0ce395f577d8a74640000c91c5ce1/core-network/core-network.png -------------------------------------------------------------------------------- /core-network/nested-templates/update-vnet-dns.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "vnetName": { 6 | "type": "string" 7 | }, 8 | "dnsServers": { 9 | "type": "array" 10 | }, 11 | "addressSpace": { 12 | "type": "object" 13 | }, 14 | "subnets": { 15 | "type": "array" 16 | } 17 | }, 18 | "variables": { 19 | }, 20 | "resources": [ 21 | { 22 | "name": "[parameters('vnetName')]", 23 | "type": "Microsoft.Network/virtualNetworks", 24 | "location": "[resourceGroup().location]", 25 | "apiVersion": "2016-03-30", 26 | "properties": { 27 | "addressSpace": "[parameters('addressSpace')]", 28 | "subnets": "[parameters('subnets')]", 29 | "dhcpOptions": { 30 | "dnsServers": "[parameters('dnsServers')]" 31 | } 32 | } 33 | } 34 | ], 35 | "outputs": { 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /devnet-tfs-ha/README.md: -------------------------------------------------------------------------------- 1 | High Availability Team Foundation Server in Azure 2 | -------------------------------------------------- 3 | 4 | This solution combined several other templates to deploy a high-availability [Team Foundation Server](https://www.visualstudio.com/tfs/) (TFS) in Azure. This solution is relevant for organizations where the SaaS [Vistual Studio Team Services](https://www.visualstudio.com/team-services/) (VSTS) is not an option. The deployed solution is outlined in the diagram below. 5 | 6 | ![private-ha-devops](private_ha_devops.png) 7 | 8 | The components included are: 9 | 10 | * [core-network](../core-network) which establishes a Virtual Network and sets up Domain Controllers. 11 | * [sql-alwayson](../sql-alwayson) which sets up SQL server in Always On configuration. 12 | * [tfs-ha](../tfs-ha) which sets up a high-availability deployment of TFS. 13 | 14 | In addition to this, some build agents are deployed. 15 | 16 | Each of the components can be deployed on their own and for users that already have domain controllers established, they can simply specify a subnet and domain credentials to deploy SQL and TFS into those environments. This combined template is mostly provided as a way of testing the complete deployment. 17 | 18 | Since TFS establishes a web endpoint on the TFS servers, it is recommended that you secure that with SSL certificates. If you do not provide a certificate, a self-signed one will be generated. You can replace it after installation too. The way to provide it during deployment is through the `secrets` and `sslThumbprint` parameters. The `secrets` parameter is used to provide an array of certificates from [Azure Key Vault](https://azure.microsoft.com/en-us/services/key-vault/). The `sslThumbprint` parameter instructs the TFS installation script on which certificate to install. The repository includes a [convenience script](../scripts/PrepareDevnetTfsDeployment.ps1), which you can use to create a key vault to hold domain admin password and the SSL certificate. This script will create the key vault, upload all secrets, and generate a deployment parameters file with the details needed for deployment. 19 | 20 | Please consult the [tfs-ha](../tfs-ha) folder for more details including post-installation instructions, which include adding the established SQL databases to an availability group. 21 | 22 | Deploy 23 | ------ 24 | 25 | Deploy this solution to either Azure Commercial or Azure Government: 26 | 27 | 28 | 29 | 30 | 31 | 32 | 34 | -------------------------------------------------------------------------------- /devnet-tfs-ha/azuredeploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "adminUsername": { 6 | "type": "string", 7 | "defaultValue": "EnterpriseAdmin" 8 | }, 9 | "adminPassword": { 10 | "type": "securestring" 11 | }, 12 | "domainName": { 13 | "type": "string", 14 | "defaultValue": "devnet.contoso.us" 15 | }, 16 | "secrets": { 17 | "type": "array", 18 | "defaultValue": [] 19 | }, 20 | "sslThumbprint": { 21 | "type": "string", 22 | "defaultValue": "generate" 23 | }, 24 | "jumpBoxVmSize": { 25 | "type": "string", 26 | "allowedValues": [ 27 | "Standard_DS1_v2", 28 | "Standard_DS2_v2", 29 | "Standard_DS3_v2", 30 | "Standard_DS4_v2", 31 | "Standard_DS5_v2", 32 | "Standard_DS11_v2", 33 | "Standard_DS12_v2", 34 | "Standard_DS13_v2", 35 | "Standard_DS14_v2", 36 | "Standard_DS15_v2" 37 | ], 38 | "defaultValue": "Standard_DS2_v2" 39 | }, 40 | "DCVmSize": { 41 | "type": "string", 42 | "allowedValues": [ 43 | "Standard_DS1_v2", 44 | "Standard_DS2_v2", 45 | "Standard_DS3_v2", 46 | "Standard_DS4_v2", 47 | "Standard_DS5_v2", 48 | "Standard_DS11_v2", 49 | "Standard_DS12_v2", 50 | "Standard_DS13_v2", 51 | "Standard_DS14_v2", 52 | "Standard_DS15_v2" 53 | ], 54 | "defaultValue": "Standard_DS1_v2" 55 | }, 56 | "tfsVmSize": { 57 | "type": "string", 58 | "allowedValues": [ 59 | "Standard_DS1_v2", 60 | "Standard_DS2_v2", 61 | "Standard_DS3_v2", 62 | "Standard_DS4_v2", 63 | "Standard_DS5_v2", 64 | "Standard_DS11_v2", 65 | "Standard_DS12_v2", 66 | "Standard_DS13_v2", 67 | "Standard_DS14_v2", 68 | "Standard_DS15_v2" 69 | ], 70 | "defaultValue": "Standard_DS2_v2" 71 | }, 72 | "sqlVmSize": { 73 | "type": "string", 74 | "allowedValues": [ 75 | "Standard_DS1_v2", 76 | "Standard_DS2_v2", 77 | "Standard_DS3_v2", 78 | "Standard_DS4_v2", 79 | "Standard_DS5_v2", 80 | "Standard_DS11_v2", 81 | "Standard_DS12_v2", 82 | "Standard_DS13_v2", 83 | "Standard_DS14_v2", 84 | "Standard_DS15_v2" 85 | ], 86 | "defaultValue": "Standard_DS4_v2" 87 | }, 88 | "tfsAgentVmSize": { 89 | "type": "string", 90 | "allowedValues": [ 91 | "Standard_DS1_v2", 92 | "Standard_DS2_v2", 93 | "Standard_DS3_v2", 94 | "Standard_DS4_v2", 95 | "Standard_DS5_v2", 96 | "Standard_DS11_v2", 97 | "Standard_DS12_v2", 98 | "Standard_DS13_v2", 99 | "Standard_DS14_v2", 100 | "Standard_DS15_v2" 101 | ], 102 | "defaultValue": "Standard_DS2_v2" 103 | } 104 | }, 105 | "variables": { 106 | "baseUri": "[deployment().properties.templateLink.uri]", 107 | "coreNetworkTemplateUri": "[uri(variables('baseUri'), '../core-network/azuredeploy.json')]", 108 | "sqlTemplateUri": "[uri(variables('baseUri'), '../sql-alwayson/azuredeploy.json')]", 109 | "tfsTemplateUri": "[uri(variables('baseUri'), '../tfs-ha/azuredeploy.json')]", 110 | "agentTemplateUri": "[uri(variables('baseUri'), '../tfs-ha/deployagents.json')]", 111 | "coreVnetName": "corevnet", 112 | "corevnetDevOpsSubnetName": "Subnet-2", 113 | "vnetid": "[resourceId('Microsoft.Network/virtualNetworks', variables('coreVnetName'))]", 114 | "devopsdubnetid": "[concat(variables('vnetid'), '/subnets/', variables('corevnetDevOpsSubnetName'))]", 115 | "tfsServerProtocol": "[if(equals(parameters('sslThumbprint'),'generate'),'http://','https://')]", 116 | "tfsServerUrl": "[concat(variables('tfsServerProtocol'),'tfs.',parameters('domainName'))]" 117 | }, 118 | "resources": [ 119 | { 120 | "name": "core-network", 121 | "type": "Microsoft.Resources/deployments", 122 | "apiVersion": "2017-05-10", 123 | "properties": { 124 | "mode": "Incremental", 125 | "templateLink": { 126 | "uri": "[variables('coreNetworkTemplateUri')]", 127 | "contentVersion": "1.0.0.0" 128 | }, 129 | "parameters": { 130 | "adminUsername": { 131 | "value": "[parameters('adminUsername')]" 132 | }, 133 | "adminPassword": { 134 | "value": "[parameters('adminPassword')]" 135 | }, 136 | "domainName": { 137 | "value": "[parameters('domainName')]" 138 | }, 139 | "jumpBoxVmSize": { 140 | "value": "[parameters('jumpBoxVmSize')]" 141 | }, 142 | "DCVmSize": { 143 | "value": "[parameters('DCVmSize')]" 144 | } 145 | } 146 | } 147 | }, 148 | { 149 | "name": "sqlalwayson", 150 | "type": "Microsoft.Resources/deployments", 151 | "apiVersion": "2017-05-10", 152 | "dependsOn": [ 153 | "Microsoft.Resources/deployments/core-network" 154 | ], 155 | "properties": { 156 | "mode": "Incremental", 157 | "templateLink": { 158 | "uri": "[variables('sqlTemplateUri')]", 159 | "contentVersion": "1.0.0.0" 160 | }, 161 | "parameters": { 162 | "subnetId": { 163 | "value": "[variables('devopsdubnetid')]" 164 | }, 165 | "adminUsername": { 166 | "value": "[parameters('adminUsername')]" 167 | }, 168 | "adminPassword": { 169 | "value": "[parameters('adminPassword')]" 170 | }, 171 | "domainName": { 172 | "value": "[parameters('domainName')]" 173 | }, 174 | "sqlVmSize": { 175 | "value": "[parameters('sqlVmSize')]" 176 | } 177 | } 178 | } 179 | }, 180 | { 181 | "name": "tfs", 182 | "type": "Microsoft.Resources/deployments", 183 | "apiVersion": "2017-05-10", 184 | "dependsOn": [ 185 | "Microsoft.Resources/deployments/sqlalwayson" 186 | ], 187 | "properties": { 188 | "mode": "Incremental", 189 | "templateLink": { 190 | "uri": "[variables('tfsTemplateUri')]", 191 | "contentVersion": "1.0.0.0" 192 | }, 193 | "parameters": { 194 | "subnetId": { 195 | "value": "[variables('devopsdubnetid')]" 196 | }, 197 | "adminUsername": { 198 | "value": "[parameters('adminUsername')]" 199 | }, 200 | "adminPassword": { 201 | "value": "[parameters('adminPassword')]" 202 | }, 203 | "domainName": { 204 | "value": "[parameters('domainName')]" 205 | }, 206 | "vmSize": { 207 | "value": "[parameters('tfsVmSize')]" 208 | }, 209 | "secrets": { 210 | "value": "[parameters('secrets')]" 211 | }, 212 | "sslThumbprint": { 213 | "value": "[parameters('sslThumbprint')]" 214 | } 215 | } 216 | } 217 | }, 218 | { 219 | "name": "tfsagents", 220 | "type": "Microsoft.Resources/deployments", 221 | "apiVersion": "2017-05-10", 222 | "dependsOn": [ 223 | "Microsoft.Resources/deployments/tfs" 224 | ], 225 | "properties": { 226 | "mode": "Incremental", 227 | "templateLink": { 228 | "uri": "[variables('agentTemplateUri')]", 229 | "contentVersion": "1.0.0.0" 230 | }, 231 | "parameters": { 232 | "subnetId": { 233 | "value": "[variables('devopsdubnetid')]" 234 | }, 235 | "adminUsername": { 236 | "value": "[parameters('adminUsername')]" 237 | }, 238 | "adminPassword": { 239 | "value": "[parameters('adminPassword')]" 240 | }, 241 | "domainName": { 242 | "value": "[parameters('domainName')]" 243 | }, 244 | "vmSize": { 245 | "value": "[parameters('tfsAgentVmSize')]" 246 | }, 247 | "tfsServerUrl": { 248 | "value": "[variables('tfsServerUrl')]" 249 | } 250 | } 251 | } 252 | } 253 | ], 254 | "outputs": {} 255 | } -------------------------------------------------------------------------------- /devnet-tfs-ha/private_ha_devops.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hansenms/iac/277b41b9f9a0ce395f577d8a74640000c91c5ce1/devnet-tfs-ha/private_ha_devops.png -------------------------------------------------------------------------------- /devnet-tfs/README.md: -------------------------------------------------------------------------------- 1 | Private TFS DevOps in Azure 2 | --------------------------- 3 | 4 | Microsoft [Azure](https://azure.microsoft.com/en-us/) offers a hosted DevOps experience with [Vistual Studio Team Services](https://www.visualstudio.com/team-services/) (VSTS). It is great, but sometimes organizations would like to host their own [Team Foundation Server](https://www.visualstudio.com/tfs/) (TFS). A particularly relevant use case is users in the [Microsoft Government Cloud](https://azure.microsoft.com/en-us/overview/clouds/government/) where VSTS is not yet available. Deploying a TFS Server with database and build agents can be complicated. 5 | 6 | This template illustrates how to create a private DevOps network with TFS server and agents. The topology is illustrated below: 7 | 8 | ![Private DevOps](./private_devops.png) 9 | 10 | This deployment is actually a nested compbination of deployment of a virtual network with two domain controllers and a JumpBox. You can deploy that without the TFS installation using [this template](https://github.com/hansenms/iac/tree/master/core-network). You can then deploy the TFS resources (e.g. in a different resource group) using [this template](https://github.com/hansenms/iac/tree/master/tfs). 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 20 | -------------------------------------------------------------------------------- /devnet-tfs/azuredeploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "adminUsername": { 6 | "type": "string", 7 | "defaultValue": "EnterpriseAdmin" 8 | }, 9 | "adminPassword": { 10 | "type": "securestring" 11 | }, 12 | "domainName": { 13 | "type": "string", 14 | "defaultValue": "devnet.contoso.us" 15 | }, 16 | "secrets": { 17 | "type": "array", 18 | "defaultValue": [] 19 | }, 20 | "sslThumbprint": { 21 | "type": "string", 22 | "defaultValue": "generate" 23 | }, 24 | "jumpBoxVmSize": { 25 | "type": "string", 26 | "allowedValues": [ 27 | "Standard_DS1_v2", 28 | "Standard_DS2_v2", 29 | "Standard_DS3_v2", 30 | "Standard_DS4_v2", 31 | "Standard_DS5_v2", 32 | "Standard_DS11_v2", 33 | "Standard_DS12_v2", 34 | "Standard_DS13_v2", 35 | "Standard_DS14_v2", 36 | "Standard_DS15_v2" 37 | ], 38 | "defaultValue": "Standard_DS2_v2" 39 | }, 40 | "DCVmSize": { 41 | "type": "string", 42 | "allowedValues": [ 43 | "Standard_DS1_v2", 44 | "Standard_DS2_v2", 45 | "Standard_DS3_v2", 46 | "Standard_DS4_v2", 47 | "Standard_DS5_v2", 48 | "Standard_DS11_v2", 49 | "Standard_DS12_v2", 50 | "Standard_DS13_v2", 51 | "Standard_DS14_v2", 52 | "Standard_DS15_v2" 53 | ], 54 | "defaultValue": "Standard_DS1_v2" 55 | }, 56 | "tfsVmSize": { 57 | "type": "string", 58 | "allowedValues": [ 59 | "Standard_DS1_v2", 60 | "Standard_DS2_v2", 61 | "Standard_DS3_v2", 62 | "Standard_DS4_v2", 63 | "Standard_DS5_v2", 64 | "Standard_DS11_v2", 65 | "Standard_DS12_v2", 66 | "Standard_DS13_v2", 67 | "Standard_DS14_v2", 68 | "Standard_DS15_v2" 69 | ], 70 | "defaultValue": "Standard_DS2_v2" 71 | }, 72 | "tfsSqlVmSize": { 73 | "type": "string", 74 | "allowedValues": [ 75 | "Standard_DS1_v2", 76 | "Standard_DS2_v2", 77 | "Standard_DS3_v2", 78 | "Standard_DS4_v2", 79 | "Standard_DS5_v2", 80 | "Standard_DS11_v2", 81 | "Standard_DS12_v2", 82 | "Standard_DS13_v2", 83 | "Standard_DS14_v2", 84 | "Standard_DS15_v2" 85 | ], 86 | "defaultValue": "Standard_DS4_v2" 87 | }, 88 | "tfsAgentVmSize": { 89 | "type": "string", 90 | "allowedValues": [ 91 | "Standard_DS1_v2", 92 | "Standard_DS2_v2", 93 | "Standard_DS3_v2", 94 | "Standard_DS4_v2", 95 | "Standard_DS5_v2", 96 | "Standard_DS11_v2", 97 | "Standard_DS12_v2", 98 | "Standard_DS13_v2", 99 | "Standard_DS14_v2", 100 | "Standard_DS15_v2" 101 | ], 102 | "defaultValue": "Standard_DS2_v2" 103 | }, 104 | "buildAgentVmInstanceCount": { 105 | "type": "int", 106 | "defaultValue": 1 107 | } 108 | }, 109 | "variables": { 110 | "baseUri": "[deployment().properties.templateLink.uri]", 111 | "coreNetworkTemplateUri": "[uri(variables('baseUri'), '../core-network/azuredeploy.json')]", 112 | "tfsTemplateUri": "[uri(variables('baseUri'), '../tfs/azuredeploy.json')]", 113 | "coreVnetName": "corevnet", 114 | "corevnetDevOpsSubnetName": "Subnet-2", 115 | "vnetid": "[resourceId('Microsoft.Network/virtualNetworks', variables('coreVnetName'))]", 116 | "devopsdubnetid": "[concat(variables('vnetid'), '/subnets/', variables('corevnetDevOpsSubnetName'))]" 117 | }, 118 | "resources": [ 119 | { 120 | "name": "core-network", 121 | "type": "Microsoft.Resources/deployments", 122 | "apiVersion": "2017-05-10", 123 | "properties": { 124 | "mode": "Incremental", 125 | "templateLink": { 126 | "uri": "[variables('coreNetworkTemplateUri')]", 127 | "contentVersion": "1.0.0.0" 128 | }, 129 | "parameters": { 130 | "adminUsername": { 131 | "value": "[parameters('adminUsername')]" 132 | }, 133 | "adminPassword": { 134 | "value": "[parameters('adminPassword')]" 135 | }, 136 | "domainName": { 137 | "value": "[parameters('domainName')]" 138 | }, 139 | "jumpBoxVmSize": { 140 | "value": "[parameters('jumpBoxVmSize')]" 141 | }, 142 | "DCVmSize": { 143 | "value": "[parameters('DCVmSize')]" 144 | } 145 | } 146 | } 147 | }, 148 | { 149 | "name": "tfs", 150 | "type": "Microsoft.Resources/deployments", 151 | "apiVersion": "2017-05-10", 152 | "dependsOn": [ 153 | "Microsoft.Resources/deployments/core-network" 154 | ], 155 | "properties": { 156 | "mode": "Incremental", 157 | "templateLink": { 158 | "uri": "[variables('tfsTemplateUri')]", 159 | "contentVersion": "1.0.0.0" 160 | }, 161 | "parameters": { 162 | "subnetId": { 163 | "value": "[variables('devopsdubnetid')]" 164 | }, 165 | "adminUsername": { 166 | "value": "[parameters('adminUsername')]" 167 | }, 168 | "adminPassword": { 169 | "value": "[parameters('adminPassword')]" 170 | }, 171 | "domainName": { 172 | "value": "[parameters('domainName')]" 173 | }, 174 | "tfsVmSize": { 175 | "value": "[parameters('tfsVmSize')]" 176 | }, 177 | "tfsSqlVmSize": { 178 | "value": "[parameters('tfsSqlVmSize')]" 179 | }, 180 | "tfsAgentVmSize": { 181 | "value": "[parameters('tfsAgentVmSize')]" 182 | }, 183 | "secrets": { 184 | "value": "[parameters('secrets')]" 185 | }, 186 | "sslThumbprint": { 187 | "value": "[parameters('sslThumbprint')]" 188 | }, 189 | "buildAgentVmInstanceCount": { 190 | "value": "[parameters('buildAgentVmInstanceCount')]" 191 | } 192 | } 193 | } 194 | } 195 | ], 196 | "outputs": {} 197 | } 198 | -------------------------------------------------------------------------------- /devnet-tfs/private_devops.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hansenms/iac/277b41b9f9a0ce395f577d8a74640000c91c5ce1/devnet-tfs/private_devops.png -------------------------------------------------------------------------------- /gadgetron-workstation/README.md: -------------------------------------------------------------------------------- 1 | Gadgetron Workstation 2 | --------------------- 3 | 4 | This template deploys a Linux (Ubuntu 18.04 LTS) development workstation for the [Gadgetron](https://github.com/gadgetron/gadgetron) project. The workstation includes all the dependencies required to compile and run the Gadgetron. Additionally, an [x2go](https://wiki.x2go.org/) server is installed for remote desktop connection. 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /gadgetron-workstation/azuredeploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "adminUsername": { 6 | "type": "string", 7 | "defaultValue": "azureuser" 8 | }, 9 | "sshKeyData": { 10 | "type": "string" 11 | }, 12 | "vmSize": { 13 | "type": "string", 14 | "allowedValues": [ 15 | "Standard_DS1_v2", 16 | "Standard_DS2_v2", 17 | "Standard_DS3_v2", 18 | "Standard_DS4_v2", 19 | "Standard_DS5_v2", 20 | "Standard_DS11_v2", 21 | "Standard_DS12_v2", 22 | "Standard_DS13_v2", 23 | "Standard_DS14_v2", 24 | "Standard_DS15_v2" 25 | ], 26 | "defaultValue": "Standard_DS2_v2" 27 | }, 28 | "vmName": { 29 | "type": "string", 30 | "defaultValue": "GadgetronWorkstation" 31 | }, 32 | "assignPublicIp": { 33 | "type": "bool", 34 | "defaultValue": true 35 | }, 36 | "subnetId": { 37 | "type": "string", 38 | "defaultValue": "null" 39 | } 40 | }, 41 | "variables": { 42 | "baseUri": "[deployment().properties.templateLink.uri]", 43 | "vmTemplateURL": "[uri(variables('baseUri'),'../primitives/linuxvm.json')]", 44 | "gadgetronConfigScriptURL": "[uri(variables('baseUri'), 'scripts/setup_gadgetron_workstation.sh')]", 45 | "gadgetronBootstrapScriptURL": "[uri(variables('baseUri'), 'scripts/bootstrap_gadgetron.sh')]" 46 | }, 47 | "resources": [ 48 | { 49 | "name": "GadgetronVmDeploy", 50 | "type": "Microsoft.Resources/deployments", 51 | "apiVersion": "2017-05-10", 52 | "properties": { 53 | "mode": "Incremental", 54 | "templateLink": { 55 | "uri": "[variables('vmTemplateURL')]", 56 | "contentVersion": "1.0.0.0" 57 | }, 58 | "parameters": { 59 | "vmName": { 60 | "value": "[parameters('vmName')]" 61 | }, 62 | "vmSize": { 63 | "value": "[parameters('vmSize')]" 64 | }, 65 | "subnetId": { 66 | "value": "[parameters('subnetId')]" 67 | }, 68 | "adminUsername": { 69 | "value": "[parameters('adminUsername')]" 70 | }, 71 | "sshKeyData": { 72 | "value": "[parameters('sshKeyData')]" 73 | }, 74 | "assignPublicIP": { 75 | "value": "[parameters('assignPublicIp')]" 76 | } 77 | } 78 | } 79 | }, 80 | { 81 | "type": "Microsoft.Compute/virtualMachines/extensions", 82 | "name": "[concat(parameters('vmName'), '/', '/configuregadgetron')]", 83 | "location": "[resourceGroup().location]", 84 | "apiVersion": "2018-06-01", 85 | "dependsOn": [ 86 | "['Microsoft.Resources/deployments/GadgetronVmDeploy']" 87 | ], 88 | "tags": { 89 | "displayName": "gadgetronsetup" 90 | }, 91 | "properties": { 92 | "publisher": "Microsoft.Azure.Extensions", 93 | "type": "CustomScript", 94 | "typeHandlerVersion": "2.0", 95 | "autoUpgradeMinorVersion": true, 96 | "settings": { 97 | "fileUris": [ 98 | "[variables('gadgetronConfigScriptURL')]", 99 | "[variables('gadgetronBootstrapScriptURL')]" 100 | ], 101 | "commandToExecute": "[concat('bash ./setup_gadgetron_workstation.sh ', parameters('adminUsername'))]" 102 | } 103 | } 104 | } 105 | ] 106 | } -------------------------------------------------------------------------------- /gadgetron-workstation/scripts/bootstrap_gadgetron.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mkdir -p ${HOME}/code 4 | mkdir -p ${HOME}/local 5 | 6 | GADGETRON_HOME=${HOME}/local 7 | ISMRMRD_HOME=${HOME}/local 8 | 9 | #ISMRMRD 10 | cd ${HOME}/code 11 | git clone https://github.com/ismrmrd/ismrmrd.git 12 | cd ismrmrd 13 | mkdir build 14 | cd build 15 | cmake -DCMAKE_INSTALL_PREFIX=~/local -DCMAKE_PREFIX_PATH=~/local ../ 16 | make -j $(nproc) 17 | make install 18 | 19 | #GADGETRON 20 | cd ${HOME}/code 21 | git clone https://github.com/gadgetron/gadgetron 22 | cd gadgetron 23 | mkdir build 24 | cd build 25 | cmake -DBUILD_WITH_PYTHON3=ON -DCMAKE_INSTALL_PREFIX=~/local -DCMAKE_PREFIX_PATH=~/local ../ 26 | make -j $(nproc) 27 | make install 28 | cp ${GADGETRON_HOME}/share/gadgetron/config/gadgetron.xml.example ${GADGETRON_HOME}/share/gadgetron/config/gadgetron.xml 29 | 30 | #ISMRMRD PYTHON API 31 | cd ${HOME}/code 32 | git clone https://github.com/ismrmrd/ismrmrd-python.git 33 | cd ismrmrd-python 34 | python3 setup.py install --user 35 | 36 | #ISMRMRD PYTHON TOOLS 37 | cd ${HOME}/code 38 | git clone https://github.com/ismrmrd/ismrmrd-python-tools.git 39 | cd ismrmrd-python-tools 40 | python3 setup.py install --user 41 | 42 | #SIEMENS_TO_ISMRMRD 43 | cd ${HOME}/code 44 | git clone https://github.com/ismrmrd/siemens_to_ismrmrd.git 45 | cd siemens_to_ismrmrd 46 | mkdir build 47 | cd build 48 | cmake -DCMAKE_INSTALL_PREFIX=${HOME}/local -DCMAKE_PREFIX_PATH=${HOME}/local ../ 49 | make -j $(nproc) 50 | make install 51 | 52 | #PHILIPS_TO_ISMRMRD 53 | cd ${HOME}/code 54 | git clone https://github.com/ismrmrd/philips_to_ismrmrd.git 55 | cd philips_to_ismrmrd 56 | mkdir build 57 | cd build 58 | cmake -DCMAKE_INSTALL_PREFIX=${HOME}/local -DCMAKE_PREFIX_PATH=${HOME}/local ../ 59 | make -j $(nproc) 60 | make install 61 | 62 | #Setup environment 63 | echo "" >> ${HOME}/.bashrc 64 | echo 'export GADGETRON_HOME=${HOME}/local' >> ${HOME}/.bashrc 65 | echo 'export LD_LIBRARY_PATH=${GADGETRON_HOME}/lib:${LD_LIBRARY_PATH}' >> ${HOME}/.bashrc 66 | echo 'export PATH=${GADGETRON_HOME}/bin:${PATH}' >> ${HOME}/.bashrc 67 | -------------------------------------------------------------------------------- /gadgetron-workstation/scripts/setup_gadgetron_workstation.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | USERNAME=$1 4 | CURRENT_FOLDER=${PWD} 5 | BOOTSTRAP_SCRIPT="bootstrap_gadgetron.sh" 6 | 7 | apt-get update 8 | 9 | apt-get install --yes software-properties-common apt-utils wget build-essential cython emacs python-dev python-pip python3-dev python3-pip libhdf5-serial-dev cmake git-core libboost-all-dev libfftw3-dev h5utils jq hdf5-tools liblapack-dev libopenblas-base libopenblas-dev libxml2-dev libfreetype6-dev pkg-config libxslt-dev libarmadillo-dev libace-dev gcc-multilib libgtest-dev python3-dev liblapack-dev liblapacke-dev libplplot-dev libdcmtk-dev cmake-curses-gui cmake neofetch net-tools cpio x2goserver ubuntu-mate-core emacs hdfview 10 | 11 | pip3 install --upgrade pip 12 | pip install -U pip setuptools 13 | apt-get install --no-install-recommends --no-install-suggests --yes python3-psutil python3-pyxb python3-lxml python3-numpy 14 | apt-get install --no-install-recommends --no-install-suggests --yes python3-pil 15 | apt-get install --no-install-recommends --no-install-suggests --yes python3-scipy 16 | apt-get install --no-install-recommends --no-install-suggests --yes python3-configargparse 17 | pip install Cython tk-tools matplotlib scikit-image opencv_python pydicom scikit-learn 18 | pip uninstall -y h5py 19 | apt-get install -y python3-h5py 20 | pip install --upgrade tensorflow 21 | pip install http://download.pytorch.org/whl/cpu/torch-0.4.0-cp36-cp36m-linux_x86_64.whl 22 | pip install torchvision 23 | pip install tensorboardx visdom 24 | 25 | mkdir -p 26 | cd /opt && \ 27 | git clone https://github.com/hansenms/ZFP.git && \ 28 | cd ZFP && \ 29 | mkdir lib && \ 30 | make && \ 31 | make shared && \ 32 | make -j $(nproc) install 33 | 34 | cd /opt && \ 35 | wget https://github.com/mrirecon/bart/archive/v0.4.03.tar.gz && \ 36 | tar -xzf v0.4.03.tar.gz && \ 37 | cd bart-0.4.03 && \ 38 | make -j $(nproc) && \ 39 | ln -s /opt/bart-v0.4.03/bart /usr/local/bin/bart 40 | 41 | apt-get install --yes libgoogle-glog-dev libeigen3-dev libsuitesparse-dev 42 | 43 | cd /opt && \ 44 | wget http://ceres-solver.org/ceres-solver-1.14.0.tar.gz && \ 45 | tar zxf ceres-solver-1.14.0.tar.gz && \ 46 | mkdir ceres-bin && \ 47 | cd ceres-bin && \ 48 | cmake ../ceres-solver-1.14.0 && \ 49 | make -j $(nproc) && \ 50 | make install 51 | 52 | cp ${CURRENT_FOLDER}/${BOOTSTRAP_SCRIPT} /home/${USERNAME}/ 53 | chown ${USERNAME}:${USERNAME} /home/${USERNAME}/${BOOTSTRAP_SCRIPT} 54 | chmod +x /home/${USERNAME}/${BOOTSTRAP_SCRIPT} 55 | su -c /home/${USERNAME}/${BOOTSTRAP_SCRIPT} ${USERNAME} -------------------------------------------------------------------------------- /iaas-web/DSC/ConfigureWeb.ps1: -------------------------------------------------------------------------------- 1 | configuration ConfigureWebDsc 2 | { 3 | param 4 | ( 5 | ) 6 | 7 | # Import the module that defines custom resources 8 | Import-DscResource -Module xWebAdministration, PSDesiredStateConfiguration 9 | 10 | Node localhost 11 | { 12 | WindowsFeature IIS 13 | { 14 | Ensure = 'Present' 15 | Name = 'Web-Server' 16 | } 17 | 18 | WindowsFeature aspnet45 19 | { 20 | Ensure = 'Present' 21 | Name = 'Web-Asp-Net45' 22 | DependsOn = '[WindowsFeature]IIS' 23 | } 24 | 25 | WindowsFeature NetFrameworkFeature 26 | { 27 | Ensure = 'Present' 28 | Name = 'NET-Framework-Features' 29 | DependsOn = '[WindowsFeature]aspnet45' 30 | } 31 | 32 | xWebsite DefaultSite 33 | { 34 | Ensure = 'Present' 35 | Name = 'Default Web Site' 36 | State = 'Started' 37 | PhysicalPath = 'C:\inetpub\wwwroot' 38 | DependsOn = '[WindowsFeature]NetFrameworkFeature' 39 | } 40 | 41 | Script InstallDotNetStuff 42 | { 43 | GetScript = { 44 | return @{ 'Result' = $true } 45 | } 46 | 47 | TestScript = { 48 | Test-Path $env:temp\dotnet-dev-win-x64.1.0.4.exe 49 | } 50 | SetScript = { 51 | # Install the .NET Core SDK 52 | Invoke-WebRequest https://go.microsoft.com/fwlink/?linkid=848827 -outfile $env:temp\dotnet-dev-win-x64.1.0.4.exe 53 | Start-Process $env:temp\dotnet-dev-win-x64.1.0.4.exe -ArgumentList '/quiet' -Wait 54 | 55 | # Install the .NET Core Windows Server Hosting bundle 56 | Invoke-WebRequest https://go.microsoft.com/fwlink/?LinkId=817246 -outfile $env:temp\DotNetCore.WindowsHosting.exe 57 | Start-Process $env:temp\DotNetCore.WindowsHosting.exe -ArgumentList '/quiet' -Wait 58 | 59 | # Restart the web server so that system PATH updates take effect 60 | net stop was /y 61 | net start w3svc 62 | } 63 | DependsOn = "[xWebsite]DefaultSite" 64 | } 65 | 66 | File WebContent 67 | { 68 | Ensure = 'Present' 69 | DestinationPath = 'C:\inetpub\wwwroot\index.html' 70 | Recurse = $true 71 | Type = 'File' 72 | Contents = '

This is the default website

' 73 | DependsOn = '[xWebsite]DefaultSite' 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /iaas-web/DSC/ConfigureWeb.ps1.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hansenms/iac/277b41b9f9a0ce395f577d8a74640000c91c5ce1/iaas-web/DSC/ConfigureWeb.ps1.zip -------------------------------------------------------------------------------- /iaas-web/README.md: -------------------------------------------------------------------------------- 1 | Simple IIS Web Server 2 | --------------------- 3 | 4 | This template deploys a Windows VM and deploys an HTML page in the default web site. 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /iaas-web/azuredeploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "adminUsername": { 6 | "type": "string", 7 | "defaultValue": "EnterpriseAdmin" 8 | }, 9 | "adminPassword": { 10 | "type": "securestring" 11 | }, 12 | "vmSize": { 13 | "type": "string", 14 | "allowedValues": [ 15 | "Standard_DS1_v2", 16 | "Standard_DS2_v2", 17 | "Standard_DS3_v2", 18 | "Standard_DS4_v2", 19 | "Standard_DS5_v2", 20 | "Standard_DS11_v2", 21 | "Standard_DS12_v2", 22 | "Standard_DS13_v2", 23 | "Standard_DS14_v2", 24 | "Standard_DS15_v2" 25 | ], 26 | "defaultValue": "Standard_DS2_v2" 27 | }, 28 | "vmName": { 29 | "type": "string", 30 | "defaultValue": "webServer" 31 | }, 32 | "assignPublicIp": { 33 | "type": "bool", 34 | "defaultValue": false 35 | }, 36 | "subnetId": { 37 | "type": "string", 38 | "defaultValue": "null" 39 | } 40 | }, 41 | "variables": { 42 | "baseUri": "[deployment().properties.templateLink.uri]", 43 | "windowsVmTemplateURL": "[uri(variables('baseUri'),'../primitives/windowsvm.json')]", 44 | "webConfigureModuleURL": "[uri(variables('baseUri'), 'DSC/ConfigureWeb.ps1.zip')]", 45 | "webConfigureFunction": "ConfigureWeb.ps1\\ConfigureWebDsc" 46 | }, 47 | "resources": [ 48 | { 49 | "name": "WebVmDeploy", 50 | "type": "Microsoft.Resources/deployments", 51 | "apiVersion": "2017-05-10", 52 | "properties": { 53 | "mode": "Incremental", 54 | "templateLink": { 55 | "uri": "[variables('windowsVmTemplateURL')]", 56 | "contentVersion": "1.0.0.0" 57 | }, 58 | "parameters": { 59 | "vmName": { 60 | "value": "[parameters('vmName')]" 61 | }, 62 | "vmSize": { 63 | "value": "[parameters('vmSize')]" 64 | }, 65 | "subnetId": { 66 | "value": "[parameters('subnetId')]" 67 | }, 68 | "adminUsername": { 69 | "value": "[parameters('adminUsername')]" 70 | }, 71 | "adminPassword": { 72 | "value": "[parameters('adminPassword')]" 73 | }, 74 | "assignPublicIP": { 75 | "value": "[parameters('assignPublicIp')]" 76 | } 77 | } 78 | } 79 | }, 80 | { 81 | "type": "Microsoft.Compute/virtualMachines/extensions", 82 | "name": "[concat(parameters('vmName'), '/configureagent')]", 83 | "dependsOn": [ 84 | "[resourceId('Microsoft.Resources/deployments', 'WebVmDeploy')]" 85 | ], 86 | "apiVersion": "2016-03-30", 87 | "location": "[resourceGroup().location]", 88 | "properties": { 89 | "publisher": "Microsoft.Powershell", 90 | "type": "DSC", 91 | "typeHandlerVersion": "2.21", 92 | "autoUpgradeMinorVersion": true, 93 | "settings": { 94 | "modulesURL": "[variables('webConfigureModuleURL')]", 95 | "configurationFunction": "[variables('webConfigureFunction')]", 96 | "properties": { 97 | } 98 | } 99 | } 100 | } 101 | ] 102 | } -------------------------------------------------------------------------------- /primitives/encryptvm.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "vmName": { 6 | "type": "string", 7 | "metadata": { 8 | "description": "Name of the virtual machine" 9 | } 10 | }, 11 | "aadClientID": { 12 | "type": "string", 13 | "metadata": { 14 | "description": "Client ID of AAD app which has permissions to KeyVault" 15 | } 16 | }, 17 | "aadClientSecret": { 18 | "type": "securestring", 19 | "metadata": { 20 | "description": "Client Secret of AAD app which has permissions to KeyVault" 21 | } 22 | }, 23 | "keyVaultResourceId": { 24 | "type": "string", 25 | "metadata": { 26 | "description": "Resource ID of the Keyvault" 27 | } 28 | }, 29 | "keyEncryptionKeyURL": { 30 | "type": "string", 31 | "defaultValue": "", 32 | "metadata": { 33 | "description": "URL of the KeyEncryptionKey used to encrypt the volume encryption key" 34 | } 35 | }, 36 | "volumeType": { 37 | "type": "string", 38 | "defaultValue": "All", 39 | "metadata": { 40 | "description": "Type of the volume OS or Data to perform encryption operation" 41 | } 42 | }, 43 | "sequenceVersion": { 44 | "type": "string", 45 | "defaultValue": "1.0", 46 | "metadata": { 47 | "description": "Pass in an unique value like a GUID everytime the operation needs to be force run" 48 | } 49 | } 50 | }, 51 | "variables": { 52 | "extensionName": "AzureDiskEncryption", 53 | "extensionVersion": "1.1", 54 | "encryptionOperation": "EnableEncryption", 55 | "keyEncryptionAlgorithm": "RSA-OAEP" 56 | }, 57 | "resources": [ 58 | { 59 | "type": "Microsoft.Compute/virtualMachines/extensions", 60 | "name": "[concat(parameters('vmName'),'/', variables('extensionName'))]", 61 | "apiVersion": "2016-04-30-preview", 62 | "location": "[resourceGroup().location]", 63 | "properties": { 64 | "publisher": "Microsoft.Azure.Security", 65 | "type": "AzureDiskEncryption", 66 | "typeHandlerVersion": "[variables('extensionVersion')]", 67 | "autoUpgradeMinorVersion": true, 68 | "forceUpdateTag": "[parameters('sequenceVersion')]", 69 | "settings": { 70 | "AADClientID": "[parameters('aadClientID')]", 71 | "KeyVaultURL": "[reference(parameters('keyVaultResourceId'), '2016-10-01').vaultUri]", 72 | "KeyEncryptionKeyURL": "[parameters('keyEncryptionKeyURL')]", 73 | "KeyEncryptionAlgorithm": "[variables('keyEncryptionAlgorithm')]", 74 | "VolumeType": "[parameters('volumeType')]", 75 | "EncryptionOperation": "[variables('encryptionOperation')]" 76 | }, 77 | "protectedSettings": { 78 | "AADClientSecret": "[parameters('aadClientSecret')]" 79 | } 80 | } 81 | }, 82 | { 83 | "apiVersion": "2016-04-30-preview", 84 | "type": "Microsoft.Compute/virtualMachines", 85 | "name": "[parameters('vmName')]", 86 | "location": "[resourceGroup().location]", 87 | "properties": { 88 | "storageProfile": { 89 | "osDisk": { 90 | "encryptionSettings": { 91 | "diskEncryptionKey": { 92 | "sourceVault": { 93 | "id": "[parameters('keyVaultResourceID')]" 94 | }, 95 | "secretUrl": "[reference(resourceId('Microsoft.Compute/virtualMachines/extensions', parameters('vmName'), variables('extensionName'))).instanceView.statuses[0].message]" 96 | }, 97 | "keyEncryptionKey": { 98 | "sourceVault": { 99 | "id": "[parameters('keyVaultResourceID')]" 100 | }, 101 | "keyUrl": "[parameters('keyEncryptionKeyURL')]" 102 | } 103 | } 104 | } 105 | } 106 | } 107 | } 108 | ], 109 | "outputs": { 110 | "BitLockerKey": { 111 | "type": "string", 112 | "value": "[reference(resourceId('Microsoft.Compute/virtualMachines/extensions', parameters('vmName'), variables('extensionName'))).instanceView.statuses[0].message]" 113 | } 114 | } 115 | } -------------------------------------------------------------------------------- /primitives/functionapp.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "appName": { 6 | "type": "string", 7 | "metadata": { 8 | "description": "The name of the function app that you wish to create." 9 | } 10 | }, 11 | "sku": { 12 | "type": "string", 13 | "allowedValues": [ 14 | "Free", 15 | "Shared", 16 | "Basic", 17 | "Standard" 18 | ], 19 | "defaultValue": "Standard", 20 | "metadata": { 21 | "description": "The pricing tier for the hosting plan (if one is needed)." 22 | } 23 | }, 24 | "workerSize": { 25 | "type": "string", 26 | "allowedValues": [ 27 | "0", 28 | "1", 29 | "2" 30 | ], 31 | "defaultValue": "0", 32 | "metadata": { 33 | "description": "The instance size of the hosting plan (small, medium, or large), if one is needed." 34 | } 35 | }, 36 | "appServicePlanId": { 37 | "type": "string", 38 | "defaultValue": "null", 39 | "metadata": { 40 | "description": "Resource ID for the App Service Plan. If null, an App Service Plan will be created." 41 | } 42 | }, 43 | "storageAccountType": { 44 | "type": "string", 45 | "defaultValue": "Standard_LRS", 46 | "allowedValues": [ 47 | "Standard_LRS", 48 | "Standard_GRS", 49 | "Standard_RAGRS" 50 | ], 51 | "metadata": { 52 | "description": "Storage Account type" 53 | } 54 | } 55 | }, 56 | "variables": { 57 | "functionAppName": "[parameters('appName')]", 58 | "hostingPlanName": "[parameters('appName')]", 59 | "genPlanId": "[resourceId('Microsoft.Web/serverFarms/', variables('hostingPlanName'))]", 60 | "hostingPlanId": "[if(equals(parameters('appServicePlanId'),'null'), variables('genPlanId'), parameters('appServicePlanId'))]", 61 | "storageAccountName": "[concat(uniquestring(resourceGroup().id), 'functions')]" 62 | }, 63 | "resources": [ 64 | { 65 | "type": "Microsoft.Storage/storageAccounts", 66 | "name": "[variables('storageAccountName')]", 67 | "apiVersion": "2017-10-01", 68 | "location": "[resourceGroup().location]", 69 | "kind": "Storage", 70 | "sku": { 71 | "name": "[parameters('storageAccountType')]" 72 | } 73 | }, 74 | { 75 | "type": "Microsoft.Web/serverfarms", 76 | "apiVersion": "2016-09-01", 77 | "condition": "[equals(parameters('appServicePlanId'),'null')]", 78 | "name": "[variables('hostingPlanName')]", 79 | "location": "[resourceGroup().location]", 80 | "properties": { 81 | "name": "[variables('hostingPlanName')]", 82 | "sku": "[parameters('sku')]", 83 | "workerSize": "[parameters('workerSize')]", 84 | "hostingEnvironment": "", 85 | "numberOfWorkers": 1 86 | } 87 | }, 88 | { 89 | "apiVersion": "2016-08-01", 90 | "type": "Microsoft.Web/sites", 91 | "name": "[variables('functionAppName')]", 92 | "location": "[resourceGroup().location]", 93 | "kind": "functionapp", 94 | "properties": { 95 | "name": "[variables('functionAppName')]", 96 | "serverFarmId": "[variables('hostingPlanId')]", 97 | "hostingEnvironment": "", 98 | "clientAffinityEnabled": false, 99 | "siteConfig": { 100 | "alwaysOn": true, 101 | "cors":{ 102 | "allowedOrigins":[ 103 | "https://functions.azure.com", 104 | "https://functions-staging.azure.com", 105 | "https://functions-next.azure.com", 106 | "https://functions-usgov-iowa.azurewebsites.us", 107 | "https://functions-usgov-texas.azurewebsites.us", 108 | "https://functions.ext.azure.us" 109 | ] 110 | } 111 | } 112 | }, 113 | "dependsOn": [ 114 | "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]", 115 | "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]" 116 | ], 117 | "resources": [ 118 | { 119 | "apiVersion": "2016-08-01", 120 | "name": "appsettings", 121 | "type": "config", 122 | "dependsOn": [ 123 | "[resourceId('Microsoft.Web/sites', variables('functionAppName'))]", 124 | "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]" 125 | ], 126 | "properties": { 127 | "AzureWebJobsStorage": "[concat('DefaultEndpointsProtocol=https;AccountName=',variables('storageAccountName'),';AccountKey=',listkeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2015-05-01-preview').key1,';')]", 128 | "AzureWebJobsDashboard": "[concat('DefaultEndpointsProtocol=https;AccountName=',variables('storageAccountName'),';AccountKey=',listkeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2015-05-01-preview').key1,';')]", 129 | "FUNCTIONS_EXTENSION_VERSION": "~1" 130 | } 131 | } 132 | ] 133 | } 134 | ] 135 | } -------------------------------------------------------------------------------- /primitives/linuxvm.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "vmName": { 6 | "type": "string" 7 | }, 8 | "adminUsername": { 9 | "type": "string", 10 | "defaultValue": "azureuser", 11 | "metadata": { 12 | "description": "User name for the Virtual Machine." 13 | } 14 | }, 15 | "sshKeyData": { 16 | "type": "string", 17 | "metadata": { 18 | "description": "SSH rsa public key file as a string." 19 | } 20 | }, 21 | "vmSize": { 22 | "type": "string", 23 | "defaultValue": "Standard_D2" 24 | }, 25 | "subnetId": { 26 | "type": "string", 27 | "defaultValue": "null" 28 | }, 29 | "imagePublisher": { 30 | "type": "string", 31 | "defaultValue": "Canonical" 32 | }, 33 | "imageOffer": { 34 | "type": "string", 35 | "defaultValue": "UbuntuServer" 36 | }, 37 | "imageSku": { 38 | "type": "string", 39 | "defaultValue": "18.04-LTS" 40 | }, 41 | "diagStorageAccountId": { 42 | "type": "string", 43 | "defaultValue": "null" 44 | }, 45 | "assignPublicIP": { 46 | "type": "bool", 47 | "defaultValue": false 48 | }, 49 | "fixedPrivateIp": { 50 | "type": "string", 51 | "defaultValue": "" 52 | }, 53 | "dataDiskSize": { 54 | "type": "int", 55 | "defaultValue": 1023 56 | }, 57 | "numberOfDataDisks": { 58 | "type": "int", 59 | "minValue": 0, 60 | "maxValue": 4, 61 | "defaultValue": 1 62 | }, 63 | "availabilitySetId": { 64 | "type": "string", 65 | "defaultValue": "" 66 | }, 67 | "LoadBalancerBEId": { 68 | "type": "string", 69 | "defaultValue": "" 70 | }, 71 | "secrets": { 72 | "type": "array", 73 | "defaultValue": [] 74 | } 75 | }, 76 | "variables": { 77 | "sshKeyPath": "[concat('/home/',parameters('adminUsername'),'/.ssh/authorized_keys')]", 78 | "publicipTemplateUrl": "[uri(deployment().properties.templateLink.uri, 'publicip.json')]", 79 | "nicName": "[concat(parameters('vmName'),'-nic')]", 80 | "genDiagStorageAccountName": "[concat('vmdiag', uniqueString( resourceGroup().id, deployment().name ))]", 81 | "genStorageAccountId": "[resourceId('Microsoft.Storage/storageAccounts/', variables('genDiagStorageAccountName'))]", 82 | "genVnetName": "[concat(parameters('vmName'),'-vnet')]", 83 | "diagStorageAccountId": "[if(equals(parameters('diagStorageAccountId'),'null'), variables('genStorageAccountId'), parameters('diagStorageAccountId'))]", 84 | "genVnetid": "[resourceId('Microsoft.Network/virtualNetworks', variables('genVnetName'))]", 85 | "genSubnetId": "[concat(variables('genVnetid'), '/subnets/default')]", 86 | "subnetId": "[if(equals(parameters('subnetId'),'null'), variables('genSubnetId'), parameters('subnetId'))]", 87 | "availabilitySetId": { 88 | "id": "[parameters('availabilitySetId')]" 89 | }, 90 | "loadBalancerBEIds": [ 91 | { 92 | "id": "[parameters('LoadBalancerBEId')]" 93 | } 94 | ], 95 | "dataDisks": [ 96 | { 97 | "diskSizeGB": "[parameters('dataDiskSize')]", 98 | "lun": 0, 99 | "createOption": "Empty" 100 | }, 101 | { 102 | "diskSizeGB": "[parameters('dataDiskSize')]", 103 | "lun": 1, 104 | "createOption": "Empty" 105 | }, 106 | { 107 | "diskSizeGB": "[parameters('dataDiskSize')]", 108 | "lun": 2, 109 | "createOption": "Empty" 110 | }, 111 | { 112 | "diskSizeGB": "[parameters('dataDiskSize')]", 113 | "lun": 3, 114 | "createOption": "Empty" 115 | } 116 | ] 117 | }, 118 | "resources": [ 119 | { 120 | "type": "Microsoft.Storage/storageAccounts", 121 | "name": "[variables('genDiagStorageAccountName')]", 122 | "apiVersion": "2016-01-01", 123 | "condition": "[equals(parameters('diagStorageAccountId'),'null')]", 124 | "location": "[resourceGroup().location]", 125 | "sku": { 126 | "name": "Standard_LRS" 127 | }, 128 | "kind": "Storage", 129 | "properties": {} 130 | }, 131 | { 132 | "name": "[variables('genVnetName')]", 133 | "type": "Microsoft.Network/virtualNetworks", 134 | "location": "[resourceGroup().location]", 135 | "apiVersion": "2016-03-30", 136 | "condition": "[equals(parameters('subnetId'),'null')]", 137 | "dependsOn": [], 138 | "properties": { 139 | "addressSpace": { 140 | "addressPrefixes": [ 141 | "10.0.0.0/16" 142 | ] 143 | }, 144 | "subnets": [ 145 | { 146 | "name": "default", 147 | "properties": { 148 | "addressPrefix": "10.0.0.0/24" 149 | } 150 | } 151 | ] 152 | } 153 | }, 154 | { 155 | "name": "[variables('nicName')]", 156 | "type": "Microsoft.Network/networkInterfaces", 157 | "location": "[resourceGroup().location]", 158 | "apiVersion": "2016-03-30", 159 | "dependsOn": [ 160 | "[variables('genVnetid')]" 161 | ], 162 | "properties": { 163 | "ipConfigurations": [ 164 | { 165 | "name": "ipconfig1", 166 | "properties": { 167 | "privateIPAllocationMethod": "[if(empty(parameters('fixedPrivateIp')), 'Dynamic', 'Static')]", 168 | "privateIPAddress": "[parameters('fixedPrivateIp')]", 169 | "subnet": { 170 | "id": "[variables('subnetId')]" 171 | }, 172 | "loadBalancerBackendAddressPools": "[if(not(empty(parameters('LoadBalancerBEId'))),variables('loadBalancerBEIds'),json('null'))]" 173 | } 174 | } 175 | ] 176 | } 177 | }, 178 | { 179 | "apiVersion": "2017-05-10", 180 | "name": "[concat(parameters('vmName'),'-publicip')]", 181 | "type": "Microsoft.Resources/deployments", 182 | "condition": "[parameters('assignPublicIP')]", 183 | "dependsOn": [ 184 | "[resourceId('Microsoft.Compute/virtualMachines',parameters('vmName'))]" 185 | ], 186 | "properties": { 187 | "mode": "Incremental", 188 | "templateLink": { 189 | "uri": "[variables('publicipTemplateUrl')]", 190 | "contentVersion": "1.0.0.0" 191 | }, 192 | "parameters": { 193 | "nicName": { 194 | "value": "[variables('nicName')]" 195 | }, 196 | "subnetId": { 197 | "value": "[variables('subnetId')]" 198 | } 199 | } 200 | } 201 | }, 202 | { 203 | "apiVersion": "2017-03-30", 204 | "type": "Microsoft.Compute/virtualMachines", 205 | "name": "[parameters('vmName')]", 206 | "location": "[resourceGroup().location]", 207 | "dependsOn": [ 208 | "[resourceId('Microsoft.Network/networkInterfaces/', variables('nicName'))]", 209 | "[variables('genStorageAccountId')]" 210 | ], 211 | "properties": { 212 | "availabilitySet": "[if(not(empty(parameters('availabilitySetId'))), variables('availabilitySetId'), json('null'))]", 213 | "hardwareProfile": { 214 | "vmSize": "[parameters('vmSize')]" 215 | }, 216 | "osProfile": { 217 | "computerName": "[parameters('vmName')]", 218 | "adminUsername": "[parameters('adminUsername')]", 219 | "linuxConfiguration": { 220 | "disablePasswordAuthentication": true, 221 | "ssh": { 222 | "publicKeys": [ 223 | { 224 | "path": "[variables('sshKeyPath')]", 225 | "keyData": "[parameters('sshKeyData')]" 226 | } 227 | ] 228 | } 229 | }, 230 | "secrets": "[parameters('secrets')]" 231 | }, 232 | "storageProfile": { 233 | "imageReference": { 234 | "publisher": "[parameters('imagePublisher')]", 235 | "offer": "[parameters('imageOffer')]", 236 | "sku": "[parameters('imageSku')]", 237 | "version": "latest" 238 | }, 239 | "osDisk": { 240 | "createOption": "FromImage" 241 | }, 242 | "dataDisks": "[take(variables('dataDisks'),parameters('numberOfDataDisks'))]" 243 | }, 244 | "networkProfile": { 245 | "networkInterfaces": [ 246 | { 247 | "id": "[resourceId('Microsoft.Network/networkInterfaces',variables('nicName'))]" 248 | } 249 | ] 250 | }, 251 | "diagnosticsProfile": { 252 | "bootDiagnostics": { 253 | "enabled": true, 254 | "storageUri": "[reference(variables('diagStorageAccountId'),'2016-01-01').primaryEndpoints.blob]" 255 | } 256 | } 257 | } 258 | } 259 | ], 260 | "outputs": { 261 | "diagStorageAccountId": { 262 | "type": "string", 263 | "value": "[variables('diagStorageAccountId')]" 264 | }, 265 | "subnetId": { 266 | "type": "string", 267 | "value": "[variables('subnetId')]" 268 | } 269 | } 270 | } -------------------------------------------------------------------------------- /primitives/publicip.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "nicName": { 6 | "type": "string" 7 | }, 8 | "ipConfigName": { 9 | "type": "string", 10 | "defaultValue": "ipconfig1" 11 | }, 12 | "subnetId": { 13 | "type": "string" 14 | } 15 | }, 16 | "variables": { 17 | "publicIpName": "[concat(parameters('nicName'),'-ip')]" 18 | }, 19 | "resources": [ 20 | { 21 | "name": "[variables('publicIpName')]", 22 | "type": "Microsoft.Network/publicIPAddresses", 23 | "location": "[resourceGroup().location]", 24 | "apiVersion": "2016-03-30", 25 | "properties": { 26 | "publicIPAllocationMethod": "Dynamic" 27 | } 28 | }, 29 | { 30 | "name": "[parameters('nicName')]", 31 | "type": "Microsoft.Network/networkInterfaces", 32 | "location": "[resourceGroup().location]", 33 | "dependsOn": [ 34 | "[resourceId('Microsoft.Network/publicIpAddresses', variables('publicIpName'))]" 35 | ], 36 | "apiVersion": "2016-03-30", 37 | "properties": { 38 | "ipConfigurations": [ 39 | { 40 | "name": "[parameters('ipConfigName')]", 41 | "properties": { 42 | "publicIPAddress": { 43 | "id": "[resourceId('Microsoft.Network/publicIpAddresses', variables('publicIpName'))]" 44 | }, 45 | "subnet": { 46 | "id": "[parameters('subnetId')]" 47 | } 48 | } 49 | } 50 | ] 51 | } 52 | } 53 | ] 54 | } -------------------------------------------------------------------------------- /primitives/vnet.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "vnetName": { 6 | "type": "string", 7 | "defaultValue": "vnet" 8 | }, 9 | "addressPrefix": { 10 | "type": "string", 11 | "defaultValue": "10.0.0.0/16" 12 | }, 13 | "subnets": { 14 | "type": "array", 15 | "defaultValue": [ 16 | { 17 | "name": "default", 18 | "properties" : { 19 | "addressPrefix": "10.0.0.0/24" 20 | } 21 | } 22 | ] 23 | } 24 | }, 25 | "resources": [ 26 | { 27 | "name": "[parameters('vnetName')]", 28 | "type": "Microsoft.Network/virtualNetworks", 29 | "location": "[resourceGroup().location]", 30 | "apiVersion": "2016-03-30", 31 | "dependsOn": [], 32 | "properties": { 33 | "addressSpace": { 34 | "addressPrefixes": [ 35 | "[parameters('addressPrefix')]" 36 | ] 37 | }, 38 | "subnets": "[parameters('subnets')]" 39 | } 40 | } 41 | ], 42 | "outputs": { 43 | "subnets": { 44 | "type": "array", 45 | "value": "[reference(resourceId('Microsoft.Network/virtualNetworks', parameters('vnetName'))).subnets]" 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /primitives/windowsvm.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "vmName": { 6 | "type": "string" 7 | }, 8 | "adminUsername": { 9 | "type": "string" 10 | }, 11 | "adminPassword": { 12 | "type": "securestring" 13 | }, 14 | "vmSize": { 15 | "type": "string", 16 | "defaultValue": "Standard_D2" 17 | }, 18 | "subnetId": { 19 | "type": "string", 20 | "defaultValue": "null" 21 | }, 22 | "imagePublisher": { 23 | "type": "string", 24 | "defaultValue": "MicrosoftWindowsServer" 25 | }, 26 | "imageOffer": { 27 | "type": "string", 28 | "defaultValue": "WindowsServer" 29 | }, 30 | "imageSku": { 31 | "type": "string", 32 | "defaultValue": "2016-Datacenter" 33 | }, 34 | "diagStorageAccountId": { 35 | "type": "string", 36 | "defaultValue": "null" 37 | }, 38 | "assignPublicIP": { 39 | "type": "bool", 40 | "defaultValue": false 41 | }, 42 | "fixedPrivateIp": { 43 | "type": "string", 44 | "defaultValue": "" 45 | }, 46 | "dataDiskSize": { 47 | "type": "int", 48 | "defaultValue": 1023 49 | }, 50 | "numberOfDataDisks": { 51 | "type": "int", 52 | "minValue": 0, 53 | "maxValue": 4, 54 | "defaultValue": 1 55 | }, 56 | "availabilitySetId": { 57 | "type": "string", 58 | "defaultValue": "" 59 | }, 60 | "LoadBalancerBEId": { 61 | "type": "string", 62 | "defaultValue": "" 63 | }, 64 | "secrets": { 65 | "type": "array", 66 | "defaultValue": [] 67 | } 68 | }, 69 | "variables": { 70 | "publicipTemplateUrl": "[uri(deployment().properties.templateLink.uri, 'publicip.json')]", 71 | "nicName": "[concat(parameters('vmName'),'-nic')]", 72 | "genDiagStorageAccountName": "[concat('vmdiag', uniqueString( resourceGroup().id, deployment().name ))]", 73 | "genStorageAccountId": "[resourceId('Microsoft.Storage/storageAccounts/', variables('genDiagStorageAccountName'))]", 74 | "genVnetName": "[concat(parameters('vmName'),'-vnet')]", 75 | "diagStorageAccountId": "[if(equals(parameters('diagStorageAccountId'),'null'), variables('genStorageAccountId'), parameters('diagStorageAccountId'))]", 76 | "genVnetid": "[resourceId('Microsoft.Network/virtualNetworks', variables('genVnetName'))]", 77 | "genSubnetId": "[concat(variables('genVnetid'), '/subnets/default')]", 78 | "subnetId": "[if(equals(parameters('subnetId'),'null'), variables('genSubnetId'), parameters('subnetId'))]", 79 | "availabilitySetId": { 80 | "id": "[parameters('availabilitySetId')]" 81 | }, 82 | "loadBalancerBEIds": [ 83 | { 84 | "id": "[parameters('LoadBalancerBEId')]" 85 | } 86 | ], 87 | "dataDisks": [ 88 | { 89 | "diskSizeGB": "[parameters('dataDiskSize')]", 90 | "lun": 0, 91 | "createOption": "Empty" 92 | }, 93 | { 94 | "diskSizeGB": "[parameters('dataDiskSize')]", 95 | "lun": 1, 96 | "createOption": "Empty" 97 | }, 98 | { 99 | "diskSizeGB": "[parameters('dataDiskSize')]", 100 | "lun": 2, 101 | "createOption": "Empty" 102 | }, 103 | { 104 | "diskSizeGB": "[parameters('dataDiskSize')]", 105 | "lun": 3, 106 | "createOption": "Empty" 107 | } 108 | ] 109 | }, 110 | "resources": [ 111 | { 112 | "type": "Microsoft.Storage/storageAccounts", 113 | "name": "[variables('genDiagStorageAccountName')]", 114 | "apiVersion": "2016-01-01", 115 | "condition": "[equals(parameters('diagStorageAccountId'),'null')]", 116 | "location": "[resourceGroup().location]", 117 | "sku": { 118 | "name": "Standard_LRS" 119 | }, 120 | "kind": "Storage", 121 | "properties": {} 122 | }, 123 | { 124 | "name": "[variables('genVnetName')]", 125 | "type": "Microsoft.Network/virtualNetworks", 126 | "location": "[resourceGroup().location]", 127 | "apiVersion": "2016-03-30", 128 | "condition": "[equals(parameters('subnetId'),'null')]", 129 | "dependsOn": [], 130 | "properties": { 131 | "addressSpace": { 132 | "addressPrefixes": [ 133 | "10.0.0.0/16" 134 | ] 135 | }, 136 | "subnets": [ 137 | { 138 | "name": "default", 139 | "properties" : { 140 | "addressPrefix": "10.0.0.0/24" 141 | } 142 | } 143 | ] 144 | } 145 | }, 146 | { 147 | "name": "[variables('nicName')]", 148 | "type": "Microsoft.Network/networkInterfaces", 149 | "location": "[resourceGroup().location]", 150 | "apiVersion": "2016-03-30", 151 | "dependsOn": [ 152 | "[variables('genVnetid')]" 153 | ], 154 | "properties": { 155 | "ipConfigurations": [ 156 | { 157 | "name": "ipconfig1", 158 | "properties": { 159 | "privateIPAllocationMethod": "[if(empty(parameters('fixedPrivateIp')), 'Dynamic', 'Static')]", 160 | "privateIPAddress": "[parameters('fixedPrivateIp')]", 161 | "subnet": { 162 | "id": "[variables('subnetId')]" 163 | }, 164 | "loadBalancerBackendAddressPools": "[if(not(empty(parameters('LoadBalancerBEId'))),variables('loadBalancerBEIds'),json('null'))]" 165 | } 166 | } 167 | ] 168 | } 169 | }, 170 | { 171 | "apiVersion": "2017-05-10", 172 | "name": "[concat(parameters('vmName'),'-publicip')]", 173 | "type": "Microsoft.Resources/deployments", 174 | "condition": "[parameters('assignPublicIP')]", 175 | "dependsOn": [ 176 | "[resourceId('Microsoft.Compute/virtualMachines',parameters('vmName'))]" 177 | ], 178 | "properties": { 179 | "mode": "Incremental", 180 | "templateLink": { 181 | "uri": "[variables('publicipTemplateUrl')]", 182 | "contentVersion": "1.0.0.0" 183 | }, 184 | "parameters": { 185 | "nicName": { 186 | "value": "[variables('nicName')]" 187 | }, 188 | "subnetId": { 189 | "value": "[variables('subnetId')]" 190 | } 191 | } 192 | } 193 | }, 194 | { 195 | "apiVersion": "2017-03-30", 196 | "type": "Microsoft.Compute/virtualMachines", 197 | "name": "[parameters('vmName')]", 198 | "location": "[resourceGroup().location]", 199 | "dependsOn": [ 200 | "[resourceId('Microsoft.Network/networkInterfaces/', variables('nicName'))]", 201 | "[variables('genStorageAccountId')]" 202 | ], 203 | "properties": { 204 | "availabilitySet": "[if(not(empty(parameters('availabilitySetId'))), variables('availabilitySetId'), json('null'))]", 205 | "hardwareProfile": { 206 | "vmSize": "[parameters('vmSize')]" 207 | }, 208 | "osProfile": { 209 | "computerName": "[parameters('vmName')]", 210 | "adminUsername": "[parameters('adminUsername')]", 211 | "adminPassword": "[parameters('adminPassword')]", 212 | "secrets": "[parameters('secrets')]" 213 | }, 214 | "storageProfile": { 215 | "imageReference": { 216 | "publisher": "[parameters('imagePublisher')]", 217 | "offer": "[parameters('imageOffer')]", 218 | "sku": "[parameters('imageSku')]", 219 | "version": "latest" 220 | }, 221 | "osDisk": { 222 | "createOption": "FromImage" 223 | }, 224 | "dataDisks": "[take(variables('dataDisks'),parameters('numberOfDataDisks'))]" 225 | }, 226 | "networkProfile": { 227 | "networkInterfaces": [ 228 | { 229 | "id": "[resourceId('Microsoft.Network/networkInterfaces',variables('nicName'))]" 230 | } 231 | ] 232 | }, 233 | "diagnosticsProfile": { 234 | "bootDiagnostics": { 235 | "enabled": true, 236 | "storageUri": "[reference(variables('diagStorageAccountId'),'2016-01-01').primaryEndpoints.blob]" 237 | } 238 | } 239 | } 240 | } 241 | ], 242 | "outputs": { 243 | "diagStorageAccountId": { 244 | "type": "string", 245 | "value": "[variables('diagStorageAccountId')]" 246 | }, 247 | "subnetId": { 248 | "type": "string", 249 | "value": "[variables('subnetId')]" 250 | } 251 | } 252 | } -------------------------------------------------------------------------------- /scripts/CreateKeyVault.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [Parameter(Mandatory = $true, Position = 1)] 3 | [String]$ResourceGroupName, 4 | 5 | [Parameter(Mandatory = $true, Position = 2)] 6 | [String]$KeyVaultName 7 | ) 8 | 9 | $azcontext = Get-AzureRmContext 10 | if ([string]::IsNullOrEmpty($azcontext.Account)) { 11 | throw "User not logged into Azure." 12 | } 13 | 14 | $rg = Get-AzureRmResourceGroup -Name $ResourceGroupName 15 | 16 | # Create a new AD application 17 | $identifierUri = [string]::Format("http://localhost:8080/{0}", [Guid]::NewGuid().ToString("N")) 18 | $defaultHomePage = 'http://contoso.com' 19 | $now = [System.DateTime]::Now 20 | $oneYearFromNow = $now.AddYears(1) 21 | $aadClientSecret = [System.Convert]::ToBase64String($([guid]::NewGuid()).ToByteArray()) 22 | $aadClientPassword = ConvertTo-SecureString -String $aadClientSecret -AsPlainText -Force 23 | $aadAppName = $KeyVaultName + "aadapp" 24 | 25 | $ADApp = New-AzureRmADApplication -DisplayName $aadAppName -HomePage $defaultHomePage -IdentifierUris $identifierUri -StartDate $now -EndDate $oneYearFromNow -Password $aadClientPassword 26 | $servicePrincipal = New-AzureRmADServicePrincipal -ApplicationId $ADApp.ApplicationId 27 | $SvcPrincipals = (Get-AzureRmADServicePrincipal -SearchString $aadAppName) 28 | if (-not $SvcPrincipals) { 29 | # AAD app wasn't created 30 | Write-Error "Failed to create AAD app $aadAppName. Please log-in to Azure using Login-AzureRmAccount and try again"; 31 | return; 32 | } 33 | $aadClientID = $servicePrincipal.ApplicationId; 34 | 35 | $kv = New-AzureRmKeyVault -VaultName $KeyVaultName -Location $rg.Location -ResourceGroupName $ResourceGroupName 36 | $kek = Add-AzureKeyVaultKey -VaultName $KeyVaultName -Name "DiskKeyEncryptionKey" -Destination Software 37 | 38 | # Specify privileges to the vault for the AAD application - https://msdn.microsoft.com/en-us/library/mt603625.aspx 39 | Set-AzureRmKeyVaultAccessPolicy -VaultName $KeyVaultName -ServicePrincipalName $aadClientID -PermissionsToKeys wrapKey -PermissionsToSecrets set; 40 | Set-AzureRmKeyVaultAccessPolicy -VaultName $KeyVaultName -EnabledForDiskEncryption -EnabledForDeployment -EnabledForTemplateDeployment 41 | 42 | $keyVaultInfo = @{ 43 | "AADClientID" = $aadClientID 44 | "AADClientSecret" = $aadClientSecret 45 | "KeyVaultURL" = $kv.VaultUri 46 | "KeyVaultResourceId" = $kv.ResourceId 47 | "KeyEncryptionKeyURL" = $kek.Key.Kid 48 | } 49 | 50 | return $keyVaultInfo 51 | -------------------------------------------------------------------------------- /scripts/PrepareAseDeployment.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding(DefaultParameterSetName="nodevops")] 2 | param( 3 | [Parameter(Mandatory=$false)] 4 | [String]$CertificatePath, 5 | 6 | [Parameter(Mandatory=$false)] 7 | [String]$DomainName = "contoso-internal.us", 8 | 9 | [Parameter(Mandatory)] 10 | [securestring]$CertificatePassword, 11 | 12 | [Parameter(Mandatory=$false,ParameterSetName="devops")] 13 | [String]$AdminUsername = "EnterpriseAdmin", 14 | 15 | [Parameter(Mandatory=$true,ParameterSetName="devops")] 16 | [securestring]$AdminPassword, 17 | 18 | [Parameter(Mandatory=$true,ParameterSetName="devops")] 19 | [String]$TSServerUrl, 20 | 21 | [Parameter(Mandatory=$true,ParameterSetName="devops")] 22 | [String]$AgentPool, 23 | 24 | [Parameter(Mandatory=$true,ParameterSetName="devops")] 25 | [String]$PAToken, 26 | 27 | [Parameter(Mandatory=$false)] 28 | [String]$OutFile = ".\azuredeploy.parameters.json" 29 | 30 | ) 31 | 32 | #Check if the user is administrator 33 | if (-not [bool](([System.Security.Principal.WindowsIdentity]::GetCurrent()).groups -match "S-1-5-32-544")) { 34 | throw "You must have administrator priveleges to run this script." 35 | } 36 | 37 | if ([String]::IsNullOrEmpty($CertificatePath)) { 38 | $CertificatePath = [System.IO.Path]::GetTempFileName() 39 | 40 | $certificate = New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "*.$DomainName","*.scm.$DomainName" 41 | $certThumbprint = "cert:\localMachine\my\" + $certificate.Thumbprint 42 | Export-PfxCertificate -cert $certThumbprint -FilePath $CertificatePath -Password $CertificatePassword 43 | } else { 44 | $certificate = Import-PfxCertificate -CertStoreLocation Cert:\LocalMachine\My -FilePath $CertificatePath -Password $CertificatePassword 45 | $certThumbprint = "cert:\localMachine\my\" + $certificate.Thumbprint 46 | } 47 | 48 | $fileContentBytes = Get-Content $CertificatePath -Encoding Byte 49 | $pfxBlobString = [System.Convert]::ToBase64String($fileContentBytes) 50 | 51 | $templateParameters = @{ 52 | "pfxBlobString" = @{ 53 | "value" = $pfxBlobString 54 | } 55 | "certificatePassword" = @{ 56 | "value" = (New-Object PSCredential "user", $CertificatePassword).GetNetworkCredential().Password 57 | } 58 | "certificateThumbprint" = @{ 59 | "value" = $certificate.Thumbprint 60 | } 61 | "domainName" = @{ 62 | "value" = $DomainName 63 | } 64 | } 65 | 66 | if (-not [String]::IsNullOrEmpty($AdminPassword)) { 67 | $templateParameters.Add("AdminUsername", @{ "value" = $AdminUsername}) 68 | $templateParameters.Add("AdminPassword", @{ "value" = (New-Object PSCredential "user", $AdminPassword).GetNetworkCredential().Password}) 69 | $templateParameters.Add("TSServerUrl", @{ "value" = $TSServerUrl}) 70 | $templateParameters.Add("AgentPool", @{ "value" = $AgentPool}) 71 | $templateParameters.Add("PAToken", @{ "value" = $PAToken}) 72 | } 73 | 74 | 75 | $templateParameters | ConvertTo-Json -Depth 10 | Out-File $OutFile 76 | 77 | Write-Host "Parameters written to $OutFile." 78 | -------------------------------------------------------------------------------- /scripts/PrepareDevnetTfsDeployment.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding(DefaultParameterSetName="nossl")] 2 | param( 3 | [Parameter(Mandatory)] 4 | [String]$DomainName, 5 | 6 | [Parameter(Mandatory)] 7 | [String]$AdminUsername, 8 | 9 | [Parameter(Mandatory)] 10 | [SecureString]$AdminPassword, 11 | 12 | [Parameter(Mandatory)] 13 | [String]$KeyVaultResourceGroupName, 14 | 15 | [Parameter(Mandatory)] 16 | [String]$KeyVaultName, 17 | 18 | [Parameter(Mandatory=$false,ParameterSetName="ssl")] 19 | [String]$CertificatePath, 20 | 21 | [Parameter(Mandatory,ParameterSetName="ssl")] 22 | [SecureString]$CertificatePassword, 23 | 24 | [Parameter(Mandatory)] 25 | [String]$Location, 26 | 27 | [Parameter(Mandatory=$false)] 28 | [String]$OutFile = ".\azuredeploy.parameters.json" 29 | ) 30 | 31 | if (Test-Path $OutFile) { 32 | throw "Output file already exists. Please delete or rename" 33 | } 34 | 35 | #Check if the user is administrator 36 | if (-not [bool](([System.Security.Principal.WindowsIdentity]::GetCurrent()).groups -match "S-1-5-32-544")) { 37 | throw "You must have administrator priveleges to run this script." 38 | } 39 | 40 | $azcontext = Get-AzureRmContext 41 | if ([string]::IsNullOrEmpty($azcontext.Account)) { 42 | throw "User not logged into Azure." 43 | } 44 | 45 | #Some settings 46 | $DomainAdminPasswordSecretName = "DomainAdminPassword" 47 | $SslCertificateSecretName = "SslCert" 48 | 49 | $kvrg = New-AzureRmResourceGroup -Name $KeyVaultResourceGroupName -Location $Location 50 | 51 | $kv = New-AzureRmKeyVault -VaultName $KeyVaultName -Location $kvrg.Location -ResourceGroupName $KeyVaultResourceGroupName 52 | Set-AzureRmKeyVaultAccessPolicy -VaultName $KeyVaultName -EnabledForDiskEncryption -EnabledForDeployment -EnabledForTemplateDeployment 53 | 54 | #Store domain password in keyvault. 55 | $passwdsecret = Set-AzureKeyVaultSecret -VaultName $KeyVaultName -Name $DomainAdminPasswordSecretName -SecretValue $AdminPassword 56 | 57 | $secrets = @() 58 | if (-not [String]::IsNullOrEmpty($CertificatePath)) { 59 | #Upload SSL cert to keyvault 60 | $cer = Import-AzureKeyVaultCertificate -VaultName $KeyVaultName -Name $SslCertificateSecretName -FilePath $CertificatePath -Password $CertificatePassword 61 | $secret = @{ 62 | "sourceVault" = @{ 63 | "id" = $kv.ResourceId 64 | } 65 | "vaultCertificates" = @( 66 | @{ 67 | "certificateUrl" = $cer.SecretId 68 | "certificateStore" = "My" 69 | } 70 | ) 71 | } 72 | $secrets = @( $secret ) 73 | } 74 | 75 | 76 | $templateParameters = @{ 77 | 78 | "domainName" = @{ 79 | "value" = $DomainName 80 | } 81 | 82 | "adminUsername" = @{ 83 | "value" = $AdminUsername 84 | } 85 | 86 | "adminPassword" = @{ 87 | "reference" = @{ 88 | "keyvault" = @{ 89 | "id" = $kv.ResourceId 90 | } 91 | "secretName" = $DomainAdminPasswordSecretName 92 | } 93 | } 94 | } 95 | 96 | if (-not [String]::IsNullOrEmpty($CertificatePath)) { 97 | $templateParameters.Add("secrets", @{ "value" = $secrets}) 98 | $templateParameters.Add("sslThumbprint", @{ "value" = $cer.Thumbprint}) 99 | } 100 | 101 | $templateParameters | ConvertTo-Json -Depth 10 | Out-File $OutFile 102 | 103 | Write-Host "Parameters written to $OutFile." 104 | -------------------------------------------------------------------------------- /sql-alwayson/DSC/AddDatabaseAG.ps1: -------------------------------------------------------------------------------- 1 | configuration AddDatabaseAGDsc 2 | { 3 | param 4 | ( 5 | [Parameter(Mandatory)] 6 | [String]$DomainName, 7 | 8 | [String]$DomainNetbiosName=(Get-NetBIOSName -DomainName $DomainName), 9 | 10 | [Parameter(Mandatory)] 11 | [String]$SQLServerAG, 12 | 13 | [Parameter(Mandatory)] 14 | [String[]]$SQLDatabases, 15 | 16 | [Parameter(Mandatory=$false)] 17 | [String]$SQLInstanceName = "MSSQLSERVER", 18 | 19 | [Parameter(Mandatory)] 20 | [System.Management.Automation.PSCredential]$Admincreds 21 | 22 | ) 23 | 24 | Import-DscResource -ModuleName xComputerManagement, xNetworking, xStorage, SmbShare, xSMBShare, SqlServer, SqlServerDsc, PSDesiredStateConfiguration 25 | 26 | [System.Management.Automation.PSCredential]$DomainCreds = New-Object System.Management.Automation.PSCredential ("${DomainNetbiosName}\$($Admincreds.UserName)", $Admincreds.Password) 27 | 28 | 29 | Node localhost 30 | { 31 | 32 | File BackupDirectory 33 | { 34 | Ensure = "Present" 35 | Type = "Directory" 36 | DestinationPath = "F:\Backup" 37 | } 38 | 39 | 40 | xSMBShare DBBackupShare 41 | { 42 | Name = "DBBackup" 43 | Path = "F:\Backup" 44 | Ensure = "Present" 45 | FullAccess = $DomainCreds.UserName 46 | Description = "Backup share for SQL Server" 47 | DependsOn = "[File]BackupDirectory" 48 | } 49 | 50 | SqlAGDatabase AddDatabaseToAG 51 | { 52 | AvailabilityGroupName = $SQLServerAG 53 | BackupPath = "\\" + $env:COMPUTERNAME + "\DBBackup" 54 | DatabaseName = $SQLDatabases 55 | InstanceName = $SQLInstanceName 56 | ServerName = $env:COMPUTERNAME 57 | Ensure = 'Present' 58 | ProcessOnlyOnActiveNode = $true 59 | PsDscRunAsCredential = $DomainCreds 60 | DependsOn = "[xSMBShare]DBBackupShare" 61 | } 62 | 63 | } 64 | 65 | } 66 | 67 | function Get-NetBIOSName 68 | { 69 | [OutputType([string])] 70 | param( 71 | [string]$DomainName 72 | ) 73 | 74 | if ($DomainName.Contains('.')) { 75 | $length=$DomainName.IndexOf('.') 76 | if ( $length -ge 16) { 77 | $length=15 78 | } 79 | return $DomainName.Substring(0,$length) 80 | } 81 | else { 82 | if ($DomainName.Length -gt 15) { 83 | return $DomainName.Substring(0,15) 84 | } 85 | else { 86 | return $DomainName 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /sql-alwayson/DSC/AddDatabaseAG.ps1.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hansenms/iac/277b41b9f9a0ce395f577d8a74640000c91c5ce1/sql-alwayson/DSC/AddDatabaseAG.ps1.zip -------------------------------------------------------------------------------- /sql-alwayson/DSC/PrepareSQLServer.ps1.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hansenms/iac/277b41b9f9a0ce395f577d8a74640000c91c5ce1/sql-alwayson/DSC/PrepareSQLServer.ps1.zip -------------------------------------------------------------------------------- /sql-alwayson/README.md: -------------------------------------------------------------------------------- 1 | 2 | SQL Server (2017) Always On Cluster 3 | ----------------------------------- 4 | 5 | This template deploys a SQL Server (2017) Always On Cluster in an existing Virtual Network and Domain. There are several other such templates in the Azure Quickstart Templates archive, but this one has been updated with the latest PowerShell Dsc resources and uses a Cloud Witness instead of a witness VM, thus eliminating one VM from the deployment. In summary the deployment features: 6 | 7 | * SQL Server 2017 Always On 8 | * Cloud Witness (eliminating one VM) 9 | * Updated DSC scripts 10 | * Automated patching using the SQLIaaSAgent. 11 | 12 | The deployment does not install any databases on the servers. In order to add a database to the cluster, you need to: 13 | 14 | 1. Create the database 15 | 2. Do a full backup of the database 16 | 3. Use the Wizard in SSMS as described [here](https://docs.microsoft.com/en-us/sql/database-engine/availability-groups/windows/availability-group-add-a-database#SSMSProcedure) 17 | 18 | The repository includes a Powershell DSC script, called [`AddDatabaseAG.ps1`](AddDatabaseAG.ps1), which illustrates the steps needed to add a database to an availability group and there is also an [example template](adddbtoag.json) that could be used to deploy it. However, it is not possible to deploy multiple DSC script extensions to a VM and since DSC extensions were used to set up the always on cluster, it is not easy to deploy this without first deleting the previous deployment. That being said, the script is useful for capturing the steps needed and you can upload the script to the SQL VM and use it to add the database to the availabilit group. 19 | 20 | To Do List 21 | ----------- 22 | 23 | Some items that still need work. Feel free to contribute back: 24 | 25 | * Create service user (domain user) for running SQL services. 26 | * Add more than one replica. This should be a matter of making sure it is appropriately parameterized and doing some testing. 27 | * Parameterize settings 28 | * Patching schedule 29 | * Availability Group settings (synchronous, asynchronous, etc) 30 | 31 | 32 | Deploy 33 | ------ 34 | 35 | 36 | 37 | 38 | 39 | 40 | 42 | -------------------------------------------------------------------------------- /sql-alwayson/adddbtoag.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "vmName": { 6 | "type": "string" 7 | }, 8 | "domainName": { 9 | "type": "string" 10 | }, 11 | "adminUserName": { 12 | "type": "string" 13 | }, 14 | "adminPassword": { 15 | "type": "securestring" 16 | }, 17 | "sqlServerAG": { 18 | "type": "string" 19 | }, 20 | "sqlDatabases": { 21 | "type": "array" 22 | }, 23 | "sqlInstanceName": { 24 | "type": "string", 25 | "defaultValue": "MSSQLSERVER" 26 | } 27 | }, 28 | "variables": { 29 | "baseUri": "[deployment().properties.templateLink.uri]", 30 | "moduleURL": "[uri(variables('baseUri'), 'DSC/AddDatabaseAG.ps1.zip')]", 31 | "DscFunction": "AddDatabaseAG.ps1\\AddDatabaseAGDsc", 32 | }, 33 | "resources": [ 34 | { 35 | "type": "Microsoft.Compute/virtualMachines/extensions", 36 | "name": "[concat(parameters('vmName'), '/adddbtoag')]", 37 | "dependsOn": [ 38 | ], 39 | "apiVersion": "2016-03-30", 40 | "location": "[resourceGroup().location]", 41 | "properties": { 42 | "publisher": "Microsoft.Powershell", 43 | "type": "DSC", 44 | "typeHandlerVersion": "2.21", 45 | "autoUpgradeMinorVersion": true, 46 | "settings": { 47 | "modulesURL": "[variables('moduleURL')]", 48 | "configurationFunction": "[variables('DscFunction')]", 49 | "properties": { 50 | "domainName": "[parameters('domainName')]", 51 | "adminCreds": { 52 | "userName": "[parameters('adminUserName')]", 53 | "password": "privateSettingsRef:adminPassword" 54 | }, 55 | "SQLServerAG": "[parameters('sqlServerAG')]", 56 | "SQLDatabases": "[parameters('sqlDatabases')]", 57 | "SQLInstanceName": "[parameters('sqlInstanceName')]" 58 | } 59 | }, 60 | "protectedSettings": { 61 | "items": { 62 | "adminPassword": "[parameters('adminPassword')]" 63 | } 64 | } 65 | } 66 | } 67 | ] 68 | } -------------------------------------------------------------------------------- /sql-alwayson/azuredeploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "subnetId": { 6 | "type": "string" 7 | }, 8 | "nodeIpAddresses": { 9 | "type": "array", 10 | "defaultValue": [ 11 | "10.0.1.10", 12 | "10.0.1.11" 13 | ] 14 | }, 15 | "clusterIpAddress": { 16 | "type": "string", 17 | "defaultValue": "10.0.1.30" 18 | }, 19 | "adminUsername": { 20 | "type": "string", 21 | "defaultValue": "EnterpriseAdmin" 22 | }, 23 | "adminPassword": { 24 | "type": "securestring" 25 | }, 26 | "domainName": { 27 | "type": "string", 28 | "defaultValue": "devnet.contoso.us" 29 | }, 30 | "sqlVmSize": { 31 | "type": "string", 32 | "allowedValues": [ 33 | "Standard_D1_v2", 34 | "Standard_D2_v2", 35 | "Standard_D3_v2", 36 | "Standard_D4_v2", 37 | "Standard_D5_v2", 38 | "Standard_D11_v2", 39 | "Standard_D12_v2", 40 | "Standard_D13_v2", 41 | "Standard_D14_v2", 42 | "Standard_D15_v2", 43 | "Standard_DS1_v2", 44 | "Standard_DS2_v2", 45 | "Standard_DS3_v2", 46 | "Standard_DS4_v2", 47 | "Standard_DS5_v2", 48 | "Standard_DS11_v2", 49 | "Standard_DS12_v2", 50 | "Standard_DS13_v2", 51 | "Standard_DS14_v2", 52 | "Standard_DS15_v2" 53 | ], 54 | "defaultValue": "Standard_DS2_v2" 55 | }, 56 | "sqlVmImagePublisher": { 57 | "type": "string", 58 | "defaultValue": "MicrosoftSqlServer" 59 | }, 60 | "sqlVmImageOffer": { 61 | "type": "string", 62 | "defaultValue": "SQL2017-WS2016" 63 | }, 64 | "sqlVmImageSku": { 65 | "type": "string", 66 | "defaultValue": "Enterprise" 67 | } 68 | }, 69 | "variables": { 70 | "diagStorageAccountName": "[concat(uniquestring(resourceGroup().id), 'sqlvmdiag')]", 71 | "cloudWitnessStorageAccountName": "[concat(uniquestring(resourceGroup().id), 'sqlw')]", 72 | "HASetName": "SqlHASet", 73 | "SqlVmPrefix": "SQL", 74 | "baseUri": "[deployment().properties.templateLink.uri]", 75 | "SQLPrepareModuleURL": "[uri(variables('baseUri'), 'DSC/PrepareSQLServer.ps1.zip')]", 76 | "SQLPrepareFunction": "PrepareSQLServer.ps1\\SQLServerPrepareDsc", 77 | "windowsVmTemplateURL": "[uri(variables('baseUri'),'../primitives/windowsvm.json')]", 78 | "LBName": "SQLLB", 79 | "LBBEName": "[concat(variables('LBName'),'BE')]", 80 | "LBFEName": "[concat(variables('LBName'),'FE')]", 81 | "LBProbeName": "[concat(variables('LBName'),'Probe')]", 82 | "sqlBEAddressPoolID": "[concat(resourceId('Microsoft.Network/loadBalancers',variables('LBName')),'/backendAddressPools/',variables('LBBEName'))]", 83 | "sqllbFEConfigID": "[concat(resourceId('Microsoft.Network/loadBalancers',variables('LBName')),'/frontendIPConfigurations/',variables('LBFEName'))]", 84 | "sqllbProbeID": "[concat(resourceId('Microsoft.Network/loadBalancers',variables('LBName')),'/probes/',variables('LBProbeName'))]" 85 | }, 86 | "resources": [ 87 | { 88 | "type": "Microsoft.Storage/storageAccounts", 89 | "name": "[variables('diagStorageAccountName')]", 90 | "apiVersion": "2016-01-01", 91 | "location": "[resourceGroup().location]", 92 | "sku": { 93 | "name": "Standard_LRS" 94 | }, 95 | "kind": "Storage", 96 | "properties": {} 97 | }, 98 | { 99 | "type": "Microsoft.Storage/storageAccounts", 100 | "name": "[variables('cloudWitnessStorageAccountName')]", 101 | "apiVersion": "2016-01-01", 102 | "location": "[resourceGroup().location]", 103 | "sku": { 104 | "name": "Standard_LRS" 105 | }, 106 | "kind": "Storage", 107 | "properties": {} 108 | }, 109 | { 110 | "name": "[variables('HASetName')]", 111 | "type": "Microsoft.Compute/availabilitySets", 112 | "location": "[resourceGroup().location]", 113 | "apiVersion": "2017-03-30", 114 | "dependsOn": [], 115 | "properties": { 116 | "platformUpdateDomainCount": 2, 117 | "platformFaultDomainCount": 2 118 | }, 119 | "sku": { 120 | "name": "Aligned" 121 | } 122 | }, 123 | { 124 | "name": "[variables('LBName')]", 125 | "type": "Microsoft.Network/loadBalancers", 126 | "apiVersion": "2017-10-01", 127 | "location": "[resourceGroup().location]", 128 | "properties": { 129 | "frontendIPConfigurations": [ 130 | { 131 | "name": "[variables('LBFEName')]", 132 | "properties": { 133 | "privateIPAddress": "[parameters('clusterIpAddress')]", 134 | "privateIPAllocationMethod": "Static", 135 | "subnet": { 136 | "id": "[parameters('subnetId')]" 137 | } 138 | } 139 | } 140 | ], 141 | "backendAddressPools": [ 142 | { 143 | "name": "[variables('LBBEName')]" 144 | } 145 | ], 146 | "loadBalancingRules": [ 147 | { 148 | "name": "SQLLBRule", 149 | "properties": { 150 | "frontendIPConfiguration": { 151 | "id": "[variables('sqllbFEConfigID')]" 152 | }, 153 | "backendAddressPool": { 154 | "id": "[variables('sqlBEAddressPoolID')]" 155 | }, 156 | "probe": { 157 | "id": "[variables('sqllbProbeID')]" 158 | }, 159 | "frontendPort": 1433, 160 | "backendPort": 1433, 161 | "enableFloatingIP": true, 162 | "idleTimeoutInMinutes": 4, 163 | "protocol": "Tcp", 164 | "loadDistribution": "Default" 165 | } 166 | } 167 | ], 168 | "probes": [ 169 | { 170 | "name": "[variables('LBProbeName')]", 171 | "properties": { 172 | "protocol": "Tcp", 173 | "port": 59999, 174 | "intervalInSeconds": 5, 175 | "numberOfProbes": 2 176 | } 177 | } 178 | ] 179 | } 180 | }, 181 | { 182 | "name": "[concat('SqlVmDeploy', copyindex())]", 183 | "type": "Microsoft.Resources/deployments", 184 | "apiVersion": "2017-05-10", 185 | "dependsOn": [ 186 | "[resourceId('Microsoft.Storage/storageAccounts', variables('diagStorageAccountName'))]", 187 | "[resourceId('Microsoft.Network/loadBalancers', variables('LBName'))]" 188 | ], 189 | "copy": { 190 | "name": "sqlvmloop", 191 | "count": 2 192 | }, 193 | "properties": { 194 | "mode": "Incremental", 195 | "templateLink": { 196 | "uri": "[variables('windowsVmTemplateURL')]", 197 | "contentVersion": "1.0.0.0" 198 | }, 199 | "parameters": { 200 | "vmName": { 201 | "value": "[concat(variables('SqlVmPrefix'), copyindex())]" 202 | }, 203 | "vmSize": { 204 | "value": "[parameters('sqlVmSize')]" 205 | }, 206 | "subnetId": { 207 | "value": "[parameters('subnetId')]" 208 | }, 209 | "fixedPrivateIp": { 210 | "value": "[parameters('nodeIpAddresses')[copyIndex()]]" 211 | }, 212 | "adminUsername": { 213 | "value": "[parameters('adminUsername')]" 214 | }, 215 | "adminPassword": { 216 | "value": "[parameters('adminPassword')]" 217 | }, 218 | "diagStorageAccountId": { 219 | "value": "[resourceId('Microsoft.Storage/storageAccounts', variables('diagStorageAccountName'))]" 220 | }, 221 | "availabilitySetId": { 222 | "value": "[resourceId('Microsoft.Compute/availabilitySets', variables('HASetName'))]" 223 | }, 224 | "imagePublisher": { 225 | "value": "[parameters('sqlVmImagePublisher')]" 226 | }, 227 | "imageOffer": { 228 | "value": "[parameters('sqlVmImageOffer')]" 229 | }, 230 | "imageSku": { 231 | "value": "[parameters('sqlVmImageSku')]" 232 | }, 233 | "LoadBalancerBEId": { 234 | "value": "[variables('sqlBEAddressPoolID')]" 235 | } 236 | } 237 | } 238 | }, 239 | { 240 | "apiVersion": "2015-06-15", 241 | "type": "Microsoft.Compute/virtualMachines/extensions", 242 | "name": "[concat(variables('SqlVmPrefix'), copyindex(), '/SqlIaasExtension')]", 243 | "location": "[resourceGroup().location]", 244 | "dependsOn": [ 245 | "[resourceId('Microsoft.Resources/deployments', concat('SqlVmDeploy', copyindex()))]" 246 | ], 247 | "copy": { 248 | "name": "sqliaasextensionsloop", 249 | "count": 2 250 | }, 251 | "properties": { 252 | "type": "SqlIaaSAgent", 253 | "publisher": "Microsoft.SqlServer.Management", 254 | "typeHandlerVersion": "1.2", 255 | "autoUpgradeMinorVersion": "true", 256 | "settings": { 257 | "AutoTelemetrySettings": { 258 | "Region": "[resourceGroup().location]" 259 | }, 260 | "AutoPatchingSettings": { 261 | "PatchCategory": "WindowsMandatoryUpdates", 262 | "Enable": true, 263 | "DayOfWeek": "Sunday", 264 | "MaintenanceWindowStartingHour": "2", 265 | "MaintenanceWindowDuration": "60" 266 | }, 267 | "KeyVaultCredentialSettings": { 268 | "Enable": false, 269 | "CredentialName": "" 270 | }, 271 | "ServerConfigurationsManagementSettings": { 272 | "SQLConnectivityUpdateSettings": { 273 | "ConnectivityType": "Private", 274 | "Port": "1433" 275 | }, 276 | "SQLWorkloadTypeUpdateSettings": { 277 | "SQLWorkloadType": "GENERAL" 278 | }, 279 | "SQLStorageUpdateSettings": { 280 | "DiskCount": "1", 281 | "NumberOfColumns": "1", 282 | "StartingDeviceID": "2", 283 | "DiskConfigurationType": "NEW" 284 | }, 285 | "AdditionalFeaturesServerConfigurations": { 286 | "IsRServicesEnabled": "false" 287 | } 288 | } 289 | }, 290 | "protectedSettings": {} 291 | } 292 | }, 293 | { 294 | "apiVersion": "2015-06-15", 295 | "type": "Microsoft.Compute/virtualMachines/extensions", 296 | "name": "[concat(variables('SqlVmPrefix'), copyindex(), '/joindomain')]", 297 | "location": "[resourceGroup().location]", 298 | "dependsOn": [ 299 | "[concat(resourceId('Microsoft.Compute/virtualMachines', concat(variables('SqlVmPrefix'), copyindex())), '/extensions/SqlIaasExtension')]" 300 | ], 301 | "copy": { 302 | "name": "sqlvmjoinloop", 303 | "count": 2 304 | }, 305 | "properties": { 306 | "publisher": "Microsoft.Compute", 307 | "type": "JsonADDomainExtension", 308 | "typeHandlerVersion": "1.3", 309 | "autoUpgradeMinorVersion": true, 310 | "settings": { 311 | "Name": "[parameters('domainName')]", 312 | "OUPath": "", 313 | "User": "[concat(parameters('domainName'), '\\', parameters('adminUserName'))]", 314 | "Restart": "true", 315 | "Options": "3" 316 | }, 317 | "protectedSettings": { 318 | "Password": "[parameters('adminPassword')]" 319 | } 320 | } 321 | }, 322 | { 323 | "type": "Microsoft.Compute/virtualMachines/extensions", 324 | "name": "[concat(variables('SqlVmPrefix'), copyindex(), '/configuresql', copyindex())]", 325 | "dependsOn": [ 326 | "[concat(resourceId('Microsoft.Compute/virtualMachines', concat(variables('SqlVmPrefix'), copyindex())), '/extensions/joindomain')]", 327 | "[resourceId('Microsoft.Storage/storageAccounts', variables('diagStorageAccountName'))]" 328 | ], 329 | "apiVersion": "2016-03-30", 330 | "copy": { 331 | "name": "sqlconfigloop", 332 | "count": 2, 333 | "mode": "Serial", 334 | "batchSize": 1 335 | }, 336 | "location": "[resourceGroup().location]", 337 | "properties": { 338 | "publisher": "Microsoft.Powershell", 339 | "type": "DSC", 340 | "typeHandlerVersion": "2.21", 341 | "autoUpgradeMinorVersion": true, 342 | "settings": { 343 | "modulesURL": "[variables('SQLPrepareModuleURL')]", 344 | "configurationFunction": "[variables('SQLPrepareFunction')]", 345 | "properties": { 346 | "domainName": "[parameters('domainName')]", 347 | "adminCreds": { 348 | "userName": "[parameters('adminUserName')]", 349 | "password": "privateSettingsRef:adminPassword" 350 | }, 351 | "ClusterName": "SQLClusterAG", 352 | "ClusterOwnerNode": "[concat(variables('SqlVmPrefix'), '0')]", 353 | "ClusterIP": "[parameters('clusterIpAddress')]", 354 | "witnessStorageBlobEndpoint": "[reference(variables('cloudWitnessStorageAccountName'), '2016-01-01').primaryEndpoints.blob]", 355 | "witnessStorageAccountKey": "[listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('cloudWitnessStorageAccountName')), providers('Microsoft.Storage', 'storageAccounts').apiVersions[0]).keys[0].value]" 356 | } 357 | }, 358 | "protectedSettings": { 359 | "items": { 360 | "adminPassword": "[parameters('adminPassword')]" 361 | } 362 | } 363 | } 364 | } 365 | ], 366 | "outputs": {} 367 | } -------------------------------------------------------------------------------- /tfs-ha/DSC/InstallTFS.ps1: -------------------------------------------------------------------------------- 1 | configuration TFSInstallDsc 2 | { 3 | param 4 | ( 5 | [Parameter(Mandatory)] 6 | [string]$DomainName, 7 | 8 | [Parameter(Mandatory)] 9 | [System.Management.Automation.PSCredential]$Admincreds, 10 | 11 | [Parameter(Mandatory)] 12 | [String]$SqlServerInstance, 13 | 14 | [Parameter(Mandatory)] 15 | [String]$primaryInstance, 16 | 17 | [Parameter(Mandatory=$true)] 18 | [String]$GlobalSiteIP, 19 | 20 | [Parameter(Mandatory=$false)] 21 | [String]$GlobalSiteName = "TFS", 22 | 23 | [Parameter(Mandatory=$false)] 24 | [String]$DnsServer = "DC1", 25 | 26 | [Parameter(Mandatory=$false)] 27 | [String]$ProbePort = '59999', 28 | 29 | [Parameter(Mandatory=$false)] 30 | [String]$SslThumbprint = "generate", 31 | 32 | [Parameter(Mandatory=$false)] 33 | [ValidateSet("TFS2018", "TFS2017Update3","TFS2017Update2")] 34 | [String]$TFSVersion = "TFS2018" 35 | ) 36 | 37 | [System.Management.Automation.PSCredential ]$DomainCreds = New-Object System.Management.Automation.PSCredential ("${DomainName}\$($Admincreds.UserName)", $Admincreds.Password) 38 | 39 | Import-DscResource -ModuleName xStorage, xPendingReboot, xDnsServer, xWebAdministration, xNetworking, 'PSDesiredStateConfiguration' 40 | 41 | <# 42 | Download links for TFS: 43 | 44 | 2017Update2: https://go.microsoft.com/fwlink/?LinkId=850949 45 | 2017Update3: https://go.microsoft.com/fwlink/?LinkId=857134 46 | 2018: https://go.microsoft.com/fwlink/?LinkId=856344 47 | #> 48 | 49 | $TFSDownloadLinks = @{ 50 | "TFS2018" = "https://go.microsoft.com/fwlink/?LinkId=856344" 51 | "TFS2017Update2" = "https://go.microsoft.com/fwlink/?LinkId=850949" 52 | "TFS2017Update3" = "https://go.microsoft.com/fwlink/?LinkId=857134" 53 | } 54 | 55 | $currentDownloadLink = $TFSDownloadLinks[$TFSVersion] 56 | $installerDownload = $env:TEMP + "\tfs_installer.exe" 57 | $isTFS2017 = $false 58 | $hostName = $env:COMPUTERNAME 59 | 60 | $isPrimaryInstance = $primaryInstance -eq $env:COMPUTERNAME 61 | 62 | if ($TFSVersion.Substring(0,7) -eq "TFS2017") { 63 | $isTFS2017 = $true 64 | } 65 | 66 | $TfsConfigExe = "C:\Program Files\Microsoft Team Foundation Server 2018\Tools\TfsConfig.exe" 67 | 68 | if ($isTFS2017) { 69 | $TfsConfigExe = "C:\Program Files\Microsoft Team Foundation Server 15.0\Tools\TfsConfig.exe" 70 | } 71 | 72 | Node localhost 73 | { 74 | 75 | WindowsFeature ADPS 76 | { 77 | Name = "RSAT-AD-PowerShell" 78 | Ensure = "Present" 79 | } 80 | 81 | 82 | WindowsFeature DNSServer 83 | { 84 | Name = "RSAT-DNS-Server" 85 | Ensure = "Present" 86 | DependsOn = "[WindowsFeature]ADPS" 87 | } 88 | 89 | WindowsFeature IISPresent 90 | { 91 | Ensure = "Present" 92 | Name = "Web-Server" 93 | } 94 | 95 | xWebsite DefaultSite 96 | { 97 | Ensure = 'Present' 98 | Name = 'Default Web Site' 99 | State = 'Stopped' 100 | PhysicalPath = 'C:\inetpub\wwwroot' 101 | DependsOn = '[WindowsFeature]IISPresent' 102 | } 103 | 104 | xWaitforDisk Disk2 105 | { 106 | DiskId = 2 107 | RetryIntervalSec =$RetryIntervalSec 108 | RetryCount = $RetryCount 109 | DependsOn = "[WindowsFeature]DNSServer" 110 | } 111 | 112 | xDisk ADDataDisk 113 | { 114 | DiskId = 2 115 | DriveLetter = "F" 116 | DependsOn = "[xWaitForDisk]Disk2" 117 | } 118 | 119 | Script DownloadTFS 120 | { 121 | GetScript = { 122 | return @{ 'Result' = $true } 123 | } 124 | SetScript = { 125 | Write-Host "Downloading TFS: " + $using:currentDownloadLink 126 | Invoke-WebRequest -Uri $using:currentDownloadLink -OutFile $using:installerDownload 127 | } 128 | TestScript = { 129 | Test-Path $using:installerDownload 130 | } 131 | DependsOn = "[xDisk]ADDataDisk" 132 | } 133 | 134 | Script InstallTFS 135 | { 136 | GetScript = { 137 | return @{ 'Result' = $true } 138 | } 139 | SetScript = { 140 | Write-Verbose "Install TFS..." 141 | 142 | $cmd = $using:installerDownload + " /full /quiet /Log $env:TEMP\tfs_install_log.txt" 143 | Write-Verbose "Command to run: $cmd" 144 | Invoke-Expression $cmd | Write-Verbose 145 | 146 | 147 | #Sleep for 10 seconds to make sure installer is going 148 | Start-Sleep -s 10 149 | 150 | #The tfs installer will per default run in the background. We will wait for it. 151 | Wait-Process -Name "tfs_installer" 152 | } 153 | TestScript = { 154 | Test-Path $using:TfsConfigExe 155 | } 156 | DependsOn = "[Script]DownloadTFS" 157 | } 158 | 159 | 160 | xPendingReboot PostInstallReboot { 161 | Name = "Check for a pending reboot before changing anything" 162 | DependsOn = "[Script]InstallTFS" 163 | } 164 | 165 | LocalConfigurationManager{ 166 | RebootNodeIfNeeded = $True 167 | } 168 | 169 | Script ConfigureTFS 170 | { 171 | GetScript = { 172 | return @{ 'Result' = $true } 173 | } 174 | SetScript = { 175 | $siteBindings = "https:*:443:" + $using:hostName + "." + $using:DomainName + ":My:" + $using:SslThumbprint 176 | 177 | if ($using:hostName -ne $using:GlobalSiteName) { 178 | $siteBindings += ",https:*:443:" + $using:GlobalSiteName + "." + $using:DomainName + ":My:" + $using:SslThumbprint 179 | } 180 | 181 | $siteBindings += ",http:*:80:" 182 | 183 | $publicUrl = "http://$using:hostName" 184 | 185 | $cmd = "" 186 | if ($using:isPrimaryInstance) { 187 | $cmd = "& '$using:TfsConfigExe' unattend /configure /continue /type:NewServerAdvanced /inputs:WebSiteVDirName=';'PublicUrl=$publicUrl';'SqlInstance=$using:SqlServerInstance';'SiteBindings='$siteBindings'" 188 | } else { 189 | $cmd = "& '$using:TfsConfigExe' unattend /configure /continue /type:ApplicationTierOnlyAdvanced /inputs:WebSiteVDirName=';'PublicUrl=$publicUrl';'SqlInstance=$using:SqlServerInstance';'SiteBindings='$siteBindings'" 190 | } 191 | 192 | Write-Verbose "$cmd" 193 | Invoke-Expression $cmd | Write-Verbose 194 | 195 | $publicUrl = "https://$using:GlobalSiteName" + "." + $using:DomainName 196 | $cmd = "& '$using:TfsConfigExe' settings /publicUrl:$publicUrl" 197 | Write-Verbose "$cmd" 198 | Invoke-Expression $cmd | Write-Verbose 199 | 200 | } 201 | TestScript = { 202 | $sites = Get-WebBinding | Where-Object {$_.bindingInformation -like "*$using:GlobalSiteName*" } 203 | -not [String]::IsNullOrEmpty($sites) 204 | } 205 | DependsOn = "[xPendingReboot]PostInstallReboot","[xWebsite]DefaultSite" 206 | PsDscRunAsCredential = $DomainCreds 207 | } 208 | 209 | xDnsRecord GlobalDNS 210 | { 211 | Name = $GlobalSiteName 212 | Target = $GlobalSiteIP 213 | Type = "ARecord" 214 | Zone = $DomainName 215 | DependsOn = "[Script]ConfigureTFS" 216 | DnsServer = $DnsServer 217 | PsDscRunAsCredential = $DomainCreds 218 | } 219 | 220 | xWebsite ProbeWebSite 221 | { 222 | Ensure = 'Present' 223 | Name = 'Probe Web Site' 224 | State = 'Started' 225 | PhysicalPath = 'C:\inetpub\wwwroot' 226 | BindingInfo = MSFT_xWebBindingInformation 227 | { 228 | Protocol = 'http' 229 | Port = $ProbePort 230 | IPAddress = '*' 231 | } 232 | DependsOn = '[xDnsRecord]GlobalDNS' 233 | } 234 | 235 | xFirewall DatabaseEngineFirewallRule 236 | { 237 | Direction = "Inbound" 238 | Name = "IIS Probe" 239 | DisplayName = "IIS Probe" 240 | Group = "TFS" 241 | Enabled = "True" 242 | Protocol = "TCP" 243 | LocalPort = $ProbePort 244 | Ensure = "Present" 245 | DependsOn = '[xWebsite]ProbeWebSite' 246 | } 247 | 248 | Script Reboot 249 | { 250 | TestScript = { 251 | return (Test-Path HKLM:\SOFTWARE\MyMainKey\RebootKey) 252 | } 253 | SetScript = { 254 | New-Item -Path HKLM:\SOFTWARE\MyMainKey\RebootKey -Force 255 | $global:DSCMachineStatus = 1 256 | 257 | } 258 | GetScript = { return @{result = 'result'}} 259 | DependsOn = '[xFirewall]DatabaseEngineFirewallRule' 260 | } 261 | 262 | xPendingReboot PostConfigReboot { 263 | Name = "Check for a pending reboot before changing anything" 264 | DependsOn = "[Script]Reboot" 265 | } 266 | } 267 | } -------------------------------------------------------------------------------- /tfs-ha/DSC/InstallTFS.ps1.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hansenms/iac/277b41b9f9a0ce395f577d8a74640000c91c5ce1/tfs-ha/DSC/InstallTFS.ps1.zip -------------------------------------------------------------------------------- /tfs-ha/DSC/InstallVSTSAgent.ps1: -------------------------------------------------------------------------------- 1 | configuration VSTSAgentInstallDsc 2 | { 3 | param 4 | ( 5 | [Parameter(Mandatory)] 6 | [string]$DomainName, 7 | 8 | [Parameter(Mandatory)] 9 | [System.Management.Automation.PSCredential]$Admincreds, 10 | 11 | [Parameter(Mandatory=$false)] 12 | [String]$TFSUrl = "https://TFS." + $DomainName, 13 | 14 | [Parameter(Mandatory=$false)] 15 | [String]$VSTSAgentUrl = "https://vstsagentpackage.azureedge.net/agent/2.127.0/vsts-agent-win-x64-2.127.0.zip" 16 | ) 17 | 18 | [System.Management.Automation.PSCredential ]$DomainCreds = New-Object System.Management.Automation.PSCredential ("${DomainName}\$($Admincreds.UserName)", $Admincreds.Password) 19 | 20 | Import-DscResource -ModuleName 'PSDesiredStateConfiguration' 21 | 22 | Node localhost 23 | { 24 | Script DownloadAgent 25 | { 26 | GetScript = { 27 | return @{ 'Result' = $true } 28 | } 29 | SetScript = { 30 | $agentUrl = $using:VSTSAgentUrl 31 | $agentZip = "$env:TEMP" + "\vsts_agent.zip" 32 | Write-Host "Downloading TFS: $agentUrl" 33 | Invoke-WebRequest -Uri $agentUrl -OutFile $agentZip 34 | } 35 | TestScript = { 36 | $agentZip = "$env:TEMP" + "\vsts_agent.zip" 37 | Test-Path $agentZip 38 | } 39 | } 40 | 41 | 42 | Script UnzipAgent 43 | { 44 | GetScript = { 45 | return @{ 'Result' = $true } 46 | } 47 | SetScript = { 48 | $agentZip = "$env:TEMP" + "\vsts_agent.zip" 49 | $agentPath = "C:\agent" 50 | If(!(Test-Path $agentPath)) 51 | { 52 | New-Item -ItemType Directory -Force -Path $agentPath 53 | } 54 | 55 | $shell = New-Object -com shell.application 56 | $zip = $shell.NameSpace($agentZip) 57 | Foreach($item in $zip.items()) 58 | { 59 | $shell.Namespace($agentPath).copyhere($item) 60 | } 61 | } 62 | TestScript = { 63 | Test-Path "C:\agent\config.cmd" 64 | } 65 | DependsOn = "[Script]DownloadAgent" 66 | } 67 | 68 | 69 | Script ConfigAgent 70 | { 71 | GetScript = { return @{ 'Result' = $true }} 72 | SetScript = { 73 | Set-Location "C:\agent" 74 | $tfsurl = $using:TFSUrl 75 | $cmd = ".\config.cmd --unattended --runAsService --work _work --url $tfsurl --auth integrated --pool default --agent $env:COMPUTERNAME" 76 | Invoke-Expression $cmd | Write-Verbose 77 | } 78 | TestScript = { 79 | Test-Path "C:\agent\.credentials" 80 | } 81 | DependsOn = "[Script]UnzipAgent" 82 | PsDscRunAsCredential = $DomainCreds 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /tfs-ha/DSC/InstallVSTSAgent.ps1.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hansenms/iac/277b41b9f9a0ce395f577d8a74640000c91c5ce1/tfs-ha/DSC/InstallVSTSAgent.ps1.zip -------------------------------------------------------------------------------- /tfs-ha/README.md: -------------------------------------------------------------------------------- 1 | High Availability Team Foundation Server 2 | ---------------------------------------- 3 | 4 | This folder contains two templates, [`azuredeploy.json`](azuredeploy.json) will deploy a high-availability version of the Team Foundation Server and [`deployagents.json`](deployagents.json) can be used to deploy build agents. The templates assume that you already have established some network infrastructure with domain controller(s). You can use the [core-network](../core-network) template to deploy that. You also need a SQL Server, for the high-availability deployment, you can use the [sql-alwayson](../sql-alwayson) template. 5 | 6 | 7 | Since TFS establishes a web endpoint on the TFS servers, it is recommended that you secure that with SSL certificates. If you do not provide a certificate, a self-signed one will be generated. You can replace it after installation too. The way to provide it during deployment is through the `secrets` and `sslThumbprint` parameters. The `secrets` parameter is used to provide an array of certificates from [Azure Key Vault](https://azure.microsoft.com/en-us/services/key-vault/). The `sslThumbprint` parameter instructs the TFS installation script on which certificate to install. The repository includes a [convenience script](../scripts/PrepareDevnetTfsDeployment.ps1), which you can use to create a key vault to hold domain admin password and the SSL certificate. This script will create the key vault, upload all secrets, and generate a deployment parameters file with the details needed for deployment. 8 | 9 | If you are looking for a complete template deployment of networking, SQL server, TFS, and agents, please use the [devnet-tfs-ha](../devnet-tfs-ha) template. 10 | 11 | Deploy 12 | ------ 13 | 14 | Deploy this solution to either Azure Commercial or Azure Government: 15 | 16 | 17 | 18 | 19 | 20 | 21 | 23 | 24 | Post Deployment Steps 25 | ---------------------- 26 | 27 | * Add databases to availability group 28 | * Log into the first database node in the SQL cluster. 29 | * Start SQL Server Management Studio (SSMS). 30 | * Backup the databases "Tfs_Configuration" and "Tfs_DefaultCollection" 31 | * Make a share that is accessible from all database nodes in the cluster, e.g. `F:\Backup` 32 | * In SSMS find "Availability Databases" by expanded a few levels under "Always On High Availability", right-click and choose "Add Databases" 33 | * Add the two TFS databases and follow the wizard. Choose "Full Database and Log Backup" option and point to the share you created. 34 | * Run the Wizard. 35 | * Install build agents. 36 | *Agents are not included in the base template, but you can use the template below if you would just like generic Windows agents. 37 | 38 | Deploy Build Agents 39 | ------------------- 40 | 41 | Deploying build agents is not part of the high availability template itself, since they are likely to require specific configuration related to the workloads they are to support. However, you can use the `deployagents.json` template as inspiration or if you simply want some Windows build agents with Visual Studio installed, deploy build agents: 42 | 43 | 44 | 45 | 46 | 47 | 48 | 50 | 51 | _Note_ : If you are experiencing problems with registering build agents. Specifically, an error message like `TF400813: Resource not available for anonymous access. Client authentication required.`, reboot the TFS nodes and try again. -------------------------------------------------------------------------------- /tfs-ha/azuredeploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "subnetId": { 6 | "type": "string" 7 | }, 8 | "adminPassword": { 9 | "type": "securestring" 10 | }, 11 | "adminUsername": { 12 | "type": "string", 13 | "defaultValue": "EnterpriseAdmin" 14 | }, 15 | "sqlServerName": { 16 | "type": "string", 17 | "defaultValue": "SQLClusterAG" 18 | }, 19 | "domainName": { 20 | "type": "string", 21 | "defaultValue": "devnet.contoso.us" 22 | }, 23 | "vmSize": { 24 | "type": "string", 25 | "allowedValues": [ 26 | "Standard_D1_v2", 27 | "Standard_D2_v2", 28 | "Standard_D3_v2", 29 | "Standard_D4_v2", 30 | "Standard_D5_v2", 31 | "Standard_D11_v2", 32 | "Standard_D12_v2", 33 | "Standard_D13_v2", 34 | "Standard_D14_v2", 35 | "Standard_D15_v2", 36 | "Standard_DS1_v2", 37 | "Standard_DS2_v2", 38 | "Standard_DS3_v2", 39 | "Standard_DS4_v2", 40 | "Standard_DS5_v2", 41 | "Standard_DS11_v2", 42 | "Standard_DS12_v2", 43 | "Standard_DS13_v2", 44 | "Standard_DS14_v2", 45 | "Standard_DS15_v2" 46 | ], 47 | "defaultValue": "Standard_DS2_v2" 48 | }, 49 | "imageSku": { 50 | "type": "string", 51 | "defaultValue": "2016-Datacenter" 52 | }, 53 | "tfsVmInstanceCount": { 54 | "type": "int", 55 | "defaultValue": 2 56 | }, 57 | "LBIpAddress": { 58 | "type": "string", 59 | "defaultValue": "10.0.1.40" 60 | }, 61 | "secrets": { 62 | "type": "array", 63 | "defaultValue": [] 64 | }, 65 | "sslThumbprint": { 66 | "type": "string", 67 | "defaultValue": "generate" 68 | } 69 | }, 70 | "variables": { 71 | "baseUri": "[deployment().properties.templateLink.uri]", 72 | "TFSInstallModuleURL": "[uri(variables('baseUri'), 'DSC/InstallTFS.ps1.zip')]", 73 | "TFSInstallFunction": "InstallTFS.ps1\\TFSInstallDsc", 74 | "windowsVmTemplateURL": "[uri(variables('baseUri'),'../primitives/windowsvm.json')]", 75 | "diagStorageAccountName": "[concat(uniquestring(resourceGroup().id), 'tfsvmdiag')]", 76 | "HASetName": "TFSHASet", 77 | "tfsVMPrefix": "TFS", 78 | "LBName": "TFSLB", 79 | "LBBEName": "[concat(variables('LBName'),'BE')]", 80 | "LBFEName": "[concat(variables('LBName'),'FE')]", 81 | "LBProbeName": "[concat(variables('LBName'),'Probe')]", 82 | "LBBEAddressPoolID": "[concat(resourceId('Microsoft.Network/loadBalancers',variables('LBName')),'/backendAddressPools/',variables('LBBEName'))]", 83 | "LBFEConfigID": "[concat(resourceId('Microsoft.Network/loadBalancers',variables('LBName')),'/frontendIPConfigurations/',variables('LBFEName'))]", 84 | "LBProbeID": "[concat(resourceId('Microsoft.Network/loadBalancers',variables('LBName')),'/probes/',variables('LBProbeName'))]", 85 | "globalSiteName": "TFS" 86 | }, 87 | "resources": [ 88 | { 89 | "type": "Microsoft.Storage/storageAccounts", 90 | "name": "[variables('diagStorageAccountName')]", 91 | "apiVersion": "2016-01-01", 92 | "location": "[resourceGroup().location]", 93 | "sku": { 94 | "name": "Standard_LRS" 95 | }, 96 | "kind": "Storage", 97 | "properties": {} 98 | }, 99 | { 100 | "name": "[variables('HASetName')]", 101 | "type": "Microsoft.Compute/availabilitySets", 102 | "location": "[resourceGroup().location]", 103 | "apiVersion": "2017-03-30", 104 | "dependsOn": [], 105 | "properties": { 106 | "platformUpdateDomainCount": 2, 107 | "platformFaultDomainCount": 2 108 | }, 109 | "sku": { 110 | "name": "Aligned" 111 | } 112 | }, 113 | { 114 | "name": "[variables('LBName')]", 115 | "type": "Microsoft.Network/loadBalancers", 116 | "apiVersion": "2017-10-01", 117 | "location": "[resourceGroup().location]", 118 | "properties": { 119 | "frontendIPConfigurations": [ 120 | { 121 | "name": "[variables('LBFEName')]", 122 | "properties": { 123 | "privateIPAddress": "[parameters('LBIpAddress')]", 124 | "privateIPAllocationMethod": "Static", 125 | "subnet": { 126 | "id": "[parameters('subnetId')]" 127 | } 128 | } 129 | } 130 | ], 131 | "backendAddressPools": [ 132 | { 133 | "name": "[variables('LBBEName')]" 134 | } 135 | ], 136 | "loadBalancingRules": [ 137 | { 138 | "name": "LBRule1", 139 | "properties": { 140 | "frontendIPConfiguration": { 141 | "id": "[variables('LBFEConfigID')]" 142 | }, 143 | "backendAddressPool": { 144 | "id": "[variables('LBBEAddressPoolID')]" 145 | }, 146 | "probe": { 147 | "id": "[variables('LBProbeID')]" 148 | }, 149 | "frontendPort": 80, 150 | "backendPort": 80, 151 | "idleTimeoutInMinutes": 4, 152 | "protocol": "Tcp", 153 | "loadDistribution": "SourceIP" 154 | } 155 | }, 156 | { 157 | "name": "LBRule2", 158 | "properties": { 159 | "frontendIPConfiguration": { 160 | "id": "[variables('LBFEConfigID')]" 161 | }, 162 | "backendAddressPool": { 163 | "id": "[variables('LBBEAddressPoolID')]" 164 | }, 165 | "probe": { 166 | "id": "[variables('LBProbeID')]" 167 | }, 168 | "frontendPort": 443, 169 | "backendPort": 443, 170 | "idleTimeoutInMinutes": 4, 171 | "protocol": "Tcp", 172 | "loadDistribution": "SourceIP" 173 | } 174 | } 175 | ], 176 | "probes": [ 177 | { 178 | "name": "[variables('LBProbeName')]", 179 | "properties": { 180 | "protocol": "Http", 181 | "port": 59999, 182 | "intervalInSeconds": 5, 183 | "numberOfProbes": 2, 184 | "requestPath": "/" 185 | } 186 | } 187 | ] 188 | } 189 | }, 190 | { 191 | "name": "[concat('TFSVmDeploy', copyindex())]", 192 | "type": "Microsoft.Resources/deployments", 193 | "apiVersion": "2017-05-10", 194 | "dependsOn": [ 195 | "[resourceId('Microsoft.Storage/storageAccounts', variables('diagStorageAccountName'))]", 196 | "[resourceId('Microsoft.Network/loadBalancers', variables('LBName'))]" 197 | ], 198 | "copy": { 199 | "name": "tfsvmloop", 200 | "count": "[parameters('tfsVmInstanceCount')]" 201 | }, 202 | "properties": { 203 | "mode": "Incremental", 204 | "templateLink": { 205 | "uri": "[variables('windowsVmTemplateURL')]", 206 | "contentVersion": "1.0.0.0" 207 | }, 208 | "parameters": { 209 | "vmName": { 210 | "value": "[concat(variables('tfsVMPrefix'), copyindex())]" 211 | }, 212 | "vmSize": { 213 | "value": "[parameters('vmSize')]" 214 | }, 215 | "subnetId": { 216 | "value": "[parameters('subnetId')]" 217 | }, 218 | "adminUsername": { 219 | "value": "[parameters('adminUsername')]" 220 | }, 221 | "adminPassword": { 222 | "value": "[parameters('adminPassword')]" 223 | }, 224 | "diagStorageAccountId": { 225 | "value": "[resourceId('Microsoft.Storage/storageAccounts', variables('diagStorageAccountName'))]" 226 | }, 227 | "availabilitySetId": { 228 | "value": "[resourceId('Microsoft.Compute/availabilitySets', variables('HASetName'))]" 229 | }, 230 | "imageSku": { 231 | "value": "[parameters('imageSku')]" 232 | }, 233 | "LoadBalancerBEId": { 234 | "value": "[variables('LBBEAddressPoolID')]" 235 | }, 236 | "secrets": { 237 | "value": "[parameters('secrets')]" 238 | } 239 | } 240 | } 241 | }, 242 | { 243 | "apiVersion": "2015-06-15", 244 | "type": "Microsoft.Compute/virtualMachines/extensions", 245 | "name": "[concat(variables('tfsVmPrefix'), copyindex(), '/joindomain')]", 246 | "location": "[resourceGroup().location]", 247 | "dependsOn": [ 248 | "[resourceId('Microsoft.Resources/deployments', concat('TFSVmDeploy', copyindex()))]" 249 | ], 250 | "copy": { 251 | "name": "tfsvmjoinloop", 252 | "count": "[parameters('tfsVmInstanceCount')]" 253 | }, 254 | "properties": { 255 | "publisher": "Microsoft.Compute", 256 | "type": "JsonADDomainExtension", 257 | "typeHandlerVersion": "1.3", 258 | "autoUpgradeMinorVersion": true, 259 | "settings": { 260 | "Name": "[parameters('domainName')]", 261 | "OUPath": "", 262 | "User": "[concat(parameters('domainName'), '\\', parameters('adminUserName'))]", 263 | "Restart": "true", 264 | "Options": "3" 265 | }, 266 | "protectedSettings": { 267 | "Password": "[parameters('adminPassword')]" 268 | } 269 | } 270 | }, 271 | { 272 | "type": "Microsoft.Compute/virtualMachines/extensions", 273 | "name": "[concat(variables('tfsVmPrefix'), copyindex(), '/configuretfs', copyindex())]", 274 | "dependsOn": [ 275 | "[concat(resourceId('Microsoft.Compute/virtualMachines', concat(variables('tfsVmPrefix'), copyindex())), '/extensions/joindomain')]" 276 | ], 277 | "apiVersion": "2016-03-30", 278 | "copy": { 279 | "name": "tfsconfigloop", 280 | "count": "[parameters('tfsVmInstanceCount')]", 281 | "mode": "Serial", 282 | "batchSize": 1 283 | }, 284 | "location": "[resourceGroup().location]", 285 | "properties": { 286 | "publisher": "Microsoft.Powershell", 287 | "type": "DSC", 288 | "typeHandlerVersion": "2.21", 289 | "autoUpgradeMinorVersion": true, 290 | "settings": { 291 | "modulesURL": "[variables('TFSInstallModuleURL')]", 292 | "configurationFunction": "[variables('TFSInstallFunction')]", 293 | "properties": { 294 | "domainName": "[parameters('domainName')]", 295 | "adminCreds": { 296 | "userName": "[parameters('adminUserName')]", 297 | "password": "privateSettingsRef:adminPassword" 298 | }, 299 | "SqlServerInstance": "[parameters('sqlServerName')]", 300 | "primaryInstance": "[concat(variables('TfsVmPrefix'), '0')]", 301 | "GlobalSiteIP": "[parameters('LBIpAddress')]", 302 | "GlobalSiteName": "[variables('globalSiteName')]", 303 | "DnsServer": "DC1", 304 | "TFSVersion": "TFS2018", 305 | "SslThumbprint": "[parameters('sslThumbprint')]" 306 | } 307 | }, 308 | "protectedSettings": { 309 | "items": { 310 | "adminPassword": "[parameters('adminPassword')]" 311 | } 312 | } 313 | } 314 | } 315 | ], 316 | "outputs": {} 317 | } -------------------------------------------------------------------------------- /tfs-ha/deployagents.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "subnetId": { 6 | "type": "string" 7 | }, 8 | "adminPassword": { 9 | "type": "securestring" 10 | }, 11 | "adminUsername": { 12 | "type": "string", 13 | "defaultValue": "EnterpriseAdmin" 14 | }, 15 | "domainName": { 16 | "type": "string", 17 | "defaultValue": "devnet.contoso.us" 18 | }, 19 | "vmSize": { 20 | "type": "string", 21 | "allowedValues": [ 22 | "Standard_DS1_v2", 23 | "Standard_DS2_v2", 24 | "Standard_DS3_v2", 25 | "Standard_DS4_v2", 26 | "Standard_DS5_v2", 27 | "Standard_DS11_v2", 28 | "Standard_DS12_v2", 29 | "Standard_DS13_v2", 30 | "Standard_DS14_v2", 31 | "Standard_DS15_v2" 32 | ], 33 | "defaultValue": "Standard_DS2_v2" 34 | }, 35 | "buildAgentVmInstanceCount": { 36 | "type": "int", 37 | "defaultValue": 2 38 | }, 39 | "tfsServerUrl": { 40 | "type": "string" 41 | } 42 | }, 43 | "variables": { 44 | "baseUri": "[deployment().properties.templateLink.uri]", 45 | "VSTSAgentInstallModuleURL": "[uri(variables('baseUri'), 'DSC/InstallVSTSAgent.ps1.zip')]", 46 | "VSTSAgentInstallFunction": "InstallVSTSAgent.ps1\\VSTSAgentInstallDsc", 47 | "agentDownloadUrl": "https://vstsagentpackage.azureedge.net/agent/2.127.0/vsts-agent-win-x64-2.127.0.zip", 48 | "VSImagePublisher": "MicrosoftVisualStudio", 49 | "VSImageOffer": "VisualStudio", 50 | "VSImageSku": "VS-2017-Ent-Latest-WS2016", 51 | "windowsVmTemplateURL": "[uri(variables('baseUri'),'../primitives/windowsvm.json')]", 52 | "diagStorageAccountName": "[concat(uniquestring(resourceGroup().id), 'agvmdiag')]", 53 | "HASetName": "AgentHASet", 54 | "HASetId": "[if(greater(parameters('buildAgentVmInstanceCount'),1),resourceId('Microsoft.Compute/availabilitySets',variables('HASetName')),'')]", 55 | "agentVMPrefix": "AGENT" 56 | }, 57 | "resources": [ 58 | { 59 | "type": "Microsoft.Storage/storageAccounts", 60 | "name": "[variables('diagStorageAccountName')]", 61 | "apiVersion": "2016-01-01", 62 | "location": "[resourceGroup().location]", 63 | "sku": { 64 | "name": "Standard_LRS" 65 | }, 66 | "kind": "Storage", 67 | "properties": {} 68 | }, 69 | { 70 | "name": "[variables('HASetName')]", 71 | "type": "Microsoft.Compute/availabilitySets", 72 | "location": "[resourceGroup().location]", 73 | "apiVersion": "2017-03-30", 74 | "condition": "[greater(parameters('buildAgentVmInstanceCount'),1)]", 75 | "dependsOn": [], 76 | "properties": { 77 | "platformUpdateDomainCount": 2, 78 | "platformFaultDomainCount": 2 79 | }, 80 | "sku": { 81 | "name": "Aligned" 82 | } 83 | }, 84 | { 85 | "name": "[concat('agentVmDeploy', copyindex())]", 86 | "type": "Microsoft.Resources/deployments", 87 | "apiVersion": "2017-05-10", 88 | "dependsOn": [ 89 | "[resourceId('Microsoft.Compute/availabilitySets', variables('HASetName'))]" 90 | ], 91 | "copy": { 92 | "name": "agentvmloop", 93 | "count": "[parameters('buildAgentVmInstanceCount')]" 94 | }, 95 | "properties": { 96 | "mode": "Incremental", 97 | "templateLink": { 98 | "uri": "[variables('windowsVmTemplateURL')]", 99 | "contentVersion": "1.0.0.0" 100 | }, 101 | "parameters": { 102 | "vmName": { 103 | "value": "[concat(variables('agentVMPrefix'), copyindex())]" 104 | }, 105 | "vmSize": { 106 | "value": "[parameters('vmSize')]" 107 | }, 108 | "subnetId": { 109 | "value": "[parameters('subnetId')]" 110 | }, 111 | "adminUsername": { 112 | "value": "[parameters('adminUsername')]" 113 | }, 114 | "adminPassword": { 115 | "value": "[parameters('adminPassword')]" 116 | }, 117 | "diagStorageAccountId": { 118 | "value": "[resourceId('Microsoft.Storage/storageAccounts', variables('diagStorageAccountName'))]" 119 | }, 120 | "availabilitySetId": { 121 | "value": "[variables('HASetId')]" 122 | }, 123 | "imagePublisher": { 124 | "value": "[variables('VSImagePublisher')]" 125 | }, 126 | "imageOffer": { 127 | "value": "[variables('VSImageOffer')]" 128 | }, 129 | "imageSku": { 130 | "value": "[variables('VSImageSku')]" 131 | } 132 | } 133 | } 134 | }, 135 | { 136 | "apiVersion": "2015-06-15", 137 | "type": "Microsoft.Compute/virtualMachines/extensions", 138 | "name": "[concat(variables('agentVmPrefix'), copyindex(), '/joindomain')]", 139 | "location": "[resourceGroup().location]", 140 | "dependsOn": [ 141 | "[resourceId('Microsoft.Resources/deployments', concat('agentVmDeploy', copyindex()))]" 142 | ], 143 | "copy": { 144 | "name": "agentvmjoinloop", 145 | "count": "[parameters('buildAgentVmInstanceCount')]" 146 | }, 147 | "properties": { 148 | "publisher": "Microsoft.Compute", 149 | "type": "JsonADDomainExtension", 150 | "typeHandlerVersion": "1.3", 151 | "autoUpgradeMinorVersion": true, 152 | "settings": { 153 | "Name": "[parameters('domainName')]", 154 | "OUPath": "", 155 | "User": "[concat(parameters('domainName'), '\\', parameters('adminUserName'))]", 156 | "Restart": "true", 157 | "Options": "3" 158 | }, 159 | "protectedSettings": { 160 | "Password": "[parameters('adminPassword')]" 161 | } 162 | } 163 | }, 164 | { 165 | "type": "Microsoft.Compute/virtualMachines/extensions", 166 | "name": "[concat(variables('agentVmPrefix'), copyindex(), '/configureagent', copyindex())]", 167 | "dependsOn": [ 168 | "[concat(resourceId('Microsoft.Compute/virtualMachines', concat(variables('agentVmPrefix'), copyindex())), '/extensions/joindomain')]" 169 | ], 170 | "apiVersion": "2016-03-30", 171 | "copy": { 172 | "name": "agentconfigloop", 173 | "count": "[parameters('buildAgentVmInstanceCount')]", 174 | }, 175 | "location": "[resourceGroup().location]", 176 | "properties": { 177 | "publisher": "Microsoft.Powershell", 178 | "type": "DSC", 179 | "typeHandlerVersion": "2.21", 180 | "autoUpgradeMinorVersion": true, 181 | "settings": { 182 | "modulesURL": "[variables('VSTSAgentInstallModuleURL')]", 183 | "configurationFunction": "[variables('VSTSAgentInstallFunction')]", 184 | "properties": { 185 | "domainName": "[parameters('domainName')]", 186 | "adminCreds": { 187 | "userName": "[parameters('adminUserName')]", 188 | "password": "privateSettingsRef:adminPassword" 189 | }, 190 | "TFSUrl": "[parameters('tfsServerUrl')]", 191 | "VSTSAgentUrl": "[variables('agentDownloadUrl')]", 192 | } 193 | }, 194 | "protectedSettings": { 195 | "items": { 196 | "adminPassword": "[parameters('adminPassword')]" 197 | } 198 | } 199 | } 200 | } 201 | ] 202 | } -------------------------------------------------------------------------------- /tfs/DSC/TFSSQLServerConfig.ps1: -------------------------------------------------------------------------------- 1 | configuration TFSSQLServerDsc 2 | { 3 | param 4 | ( 5 | [Parameter(Mandatory)] 6 | [String]$DomainName, 7 | 8 | [String]$DomainNetbiosName=(Get-NetBIOSName -DomainName $DomainName), 9 | 10 | [Parameter(Mandatory)] 11 | [System.Management.Automation.PSCredential]$Admincreds, 12 | 13 | [Int]$RetryCount=20, 14 | [Int]$RetryIntervalSec=30 15 | ) 16 | 17 | Import-DscResource -ModuleName xComputerManagement, xNetworking, xActiveDirectory, xSQLServer, xSQLps 18 | [System.Management.Automation.PSCredential]$DomainCreds = New-Object System.Management.Automation.PSCredential ("${DomainNetbiosName}\$($Admincreds.UserName)", $Admincreds.Password) 19 | 20 | Node localhost 21 | { 22 | xFirewall DatabaseEngineFirewallRule 23 | { 24 | Direction = "Inbound" 25 | Name = "SQL-Server-Database-Engine-TCP-In" 26 | DisplayName = "SQL Server Database Engine (TCP-In)" 27 | Description = "Inbound rule for SQL Server to allow TCP traffic for the Database Engine." 28 | Group = "SQL Server" 29 | Enabled = "True" 30 | Protocol = "TCP" 31 | LocalPort = "1433" 32 | Ensure = "Present" 33 | } 34 | 35 | WindowsFeature ADPS 36 | { 37 | Name = "RSAT-AD-PowerShell" 38 | Ensure = "Present" 39 | } 40 | 41 | xWaitForADDomain DscForestWait 42 | { 43 | DomainName = $DomainName 44 | DomainUserCredential= $DomainCreds 45 | RetryCount = $RetryCount 46 | RetryIntervalSec = $RetryIntervalSec 47 | DependsOn = "[WindowsFeature]ADPS" 48 | } 49 | 50 | xComputer DomainJoin 51 | { 52 | Name = $env:COMPUTERNAME 53 | DomainName = $DomainName 54 | Credential = $DomainCreds 55 | DependsOn = "[xWaitForADDomain]DscForestWait" 56 | } 57 | 58 | xSqlServerLogin AddDomainAdminAccountToSqlServer 59 | { 60 | Name = $DomainCreds.UserName 61 | LoginType = "WindowsUser" 62 | SQLServer = "$env:COMPUTERNAME,1433" 63 | SQLInstanceName = $env:COMPUTERNAME 64 | } 65 | 66 | xSqlServerRole AddDomainAdminAccountToSysAdmin 67 | { 68 | Ensure = "Present" 69 | MembersToInclude = $DomainCreds.UserName 70 | ServerRoleName = "sysadmin" 71 | SQLServer = "$env:COMPUTERNAME,1433" 72 | SQLInstanceName = $env:COMPUTERNAME 73 | DependsOn = "[xSqlServerLogin]AddDomainAdminAccountToSqlServer" 74 | } 75 | 76 | LocalConfigurationManager 77 | { 78 | RebootNodeIfNeeded = $true 79 | } 80 | 81 | } 82 | } 83 | 84 | function Get-NetBIOSName 85 | { 86 | [OutputType([string])] 87 | param( 88 | [string]$DomainName 89 | ) 90 | 91 | if ($DomainName.Contains('.')) { 92 | $length=$DomainName.IndexOf('.') 93 | if ( $length -ge 16) { 94 | $length=15 95 | } 96 | return $DomainName.Substring(0,$length) 97 | } 98 | else { 99 | if ($DomainName.Length -gt 15) { 100 | return $DomainName.Substring(0,15) 101 | } 102 | else { 103 | return $DomainName 104 | } 105 | } 106 | } -------------------------------------------------------------------------------- /tfs/DSC/TFSSQLServerConfig.ps1.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hansenms/iac/277b41b9f9a0ce395f577d8a74640000c91c5ce1/tfs/DSC/TFSSQLServerConfig.ps1.zip -------------------------------------------------------------------------------- /tfs/README.md: -------------------------------------------------------------------------------- 1 | Private TFS in Azure 2 | --------------------------- 3 | 4 | Microsoft [Azure](https://azure.microsoft.com/en-us/) offers a hosted DevOps experience with [Vistual Studio Team Services](https://www.visualstudio.com/team-services/) (VSTS). It is great, but sometimes organizations would like to host their own [Team Foundation Server](https://www.visualstudio.com/tfs/) (TFS). A particularly relevant use case is users in the [Microsoft Government Cloud](https://azure.microsoft.com/en-us/overview/clouds/government/) where VSTS is not yet available. Deploying a TFS Server with database and build agents can be complicated. 5 | 6 | This template illustrates how to a TFS server and agents. The topology is illustrated below: 7 | 8 | ![Private DevOps](../devnet-tfs/private_devops.png) 9 | 10 | The deployment needs an existing subnet (in a virtual network) and a domain to join. The basic networking and domain controllers can be deployed with the [core-network](../core-network) template, or the network and TFS deployment can be combined as in [devnet-tfs](../devnet-tfs) 11 | 12 | 13 | 14 | 15 | 16 | 17 | 19 | --------------------------------------------------------------------------------