├── .gitignore ├── AvailabilitySet.tf ├── Deploy.PS1 ├── FirstLogonCommands.xml ├── LoadBalancer.tf ├── Network.tf ├── README.md ├── ResourceGroup.tf ├── Storage.tf ├── TerraformCredentials.ps1 ├── Variables.tf ├── WebserverDsc.PS1 ├── default.htm ├── testvm-SecurityGroup.tf └── testvm.tf /.gitignore: -------------------------------------------------------------------------------- 1 | terraform.tfstate 2 | terraform.tfstate.backup 3 | TempCommands.PS1 -------------------------------------------------------------------------------- /AvailabilitySet.tf: -------------------------------------------------------------------------------- 1 | resource "azurerm_availability_set" "availability_set" { 2 | name = "${var.vm_name_prefix}-avset" 3 | location = "${var.azure_region_fullname}" 4 | resource_group_name = "${azurerm_resource_group.resource_group.name}" 5 | platform_update_domain_count = "5" 6 | platform_fault_domain_count = "3" 7 | 8 | tags { 9 | environment = "${var.environment_tag}" 10 | } 11 | } -------------------------------------------------------------------------------- /Deploy.PS1: -------------------------------------------------------------------------------- 1 | 2 | 3 | Start-Transcript -Path C:\Deploy.Log 4 | 5 | Write-Host "Setup WinRM for $RemoteHostName" 6 | 7 | $Cert = New-SelfSignedCertificate -DnsName $RemoteHostName, $ComputerName ` 8 | -CertStoreLocation "cert:\LocalMachine\My" ` 9 | -FriendlyName "Test WinRM Cert" 10 | 11 | $Cert | Out-String 12 | 13 | $Thumbprint = $Cert.Thumbprint 14 | 15 | Write-Host "Enable HTTPS in WinRM" 16 | $WinRmHttps = "@{Hostname=`"$RemoteHostName`"; CertificateThumbprint=`"$Thumbprint`"}" 17 | winrm create winrm/config/Listener?Address=*+Transport=HTTPS $WinRmHttps 18 | 19 | Write-Host "Set Basic Auth in WinRM" 20 | $WinRmBasic = "@{Basic=`"true`"}" 21 | winrm set winrm/config/service/Auth $WinRmBasic 22 | 23 | Write-Host "Open Firewall Port" 24 | netsh advfirewall firewall add rule name="Windows Remote Management (HTTPS-In)" dir=in action=allow protocol=TCP localport=$WinRmPort 25 | 26 | Stop-Transcript -------------------------------------------------------------------------------- /FirstLogonCommands.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | cmd /c "copy C:\AzureData\CustomData.bin C:\Deploy.PS1"CopyScript 5 | 11 6 | 7 | 8 | powershell.exe -sta -ExecutionPolicy Unrestricted -file C:\Deploy.PS1RunScript 10 | 12 11 | 12 | -------------------------------------------------------------------------------- /LoadBalancer.tf: -------------------------------------------------------------------------------- 1 | # TODO: Add the availability set and the VMs to the load balanacer 2 | 3 | # VIP address 4 | resource "azurerm_public_ip" "load_balancer_public_ip" { 5 | name = "${var.vm_name_prefix}-ip" 6 | location = "${var.azure_region_fullname}" 7 | resource_group_name = "${azurerm_resource_group.resource_group.name}" 8 | public_ip_address_allocation = "dynamic" 9 | domain_name_label = "${azurerm_resource_group.resource_group.name}" 10 | } 11 | 12 | # Front End Load Balancer 13 | resource "azurerm_lb" "load_balancer" { 14 | name = "${var.vm_name_prefix}-lb" 15 | location = "${var.azure_region_fullname}" 16 | resource_group_name = "${azurerm_resource_group.resource_group.name}" 17 | 18 | frontend_ip_configuration { 19 | name = "${var.vm_name_prefix}-ipconfig" 20 | public_ip_address_id = "${azurerm_public_ip.load_balancer_public_ip.id}" 21 | } 22 | } 23 | 24 | # Back End Address Pool 25 | resource "azurerm_lb_backend_address_pool" "backend_pool" { 26 | location = "${var.azure_region_fullname}" 27 | resource_group_name = "${azurerm_resource_group.resource_group.name}" 28 | loadbalancer_id = "${azurerm_lb.load_balancer.id}" 29 | name = "${var.vm_name_prefix}-backend_address_pool" 30 | } 31 | 32 | # Load Balancer Rule 33 | resource "azurerm_lb_rule" "load_balancer_http_rule" { 34 | location = "${var.azure_region_fullname}" 35 | resource_group_name = "${azurerm_resource_group.resource_group.name}" 36 | loadbalancer_id = "${azurerm_lb.load_balancer.id}" 37 | name = "HTTPRule" 38 | protocol = "Tcp" 39 | frontend_port = 80 40 | backend_port = 80 41 | frontend_ip_configuration_name = "${var.vm_name_prefix}-ipconfig" 42 | backend_address_pool_id = "${azurerm_lb_backend_address_pool.backend_pool.id}" 43 | probe_id = "${azurerm_lb_probe.load_balancer_probe.id}" 44 | depends_on = ["azurerm_lb_probe.load_balancer_probe"] 45 | } 46 | 47 | resource "azurerm_lb_rule" "load_balancer_https_rule" { 48 | location = "${var.azure_region_fullname}" 49 | resource_group_name = "${azurerm_resource_group.resource_group.name}" 50 | loadbalancer_id = "${azurerm_lb.load_balancer.id}" 51 | name = "HTTPSRule" 52 | protocol = "Tcp" 53 | frontend_port = 443 54 | backend_port = 443 55 | frontend_ip_configuration_name = "${var.vm_name_prefix}-ipconfig" 56 | backend_address_pool_id = "${azurerm_lb_backend_address_pool.backend_pool.id}" 57 | probe_id = "${azurerm_lb_probe.load_balancer_probe.id}" 58 | depends_on = ["azurerm_lb_probe.load_balancer_probe"] 59 | } 60 | 61 | #LB Probe - Checks to see which VMs are healthy and available 62 | resource "azurerm_lb_probe" "load_balancer_probe" { 63 | location = "${var.azure_region_fullname}" 64 | resource_group_name = "${azurerm_resource_group.resource_group.name}" 65 | loadbalancer_id = "${azurerm_lb.load_balancer.id}" 66 | name = "HTTP" 67 | port = 80 68 | } 69 | 70 | #TODO: Dynamic NAT rules for each VM for WinRM -------------------------------------------------------------------------------- /Network.tf: -------------------------------------------------------------------------------- 1 | # Create a virtual network in the web_servers resource group 2 | resource "azurerm_virtual_network" "network" { 3 | name = "${var.azure_resource_group_name}-Network" 4 | address_space = ["10.0.0.0/16"] 5 | location = "${var.azure_region_fullname}" 6 | resource_group_name = "${azurerm_resource_group.resource_group.name}" 7 | } 8 | 9 | resource "azurerm_subnet" "subnet1" { 10 | name = "${var.azure_resource_group_name}-Subnet1" 11 | resource_group_name = "${azurerm_resource_group.resource_group.name}" 12 | virtual_network_name = "${azurerm_virtual_network.network.name}" 13 | address_prefix = "10.0.1.0/24" 14 | } 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Terraform-AzureRM-Example 2 | 3 | Quick Instruction: 4 | 5 | Download and install Terraform 6 | https://www.terraform.io/downloads.html 7 | 8 | Edit the TerraformCredentials.ps1 and enter API keys 9 | 10 | Load Powershell 11 | 12 | "dot source" the credentials to load them into environment variables 13 | 14 | . .\TerraformCredentials.ps1 15 | 16 | Test the configuration 17 | 18 | terraform plan 19 | 20 | Build the resources 21 | 22 | terraform apply 23 | 24 | Delete the resources 25 | 26 | terraform destroy 27 | 28 | Information 29 | 30 | http://superautomation.blogspot.co.uk/2016/11/terraform-with-azure-resource-manager.html 31 | http://superautomation.blogspot.co.uk/2016/11/configuring-terraform-to-use-winrm-over.html 32 | http://superautomation.blogspot.co.uk/2016/11/azure-resource-manager-load-balancer.html 33 | -------------------------------------------------------------------------------- /ResourceGroup.tf: -------------------------------------------------------------------------------- 1 | resource "azurerm_resource_group" "resource_group" { 2 | name = "${var.azure_resource_group_name}" 3 | location = "${var.azure_region_fullname}" 4 | 5 | tags { 6 | environment = "${var.environment_tag}" 7 | } 8 | } -------------------------------------------------------------------------------- /Storage.tf: -------------------------------------------------------------------------------- 1 | 2 | resource "azurerm_storage_account" "storage_account" { 3 | name = "${var.azure_resource_group_name}storages" 4 | resource_group_name = "${azurerm_resource_group.resource_group.name}" 5 | location = "${var.azure_region_fullname}" 6 | account_type = "Standard_LRS" 7 | 8 | tags { 9 | environment = "${var.environment_tag}" 10 | } 11 | } 12 | 13 | resource "azurerm_storage_container" "container" { 14 | name = "vhds" 15 | resource_group_name = "${azurerm_resource_group.resource_group.name}" 16 | storage_account_name = "${azurerm_storage_account.storage_account.name}" 17 | container_access_type = "private" 18 | } 19 | -------------------------------------------------------------------------------- /TerraformCredentials.ps1: -------------------------------------------------------------------------------- 1 | #Set Environment Variables for Azure RM Terraform 2 | 3 | $ENV:ARM_SUBSCRIPTION_ID = "" 4 | $ENV:ARM_CLIENT_ID = "" 5 | $ENV:ARM_CLIENT_SECRET = "" # This should end with an '=' symbol 6 | $ENV:ARM_TENANT_ID = "" 7 | 8 | -------------------------------------------------------------------------------- /Variables.tf: -------------------------------------------------------------------------------- 1 | variable "azure_resource_group_name" { 2 | description = "Resource Group Name" 3 | default = "ninjagroup" 4 | } 5 | 6 | variable "vm_name_prefix" { 7 | description = "The Virtual Machine Name" 8 | default = "ninjatestvm" 9 | } 10 | 11 | variable "vm_count" { 12 | description = "Number of VMs to create" 13 | default = "2" 14 | } 15 | 16 | #Re-applying a new size reboots the VMs and re-runs the provisioner scripts - Use DSC Push to configure to avoid errors 17 | variable "vm_size" { 18 | description = "Azure VM Size" 19 | default = "Standard_A1" 20 | } 21 | 22 | variable "vm_winrm_port" { 23 | description = "WinRM Public Port" 24 | default = "5986" 25 | } 26 | 27 | variable "azure_region" { 28 | description = "Azure Region for all resources" 29 | default = "northeurope" 30 | } 31 | 32 | variable "azure_region_fullname" { 33 | description = "Long name for the Azure Region, ie. North Europe" 34 | default = "North Europe" 35 | } 36 | 37 | variable "azure_dns_suffix" { 38 | description = "Azure DNS suffix for the Public IP" 39 | default = "cloudapp.azure.com" 40 | } 41 | 42 | variable "admin_username" { 43 | description = "Username for the Administrator account" 44 | default = "TestAdmin" 45 | } 46 | 47 | variable "admin_password" { 48 | description = "Password for the Administrator account" 49 | default = "jgjgJGJG!!!!" 50 | } 51 | 52 | variable "environment_tag" { 53 | description = "Tag to apply to the resoucrces" 54 | default = "Terraform-AzureRM-Example" 55 | } 56 | 57 | #Null resource to make the VM intermediate varable - probably not the right way to do this 58 | #resource "null_resource" "intermediates" { 59 | # triggers = { 60 | # full_vm_dns_name = "Param($RemoteHostName = \"${var.vm_name_prefix}-1.${var.azure_region}.${var.azure_dns_suffix}\", $ComputerName = \"${var.vm_name_prefix}-1\", $WinRmPort = ${var.vm_winrm_port}) ${file("Deploy.PS1")}" 61 | # #full_vm_dns_name = "Param($RemoteHostName = \"${null_resource.intermediates.triggers.full_vm_dns_name}\", $ComputerName = \"${var.vm_name}\", $WinRmPort = ${var.vm_winrm_port}) ${file("Deploy.PS1")}" 62 | # } 63 | #} 64 | 65 | #output "full_vm_dns_name" { 66 | # value = "${null_resource.intermediates.triggers.full_vm_dns_name}" 67 | #} 68 | -------------------------------------------------------------------------------- /WebserverDsc.PS1: -------------------------------------------------------------------------------- 1 | Start-Transcript -Path C:\Scripts\WebserverDsc.Log 2 | Write-Host "Provisioning $($env:COMPUTERNAME)" 3 | 4 | configuration LocalIIS { 5 | Import-DscResource -ModuleName 'PSDesiredStateConfiguration' 6 | Node $ComputerName { 7 | 8 | WindowsFeature IIS{ 9 | Name = 'web-server' 10 | Ensure = 'Present' 11 | } 12 | 13 | } 14 | } 15 | $computername = 'localhost' 16 | 17 | LocalIIS -OutputPath c:\DSC\Config 18 | 19 | Start-DscConfiguration -Path C:\DSC\Config -ComputerName localhost 20 | 21 | Write-Host "DSC Configuration Started." 22 | 23 | Stop-Transcript 24 | -------------------------------------------------------------------------------- /default.htm: -------------------------------------------------------------------------------- 1 | $env:COMPUTERNAME -------------------------------------------------------------------------------- /testvm-SecurityGroup.tf: -------------------------------------------------------------------------------- 1 | 2 | resource "azurerm_network_security_group" "vm_security_group" { 3 | name = "${var.vm_name_prefix}-sg" 4 | location = "${var.azure_region_fullname}" 5 | resource_group_name = "${azurerm_resource_group.resource_group.name}" 6 | 7 | tags { 8 | environment = "${var.environment_tag}" 9 | } 10 | } 11 | 12 | resource "azurerm_network_security_rule" "rdpRule" { 13 | name = "rdpRule" 14 | priority = 100 15 | direction = "Inbound" 16 | access = "Allow" 17 | protocol = "Tcp" 18 | source_port_range = "*" 19 | destination_port_range = "3389" 20 | source_address_prefix = "*" 21 | destination_address_prefix = "*" 22 | resource_group_name = "${azurerm_resource_group.resource_group.name}" 23 | network_security_group_name = "${azurerm_network_security_group.vm_security_group.name}" 24 | } 25 | 26 | resource "azurerm_network_security_rule" "winrmRule" { 27 | name = "winrmRule" 28 | priority = 110 29 | direction = "Inbound" 30 | access = "Allow" 31 | protocol = "Tcp" 32 | source_port_range = "*" 33 | destination_port_range = "${var.vm_winrm_port}" 34 | source_address_prefix = "*" 35 | destination_address_prefix = "*" 36 | resource_group_name = "${azurerm_resource_group.resource_group.name}" 37 | network_security_group_name = "${azurerm_network_security_group.vm_security_group.name}" 38 | } 39 | 40 | resource "azurerm_network_security_rule" "httpRule" { 41 | name = "HTTP" 42 | priority = 120 43 | direction = "Inbound" 44 | access = "Allow" 45 | protocol = "Tcp" 46 | source_port_range = "*" 47 | destination_port_range = "80" 48 | source_address_prefix = "*" 49 | destination_address_prefix = "*" 50 | resource_group_name = "${azurerm_resource_group.resource_group.name}" 51 | network_security_group_name = "${azurerm_network_security_group.vm_security_group.name}" 52 | } 53 | 54 | resource "azurerm_network_security_rule" "httpsRule" { 55 | name = "HTTPS" 56 | priority = 130 57 | direction = "Inbound" 58 | access = "Allow" 59 | protocol = "Tcp" 60 | source_port_range = "*" 61 | destination_port_range = "443" 62 | source_address_prefix = "*" 63 | destination_address_prefix = "*" 64 | resource_group_name = "${azurerm_resource_group.resource_group.name}" 65 | network_security_group_name = "${azurerm_network_security_group.vm_security_group.name}" 66 | } 67 | 68 | -------------------------------------------------------------------------------- /testvm.tf: -------------------------------------------------------------------------------- 1 | resource "azurerm_lb_nat_rule" "winrm_nat" { 2 | location = "${var.azure_region_fullname}" 3 | resource_group_name = "${azurerm_resource_group.resource_group.name}" 4 | loadbalancer_id = "${azurerm_lb.load_balancer.id}" 5 | name = "WinRM-HTTPS-vm-${count.index}" 6 | protocol = "Tcp" 7 | frontend_port = "${count.index + 10000}" 8 | backend_port = "${var.vm_winrm_port}" 9 | frontend_ip_configuration_name = "${var.vm_name_prefix}-ipconfig" 10 | count = "${var.vm_count}" 11 | } 12 | 13 | resource "azurerm_lb_nat_rule" "rdp_nat" { 14 | location = "${var.azure_region_fullname}" 15 | resource_group_name = "${azurerm_resource_group.resource_group.name}" 16 | loadbalancer_id = "${azurerm_lb.load_balancer.id}" 17 | name = "RDP-vm-${count.index}" 18 | protocol = "Tcp" 19 | frontend_port = "${count.index + 11000}" 20 | backend_port = "3389" 21 | frontend_ip_configuration_name = "${var.vm_name_prefix}-ipconfig" 22 | count = "${var.vm_count}" 23 | } 24 | 25 | 26 | resource "azurerm_network_interface" "vm_nic" { 27 | name = "${var.vm_name_prefix}-${count.index}-nic" 28 | location = "${var.azure_region_fullname}" 29 | resource_group_name = "${azurerm_resource_group.resource_group.name}" 30 | network_security_group_id = "${azurerm_network_security_group.vm_security_group.id}" 31 | count = "${var.vm_count}" 32 | 33 | ip_configuration { 34 | name = "${var.vm_name_prefix}-${count.index}-ipConfig" 35 | subnet_id = "${azurerm_subnet.subnet1.id}" 36 | private_ip_address_allocation = "dynamic" 37 | load_balancer_backend_address_pools_ids = ["${azurerm_lb_backend_address_pool.backend_pool.id}"] 38 | load_balancer_inbound_nat_rules_ids = ["${element(azurerm_lb_nat_rule.winrm_nat.*.id, count.index)}"] 39 | } 40 | 41 | tags { 42 | environment = "${var.environment_tag}" 43 | } 44 | } 45 | 46 | resource "azurerm_virtual_machine" "virtual_machine" { 47 | name = "${var.vm_name_prefix}-${count.index}" 48 | location = "${var.azure_region_fullname}" 49 | resource_group_name = "${azurerm_resource_group.resource_group.name}" 50 | network_interface_ids = ["${element(azurerm_network_interface.vm_nic.*.id, count.index)}"] 51 | vm_size = "${var.vm_size}" 52 | count = "${var.vm_count}" 53 | availability_set_id = "${azurerm_availability_set.availability_set.id}" 54 | delete_os_disk_on_termination = true 55 | delete_data_disks_on_termination = true 56 | 57 | storage_image_reference { 58 | publisher = "MicrosoftWindowsServer" 59 | offer = "WindowsServer" 60 | sku = "2016-Datacenter" 61 | version = "latest" 62 | } 63 | 64 | storage_os_disk { 65 | name = "${var.vm_name_prefix}-${count.index}-osdisk" 66 | vhd_uri = "${azurerm_storage_account.storage_account.primary_blob_endpoint}${azurerm_storage_container.container.name}/${var.vm_name_prefix}-${count.index}-osdisk.vhd" 67 | caching = "ReadWrite" 68 | create_option = "FromImage" 69 | } 70 | 71 | os_profile { 72 | computer_name = "${var.vm_name_prefix}-${count.index}" 73 | admin_username = "${var.admin_username}" 74 | admin_password = "${var.admin_password}" 75 | #Include Deploy.PS1 with variables injected as custom_data 76 | custom_data = "${base64encode("Param($RemoteHostName = \"${var.vm_name_prefix}-${count.index}.${var.azure_region}.${var.azure_dns_suffix}\", $ComputerName = \"${var.vm_name_prefix}-${count.index}\", $WinRmPort = ${var.vm_winrm_port}) ${file("Deploy.PS1")}")}" 77 | } 78 | 79 | tags { 80 | environment = "${var.environment_tag}" 81 | } 82 | 83 | os_profile_windows_config { 84 | provision_vm_agent = true 85 | enable_automatic_upgrades = true 86 | 87 | additional_unattend_config { 88 | pass = "oobeSystem" 89 | component = "Microsoft-Windows-Shell-Setup" 90 | setting_name = "AutoLogon" 91 | content = "${var.admin_password}true1${var.admin_username}" 92 | } 93 | #Unattend config is to enable basic auth in WinRM, required for the provisioner stage. 94 | additional_unattend_config { 95 | pass = "oobeSystem" 96 | component = "Microsoft-Windows-Shell-Setup" 97 | setting_name = "FirstLogonCommands" 98 | content = "${file("FirstLogonCommands.xml")}" 99 | } 100 | } 101 | 102 | provisioner "file" { 103 | source = "WebserverDsc.PS1" 104 | destination = "C:\\Scripts\\WebserverDsc.PS1" 105 | connection { 106 | type = "winrm" 107 | https = true 108 | insecure = true 109 | user = "${var.admin_username}" 110 | password = "${var.admin_password}" 111 | host = "${azurerm_resource_group.resource_group.name}.${var.azure_region}.${var.azure_dns_suffix}" 112 | port = "${count.index + 10000}" 113 | } 114 | } 115 | 116 | provisioner "file" { 117 | content = "${var.vm_name_prefix}-${count.index}" 118 | destination = "C:\\inetpub\\wwwroot\\default.htm" 119 | connection { 120 | type = "winrm" 121 | https = true 122 | insecure = true 123 | user = "${var.admin_username}" 124 | password = "${var.admin_password}" 125 | host = "${azurerm_resource_group.resource_group.name}.${var.azure_region}.${var.azure_dns_suffix}" 126 | port = "${count.index + 10000}" 127 | } 128 | } 129 | 130 | provisioner "remote-exec" { 131 | inline = [ 132 | "powershell.exe -sta -ExecutionPolicy Unrestricted -file C:\\Scripts\\WebserverDsc.ps1", 133 | ] 134 | connection { 135 | type = "winrm" 136 | timeout = "20m" 137 | https = true 138 | insecure = true 139 | user = "${var.admin_username}" 140 | password = "${var.admin_password}" 141 | host = "${azurerm_resource_group.resource_group.name}.${var.azure_region}.${var.azure_dns_suffix}" 142 | port = "${count.index + 10000}" 143 | } 144 | } 145 | 146 | } 147 | 148 | 149 | # Get Server types 150 | # Get-AzureRmVMImagePublisher -Location "North Europe" | ? {$_.PublisherName -match "MicrosoftWindows"} 151 | # Get-AzureRmVMImageOffer -Location "North Europe" -PublisherName "MicrosoftWindowsServer" 152 | # Get-AzureRmVMImageSku -Location "North Europe" -PublisherName "MicrosoftWindowsServer" -Offer "WindowsServer" 153 | # Get-AzureRmVMImage -Location "North Europe" -PublisherName "MicrosoftWindowsServer" -Offer "WindowsServer" -Skus "2016-Nano-Server" --------------------------------------------------------------------------------