A deployment of App Service-hosted workloads typically experiences a separation of duties and lifecycle management in the area of prerequisites, the host network, the App Service plan, and finally the workload itself. This reference implementation is similar. Also, be aware our primary purpose is to illustrate the topology and decisions of a baseline cluster. We feel a “step-by-step” flow will help you learn the pieces of the solution and give you insight into the relationship between them. Ultimately, lifecycle/SDLC management of your cluster and its dependencies will depend on your situation (team roles, organizational standards, tooling, etc), and must be implemented as appropriate for your needs.
While the code here is located in one folder in a single repo, the steps are designed to mimic how an organization may break up the deployment of various Azure components across teams, into different code repos or have them run by different pipelines with specific credentials.
The code here is purposely written to avoid loops, complex variables and logic. In most cases, it is resource blocks, small modules and limited variables, with the goal of making it easier to determine what is being deployed and how they are connected. Resources are broken into separate files for future modularization or adjustments as needed by your organization.
In this example, state is stored in an Azure Storage account that was created out-of-band. All deployments reference this storage account to either store state or reference variables from other parts of the deployment however you may choose to use other tools for state managment, like Terraform Cloud after making the necessary code changes.
This section is organized using folders that match the steps outlined below. Make any necessary adjustments to the variables and settings within that folder to match the needs of your deployment.
An Azure AD user for the DevOps VM admin account and an Azure AD group is required for the SQL Admins. The group must be created before running the Terraform code. This is the minimum required information for the terraform.tfvars file that needs to be created in this folder.:
tenant_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
aad_admin_group_object_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
aad_admin_group_name = "Azure AD SQL Admins"
vm_aad_admin_username = "bob@contoso.com"
terraform init --upgrade
terraform plan
terraform apply --auto-approve
Take note of the output values from the Terraform deployment. These will be used in the next steps.
This is a manual step that is required to complete the private endpoint connection.
# Update the resource group name to match the one used in the deployment of the webapp
rg_name="rg-secure-baseline-dev"
webapp_id=$(az webapp list -g $rg_name --query "[].id" -o tsv)
fd_conn_id=$(az network private-endpoint-connection list --id $webapp_id --query "[?properties.provisioningState == 'Pending'].{id:id}" -o tsv)
az network private-endpoint-connection approve --id $fd_conn_id --description "Approved"
From a PowerShell terminal, connect to the DevOps VM using your Azure AD credentials (or Windows Hello). The exact az network bastion rdp command will be provided in the output of the Terraform deployment.
az upgrade
az network bastion rdp --name bast-bastion --resource-group rg-hub --target-resource-id /subscriptions/{subscription-id}/resourceGroups/{rg-name}/providers/Microsoft.Compute/virtualMachines/{vm-name} --disable-gateway
The Azure AD enrollment can take a few minutes to complete. Check: https://portal.manage-beta.microsoft.com/devices
If your organization requires device enrollment before accessing corporate resources (i.e. if you see an error “You can’t get there from here.” or “This device does not meet your organization’s compliance requirements”), enroll the Jumpbox to Azure AD by following the steps in Edge: open Edge and click “Sign in to sync data”, select “Work or school account”, and then press OK on “Allow my organization to manage my device”. It takes a few minutes for the policies to be applied, device scanned and confirmed as secure to access corporate resources. You will know that the process is complete.
Once completed, you should be able to connect to the SQL Server using the Azure AD account from SQL Server Management Studio. On the sample database (sample-db by default), run the following commands to create the user and grant minimal permissions (the exact command will be provided in the output of the Terraform deployment):
CREATE USER [web-app-name] FROM EXTERNAL PROVIDER;
ALTER ROLE db_datareader ADD MEMBER [web-app-name];
ALTER ROLE db_datawriter ADD MEMBER [web-app-name];
ALTER ROLE db_ddladmin ADD MEMBER [web-app-name];
GO
CREATE USER [web-app-name/slots/slot-name] FROM EXTERNAL PROVIDER;
ALTER ROLE db_datareader ADD MEMBER [web-app-name/slots/slot-name];
ALTER ROLE db_datawriter ADD MEMBER [web-app-name/slots/slot-name];
ALTER ROLE db_ddladmin ADD MEMBER [web-app-name/slots/slot-name];
GO
From a PowerShell terminal in your DevOps VM, you’ll need to add a Key Vault secret for Redis Cache connection string by executing az keyvault secret set. The exact command will be provided in the output of the Terraform deployment (terraform output -raw cmd_redis_connection_kvsecret).
az keyvault secret set --vault-name <keyvault-name> --name <kv-secret-name> --value <redis-cache-connection-string>
az network front-door frontend-endpoint show --front-door-name <front-door-name> --name <front-door-frontend-endpoint-name> --resource-group <front-door-resource-group>
The steps above assume that you will be creating the Hub and Spoke (Landing Zone) Network and supporting components using the code provided, where each step refers to state file information from the previous steps.
To deploy App Service into an existing network, use the App Service for Existing Cluster folder. Update the “existing-infra.variables.tf” file to reference the names and resource IDs of the pre-existing infrastructure.
| Name | Version |
|---|---|
| terraform | >=1.2 |
| azurecaf | >=1.2.22 |
| azurerm | >=3.34.0 |
| Name | Version |
|---|---|
| azurerm | 3.40.0 |
| Name | Source | Version |
|---|---|---|
| hub | ./modules/hub | n/a |
| spoke | ./modules/spoke | n/a |
| Name | Type |
|---|---|
| azurerm_virtual_network_peering.hub_to_spoke | resource |
| azurerm_virtual_network_peering.spoke_to_hub | resource |
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
| aad_admin_group_name | The name of the Azure AD group that should be granted SQL Admin permissions to the SQL Server | string |
n/a | yes |
| aad_admin_group_object_id | The object ID of the Azure AD group that should be granted SQL Admin permissions to the SQL Server | string |
n/a | yes |
| application_name | The name of your application | string |
"secure-baseline" |
no |
| appsvc_int_subnet_cidr | The CIDR block for the subnet. | list(string) |
null |
no |
| bastion_subnet_cidr | The CIDR block for the bastion subnet. | string |
null |
no |
| deployment_options | Opt-in settings for the deployment: enable WAF in Front Door, deploy Azure Firewall and UDRs in the spoke network to force outbound traffic to the Azure Firewall, deploy Redis Cache. | <pre>object({ enable_waf = bool enable_egress_lockdown = bool deploy_redis = bool })</pre> |
<pre>{ “deploy_redis”: true, “enable_egress_lockdown”: true, “enable_waf”: true }</pre> |
no |
| devops_subnet_cidr | The CIDR block for the subnet. | list(string) |
null |
no |
| environment | The environment (dev, qa, staging, prod) | string |
"dev" |
no |
| firewall_subnet_cidr | The CIDR block for the firewall subnet. | string |
null |
no |
| front_door_subnet_cidr | The CIDR block for the subnet. | list(string) |
null |
no |
| hub_vnet_cidr | The CIDR block for the hub virtual network. | list(string) |
null |
no |
| location | The Azure region where all resources in this example should be created | string |
"westeurope" |
no |
| private_link_subnet_cidr | The CIDR block for the subnet. | list(string) |
null |
no |
| spoke_vnet_cidr | The CIDR block for the virtual network. | list(string) |
null |
no |
| tenant_id | The Azure AD tenant ID for the identities | string |
n/a | yes |
| vm_aad_admin_username | The Azure AD username for the VM admin account. | string |
n/a | yes |
| vm_admin_password | The password for the local VM admin account. Prefer using the Azure AD admin account. | string |
null |
no |
| vm_admin_username | The username for the local VM admin account. Prefer using the Azure AD admin account. | string |
null |
no |
| webapp_slot_name | The name of the app service slot | string |
"deployment" |
no |
| Name | Description | |——|————-| | cmd_devops_vm_rdp | n/a | | cmd_grant_sql_permissions | n/a | | cmd_redis_connection_kvsecret | n/a | | cmd_swap_slots | n/a | | sql_db_connection_string | n/a | | vault_uri | n/a | | web_app_uri | n/a |