Skip to content

Commit

Permalink
[DEVEX-131] New TF module to create an Azure App Service (#26)
Browse files Browse the repository at this point in the history
  • Loading branch information
Krusty93 authored May 27, 2024
1 parent 8d33535 commit d4154ae
Show file tree
Hide file tree
Showing 11 changed files with 461 additions and 0 deletions.
65 changes: 65 additions & 0 deletions infra/modules/azure_app_service/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# DX - Azure App Service Module

<!-- markdownlint-disable -->
<!-- BEGINNING OF PRE-COMMIT-TERRAFORM DOCS HOOK -->
## Requirements

| Name | Version |
|------|---------|
| <a name="requirement_azurerm"></a> [azurerm](#requirement\_azurerm) | >= 3.100.0 |

## Providers

| Name | Version |
|------|---------|
| <a name="provider_azurerm"></a> [azurerm](#provider\_azurerm) | 3.105.0 |

## Modules

| Name | Source | Version |
|------|--------|---------|
| <a name="module_naming_convention"></a> [naming\_convention](#module\_naming\_convention) | ../azure_naming_convention | n/a |

## Resources

| Name | Type |
|------|------|
| [azurerm_linux_web_app.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/linux_web_app) | resource |
| [azurerm_linux_web_app_slot.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/linux_web_app_slot) | resource |
| [azurerm_private_endpoint.app_service_sites](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/private_endpoint) | resource |
| [azurerm_private_endpoint.staging_app_service_sites](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/private_endpoint) | resource |
| [azurerm_service_plan.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/service_plan) | resource |
| [azurerm_subnet.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/subnet) | resource |
| [azurerm_private_dns_zone.app_service](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/private_dns_zone) | data source |
| [azurerm_virtual_network.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/virtual_network) | data source |

## Inputs

| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
| <a name="input_app_service_plan_id"></a> [app\_service\_plan\_id](#input\_app\_service\_plan\_id) | (Optional) Set the AppService Id where you want to host the Function App | `string` | `null` | no |
| <a name="input_app_settings"></a> [app\_settings](#input\_app\_settings) | Application settings | `map(string)` | n/a | yes |
| <a name="input_application_insights_connection_string"></a> [application\_insights\_connection\_string](#input\_application\_insights\_connection\_string) | (Optional) Application Insights connection string | `string` | `null` | no |
| <a name="input_application_insights_sampling_percentage"></a> [application\_insights\_sampling\_percentage](#input\_application\_insights\_sampling\_percentage) | (Optional) The sampling percentage of Application Insights. Default is 5 | `number` | `5` | no |
| <a name="input_environment"></a> [environment](#input\_environment) | Values which are used to generate resource names and location short names. They are all mandatory except for domain, which should not be used only in the case of a resource used by multiple domains. | <pre>object({<br> prefix = string<br> env_short = string<br> location = string<br> domain = optional(string)<br> app_name = string<br> instance_number = string<br> })</pre> | n/a | yes |
| <a name="input_health_check_path"></a> [health\_check\_path](#input\_health\_check\_path) | Endpoint where health probe is exposed | `string` | n/a | yes |
| <a name="input_java_version"></a> [java\_version](#input\_java\_version) | Java version to use | `string` | `17` | no |
| <a name="input_node_version"></a> [node\_version](#input\_node\_version) | Node version to use | `number` | `20` | no |
| <a name="input_private_dns_zone_resource_group_name"></a> [private\_dns\_zone\_resource\_group\_name](#input\_private\_dns\_zone\_resource\_group\_name) | (Optional) The name of the resource group holding private DNS zone to use for private endpoints. Default is Virtual Network resource group | `string` | `null` | no |
| <a name="input_resource_group_name"></a> [resource\_group\_name](#input\_resource\_group\_name) | Resource group to deploy resources to | `string` | n/a | yes |
| <a name="input_slot_app_settings"></a> [slot\_app\_settings](#input\_slot\_app\_settings) | Staging slot application settings | `map(string)` | `{}` | no |
| <a name="input_stack"></a> [stack](#input\_stack) | n/a | `string` | `"node"` | no |
| <a name="input_sticky_app_setting_names"></a> [sticky\_app\_setting\_names](#input\_sticky\_app\_setting\_names) | (Optional) A list of application setting names that are not swapped between slots | `list(string)` | `[]` | no |
| <a name="input_subnet_cidr"></a> [subnet\_cidr](#input\_subnet\_cidr) | CIDR block to use for the subnet the Function App uses for outbound connectivity | `string` | n/a | yes |
| <a name="input_subnet_pep_id"></a> [subnet\_pep\_id](#input\_subnet\_pep\_id) | Id of the subnet which holds private endpoints | `string` | n/a | yes |
| <a name="input_tags"></a> [tags](#input\_tags) | Resources tags | `map(any)` | n/a | yes |
| <a name="input_tier"></a> [tier](#input\_tier) | Resource tiers depending on demanding workload. Allowed values are 'premium', 'standard', 'test'. Note, "test" does not support deployment slots. | `string` | `"premium"` | no |
| <a name="input_virtual_network"></a> [virtual\_network](#input\_virtual\_network) | Virtual network in which to create the subnet | <pre>object({<br> name = string<br> resource_group_name = string<br> })</pre> | n/a | yes |

## Outputs

| Name | Description |
|------|-------------|
| <a name="output_app_service"></a> [app\_service](#output\_app\_service) | n/a |
| <a name="output_subnet"></a> [subnet](#output\_subnet) | n/a |
<!-- END OF PRE-COMMIT-TERRAFORM DOCS HOOK -->
66 changes: 66 additions & 0 deletions infra/modules/azure_app_service/app_service.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
resource "azurerm_linux_web_app" "this" {
name = "${local.project}-${var.environment.domain}-${var.environment.app_name}-app-${var.environment.instance_number}"
location = var.environment.location
resource_group_name = var.resource_group_name

service_plan_id = local.app_service_plan.enable ? azurerm_service_plan.this[0].id : var.app_service_plan_id

https_only = true
public_network_access_enabled = false
virtual_network_subnet_id = azurerm_subnet.this.id

identity {
type = "SystemAssigned"
}

site_config {
http2_enabled = true
always_on = true
vnet_route_all_enabled = true
health_check_path = var.health_check_path
health_check_eviction_time_in_min = 2
ip_restriction_default_action = "Deny"

application_stack {
node_version = var.stack == "node" ? "${var.node_version}-lts" : null
java_version = var.stack == "java" ? var.java_version : null
}
}

app_settings = merge(
{
# https://github.com/projectkudu/kudu/wiki/Configurable-settings#attempt-to-rename-dlls-if-they-cant-be-copied-during-a-webdeploy-deployment-1
WEBSITE_ADD_SITENAME_BINDINGS_IN_APPHOST_CONFIG = 1
# https://learn.microsoft.com/en-us/azure/azure-functions/run-functions-from-deployment-package#using-website_run_from_package--1
WEBSITE_RUN_FROM_PACKAGE = 1
# https://docs.microsoft.com/en-us/azure/virtual-network/what-is-ip-address-168-63-129-16
WEBSITE_DNS_SERVER = "168.63.129.16"
},
var.app_settings,
local.application_insights.enable ? {
# https://learn.microsoft.com/en-us/azure/azure-functions/functions-app-settings#applicationinsights_connection_string
APPLICATIONINSIGHTS_CONNECTION_STRING = var.application_insights_connection_string
# https://docs.microsoft.com/en-us/azure/azure-monitor/app/sampling
APPINSIGHTS_SAMPLING_PERCENTAGE = var.application_insights_sampling_percentage
} : {}
)

dynamic "sticky_settings" {
for_each = length(var.sticky_app_setting_names) == 0 ? [] : [1]
content {
app_setting_names = var.sticky_app_setting_names
}
}

lifecycle {
ignore_changes = [
app_settings["DOCKER_CUSTOM_IMAGE_NAME"],
app_settings["WEBSITE_HEALTHCHECK_MAXPINGFAILURES"],
tags["hidden-link: /app-insights-conn-string"],
tags["hidden-link: /app-insights-instrumentation-key"],
tags["hidden-link: /app-insights-resource-id"]
]
}

tags = var.tags
}
12 changes: 12 additions & 0 deletions infra/modules/azure_app_service/app_service_plan.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
resource "azurerm_service_plan" "this" {
count = local.app_service_plan.enable ? 1 : 0

name = "${local.project}-${var.environment.domain}-${var.environment.app_name}-asp-${var.environment.instance_number}"
location = var.environment.location
resource_group_name = var.resource_group_name
os_type = "Linux"
sku_name = local.app_service.sku_name
zone_balancing_enabled = local.app_service.zone_balancing_enabled

tags = var.tags
}
59 changes: 59 additions & 0 deletions infra/modules/azure_app_service/app_service_slot.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
resource "azurerm_linux_web_app_slot" "this" {
count = local.app_service.is_slot_enabled

name = "${local.project}-${var.environment.domain}-${var.environment.app_name}-staging-app-${var.environment.instance_number}"

app_service_id = azurerm_linux_web_app.this.id

https_only = true
public_network_access_enabled = false
virtual_network_subnet_id = azurerm_subnet.this.id

identity {
type = "SystemAssigned"
}

site_config {
http2_enabled = true
always_on = true
vnet_route_all_enabled = true
health_check_path = var.health_check_path
health_check_eviction_time_in_min = 2
ip_restriction_default_action = "Deny"

application_stack {
node_version = var.stack == "node" ? var.node_version : null
java_version = var.stack == "java" ? var.java_version : null
}
}

app_settings = merge(
{
# https://github.com/projectkudu/kudu/wiki/Configurable-settings#attempt-to-rename-dlls-if-they-cant-be-copied-during-a-webdeploy-deployment-1
WEBSITE_ADD_SITENAME_BINDINGS_IN_APPHOST_CONFIG = 1
# https://learn.microsoft.com/en-us/azure/azure-functions/run-functions-from-deployment-package#using-website_run_from_package--1
WEBSITE_RUN_FROM_PACKAGE = 1
# https://docs.microsoft.com/en-us/azure/virtual-network/what-is-ip-address-168-63-129-16
WEBSITE_DNS_SERVER = "168.63.129.16"
},
var.slot_app_settings,
local.application_insights.enable ? {
# https://learn.microsoft.com/en-us/azure/azure-functions/functions-app-settings#applicationinsights_connection_string
APPLICATIONINSIGHTS_CONNECTION_STRING = var.application_insights_connection_string
# https://docs.microsoft.com/en-us/azure/azure-monitor/app/sampling
APPINSIGHTS_SAMPLING_PERCENTAGE = var.application_insights_sampling_percentage
} : {}
)

lifecycle {
ignore_changes = [
app_settings["DOCKER_CUSTOM_IMAGE_NAME"],
app_settings["WEBSITE_HEALTHCHECK_MAXPINGFAILURES"],
tags["hidden-link: /app-insights-conn-string"],
tags["hidden-link: /app-insights-instrumentation-key"],
tags["hidden-link: /app-insights-resource-id"]
]
}

tags = var.tags
}
9 changes: 9 additions & 0 deletions infra/modules/azure_app_service/data.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
data "azurerm_virtual_network" "this" {
name = var.virtual_network.name
resource_group_name = var.virtual_network.resource_group_name
}

data "azurerm_private_dns_zone" "app_service" {
name = "privatelink.azurewebsites.net"
resource_group_name = local.private_dns_zone.resource_group_name
}
22 changes: 22 additions & 0 deletions infra/modules/azure_app_service/locals.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
locals {
location_short = var.environment.location == "italynorth" ? "itn" : var.environment.location == "westeurope" ? "weu" : var.environment.location == "germanywestcentral" ? "gwc" : "neu"
project = "${var.environment.prefix}-${var.environment.env_short}-${local.location_short}"

app_service_plan = {
enable = var.app_service_plan_id == null
}

app_service = {
sku_name = var.tier == "test" ? "B1" : var.tier == "standard" ? "P0v3" : "P1v3"
zone_balancing_enabled = var.tier != "test"
is_slot_enabled = var.tier == "test" ? 0 : 1
}

application_insights = {
enable = var.application_insights_connection_string != null
}

private_dns_zone = {
resource_group_name = var.private_dns_zone_resource_group_name == null ? var.virtual_network.resource_group_name : var.private_dns_zone_resource_group_name
}
}
25 changes: 25 additions & 0 deletions infra/modules/azure_app_service/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = ">= 3.100.0"
}
}
}

provider "azurerm" {
features {}
}

module "naming_convention" {
source = "../azure_naming_convention"

environment = {
prefix = var.environment.prefix
env_short = var.environment.env_short
location = var.environment.location
domain = var.environment.domain
app_name = var.environment.app_name
instance_number = var.environment.instance_number
}
}
43 changes: 43 additions & 0 deletions infra/modules/azure_app_service/networking.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
resource "azurerm_private_endpoint" "app_service_sites" {
name = "${local.project}-${var.environment.domain}-${var.environment.app_name}-app-pep-${var.environment.instance_number}"
location = var.environment.location
resource_group_name = var.resource_group_name
subnet_id = var.subnet_pep_id

private_service_connection {
name = "${local.project}-${var.environment.domain}-${var.environment.app_name}-app-pep-${var.environment.instance_number}"
private_connection_resource_id = azurerm_linux_web_app.this.id
is_manual_connection = false
subresource_names = ["sites"]
}

private_dns_zone_group {
name = "private-dns-zone-group"
private_dns_zone_ids = [data.azurerm_private_dns_zone.app_service.id]
}

tags = var.tags
}

resource "azurerm_private_endpoint" "staging_app_service_sites" {
count = local.app_service.is_slot_enabled

name = "${local.project}-${var.environment.domain}-${var.environment.app_name}-staging-app-pep-${var.environment.instance_number}"
location = var.environment.location
resource_group_name = var.resource_group_name
subnet_id = var.subnet_pep_id

private_service_connection {
name = "${local.project}-${var.environment.domain}-${var.environment.app_name}-staging-app-pep-${var.environment.instance_number}"
private_connection_resource_id = azurerm_linux_web_app.this.id
is_manual_connection = false
subresource_names = ["sites-${azurerm_linux_web_app_slot.this[0].name}"]
}

private_dns_zone_group {
name = "private-dns-zone-group"
private_dns_zone_ids = [data.azurerm_private_dns_zone.app_service.id]
}

tags = var.tags
}
26 changes: 26 additions & 0 deletions infra/modules/azure_app_service/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
output "subnet" {
value = {
id = azurerm_subnet.this.id
name = azurerm_subnet.this.name
}
}

output "app_service" {
value = {
resource_group_name = azurerm_linux_web_app.this.resource_group_name
plan = {
id = try(azurerm_service_plan.this[0].id, null)
name = try(azurerm_service_plan.this[0].name, null)
}
app_service = {
id = azurerm_linux_web_app.this.id
name = azurerm_linux_web_app.this.name
principal_id = azurerm_linux_web_app.this.identity[0].principal_id
slot = {
id = try(azurerm_linux_web_app_slot.this[0].id, null)
name = try(azurerm_linux_web_app_slot.this[0].name, null)
principal_id = try(azurerm_linux_web_app_slot.this[0].identity[0].principal_id, null)
}
}
}
}
14 changes: 14 additions & 0 deletions infra/modules/azure_app_service/subnets.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
resource "azurerm_subnet" "this" {
name = "${local.project}-${var.environment.domain}-${var.environment.app_name}-app-snet-${var.environment.instance_number}"
virtual_network_name = data.azurerm_virtual_network.this.name
resource_group_name = data.azurerm_virtual_network.this.resource_group_name
address_prefixes = [var.subnet_cidr]

delegation {
name = "default"
service_delegation {
name = "Microsoft.Web/serverFarms"
actions = ["Microsoft.Network/virtualNetworks/subnets/action"]
}
}
}
Loading

0 comments on commit d4154ae

Please sign in to comment.