├── .gitignore ├── 1-main-vnet ├── commands.txt └── main.tf ├── 10-arm-template ├── azuredeploy.json ├── commands.txt ├── subnet_delegation.tf └── template-deploy.tf ├── 2-sec-vnet ├── commands.txt └── main.tf ├── 3-vnet-peering ├── commands.txt └── vnet-peering.tf ├── 4-remote-state-prep ├── commands.txt └── main.tf ├── 5-remote-state ├── backend.tf └── commands.txt ├── 6-azuredevops ├── azure-pipelines.yaml ├── production-pipeline.yaml └── uat-pipeline.yaml ├── 7-azure-devops-workspaces ├── backend.tf ├── commands.txt ├── main.tf ├── terraform.tfvars.example ├── vnet-peering.tf └── workspacetest.sh ├── 8-app-remote-state ├── backend-config-example.txt ├── commands.txt └── main.tf ├── 9-app-deploy ├── commands.txt ├── main.tf └── terraform.tfvars.example ├── CHANGELOG.md ├── LICENSE ├── README.md └── zz-terraform-vm ├── commands.txt ├── terraform-vm.tmpl ├── terraform.tf ├── variables.tf └── vm.tf /.gitignore: -------------------------------------------------------------------------------- 1 | # Local .terraform directories 2 | **/.terraform/* 3 | 4 | # .tfstate files 5 | *.tfstate 6 | *.tfstate.* 7 | 8 | # .tfvars files 9 | *.tfvars 10 | 11 | # .tfplan files 12 | *.tfplan 13 | 14 | *next-step.txt 15 | -------------------------------------------------------------------------------- /1-main-vnet/commands.txt: -------------------------------------------------------------------------------- 1 | # First we are going to deploy resources in our networking subscription 2 | # Be sure to select the networking subscription for your subname 3 | az account show 4 | az account set --subscription SUB_NAME 5 | 6 | terraform init 7 | terraform plan -var resource_group_name=main-vnet -out vnet.tfplan 8 | terraform apply "vnet.tfplan" -------------------------------------------------------------------------------- /1-main-vnet/main.tf: -------------------------------------------------------------------------------- 1 | ############################################################################# 2 | # TERRAFORM CONFIG 3 | ############################################################################# 4 | 5 | terraform { 6 | required_providers { 7 | azurerm = { 8 | source = "hashicorp/azurerm" 9 | version = "~> 2.0" 10 | } 11 | } 12 | } 13 | 14 | ############################################################################# 15 | # VARIABLES 16 | ############################################################################# 17 | 18 | variable "resource_group_name" { 19 | type = string 20 | } 21 | 22 | variable "location" { 23 | type = string 24 | default = "eastus" 25 | } 26 | 27 | 28 | variable "vnet_cidr_range" { 29 | type = string 30 | default = "10.0.0.0/16" 31 | } 32 | 33 | variable "subnet_prefixes" { 34 | type = list(string) 35 | default = ["10.0.0.0/24", "10.0.1.0/24"] 36 | } 37 | 38 | variable "subnet_names" { 39 | type = list(string) 40 | default = ["web", "database"] 41 | } 42 | 43 | ############################################################################# 44 | # PROVIDERS 45 | ############################################################################# 46 | 47 | provider "azurerm" { 48 | features {} 49 | } 50 | 51 | ############################################################################# 52 | # RESOURCES 53 | ############################################################################# 54 | 55 | resource "azurerm_resource_group" "vnet_main" { 56 | name = var.resource_group_name 57 | location = var.location 58 | } 59 | 60 | module "vnet-main" { 61 | source = "Azure/vnet/azurerm" 62 | version = "~> 2.0" 63 | resource_group_name = azurerm_resource_group.vnet_main.name 64 | vnet_name = var.resource_group_name 65 | address_space = [var.vnet_cidr_range] 66 | subnet_prefixes = var.subnet_prefixes 67 | subnet_names = var.subnet_names 68 | nsg_ids = {} 69 | 70 | tags = { 71 | environment = "dev" 72 | costcenter = "it" 73 | 74 | } 75 | 76 | depends_on = [azurerm_resource_group.vnet_main] 77 | } 78 | 79 | ############################################################################# 80 | # OUTPUTS 81 | ############################################################################# 82 | 83 | output "vnet_id" { 84 | value = module.vnet-main.vnet_id 85 | } 86 | -------------------------------------------------------------------------------- /10-arm-template/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 | "webAppName": { 6 | "type": "string", 7 | "metadata": { 8 | "description": "Base name of the resource such as web app name and app service plan" 9 | }, 10 | "minLength": 2 11 | }, 12 | "sku": { 13 | "type": "string", 14 | "defaultValue": "S1", 15 | "metadata": { 16 | "description": "The SKU of App Service Plan, by default is Standard S1" 17 | } 18 | }, 19 | "location": { 20 | "type": "string", 21 | "defaultValue": "[resourceGroup().location]", 22 | "metadata": { 23 | "description": "Location for all resources" 24 | } 25 | }, 26 | "vnetName": { 27 | "type": "string", 28 | "metadata": { 29 | "description": "Vnet Name that contains the subnet to allow for access" 30 | } 31 | }, 32 | "subnetRef": { 33 | "type": "string", 34 | "metadata": { 35 | "description": "Resource ID of the delegated subnet" 36 | } 37 | } 38 | }, 39 | "variables": { 40 | "webAppPortalName": "[concat(parameters('webAppName'), '-webapp')]", 41 | "appServicePlanName": "[concat('AppServicePlan-', parameters('webAppName'))]" 42 | }, 43 | "resources": [ 44 | { 45 | "apiVersion": "2018-02-01", 46 | "type": "Microsoft.Web/serverfarms", 47 | "kind": "app", 48 | "name": "[variables('appServicePlanName')]", 49 | "location": "[parameters('location')]", 50 | "properties": {}, 51 | "dependsOn": [], 52 | "sku": { 53 | "name": "[parameters('sku')]" 54 | } 55 | }, 56 | { 57 | "apiVersion": "2018-11-01", 58 | "type": "Microsoft.Web/sites", 59 | "kind": "app", 60 | "name": "[variables('webAppPortalName')]", 61 | "location": "[parameters('location')]", 62 | "properties": { 63 | "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('appServicePlanName'))]" 64 | }, 65 | "dependsOn": [ 66 | "[resourceId('Microsoft.Web/serverfarms', variables('appServicePlanName'))]" 67 | ], 68 | "resources": [ 69 | { 70 | "apiVersion": "2018-02-01", 71 | "type": "config", 72 | "name": "virtualNetwork", 73 | "location": "[parameters('location')]", 74 | "dependsOn": [ 75 | "[concat('Microsoft.Web/sites/', variables('webAppPortalName'))]" 76 | ], 77 | "properties": { 78 | "subnetResourceId": "[parameters('subnetRef')]", 79 | "swiftSupported": true 80 | } 81 | } 82 | ] 83 | } 84 | ] 85 | } -------------------------------------------------------------------------------- /10-arm-template/commands.txt: -------------------------------------------------------------------------------- 1 | # Next we're going to update the networking config with a subnet delegation 2 | # Upload the 10-arm-template/subnet_delegation.tf file to the networking 3 | # directory in the Azure DevOps Repo 4 | 5 | # Adding the file should kick off a pipeline run 6 | # Approve the Production deployment and let the pipeline finish 7 | 8 | # Now we've set the stage for deploying the arm template 9 | # Copy the template and template-deploy.tf files over to 9-app-deploy 10 | cp template-deploy.tf ../9-app-deploy/ 11 | cp azuredeploy.json ../9-app-deploy/ 12 | 13 | # Now we'll run the template deploy 14 | 15 | terraform plan -out app.tfplan 16 | terraform apply "app.tfplan" 17 | 18 | # Rinse and repeat for the other workspaces -------------------------------------------------------------------------------- /10-arm-template/subnet_delegation.tf: -------------------------------------------------------------------------------- 1 | resource "azurerm_subnet" "app_service" { 2 | name = "appservice" 3 | resource_group_name = azurerm_resource_group.vnet_main.name 4 | virtual_network_name = module.vnet-main.vnet_name 5 | address_prefix = cidrsubnet(var.vnet_cidr_range[terraform.workspace], 8, length(var.subnet_names)) 6 | 7 | delegation { 8 | name = "appservicedelegation" 9 | 10 | service_delegation { 11 | name = "Microsoft.Web/serverFarms" 12 | actions = [ 13 | "Microsoft.Network/virtualNetworks/subnets/prepareNetworkPolicies/action", 14 | "Microsoft.Network/virtualNetworks/subnets/action", 15 | "Microsoft.Network/virtualNetworks/subnets/join/action" 16 | ] 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /10-arm-template/template-deploy.tf: -------------------------------------------------------------------------------- 1 | ############################################################################# 2 | # DATA 3 | ############################################################################# 4 | 5 | data "azurerm_subnet" "appservice" { 6 | name = "appservice" 7 | virtual_network_name = data.terraform_remote_state.networking.outputs.vnet_name 8 | resource_group_name = data.terraform_remote_state.networking.outputs.resource_group_name 9 | } 10 | 11 | ############################################################################# 12 | # RESOURCES 13 | ############################################################################# 14 | 15 | resource "azurerm_resource_group" "webapp" { 16 | name = "${local.prefix}-webapp" 17 | location = var.location 18 | } 19 | 20 | resource "azurerm_template_deployment" "webapp" { 21 | name = "webappdeployment" 22 | resource_group_name = azurerm_resource_group.webapp.name 23 | 24 | template_body = file("azuredeploy.json") 25 | 26 | parameters = { 27 | "webAppName" = "${local.prefix}-web" 28 | "vnetName" = data.terraform_remote_state.networking.outputs.vnet_name 29 | "subnetRef" = data.azurerm_subnet.appservice.id 30 | } 31 | 32 | deployment_mode = "Incremental" 33 | } -------------------------------------------------------------------------------- /2-sec-vnet/commands.txt: -------------------------------------------------------------------------------- 1 | ### The original Terraform marketplace item is gone! ### 2 | ## You have two options: 3 | 4 | # 1. Run the commands from your local workstation while logged into an account in the security subscription 5 | # 2. Run the Terraform config in the zz-terraform-vm directory 6 | 7 | # The config in the zz-terraform-vm directory mirrors what was in the marketplace item 8 | # except for provisioning an MSI, which we don't use anyway 9 | 10 | #Log into the Azure CLI 11 | az login 12 | 13 | # This time we are going to create resources in the security subscription 14 | # Be sure to select the security subscription for your SUB_NAME 15 | 16 | az account set -s SUB_NAME 17 | 18 | # If you're running in the remote Terraform VM you'll need to copy the main.tf contents 19 | # over to the VM. If you're running locally, you can simply navigate to the 2-sec-vnet directory 20 | # and run the commands from there. You won't be using the storage account for the remote backend, b/c 21 | # it doesn't exist. 22 | 23 | # Copy the ~/tfTemplate/remoteState.tf file to the working directory 24 | cp ~/tfTemplate/remoteState.tf . 25 | 26 | # Create the security environment 27 | terraform init 28 | terraform plan -out sec.tfplan 29 | terraform apply sec.tfplan 30 | 31 | # We're going to need the contents of the next-step.txt file for the next section 32 | # Run the following command to get the contents 33 | cat next-step.txt 34 | 35 | ##################################################################################################### 36 | 37 | # Now we are going to create the peering connection 38 | 39 | # These commands are run from the Cloud Shell 40 | # Make sure you're using the network subscription 41 | 42 | az account set -s NETWORK_SUB_NAME 43 | 44 | # Run the export commands from the next-step.txt file to prepare 45 | # the proper environment variables 46 | 47 | # Then copy over the contents of the 3-vnet-peering/vnet-peering.tf 48 | # file to the same named file in the cloud shell 1-main-vnet directory 49 | 50 | # Run the standard plan and apply to update the config 51 | terraform plan -var resource_group_name=main-vnet -out vnet.tfplan 52 | terraform apply "vnet.tfplan" -------------------------------------------------------------------------------- /2-sec-vnet/main.tf: -------------------------------------------------------------------------------- 1 | ############################################################################# 2 | # TERRAFORM CONFIG 3 | ############################################################################# 4 | 5 | terraform { 6 | required_providers { 7 | azurerm = { 8 | source = "hashicorp/azurerm" 9 | version = "~> 2.0" 10 | } 11 | 12 | azuread = { 13 | source = "hashicorp/azuread" 14 | version = "~> 1.0" 15 | } 16 | 17 | local = { 18 | source = "hashicorp/local" 19 | version = "~> 2.0" 20 | } 21 | } 22 | } 23 | 24 | ############################################################################# 25 | # VARIABLES 26 | ############################################################################# 27 | 28 | variable "sec_resource_group_name" { 29 | type = string 30 | default = "security" 31 | } 32 | 33 | variable "location" { 34 | type = string 35 | default = "eastus" 36 | } 37 | 38 | variable "vnet_cidr_range" { 39 | type = string 40 | default = "10.1.0.0/16" 41 | } 42 | 43 | variable "sec_subnet_prefixes" { 44 | type = list(string) 45 | default = ["10.1.0.0/24", "10.1.1.0/24"] 46 | } 47 | 48 | variable "sec_subnet_names" { 49 | type = list(string) 50 | default = ["siem", "inspect"] 51 | } 52 | 53 | ############################################################################# 54 | # DATA 55 | ############################################################################# 56 | 57 | data "azurerm_subscription" "current" {} 58 | 59 | ############################################################################# 60 | # PROVIDERS 61 | ############################################################################# 62 | 63 | provider "azurerm" { 64 | features {} 65 | } 66 | 67 | ############################################################################# 68 | # RESOURCES 69 | ############################################################################# 70 | 71 | ## NETWORKING ## 72 | 73 | resource "azurerm_resource_group" "vnet_sec" { 74 | name = var.sec_resource_group_name 75 | location = var.location 76 | } 77 | 78 | module "vnet-sec" { 79 | source = "Azure/vnet/azurerm" 80 | version = "~> 2.0" 81 | resource_group_name = azurerm_resource_group.vnet_sec.name 82 | vnet_name = var.sec_resource_group_name 83 | address_space = [var.vnet_cidr_range] 84 | subnet_prefixes = var.sec_subnet_prefixes 85 | subnet_names = var.sec_subnet_names 86 | nsg_ids = {} 87 | 88 | tags = { 89 | environment = "security" 90 | costcenter = "security" 91 | 92 | } 93 | 94 | depends_on = [azurerm_resource_group.vnet_sec] 95 | } 96 | 97 | ## AZURE AD SP ## 98 | 99 | resource "random_password" "vnet_peering" { 100 | length = 16 101 | special = true 102 | } 103 | 104 | resource "azuread_application" "vnet_peering" { 105 | display_name = "vnet-peer" 106 | } 107 | 108 | resource "azuread_service_principal" "vnet_peering" { 109 | application_id = azuread_application.vnet_peering.application_id 110 | } 111 | 112 | resource "azuread_service_principal_password" "vnet_peering" { 113 | service_principal_id = azuread_service_principal.vnet_peering.id 114 | value = random_password.vnet_peering.result 115 | end_date_relative = "17520h" 116 | } 117 | 118 | resource "azurerm_role_definition" "vnet-peering" { 119 | name = "allow-vnet-peering" 120 | scope = data.azurerm_subscription.current.id 121 | 122 | permissions { 123 | actions = ["Microsoft.Network/virtualNetworks/virtualNetworkPeerings/write", "Microsoft.Network/virtualNetworks/peer/action", "Microsoft.Network/virtualNetworks/virtualNetworkPeerings/read", "Microsoft.Network/virtualNetworks/virtualNetworkPeerings/delete"] 124 | not_actions = [] 125 | } 126 | 127 | assignable_scopes = [ 128 | data.azurerm_subscription.current.id, 129 | ] 130 | } 131 | 132 | resource "azurerm_role_assignment" "vnet" { 133 | scope = module.vnet-sec.vnet_id 134 | role_definition_id = azurerm_role_definition.vnet-peering.role_definition_resource_id 135 | principal_id = azuread_service_principal.vnet_peering.id 136 | } 137 | 138 | ############################################################################# 139 | # FILE OUTPUT 140 | ############################################################################# 141 | 142 | resource "local_file" "linux" { 143 | filename = "${path.module}/next-step.txt" 144 | content = < /dev/null 19 | AZ_REPO=$(lsb_release -cs) 20 | echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ $AZ_REPO main" | sudo tee /etc/apt/sources.list.d/azure-cli.list 21 | sudo apt-get update && sudo apt-get install azure-cli -y 22 | 23 | # Create a directory and the remote backend example file 24 | mkdir tfTemplate 25 | cat < ~/tfTemplate/remoteState.tf 26 | terraform { 27 | backend "azurerm" { 28 | storage_account_name = "${storage_account_name}" 29 | container_name = "terraform-state" 30 | key = "prod.terraform.tfstate" 31 | access_key = "${access_key}" 32 | } 33 | } 34 | 35 | TFF 36 | 37 | EOF -------------------------------------------------------------------------------- /zz-terraform-vm/terraform.tf: -------------------------------------------------------------------------------- 1 | ############################################################################# 2 | # TERRAFORM CONFIG 3 | ############################################################################# 4 | 5 | terraform { 6 | required_providers { 7 | azurerm = { 8 | source = "hashicorp/azurerm" 9 | version = "~> 2.0" 10 | } 11 | } 12 | } 13 | 14 | provider "azurerm" { 15 | features {} 16 | } -------------------------------------------------------------------------------- /zz-terraform-vm/variables.tf: -------------------------------------------------------------------------------- 1 | variable "resource_group_name" { 2 | type = string 3 | default = "terraform-vm" 4 | } 5 | 6 | variable "location" { 7 | type = string 8 | default = "eastus" 9 | } 10 | 11 | variable "vnet_cidr_range" { 12 | type = string 13 | default = "10.0.0.0/16" 14 | } 15 | 16 | variable "subnet_prefixes" { 17 | type = list(string) 18 | default = ["10.0.0.0/24"] 19 | } 20 | 21 | variable "subnet_names" { 22 | type = list(string) 23 | default = ["subnet1"] 24 | } 25 | 26 | resource "random_id" "id" { 27 | byte_length = 4 28 | } 29 | 30 | locals { 31 | storage_account_name = "terraformstate${lower(random_id.id.hex)}" 32 | vm_public_dns = "tfvm-${random_id.id.hex}" 33 | } -------------------------------------------------------------------------------- /zz-terraform-vm/vm.tf: -------------------------------------------------------------------------------- 1 | # Password for Terraform VM 2 | resource "random_password" "terraform_vm" { 3 | length = 16 4 | special = true 5 | } 6 | 7 | # Resource group for Terraform VM 8 | resource "azurerm_resource_group" "vnet_main" { 9 | name = var.resource_group_name 10 | location = var.location 11 | } 12 | 13 | # Network for Terraform VM 14 | module "vnet-main" { 15 | source = "Azure/vnet/azurerm" 16 | version = "~> 2.0" 17 | resource_group_name = azurerm_resource_group.vnet_main.name 18 | vnet_name = var.resource_group_name 19 | address_space = [var.vnet_cidr_range] 20 | subnet_prefixes = var.subnet_prefixes 21 | subnet_names = var.subnet_names 22 | nsg_ids = {} 23 | 24 | tags = { 25 | environment = "terraform" 26 | costcenter = "it" 27 | 28 | } 29 | 30 | depends_on = [azurerm_resource_group.vnet_main] 31 | } 32 | 33 | # Storage account for Terraform VM 34 | resource "azurerm_storage_account" "sa" { 35 | name = local.storage_account_name 36 | resource_group_name = azurerm_resource_group.vnet_main.name 37 | location = var.location 38 | account_tier = "Standard" 39 | account_replication_type = "LRS" 40 | 41 | } 42 | 43 | resource "azurerm_storage_container" "ct" { 44 | name = "terraform-state" 45 | storage_account_name = azurerm_storage_account.sa.name 46 | 47 | } 48 | 49 | # Compute deployment for Terraform VM using Ubuntu 50 | module "terraform-vm" { 51 | source = "Azure/compute/azurerm" 52 | version = "~> 3.0" 53 | resource_group_name = azurerm_resource_group.vnet_main.name 54 | vm_os_simple = "UbuntuServer" 55 | public_ip_dns = [local.vm_public_dns] 56 | vnet_subnet_id = module.vnet-main.vnet_subnets[0] 57 | remote_port = "22" 58 | admin_password = random_password.terraform_vm.result 59 | enable_ssh_key = false 60 | delete_os_disk_on_termination = true 61 | 62 | custom_data = base64encode(templatefile("${path.module}/terraform-vm.tmpl", { 63 | storage_account_name = azurerm_storage_account.sa.name 64 | access_key = azurerm_storage_account.sa.primary_access_key 65 | })) 66 | 67 | 68 | depends_on = [azurerm_resource_group.vnet_main] 69 | } 70 | 71 | output "public_ip_address" { 72 | value = module.terraform-vm.public_ip_address 73 | } 74 | 75 | output "password" { 76 | value = nonsensitive(random_password.terraform_vm.result) 77 | } --------------------------------------------------------------------------------