diff --git a/README.md b/README.md index 00a0f76..a23aa40 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,21 @@ To regenerate the readme, run `npm run readme` - resource aws_ssm_parameter - resource random_string - [azure](azure) + - [azure/azure_linux_docker_app_service](azure/azure_linux_docker_app_service) + - resource azurerm_app_service + - resource azurerm_app_service_plan + - resource azurerm_app_service_slot + - resource azurerm_application_insights + - resource azurerm_application_insights_web_test + - resource azurerm_container_registry + - resource azurerm_key_vault + - resource azurerm_key_vault_access_policy + - resource azurerm_key_vault_secret + - resource azurerm_monitor_action_group + - resource azurerm_monitor_metric_alert + - resource azurerm_monitor_scheduled_query_rules_alert + - resource azurerm_role_assignment + - resource random_string - [azure/layers](azure/layers) - resource azurerm_resource_group - resource azurerm_storage_account @@ -1323,6 +1338,101 @@ p.s. Instead of environment variables, you can obviously use .tfvar files for as # Azure Examples +# [azure/azure_linux_docker_app_service](azure/azure_linux_docker_app_service) +# azure_linux_docker_app_service + +This terraform example demonstrates how to create a container based Linux App Service with secret management and monitoring. + +## Features + +- [Managed identity](https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview) for authentication instead of credentials +- [Key vault references](https://docs.microsoft.com/en-us/azure/app-service/app-service-key-vault-references) for accessing secrets from App Service +- Email alerts for errors and failed availability checks +- Random suffix for resources requiring globally unique name + +## Azure services + +![Architecture](azure/azure_linux_docker_app_service/images/architecture.png) + +### [Azure Container Registry](https://azure.microsoft.com/en-us/services/) + +For storing container images + +- App Service pulls the image from the registry during deployment +- Authentication using managed identity + +### [Key Vault](https://azure.microsoft.com/en-us/services/key-vault/) + +For storing and accessing secrets + +- Access management using access policies + +### App Service plan & [App Service](https://azure.microsoft.com/en-us/services/app-service/) + +For hosting the application. App Service is created into the plan. If you have multiple App Services, it is possible to share the same plan among them. + +- The application's docker image is deployed from the container registry +- Managed identity for accessing the Key Vault & Container registry +- Deployment slot for high availability deploys +- App service has a lot of settings that can be configured. See all of them [here](https://github.com/projectkudu/kudu/wiki/Configurable-settings). + +### [Application Insights](https://docs.microsoft.com/en-us/azure/azure-monitor/app/app-insights-overview) + +User for monitoring, metrics, logs and alerts. + +- The application should use Application Insights library (e.g. for [Node.js](https://www.npmjs.com/package/applicationinsights)) to instrument the application and integrate it with App Insights +- Includes availability checks from multiple locations +- Email alert for: + - Failed availability checks + - Responses with 5xx response code + - Failed dependencies (e.g. database query or HTTP request fails) + +## Example usage + +Prerequisites + +- Azure account and a service principal +- Resource group +- Terraform [Azure Provider](https://www.terraform.io/docs/providers/azurerm/) set up + +```tf +module "my_app" { + # Required + resource_group_name = "my-resource-group" + alert_email_address = "example@example.com" + + # Optional (with their default values) + name_prefix = "azure-app-example--" + app_service_name = "appservice" + app_insights_app_type = "other" + app_service_plan_tier = "PremiumV2" + app_service_plan_size = "P1v2" +} +``` + +We can create rest of the resources with `terraform apply`. + +An example of a Node.js application can be found in `./example-app` directory. + +## Building an image and deploying to the App Service + +- [Using Github actions](https://docs.microsoft.com/en-us/azure/app-service/deploy-container-github-action) +- [Using Azure DevOps pipelines](https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/deploy/azure-rm-web-app-containers?view=azure-devops) + +## Inputs + +| Name | Description | Type | Default | Required | +| --------------------- | ------------------------------------------------------------------------------------------------------------------- | :----: | :---------------------: | :------: | +| resource_group_name | Name of the resource group where the resources are deployed | string | | yes | +| alert_email_address | Email address where alerts are sent | string | | yes | +| name_prefix | Name prefix to use for objects that need to be created (only lowercase alphanumeric characters and hyphens allowed) | string | `"azure-app-example--"` | no | +| app_service_name | Name of the app service to be created. Must be globally unique | string | `"appservice"` | no | +| app_insights_app_type | Application insights application type | string | `"other"` | no | +| app_service_plan_tier | App service plan tier | string | `"PremiumV2"` | no | +| app_service_plan_size | App service plan size | string | `"P1v2"` | no | + + + # [azure/layers](azure/layers) # Terraform Azure Layers example @@ -1348,7 +1458,11 @@ sh destroy.sh ${USER}trylayers - `destroy.sh` takes a quick, resource-group based approach to wiping out the whole deployment. - `layers.tf` lists each layer with associated dependencies. - `main.tf` contains sample resources used on different layers. -- `variables.sh` declares associated variables with sane defaults. +- `variables.sh` declares associated variables with sane defaults + + +# [azure/layers](azure/layers) +. @@ -1463,11 +1577,7 @@ Terraform receipe for running Camunda BPMN workflow engine serverlessly on Cloud Customize the base image in the main.tf locals. Read more on the blog -- [Provisioning Serverless Ca - - -# [google_cloud/camunda-secure](google_cloud/camunda-secure) -munda on Cloud Run](https://www.futurice.com/blog/serverless-camunda-terraform-recipe-using-cloud-run-and-cloud-sql) +- [Provisioning Serverless Camunda on Cloud Run](https://www.futurice.com/blog/serverless-camunda-terraform-recipe-using-cloud-run-and-cloud-sql) - [Call external services with at-least-once delevery](https://www.futurice.com/blog/at-least-once-delivery-for-serverless-camunda-workflow-automation) diff --git a/azure/azure_linux_docker_app_service/README.md b/azure/azure_linux_docker_app_service/README.md new file mode 100644 index 0000000..80e7769 --- /dev/null +++ b/azure/azure_linux_docker_app_service/README.md @@ -0,0 +1,91 @@ +# azure_linux_docker_app_service + +This terraform example demonstrates how to create a container based Linux App Service with secret management and monitoring. + +## Features + +- [Managed identity](https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview) for authentication instead of credentials +- [Key vault references](https://docs.microsoft.com/en-us/azure/app-service/app-service-key-vault-references) for accessing secrets from App Service +- Email alerts for errors and failed availability checks +- Random suffix for resources requiring globally unique name + +## Azure services + +![Architecture](images/architecture.png) + +### [Azure Container Registry](https://azure.microsoft.com/en-us/services/) + +For storing container images + +- App Service pulls the image from the registry during deployment +- Authentication using managed identity + +### [Key Vault](https://azure.microsoft.com/en-us/services/key-vault/) + +For storing and accessing secrets + +- Access management using access policies + +### App Service plan & [App Service](https://azure.microsoft.com/en-us/services/app-service/) + +For hosting the application. App Service is created into the plan. If you have multiple App Services, it is possible to share the same plan among them. + +- The application's docker image is deployed from the container registry +- Managed identity for accessing the Key Vault & Container registry +- Deployment slot for high availability deploys +- App service has a lot of settings that can be configured. See all of them [here](https://github.com/projectkudu/kudu/wiki/Configurable-settings). + +### [Application Insights](https://docs.microsoft.com/en-us/azure/azure-monitor/app/app-insights-overview) + +User for monitoring, metrics, logs and alerts. + +- The application should use Application Insights library (e.g. for [Node.js](https://www.npmjs.com/package/applicationinsights)) to instrument the application and integrate it with App Insights +- Includes availability checks from multiple locations +- Email alert for: + - Failed availability checks + - Responses with 5xx response code + - Failed dependencies (e.g. database query or HTTP request fails) + +## Example usage + +Prerequisites + +- Azure account and a service principal +- Resource group +- Terraform [Azure Provider](https://www.terraform.io/docs/providers/azurerm/) set up + +```tf +module "my_app" { + # Required + resource_group_name = "my-resource-group" + alert_email_address = "example@example.com" + + # Optional (with their default values) + name_prefix = "azure-app-example--" + app_service_name = "appservice" + app_insights_app_type = "other" + app_service_plan_tier = "PremiumV2" + app_service_plan_size = "P1v2" +} +``` + +We can create rest of the resources with `terraform apply`. + +An example of a Node.js application can be found in `./example-app` directory. + +## Building an image and deploying to the App Service + +- [Using Github actions](https://docs.microsoft.com/en-us/azure/app-service/deploy-container-github-action) +- [Using Azure DevOps pipelines](https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/deploy/azure-rm-web-app-containers?view=azure-devops) + +## Inputs + +| Name | Description | Type | Default | Required | +| --------------------- | ------------------------------------------------------------------------------------------------------------------- | :----: | :---------------------: | :------: | +| resource_group_name | Name of the resource group where the resources are deployed | string | | yes | +| alert_email_address | Email address where alerts are sent | string | | yes | +| name_prefix | Name prefix to use for objects that need to be created (only lowercase alphanumeric characters and hyphens allowed) | string | `"azure-app-example--"` | no | +| app_service_name | Name of the app service to be created. Must be globally unique | string | `"appservice"` | no | +| app_insights_app_type | Application insights application type | string | `"other"` | no | +| app_service_plan_tier | App service plan tier | string | `"PremiumV2"` | no | +| app_service_plan_size | App service plan size | string | `"P1v2"` | no | diff --git a/azure/azure_linux_docker_app_service/access_policies.tf b/azure/azure_linux_docker_app_service/access_policies.tf new file mode 100644 index 0000000..ed46b3e --- /dev/null +++ b/azure/azure_linux_docker_app_service/access_policies.tf @@ -0,0 +1,51 @@ +# Key vault access for the current client principal +resource "azurerm_key_vault_access_policy" "principal" { + key_vault_id = azurerm_key_vault.current.id + + tenant_id = data.azurerm_client_config.current.tenant_id + object_id = data.azurerm_client_config.current.object_id + + secret_permissions = [ + "get", + "set", + "delete" + ] +} + +# Key vault access for the App Service +resource "azurerm_key_vault_access_policy" "app_service" { + key_vault_id = azurerm_key_vault.current.id + + tenant_id = data.azurerm_client_config.current.tenant_id + object_id = azurerm_app_service.current.identity.0.principal_id + + secret_permissions = [ + "get", + ] +} + +# Key vault access for the App Service's next slot +resource "azurerm_key_vault_access_policy" "app_service_next_slot" { + key_vault_id = azurerm_key_vault.current.id + + tenant_id = data.azurerm_client_config.current.tenant_id + object_id = azurerm_app_service_slot.next.identity.0.principal_id + + secret_permissions = [ + "get", + ] +} + +# Pull access for the app service +resource "azurerm_role_assignment" "app_service_acr_pull" { + scope = azurerm_container_registry.current.id + role_definition_name = "AcrPull" + principal_id = azurerm_app_service.current.identity.0.principal_id +} + +# Pull access for the app service's next slot +resource "azurerm_role_assignment" "app_service_next_slot_acr_pull" { + scope = azurerm_container_registry.current.id + role_definition_name = "AcrPull" + principal_id = azurerm_app_service_slot.next.identity.0.principal_id +} diff --git a/azure/azure_linux_docker_app_service/app_service.tf b/azure/azure_linux_docker_app_service/app_service.tf new file mode 100644 index 0000000..d7819d5 --- /dev/null +++ b/azure/azure_linux_docker_app_service/app_service.tf @@ -0,0 +1,117 @@ + +locals { + # Service plan needs to be unique only within the resource group + app_service_plan_name = "${var.name_prefix}app-service-plan" + # Needs to be globally unique + app_service_name = "${var.name_prefix}${var.app_service_name}" + + # https://github.com/projectkudu/kudu/wiki/Configurable-settings + app_service_settings = { + # Enable if you need a persistant file storage (/home/ directory) + WEBSITES_ENABLE_APP_SERVICE_STORAGE = false + + # Prevent recycling of the app when storage infra changes + # https://github.com/projectkudu/kudu/wiki/Configurable-settings#disable-the-generation-of-bindings-in-applicationhostconfig + WEBSITE_ADD_SITENAME_BINDINGS_IN_APPHOST_CONFIG = 1 + + APPINSIGHTS_INSTRUMENTATIONKEY = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault.current.vault_uri}secrets/app-insights-key)" + } + + app_service_site_config = { + always_on = true + min_tls_version = "1.2" + health_check_path = "/api/healthcheck" + use_32_bit_worker_process = false + } +} + +resource "azurerm_app_service_plan" "current" { + name = local.app_service_plan_name + location = data.azurerm_resource_group.current.location + resource_group_name = data.azurerm_resource_group.current.name + kind = "linux" + reserved = true + + sku { + tier = var.app_service_plan_tier + size = var.app_service_plan_size + } +} + +# App service +resource "azurerm_app_service" "current" { + name = local.app_service_name + location = data.azurerm_resource_group.current.location + resource_group_name = data.azurerm_resource_group.current.name + app_service_plan_id = azurerm_app_service_plan.current.id + + https_only = true + + site_config { + always_on = local.app_service_site_config.always_on + min_tls_version = local.app_service_site_config.min_tls_version + health_check_path = local.app_service_site_config.health_check_path + use_32_bit_worker_process = local.app_service_site_config.use_32_bit_worker_process + } + + app_settings = local.app_service_settings + + identity { + type = "SystemAssigned" + } + + lifecycle { + ignore_changes = [ + app_settings["DOCKER_CUSTOM_IMAGE_NAME"], + site_config.0.scm_type, + ] + } + + logs { + http_logs { + file_system { + retention_in_days = 7 + retention_in_mb = 100 + } + } + } + + # Use managed identity to login to ACR + # https://github.com/Azure/app-service-linux-docs/blob/master/HowTo/use_system-assigned_managed_identities.md + provisioner "local-exec" { + command = "az resource update --ids ${azurerm_app_service.current.id} --set properties.acrUseManagedIdentityCreds=True -o none" + } + + # Configure if you need EasyAuth + # auth_settings { + # } +} + +# Deployment slot for better availability during deployments +resource "azurerm_app_service_slot" "next" { + name = "${local.app_service_name}-next" + resource_group_name = data.azurerm_resource_group.current.name + location = data.azurerm_resource_group.current.location + app_service_name = azurerm_app_service.current.name + app_service_plan_id = azurerm_app_service_plan.current.id + + site_config { + always_on = local.app_service_site_config.always_on + min_tls_version = local.app_service_site_config.min_tls_version + health_check_path = local.app_service_site_config.health_check_path + use_32_bit_worker_process = local.app_service_site_config.use_32_bit_worker_process + } + + app_settings = local.app_service_settings + + identity { + type = "SystemAssigned" + } + + lifecycle { + ignore_changes = [ + app_settings["DOCKER_CUSTOM_IMAGE_NAME"], + site_config.0.scm_type, + ] + } +} diff --git a/azure/azure_linux_docker_app_service/data.tf b/azure/azure_linux_docker_app_service/data.tf new file mode 100644 index 0000000..1a4e6cd --- /dev/null +++ b/azure/azure_linux_docker_app_service/data.tf @@ -0,0 +1,5 @@ +data "azurerm_client_config" "current" {} + +data "azurerm_resource_group" "current" { + name = var.resource_group_name +} diff --git a/azure/azure_linux_docker_app_service/example-app/Dockerfile b/azure/azure_linux_docker_app_service/example-app/Dockerfile new file mode 100644 index 0000000..15b02cd --- /dev/null +++ b/azure/azure_linux_docker_app_service/example-app/Dockerfile @@ -0,0 +1,9 @@ +FROM node:14-slim + +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci +COPY index.js ./ + +EXPOSE 4000 +CMD ["node", "index.js"] diff --git a/azure/azure_linux_docker_app_service/example-app/build-and-push.sh b/azure/azure_linux_docker_app_service/example-app/build-and-push.sh new file mode 100755 index 0000000..a40943d --- /dev/null +++ b/azure/azure_linux_docker_app_service/example-app/build-and-push.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +TAG=$(date +%s) +ACR_URI=$1.azurecr.io +az acr login -n "$1" + +docker build -t "$ACR_URI/nodeapp:$TAG" . + +docker push "$ACR_URI/nodeapp:$TAG" + +echo "TAGGED AS $TAG" diff --git a/azure/azure_linux_docker_app_service/example-app/deploy.sh b/azure/azure_linux_docker_app_service/example-app/deploy.sh new file mode 100755 index 0000000..7346433 --- /dev/null +++ b/azure/azure_linux_docker_app_service/example-app/deploy.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +RG_NAME=$1 +APP_SERVICE_NAME=$2 +ACR_URI=$3.azurecr.io +TAG=$4 + +FX_Version="Docker|"$ACR_URI"/"nodeapp:$TAG +WEBAPP_ID=$(az webapp show -g "$RG_NAME" -n "$APP_SERVICE_NAME" --query id --output tsv)"/config/web" +az resource update --ids "$WEBAPP_ID" --set "properties.linuxFxVersion=$FX_Version" -o none --force-string diff --git a/azure/azure_linux_docker_app_service/example-app/index.js b/azure/azure_linux_docker_app_service/example-app/index.js new file mode 100644 index 0000000..3f15b21 --- /dev/null +++ b/azure/azure_linux_docker_app_service/example-app/index.js @@ -0,0 +1,70 @@ +const http = require("http"); +const appInsights = require("applicationinsights"); + +if (!process.env.APPINSIGHTS_INSTRUMENTATIONKEY) { + console.error("Missing APPINSIGHTS_INSTRUMENTATIONKEY config"); + process.exit(2); +} + +// Instrumentation key is loaded from APPINSIGHTS_INSTRUMENTATIONKEY env var +appInsights.setup().setAutoCollectConsole(true, true); +// The cloud role can be used to identify the app in Application Insights +appInsights.defaultClient.context.tags[ + appInsights.defaultClient.context.keys.cloudRole +] = "MyApplication"; +appInsights.start(); + +const port = 4000; + +async function connectToDatabase() { + try { + const connectionString = process.env.DB_URL; + const parsedConnectionString = parseConnectionString(connectionString); + await sql.connect({ + authentication: { + type: "azure-active-directory-msi-app-service", + options: { + // These are available when we have enabled system managed identity + msiEndpoint: process.env.MSI_ENDPOINT, + msiSecret: process.env.MSI_SECRET, + }, + }, + server: parsedConnectionString.host, + database: parsedConnectionString.database, + options: { + trustServerCertificate: false, + encrypt: true, + port: 1433, + }, + }); + + console.log("Connected successfully to database"); + } catch (err) { + console.error(err); + process.exit(2); + } +} + +async function startServer() { + const requestListener = async function (req, res) { + try { + console.log("Request to", req.url); + + res.writeHead(200); + res.end("My first server!"); + } catch (error) { + console.error(err); + res.writeHead(500); + res.end("2"); + } + }; + + const server = http.createServer(requestListener); + server.listen(port, () => { + console.log(`Server is running on port ${port}`); + }); +} + +(async function start() { + await startServer(); +})(); diff --git a/azure/azure_linux_docker_app_service/example-app/package-lock.json b/azure/azure_linux_docker_app_service/example-app/package-lock.json new file mode 100644 index 0000000..a1697cb --- /dev/null +++ b/azure/azure_linux_docker_app_service/example-app/package-lock.json @@ -0,0 +1,91 @@ +{ + "name": "app", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "applicationinsights": { + "version": "1.8.8", + "resolved": "https://registry.npmjs.org/applicationinsights/-/applicationinsights-1.8.8.tgz", + "integrity": "sha512-B43D4t/taGP5quGviVSdFWqarhIlzyGSi5mfngjbXpR2Ed3VrikJGIr1i5UtGzvwWqEbfIF6i298GvjFaB8RFA==", + "requires": { + "cls-hooked": "^4.2.2", + "continuation-local-storage": "^3.2.1", + "diagnostic-channel": "0.3.1", + "diagnostic-channel-publishers": "0.4.2" + } + }, + "async-hook-jl": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/async-hook-jl/-/async-hook-jl-1.7.6.tgz", + "integrity": "sha512-gFaHkFfSxTjvoxDMYqDuGHlcRyUuamF8s+ZTtJdDzqjws4mCt7v0vuV79/E2Wr2/riMQgtG4/yUtXWs1gZ7JMg==", + "requires": { + "stack-chain": "^1.3.7" + } + }, + "async-listener": { + "version": "0.6.10", + "resolved": "https://registry.npmjs.org/async-listener/-/async-listener-0.6.10.tgz", + "integrity": "sha512-gpuo6xOyF4D5DE5WvyqZdPA3NGhiT6Qf07l7DCB0wwDEsLvDIbCr6j9S5aj5Ch96dLace5tXVzWBZkxU/c5ohw==", + "requires": { + "semver": "^5.3.0", + "shimmer": "^1.1.0" + } + }, + "cls-hooked": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/cls-hooked/-/cls-hooked-4.2.2.tgz", + "integrity": "sha512-J4Xj5f5wq/4jAvcdgoGsL3G103BtWpZrMo8NEinRltN+xpTZdI+M38pyQqhuFU/P792xkMFvnKSf+Lm81U1bxw==", + "requires": { + "async-hook-jl": "^1.7.6", + "emitter-listener": "^1.0.1", + "semver": "^5.4.1" + } + }, + "continuation-local-storage": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/continuation-local-storage/-/continuation-local-storage-3.2.1.tgz", + "integrity": "sha512-jx44cconVqkCEEyLSKWwkvUXwO561jXMa3LPjTPsm5QR22PA0/mhe33FT4Xb5y74JDvt/Cq+5lm8S8rskLv9ZA==", + "requires": { + "async-listener": "^0.6.0", + "emitter-listener": "^1.1.1" + } + }, + "diagnostic-channel": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/diagnostic-channel/-/diagnostic-channel-0.3.1.tgz", + "integrity": "sha512-6eb9YRrimz8oTr5+JDzGmSYnXy5V7YnK5y/hd8AUDK1MssHjQKm9LlD6NSrHx4vMDF3+e/spI2hmWTviElgWZA==", + "requires": { + "semver": "^5.3.0" + } + }, + "diagnostic-channel-publishers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/diagnostic-channel-publishers/-/diagnostic-channel-publishers-0.4.2.tgz", + "integrity": "sha512-gbt5BVjwTV1wnng0Xi766DVrRxSeGECAX8Qrig7tKCDfXW2SbK7bKY6A3tgGjk5BB50aXgVXIsbtQiYIkt57Mg==" + }, + "emitter-listener": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/emitter-listener/-/emitter-listener-1.1.2.tgz", + "integrity": "sha512-Bt1sBAGFHY9DKY+4/2cV6izcKJUf5T7/gkdmkxzX/qv9CcGH8xSwVRW5mtX03SWJtRTWSOpzCuWN9rBFYZepZQ==", + "requires": { + "shimmer": "^1.2.0" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + }, + "shimmer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz", + "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==" + }, + "stack-chain": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/stack-chain/-/stack-chain-1.3.7.tgz", + "integrity": "sha1-0ZLJ/06moiyUxN1FkXHj8AzqEoU=" + } + } +} diff --git a/azure/azure_linux_docker_app_service/example-app/package.json b/azure/azure_linux_docker_app_service/example-app/package.json new file mode 100644 index 0000000..cd8c959 --- /dev/null +++ b/azure/azure_linux_docker_app_service/example-app/package.json @@ -0,0 +1,14 @@ +{ + "name": "app", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "MIT", + "dependencies": { + "applicationinsights": "^1.8.8" + } +} diff --git a/azure/azure_linux_docker_app_service/images/architecture.png b/azure/azure_linux_docker_app_service/images/architecture.png new file mode 100644 index 0000000..8f1cc11 Binary files /dev/null and b/azure/azure_linux_docker_app_service/images/architecture.png differ diff --git a/azure/azure_linux_docker_app_service/monitoring.tf b/azure/azure_linux_docker_app_service/monitoring.tf new file mode 100644 index 0000000..965179b --- /dev/null +++ b/azure/azure_linux_docker_app_service/monitoring.tf @@ -0,0 +1,125 @@ +locals { + healthcheck_endpoint = "https://${azurerm_app_service.current.default_site_hostname}/api/healthcheck" +} + +# Action group to send an email for alerts +resource "azurerm_monitor_action_group" "current" { + name = "SendAlertEmail" + resource_group_name = data.azurerm_resource_group.current.name + short_name = "Alert" + + email_receiver { + name = "sendtoemail" + email_address = var.alert_email_address + } +} + +# Availability ping +resource "azurerm_application_insights_web_test" "app_availability" { + name = "availability-${azurerm_app_service.current.name}" + resource_group_name = data.azurerm_resource_group.current.name + location = data.azurerm_resource_group.current.location + application_insights_id = azurerm_application_insights.current.id + kind = "ping" + frequency = 300 + timeout = 60 + enabled = true + geo_locations = ["emea-nl-ams-azr", "emea-ru-msa-edge", "emea-gb-db3-azr", "emea-fr-pra-edge", "us-va-ash-azr"] + + configuration = < + + + + +XML + + lifecycle { + ignore_changes = [ + tags + ] + } +} + +# Availability ping failed alert +resource "azurerm_monitor_metric_alert" "app_availability" { + name = "${azurerm_app_service.current.name} server availability" + resource_group_name = data.azurerm_resource_group.current.name + # Both the availability web test AND the application insights need to be in the scope + # https://github.com/terraform-providers/terraform-provider-azurerm/issues/8551 + scopes = [ + azurerm_application_insights_web_test.app_availability.id, + azurerm_application_insights.current.id + ] + + # Every 1 mins in 5 min window + frequency = "PT1M" + window_size = "PT5M" + # Critical + severity = 0 + + application_insights_web_test_location_availability_criteria { + web_test_id = azurerm_application_insights_web_test.app_availability.id + component_id = azurerm_application_insights.current.id + failed_location_count = 3 + } + + action { + action_group_id = azurerm_monitor_action_group.current.id + } +} + + +# HTTP 5xx errors +resource "azurerm_monitor_metric_alert" "ms_5xx_errors" { + name = "${azurerm_app_service.current.name} server had HTTP 5xx errors" + resource_group_name = data.azurerm_resource_group.current.name + scopes = [ + azurerm_app_service.current.id + ] + + # Every 15 mins in 15 min window + frequency = "PT15M" + window_size = "PT15M" + # Error + severity = 1 + + criteria { + metric_namespace = "Microsoft.Web/sites" + metric_name = "Http5xx" + aggregation = "Total" + operator = "GreaterThan" + threshold = 0 + } + + action { + action_group_id = azurerm_monitor_action_group.current.id + } +} + +# Dependency failures (e.g. HTTP request to another service or database query failed) +resource "azurerm_monitor_scheduled_query_rules_alert" "dependency_failures_in_app_service" { + name = "${azurerm_app_service.current.name} had dependency failures" + resource_group_name = data.azurerm_resource_group.current.name + location = data.azurerm_resource_group.current.location + data_source_id = azurerm_application_insights.current.id + frequency = 15 + time_window = 15 + # Error + severity = 1 + query = <<-QUERY + dependencies + | where resultCode == "False" +QUERY + + trigger { + operator = "GreaterThan" + threshold = 0 + } + + action { + action_group = [ + azurerm_monitor_action_group.current.id + ] + } +} diff --git a/azure/azure_linux_docker_app_service/outputs.tf b/azure/azure_linux_docker_app_service/outputs.tf new file mode 100644 index 0000000..14f63d9 --- /dev/null +++ b/azure/azure_linux_docker_app_service/outputs.tf @@ -0,0 +1,13 @@ +output "app_service_name" { + description = "This is the unique name of the App Service that was created" + value = azurerm_app_service.current.name +} + +output "app_service_url" { + description = "This is the URL of the App Service that was created" + value = azurerm_app_service.current.default_site_hostname +} + +output "container_registry" { + value = azurerm_container_registry.current.login_server +} diff --git a/azure/azure_linux_docker_app_service/provider.tf b/azure/azure_linux_docker_app_service/provider.tf new file mode 100644 index 0000000..93ee3fd --- /dev/null +++ b/azure/azure_linux_docker_app_service/provider.tf @@ -0,0 +1,14 @@ +# Configure the Azure Provider +provider "azurerm" { + version = "= 2.37.0" + skip_provider_registration = true + features {} +} + +provider "random" { + version = "~> 2.3" +} + +provider "template" { + version = "~> 2.1" +} diff --git a/azure/azure_linux_docker_app_service/secrets.tf b/azure/azure_linux_docker_app_service/secrets.tf new file mode 100644 index 0000000..ee67286 --- /dev/null +++ b/azure/azure_linux_docker_app_service/secrets.tf @@ -0,0 +1,8 @@ +# Application insights instrumentation key +resource "azurerm_key_vault_secret" "app_insights_instrumentation_key" { + key_vault_id = azurerm_key_vault.current.id + name = "app-insights-key" + value = azurerm_application_insights.current.instrumentation_key + + depends_on = [azurerm_key_vault_access_policy.principal] +} diff --git a/azure/azure_linux_docker_app_service/shared.tf b/azure/azure_linux_docker_app_service/shared.tf new file mode 100644 index 0000000..857ad76 --- /dev/null +++ b/azure/azure_linux_docker_app_service/shared.tf @@ -0,0 +1,46 @@ +# Since many services require a globally unique name (such as keyvault), +# generate random suffix for the resources +resource "random_string" "suffix" { + length = 8 + special = false + upper = false +} + +locals { + # Key vault and ACR required globally unique names. Only alphanumeric characters allowed + key_vault_name = "${local.cleansed_prefix}${random_string.suffix.result}" + acr_name = "${local.cleansed_prefix}${random_string.suffix.result}" + + # App insights needs to be unique only within the resource group + app_insights_name = "${var.name_prefix}app-insights" +} + +resource "azurerm_key_vault" "current" { + name = local.key_vault_name + location = data.azurerm_resource_group.current.location + resource_group_name = data.azurerm_resource_group.current.name + tenant_id = data.azurerm_client_config.current.tenant_id + + soft_delete_enabled = true + soft_delete_retention_days = 7 + purge_protection_enabled = false + + sku_name = "standard" +} + +resource "azurerm_container_registry" "current" { + name = local.acr_name + resource_group_name = data.azurerm_resource_group.current.name + location = data.azurerm_resource_group.current.location + sku = "Standard" + + # We'll be using AD login + admin_enabled = false +} + +resource "azurerm_application_insights" "current" { + name = local.app_insights_name + resource_group_name = data.azurerm_resource_group.current.name + location = data.azurerm_resource_group.current.location + application_type = var.app_insights_app_type +} diff --git a/azure/azure_linux_docker_app_service/variables.tf b/azure/azure_linux_docker_app_service/variables.tf new file mode 100644 index 0000000..eb785c5 --- /dev/null +++ b/azure/azure_linux_docker_app_service/variables.tf @@ -0,0 +1,45 @@ +variable "resource_group_name" { + type = string + description = "Name of the resource group where resources are to be deployed" +} + +variable "alert_email_address" { + type = string + description = "Email address where alert emails are sent" +} + +variable "name_prefix" { + type = string + description = "Name prefix to use for resources that need to be created (only lowercase characters and hyphens allowed)" + default = "azure-app-example--" +} + +variable "app_service_name" { + type = string + description = "Name for the app service" + default = "appservice" +} + +# https://www.terraform.io/docs/providers/azurerm/r/application_insights.html#application_type +variable "app_insights_app_type" { + type = string + description = "The type of Application Insights to create." + default = "other" +} + +# https://azure.microsoft.com/en-gb/pricing/details/app-service/linux/ +variable "app_service_plan_tier" { + type = string + description = "App service plan's tier" + default = "PremiumV2" +} + +variable "app_service_plan_size" { + type = string + description = "App service plan's size" + default = "P1v2" +} + +locals { + cleansed_prefix = replace(var.name_prefix, "/[^a-zA-Z0-9]+/", "") +}