diff --git a/.github/workflows/run-samples.yml b/.github/workflows/run-samples.yml index 1504ae2..0f32806 100644 --- a/.github/workflows/run-samples.yml +++ b/.github/workflows/run-samples.yml @@ -157,7 +157,20 @@ jobs: - name: Install Azure Functions Core Tools # Required for publishing function app samples to the emulator. - run: npm install -g azure-functions-core-tools@4 --unsafe-perm true + run: | + MAX_RETRIES=3 + DELAY=15 + for i in $(seq 1 $MAX_RETRIES); do + echo "Attempt $i/$MAX_RETRIES..." + npm install -g azure-functions-core-tools@4 --unsafe-perm true && break + if [ "$i" -eq "$MAX_RETRIES" ]; then + echo "All $MAX_RETRIES attempts failed" + exit 1 + fi + echo "Retrying in ${DELAY}s..." + sleep $DELAY + DELAY=$((DELAY * 2)) + done - name: Install MSSQL ODBC and Tools # Required for the 'web-app-sql-database' sample which uses 'sqlcmd' to diff --git a/.gitignore b/.gitignore index c7d539a..6f73d44 100644 --- a/.gitignore +++ b/.gitignore @@ -217,4 +217,13 @@ cython_debug/ # before adapting. # Usually, these files will be generated by your IDE, so it is # better to have them ignored. -.idea/ \ No newline at end of file +.idea/ +## Terraform +.terraform/ +*.tfstate +*.tfstate.backup +tfplan +override.tf +override.tf.json +*_override.tf +*_override.tf.json diff --git a/README.md b/README.md index e7cf2c1..a25929e 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ Follow the comprehensive setup guide in [LocalStack for Azure Quick Start](./doc ## Documentation -- [LocalStack for Azure Documentation](https://azure.localstack.cloud/) +- [LocalStack for Azure Documentation](https://docs.localstack.cloud/azure/) - [Azure CLI with LocalStack](https://azure.localstack.cloud/user-guides/sdks/az/) - [Supported Azure Services](https://azure.localstack.cloud/references/coverage/) diff --git a/samples/aci-blob-storage/python/bicep/README.md b/samples/aci-blob-storage/python/bicep/README.md index 680ab0b..7b8b9e8 100644 --- a/samples/aci-blob-storage/python/bicep/README.md +++ b/samples/aci-blob-storage/python/bicep/README.md @@ -4,7 +4,7 @@ This directory contains the Bicep template and a deployment script for provision ## Prerequisites -- [LocalStack for Azure](https://azure.localstack.cloud/): Local Azure cloud emulator for development and testing +- [LocalStack for Azure](https://docs.localstack.cloud/azure/): Local Azure cloud emulator for development and testing - [Docker](https://docs.docker.com/get-docker/): Container runtime required for LocalStack - [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli): Azure command-line interface - [Bicep extension](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-bicep): VS Code extension for Bicep language support @@ -59,4 +59,4 @@ bash scripts/cleanup.sh - [Azure Bicep Documentation](https://docs.microsoft.com/en-us/azure/azure-resource-manager/bicep/) - [Bicep Language Reference](https://docs.microsoft.com/en-us/azure/azure-resource-manager/bicep/bicep-functions) -- [LocalStack for Azure Documentation](https://azure.localstack.cloud/) +- [LocalStack for Azure Documentation](https://docs.localstack.cloud/azure/) diff --git a/samples/aci-blob-storage/python/scripts/README.md b/samples/aci-blob-storage/python/scripts/README.md index 42039bd..1cfdbd7 100644 --- a/samples/aci-blob-storage/python/scripts/README.md +++ b/samples/aci-blob-storage/python/scripts/README.md @@ -4,7 +4,7 @@ This directory includes Bash scripts for deploying and testing the ACI Vacation ## Prerequisites -- [LocalStack for Azure](https://azure.localstack.cloud/): Local Azure cloud emulator for development and testing +- [LocalStack for Azure](https://docs.localstack.cloud/azure/): Local Azure cloud emulator for development and testing - [Docker](https://docs.docker.com/get-docker/): Container runtime required for LocalStack - [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli): Azure command-line interface - [azlocal CLI](https://azure.localstack.cloud/user-guides/sdks/az/): LocalStack Azure CLI wrapper @@ -48,4 +48,4 @@ bash scripts/cleanup.sh ## Related Documentation - [Azure CLI Documentation](https://docs.microsoft.com/en-us/cli/azure/) -- [LocalStack for Azure Documentation](https://azure.localstack.cloud/) +- [LocalStack for Azure Documentation](https://docs.localstack.cloud/azure/) diff --git a/samples/aci-blob-storage/python/terraform/README.md b/samples/aci-blob-storage/python/terraform/README.md index 69da3ca..b487e9d 100644 --- a/samples/aci-blob-storage/python/terraform/README.md +++ b/samples/aci-blob-storage/python/terraform/README.md @@ -4,7 +4,7 @@ This directory contains Terraform modules and a deployment script for provisioni ## Prerequisites -- [LocalStack for Azure](https://azure.localstack.cloud/): Local Azure cloud emulator for development and testing +- [LocalStack for Azure](https://docs.localstack.cloud/azure/): Local Azure cloud emulator for development and testing - [Terraform](https://developer.hashicorp.com/terraform/downloads): Infrastructure as Code tool - [Docker](https://docs.docker.com/get-docker/): Container runtime required for LocalStack - [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli): Azure command-line interface @@ -66,4 +66,4 @@ rm -rf .terraform terraform.tfstate terraform.tfstate.backup .terraform.lock.hcl ## Related Documentation - [Terraform Azure Provider](https://registry.terraform.io/providers/hashicorp/azurerm/latest) -- [LocalStack for Azure Documentation](https://azure.localstack.cloud/) +- [LocalStack for Azure Documentation](https://docs.localstack.cloud/azure/) diff --git a/samples/function-app-managed-identity/python/README.md b/samples/function-app-managed-identity/python/README.md index a81d21f..4c84ad9 100644 --- a/samples/function-app-managed-identity/python/README.md +++ b/samples/function-app-managed-identity/python/README.md @@ -239,4 +239,4 @@ You can use [Azure Storage Explorer](https://learn.microsoft.com/en-us/azure/sto - [What is Azure Blob storage?](https://learn.microsoft.com/en-us/azure/storage/blobs/storage-blobs-overview) - [What is managed identities for Azure resources?](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview) - [How managed identities for Azure resources work with Azure virtual machines](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/how-managed-identities-work-vm) -- [LocalStack for Azure](https://azure.localstack.cloud/) \ No newline at end of file +- [LocalStack for Azure](https://docs.localstack.cloud/azure/) \ No newline at end of file diff --git a/samples/function-app-managed-identity/python/bicep/README.md b/samples/function-app-managed-identity/python/bicep/README.md index f15afc7..ec822ba 100644 --- a/samples/function-app-managed-identity/python/bicep/README.md +++ b/samples/function-app-managed-identity/python/bicep/README.md @@ -6,7 +6,7 @@ This directory contains the Bicep template and a deployment script for provision Before deploying this solution, ensure you have the following tools installed: -- [LocalStack for Azure](https://azure.localstack.cloud/): Local Azure cloud emulator for development and testing +- [LocalStack for Azure](https://docs.localstack.cloud/azure/): Local Azure cloud emulator for development and testing - [Visual Studio Code](https://code.visualstudio.com/): Code editor installed on one of the [supported platforms](https://code.visualstudio.com/docs/supporting/requirements#_platforms) - [Bicep extension](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-bicep): VS Code extension for Bicep language support and IntelliSense - [Docker](https://docs.docker.com/get-docker/): Container runtime required for LocalStack @@ -167,4 +167,4 @@ This will remove all Azure resources created by the CLI deployment script. - [Azure Bicep Documentation](https://docs.microsoft.com/en-us/azure/azure-resource-manager/bicep/) - [Bicep Language Reference](https://docs.microsoft.com/en-us/azure/azure-resource-manager/bicep/bicep-functions) -- [LocalStack for Azure Documentation](https://azure.localstack.cloud/) +- [LocalStack for Azure Documentation](https://docs.localstack.cloud/azure/) diff --git a/samples/function-app-managed-identity/python/scripts/README.md b/samples/function-app-managed-identity/python/scripts/README.md index 132f58e..30aba0d 100644 --- a/samples/function-app-managed-identity/python/scripts/README.md +++ b/samples/function-app-managed-identity/python/scripts/README.md @@ -6,7 +6,7 @@ This directory includes Bash scripts designed for deploying and testing the samp Before deploying this solution, ensure you have the following tools installed: -- [LocalStack for Azure](https://azure.localstack.cloud/): Local Azure cloud emulator for development and testing +- [LocalStack for Azure](https://docs.localstack.cloud/azure/): Local Azure cloud emulator for development and testing - [Visual Studio Code](https://code.visualstudio.com/): Code editor installed on one of the [supported platforms](https://code.visualstudio.com/docs/supporting/requirements#_platforms) - [Docker](https://docs.docker.com/get-docker/): Container runtime required for LocalStack - [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli): Azure command-line interface @@ -143,4 +143,4 @@ This will remove all Azure resources created by the CLI deployment script. ## Related Documentation - [Azure CLI Documentation](https://docs.microsoft.com/en-us/cli/azure/) -- [LocalStack for Azure Documentation](https://azure.localstack.cloud/) +- [LocalStack for Azure Documentation](https://docs.localstack.cloud/azure/) diff --git a/samples/function-app-managed-identity/python/terraform/README.md b/samples/function-app-managed-identity/python/terraform/README.md index 8bd1e34..a89f929 100644 --- a/samples/function-app-managed-identity/python/terraform/README.md +++ b/samples/function-app-managed-identity/python/terraform/README.md @@ -6,7 +6,7 @@ This directory contains Terraform modules and a deployment script for provisioni Before deploying this solution, ensure you have the following tools installed: -- [LocalStack for Azure](https://azure.localstack.cloud/): Local Azure cloud emulator for development and testing +- [LocalStack for Azure](https://docs.localstack.cloud/azure/): Local Azure cloud emulator for development and testing - [Visual Studio Code](https://code.visualstudio.com/): Code editor installed on one of the [supported platforms](https://code.visualstudio.com/docs/supporting/requirements#_platforms) - [Terraform](https://developer.hashicorp.com/terraform/downloads): Infrastructure as Code tool for provisioning Azure resources - [Python](https://www.python.org/downloads/): Python runtime (version 3.13 or above) @@ -171,4 +171,4 @@ This will remove all Azure resources created by the CLI deployment script. ## Related Documentation - [Terraform Azure Provider](https://registry.terraform.io/providers/hashicorp/azurerm/latest) -- [LocalStack for Azure Documentation](https://azure.localstack.cloud/) +- [LocalStack for Azure Documentation](https://docs.localstack.cloud/azure/) diff --git a/samples/function-app-service-bus/dotnet/README.md b/samples/function-app-service-bus/dotnet/README.md index a82fd6a..eb17668 100644 --- a/samples/function-app-service-bus/dotnet/README.md +++ b/samples/function-app-service-bus/dotnet/README.md @@ -219,4 +219,4 @@ You can also inspect the function app's runtime behavior by viewing the logs of - [Azure Functions Apps Documentation](https://learn.microsoft.com/en-us/azure/app-service/) - [Azure Service Bus](https://learn.microsoft.com/en-us/azure/service-bus-messaging/service-bus-messaging-overview) -- [LocalStack for Azure](https://azure.localstack.cloud/) \ No newline at end of file +- [LocalStack for Azure](https://docs.localstack.cloud/azure/) \ No newline at end of file diff --git a/samples/function-app-service-bus/dotnet/bicep/README.md b/samples/function-app-service-bus/dotnet/bicep/README.md index bca1b9d..cb2df22 100644 --- a/samples/function-app-service-bus/dotnet/bicep/README.md +++ b/samples/function-app-service-bus/dotnet/bicep/README.md @@ -7,7 +7,7 @@ This directory contains the Bicep template and a deployment script for provision Before deploying this solution, ensure you have the following tools installed: - [Azure Subscription](https://azure.microsoft.com/free/) -- [LocalStack for Azure](https://azure.localstack.cloud/): Local Azure cloud emulator for development and testing +- [LocalStack for Azure](https://docs.localstack.cloud/azure/): Local Azure cloud emulator for development and testing - [Visual Studio Code](https://code.visualstudio.com/): Code editor installed on one of the [supported platforms](https://code.visualstudio.com/docs/supporting/requirements#_platforms) - [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli): Azure command-line interface - [Azlocal CLI](https://azure.localstack.cloud/user-guides/sdks/az/): LocalStack Azure CLI wrapper @@ -310,4 +310,4 @@ This will remove all Azure resources created by the CLI deployment script. - [Azure Bicep Documentation](https://docs.microsoft.com/en-us/azure/azure-resource-manager/bicep/) - [Bicep Language Reference](https://docs.microsoft.com/en-us/azure/azure-resource-manager/bicep/bicep-functions) -- [LocalStack for Azure Documentation](https://azure.localstack.cloud/) \ No newline at end of file +- [LocalStack for Azure Documentation](https://docs.localstack.cloud/azure/) \ No newline at end of file diff --git a/samples/function-app-service-bus/dotnet/scripts/README.md b/samples/function-app-service-bus/dotnet/scripts/README.md index d935c05..0bcb818 100644 --- a/samples/function-app-service-bus/dotnet/scripts/README.md +++ b/samples/function-app-service-bus/dotnet/scripts/README.md @@ -7,7 +7,7 @@ This directory includes Bash scripts designed for deploying and testing the samp Before deploying this solution, ensure you have the following tools installed: - [Azure Subscription](https://azure.microsoft.com/free/) -- [LocalStack for Azure](https://azure.localstack.cloud/): Local Azure cloud emulator for development and testing +- [LocalStack for Azure](https://docs.localstack.cloud/azure/): Local Azure cloud emulator for development and testing - [Visual Studio Code](https://code.visualstudio.com/): Code editor installed on one of the [supported platforms](https://code.visualstudio.com/docs/supporting/requirements#_platforms) - [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli): Azure command-line interface - [Azlocal CLI](https://azure.localstack.cloud/user-guides/sdks/az/): LocalStack Azure CLI wrapper @@ -278,4 +278,4 @@ This will remove all Azure resources created by the CLI deployment script. ## Related Documentation - [Azure CLI Documentation](https://docs.microsoft.com/en-us/cli/azure/) -- [LocalStack for Azure Documentation](https://azure.localstack.cloud/) \ No newline at end of file +- [LocalStack for Azure Documentation](https://docs.localstack.cloud/azure/) \ No newline at end of file diff --git a/samples/function-app-service-bus/dotnet/terraform/README.md b/samples/function-app-service-bus/dotnet/terraform/README.md index 0feaede..f278281 100644 --- a/samples/function-app-service-bus/dotnet/terraform/README.md +++ b/samples/function-app-service-bus/dotnet/terraform/README.md @@ -7,7 +7,7 @@ This directory contains Terraform modules and a deployment script for provisioni Before deploying this solution, ensure you have the following tools installed: - [Azure Subscription](https://azure.microsoft.com/free/) -- [LocalStack for Azure](https://azure.localstack.cloud/): Local Azure cloud emulator for development and testing +- [LocalStack for Azure](https://docs.localstack.cloud/azure/): Local Azure cloud emulator for development and testing - [Visual Studio Code](https://code.visualstudio.com/): Code editor installed on one of the [supported platforms](https://code.visualstudio.com/docs/supporting/requirements#_platforms) - [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli): Azure command-line interface - [Azlocal CLI](https://azure.localstack.cloud/user-guides/sdks/az/): LocalStack Azure CLI wrapper @@ -314,4 +314,4 @@ This will remove all Azure resources created by the CLI deployment script. ## Related Documentation - [Terraform Azure Provider](https://registry.terraform.io/providers/hashicorp/azurerm/latest) -- [LocalStack for Azure Documentation](https://azure.localstack.cloud/) \ No newline at end of file +- [LocalStack for Azure Documentation](https://docs.localstack.cloud/azure/) \ No newline at end of file diff --git a/samples/function-app-service-bus/dotnet/terraform/variables.tf b/samples/function-app-service-bus/dotnet/terraform/variables.tf index 3edba4e..300259f 100644 --- a/samples/function-app-service-bus/dotnet/terraform/variables.tf +++ b/samples/function-app-service-bus/dotnet/terraform/variables.tf @@ -321,12 +321,6 @@ variable "nat_gateway_zones" { default = ["1"] } -variable "website_port" { - description = "(Optional) Specifies the port on which the Web App will listen. Defaults to 8000." - type = number - default = 8000 -} - variable "queue_names" { description = "(Optional) Specifies the names of the queues to be created within the Service Bus Namespace." type = set(string) diff --git a/samples/function-app-storage-http/dotnet/README.md b/samples/function-app-storage-http/dotnet/README.md index 0c5132c..3cd8424 100644 --- a/samples/function-app-storage-http/dotnet/README.md +++ b/samples/function-app-storage-http/dotnet/README.md @@ -1,6 +1,6 @@ # Azure Functions Sample with LocalStack for Azure -This sample demonstrates a comprehensive gaming scoreboard system built with [Azure Functions](https://learn.microsoft.com/en-us/azure/azure-functions/functions-overview) running against [LocalStack for Azure](https://azure.localstack.cloud/). The application showcases how Azure Functions can seamlessly interact with Azure Storage services (Queues, Blobs, and Tables) when both the Azure Function App and storage services are running in emulated fashion locally on your machine using LocalStack for Azure. +This sample demonstrates a comprehensive gaming scoreboard system built with [Azure Functions](https://learn.microsoft.com/en-us/azure/azure-functions/functions-overview) running against [LocalStack for Azure](https://docs.localstack.cloud/azure/). The application showcases how Azure Functions can seamlessly interact with Azure Storage services (Queues, Blobs, and Tables) when both the Azure Function App and storage services are running in emulated fashion locally on your machine using LocalStack for Azure. ## Overview @@ -121,7 +121,7 @@ The sample uses the following configurable settings in `local.settings.json`: ## Prerequisites -- [LocalStack for Azure](https://azure.localstack.cloud/) for Azure services emulation. +- [LocalStack for Azure](https://docs.localstack.cloud/azure/) for Azure services emulation. - [Visual Studio Code](https://code.visualstudio.com/) installed on one of the [supported platforms](https://code.visualstudio.com/docs/supporting/requirements#_platforms). - [Azure Functions Core Tools](https://learn.microsoft.com/en-us/azure/azure-functions/functions-run-local) let you develop and test your functions on your local computer. - [Bicep extension](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-bicep), if you plan to install the sample via Bicep. diff --git a/samples/function-app-storage-http/dotnet/bicep/README.md b/samples/function-app-storage-http/dotnet/bicep/README.md index 8241b6d..9594052 100644 --- a/samples/function-app-storage-http/dotnet/bicep/README.md +++ b/samples/function-app-storage-http/dotnet/bicep/README.md @@ -6,7 +6,7 @@ This directory contains the `main.bicep` Bicep module and `deploy.sh` deployment Before deploying this solution, ensure you have the following tools installed: -- [LocalStack for Azure](https://azure.localstack.cloud/): Local Azure cloud emulator for development and testing +- [LocalStack for Azure](https://docs.localstack.cloud/azure/): Local Azure cloud emulator for development and testing - [Visual Studio Code](https://code.visualstudio.com/): Code editor installed on one of the [supported platforms](https://code.visualstudio.com/docs/supporting/requirements#_platforms) - [Bicep extension](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-bicep): VS Code extension for Bicep language support and IntelliSense - [.NET SDK](https://dotnet.microsoft.com/en-us/download): Required for building and publishing the C# Azure Functions application @@ -155,5 +155,5 @@ This will remove all Azure resources created by the CLI deployment script. - [Azure Bicep Documentation](https://docs.microsoft.com/en-us/azure/azure-resource-manager/bicep/) - [Bicep Language Reference](https://docs.microsoft.com/en-us/azure/azure-resource-manager/bicep/bicep-functions) - [Azure Functions Documentation](https://docs.microsoft.com/en-us/azure/azure-functions/) -- [LocalStack for Azure Documentation](https://azure.localstack.cloud/) +- [LocalStack for Azure Documentation](https://docs.localstack.cloud/azure/) - [Azure Functions Methods Documentation](../src/sample/Methods.md) - Detailed documentation of all implemented functions \ No newline at end of file diff --git a/samples/function-app-storage-http/dotnet/scripts/README.md b/samples/function-app-storage-http/dotnet/scripts/README.md index f9b53d1..79312a3 100644 --- a/samples/function-app-storage-http/dotnet/scripts/README.md +++ b/samples/function-app-storage-http/dotnet/scripts/README.md @@ -6,7 +6,7 @@ This folder contains Bash scripts for deploying an Azure Functions application w Before deploying this solution, ensure you have the following tools installed: -- [LocalStack for Azure](https://azure.localstack.cloud/): Local Azure cloud emulator for development and testing +- [LocalStack for Azure](https://docs.localstack.cloud/azure/): Local Azure cloud emulator for development and testing - [Visual Studio Code](https://code.visualstudio.com/): Code editor installed on one of the [supported platforms](https://code.visualstudio.com/docs/supporting/requirements#_platforms) - [.NET SDK](https://dotnet.microsoft.com/en-us/download): Required for building and publishing the C# Azure Functions application - [Docker](https://docs.docker.com/get-docker/): Container runtime required for LocalStack @@ -199,4 +199,4 @@ This will remove all Azure resources created by the CLI deployment script. - [Azure CLI Documentation](https://docs.microsoft.com/en-us/cli/azure/) - [Azure Functions CLI Documentation](https://docs.microsoft.com/en-us/azure/azure-functions/functions-run-local) - [Azure Functions Methods Documentation](../src/sample/Methods.md) - Detailed documentation of all implemented functions -- [LocalStack for Azure Documentation](https://azure.localstack.cloud/) +- [LocalStack for Azure Documentation](https://docs.localstack.cloud/azure/) diff --git a/samples/function-app-storage-http/dotnet/terraform/README.md b/samples/function-app-storage-http/dotnet/terraform/README.md index a060dbe..dbcbcb7 100644 --- a/samples/function-app-storage-http/dotnet/terraform/README.md +++ b/samples/function-app-storage-http/dotnet/terraform/README.md @@ -6,7 +6,7 @@ This directory contains Terraform modules and `deploy.sh` deployment script for Before deploying this solution, ensure you have the following tools installed: -- [LocalStack for Azure](https://azure.localstack.cloud/): Local Azure cloud emulator for development and testing +- [LocalStack for Azure](https://docs.localstack.cloud/azure/): Local Azure cloud emulator for development and testing - [Visual Studio Code](https://code.visualstudio.com/): Code editor installed on one of the [supported platforms](https://code.visualstudio.com/docs/supporting/requirements#_platforms) - [Terraform](https://developer.hashicorp.com/terraform/downloads): Infrastructure as Code tool for provisioning Azure resources - [.NET SDK](https://dotnet.microsoft.com/en-us/download): Required for building and publishing the C# Azure Functions application @@ -179,5 +179,5 @@ This will remove all Azure resources created by the CLI deployment script. - [Azure Functions Documentation](https://docs.microsoft.com/en-us/azure/azure-functions/) - [Terraform Azure Provider](https://registry.terraform.io/providers/hashicorp/azurerm/latest) -- [LocalStack for Azure Documentation](https://azure.localstack.cloud/) +- [LocalStack for Azure Documentation](https://docs.localstack.cloud/azure/) - [Azure Functions Methods Documentation](../src/sample/Methods.md) - Detailed documentation of all implemented functions \ No newline at end of file diff --git a/samples/servicebus/java/README.md b/samples/servicebus/java/README.md index ccf882c..36476ee 100644 --- a/samples/servicebus/java/README.md +++ b/samples/servicebus/java/README.md @@ -82,6 +82,6 @@ The application then: - [Spring Cloud Azure Service Bus](https://learn.microsoft.com/en-us/azure/developer/java/spring-framework/configure-spring-cloud-stream-binder-java-app-with-service-bus) - [Azure Service Bus Queues](https://learn.microsoft.com/en-us/azure/service-bus-messaging/service-bus-queues-topics-subscriptions) - [Spring Boot Starter for Azure Service Bus](https://learn.microsoft.com/en-us/azure/developer/java/spring-framework/spring-cloud-azure) -- [LocalStack for Azure](https://azure.localstack.cloud/) +- [LocalStack for Azure](https://docs.localstack.cloud/azure/) diff --git a/samples/servicebus/java/bicep/README.md b/samples/servicebus/java/bicep/README.md index f2ac6c3..3ed3c40 100644 --- a/samples/servicebus/java/bicep/README.md +++ b/samples/servicebus/java/bicep/README.md @@ -6,7 +6,7 @@ This directory contains a Bicep template and a deployment script for provisionin Before deploying this solution, ensure you have the following tools installed: -- [LocalStack for Azure](https://azure.localstack.cloud/): Local Azure cloud emulator for development and testing +- [LocalStack for Azure](https://docs.localstack.cloud/azure/): Local Azure cloud emulator for development and testing - [Visual Studio Code](https://code.visualstudio.com/): Code editor installed on one of the [supported platforms](https://code.visualstudio.com/docs/supporting/requirements#_platforms) - [Bicep extension](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-bicep): VS Code extension for Bicep language support and IntelliSense - [Docker](https://docs.docker.com/get-docker/): Container runtime required for LocalStack @@ -179,4 +179,4 @@ This will remove all Azure resources created by the Bicep deployment script. - [Azure Bicep Documentation](https://docs.microsoft.com/en-us/azure/azure-resource-manager/bicep/) - [Bicep Language Reference](https://docs.microsoft.com/en-us/azure/azure-resource-manager/bicep/bicep-functions) -- [LocalStack for Azure Documentation](https://azure.localstack.cloud/) \ No newline at end of file +- [LocalStack for Azure Documentation](https://docs.localstack.cloud/azure/) \ No newline at end of file diff --git a/samples/servicebus/java/scripts/README.md b/samples/servicebus/java/scripts/README.md index 217c65e..b16ca3f 100644 --- a/samples/servicebus/java/scripts/README.md +++ b/samples/servicebus/java/scripts/README.md @@ -6,7 +6,7 @@ This directory contains Azure CLI scripts and a deployment script for provisioni Before deploying this solution, ensure you have the following tools installed: -- [LocalStack for Azure](https://azure.localstack.cloud/): Local Azure cloud emulator for development and testing +- [LocalStack for Azure](https://docs.localstack.cloud/azure/): Local Azure cloud emulator for development and testing - [Visual Studio Code](https://code.visualstudio.com/): Code editor installed on one of the [supported platforms](https://code.visualstudio.com/docs/supporting/requirements#_platforms) - [Docker](https://docs.docker.com/get-docker/): Container runtime required for LocalStack - [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli): Azure command-line interface @@ -161,4 +161,4 @@ This will remove all Azure resources created by the CLI deployment script. ## Related Documentation - [Azure CLI Documentation](https://docs.microsoft.com/en-us/cli/azure/) -- [LocalStack for Azure Documentation](https://azure.localstack.cloud/) \ No newline at end of file +- [LocalStack for Azure Documentation](https://docs.localstack.cloud/azure/) \ No newline at end of file diff --git a/samples/servicebus/java/terraform/README.md b/samples/servicebus/java/terraform/README.md index eae6660..e94ce25 100644 --- a/samples/servicebus/java/terraform/README.md +++ b/samples/servicebus/java/terraform/README.md @@ -6,7 +6,7 @@ This directory contains Terraform modules and a deployment script for provisioni Before deploying this solution, ensure you have the following tools installed: -- [LocalStack for Azure](https://azure.localstack.cloud/): Local Azure cloud emulator for development and testing +- [LocalStack for Azure](https://docs.localstack.cloud/azure/): Local Azure cloud emulator for development and testing - [Visual Studio Code](https://code.visualstudio.com/): Code editor installed on one of the [supported platforms](https://code.visualstudio.com/docs/supporting/requirements#_platforms) - [Terraform](https://developer.hashicorp.com/terraform/downloads): Infrastructure as Code tool for provisioning Azure resources - [Docker](https://docs.docker.com/get-docker/): Container runtime required for LocalStack @@ -184,4 +184,4 @@ This will remove all Azure resources created by the CLI deployment script. ## Related Documentation - [Terraform Azure Provider](https://registry.terraform.io/providers/hashicorp/azurerm/latest) -- [LocalStack for Azure Documentation](https://azure.localstack.cloud/) \ No newline at end of file +- [LocalStack for Azure Documentation](https://docs.localstack.cloud/azure/) \ No newline at end of file diff --git a/samples/web-app-cosmosdb-mongodb-api/python/README.md b/samples/web-app-cosmosdb-mongodb-api/python/README.md index b936381..59b4b52 100644 --- a/samples/web-app-cosmosdb-mongodb-api/python/README.md +++ b/samples/web-app-cosmosdb-mongodb-api/python/README.md @@ -150,4 +150,4 @@ sampledb> db.activities.find().pretty() - [Quickstart: Python Flask on Azure](https://learn.microsoft.com/en-us/azure/app-service/quickstart-python?tabs=flask%2Cbrowser) - [Quickstart: CosmosDB for MongoDB](https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/quickstart?tabs=azure-portal) - [Azure Identity Client Library for Python](https://learn.microsoft.com/en-us/python/api/overview/azure/identity-readme?view=azure-python) -- [LocalStack for Azure](https://azure.localstack.cloud/) \ No newline at end of file +- [LocalStack for Azure](https://docs.localstack.cloud/azure/) \ No newline at end of file diff --git a/samples/web-app-cosmosdb-mongodb-api/python/bicep/README.md b/samples/web-app-cosmosdb-mongodb-api/python/bicep/README.md index 1d05a9a..39c7048 100644 --- a/samples/web-app-cosmosdb-mongodb-api/python/bicep/README.md +++ b/samples/web-app-cosmosdb-mongodb-api/python/bicep/README.md @@ -6,7 +6,7 @@ This directory contains the Bicep template and a deployment script for provision Before deploying this solution, ensure you have the following tools installed: -- [LocalStack for Azure](https://azure.localstack.cloud/): Local Azure cloud emulator for development and testing +- [LocalStack for Azure](https://docs.localstack.cloud/azure/): Local Azure cloud emulator for development and testing - [Visual Studio Code](https://code.visualstudio.com/): Code editor installed on one of the [supported platforms](https://code.visualstudio.com/docs/supporting/requirements#_platforms) - [Bicep extension](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-bicep): VS Code extension for Bicep language support and IntelliSense - [Docker](https://docs.docker.com/get-docker/): Container runtime required for LocalStack @@ -276,4 +276,4 @@ This will remove all Azure resources created by the CLI deployment script. - [Azure Bicep Documentation](https://docs.microsoft.com/en-us/azure/azure-resource-manager/bicep/) - [Bicep Language Reference](https://docs.microsoft.com/en-us/azure/azure-resource-manager/bicep/bicep-functions) -- [LocalStack for Azure Documentation](https://azure.localstack.cloud/) \ No newline at end of file +- [LocalStack for Azure Documentation](https://docs.localstack.cloud/azure/) \ No newline at end of file diff --git a/samples/web-app-cosmosdb-mongodb-api/python/bicep/main.bicep b/samples/web-app-cosmosdb-mongodb-api/python/bicep/main.bicep index 9699914..463b07f 100644 --- a/samples/web-app-cosmosdb-mongodb-api/python/bicep/main.bicep +++ b/samples/web-app-cosmosdb-mongodb-api/python/bicep/main.bicep @@ -269,6 +269,7 @@ param tags object = { var webAppName = '${prefix}-webapp-${suffix}' var appServicePlanName = '${prefix}-app-service-plan-${suffix}' var accountName = '${prefix}-mongodb-${suffix}' +var privateDnsZoneName = 'privatelink.mongo.cosmos.azure.com' //******************************************** // Modules and Resources @@ -332,7 +333,7 @@ module network 'modules/virtual-network.bicep' = { module privateDnsZone 'modules/private-dns-zone.bicep' = { name: 'privateDnsZone' params: { - name: 'privatelink.mongo.cosmos.azure.com' + name: privateDnsZoneName vnetId: network.outputs.virtualNetworkId tags: tags } diff --git a/samples/web-app-cosmosdb-mongodb-api/python/bicep/main.bicepparam b/samples/web-app-cosmosdb-mongodb-api/python/bicep/main.bicepparam index 3819eeb..810db4d 100644 --- a/samples/web-app-cosmosdb-mongodb-api/python/bicep/main.bicepparam +++ b/samples/web-app-cosmosdb-mongodb-api/python/bicep/main.bicepparam @@ -1,6 +1,6 @@ using 'main.bicep' -param prefix = 'paolo' +param prefix = 'local' param suffix = 'test' param runtimeName = 'python' param runtimeVersion = '3.13' diff --git a/samples/web-app-cosmosdb-mongodb-api/python/bicep/modules/web-app.bicep b/samples/web-app-cosmosdb-mongodb-api/python/bicep/modules/web-app.bicep index 8af4bec..67ce3d2 100644 --- a/samples/web-app-cosmosdb-mongodb-api/python/bicep/modules/web-app.bicep +++ b/samples/web-app-cosmosdb-mongodb-api/python/bicep/modules/web-app.bicep @@ -179,7 +179,7 @@ resource configAppSettings 'Microsoft.Web/sites/config@2024-11-01' = { COSMOSDB_CONNECTION_STRING: account.listConnectionStrings().connectionStrings[0].connectionString COSMOSDB_DATABASE_NAME: databaseName COSMOSDB_COLLECTION_NAME: collectionName - WEBSITE_PORT: '8000' + WEBSITES_PORT: '8000' LOGIN_NAME: username } } diff --git a/samples/web-app-cosmosdb-mongodb-api/python/scripts/README.md b/samples/web-app-cosmosdb-mongodb-api/python/scripts/README.md index 1f6eaeb..0168135 100644 --- a/samples/web-app-cosmosdb-mongodb-api/python/scripts/README.md +++ b/samples/web-app-cosmosdb-mongodb-api/python/scripts/README.md @@ -6,7 +6,7 @@ This directory includes Bash scripts designed for deploying and testing the samp Before deploying this solution, ensure you have the following tools installed: -- [LocalStack for Azure](https://azure.localstack.cloud/): Local Azure cloud emulator for development and testing +- [LocalStack for Azure](https://docs.localstack.cloud/azure/): Local Azure cloud emulator for development and testing - [Visual Studio Code](https://code.visualstudio.com/): Code editor installed on one of the [supported platforms](https://code.visualstudio.com/docs/supporting/requirements#_platforms) - [Docker](https://docs.docker.com/get-docker/): Container runtime required for LocalStack - [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli): Azure command-line interface @@ -260,4 +260,4 @@ This will remove all Azure resources created by the CLI deployment script. ## Related Documentation - [Azure CLI Documentation](https://docs.microsoft.com/en-us/cli/azure/) -- [LocalStack for Azure Documentation](https://azure.localstack.cloud/) \ No newline at end of file +- [LocalStack for Azure Documentation](https://docs.localstack.cloud/azure/) \ No newline at end of file diff --git a/samples/web-app-cosmosdb-mongodb-api/python/scripts/call-web-app.sh b/samples/web-app-cosmosdb-mongodb-api/python/scripts/call-web-app.sh index 5f79052..075a3b4 100755 --- a/samples/web-app-cosmosdb-mongodb-api/python/scripts/call-web-app.sh +++ b/samples/web-app-cosmosdb-mongodb-api/python/scripts/call-web-app.sh @@ -133,7 +133,6 @@ call_web_app() { echo "Mapped host port [$host_port] retrieved successfully for container [$container_name]" else echo "Failed to get mapped host port for container [$container_name]" - exit 1 fi # Retrieve LocalStack proxy port diff --git a/samples/web-app-cosmosdb-mongodb-api/python/scripts/deploy.sh b/samples/web-app-cosmosdb-mongodb-api/python/scripts/deploy.sh index a28755a..a9b3408 100755 --- a/samples/web-app-cosmosdb-mongodb-api/python/scripts/deploy.sh +++ b/samples/web-app-cosmosdb-mongodb-api/python/scripts/deploy.sh @@ -7,14 +7,14 @@ LOCATION='westeurope' RESOURCE_GROUP_NAME="${PREFIX}-rg" LOG_ANALYTICS_NAME="${PREFIX}-log-analytics-${SUFFIX}" DIAGNOSTIC_SETTINGS_NAME='default' -WEBAPP_SUBNET_NSG_NAME="${PREFIX}-webapp-subnet-nsg-${SUFFIX}" +WEB_APP_SUBNET_NSG_NAME="${PREFIX}-webapp-subnet-nsg-${SUFFIX}" PE_SUBNET_NSG_NAME="${PREFIX}-pe-subnet-nsg-${SUFFIX}" NAT_GATEWAY_NAME="${PREFIX}-nat-gateway-${SUFFIX}" PIP_PREFIX_NAME="${PREFIX}-nat-gateway-pip-prefix-${SUFFIX}" VIRTUAL_NETWORK_NAME="${PREFIX}-vnet-${SUFFIX}" VIRTUAL_NETWORK_ADDRESS_PREFIX="10.0.0.0/8" -WEBAPP_SUBNET_NAME="app-subnet" -WEBAPP_SUBNET_PREFIX="10.0.0.0/24" +WEB_APP_SUBNET_NAME="app-subnet" +WEB_APP_SUBNET_PREFIX="10.0.0.0/24" PE_SUBNET_NAME="pe-subnet" PE_SUBNET_PREFIX="10.0.1.0/24" VIRTUAL_NETWORK_LINK_NAME="link-to-vnet" @@ -24,7 +24,7 @@ PRIVATE_ENDPOINT_GROUP="mongodb" PRIVATE_DNS_ZONE_GROUP_NAME="default" APP_SERVICE_PLAN_NAME="${PREFIX}-app-service-plan-${SUFFIX}" APP_SERVICE_PLAN_SKU="S1" -WEBAPP_NAME="${PREFIX}-webapp-${SUFFIX}" +WEB_APP_NAME="${PREFIX}-webapp-${SUFFIX}" COSMOSDB_ACCOUNT_NAME="${PREFIX}-mongodb-${SUFFIX}" MONGODB_API_VERSION="7.0" MONGODB_DATABASE_NAME="sampledb" @@ -37,7 +37,6 @@ RUNTIME_VERSION="3.13" LOGIN_NAME="paolo" CURRENT_DIR="$(cd "$(dirname "$0")" && pwd)" ZIPFILE="planner_website.zip" -SUBSCRIPTION_ID=$(az account show --query id --output tsv) # Change the current directory to the script's directory cd "$CURRENT_DIR" || exit @@ -199,46 +198,46 @@ else fi # Check if the network security group for the web app subnet already exists -echo "Checking if [$WEBAPP_SUBNET_NSG_NAME] network security group for the web app subnet actually exists in the [$RESOURCE_GROUP_NAME] resource group..." +echo "Checking if [$WEB_APP_SUBNET_NSG_NAME] network security group for the web app subnet actually exists in the [$RESOURCE_GROUP_NAME] resource group..." az network nsg show \ - --name "$WEBAPP_SUBNET_NSG_NAME" \ + --name "$WEB_APP_SUBNET_NSG_NAME" \ --resource-group "$RESOURCE_GROUP_NAME" \ --only-show-errors &>/dev/null if [[ $? != 0 ]]; then - echo "No [$WEBAPP_SUBNET_NSG_NAME] network security group for the web app subnet actually exists in the [$RESOURCE_GROUP_NAME] resource group" - echo "Creating [$WEBAPP_SUBNET_NSG_NAME] network security group for the web app subnet..." + echo "No [$WEB_APP_SUBNET_NSG_NAME] network security group for the web app subnet actually exists in the [$RESOURCE_GROUP_NAME] resource group" + echo "Creating [$WEB_APP_SUBNET_NSG_NAME] network security group for the web app subnet..." # Create the network security group for the web app subnet az network nsg create \ - --name "$WEBAPP_SUBNET_NSG_NAME" \ + --name "$WEB_APP_SUBNET_NSG_NAME" \ --resource-group "$RESOURCE_GROUP_NAME" \ --location "$LOCATION" \ --only-show-errors 1>/dev/null if [[ $? == 0 ]]; then - echo "[$WEBAPP_SUBNET_NSG_NAME] network security group for the web app subnet successfully created in the [$RESOURCE_GROUP_NAME] resource group" + echo "[$WEB_APP_SUBNET_NSG_NAME] network security group for the web app subnet successfully created in the [$RESOURCE_GROUP_NAME] resource group" else - echo "Failed to create [$WEBAPP_SUBNET_NSG_NAME] network security group for the web app subnet in the [$RESOURCE_GROUP_NAME] resource group" + echo "Failed to create [$WEB_APP_SUBNET_NSG_NAME] network security group for the web app subnet in the [$RESOURCE_GROUP_NAME] resource group" exit 1 fi else - echo "[$WEBAPP_SUBNET_NSG_NAME] network security group for the web app subnet already exists in the [$RESOURCE_GROUP_NAME] resource group" + echo "[$WEB_APP_SUBNET_NSG_NAME] network security group for the web app subnet already exists in the [$RESOURCE_GROUP_NAME] resource group" fi # Get the resource id of the network security group for the web app subnet -echo "Getting [$WEBAPP_SUBNET_NSG_NAME] network security group for the web app subnet resource id in the [$RESOURCE_GROUP_NAME] resource group..." -WEBAPP_SUBNET_NSG_ID=$(az network nsg show \ - --name "$WEBAPP_SUBNET_NSG_NAME" \ +echo "Getting [$WEB_APP_SUBNET_NSG_NAME] network security group for the web app subnet resource id in the [$RESOURCE_GROUP_NAME] resource group..." +WEB_APP_SUBNET_NSG_ID=$(az network nsg show \ + --name "$WEB_APP_SUBNET_NSG_NAME" \ --resource-group "$RESOURCE_GROUP_NAME" \ --query id \ --output tsv \ --only-show-errors) -if [[ -n $WEBAPP_SUBNET_NSG_ID ]]; then - echo "[$WEBAPP_SUBNET_NSG_NAME] network security group for the web app subnet resource id retrieved successfully: $WEBAPP_SUBNET_NSG_ID" +if [[ -n $WEB_APP_SUBNET_NSG_ID ]]; then + echo "[$WEB_APP_SUBNET_NSG_NAME] network security group for the web app subnet resource id retrieved successfully: $WEB_APP_SUBNET_NSG_ID" else - echo "Failed to retrieve [$WEBAPP_SUBNET_NSG_NAME] network security group for the web app subnet resource id in the [$RESOURCE_GROUP_NAME] resource group" + echo "Failed to retrieve [$WEB_APP_SUBNET_NSG_NAME] network security group for the web app subnet resource id in the [$RESOURCE_GROUP_NAME] resource group" exit 1 fi @@ -362,8 +361,8 @@ if [[ $? != 0 ]]; then --resource-group "$RESOURCE_GROUP_NAME" \ --location "$LOCATION" \ --address-prefixes "$VIRTUAL_NETWORK_ADDRESS_PREFIX" \ - --subnet-name "$WEBAPP_SUBNET_NAME" \ - --subnet-prefix "$WEBAPP_SUBNET_PREFIX" \ + --subnet-name "$WEB_APP_SUBNET_NAME" \ + --subnet-prefix "$WEB_APP_SUBNET_PREFIX" \ --only-show-errors 1>/dev/null if [[ $? == 0 ]]; then @@ -374,21 +373,21 @@ if [[ $? != 0 ]]; then fi # Update the web app subnet to associate it with the NAT Gateway and the NSG - echo "Associating [$WEBAPP_SUBNET_NAME] subnet with the [$NAT_GATEWAY_NAME] NAT Gateway and the [$WEBAPP_SUBNET_NSG_NAME] network security group..." + echo "Associating [$WEB_APP_SUBNET_NAME] subnet with the [$NAT_GATEWAY_NAME] NAT Gateway and the [$WEB_APP_SUBNET_NSG_NAME] network security group..." # Update the web app subnet to associate it with the NAT Gateway and the NSG az network vnet subnet update \ - --name "$WEBAPP_SUBNET_NAME" \ + --name "$WEB_APP_SUBNET_NAME" \ --vnet-name "$VIRTUAL_NETWORK_NAME" \ --resource-group "$RESOURCE_GROUP_NAME" \ --nat-gateway "$NAT_GATEWAY_NAME" \ - --network-security-group "$WEBAPP_SUBNET_NSG_NAME" \ + --network-security-group "$WEB_APP_SUBNET_NSG_NAME" \ --only-show-errors 1>/dev/null if [[ $? == 0 ]]; then - echo "[$WEBAPP_SUBNET_NAME] subnet successfully associated with the [$NAT_GATEWAY_NAME] NAT Gateway and the [$WEBAPP_SUBNET_NSG_NAME] network security group" + echo "[$WEB_APP_SUBNET_NAME] subnet successfully associated with the [$NAT_GATEWAY_NAME] NAT Gateway and the [$WEB_APP_SUBNET_NSG_NAME] network security group" else - echo "Failed to associate [$WEBAPP_SUBNET_NAME] subnet with the [$NAT_GATEWAY_NAME] NAT Gateway and the [$WEBAPP_SUBNET_NSG_NAME] network security group" + echo "Failed to associate [$WEB_APP_SUBNET_NAME] subnet with the [$NAT_GATEWAY_NAME] NAT Gateway and the [$WEB_APP_SUBNET_NSG_NAME] network security group" exit 1 fi else @@ -604,58 +603,58 @@ else fi # Create the web app -echo "Creating web app [$WEBAPP_NAME]..." +echo "Creating web app [$WEB_APP_NAME]..." az webapp create \ --resource-group "$RESOURCE_GROUP_NAME" \ --plan "$APP_SERVICE_PLAN_NAME" \ - --name "$WEBAPP_NAME" \ + --name "$WEB_APP_NAME" \ --runtime "$RUNTIME:$RUNTIME_VERSION" \ --vnet "$VIRTUAL_NETWORK_NAME" \ - --subnet "$WEBAPP_SUBNET_NAME" \ + --subnet "$WEB_APP_SUBNET_NAME" \ --only-show-errors 1>/dev/null if [ $? -eq 0 ]; then - echo "Web app [$WEBAPP_NAME] created successfully." + echo "Web app [$WEB_APP_NAME] created successfully." else - echo "Failed to create web app [$WEBAPP_NAME]." - exit 1 -fi - -# Enabling -echo "Enabling forced tunneling for web app [$WEBAPP_NAME] to route all outbound traffic through the virtual network..." - -az resource update \ - --ids "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP_NAME/providers/Microsoft.Web/sites/$WEBAPP_NAME" \ - --set properties.outboundVnetRouting.allTraffic=true \ - --only-show-errors 1>/dev/null - -if [ $? -eq 0 ]; then - echo "Forced tunneling enabled for web app [$WEBAPP_NAME]." -else - echo "Failed to enable forced tunneling for web app [$WEBAPP_NAME]." + echo "Failed to create web app [$WEB_APP_NAME]." exit 1 fi # Get the web app resource id -echo "Getting [$WEBAPP_NAME] web app resource id in the [$RESOURCE_GROUP_NAME] resource group..." -WEBAPP_ID=$(az webapp show \ - --name "$WEBAPP_NAME" \ +echo "Getting [$WEB_APP_NAME] web app resource id in the [$RESOURCE_GROUP_NAME] resource group..." +WEB_APP_ID=$(az webapp show \ + --name "$WEB_APP_NAME" \ --resource-group "$RESOURCE_GROUP_NAME" \ --query id \ --output tsv \ --only-show-errors) -if [[ -n $WEBAPP_ID ]]; then - echo "[$WEBAPP_NAME] web app resource id retrieved successfully: $WEBAPP_ID" +if [[ -n $WEB_APP_ID ]]; then + echo "[$WEB_APP_NAME] web app resource id retrieved successfully: $WEB_APP_ID" +else + echo "Failed to retrieve [$WEB_APP_NAME] web app resource id in the [$RESOURCE_GROUP_NAME] resource group" + exit 1 +fi + +# Enabling forced tunneling for web app [$WEB_APP_NAME] to route all outbound traffic through the virtual network... +echo "Enabling forced tunneling for web app [$WEB_APP_NAME] to route all outbound traffic through the virtual network..." + +az resource update \ + --ids "$WEB_APP_ID" \ + --set properties.outboundVnetRouting.allTraffic=true \ + --only-show-errors 1>/dev/null + +if [ $? -eq 0 ]; then + echo "Forced tunneling enabled for web app [$WEB_APP_NAME]." else - echo "Failed to retrieve [$WEBAPP_NAME] web app resource id in the [$RESOURCE_GROUP_NAME] resource group" + echo "Failed to enable forced tunneling for web app [$WEB_APP_NAME]." exit 1 fi # Set web app settings -echo "Setting web app settings for [$WEBAPP_NAME]..." +echo "Setting web app settings for [$WEB_APP_NAME]..." az webapp config appsettings set \ - --name $WEBAPP_NAME \ + --name $WEB_APP_NAME \ --resource-group $RESOURCE_GROUP_NAME \ --settings \ SCM_DO_BUILD_DURING_DEPLOYMENT='true' \ @@ -664,13 +663,13 @@ az webapp config appsettings set \ COSMOSDB_DATABASE_NAME="$MONGODB_DATABASE_NAME" \ COSMOSDB_COLLECTION_NAME="$COLLECTION_NAME" \ LOGIN_NAME="$LOGIN_NAME" \ - WEBSITE_PORT="8000" \ + WEBSITES_PORT="8000" \ --only-show-errors 1>/dev/null if [ $? -eq 0 ]; then - echo "Web app settings for [$WEBAPP_NAME] set successfully." + echo "Web app settings for [$WEB_APP_NAME] set successfully." else - echo "Failed to set web app settings for [$WEBAPP_NAME]." + echo "Failed to set web app settings for [$WEB_APP_NAME]." exit 1 fi @@ -706,20 +705,20 @@ else fi # Check whether the diagnostic settings for the web app already exist -echo "Checking if [$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$WEBAPP_NAME] web app already exist..." +echo "Checking if [$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$WEB_APP_NAME] web app already exist..." az monitor diagnostic-settings show \ --name "$DIAGNOSTIC_SETTINGS_NAME" \ - --resource "$WEBAPP_ID" \ + --resource "$WEB_APP_ID" \ --only-show-errors &>/dev/null if [[ $? != 0 ]]; then - echo "No [$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$WEBAPP_NAME] web app actually exist" - echo "Creating [$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$WEBAPP_NAME] web app..." + echo "No [$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$WEB_APP_NAME] web app actually exist" + echo "Creating [$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$WEB_APP_NAME] web app..." # Create the diagnostic settings for the web app to send logs to the Log Analytics workspace az monitor diagnostic-settings create \ --name "$DIAGNOSTIC_SETTINGS_NAME" \ - --resource "$WEBAPP_ID" \ + --resource "$WEB_APP_ID" \ --workspace "$LOG_ANALYTICS_NAME" \ --logs '[ {"category": "AppServiceHTTPLogs", "enabled": true}, @@ -737,13 +736,13 @@ if [[ $? != 0 ]]; then if [[ $? == 0 ]]; then - echo "[$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$WEBAPP_NAME] web app successfully created" + echo "[$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$WEB_APP_NAME] web app successfully created" else - echo "Failed to create [$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$WEBAPP_NAME] web app" + echo "Failed to create [$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$WEB_APP_NAME] web app" exit 1 fi else - echo "[$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$WEBAPP_NAME] web app already exist" + echo "[$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$WEB_APP_NAME] web app already exist" fi # Check whether the diagnostic settings for the app service plan already exist @@ -847,20 +846,20 @@ else fi # Check whether the diagnostic settings for the network security group for the web app subnet already exist -echo "Checking if [$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$WEBAPP_SUBNET_NSG_NAME] network security group for the web app subnet already exist..." +echo "Checking if [$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$WEB_APP_SUBNET_NSG_NAME] network security group for the web app subnet already exist..." az monitor diagnostic-settings show \ --name "$DIAGNOSTIC_SETTINGS_NAME" \ - --resource "$WEBAPP_SUBNET_NSG_ID" \ + --resource "$WEB_APP_SUBNET_NSG_ID" \ --only-show-errors &>/dev/null if [[ $? != 0 ]]; then - echo "No [$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$WEBAPP_SUBNET_NSG_NAME] network security group for the web app subnet actually exist" - echo "Creating [$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$WEBAPP_SUBNET_NSG_NAME] network security group for the web app subnet..." + echo "No [$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$WEB_APP_SUBNET_NSG_NAME] network security group for the web app subnet actually exist" + echo "Creating [$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$WEB_APP_SUBNET_NSG_NAME] network security group for the web app subnet..." # Create the diagnostic settings for the network security group for the web app subnet to send logs to the Log Analytics workspace az monitor diagnostic-settings create \ --name "$DIAGNOSTIC_SETTINGS_NAME" \ - --resource "$WEBAPP_SUBNET_NSG_ID" \ + --resource "$WEB_APP_SUBNET_NSG_ID" \ --workspace "$LOG_ANALYTICS_NAME" \ --logs '[ {"category": "NetworkSecurityGroupEvent", "enabled": true}, @@ -869,13 +868,13 @@ if [[ $? != 0 ]]; then --only-show-errors 1>/dev/null if [[ $? == 0 ]]; then - echo "[$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$WEBAPP_SUBNET_NSG_NAME] network security group for the web app subnet successfully created" + echo "[$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$WEB_APP_SUBNET_NSG_NAME] network security group for the web app subnet successfully created" else - echo "Failed to create [$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$WEBAPP_SUBNET_NSG_NAME] network security group for the web app subnet" + echo "Failed to create [$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$WEB_APP_SUBNET_NSG_NAME] network security group for the web app subnet" exit 1 fi else - echo "[$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$WEBAPP_SUBNET_NSG_NAME] network security group for the web app subnet already exist" + echo "[$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$WEB_APP_SUBNET_NSG_NAME] network security group for the web app subnet already exist" fi # Check whether the diagnostic settings for the network security group for the private endpoint subnet already exist @@ -927,11 +926,11 @@ echo "Contents of the zip package [$ZIPFILE]:" unzip -l "$ZIPFILE" # Deploy the web app -echo "Deploying web app [$WEBAPP_NAME] with zip file [$ZIPFILE]..." +echo "Deploying web app [$WEB_APP_NAME] with zip file [$ZIPFILE]..." echo "Using standard az webapp deploy command for AzureCloud environment." az webapp deploy \ --resource-group "$RESOURCE_GROUP_NAME" \ - --name "$WEBAPP_NAME" \ + --name "$WEB_APP_NAME" \ --src-path "$ZIPFILE" \ --type zip \ --async true 1>/dev/null diff --git a/samples/web-app-cosmosdb-mongodb-api/python/scripts/validate.sh b/samples/web-app-cosmosdb-mongodb-api/python/scripts/validate.sh index b2f0459..4360e51 100755 --- a/samples/web-app-cosmosdb-mongodb-api/python/scripts/validate.sh +++ b/samples/web-app-cosmosdb-mongodb-api/python/scripts/validate.sh @@ -5,14 +5,14 @@ PREFIX='local' SUFFIX='test' RESOURCE_GROUP_NAME="${PREFIX}-rg" LOG_ANALYTICS_NAME="${PREFIX}-log-analytics-${SUFFIX}" -WEBAPP_SUBNET_NSG_NAME="${PREFIX}-webapp-subnet-nsg-${SUFFIX}" +WEB_APP_SUBNET_NSG_NAME="${PREFIX}-webapp-subnet-nsg-${SUFFIX}" PE_SUBNET_NSG_NAME="${PREFIX}-pe-subnet-nsg-${SUFFIX}" NAT_GATEWAY_NAME="${PREFIX}-nat-gateway-${SUFFIX}" VIRTUAL_NETWORK_NAME="${PREFIX}-vnet-${SUFFIX}" PRIVATE_DNS_ZONE_NAME="privatelink.mongo.cosmos.azure.com" PRIVATE_ENDPOINT_NAME="${PREFIX}-mongodb-pe-${SUFFIX}" APP_SERVICE_PLAN_NAME="${PREFIX}-app-service-plan-${SUFFIX}" -WEBAPP_NAME="${PREFIX}-webapp-${SUFFIX}" +WEB_APP_NAME="${PREFIX}-webapp-${SUFFIX}" COSMOSDB_ACCOUNT_NAME="${PREFIX}-mongodb-${SUFFIX}" MONGODB_DATABASE_NAME="sampledb" COLLECTION_NAME="activities" @@ -32,9 +32,9 @@ az appservice plan show \ --only-show-errors # Check Azure Web App -echo -e "\n[$WEBAPP_NAME] web app:\n" +echo -e "\n[$WEB_APP_NAME] web app:\n" az webapp show \ - --name "$WEBAPP_NAME" \ + --name "$WEB_APP_NAME" \ --resource-group "$RESOURCE_GROUP_NAME" \ --output table \ --only-show-errors @@ -111,9 +111,9 @@ az network private-endpoint show \ --only-show-errors # Check Web App Subnet NSG -echo -e "\n[$WEBAPP_SUBNET_NSG_NAME] network security group:\n" +echo -e "\n[$WEB_APP_SUBNET_NSG_NAME] network security group:\n" az network nsg show \ - --name "$WEBAPP_SUBNET_NSG_NAME" \ + --name "$WEB_APP_SUBNET_NSG_NAME" \ --resource-group "$RESOURCE_GROUP_NAME" \ --output table \ --only-show-errors diff --git a/samples/web-app-cosmosdb-mongodb-api/python/terraform/README.md b/samples/web-app-cosmosdb-mongodb-api/python/terraform/README.md index 564d95b..46f3c53 100644 --- a/samples/web-app-cosmosdb-mongodb-api/python/terraform/README.md +++ b/samples/web-app-cosmosdb-mongodb-api/python/terraform/README.md @@ -6,7 +6,7 @@ This directory contains Terraform modules and a deployment script for provisioni Before deploying this solution, ensure you have the following tools installed: -- [LocalStack for Azure](https://azure.localstack.cloud/): Local Azure cloud emulator for development and testing +- [LocalStack for Azure](https://docs.localstack.cloud/azure/): Local Azure cloud emulator for development and testing - [Visual Studio Code](https://code.visualstudio.com/): Code editor installed on one of the [supported platforms](https://code.visualstudio.com/docs/supporting/requirements#_platforms) - [Terraform](https://developer.hashicorp.com/terraform/downloads): Infrastructure as Code tool for provisioning Azure resources - [Docker](https://docs.docker.com/get-docker/): Container runtime required for LocalStack @@ -280,4 +280,4 @@ This will remove all Azure resources created by the CLI deployment script. ## Related Documentation - [Terraform Azure Provider](https://registry.terraform.io/providers/hashicorp/azurerm/latest) -- [LocalStack for Azure Documentation](https://azure.localstack.cloud/) \ No newline at end of file +- [LocalStack for Azure Documentation](https://docs.localstack.cloud/azure/) \ No newline at end of file diff --git a/samples/web-app-cosmosdb-mongodb-api/python/terraform/main.tf b/samples/web-app-cosmosdb-mongodb-api/python/terraform/main.tf index 9439ff2..55d3f1c 100644 --- a/samples/web-app-cosmosdb-mongodb-api/python/terraform/main.tf +++ b/samples/web-app-cosmosdb-mongodb-api/python/terraform/main.tf @@ -4,7 +4,6 @@ locals { suffix = lower(var.suffix) resource_group_name = "${var.prefix}-rg" log_analytics_name = "${local.prefix}-log-analytics-${local.suffix}" - storage_account_name = "${local.prefix}datastore${local.suffix}" virtual_network_name = "${local.prefix}-vnet-${local.suffix}" nat_gateway_name = "${local.prefix}-nat-gateway-${local.suffix}" private_endpoint_name = "${local.prefix}-mongodb-pe-${local.suffix}" @@ -189,6 +188,6 @@ module "web_app" { COSMOSDB_DATABASE_NAME = module.cosmosdb_mongodb.database_name COSMOSDB_COLLECTION_NAME = var.cosmosdb_collection_name LOGIN_NAME = var.login_name - WEBSITE_PORT = var.website_port + WEBSITES_PORT = var.websites_port } } \ No newline at end of file diff --git a/samples/web-app-cosmosdb-mongodb-api/python/terraform/modules/app_service_plan/main.tf b/samples/web-app-cosmosdb-mongodb-api/python/terraform/modules/app_service_plan/main.tf index 0f67edb..98a3e4d 100644 --- a/samples/web-app-cosmosdb-mongodb-api/python/terraform/modules/app_service_plan/main.tf +++ b/samples/web-app-cosmosdb-mongodb-api/python/terraform/modules/app_service_plan/main.tf @@ -19,10 +19,6 @@ resource "azurerm_monitor_diagnostic_setting" "example" { target_resource_id = azurerm_service_plan.example.id log_analytics_workspace_id = var.log_analytics_workspace_id - enabled_log { - category = "VMProtectionAlerts" - } - enabled_metric { category = "AllMetrics" } diff --git a/samples/web-app-cosmosdb-mongodb-api/python/terraform/variables.tf b/samples/web-app-cosmosdb-mongodb-api/python/terraform/variables.tf index 702e90d..d33bed9 100644 --- a/samples/web-app-cosmosdb-mongodb-api/python/terraform/variables.tf +++ b/samples/web-app-cosmosdb-mongodb-api/python/terraform/variables.tf @@ -310,7 +310,7 @@ variable "nat_gateway_zones" { default = ["1"] } -variable "website_port" { +variable "websites_port" { description = "(Optional) Specifies the port on which the Web App will listen. Defaults to 8000." type = number default = 8000 diff --git a/samples/web-app-cosmosdb-nosql-api/python/README.md b/samples/web-app-cosmosdb-nosql-api/python/README.md index a5c11b5..224beb2 100644 --- a/samples/web-app-cosmosdb-nosql-api/python/README.md +++ b/samples/web-app-cosmosdb-nosql-api/python/README.md @@ -75,4 +75,4 @@ You can utilize **CosmosDB Data Explorer** to explore and manage your CosmosDB d - [Azure CosmosDB Documentation](https://learn.microsoft.com/en-us/azure/cosmos-db/) - [Quickstart: Python Flask on Azure](https://learn.microsoft.com/en-us/azure/app-service/quickstart-python?tabs=flask%2Cbrowser) - [Azure Identity Client Library for Python](https://learn.microsoft.com/en-us/python/api/overview/azure/identity-readme?view=azure-python) -- [LocalStack for Azure](https://azure.localstack.cloud/) \ No newline at end of file +- [LocalStack for Azure](https://docs.localstack.cloud/azure/) \ No newline at end of file diff --git a/samples/web-app-cosmosdb-nosql-api/python/scripts/README.md b/samples/web-app-cosmosdb-nosql-api/python/scripts/README.md index dadeb7a..806ae97 100644 --- a/samples/web-app-cosmosdb-nosql-api/python/scripts/README.md +++ b/samples/web-app-cosmosdb-nosql-api/python/scripts/README.md @@ -6,7 +6,7 @@ This directory includes Bash scripts designed for deploying and testing the samp Before deploying this solution, ensure you have the following tools installed: -- [LocalStack for Azure](https://azure.localstack.cloud/): Local Azure cloud emulator for development and testing +- [LocalStack for Azure](https://docs.localstack.cloud/azure/): Local Azure cloud emulator for development and testing - [Visual Studio Code](https://code.visualstudio.com/): Code editor installed on one of the [supported platforms](https://code.visualstudio.com/docs/supporting/requirements#_platforms) - [Docker](https://docs.docker.com/get-docker/): Container runtime required for LocalStack - [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli): Azure command-line interface @@ -112,4 +112,4 @@ This will remove all Azure resources created by the CLI deployment script. ## Related Documentation - [Azure CLI Documentation](https://docs.microsoft.com/en-us/cli/azure/) -- [LocalStack for Azure Documentation](https://azure.localstack.cloud/) \ No newline at end of file +- [LocalStack for Azure Documentation](https://docs.localstack.cloud/azure/) \ No newline at end of file diff --git a/samples/web-app-custom-image/python/README.md b/samples/web-app-custom-image/python/README.md index 6662fc6..389054c 100644 --- a/samples/web-app-custom-image/python/README.md +++ b/samples/web-app-custom-image/python/README.md @@ -1,59 +1,80 @@ -# Azure Web App With Custom Docker Image +# Azure Web App with Custom Docker Image -This sample demonstrates a Python Flask web application hosted on an Azure Web App using a custom Docker image. The deployment builds the image from the local `src/Dockerfile`, creates an Azure Container Registry resource, and configures a Linux Web App to run the custom image in the LocalStack Azure emulator. +This sample demonstrates a Python Flask web application hosted on an [Azure Web App](https://learn.microsoft.com/en-us/azure/app-service/overview) using a custom Docker image. The app runs on an Azure App Service Plan and uses a container image stored in an [Azure Container Registry](https://learn.microsoft.com/azure/container-registry/container-registry-intro). For more information on configuring a web app to use a custom container, see [Configure a custom container for Azure App Service](https://learn.microsoft.com/azure/app-service/configure-custom-container?tabs=debian&pivots=container-linux). ## Architecture -The sample creates the following Azure resources: +The following diagram illustrates the architecture of the solution: -1. **Azure Resource Group**: Logical container for all resources in the sample. -2. **Azure Container Registry**: Stores the custom Docker image metadata and credentials. -3. **Azure App Service Plan**: Linux plan used by the Web App. -4. **Azure Web App**: Runs the Flask application from the custom image. +![Architecture Diagram](./images/architecture.png) + +The solution is composed of the following Azure resources: + +1. [Azure Resource Group](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/manage-resource-groups-cli): A logical container scoping all resources in this sample. +2. [Azure Virtual Network](https://learn.microsoft.com/azure/virtual-network/virtual-networks-overview): Hosts two subnets: + - *app-subnet*: Dedicated to [regional VNet integration](https://learn.microsoft.com/azure/azure-functions/functions-networking-options?tabs=azure-portal#outbound-networking-features) with the Web App. + - *pe-subnet*: Used for hosting Azure Private Endpoints. +3. [Azure Private DNS Zone](https://learn.microsoft.com/azure/dns/private-dns-privatednszone): Handles DNS resolution for the Azure Container Registry Private Endpoint within the virtual network. +4. [Azure Private Endpoint](https://learn.microsoft.com/azure/private-link/private-endpoint-overview): Secures network access to the Azure Container Registry via a private IP within the VNet. +5. [Azure NAT Gateway](https://learn.microsoft.com/azure/nat-gateway/nat-overview): Provides deterministic outbound connectivity for the Web App. Included for completeness; the sample app does not call any external services. +6. [Azure Network Security Group](https://learn.microsoft.com/en-us/azure/virtual-network/network-security-groups-overview): Enforces inbound and outbound traffic rules across the virtual network's subnets. +7. [Azure Log Analytics Workspace](https://learn.microsoft.com/azure/azure-monitor/logs/log-analytics-overview): Centralizes diagnostic logs and metrics from all resources in the solution. +8. [Azure Container Registry](https://learn.microsoft.com/azure/container-registry/container-registry-intro): A fully-managed container registry service based on the open-source [Docker platform](https://docs.docker.com/get-started/docker-overview/) used to hold the container image used by the web app. +9. [User-Assigned Managed Identity](https://learn.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview): Assigned the [AcrPull](https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles/containers#acrpull) role on the Azure Container Registry, enabling the Web App to pull the container image without storing credentials. +10. [Azure App Service Plan](https://learn.microsoft.com/en-us/azure/app-service/overview-hosting-plans): The underlying compute tier that hosts the web application. +11. [Azure Web App](https://learn.microsoft.com/en-us/azure/app-service/overview): Runs the Python Flask application from the custom container image stored in the Azure Container Registry. ## Prerequisites -- Docker -- Azure CLI -- azlocal CLI -- jq -- LocalStack for Azure +- [Azure Subscription](https://azure.microsoft.com/free/) +- [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli) +- [Docker](https://docs.docker.com/get-docker/) +- [Bicep](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-bicep), if you plan to install the sample via Bicep. +- [Terraform](https://developer.hashicorp.com/terraform/downloads), if you plan to install the sample via Terraform. +- [Azlocal CLI](https://azure.localstack.cloud/user-guides/sdks/az/): LocalStack Azure CLI wrapper +- [jq](https://jqlang.org/): JSON processor for scripting and parsing command outputs -## Deploy +## Deployment -Start LocalStack for Azure and configure Azure CLI interception as described in the repository root README. Then run: +Set up the Azure emulator using the LocalStack for Azure Docker image. Before starting, ensure you have a valid `LOCALSTACK_AUTH_TOKEN` to access the Azure emulator. Refer to the [Auth Token guide](https://docs.localstack.cloud/getting-started/auth-token/?__hstc=108988063.8aad2b1a7229945859f4d9b9bb71e05d.1743148429561.1758793541854.1758810151462.32&__hssc=108988063.3.1758810151462&__hsfp=3945774529) to obtain your Auth Token and set it in the `LOCALSTACK_AUTH_TOKEN` environment variable. The Azure Docker image is available on the [LocalStack Docker Hub](https://hub.docker.com/r/localstack/localstack-azure-alpha). To pull the image, execute: ```bash -cd samples/web-app-custom-image/python -bash scripts/deploy.sh +docker pull localstack/localstack-azure-alpha ``` -The script builds the Docker image from `src/`, creates the App Service resources, and configures the Web App to use the custom image. - -## Validate +Start the LocalStack Azure emulator by running: ```bash -bash scripts/validate.sh -``` +# Set the authentication token +export LOCALSTACK_AUTH_TOKEN= -## Invoke The App +# Start the LocalStack Azure emulator +IMAGE_NAME=localstack/localstack-azure-alpha localstack start -d +localstack wait -t 60 -```bash -bash scripts/call-web-app.sh +# Route all Azure CLI calls to the LocalStack Azure emulator +azlocal start-interception ``` -The app exposes: +Deploy the application to LocalStack for Azure using one of these methods: -- `/` for the HTML page -- `/api/status` for a JSON health response +- [Azure CLI Deployment](./scripts/README.md) +- [Bicep Deployment](./bicep/README.md) +- [Terraform Deployment](./terraform/README.md) -## Local Docker Run +All deployment methods have been fully tested against Azure and the LocalStack for Azure local emulator. -You can run the same image directly with Docker: +> **Note** +> When you deploy the application to LocalStack for Azure for the first time, the initialization process involves downloading and building Docker images. This is a one-time operation—subsequent deployments will be significantly faster. Depending on your internet connection and system resources, this initial setup may take several minutes. -```bash -cd src -docker build -t vacation-planner-webapp:v1 . -docker run --rm -p 8080:80 vacation-planner-webapp:v1 -curl http://127.0.0.1:8080/api/status -``` +## Test + +You can use the `call-web-app.sh` Bash script below to call the web app. The script calls the web app via the default hostname `.azurewebsites.azure.localhost.localstack.cloud:4566`. + +## References + +- [Azure Web Apps Documentation](https://learn.microsoft.com/en-us/azure/app-service/) +- [Azure Container Registry Documentation](https://learn.microsoft.com/azure/container-registry/container-registry-intro) +- [Configure a custom container for Azure App Service](https://learn.microsoft.com/azure/app-service/configure-custom-container?tabs=debian&pivots=container-linux) +- [Azure Identity Client Library for Python](https://learn.microsoft.com/en-us/python/api/overview/azure/identity-readme?view=azure-python) +- [LocalStack for Azure](https://docs.localstack.cloud/azure/) diff --git a/samples/web-app-custom-image/python/bicep/README.md b/samples/web-app-custom-image/python/bicep/README.md new file mode 100644 index 0000000..4fe9889 --- /dev/null +++ b/samples/web-app-custom-image/python/bicep/README.md @@ -0,0 +1,286 @@ +# Bicep Deployment + +This directory contains the Bicep templates and a deployment script for provisioning Azure services in LocalStack for Azure. For further details about the sample application, refer to the [Azure Web App with Custom Docker Image](../README.md). + +## Prerequisites + +Before deploying this solution, ensure you have the following tools installed: + +- [LocalStack for Azure](https://docs.localstack.cloud/azure/): Local Azure cloud emulator for development and testing +- [Visual Studio Code](https://code.visualstudio.com/): Code editor installed on one of the [supported platforms](https://code.visualstudio.com/docs/supporting/requirements#_platforms) +- [Bicep extension](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-bicep): VS Code extension for Bicep language support and IntelliSense +- [Docker](https://docs.docker.com/get-docker/): Container runtime required for LocalStack and building the custom image +- [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli): Azure command-line interface +- [Azlocal CLI](https://azure.localstack.cloud/user-guides/sdks/az/): LocalStack Azure CLI wrapper +- [jq](https://jqlang.org/): JSON processor for scripting and parsing command outputs + +### Installing azlocal CLI + +The [deploy.sh](deploy.sh) Bash script uses the `azlocal` CLI instead of the standard Azure CLI to work with LocalStack. Install it using: + +```bash +pip install azlocal +``` + +For more information, see [Get started with the az tool on LocalStack](https://azure.localstack.cloud/user-guides/sdks/az/). + +## Architecture Overview + +The [deploy.sh](deploy.sh) script creates the [Azure Resource Group](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/manage-resource-groups-cli) for all the Azure resources. The deployment is split into two Bicep phases with an image push step between them. + +### First Bicep Deployment +In this phase, the [acr.bicep](acr.bicep) template deploys: + +1. [Azure Container Registry](https://learn.microsoft.com/azure/container-registry/container-registry-intro): A fully-managed container registry service based on the open-source [Docker platform](https://docs.docker.com/get-started/docker-overview/) used to hold the container image used by the web app. +2. [Azure Log Analytics Workspace](https://learn.microsoft.com/azure/azure-monitor/logs/log-analytics-overview): Centralizes diagnostic logs and metrics from all resources in the solution. + +## Container Image Push +After the first Bicep deployment, the script builds the container image locally from the `src/Dockerfile` and pushes it to the Azure Container Registry. + +### Second Bicep Deployment +The [main.bicep](main.bicep) template deploys the remaining resources: + +1. [Azure Virtual Network](https://learn.microsoft.com/azure/virtual-network/virtual-networks-overview): Hosts two subnets: + - *app-subnet*: Dedicated to [regional VNet integration](https://learn.microsoft.com/azure/azure-functions/functions-networking-options?tabs=azure-portal#outbound-networking-features) with the Web App. + - *pe-subnet*: Used for hosting Azure Private Endpoints. +2. [Azure Private DNS Zone](https://learn.microsoft.com/azure/dns/private-dns-privatednszone): Handles DNS resolution for the Azure Container Registry Private Endpoint within the virtual network. +3. [Azure Private Endpoint](https://learn.microsoft.com/azure/private-link/private-endpoint-overview): Secures network access to the Azure Container Registry via a private IP within the VNet. +4. [Azure NAT Gateway](https://learn.microsoft.com/azure/nat-gateway/nat-overview): Provides deterministic outbound connectivity for the Web App. Included for completeness; the sample app does not call any external services. +5. [Azure Network Security Group](https://learn.microsoft.com/en-us/azure/virtual-network/network-security-groups-overview): Enforces inbound and outbound traffic rules across the virtual network's subnets. +6. [Azure App Service Plan](https://learn.microsoft.com/en-us/azure/app-service/overview-hosting-plans): The underlying compute tier that hosts the web application. +7. [User-Assigned Managed Identity](https://learn.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview): Assigned the [AcrPull](https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles/containers#acrpull) role on the Azure Container Registry, enabling the Web App to pull the container image without storing credentials. +8. [Azure Web App](https://learn.microsoft.com/en-us/azure/app-service/overview): Runs the Python Flask application from the custom container image stored in the Azure Container Registry. + +The provisioning process assigns the user-defined managed identity to the web app and uses its credentials to access the Azure Container Registry to pull the container image. The [AcrPull](https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles/containers#acrpull) role is assigned to the managed identity with the Azure Container Registry as the scope. + +For more information on the sample application, see [Azure Web App with Custom Docker Image](../README.md). + +## Configuration + +Before deploying the `main.bicep` template, update the `main.bicepparam` file with your specific values: + +```bicep +using 'main.bicep' + +param prefix = 'local' +param suffix = 'test' +param imageName = 'custom-image-webapp' +param imageTag = 'v1' +param tags = { + environment: 'test' + project: 'custom-image-webapp' +} +``` + +## Provisioning Scripts + +See [deploy.sh](deploy.sh) for the complete deployment automation. The script performs: + +- Detects environment (LocalStack vs Azure Cloud) and uses appropriate CLI +- Creates resource group if it doesn't exist +- Optionally validates the Bicep template +- Optionally runs what-if deployment for preview +- Deploys [acr.bicep](acr.bicep) to provision the Azure Container Registry and Log Analytics Workspace +- Extracts deployment outputs (ACR name, ACR login server) +- Builds the container image locally and pushes it to the Azure Container Registry +- Deploys [main.bicep](main.bicep) with parameters from [main.bicepparam](main.bicepparam) to provision the remaining resources +- Extracts deployment outputs (Web App name, App Service Plan name, Managed Identity name) + +## Deployment + +You can set up the Azure emulator by utilizing the LocalStack for Azure Docker image. Before starting, ensure you have a valid `LOCALSTACK_AUTH_TOKEN` to access the Azure emulator. Refer to the [Auth Token guide](https://docs.localstack.cloud/getting-started/auth-token/?__hstc=108988063.8aad2b1a7229945859f4d9b9bb71e05d.1743148429561.1758793541854.1758810151462.32&__hssc=108988063.3.1758810151462&__hsfp=3945774529) to obtain your Auth Token and specify it in the `LOCALSTACK_AUTH_TOKEN` environment variable. The Azure Docker image is available on the [LocalStack Docker Hub](https://hub.docker.com/r/localstack/localstack-azure-alpha). To pull the Azure Docker image, execute the following command: + +```bash +docker pull localstack/localstack-azure-alpha +``` + +Start the LocalStack Azure emulator using the localstack CLI, execute the following command: + +```bash +# Set the authentication token +export LOCALSTACK_AUTH_TOKEN= + +# Start the LocalStack Azure emulator +IMAGE_NAME=localstack/localstack-azure-alpha localstack start -d +localstack wait -t 60 + +# Route all Azure CLI calls to the LocalStack Azure emulator +azlocal start-interception +``` + +Navigate to the `bicep` folder: + +```bash +cd samples/web-app-custom-image/python/bicep +``` + +Make the script executable: + +```bash +chmod +x deploy.sh +``` + +Run the deployment script: + +```bash +./deploy.sh +``` + +## Validation + +Once the deployment completes, run the [validate.sh](../scripts/validate.sh) script to confirm that all resources were provisioned and configured as expected: + +```bash +#!/bin/bash +set -euo pipefail + +PREFIX='local' +SUFFIX='test' +RESOURCE_GROUP_NAME="${PREFIX}-rg" +ACR_NAME="${PREFIX}acr${SUFFIX}" +MANAGED_IDENTITY_NAME="${PREFIX}-identity-${SUFFIX}" +APP_SERVICE_PLAN_NAME="${PREFIX}-app-service-plan-${SUFFIX}" +WEB_APP_NAME="${PREFIX}-webapp-${SUFFIX}" +VIRTUAL_NETWORK_NAME="${PREFIX}-vnet-${SUFFIX}" +PRIVATE_DNS_ZONE_NAME="privatelink.azurecr.io" +PRIVATE_ENDPOINT_NAME="${PREFIX}-acr-pe-${SUFFIX}" +WEB_APP_SUBNET_NSG_NAME="${PREFIX}-webapp-subnet-nsg-${SUFFIX}" +PE_SUBNET_NSG_NAME="${PREFIX}-pe-subnet-nsg-${SUFFIX}" +NAT_GATEWAY_NAME="${PREFIX}-nat-gateway-${SUFFIX}" +PIP_PREFIX_NAME="${PREFIX}-nat-gateway-pip-prefix-${SUFFIX}" +LOG_ANALYTICS_NAME="${PREFIX}-log-analytics-${SUFFIX}" + +# Check resource group +echo -e "[$RESOURCE_GROUP_NAME] resource group:\n" +az group show \ + --name "$RESOURCE_GROUP_NAME" \ + --output table + +# Check managed identity +echo -e "[$MANAGED_IDENTITY_NAME] managed identity:\n" +az identity show \ + --name "$MANAGED_IDENTITY_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --output table + +# Check App Service Plan +echo -e "\n[$APP_SERVICE_PLAN_NAME] App Service Plan:\n" +az appservice plan show \ + --name "$APP_SERVICE_PLAN_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --output table + +# Check Azure Container Registry +echo -e "\n[$ACR_NAME] Azure Container Registry:\n" +az acr show \ + --name "$ACR_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --output table + +# Check Azure Web App +echo -e "\n[$WEB_APP_NAME] Web App:\n" +az webapp show \ + --name "$WEB_APP_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --query "{name:name, state:state, defaultHostName:defaultHostName, kind:kind}" \ + --output table + +# Check App Settings +echo -e "\n[$WEB_APP_NAME] app settings:\n" +az webapp config appsettings list \ + --name "$WEB_APP_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --query "[?name=='IMAGE_NAME' || name=='APP_NAME' || name=='WEBSITES_PORT']" \ + --output table + +# Check Virtual Network +echo -e "\n[$VIRTUAL_NETWORK_NAME] virtual network:\n" +az network vnet show \ + --name "$VIRTUAL_NETWORK_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --output table \ + --only-show-errors + +# Check Private DNS Zone +echo -e "\n[$PRIVATE_DNS_ZONE_NAME] private dns zone:\n" +az network private-dns zone show \ + --name "$PRIVATE_DNS_ZONE_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --query '{Name:name,ResourceGroup:resourceGroup,RecordSets:recordSets,VirtualNetworkLinks:virtualNetworkLinks}' \ + --output table \ + --only-show-errors + +# Check Private Endpoint +echo -e "\n[$PRIVATE_ENDPOINT_NAME] private endpoint:\n" +az network private-endpoint show \ + --name "$PRIVATE_ENDPOINT_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --output table \ + --only-show-errors + +# Check Web App Subnet NSG +echo -e "\n[$WEB_APP_SUBNET_NSG_NAME] network security group:\n" +az network nsg show \ + --name "$WEB_APP_SUBNET_NSG_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --output table \ + --only-show-errors + +# Check Private Endpoint Subnet NSG +echo -e "\n[$PE_SUBNET_NSG_NAME] network security group:\n" +az network nsg show \ + --name "$PE_SUBNET_NSG_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --output table \ + --only-show-errors + +# Check NAT Gateway +echo -e "\n[$NAT_GATEWAY_NAME] nat gateway:\n" +az network nat gateway show \ + --name "$NAT_GATEWAY_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --output table \ + --only-show-errors + +# Check Public IP Prefix +echo -e "\n[$PIP_PREFIX_NAME] public ip prefix:\n" +az network public-ip prefix show \ + --name "$PIP_PREFIX_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --output table \ + --only-show-errors + +# Check Log Analytics Workspace +echo -e "\n[$LOG_ANALYTICS_NAME] log analytics workspace:\n" +az monitor log-analytics workspace show \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --workspace-name "$LOG_ANALYTICS_NAME" \ + --query '{Name:name,Location:location,ResourceGroup:resourceGroup}' \ + --output table \ + --only-show-errors + +echo -e "\nResources in [$RESOURCE_GROUP_NAME]:\n" +az resource list \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --output table +``` + +## Cleanup + +To destroy all created resources: + +```bash +# Delete resource group and all contained resources +az group delete --name local-rg --yes --no-wait + +# Verify deletion +az group list --output table +``` + +This will remove all Azure resources created by the Bicep deployment script. + +## Related Documentation + +- [Azure Bicep Documentation](https://docs.microsoft.com/en-us/azure/azure-resource-manager/bicep/) +- [Bicep Language Reference](https://docs.microsoft.com/en-us/azure/azure-resource-manager/bicep/bicep-functions) +- [LocalStack for Azure Documentation](https://docs.localstack.cloud/azure/) \ No newline at end of file diff --git a/samples/web-app-custom-image/python/bicep/acr.bicep b/samples/web-app-custom-image/python/bicep/acr.bicep new file mode 100644 index 0000000..01a4e6b --- /dev/null +++ b/samples/web-app-custom-image/python/bicep/acr.bicep @@ -0,0 +1,81 @@ +//******************************************** +// Parameters +//******************************************** +@description('Specifies the prefix for the name of the Azure resources.') +@minLength(2) +param prefix string = take(uniqueString(resourceGroup().id), 4) + +@description('Specifies the suffix for the name of the Azure resources.') +@minLength(2) +param suffix string = take(uniqueString(resourceGroup().id), 4) + +@description('Specifies the location for all resources.') +param location string = resourceGroup().location + +@description('Specifies the name of the Azure Log Analytics resource.') +param logAnalyticsName string = '' + +@description('Specifies the service tier of the workspace: Free, Standalone, PerNode, Per-GB.') +@allowed([ + 'Free' + 'Standalone' + 'PerNode' + 'PerGB2018' +]) +param logAnalyticsSku string = 'PerNode' + +@description('Specifies the workspace data retention in days. -1 means Unlimited retention for the Unlimited Sku. 730 days is the maximum allowed for all other Skus.') +param logAnalyticsRetentionInDays int = 60 + +@description('Tier of your Azure Container Registry.') +@allowed([ + 'Basic' + 'Standard' + 'Premium' +]) +param acrSku string = 'Premium' + +@description('Specifies the tags to be applied to the resources.') +param tags object = { + environment: 'test' + iac: 'bicep' +} + +//******************************************** +// Variables +//******************************************** +var acrName = '${prefix}acr${suffix}' + +//******************************************** +// Modules and Resources +//******************************************** +module workspace 'modules/log-analytics.bicep' = { + name: 'workspace' + params: { + // properties + name: empty(logAnalyticsName) ? toLower('${prefix}-log-analytics-${suffix}') : logAnalyticsName + location: location + tags: tags + sku: logAnalyticsSku + retentionInDays: logAnalyticsRetentionInDays + } +} + +module containerRegistry './modules/container-registry.bicep' = { + name: 'containerRegistry' + params: { + name: acrName + sku: acrSku + adminUserEnabled: true + workspaceId: workspace.outputs.id + location: location + tags: tags + } +} + +//******************************************** +// Outputs +//******************************************** +output acrName string = containerRegistry.outputs.name +output acrLoginServer string = containerRegistry.outputs.loginServer +output logAnalyticsWorkspaceName string = workspace.outputs.name diff --git a/samples/web-app-custom-image/python/bicep/acr.bicepparam b/samples/web-app-custom-image/python/bicep/acr.bicepparam new file mode 100644 index 0000000..f58b20d --- /dev/null +++ b/samples/web-app-custom-image/python/bicep/acr.bicepparam @@ -0,0 +1,8 @@ +using 'acr.bicep' + +param prefix = 'local' +param suffix = 'test' +param tags = { + environment: 'test' + project: 'custom-image-webapp' +} diff --git a/samples/web-app-custom-image/python/bicep/deploy.sh b/samples/web-app-custom-image/python/bicep/deploy.sh new file mode 100755 index 0000000..9c16125 --- /dev/null +++ b/samples/web-app-custom-image/python/bicep/deploy.sh @@ -0,0 +1,254 @@ +#!/bin/bash + +# Variables +PREFIX='local' +SUFFIX='test' +ACR_TEMPLATE="acr.bicep" +ACR_PARAMETERS="acr.bicepparam" +MAIN_TEMPLATE="main.bicep" +MAIN_PARAMETERS="main.bicepparam" +RESOURCE_GROUP_NAME="${PREFIX}-rg" +LOCATION="westeurope" +IMAGE_NAME="custom-image-webapp" +IMAGE_TAG="v1" +LOCAL_IMAGE="${IMAGE_NAME}:${IMAGE_TAG}" +VALIDATE_TEMPLATE=1 +USE_WHAT_IF=0 +SUBSCRIPTION_NAME=$(az account show --query name --output tsv) +CURRENT_DIR="$(cd "$(dirname "$0")" && pwd)" + +# Change the current directory to the script's directory +cd "$CURRENT_DIR" || exit + +# Validates if the resource group exists in the subscription, if not creates it +echo "Checking if resource group [$RESOURCE_GROUP_NAME] exists in the subscription [$SUBSCRIPTION_NAME]..." +az group show --name $RESOURCE_GROUP_NAME &>/dev/null + +if [[ $? != 0 ]]; then + echo "No resource group [$RESOURCE_GROUP_NAME] exists in the subscription [$SUBSCRIPTION_NAME]" + echo "Creating resource group [$RESOURCE_GROUP_NAME] in the subscription [$SUBSCRIPTION_NAME]..." + + # Create the resource group + az group create \ + --name $RESOURCE_GROUP_NAME \ + --location $LOCATION \ + --only-show-errors 1> /dev/null + + if [[ $? == 0 ]]; then + echo "Resource group [$RESOURCE_GROUP_NAME] successfully created in the subscription [$SUBSCRIPTION_NAME]" + else + echo "Failed to create resource group [$RESOURCE_GROUP_NAME] in the subscription [$SUBSCRIPTION_NAME]" + exit + fi +else + echo "Resource group [$RESOURCE_GROUP_NAME] already exists in the subscription [$SUBSCRIPTION_NAME]" +fi + +echo "Deploying Azure Container Registry and Log Analytics Workspace Bicep..." + +# Validates the Bicep template +if [[ $VALIDATE_TEMPLATE == 1 ]]; then + if [[ $USE_WHAT_IF == 1 ]]; then + # Execute a deployment What-If operation at resource group scope. + echo "Previewing changes deployed by Bicep template [$ACR_TEMPLATE]..." + az deployment group what-if \ + --resource-group $RESOURCE_GROUP_NAME \ + --template-file $ACR_TEMPLATE \ + --parameters $ACR_PARAMETERS \ + --parameters location=$LOCATION \ + prefix=$PREFIX \ + suffix=$SUFFIX \ + --only-show-errors + + if [[ $? == 0 ]]; then + echo "Bicep template [$ACR_TEMPLATE] validation succeeded" + else + echo "Failed to validate Bicep template [$ACR_TEMPLATE]" + exit + fi + else + # Validate the Bicep template + echo "Validating Bicep template [$ACR_TEMPLATE]..." + output=$(az deployment group validate \ + --resource-group $RESOURCE_GROUP_NAME \ + --template-file $ACR_TEMPLATE \ + --parameters $ACR_PARAMETERS \ + --parameters location=$LOCATION \ + prefix=$PREFIX \ + suffix=$SUFFIX \ + --only-show-errors) + + if [[ $? == 0 ]]; then + echo "Bicep template [$ACR_TEMPLATE] validation succeeded" + else + echo "Failed to validate Bicep template [$ACR_TEMPLATE]" + echo "$output" + exit + fi + fi +fi + +# Deploy the Bicep template +echo "Deploying Bicep template [$ACR_TEMPLATE]..." +if DEPLOYMENT_OUTPUTS=$(az deployment group create \ + --resource-group $RESOURCE_GROUP_NAME \ + --only-show-errors \ + --template-file $ACR_TEMPLATE \ + --parameters $ACR_PARAMETERS \ + --parameters location=$LOCATION \ + prefix=$PREFIX \ + suffix=$SUFFIX \ + --query 'properties.outputs' -o json); then + # Extract only the JSON portion (everything from first { to the end) + DEPLOYMENT_JSON=$(echo "$DEPLOYMENT_OUTPUTS" | sed -n '/{/,$ p') + echo "Bicep template [$ACR_TEMPLATE] deployed successfully. Outputs:" + echo "$DEPLOYMENT_JSON" | jq . + ACR_NAME=$(echo "$DEPLOYMENT_JSON" | jq -r '.acrName.value') + ACR_LOGIN_SERVER=$(echo "$DEPLOYMENT_JSON" | jq -r '.acrLoginServer.value') + LOG_ANALYTICS_WORKSPACE_NAME=$(echo "$DEPLOYMENT_JSON" | jq -r '.logAnalyticsWorkspaceName.value') + echo "Deployment complete." + echo "Resource Group: $RESOURCE_GROUP_NAME" + echo "Azure Container Registry: $ACR_NAME ($ACR_LOGIN_SERVER)" +else + echo "Failed to deploy Bicep template [$ACR_TEMPLATE]" + exit 1 +fi + +if [[ -z "$ACR_NAME" || -z "$ACR_LOGIN_SERVER" || -z "$LOG_ANALYTICS_WORKSPACE_NAME" ]]; then + echo "ACR Name, ACR Login Server, or Log Analytics Workspace Name is empty. Exiting." + exit 1 +fi + +echo "Logging into Azure Container Registry [$ACR_NAME]..." +az acr login --name "$ACR_NAME" --only-show-errors + +if [ $? -eq 0 ]; then + echo "Logged into Azure Container Registry [$ACR_NAME] successfully." +else + echo "Failed to log into Azure Container Registry [$ACR_NAME]." + exit 1 +fi + +# Create full image name with login server, image name, and tag +FULL_IMAGE="${ACR_LOGIN_SERVER}/${IMAGE_NAME}:${IMAGE_TAG}" + +echo "Building custom Docker image [$LOCAL_IMAGE]..." +docker build -t "$LOCAL_IMAGE" ../src/ + +if [ $? -eq 0 ]; then + echo "Docker image [$LOCAL_IMAGE] built successfully." +else + echo "Failed to build Docker image [$LOCAL_IMAGE]." + exit 1 +fi + +echo "Tagging Docker image [$LOCAL_IMAGE] as [$FULL_IMAGE]..." +docker tag "$LOCAL_IMAGE" "$FULL_IMAGE" + +if [ $? -eq 0 ]; then + echo "Docker image [$LOCAL_IMAGE] tagged as [$FULL_IMAGE] successfully." +else + echo "Failed to tag Docker image [$LOCAL_IMAGE] as [$FULL_IMAGE]." + exit 1 +fi + +echo "Pushing image [$FULL_IMAGE] to ACR..." +docker push "$FULL_IMAGE" + +if [ $? -eq 0 ]; then + echo "Docker image [$FULL_IMAGE] pushed to ACR successfully." +else + echo "Failed to push Docker image [$FULL_IMAGE] to ACR." + exit 1 +fi + +echo "Deploying the remaining Azure resources..." + +# Validates the Bicep template +if [[ $VALIDATE_TEMPLATE == 1 ]]; then + if [[ $USE_WHAT_IF == 1 ]]; then + # Execute a deployment What-If operation at resource group scope. + echo "Previewing changes deployed by Bicep template [$MAIN_TEMPLATE]..." + az deployment group what-if \ + --resource-group $RESOURCE_GROUP_NAME \ + --template-file $MAIN_TEMPLATE \ + --parameters $MAIN_PARAMETERS \ + --parameters location=$LOCATION \ + prefix=$PREFIX \ + suffix=$SUFFIX \ + imageName=$IMAGE_NAME \ + imageTag=$IMAGE_TAG \ + acrName="$ACR_NAME" \ + logAnalyticsWorkspaceName="$LOG_ANALYTICS_WORKSPACE_NAME" \ + --only-show-errors + + if [[ $? == 0 ]]; then + echo "Bicep template [$MAIN_TEMPLATE] validation succeeded" + else + echo "Failed to validate Bicep template [$MAIN_TEMPLATE]" + exit + fi + else + # Validate the Bicep template + echo "Validating Bicep template [$MAIN_TEMPLATE]..." + output=$(az deployment group validate \ + --resource-group $RESOURCE_GROUP_NAME \ + --template-file $MAIN_TEMPLATE \ + --parameters $MAIN_PARAMETERS \ + --parameters location=$LOCATION \ + prefix=$PREFIX \ + suffix=$SUFFIX \ + imageName=$IMAGE_NAME \ + imageTag=$IMAGE_TAG \ + acrName="$ACR_NAME" \ + logAnalyticsWorkspaceName="$LOG_ANALYTICS_WORKSPACE_NAME" \ + --only-show-errors) + + if [[ $? == 0 ]]; then + echo "Bicep template [$MAIN_TEMPLATE] validation succeeded" + else + echo "Failed to validate Bicep template [$MAIN_TEMPLATE]" + echo "$output" + exit + fi + fi +fi + +# Deploy the Bicep template +echo "Deploying Bicep template [$MAIN_TEMPLATE]..." +if DEPLOYMENT_OUTPUTS=$(az deployment group create \ + --resource-group $RESOURCE_GROUP_NAME \ + --only-show-errors \ + --template-file $MAIN_TEMPLATE \ + --parameters $MAIN_PARAMETERS \ + --parameters location=$LOCATION \ + prefix=$PREFIX \ + suffix=$SUFFIX \ + imageName=$IMAGE_NAME \ + imageTag=$IMAGE_TAG \ + acrName="$ACR_NAME" \ + logAnalyticsWorkspaceName="$LOG_ANALYTICS_WORKSPACE_NAME" \ + --query 'properties.outputs' -o json); then + # Extract only the JSON portion (everything from first { to the end) + DEPLOYMENT_JSON=$(echo "$DEPLOYMENT_OUTPUTS" | sed -n '/{/,$ p') + echo "Bicep template [$MAIN_TEMPLATE] deployed successfully. Outputs:" + echo "$DEPLOYMENT_JSON" | jq . + APP_SERVICE_PLAN_NAME=$(echo "$DEPLOYMENT_JSON" | jq -r '.appServicePlanName.value') + WEB_APP_NAME=$(echo "$DEPLOYMENT_JSON" | jq -r '.webAppName.value') + ACR_NAME=$(echo "$DEPLOYMENT_JSON" | jq -r '.acrName.value') + ACR_LOGIN_SERVER=$(echo "$DEPLOYMENT_JSON" | jq -r '.acrLoginServer.value') + MANAGED_IDENTITY_NAME=$(echo "$DEPLOYMENT_JSON" | jq -r '.managedIdentityName.value') + echo "Deployment complete." + echo "Resource Group: $RESOURCE_GROUP_NAME" + echo "App Service Plan: $APP_SERVICE_PLAN_NAME" + echo "Web App: $WEB_APP_NAME" + echo "Azure Container Registry: $ACR_NAME ($ACR_LOGIN_SERVER)" + echo "Managed Identity: $MANAGED_IDENTITY_NAME" +else + echo "Failed to deploy Bicep template [$MAIN_TEMPLATE]" + exit 1 +fi + +# Print the list of resources in the resource group +echo "Listing resources in resource group [$RESOURCE_GROUP_NAME]..." +az resource list --resource-group "$RESOURCE_GROUP_NAME" --output table \ No newline at end of file diff --git a/samples/web-app-custom-image/python/bicep/main.bicep b/samples/web-app-custom-image/python/bicep/main.bicep new file mode 100644 index 0000000..802b75c --- /dev/null +++ b/samples/web-app-custom-image/python/bicep/main.bicep @@ -0,0 +1,305 @@ +//******************************************** +// Parameters +//******************************************** +@description('Specifies the prefix for the name of the Azure resources.') +@minLength(2) +param prefix string = take(uniqueString(resourceGroup().id), 4) + +@description('Specifies the suffix for the name of the Azure resources.') +@minLength(2) +param suffix string = take(uniqueString(resourceGroup().id), 4) + +@description('Specifies the location for all resources.') +param location string = resourceGroup().location + +@description('Specifies the name of the image to be used for the Web App.') +param imageName string + +@description('Specifies the tag of the image to be used for the Web App.') +param imageTag string + +@description('Specifies the tier name for the hosting plan.') +@allowed([ + 'Basic' + 'Standard' + 'ElasticPremium' + 'Premium' + 'PremiumV2' + 'Premium0V3' + 'PremiumV3' + 'PremiumMV3' + 'Isolated' + 'IsolatedV2' + 'WorkflowStandard' + 'FlexConsumption' +]) +param skuTier string = 'Standard' + +@description('Specifies the SKU name for the hosting plan.') +@allowed([ + 'B1' + 'B2' + 'B3' + 'S1' + 'S2' + 'S3' + 'EP1' + 'EP2' + 'EP3' + 'P1' + 'P2' + 'P3' + 'P1V2' + 'P2V2' + 'P3V2' + 'P0V3' + 'P1V3' + 'P2V3' + 'P3V3' + 'P1MV3' + 'P2MV3' + 'P3MV3' + 'P4MV3' + 'P5MV3' + 'I1' + 'I2' + 'I3' + 'I1V2' + 'I2V2' + 'I3V2' + 'I4V2' + 'I5V2' + 'I6V2' + 'WS1' + 'WS2' + 'WS3' + 'FC1' +]) +param skuName string = 'S1' + +@description('Specifies the kind of the hosting plan.') +@allowed([ + 'app' + 'elastic' + 'functionapp' + 'windows' + 'linux' +]) +param appServicePlanKind string = 'linux' + +@description('Specifies whether the hosting plan is reserved.') +param reserved bool = true + +@description('Specifies whether the hosting plan is zone redundant.') +param zoneRedundant bool = false + +@description('Specifies the kind of the hosting plan.') +@allowed([ + 'app' // Windows Web app + 'app,linux' // Linux Web app + 'app,linux,container' // Linux Container Web app + 'hyperV' // Windows Container Web App + 'app,container,windows' // Windows Container Web App + 'app,linux,kubernetes' // Linux Web App on ARC + 'app,linux,container,kubernetes' // Linux Container Web App on ARC + 'functionapp' // Function Code App + 'functionapp,linux' // Linux Consumption Function app + 'functionapp,linux,container,kubernetes' // Function Container App on ARC + 'functionapp,linux,kubernetes' // Function Code App on ARC +]) +param webAppKind string = 'app,linux' + +@description('Specifies whether HTTPS is enforced for the Azure Web App.') +param httpsOnly bool = false + +@description('Specifies the minimum TLS version for the Azure Web App.') +@allowed([ + '1.2' + '1.3' +]) +param minTlsVersion string = '1.2' + +@description('Specifies whether the public network access is enabled or disabled') +@allowed([ + 'Enabled' + 'Disabled' +]) +param publicNetworkAccess string = 'Enabled' + +@description('Specifies the optional Git Repo URL.') +param repoUrl string = ' ' + +@description('Specifies the name of the virtual network.') +param virtualNetworkName string = '' + +@description('Specifies the address prefixes of the virtual network.') +param virtualNetworkAddressPrefixes string = '10.0.0.0/8' + +@description('Specifies the name of the subnet used by the Web App for the regional virtual network integration.') +param webAppSubnetName string = 'app-subnet' + +@description('Specifies the address prefix of the subnet used by the Web App for the regional virtual network integration.') +param webAppSubnetAddressPrefix string = '10.0.0.0/24' + +@description('Specifies the name of the network security group associated to the subnet hosting the Web App.') +param webAppSubnetNsgName string = '' + +@description('Specifies the name of the subnet which contains the private endpoint to the Azure CosmosDB for MongoDB API account.') +param peSubnetName string = 'pe-subnet' + +@description('Specifies the address prefix of the subnet which contains the private endpoint to the Azure CosmosDB for MongoDB API account.') +param peSubnetAddressPrefix string = '10.0.1.0/24' + +@description('Specifies the name of the network security group associated to the subnet hosting the private endpoint to the Azure CosmosDB for MongoDB API account.') +param peSubnetNsgName string = '' + +@description('Specifies the length of the Public IP Prefix.') +@minValue(28) +@maxValue(32) +param natGatewayPublicIpPrefixLength int = 31 + +@description('Specifies the name of the Azure NAT Gateway.') +param natGatewayName string = '' + +@description('Specifies a list of availability zones denoting the zone in which Nat Gateway should be deployed.') +param natGatewayZones array = [] + +@description('Specifies the idle timeout in minutes for the Azure NAT Gateway.') +param natGatewayIdleTimeoutMins int = 30 + +@description('Specifies the name of the Azure Container Registry resource.') +param acrName string = '' + +@description('Specifies the name of the Azure Log Analytics resource.') +param logAnalyticsWorkspaceName string = '' + +@description('Specifies the tags to be applied to the resources.') +param tags object = { + environment: 'test' + iac: 'bicep' +} + +//******************************************** +// Variables +//******************************************** +var webAppName = '${prefix}-webapp-${suffix}' +var appServicePlanName = '${prefix}-app-service-plan-${suffix}' +var managedIdentityName = '${prefix}-identity-${suffix}' +var privateDnsZoneName = 'privatelink.azurecr.io' +var privateEndpointName = '${prefix}-acr-pe-${suffix}' + +//******************************************** +// Modules and Resources +//******************************************** +resource workspace 'Microsoft.OperationalInsights/workspaces@2025-07-01' existing = { + name: logAnalyticsWorkspaceName == '' ? toLower('${prefix}-log-analytics-${suffix}') : logAnalyticsWorkspaceName +} +resource containerRegistry 'Microsoft.ContainerRegistry/registries@2024-11-01-preview' existing = { + name: acrName == '' ? toLower('${prefix}acr${suffix}') : acrName +} + +module managedIdentity 'modules/managed-identity.bicep' = { + name: 'managedIdentity' + params: { + // properties + name: managedIdentityName + containerRegistryName: containerRegistry.name + location: location + tags: tags + } +} + +module network 'modules/virtual-network.bicep' = { + name: 'network' + params: { + virtualNetworkName: empty(virtualNetworkName) ? toLower('${prefix}-vnet-${suffix}') : virtualNetworkName + virtualNetworkAddressPrefixes: virtualNetworkAddressPrefixes + webAppSubnetName: webAppSubnetName + webAppSubnetAddressPrefix: webAppSubnetAddressPrefix + webAppSubnetNsgName: empty(webAppSubnetNsgName) ? toLower('${prefix}-webapp-subnet-nsg-${suffix}') : webAppSubnetNsgName + peSubnetName: peSubnetName + peSubnetAddressPrefix: peSubnetAddressPrefix + peSubnetNsgName: empty(peSubnetNsgName) ? toLower('${prefix}-pe-subnet-nsg-${suffix}') : peSubnetNsgName + natGatewayName: empty(natGatewayName) ? toLower('${prefix}-nat-gateway-${suffix}') : natGatewayName + natGatewayZones: natGatewayZones + natGatewayPublicIpPrefixName: toLower('${prefix}-nat-gateway-pip-prefix-${suffix}') + natGatewayPublicIpPrefixLength: natGatewayPublicIpPrefixLength + natGatewayIdleTimeoutMins: natGatewayIdleTimeoutMins + delegationServiceName: skuTier == 'FlexConsumption' ? 'Microsoft.App/environments' : 'Microsoft.Web/serverfarms' + workspaceId: workspace.id + location: location + tags: tags + } +} + +module privateDnsZone 'modules/private-dns-zone.bicep' = { + name: 'privateDnsZone' + params: { + name: privateDnsZoneName + vnetId: network.outputs.virtualNetworkId + tags: tags + } +} + +module privateEndpoints 'modules/private-endpoint.bicep' = { + name: 'privateEndpoints' + params: { + name: privateEndpointName + privateLinkServiceId: containerRegistry.id + privateDnsZoneId: privateDnsZone.outputs.id + vnetId: network.outputs.virtualNetworkId + subnetId: network.outputs.peSubnetId + groupIds: [ + 'registry' + ] + location: location + tags: tags + } +} + +module appServicePlan 'modules/app-service-plan.bicep' = { + name: 'appServicePlan' + params: { + name: appServicePlanName + location: location + skuName: skuName + skuTier: skuTier + kind: appServicePlanKind + reserved: reserved + zoneRedundant: zoneRedundant + workspaceId: workspace.id + tags: tags + } +} + +module webApp 'modules/web-app.bicep' = { + name: webAppName + params: { + name: webAppName + location: location + kind: webAppKind + httpsOnly: httpsOnly + minTlsVersion: minTlsVersion + publicNetworkAccess: publicNetworkAccess + repoUrl: repoUrl + virtualNetworkName: network.outputs.virtualNetworkName + subnetName: network.outputs.webAppSubnetName + hostingPlanName: appServicePlan.outputs.name + loginServer: containerRegistry.properties.loginServer + imageName: imageName + imageTag: imageTag + managedIdentityName: managedIdentity.outputs.name + managedIdentityType: 'UserAssigned' + workspaceId: workspace.id + tags: tags + } +} + +//******************************************** +// Outputs +//******************************************** +output appServicePlanName string = appServicePlan.outputs.name +output webAppName string = webApp.outputs.name +output acrName string = containerRegistry.name +output acrLoginServer string = containerRegistry.properties.loginServer +output managedIdentityName string = managedIdentity.outputs.name diff --git a/samples/web-app-custom-image/python/bicep/main.bicepparam b/samples/web-app-custom-image/python/bicep/main.bicepparam new file mode 100644 index 0000000..0bfd022 --- /dev/null +++ b/samples/web-app-custom-image/python/bicep/main.bicepparam @@ -0,0 +1,10 @@ +using 'main.bicep' + +param prefix = 'local' +param suffix = 'test' +param imageName = 'custom-image-webapp' +param imageTag = 'v1' +param tags = { + environment: 'test' + project: 'custom-image-webapp' +} diff --git a/samples/web-app-custom-image/python/bicep/modules/app-service-plan.bicep b/samples/web-app-custom-image/python/bicep/modules/app-service-plan.bicep new file mode 100644 index 0000000..4b5cfb3 --- /dev/null +++ b/samples/web-app-custom-image/python/bicep/modules/app-service-plan.bicep @@ -0,0 +1,154 @@ +//******************************************** +// Parameters +//******************************************** +@description('Specifies the name of the App Service Plan.') +param name string + +@description('Specifies the location.') +param location string = resourceGroup().location + +@description('Specifies the tier name for the hosting plan.') +@allowed([ + 'Basic' + 'Standard' + 'ElasticPremium' + 'Premium' + 'PremiumV2' + 'Premium0V3' + 'PremiumV3' + 'PremiumMV3' + 'Isolated' + 'IsolatedV2' + 'WorkflowStandard' + 'FlexConsumption' +]) +param skuTier string = 'Standard' + +@description('Specifies the SKU name for the hosting plan.') +@allowed([ + 'B1' + 'B2' + 'B3' + 'S1' + 'S2' + 'S3' + 'EP1' + 'EP2' + 'EP3' + 'P1' + 'P2' + 'P3' + 'P1V2' + 'P2V2' + 'P3V2' + 'P0V3' + 'P1V3' + 'P2V3' + 'P3V3' + 'P1MV3' + 'P2MV3' + 'P3MV3' + 'P4MV3' + 'P5MV3' + 'I1' + 'I2' + 'I3' + 'I1V2' + 'I2V2' + 'I3V2' + 'I4V2' + 'I5V2' + 'I6V2' + 'WS1' + 'WS2' + 'WS3' + 'FC1' +]) +param skuName string = 'S1' + +@description('Specifies the kind of the hosting plan.') +@allowed([ + 'app' + 'elastic' + 'functionapp' + 'windows' + 'linux' +]) +param kind string = 'linux' + +@description('Specifies whether the hosting plan is reserved.') +param reserved bool = true + +@description('Specifies whether the hosting plan is zone redundant.') +param zoneRedundant bool = false + +@description('Specifies the resource id of the Log Analytics workspace.') +param workspaceId string + +@description('Specifies the tags to be applied to the resources.') +param tags object = {} + +//******************************************** +// Variables +//******************************************** + +var diagnosticSettingsName = 'default' +var logCategories = [] +var metricCategories = [ + 'AllMetrics' +] +var logs = [ + for category in logCategories: { + category: category + enabled: true + retentionPolicy: { + enabled: true + days: 0 + } + } +] +var metrics = [ + for category in metricCategories: { + category: category + enabled: true + retentionPolicy: { + enabled: true + days: 0 + } + } +] + +//******************************************** +// Resources +//******************************************** +resource appServicePlan 'Microsoft.Web/serverfarms@2024-11-01' = { + name: name + location: location + tags: tags + kind: kind + sku: { + tier: skuTier + name: skuName + } + properties: { + reserved: reserved + zoneRedundant: zoneRedundant + maximumElasticWorkerCount: skuTier == 'FlexConsumption' ? 1 : 20 + } +} + +resource diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if(!empty(workspaceId)) { + name: diagnosticSettingsName + scope: appServicePlan + properties: { + workspaceId: workspaceId + logs: logs + metrics: metrics + } +} + +//******************************************** +// Outputs +//******************************************** +output id string = appServicePlan.id +output name string = appServicePlan.name diff --git a/samples/web-app-custom-image/python/bicep/modules/container-registry.bicep b/samples/web-app-custom-image/python/bicep/modules/container-registry.bicep new file mode 100644 index 0000000..e830e39 --- /dev/null +++ b/samples/web-app-custom-image/python/bicep/modules/container-registry.bicep @@ -0,0 +1,135 @@ + +//******************************************** +// Parameters +//******************************************** + +@description('Name of your Azure Container Registry') +@minLength(5) +@maxLength(50) +param name string = 'acr${uniqueString(resourceGroup().id)}' + +@description('Enable admin user that have push / pull permission to the registry.') +param adminUserEnabled bool = true + +@description('Specifies whether to allow public network access for the container registry.') +@allowed([ + 'Disabled' + 'Enabled' +]) +param publicNetworkAccess string = 'Enabled' + +@description('Tier of your Azure Container Registry.') +@allowed([ + 'Basic' + 'Standard' + 'Premium' +]) +param sku string = 'Premium' + +@description('Specifies whether or not registry-wide pull is enabled from unauthenticated clients.') +param anonymousPullEnabled bool = true + +@description('Specifies whether or not a single data endpoint is enabled per region for serving data.') +param dataEndpointEnabled bool = true + +@description('Specifies the network rule set for the container registry.') +param networkRuleSet object = { + defaultAction: 'Allow' +} + +@description('Specifies ehether to allow trusted Azure services to access a network restricted registry.') +@allowed([ + 'AzureServices' + 'None' +]) +param networkRuleBypassOptions string = 'AzureServices' + +@description('Specifies whether or not zone redundancy is enabled for this container registry.') +@allowed([ + 'Disabled' + 'Enabled' +]) +param zoneRedundancy string = 'Disabled' + +@description('Specifies the resource id of the Log Analytics workspace.') +param workspaceId string + +@description('Specifies the location.') +param location string = resourceGroup().location + +@description('Specifies the resource tags.') +param tags object + +//******************************************** +// Variables +//******************************************** + +var diagnosticSettingsName = 'diagnosticSettings' +var logCategories = [ + 'ContainerRegistryRepositoryEvents' + 'ContainerRegistryLoginEvents' +] +var metricCategories = [ + 'AllMetrics' +] +var logs = [ + for category in logCategories: { + category: category + enabled: true + retentionPolicy: { + enabled: true + days: 0 + } + } +] +var metrics = [ + for category in metricCategories: { + category: category + enabled: true + retentionPolicy: { + enabled: true + days: 0 + } + } +] + +//******************************************** +// Resources +//******************************************** + +resource containerRegistry 'Microsoft.ContainerRegistry/registries@2024-11-01-preview' = { + name: name + location: location + tags: tags + sku: { + name: sku + } + properties: { + adminUserEnabled: adminUserEnabled + anonymousPullEnabled: anonymousPullEnabled + dataEndpointEnabled: dataEndpointEnabled + networkRuleBypassOptions: networkRuleBypassOptions + networkRuleSet: networkRuleSet + publicNetworkAccess: publicNetworkAccess + zoneRedundancy: zoneRedundancy + } +} + +resource diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { + name: diagnosticSettingsName + scope: containerRegistry + properties: { + workspaceId: workspaceId + logs: logs + metrics: metrics + } +} + +//******************************************** +// Outputs +//******************************************** + +output id string = containerRegistry.id +output name string = containerRegistry.name +output sku string = containerRegistry.sku.name +output loginServer string = containerRegistry.properties.loginServer diff --git a/samples/web-app-custom-image/python/bicep/modules/log-analytics.bicep b/samples/web-app-custom-image/python/bicep/modules/log-analytics.bicep new file mode 100644 index 0000000..2618829 --- /dev/null +++ b/samples/web-app-custom-image/python/bicep/modules/log-analytics.bicep @@ -0,0 +1,45 @@ +//******************************************** +// Parameters +//******************************************** +@description('Specifies the name of the Log Analytics workspace.') +param name string + +@description('Specifies the location.') +param location string = resourceGroup().location + +@description('Specifies the service tier of the workspace: Free, Standalone, PerNode, Per-GB.') +@allowed([ + 'Free' + 'Standalone' + 'PerNode' + 'PerGB2018' +]) +param sku string = 'PerNode' + +@description('Specifies the workspace data retention in days. -1 means Unlimited retention for the Unlimited Sku. 730 days is the maximum allowed for all other Skus.') +param retentionInDays int = 60 + +@description('Specifies the resource tags.') +param tags object + +//******************************************** +// Resources +//******************************************** +resource workspace 'Microsoft.OperationalInsights/workspaces@2025-07-01' = { + name: name + tags: tags + location: location + properties: { + sku: { + name: sku + } + retentionInDays: retentionInDays + } +} + +//******************************************** +// Outputs +//******************************************** +output id string = workspace.id +output name string = workspace.name +output customerId string = workspace.properties.customerId diff --git a/samples/web-app-custom-image/python/bicep/modules/managed-identity.bicep b/samples/web-app-custom-image/python/bicep/modules/managed-identity.bicep new file mode 100644 index 0000000..656f1a4 --- /dev/null +++ b/samples/web-app-custom-image/python/bicep/modules/managed-identity.bicep @@ -0,0 +1,54 @@ +//******************************************** +// Parameters +//******************************************** + +@description('Specifies the name of the user-defined managed identity.') +param name string + +@description('Specifies the location.') +param location string = resourceGroup().location + +@description('Specifies the name of the Azure Container Registry.') +param containerRegistryName string + +@description('Specifies the resource tags.') +param tags object + +//******************************************** +// Resources +//******************************************** + + +resource containerRegistry 'Microsoft.ContainerRegistry/registries@2025-06-01-preview' existing = { + name: containerRegistryName +} + +resource acrPullRoleDefinition 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { + name: '7f951dda-4ed3-4680-a7ca-43fe172d538d' + scope: subscription() +} + +resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2025-01-31-preview' = { + name: name + location: location + tags: tags +} + +resource acrPullRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(containerRegistry.id, managedIdentity.id, acrPullRoleDefinition.id) + scope: containerRegistry + properties: { + roleDefinitionId: acrPullRoleDefinition.id + principalId: managedIdentity.properties.principalId + principalType: 'ServicePrincipal' + } +} + +//******************************************** +// Outputs +//******************************************** + +output id string = managedIdentity.id +output name string = managedIdentity.name +output clientId string = managedIdentity.properties.clientId +output principalId string = managedIdentity.properties.principalId diff --git a/samples/web-app-custom-image/python/bicep/modules/private-dns-zone.bicep b/samples/web-app-custom-image/python/bicep/modules/private-dns-zone.bicep new file mode 100644 index 0000000..d849259 --- /dev/null +++ b/samples/web-app-custom-image/python/bicep/modules/private-dns-zone.bicep @@ -0,0 +1,41 @@ +//******************************************** +// Parameters +//******************************************** +@description('Specifies the name of the private DNS zone.') +param name string + +@description('Specifies the resource ID of the virtual network where private endpoints will be created.') +param vnetId string + +@description('Specifies the resource tags.') +param tags object + +//******************************************** +// Resources +//******************************************** + +// Private DNS Zones +resource privateDnsZone 'Microsoft.Network/privateDnsZones@2024-06-01' = { + name: name + location: 'global' + tags: tags +} + +// Virtual Network Links +resource privateDnsZoneVirtualNetworkLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2024-06-01' = { + parent: privateDnsZone + name: 'link-to-vnet' + location: 'global' + properties: { + registrationEnabled: false + virtualNetwork: { + id: vnetId + } + } +} + +//******************************************** +// Outputs +//******************************************** +output id string = privateDnsZone.id +output name string = privateDnsZone.name diff --git a/samples/web-app-custom-image/python/bicep/modules/private-endpoint.bicep b/samples/web-app-custom-image/python/bicep/modules/private-endpoint.bicep new file mode 100644 index 0000000..8fd35b8 --- /dev/null +++ b/samples/web-app-custom-image/python/bicep/modules/private-endpoint.bicep @@ -0,0 +1,72 @@ +//******************************************** +// Parameters +//******************************************** +@description('Specifies the name of the private endpoint.') +param name string + +@description('Specifies the location.') +param location string = resourceGroup().location + +@description('Specifies the resource ID of the virtual network where private endpoints will be created.') +param vnetId string + +@description('Specifies the resource ID of the subnet where private endpoints will be created.') +param subnetId string + +@description('Specifies the group IDs for the private link service connection.') +param groupIds array + +@description('Specifies the resource ID of the target resource.') +param privateLinkServiceId string + +@description('Specifies the resource ID of the private DNS zone.') +param privateDnsZoneId string + +@description('Specifies the resource tags.') +param tags object + +//******************************************** +// Resources +//******************************************** + +// Private Endpoints +resource privateEndpoint 'Microsoft.Network/privateEndpoints@2025-05-01' = { + name: name + location: location + tags: tags + properties: { + privateLinkServiceConnections: [ + { + name: '${name}-pls-connection' + properties: { + privateLinkServiceId: privateLinkServiceId + groupIds: groupIds + } + } + ] + subnet: { + id: subnetId + } + } +} + +resource privateDnsZoneGroupName 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2025-05-01' = { + parent: privateEndpoint + name: 'private-dns-zone-group' + properties: { + privateDnsZoneConfigs: [ + { + name: 'dnsConfig' + properties: { + privateDnsZoneId: privateDnsZoneId + } + } + ] + } +} + +//******************************************** +// Outputs +//******************************************** +output id string = privateEndpoint.id +output name string = privateEndpoint.name diff --git a/samples/web-app-custom-image/python/bicep/modules/virtual-network.bicep b/samples/web-app-custom-image/python/bicep/modules/virtual-network.bicep new file mode 100644 index 0000000..1c7a088 --- /dev/null +++ b/samples/web-app-custom-image/python/bicep/modules/virtual-network.bicep @@ -0,0 +1,239 @@ +//******************************************** +// Parameters +//******************************************** +@description('Specifies the name of the virtual network.') +param virtualNetworkName string + +@description('Specifies the location.') +param location string = resourceGroup().location + +@description('Specifies the address prefixes of the virtual network.') +param virtualNetworkAddressPrefixes string = '10.0.0.0/8' + +@description('Specifies the name of the subnet used by the Web App for the regional virtual network integration.') +param webAppSubnetName string = 'functionAppSubnet' + +@description('Specifies the address prefix of the subnet used by the Web App for the regional virtual network integration.') +param webAppSubnetAddressPrefix string = '10.0.0.0/24' + +@description('Specifies the name of the network security group associated to the subnet hosting the Web App.') +param webAppSubnetNsgName string = '' + +@description('Specifies the name of the subnet which contains the private endpoint to the Azure CosmosDB for MongoDB API account.') +param peSubnetName string = 'pe-subnet' + +@description('Specifies the address prefix of the subnet which contains the private endpoint to the Azure CosmosDB for MongoDB API account.') +param peSubnetAddressPrefix string = '10.0.1.0/24' + +@description('Specifies the name of the network security group associated to the subnet hosting the private endpoint to the Azure CosmosDB for MongoDB API account.') +param peSubnetNsgName string = '' + +@description('Specifies the name of the Azure NAT Gateway.') +param natGatewayName string + +@description('Specifies a list of availability zones denoting the zone in which Nat Gateway should be deployed.') +param natGatewayZones array = [] + +@description('Specifies the name of the public IP prefix for the Azure NAT Gateway.') +param natGatewayPublicIpPrefixName string + +@description('Specifies the length of the Public IP Prefix.') +@minValue(28) +@maxValue(32) +param natGatewayPublicIpPrefixLength int = 31 + +@description('Specifies the idle timeout in minutes for the Azure NAT Gateway.') +param natGatewayIdleTimeoutMins int = 30 + +@description('Specifies the delegation service name.') +param delegationServiceName string + +@description('Specifies the resource id of the Log Analytics workspace.') +param workspaceId string + +@description('Specifies the resource tags.') +param tags object + +//******************************************** +// Variables +//******************************************** +var diagnosticSettingsName = 'default' +var nsgLogCategories = [ + 'NetworkSecurityGroupEvent' + 'NetworkSecurityGroupRuleCounter' +] +var nsgLogs = [for category in nsgLogCategories: { + category: category + enabled: true + retentionPolicy: { + enabled: true + days: 0 + } +}] +var vnetLogCategories = [ + 'VMProtectionAlerts' +] +var vnetMetricCategories = [ + 'AllMetrics' +] +var vnetLogs = [for category in vnetLogCategories: { + category: category + enabled: true + retentionPolicy: { + enabled: true + days: 0 + } +}] +var vnetMetrics = [for category in vnetMetricCategories: { + category: category + enabled: true + retentionPolicy: { + enabled: true + days: 0 + } +}] + +//******************************************** +// Resources +//******************************************** + +// Virtual Network +resource vnet 'Microsoft.Network/virtualNetworks@2024-03-01' = { + name: virtualNetworkName + location: location + tags: tags + properties: { + addressSpace: { + addressPrefixes: [ + virtualNetworkAddressPrefixes + ] + } + subnets: [ + { + name: webAppSubnetName + properties: { + addressPrefix: webAppSubnetAddressPrefix + privateEndpointNetworkPolicies: 'Disabled' + privateLinkServiceNetworkPolicies: 'Disabled' + networkSecurityGroup: { + id: webAppSubnetNsg.id + } + natGateway: { + id: natGateway.id + } + delegations: [ + { + name: 'delegation' + properties: { + serviceName: delegationServiceName + } + } + ] + } + } + { + name: peSubnetName + properties: { + addressPrefix: peSubnetAddressPrefix + networkSecurityGroup: { + id: peSubnetNsg.id + } + privateEndpointNetworkPolicies: 'Disabled' + privateLinkServiceNetworkPolicies: 'Disabled' + natGateway: { + id: natGateway.id + } + } + } + ] + } +} + +resource webAppSubnetNsg 'Microsoft.Network/networkSecurityGroups@2025-05-01' = { + name: webAppSubnetNsgName + location: location + tags: tags + properties: { + securityRules: [ + ] + } +} + +resource peSubnetNsg 'Microsoft.Network/networkSecurityGroups@2025-05-01' = { + name: peSubnetNsgName + location: location + tags: tags + properties: { + securityRules: [ + ] + } +} + +// NAT Gateway +resource natGatewayPublicIpPrefix 'Microsoft.Network/publicIPPrefixes@2025-05-01' = { + name: natGatewayPublicIpPrefixName + location: location + sku: { + name: 'Standard' + } + zones: !empty(natGatewayZones) ? natGatewayZones : [] + properties: { + publicIPAddressVersion: 'IPv4' + prefixLength: natGatewayPublicIpPrefixLength + } +} + +resource natGateway 'Microsoft.Network/natGateways@2025-05-01' = { + name: natGatewayName + location: location + sku: { + name: 'Standard' + } + zones: !empty(natGatewayZones) ? natGatewayZones : [] + properties: { + publicIpPrefixes: [ + { + id: natGatewayPublicIpPrefix.id + } + ] + idleTimeoutInMinutes: natGatewayIdleTimeoutMins + } +} + +resource peSubnetNsgDiagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (!empty(workspaceId)) { + name: diagnosticSettingsName + scope: peSubnetNsg + properties: { + workspaceId: workspaceId + logs: nsgLogs + } +} + +resource webAppSubnetNsgDiagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (!empty(workspaceId)) { + name: diagnosticSettingsName + scope: webAppSubnetNsg + properties: { + workspaceId: workspaceId + logs: nsgLogs + } +} + +resource vnetDiagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (!empty(workspaceId)) { + name: diagnosticSettingsName + scope: vnet + properties: { + workspaceId: workspaceId + logs: vnetLogs + metrics: vnetMetrics + } +} + +//******************************************** +// Outputs +//******************************************** +output virtualNetworkId string = vnet.id +output virtualNetworkName string = vnet.name +output webAppSubnetId string = resourceId('Microsoft.Network/virtualNetworks/subnets', vnet.name, webAppSubnetName) +output webAppSubnetName string = webAppSubnetName +output peSubnetId string = resourceId('Microsoft.Network/virtualNetworks/subnets', vnet.name, peSubnetName) +output peSubnetName string = peSubnetName diff --git a/samples/web-app-custom-image/python/bicep/modules/web-app.bicep b/samples/web-app-custom-image/python/bicep/modules/web-app.bicep new file mode 100644 index 0000000..a51c307 --- /dev/null +++ b/samples/web-app-custom-image/python/bicep/modules/web-app.bicep @@ -0,0 +1,210 @@ +//******************************************** +// Parameters +//******************************************** + +@description('Specifies a globally unique name the Azure Web App.') +param name string + +@description('Specifies the location.') +param location string = resourceGroup().location + +@description('Specifies the kind of the hosting plan.') +@allowed([ + 'app' // Windows Web app + 'app,linux' // Linux Web app + 'app,linux,container' // Linux Container Web app + 'hyperV' // Windows Container Web App + 'app,container,windows' // Windows Container Web App + 'app,linux,kubernetes' // Linux Web App on ARC + 'app,linux,container,kubernetes' // Linux Container Web App on ARC + 'functionapp' // Function Code App + 'functionapp,linux' // Linux Consumption Function app + 'functionapp,linux,container,kubernetes' // Function Container App on ARC + 'functionapp,linux,kubernetes' // Function Code App on ARC +]) +param kind string = 'app,linux' + +@description('Specifies the minimum TLS version for the Azure Web App.') +@allowed([ + '1.2' + '1.3' +]) +param minTlsVersion string = '1.2' + +@description('Specifies whether the public network access is enabled or disabled') +@allowed([ + 'Enabled' + 'Disabled' +]) +param publicNetworkAccess string = 'Enabled' + +@description('Specifies whether HTTPS is enforced for the Azure Web App.') +param httpsOnly bool = true + +@description('Specifies the name of the hosting plan.') +param hostingPlanName string + +@description('Specifies the name of the virtual network.') +param virtualNetworkName string + +@description('Specifies the name of the subnet used by Azure Functions for the regional virtual network integration.') +param subnetName string + +@description('Specifies the resource id of the Log Analytics workspace.') +param workspaceId string + +@description('Specifies the login server of the Azure Container Registry.') +param loginServer string + +@description('Specifies the name of the image to be used for the Web App.') +param imageName string + +@description('Specifies the tag of the image to be used for the Web App.') +param imageTag string + +@description('Specifies the type of the managed identity to be used by the Web App.') +@allowed([ + 'SystemAssigned' + 'UserAssigned' +]) +param managedIdentityType string = 'SystemAssigned' + +@description('Specifies the name of the managed identity to be used by the Web App if user assigned identity is selected.') +param managedIdentityName string = '' + +@description('Specifies the optional Git Repo URL.') +param repoUrl string = ' ' + +@description('Specifies the resource tags.') +param tags object + +//******************************************** +// Variables +//******************************************** + +// Generates a unique container name for deployments. +var diagnosticSettingsName = 'default' +var logCategories = [ + 'AppServiceHTTPLogs' + 'AppServiceConsoleLogs' + 'AppServiceAppLogs' + 'AppServiceAuditLogs' + 'AppServiceIPSecAuditLogs' + 'AppServicePlatformLogs' + 'AppServiceAuthenticationLogs' +] +var metricCategories = [ + 'AllMetrics' +] +var logs = [ + for category in logCategories: { + category: category + enabled: true + retentionPolicy: { + enabled: true + days: 0 + } + } +] +var metrics = [ + for category in metricCategories: { + category: category + enabled: true + retentionPolicy: { + enabled: true + days: 0 + } + } +] + +//******************************************** +// Resources +//******************************************** + +resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2025-01-31-preview' existing = { + name: managedIdentityName +} + +resource virtualNetwork 'Microsoft.Network/virtualNetworks@2024-05-01' existing = { + name: virtualNetworkName +} + +resource subnet 'Microsoft.Network/virtualNetworks/subnets@2024-05-01' existing = { + parent: virtualNetwork + name: subnetName +} + +resource hostingPlan 'Microsoft.Web/serverfarms@2024-04-01' existing = { + name: hostingPlanName +} + +resource webApp 'Microsoft.Web/sites@2025-03-01' = { + name: name + location: location + tags: tags + kind: kind + properties: { + httpsOnly: httpsOnly + serverFarmId: hostingPlan.id + virtualNetworkSubnetId: subnet.id + outboundVnetRouting: { + allTraffic: true + applicationTraffic: true + contentShareTraffic: true + imagePullTraffic: true + backupRestoreTraffic: true + } + siteConfig: { + acrUseManagedIdentityCreds: true + acrUserManagedIdentityID: managedIdentity.properties.clientId + linuxFxVersion: 'DOCKER|${loginServer}/${imageName}:${imageTag}' + minTlsVersion: minTlsVersion + publicNetworkAccess: publicNetworkAccess + } + } + identity: { + type: managedIdentityType + userAssignedIdentities : managedIdentityType == 'SystemAssigned' ? null : { + '${managedIdentity.id}': {} + } + } +} + +resource configAppSettings 'Microsoft.Web/sites/config@2024-11-01' = { + parent: webApp + name: 'appsettings' + properties: { + SCM_DO_BUILD_DURING_DEPLOYMENT: 'true' + ENABLE_ORYX_BUILD: 'true' + WEBSITES_PORT: '80' + APP_NAME: 'Custom Image' + IMAGE_NAME: '${loginServer}/${imageName}:${imageTag}' + } +} + +resource webAppSourceControl 'Microsoft.Web/sites/sourcecontrols@2024-11-01' = if (contains(repoUrl,'http')){ + name: 'web' + parent: webApp + properties: { + repoUrl: repoUrl + branch: 'master' + isManualIntegration: true + } +} + +resource diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if(!empty(workspaceId)) { + name: diagnosticSettingsName + scope: webApp + properties: { + workspaceId: workspaceId + logs: logs + metrics: metrics + } +} + +//******************************************** +// Outputs +//******************************************** +output id string = webApp.id +output name string = webApp.name +output defaultHostName string = webApp.properties.defaultHostName diff --git a/samples/web-app-custom-image/python/images/architecture.png b/samples/web-app-custom-image/python/images/architecture.png new file mode 100644 index 0000000..6ca0fe3 Binary files /dev/null and b/samples/web-app-custom-image/python/images/architecture.png differ diff --git a/samples/web-app-custom-image/python/scripts/README.md b/samples/web-app-custom-image/python/scripts/README.md index ca16963..9d2eda8 100644 --- a/samples/web-app-custom-image/python/scripts/README.md +++ b/samples/web-app-custom-image/python/scripts/README.md @@ -1,33 +1,250 @@ -# Web App Custom Image Scripts +# Azure CLI Deployment -These scripts deploy and validate a Python Flask application running on Azure Web App for Containers. +This directory contains the Azure CLI scripts for provisioning Azure services in LocalStack for Azure. For further details about the sample application, refer to the [Azure Web App with Custom Docker Image](../README.md). -## Deploy +## Prerequisites + +Before deploying this solution, ensure you have the following tools installed: + +- [LocalStack for Azure](https://docs.localstack.cloud/azure/): Local Azure cloud emulator for development and testing +- [Docker](https://docs.docker.com/get-docker/): Container runtime required for LocalStack and building the custom image +- [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli): Azure command-line interface +- [Azlocal CLI](https://azure.localstack.cloud/user-guides/sdks/az/): LocalStack Azure CLI wrapper +- [jq](https://jqlang.org/): JSON processor for scripting and parsing command outputs + +### Installing azlocal CLI + +The [deploy.sh](deploy.sh) Bash script uses the `azlocal` CLI instead of the standard Azure CLI to work with LocalStack. Install it using: + +```bash +pip install azlocal +``` + +For more information, see [Get started with the az tool on LocalStack](https://azure.localstack.cloud/user-guides/sdks/az/). + +## Architecture Overview + +The [deploy.sh](deploy.sh) script creates all Azure resources from scratch using the Azure CLI: + +1. [Azure Resource Group](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/manage-resource-groups-cli): A logical container scoping all resources in this sample. +2. [Azure Virtual Network](https://learn.microsoft.com/azure/virtual-network/virtual-networks-overview): Hosts two subnets: + - *app-subnet*: Dedicated to [regional VNet integration](https://learn.microsoft.com/azure/azure-functions/functions-networking-options?tabs=azure-portal#outbound-networking-features) with the Web App. + - *pe-subnet*: Used for hosting Azure Private Endpoints. +3. [Azure Private DNS Zone](https://learn.microsoft.com/azure/dns/private-dns-privatednszone): Handles DNS resolution for the Azure Container Registry Private Endpoint within the virtual network. +4. [Azure Private Endpoint](https://learn.microsoft.com/azure/private-link/private-endpoint-overview): Secures network access to the Azure Container Registry via a private IP within the VNet. +5. [Azure NAT Gateway](https://learn.microsoft.com/azure/nat-gateway/nat-overview): Provides deterministic outbound connectivity for the Web App. Included for completeness; the sample app does not call any external services. +6. [Azure Network Security Group](https://learn.microsoft.com/en-us/azure/virtual-network/network-security-groups-overview): Enforces inbound and outbound traffic rules across the virtual network's subnets. +7. [Azure Log Analytics Workspace](https://learn.microsoft.com/azure/azure-monitor/logs/log-analytics-overview): Centralizes diagnostic logs and metrics from all resources in the solution. +8. [Azure Container Registry](https://learn.microsoft.com/azure/container-registry/container-registry-intro): A fully-managed container registry service based on the open-source [Docker platform](https://docs.docker.com/get-started/docker-overview/) used to hold the container image used by the web app. +9. [User-Assigned Managed Identity](https://learn.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview): Assigned the [AcrPull](https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles/containers#acrpull) role on the Azure Container Registry, enabling the Web App to pull the container image without storing credentials. +10. [Azure App Service Plan](https://learn.microsoft.com/en-us/azure/app-service/overview-hosting-plans): The underlying compute tier that hosts the web application. +11. [Azure Web App](https://learn.microsoft.com/en-us/azure/app-service/overview): Runs the Python Flask application from the custom container image stored in the Azure Container Registry. + +## Provisioning Scripts + +See [deploy.sh](deploy.sh) for the complete deployment automation. The script performs: + +- Creates resource group +- Deploys Azure Container Registry +- Builds container image locally and pushes it to ACR +- Deploys remaining Azure resources (VNet, NSG, NAT Gateway, DNS, Private Endpoint, App Service Plan, managed identity, Web App) +- Configures Web App to use the container image from ACR +- Assigns [AcrPull](https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles/containers#acrpull) role to the user-assigned managed identity + +## Deployment + +You can set up the Azure emulator by utilizing the LocalStack for Azure Docker image. Before starting, ensure you have a valid `LOCALSTACK_AUTH_TOKEN` to access the Azure emulator. Refer to the [Auth Token guide](https://docs.localstack.cloud/getting-started/auth-token/?__hstc=108988063.8aad2b1a7229945859f4d9b9bb71e05d.1743148429561.1758793541854.1758810151462.32&__hssc=108988063.3.1758810151462&__hsfp=3945774529) to obtain your Auth Token and specify it in the `LOCALSTACK_AUTH_TOKEN` environment variable. The Azure Docker image is available on the [LocalStack Docker Hub](https://hub.docker.com/r/localstack/localstack-azure-alpha). To pull the Azure Docker image, execute the following command: + +```bash +docker pull localstack/localstack-azure-alpha +``` + +Start the LocalStack Azure emulator using the localstack CLI, execute the following command: + +```bash +# Set the authentication token +export LOCALSTACK_AUTH_TOKEN= + +# Start the LocalStack Azure emulator +IMAGE_NAME=localstack/localstack-azure-alpha localstack start -d +localstack wait -t 60 + +# Route all Azure CLI calls to the LocalStack Azure emulator +azlocal start-interception +``` + +Navigate to the `scripts` folder: ```bash -bash scripts/deploy.sh +cd samples/web-app-custom-image/python/scripts ``` -The deployment script creates: +Make the script executable: -- Resource group -- Azure Container Registry with admin credentials enabled -- Custom Docker image built from `src/Dockerfile` -- Linux App Service Plan -- Web App configured to use the custom image +```bash +chmod +x deploy.sh +``` -If pushing to the emulated registry is unavailable in the current LocalStack environment, the script falls back to the local Docker image tag. +Run the deployment script: -## Validate +```bash +./deploy.sh +``` + +## Validation + +Once the deployment completes, run the [validate.sh](validate.sh) script to confirm that all resources were provisioned and configured as expected: ```bash -bash scripts/validate.sh +#!/bin/bash +set -euo pipefail + +PREFIX='local' +SUFFIX='test' +RESOURCE_GROUP_NAME="${PREFIX}-rg" +ACR_NAME="${PREFIX}acr${SUFFIX}" +MANAGED_IDENTITY_NAME="${PREFIX}-identity-${SUFFIX}" +APP_SERVICE_PLAN_NAME="${PREFIX}-app-service-plan-${SUFFIX}" +WEB_APP_NAME="${PREFIX}-webapp-${SUFFIX}" +VIRTUAL_NETWORK_NAME="${PREFIX}-vnet-${SUFFIX}" +PRIVATE_DNS_ZONE_NAME="privatelink.azurecr.io" +PRIVATE_ENDPOINT_NAME="${PREFIX}-acr-pe-${SUFFIX}" +WEB_APP_SUBNET_NSG_NAME="${PREFIX}-webapp-subnet-nsg-${SUFFIX}" +PE_SUBNET_NSG_NAME="${PREFIX}-pe-subnet-nsg-${SUFFIX}" +NAT_GATEWAY_NAME="${PREFIX}-nat-gateway-${SUFFIX}" +PIP_PREFIX_NAME="${PREFIX}-nat-gateway-pip-prefix-${SUFFIX}" +LOG_ANALYTICS_NAME="${PREFIX}-log-analytics-${SUFFIX}" + +# Check resource group +echo -e "[$RESOURCE_GROUP_NAME] resource group:\n" +az group show \ + --name "$RESOURCE_GROUP_NAME" \ + --output table + +# Check managed identity +echo -e "[$MANAGED_IDENTITY_NAME] managed identity:\n" +az identity show \ + --name "$MANAGED_IDENTITY_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --output table + +# Check App Service Plan +echo -e "\n[$APP_SERVICE_PLAN_NAME] App Service Plan:\n" +az appservice plan show \ + --name "$APP_SERVICE_PLAN_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --output table + +# Check Azure Container Registry +echo -e "\n[$ACR_NAME] Azure Container Registry:\n" +az acr show \ + --name "$ACR_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --output table + +# Check Azure Web App +echo -e "\n[$WEB_APP_NAME] Web App:\n" +az webapp show \ + --name "$WEB_APP_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --query "{name:name, state:state, defaultHostName:defaultHostName, kind:kind}" \ + --output table + +# Check App Settings +echo -e "\n[$WEB_APP_NAME] app settings:\n" +az webapp config appsettings list \ + --name "$WEB_APP_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --query "[?name=='IMAGE_NAME' || name=='APP_NAME' || name=='WEBSITES_PORT']" \ + --output table + +# Check Virtual Network +echo -e "\n[$VIRTUAL_NETWORK_NAME] virtual network:\n" +az network vnet show \ + --name "$VIRTUAL_NETWORK_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --output table \ + --only-show-errors + +# Check Private DNS Zone +echo -e "\n[$PRIVATE_DNS_ZONE_NAME] private dns zone:\n" +az network private-dns zone show \ + --name "$PRIVATE_DNS_ZONE_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --query '{Name:name,ResourceGroup:resourceGroup,RecordSets:recordSets,VirtualNetworkLinks:virtualNetworkLinks}' \ + --output table \ + --only-show-errors + +# Check Private Endpoint +echo -e "\n[$PRIVATE_ENDPOINT_NAME] private endpoint:\n" +az network private-endpoint show \ + --name "$PRIVATE_ENDPOINT_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --output table \ + --only-show-errors + +# Check Web App Subnet NSG +echo -e "\n[$WEB_APP_SUBNET_NSG_NAME] network security group:\n" +az network nsg show \ + --name "$WEB_APP_SUBNET_NSG_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --output table \ + --only-show-errors + +# Check Private Endpoint Subnet NSG +echo -e "\n[$PE_SUBNET_NSG_NAME] network security group:\n" +az network nsg show \ + --name "$PE_SUBNET_NSG_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --output table \ + --only-show-errors + +# Check NAT Gateway +echo -e "\n[$NAT_GATEWAY_NAME] nat gateway:\n" +az network nat gateway show \ + --name "$NAT_GATEWAY_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --output table \ + --only-show-errors + +# Check Public IP Prefix +echo -e "\n[$PIP_PREFIX_NAME] public ip prefix:\n" +az network public-ip prefix show \ + --name "$PIP_PREFIX_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --output table \ + --only-show-errors + +# Check Log Analytics Workspace +echo -e "\n[$LOG_ANALYTICS_NAME] log analytics workspace:\n" +az monitor log-analytics workspace show \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --workspace-name "$LOG_ANALYTICS_NAME" \ + --query '{Name:name,Location:location,ResourceGroup:resourceGroup}' \ + --output table \ + --only-show-errors + +echo -e "\nResources in [$RESOURCE_GROUP_NAME]:\n" +az resource list \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --output table ``` -## Call The Web App +## Cleanup + +To destroy all created resources: ```bash -bash scripts/call-web-app.sh +# Delete resource group and all contained resources +az group delete --name local-rg --yes --no-wait + +# Verify deletion +az group list --output table ``` -The call script first uses the LocalStack proxy endpoint and then, when available, calls the Docker host port mapped to the emulated Web App container. +This will remove all Azure resources created by the Azure CLI deployment script. + +## Related Documentation + +- [Azure CLI Documentation](https://learn.microsoft.com/en-us/cli/azure/) +- [LocalStack for Azure Documentation](https://docs.localstack.cloud/azure/) diff --git a/samples/web-app-custom-image/python/scripts/call-web-app.sh b/samples/web-app-custom-image/python/scripts/call-web-app.sh index 7747690..bff883f 100755 --- a/samples/web-app-custom-image/python/scripts/call-web-app.sh +++ b/samples/web-app-custom-image/python/scripts/call-web-app.sh @@ -3,8 +3,8 @@ set -euo pipefail PREFIX='local' SUFFIX='test' -RESOURCE_GROUP_NAME="${PREFIX}-custom-image-rg" -WEB_APP_NAME="${PREFIX}-custom-image-webapp-${SUFFIX}" +RESOURCE_GROUP_NAME="${PREFIX}-rg" +WEB_APP_NAME="${PREFIX}-webapp-${SUFFIX}" get_docker_container_name_by_prefix() { local app_prefix="$1" diff --git a/samples/web-app-custom-image/python/scripts/deploy.sh b/samples/web-app-custom-image/python/scripts/deploy.sh index 9e16746..fbd7ada 100755 --- a/samples/web-app-custom-image/python/scripts/deploy.sh +++ b/samples/web-app-custom-image/python/scripts/deploy.sh @@ -1,80 +1,1012 @@ #!/bin/bash -set -euo pipefail # Variables PREFIX='local' SUFFIX='test' LOCATION='westeurope' -RESOURCE_GROUP_NAME="${PREFIX}-custom-image-rg" -ACR_NAME="${PREFIX}customimageacr" -APP_SERVICE_PLAN_NAME="${PREFIX}-custom-image-plan-${SUFFIX}" -APP_SERVICE_PLAN_SKU="B1" -WEB_APP_NAME="${PREFIX}-custom-image-webapp-${SUFFIX}" +RESOURCE_GROUP_NAME="${PREFIX}-rg" +ACR_NAME="${PREFIX}acr${SUFFIX}" +ACR_SKU='Premium' +MANAGED_IDENTITY_NAME="${PREFIX}-identity-${SUFFIX}" +APP_SERVICE_PLAN_NAME="${PREFIX}-app-service-plan-${SUFFIX}" +APP_SERVICE_PLAN_SKU="S1" +WEB_APP_NAME="${PREFIX}-webapp-${SUFFIX}" IMAGE_NAME="custom-image-webapp" IMAGE_TAG="v1" +LOCAL_IMAGE="${IMAGE_NAME}:${IMAGE_TAG}" +VIRTUAL_NETWORK_NAME="${PREFIX}-vnet-${SUFFIX}" +VIRTUAL_NETWORK_ADDRESS_PREFIX="10.0.0.0/8" +WEB_APP_SUBNET_NAME="app-subnet" +WEB_APP_SUBNET_PREFIX="10.0.0.0/24" +WEB_APP_SUBNET_NSG_NAME="${PREFIX}-webapp-subnet-nsg-${SUFFIX}" +PE_SUBNET_NAME="pe-subnet" +PE_SUBNET_PREFIX="10.0.1.0/24" +PE_SUBNET_NSG_NAME="${PREFIX}-pe-subnet-nsg-${SUFFIX}" +VIRTUAL_NETWORK_LINK_NAME="link-to-vnet" +PRIVATE_DNS_ZONE_NAME="privatelink.azurecr.io" +PRIVATE_ENDPOINT_NAME="${PREFIX}-acr-pe-${SUFFIX}" +PRIVATE_ENDPOINT_GROUP="registry" +PRIVATE_DNS_ZONE_GROUP_NAME="default" +NAT_GATEWAY_NAME="${PREFIX}-nat-gateway-${SUFFIX}" +PIP_PREFIX_NAME="${PREFIX}-nat-gateway-pip-prefix-${SUFFIX}" +LOG_ANALYTICS_NAME="${PREFIX}-log-analytics-${SUFFIX}" +DIAGNOSTIC_SETTINGS_NAME='default' CURRENT_DIR="$(cd "$(dirname "$0")" && pwd)" +RETRY_COUNT=3 +SLEEP=5 cd "$CURRENT_DIR" || exit +# Create a resource group echo "Creating resource group [$RESOURCE_GROUP_NAME]..." az group create \ --name "$RESOURCE_GROUP_NAME" \ - --location "$LOCATION" + --location "$LOCATION" \ + --only-show-errors 1>/dev/null + +if [ $? -eq 0 ]; then + echo "Resource group [$RESOURCE_GROUP_NAME] created successfully." +else + echo "Failed to create resource group [$RESOURCE_GROUP_NAME]." + exit 1 +fi -echo "Creating Azure Container Registry [$ACR_NAME]..." -az acr create \ +# Check if the Azure Container Registry already exists +echo "Checking if [$ACR_NAME] Azure Container Registry already exists in the [$RESOURCE_GROUP_NAME] resource group..." +az acr show \ --name "$ACR_NAME" \ --resource-group "$RESOURCE_GROUP_NAME" \ - --location "$LOCATION" \ - --sku Basic \ - --admin-enabled true + --only-show-errors &>/dev/null + +if [[ $? != 0 ]]; then + echo "No [$ACR_NAME] Azure Container Registry exists in the [$RESOURCE_GROUP_NAME] resource group" + echo "Creating Azure Container Registry [$ACR_NAME]..." + az acr create \ + --name "$ACR_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --location "$LOCATION" \ + --sku "$ACR_SKU" \ + --admin-enabled true \ + --only-show-errors 1>/dev/null + + if [ $? -eq 0 ]; then + echo "Azure Container Registry [$ACR_NAME] created successfully." + else + echo "Failed to create Azure Container Registry [$ACR_NAME]." + exit 1 + fi +else + echo "[$ACR_NAME] Azure Container Registry already exists in the [$RESOURCE_GROUP_NAME] resource group" +fi + +# Get the Azure Container Registry resource id +echo "Getting [$ACR_NAME] Azure Container Registry resource id in the [$RESOURCE_GROUP_NAME] resource group..." +ACR_ID=$(az acr show \ + --name "$ACR_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --query id \ + --output tsv \ + --only-show-errors) + +if [[ -n $ACR_ID ]]; then + echo "[$ACR_NAME] Azure Container Registry resource id retrieved successfully: $ACR_ID" +else + echo "Failed to retrieve [$ACR_NAME] Azure Container Registry resource id in the [$RESOURCE_GROUP_NAME] resource group" + exit 1 +fi + +echo "Logging into Azure Container Registry [$ACR_NAME]..." +az acr login --name "$ACR_NAME" --only-show-errors -az acr login --name $ACR_NAME +if [ $? -eq 0 ]; then + echo "Logged into Azure Container Registry [$ACR_NAME] successfully." +else + echo "Failed to log into Azure Container Registry [$ACR_NAME]." + exit 1 +fi -LOGIN_SERVER=$(az acr show \ +echo "Getting login server for Azure Container Registry [$ACR_NAME]..." +ACR_LOGIN_SERVER=$(az acr show \ --name "$ACR_NAME" \ --resource-group "$RESOURCE_GROUP_NAME" \ --query "loginServer" \ --output tsv \ --only-show-errors) -FULL_IMAGE="${LOGIN_SERVER}/${IMAGE_NAME}:${IMAGE_TAG}" -LOCAL_IMAGE="${IMAGE_NAME}:${IMAGE_TAG}" +if [ -n "$ACR_LOGIN_SERVER" ]; then + echo "Login server retrieved successfully: $ACR_LOGIN_SERVER" +else + echo "Failed to retrieve login server for Azure Container Registry [$ACR_NAME]." + exit 1 +fi + +# Create full image name with login server, image name, and tag +FULL_IMAGE="${ACR_LOGIN_SERVER}/${IMAGE_NAME}:${IMAGE_TAG}" echo "Building custom Docker image [$LOCAL_IMAGE]..." docker build -t "$LOCAL_IMAGE" ../src/ + +if [ $? -eq 0 ]; then + echo "Docker image [$LOCAL_IMAGE] built successfully." +else + echo "Failed to build Docker image [$LOCAL_IMAGE]." + exit 1 +fi + +echo "Tagging Docker image [$LOCAL_IMAGE] as [$FULL_IMAGE]..." docker tag "$LOCAL_IMAGE" "$FULL_IMAGE" +if [ $? -eq 0 ]; then + echo "Docker image [$LOCAL_IMAGE] tagged as [$FULL_IMAGE] successfully." +else + echo "Failed to tag Docker image [$LOCAL_IMAGE] as [$FULL_IMAGE]." + exit 1 +fi + echo "Pushing image [$FULL_IMAGE] to ACR..." docker push "$FULL_IMAGE" -WEBAPP_IMAGE="$FULL_IMAGE" +if [ $? -eq 0 ]; then + echo "Docker image [$FULL_IMAGE] pushed to ACR successfully." +else + echo "Failed to push Docker image [$FULL_IMAGE] to ACR." + exit 1 +fi + +# Check if the user-assigned managed identity already exists +echo "Checking if [$MANAGED_IDENTITY_NAME] user-assigned managed identity actually exists in the [$RESOURCE_GROUP_NAME] resource group..." + +az identity show \ + --name "$MANAGED_IDENTITY_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" &>/dev/null + +if [[ $? != 0 ]]; then + echo "No [$MANAGED_IDENTITY_NAME] user-assigned managed identity actually exists in the [$RESOURCE_GROUP_NAME] resource group" + echo "Creating [$MANAGED_IDENTITY_NAME] user-assigned managed identity in the [$RESOURCE_GROUP_NAME] resource group..." + + # Create the user-assigned managed identity + az identity create \ + --name "$MANAGED_IDENTITY_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --location "$LOCATION" 1>/dev/null + + if [[ $? == 0 ]]; then + echo "[$MANAGED_IDENTITY_NAME] user-assigned managed identity successfully created in the [$RESOURCE_GROUP_NAME] resource group" + else + echo "Failed to create [$MANAGED_IDENTITY_NAME] user-assigned managed identity in the [$RESOURCE_GROUP_NAME] resource group" + exit 1 + fi +else + echo "[$MANAGED_IDENTITY_NAME] user-assigned managed identity already exists in the [$RESOURCE_GROUP_NAME] resource group" +fi + +# Retrieve the principalId of the user-assigned managed identity +echo "Retrieving principalId for [$MANAGED_IDENTITY_NAME] managed identity..." +PRINCIPAL_ID=$(az identity show \ + --name "$MANAGED_IDENTITY_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --query principalId \ + --output tsv) + +if [[ -n $PRINCIPAL_ID ]]; then + echo "[$PRINCIPAL_ID] principalId for the [$MANAGED_IDENTITY_NAME] managed identity successfully retrieved" +else + echo "Failed to retrieve principalId for the [$MANAGED_IDENTITY_NAME] managed identity" + exit 1 +fi + +# Retrieve the clientId of the user-assigned managed identity +echo "Retrieving clientId for [$MANAGED_IDENTITY_NAME] managed identity..." +CLIENT_ID=$(az identity show \ + --name "$MANAGED_IDENTITY_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --query clientId \ + --output tsv) + +if [[ -n $CLIENT_ID ]]; then + echo "[$CLIENT_ID] clientId for the [$MANAGED_IDENTITY_NAME] managed identity successfully retrieved" +else + echo "Failed to retrieve clientId for the [$MANAGED_IDENTITY_NAME] managed identity" + exit 1 +fi + +# Retrieve the resource id of the user-assigned managed identity +echo "Retrieving resource id for the [$MANAGED_IDENTITY_NAME] managed identity..." +IDENTITY_ID=$(az identity show \ + --name "$MANAGED_IDENTITY_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --query id \ + --output tsv) + +if [[ -n $IDENTITY_ID ]]; then + echo "Resource id for the [$MANAGED_IDENTITY_NAME] managed identity successfully retrieved" +else + echo "Failed to retrieve the resource id for the [$MANAGED_IDENTITY_NAME] managed identity" + exit 1 +fi + +# Assign the AcrPull role to the managed identity with the Azure Container Registry as scope +ROLE="AcrPull" +echo "Checking if the [$MANAGED_IDENTITY_NAME] managed identity has the [$ROLE] role assignment on Azure Container Registry [$ACR_NAME]..." +current=$(az role assignment list \ + --assignee "$PRINCIPAL_ID" \ + --scope "$ACR_ID" \ + --query "[?roleDefinitionName=='$ROLE'].roleDefinitionName" \ + --output tsv 2>/dev/null) + +if [[ $current == "$ROLE" ]]; then + echo "Managed identity [$MANAGED_IDENTITY_NAME] already has the [$ROLE] role assignment on Azure Container Registry [$ACR_NAME]" +else + echo "Managed identity [$MANAGED_IDENTITY_NAME] does not have the [$ROLE] role assignment on Azure Container Registry [$ACR_NAME]" + echo "Creating role assignment: assigning [$ROLE] role to managed identity [$MANAGED_IDENTITY_NAME] on Azure Container Registry [$ACR_NAME]..." + ATTEMPT=1 + while [ $ATTEMPT -le $RETRY_COUNT ]; do + echo "Attempt $ATTEMPT of $RETRY_COUNT to assign role..." + az role assignment create \ + --assignee "$PRINCIPAL_ID" \ + --role "$ROLE" \ + --scope "$ACR_ID" 1>/dev/null + + if [[ $? == 0 ]]; then + break + else + if [ $ATTEMPT -lt $RETRY_COUNT ]; then + echo "Role assignment failed. Waiting [$SLEEP] seconds before retry..." + sleep $SLEEP + fi + ATTEMPT=$((ATTEMPT + 1)) + fi + done + + if [[ $? == 0 ]]; then + echo "Successfully assigned [$ROLE] role to managed identity [$MANAGED_IDENTITY_NAME] on Azure Container Registry [$ACR_NAME]" + else + echo "Failed to assign [$ROLE] role to managed identity [$MANAGED_IDENTITY_NAME] on Azure Container Registry [$ACR_NAME]" + exit 1 + fi +fi + +# Check if the network security group for the web app subnet already exists +echo "Checking if [$WEB_APP_SUBNET_NSG_NAME] network security group for the web app subnet actually exists in the [$RESOURCE_GROUP_NAME] resource group..." +az network nsg show \ + --name "$WEB_APP_SUBNET_NSG_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --only-show-errors &>/dev/null + +if [[ $? != 0 ]]; then + echo "No [$WEB_APP_SUBNET_NSG_NAME] network security group for the web app subnet actually exists in the [$RESOURCE_GROUP_NAME] resource group" + echo "Creating [$WEB_APP_SUBNET_NSG_NAME] network security group for the web app subnet..." + + # Create the network security group for the web app subnet + az network nsg create \ + --name "$WEB_APP_SUBNET_NSG_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --location "$LOCATION" \ + --only-show-errors 1>/dev/null -echo "Creating Linux App Service Plan [$APP_SERVICE_PLAN_NAME]..." -az appservice plan create \ + if [[ $? == 0 ]]; then + echo "[$WEB_APP_SUBNET_NSG_NAME] network security group for the web app subnet successfully created in the [$RESOURCE_GROUP_NAME] resource group" + else + echo "Failed to create [$WEB_APP_SUBNET_NSG_NAME] network security group for the web app subnet in the [$RESOURCE_GROUP_NAME] resource group" + exit 1 + fi +else + echo "[$WEB_APP_SUBNET_NSG_NAME] network security group for the web app subnet already exists in the [$RESOURCE_GROUP_NAME] resource group" +fi + +# Get the resource id of the network security group for the web app subnet +echo "Getting [$WEB_APP_SUBNET_NSG_NAME] network security group for the web app subnet resource id in the [$RESOURCE_GROUP_NAME] resource group..." +WEB_APP_SUBNET_NSG_ID=$(az network nsg show \ + --name "$WEB_APP_SUBNET_NSG_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --query id \ + --output tsv \ + --only-show-errors) + +if [[ -n $WEB_APP_SUBNET_NSG_ID ]]; then + echo "[$WEB_APP_SUBNET_NSG_NAME] network security group for the web app subnet resource id retrieved successfully: $WEB_APP_SUBNET_NSG_ID" +else + echo "Failed to retrieve [$WEB_APP_SUBNET_NSG_NAME] network security group for the web app subnet resource id in the [$RESOURCE_GROUP_NAME] resource group" + exit 1 +fi + +# Check if the network security group for the private endpoint subnet already exists +echo "Checking if [$PE_SUBNET_NSG_NAME] network security group for the private endpoint subnet actually exists in the [$RESOURCE_GROUP_NAME] resource group..." +az network nsg show \ + --name "$PE_SUBNET_NSG_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --only-show-errors &>/dev/null + +if [[ $? != 0 ]]; then + echo "No [$PE_SUBNET_NSG_NAME] network security group for the private endpoint subnet actually exists in the [$RESOURCE_GROUP_NAME] resource group" + echo "Creating [$PE_SUBNET_NSG_NAME] network security group for the private endpoint subnet..." + + # Create the network security group for the private endpoint subnet + az network nsg create \ + --name "$PE_SUBNET_NSG_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --location "$LOCATION" \ + --only-show-errors 1>/dev/null + + if [[ $? == 0 ]]; then + echo "[$PE_SUBNET_NSG_NAME] network security group for the private endpoint subnet successfully created in the [$RESOURCE_GROUP_NAME] resource group" + else + echo "Failed to create [$PE_SUBNET_NSG_NAME] network security group for the private endpoint subnet in the [$RESOURCE_GROUP_NAME] resource group" + exit 1 + fi +else + echo "[$PE_SUBNET_NSG_NAME] network security group for the private endpoint subnet already exists in the [$RESOURCE_GROUP_NAME] resource group" +fi + +# Get the resource id of the network security group for the private endpoint subnet +echo "Getting [$PE_SUBNET_NSG_NAME] network security group for the private endpoint subnet resource id in the [$RESOURCE_GROUP_NAME] resource group..." +PE_SUBNET_NSG_ID=$(az network nsg show \ + --name "$PE_SUBNET_NSG_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --query id \ + --output tsv \ + --only-show-errors) + +if [[ -n $PE_SUBNET_NSG_ID ]]; then + echo "[$PE_SUBNET_NSG_NAME] network security group for the private endpoint subnet resource id retrieved successfully: $PE_SUBNET_NSG_ID" +else + echo "Failed to retrieve [$PE_SUBNET_NSG_NAME] network security group for the private endpoint subnet resource id in the [$RESOURCE_GROUP_NAME] resource group" + exit 1 +fi + +# Check if the public IP prefix for the NAT Gateway already exists +echo "Checking if [$PIP_PREFIX_NAME] public IP prefix for the NAT Gateway actually exists in the [$RESOURCE_GROUP_NAME] resource group..." +az network public-ip prefix show \ + --name "$PIP_PREFIX_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --only-show-errors &>/dev/null + +if [[ $? != 0 ]]; then + echo "No [$PIP_PREFIX_NAME] public IP prefix for the NAT Gateway actually exists in the [$RESOURCE_GROUP_NAME] resource group" + echo "Creating [$PIP_PREFIX_NAME] public IP prefix for the NAT Gateway in the [$RESOURCE_GROUP_NAME] resource group..." + + # Create the public IP prefix for the NAT Gateway + az network public-ip prefix create \ + --name "$PIP_PREFIX_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --location "$LOCATION" \ + --length 31 \ + --only-show-errors 1>/dev/null + + if [[ $? == 0 ]]; then + echo "[$PIP_PREFIX_NAME] public IP prefix for the NAT Gateway successfully created in the [$RESOURCE_GROUP_NAME] resource group" + else + echo "Failed to create [$PIP_PREFIX_NAME] public IP prefix for the NAT Gateway in the [$RESOURCE_GROUP_NAME] resource group" + exit 1 + fi +else + echo "[$PIP_PREFIX_NAME] public IP prefix for the NAT Gateway already exists in the [$RESOURCE_GROUP_NAME] resource group" +fi + +# Check if the NAT Gateway already exists +echo "Checking if [$NAT_GATEWAY_NAME] NAT Gateway actually exists in the [$RESOURCE_GROUP_NAME] resource group..." +az network nat gateway show \ + --name "$NAT_GATEWAY_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --only-show-errors &>/dev/null + +if [[ $? != 0 ]]; then + echo "No [$NAT_GATEWAY_NAME] NAT Gateway actually exists in the [$RESOURCE_GROUP_NAME] resource group" + echo "Creating [$NAT_GATEWAY_NAME] NAT Gateway in the [$RESOURCE_GROUP_NAME] resource group..." + + # Create the NAT Gateway + az network nat gateway create \ + --name "$NAT_GATEWAY_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --location "$LOCATION" \ + --public-ip-prefixes "$PIP_PREFIX_NAME" \ + --idle-timeout 4 \ + --only-show-errors 1>/dev/null + + if [[ $? == 0 ]]; then + echo "[$NAT_GATEWAY_NAME] NAT Gateway successfully created in the [$RESOURCE_GROUP_NAME] resource group" + else + echo "Failed to create [$NAT_GATEWAY_NAME] NAT Gateway in the [$RESOURCE_GROUP_NAME] resource group" + exit 1 + fi +else + echo "[$NAT_GATEWAY_NAME] NAT Gateway already exists in the [$RESOURCE_GROUP_NAME] resource group" +fi + +# Check if the virtual network already exists +echo "Checking if [$VIRTUAL_NETWORK_NAME] virtual network actually exists in the [$RESOURCE_GROUP_NAME] resource group..." +az network vnet show \ + --name "$VIRTUAL_NETWORK_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --only-show-errors &>/dev/null + +if [[ $? != 0 ]]; then + echo "No [$VIRTUAL_NETWORK_NAME] virtual network actually exists in the [$RESOURCE_GROUP_NAME] resource group" + echo "Creating [$VIRTUAL_NETWORK_NAME] virtual network in the [$RESOURCE_GROUP_NAME] resource group..." + + # Create the virtual network + az network vnet create \ + --name "$VIRTUAL_NETWORK_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --location "$LOCATION" \ + --address-prefixes "$VIRTUAL_NETWORK_ADDRESS_PREFIX" \ + --subnet-name "$WEB_APP_SUBNET_NAME" \ + --subnet-prefix "$WEB_APP_SUBNET_PREFIX" \ + --only-show-errors 1>/dev/null + + if [[ $? == 0 ]]; then + echo "[$VIRTUAL_NETWORK_NAME] virtual network successfully created in the [$RESOURCE_GROUP_NAME] resource group" + else + echo "Failed to create [$VIRTUAL_NETWORK_NAME] virtual network in the [$RESOURCE_GROUP_NAME] resource group" + exit 1 + fi + + # Update the web app subnet to associate it with the NAT Gateway and the NSG + echo "Associating [$WEB_APP_SUBNET_NAME] subnet with the [$NAT_GATEWAY_NAME] NAT Gateway and the [$WEB_APP_SUBNET_NSG_NAME] network security group..." + az network vnet subnet update \ + --name "$WEB_APP_SUBNET_NAME" \ + --vnet-name "$VIRTUAL_NETWORK_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --nat-gateway "$NAT_GATEWAY_NAME" \ + --network-security-group "$WEB_APP_SUBNET_NSG_NAME" \ + --only-show-errors 1>/dev/null + + if [[ $? == 0 ]]; then + echo "[$WEB_APP_SUBNET_NAME] subnet successfully associated with the [$NAT_GATEWAY_NAME] NAT Gateway and the [$WEB_APP_SUBNET_NSG_NAME] network security group" + else + echo "Failed to associate [$WEB_APP_SUBNET_NAME] subnet with the [$NAT_GATEWAY_NAME] NAT Gateway and the [$WEB_APP_SUBNET_NSG_NAME] network security group" + exit 1 + fi +else + echo "[$VIRTUAL_NETWORK_NAME] virtual network already exists in the [$RESOURCE_GROUP_NAME] resource group" +fi + +# Check if the subnet already exists +echo "Checking if [$PE_SUBNET_NAME] subnet actually exists in the [$VIRTUAL_NETWORK_NAME] virtual network..." +az network vnet subnet show \ + --name "$PE_SUBNET_NAME" \ + --vnet-name "$VIRTUAL_NETWORK_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --only-show-errors &>/dev/null + +if [[ $? != 0 ]]; then + echo "No [$PE_SUBNET_NAME] subnet actually exists in the [$VIRTUAL_NETWORK_NAME] virtual network" + echo "Creating [$PE_SUBNET_NAME] subnet in the [$VIRTUAL_NETWORK_NAME] virtual network..." + + # Create the subnet + az network vnet subnet create \ + --name "$PE_SUBNET_NAME" \ + --vnet-name "$VIRTUAL_NETWORK_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --address-prefix "$PE_SUBNET_PREFIX" \ + --network-security-group "$PE_SUBNET_NSG_NAME" \ + --private-endpoint-network-policies "Disabled" \ + --private-link-service-network-policies "Disabled" \ + --only-show-errors 1>/dev/null + + if [[ $? == 0 ]]; then + echo "[$PE_SUBNET_NAME] subnet successfully created in the [$VIRTUAL_NETWORK_NAME] virtual network" + else + echo "Failed to create [$PE_SUBNET_NAME] subnet in the [$VIRTUAL_NETWORK_NAME] virtual network" + exit + fi +else + echo "[$PE_SUBNET_NAME] subnet already exists in the [$VIRTUAL_NETWORK_NAME] virtual network" +fi + +# Retrieve the virtual network resource id +echo "Getting [$VIRTUAL_NETWORK_NAME] virtual network resource id in the [$RESOURCE_GROUP_NAME] resource group..." +VIRTUAL_NETWORK_ID=$(az network vnet show \ + --name "$VIRTUAL_NETWORK_NAME" \ --resource-group "$RESOURCE_GROUP_NAME" \ + --query id \ + --output tsv \ + --only-show-errors) + +if [[ -n $VIRTUAL_NETWORK_ID ]]; then + echo "[$VIRTUAL_NETWORK_NAME] virtual network resource id retrieved successfully: $VIRTUAL_NETWORK_ID" +else + echo "Failed to retrieve [$VIRTUAL_NETWORK_NAME] virtual network resource id in the [$RESOURCE_GROUP_NAME] resource group" + exit 1 +fi + +# Check if the private DNS Zone already exists +echo "Checking if [$PRIVATE_DNS_ZONE_NAME] private DNS zone actually exists in the [$RESOURCE_GROUP_NAME] resource group..." +az network private-dns zone show \ + --name "$PRIVATE_DNS_ZONE_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --only-show-errors &>/dev/null + +if [[ $? != 0 ]]; then + echo "No [$PRIVATE_DNS_ZONE_NAME] private DNS zone actually exists in the [$RESOURCE_GROUP_NAME] resource group" + echo "Creating [$PRIVATE_DNS_ZONE_NAME] private DNS zone in the [$RESOURCE_GROUP_NAME] resource group..." + + # Create the private DNS Zone + az network private-dns zone create \ + --name "$PRIVATE_DNS_ZONE_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --only-show-errors 1>/dev/null + + if [[ $? == 0 ]]; then + echo "[$PRIVATE_DNS_ZONE_NAME] private DNS zone successfully created in the [$RESOURCE_GROUP_NAME] resource group" + else + echo "Failed to create [$PRIVATE_DNS_ZONE_NAME] private DNS zone in the [$RESOURCE_GROUP_NAME] resource group" + exit + fi +else + echo "[$PRIVATE_DNS_ZONE_NAME] private DNS zone already exists in the [$RESOURCE_GROUP_NAME] resource group" +fi + +# Check if the virtual network link between the private DNS zone and the virtual network already exists +echo "Checking if [$VIRTUAL_NETWORK_LINK_NAME] virtual network link between [$PRIVATE_DNS_ZONE_NAME] private DNS zone and [$VIRTUAL_NETWORK_NAME] virtual network actually exists..." +az network private-dns link vnet show \ + --name "$VIRTUAL_NETWORK_LINK_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --zone-name "$PRIVATE_DNS_ZONE_NAME" \ + --only-show-errors &>/dev/null + +if [[ $? != 0 ]]; then + echo "No [$VIRTUAL_NETWORK_LINK_NAME] virtual network link between [$PRIVATE_DNS_ZONE_NAME] private DNS zone and [$VIRTUAL_NETWORK_NAME] virtual network actually exists" + + echo "Creating [$VIRTUAL_NETWORK_LINK_NAME] virtual network link between [$PRIVATE_DNS_ZONE_NAME] private DNS zone and [$VIRTUAL_NETWORK_NAME] virtual network..." + + # Create the virtual network link between [$PRIVATE_DNS_ZONE_NAME] private DNS zone and [$VIRTUAL_NETWORK_NAME] virtual network + az network private-dns link vnet create \ + --name "$VIRTUAL_NETWORK_LINK_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --zone-name "$PRIVATE_DNS_ZONE_NAME" \ + --virtual-network "$VIRTUAL_NETWORK_ID" \ + --registration-enabled false \ + --only-show-errors 1>/dev/null + + if [[ $? == 0 ]]; then + echo "[$VIRTUAL_NETWORK_LINK_NAME] virtual network link between [$PRIVATE_DNS_ZONE_NAME] private DNS zone and [$VIRTUAL_NETWORK_NAME] virtual network successfully created" + else + echo "Failed to create [$VIRTUAL_NETWORK_LINK_NAME] virtual network link between [$PRIVATE_DNS_ZONE_NAME] private DNS zone and [$VIRTUAL_NETWORK_NAME] virtual network" + exit + fi +else + echo "[$VIRTUAL_NETWORK_LINK_NAME] virtual network link between [$PRIVATE_DNS_ZONE_NAME] private DNS zone and [$VIRTUAL_NETWORK_NAME] virtual network already exists" +fi + +# Check if the private endpoint already exists +echo "Checking if private endpoint [$PRIVATE_ENDPOINT_NAME] exists in the [$RESOURCE_GROUP_NAME] resource group..." +privateEndpointId=$(az network private-endpoint list \ + --resource-group $RESOURCE_GROUP_NAME \ + --only-show-errors \ + --query "[?name=='$PRIVATE_ENDPOINT_NAME'].id" \ + --output tsv) + +if [[ -z $privateEndpointId ]]; then + echo "Private endpoint [$PRIVATE_ENDPOINT_NAME] does not exist in the [$RESOURCE_GROUP_NAME] resource group" + echo "Creating [$PRIVATE_ENDPOINT_NAME] private endpoint for the [$ACR_NAME] Azure Container Registry in the [$RESOURCE_GROUP_NAME] resource group..." + + # Create a private endpoint for the Azure Container Registry + az network private-endpoint create \ + --name "$PRIVATE_ENDPOINT_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --location "$LOCATION" \ + --vnet-name "$VIRTUAL_NETWORK_NAME" \ + --subnet "$PE_SUBNET_NAME" \ + --private-connection-resource-id "$ACR_ID" \ + --group-id "$PRIVATE_ENDPOINT_GROUP" \ + --connection-name "acr-connection" \ + --only-show-errors 1>/dev/null + + if [[ $? == 0 ]]; then + echo "Private endpoint [$PRIVATE_ENDPOINT_NAME] successfully created for the [$ACR_NAME] Azure Container Registry in the [$RESOURCE_GROUP_NAME] resource group" + else + echo "Failed to create a private endpoint for the [$ACR_NAME] Azure Container Registry in the [$RESOURCE_GROUP_NAME] resource group" + exit + fi +else + echo "Private endpoint [$PRIVATE_ENDPOINT_NAME] already exists in the [$RESOURCE_GROUP_NAME] resource group" +fi + +# Check if the private DNS zone group is already created for the Azure Container Registry private endpoint +echo "Checking if the private DNS zone group [$PRIVATE_DNS_ZONE_GROUP_NAME] for the [$PRIVATE_ENDPOINT_NAME] private endpoint already exists..." +NAME=$(az network private-endpoint dns-zone-group show \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --endpoint-name "$PRIVATE_ENDPOINT_NAME" \ + --name "$PRIVATE_DNS_ZONE_GROUP_NAME" \ + --query name \ + --output tsv \ + --only-show-errors) + +if [[ -z $NAME ]]; then + echo "No private DNS zone group [$PRIVATE_DNS_ZONE_GROUP_NAME] for the [$PRIVATE_ENDPOINT_NAME] private endpoint actually exists" + echo "Creating private DNS zone group [$PRIVATE_DNS_ZONE_GROUP_NAME] for the [$PRIVATE_ENDPOINT_NAME] private endpoint..." + + # Create the private DNS zone group for the Azure Container Registry private endpoint + az network private-endpoint dns-zone-group create \ + --name "$PRIVATE_DNS_ZONE_GROUP_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --endpoint-name "$PRIVATE_ENDPOINT_NAME" \ + --private-dns-zone "$PRIVATE_DNS_ZONE_NAME" \ + --zone-name "$PRIVATE_DNS_ZONE_NAME" \ + --only-show-errors 1>/dev/null + + if [[ $? == 0 ]]; then + echo "Private DNS zone group [$PRIVATE_DNS_ZONE_GROUP_NAME] for the [$PRIVATE_ENDPOINT_NAME] private endpoint successfully created" + else + echo "Failed to create private DNS zone group [$PRIVATE_DNS_ZONE_GROUP_NAME] for the [$PRIVATE_ENDPOINT_NAME] private endpoint" + exit + fi +else + echo "Private DNS zone group [$PRIVATE_DNS_ZONE_GROUP_NAME] for the [$PRIVATE_ENDPOINT_NAME] private endpoint already exists" +fi + +# Check if the App Service Plan already exists +echo "Checking if [$APP_SERVICE_PLAN_NAME] App Service Plan already exists in the [$RESOURCE_GROUP_NAME] resource group..." +az appservice plan show \ --name "$APP_SERVICE_PLAN_NAME" \ - --location "$LOCATION" \ - --sku "$APP_SERVICE_PLAN_SKU" \ - --is-linux + --resource-group "$RESOURCE_GROUP_NAME" \ + --only-show-errors &>/dev/null + +if [[ $? != 0 ]]; then + echo "No [$APP_SERVICE_PLAN_NAME] App Service Plan exists in the [$RESOURCE_GROUP_NAME] resource group" + echo "Creating Linux App Service Plan [$APP_SERVICE_PLAN_NAME]..." + az appservice plan create \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --name "$APP_SERVICE_PLAN_NAME" \ + --location "$LOCATION" \ + --sku "$APP_SERVICE_PLAN_SKU" \ + --is-linux \ + --only-show-errors 1>/dev/null + + if [ $? -eq 0 ]; then + echo "App Service Plan [$APP_SERVICE_PLAN_NAME] created successfully." + else + echo "Failed to create App Service Plan [$APP_SERVICE_PLAN_NAME]." + exit 1 + fi +else + echo "[$APP_SERVICE_PLAN_NAME] App Service Plan already exists in the [$RESOURCE_GROUP_NAME] resource group" +fi + +# Get the App Service Plan resource id +echo "Getting [$APP_SERVICE_PLAN_NAME] App Service Plan resource id in the [$RESOURCE_GROUP_NAME] resource group..." +APP_SERVICE_PLAN_ID=$(az appservice plan show \ + --name "$APP_SERVICE_PLAN_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --query id \ + --output tsv \ + --only-show-errors) + +if [[ -n $APP_SERVICE_PLAN_ID ]]; then + echo "[$APP_SERVICE_PLAN_NAME] App Service Plan resource id retrieved successfully: $APP_SERVICE_PLAN_ID" +else + echo "Failed to retrieve [$APP_SERVICE_PLAN_NAME] App Service Plan resource id in the [$RESOURCE_GROUP_NAME] resource group" + exit 1 +fi + +# Check if the Web App already exists +echo "Checking if [$WEB_APP_NAME] Web App already exists in the [$RESOURCE_GROUP_NAME] resource group..." +az webapp show \ + --name "$WEB_APP_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --only-show-errors &>/dev/null + +if [[ $? != 0 ]]; then + echo "No [$WEB_APP_NAME] Web App exists in the [$RESOURCE_GROUP_NAME] resource group" + echo "Creating Web App [$WEB_APP_NAME] from custom image [$FULL_IMAGE]..." + az webapp create \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --plan "$APP_SERVICE_PLAN_NAME" \ + --name "$WEB_APP_NAME" \ + --assign-identity "${IDENTITY_ID}" \ + --container-image-name "$FULL_IMAGE" \ + --vnet "$VIRTUAL_NETWORK_NAME" \ + --subnet "$WEB_APP_SUBNET_NAME" \ + --only-show-errors 1>/dev/null -echo "Creating Web App [$WEB_APP_NAME] from custom image [$WEBAPP_IMAGE]..." + if [ $? -eq 0 ]; then + echo "Web App [$WEB_APP_NAME] created successfully." + else + echo "Failed to create Web App [$WEB_APP_NAME]." + exit 1 + fi +else + echo "[$WEB_APP_NAME] Web App already exists in the [$RESOURCE_GROUP_NAME] resource group" +fi -az webapp create \ - --resource-group "$RESOURCE_GROUP_NAME" \ - --plan "$APP_SERVICE_PLAN_NAME" \ +# Configure the App Service to use managed identity for ACR authentication +echo "Configuring Web App [$WEB_APP_NAME] to use managed identity [$MANAGED_IDENTITY_NAME] to access Azure Container Registry [$ACR_NAME]..." +az webapp config set \ --name "$WEB_APP_NAME" \ - --container-image-name "$WEBAPP_IMAGE" + --resource-group "$RESOURCE_GROUP_NAME" \ + --generic-configurations "{\"acrUseManagedIdentityCreds\": true, \"acrUserManagedIdentityID\": \"$CLIENT_ID\"}" 1>/dev/null + +if [ $? -eq 0 ]; then + echo "Web App [$WEB_APP_NAME] configured to use managed identity [$MANAGED_IDENTITY_NAME] to access Azure Container Registry [$ACR_NAME] successfully." +else + echo "Failed to configure Web App [$WEB_APP_NAME] to use managed identity [$MANAGED_IDENTITY_NAME] to access Azure Container Registry [$ACR_NAME]." + exit 1 +fi -echo "Setting Web App container settings..." +# Get the Web App resource id +echo "Getting [$WEB_APP_NAME] Web App resource id in the [$RESOURCE_GROUP_NAME] resource group..." +WEB_APP_ID=$(az webapp show \ + --name "$WEB_APP_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --query id \ + --output tsv \ + --only-show-errors) + +if [[ -n $WEB_APP_ID ]]; then + echo "[$WEB_APP_NAME] Web App resource id retrieved successfully: $WEB_APP_ID" +else + echo "Failed to retrieve [$WEB_APP_NAME] Web App resource id in the [$RESOURCE_GROUP_NAME] resource group" + exit 1 +fi + +# Enabling forced tunneling for the web app to route all outbound traffic through the virtual network +echo "Enabling forced tunneling for web app [$WEB_APP_NAME] to route all outbound traffic through the virtual network..." + +az resource update \ + --ids "$WEB_APP_ID" \ + --set properties.outboundVnetRouting.allTraffic=true \ + --only-show-errors 1>/dev/null + +if [ $? -eq 0 ]; then + echo "Forced tunneling enabled for web app [$WEB_APP_NAME]." +else + echo "Failed to enable forced tunneling for web app [$WEB_APP_NAME]." + exit 1 +fi + +# Set web app settings +echo "Setting Web App container settings for [$WEB_APP_NAME]..." az webapp config appsettings set \ --name "$WEB_APP_NAME" \ --resource-group "$RESOURCE_GROUP_NAME" \ --settings \ - WEBSITE_PORT="80" \ WEBSITES_PORT="80" \ APP_NAME="Custom Image" \ - IMAGE_NAME="$WEBAPP_IMAGE" + IMAGE_NAME="$FULL_IMAGE" \ + --only-show-errors 1>/dev/null + +if [ $? -eq 0 ]; then + echo "Web App settings for [$WEB_APP_NAME] set successfully." +else + echo "Failed to set Web App settings for [$WEB_APP_NAME]." + exit 1 +fi + +# Check if the Log Analytics workspace already exists +echo "Checking if [$LOG_ANALYTICS_NAME] Log Analytics workspace already exists in the [$RESOURCE_GROUP_NAME] resource group..." +az monitor log-analytics workspace show \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --workspace-name "$LOG_ANALYTICS_NAME" \ + --only-show-errors &>/dev/null + +if [[ $? != 0 ]]; then + echo "No [$LOG_ANALYTICS_NAME] Log Analytics workspace actually exists in the [$RESOURCE_GROUP_NAME] resource group" + echo "Creating [$LOG_ANALYTICS_NAME] Log Analytics workspace in the [$RESOURCE_GROUP_NAME] resource group..." + + # Create the Log Analytics workspace + az monitor log-analytics workspace create \ + --name "$LOG_ANALYTICS_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --location "$LOCATION" \ + --query-access "Enabled" \ + --retention-time 30 \ + --sku "PerNode" \ + --only-show-errors 1>/dev/null + + if [[ $? == 0 ]]; then + echo "[$LOG_ANALYTICS_NAME] Log Analytics workspace successfully created in the [$RESOURCE_GROUP_NAME] resource group" + else + echo "Failed to create [$LOG_ANALYTICS_NAME] Log Analytics workspace in the [$RESOURCE_GROUP_NAME] resource group" + exit 1 + fi +else + echo "[$LOG_ANALYTICS_NAME] Log Analytics workspace already exists in the [$RESOURCE_GROUP_NAME] resource group" +fi + +# Check whether the diagnostic settings for the container registry already exist +echo "Checking if [$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$ACR_NAME] container registry already exist..." +az monitor diagnostic-settings show \ + --name "$DIAGNOSTIC_SETTINGS_NAME" \ + --resource "$ACR_ID" \ + --only-show-errors &>/dev/null + +if [[ $? != 0 ]]; then + echo "No [$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$ACR_NAME] container registry actually exist" + echo "Creating [$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$ACR_NAME] container registry..." + + # Create the diagnostic settings for the container registry to send logs to the Log Analytics workspace + az monitor diagnostic-settings create \ + --name "$DIAGNOSTIC_SETTINGS_NAME" \ + --resource "$ACR_ID" \ + --workspace "$LOG_ANALYTICS_NAME" \ + --logs '[ + {"category": "ContainerRegistryRepositoryEvents", "enabled": true}, + {"category": "ContainerRegistryLoginEvents", "enabled": true} + ]' \ + --metrics '[ + {"category": "AllMetrics", "enabled": true} + ]' \ + --only-show-errors 1>/dev/null + + if [[ $? == 0 ]]; then + echo "[$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$ACR_NAME] container registry successfully created" + else + echo "Failed to create [$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$ACR_NAME] container registry" + exit 1 + fi +else + echo "[$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$ACR_NAME] container registry already exist" +fi + +# Check whether the diagnostic settings for the web app already exist +echo "Checking if [$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$WEB_APP_NAME] web app already exist..." +az monitor diagnostic-settings show \ + --name "$DIAGNOSTIC_SETTINGS_NAME" \ + --resource "$WEB_APP_ID" \ + --only-show-errors &>/dev/null + +if [[ $? != 0 ]]; then + echo "No [$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$WEB_APP_NAME] web app actually exist" + echo "Creating [$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$WEB_APP_NAME] web app..." + + # Create the diagnostic settings for the web app to send logs to the Log Analytics workspace + az monitor diagnostic-settings create \ + --name "$DIAGNOSTIC_SETTINGS_NAME" \ + --resource "$WEB_APP_ID" \ + --workspace "$LOG_ANALYTICS_NAME" \ + --logs '[ + {"category": "AppServiceHTTPLogs", "enabled": true}, + {"category": "AppServiceConsoleLogs", "enabled": true}, + {"category": "AppServiceAppLogs", "enabled": true}, + {"category": "AppServiceAuditLogs", "enabled": true}, + {"category": "AppServiceIPSecAuditLogs", "enabled": true}, + {"category": "AppServicePlatformLogs", "enabled": true}, + {"category": "AppServiceAuthenticationLogs", "enabled": true} + ]' \ + --metrics '[ + {"category": "AllMetrics", "enabled": true} + ]' \ + --only-show-errors 1>/dev/null + + if [[ $? == 0 ]]; then + echo "[$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$WEB_APP_NAME] web app successfully created" + else + echo "Failed to create [$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$WEB_APP_NAME] web app" + exit 1 + fi +else + echo "[$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$WEB_APP_NAME] web app already exist" +fi + +# Check whether the diagnostic settings for the App Service Plan already exist +echo "Checking if [$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$APP_SERVICE_PLAN_NAME] App Service Plan already exist..." +az monitor diagnostic-settings show \ + --name "$DIAGNOSTIC_SETTINGS_NAME" \ + --resource "$APP_SERVICE_PLAN_ID" \ + --only-show-errors &>/dev/null + +if [[ $? != 0 ]]; then + echo "No [$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$APP_SERVICE_PLAN_NAME] App Service Plan actually exist" + echo "Creating [$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$APP_SERVICE_PLAN_NAME] App Service Plan..." + + # Create the diagnostic settings for the App Service Plan to send metrics to the Log Analytics workspace + az monitor diagnostic-settings create \ + --name "$DIAGNOSTIC_SETTINGS_NAME" \ + --resource "$APP_SERVICE_PLAN_ID" \ + --workspace "$LOG_ANALYTICS_NAME" \ + --metrics '[ + {"category": "AllMetrics", "enabled": true} + ]' \ + --only-show-errors 1>/dev/null + + if [[ $? == 0 ]]; then + echo "[$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$APP_SERVICE_PLAN_NAME] App Service Plan successfully created" + else + echo "Failed to create [$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$APP_SERVICE_PLAN_NAME] App Service Plan" + exit 1 + fi +else + echo "[$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$APP_SERVICE_PLAN_NAME] App Service Plan already exist" +fi + +# Check whether the diagnostic settings for the virtual network already exist +echo "Checking if [$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$VIRTUAL_NETWORK_NAME] virtual network already exist..." +az monitor diagnostic-settings show \ + --name "$DIAGNOSTIC_SETTINGS_NAME" \ + --resource "$VIRTUAL_NETWORK_ID" \ + --only-show-errors &>/dev/null + +if [[ $? != 0 ]]; then + echo "No [$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$VIRTUAL_NETWORK_NAME] virtual network actually exist" + echo "Creating [$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$VIRTUAL_NETWORK_NAME] virtual network..." + + # Create the diagnostic settings for the virtual network to send logs to the Log Analytics workspace + az monitor diagnostic-settings create \ + --name "$DIAGNOSTIC_SETTINGS_NAME" \ + --resource "$VIRTUAL_NETWORK_ID" \ + --workspace "$LOG_ANALYTICS_NAME" \ + --logs '[ + {"category": "VMProtectionAlerts", "enabled": true} + ]' \ + --metrics '[ + {"category": "AllMetrics", "enabled": true} + ]' \ + --only-show-errors 1>/dev/null + + if [[ $? == 0 ]]; then + echo "[$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$VIRTUAL_NETWORK_NAME] virtual network successfully created" + else + echo "Failed to create [$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$VIRTUAL_NETWORK_NAME] virtual network" + exit 1 + fi +else + echo "[$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$VIRTUAL_NETWORK_NAME] virtual network already exist" +fi + +# Check whether the diagnostic settings for the network security group for the web app subnet already exist +echo "Checking if [$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$WEB_APP_SUBNET_NSG_NAME] network security group for the web app subnet already exist..." +az monitor diagnostic-settings show \ + --name "$DIAGNOSTIC_SETTINGS_NAME" \ + --resource "$WEB_APP_SUBNET_NSG_ID" \ + --only-show-errors &>/dev/null + +if [[ $? != 0 ]]; then + echo "No [$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$WEB_APP_SUBNET_NSG_NAME] network security group for the web app subnet actually exist" + echo "Creating [$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$WEB_APP_SUBNET_NSG_NAME] network security group for the web app subnet..." + + # Create the diagnostic settings for the network security group for the web app subnet to send logs to the Log Analytics workspace + az monitor diagnostic-settings create \ + --name "$DIAGNOSTIC_SETTINGS_NAME" \ + --resource "$WEB_APP_SUBNET_NSG_ID" \ + --workspace "$LOG_ANALYTICS_NAME" \ + --logs '[ + {"category": "NetworkSecurityGroupEvent", "enabled": true}, + {"category": "NetworkSecurityGroupRuleCounter", "enabled": true} + ]' \ + --only-show-errors 1>/dev/null + + if [[ $? == 0 ]]; then + echo "[$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$WEB_APP_SUBNET_NSG_NAME] network security group for the web app subnet successfully created" + else + echo "Failed to create [$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$WEB_APP_SUBNET_NSG_NAME] network security group for the web app subnet" + exit 1 + fi +else + echo "[$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$WEB_APP_SUBNET_NSG_NAME] network security group for the web app subnet already exist" +fi + +# Check whether the diagnostic settings for the network security group for the private endpoint subnet already exist +echo "Checking if [$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$PE_SUBNET_NSG_NAME] network security group for the private endpoint subnet already exist..." +az monitor diagnostic-settings show \ + --name "$DIAGNOSTIC_SETTINGS_NAME" \ + --resource "$PE_SUBNET_NSG_ID" \ + --only-show-errors &>/dev/null + +if [[ $? != 0 ]]; then + echo "No [$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$PE_SUBNET_NSG_NAME] network security group for the private endpoint subnet actually exist" + echo "Creating [$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$PE_SUBNET_NSG_NAME] network security group for the private endpoint subnet..." + + # Create the diagnostic settings for the network security group for the private endpoint subnet to send logs to the Log Analytics workspace + az monitor diagnostic-settings create \ + --name "$DIAGNOSTIC_SETTINGS_NAME" \ + --resource "$PE_SUBNET_NSG_ID" \ + --workspace "$LOG_ANALYTICS_NAME" \ + --logs '[ + {"category": "NetworkSecurityGroupEvent", "enabled": true}, + {"category": "NetworkSecurityGroupRuleCounter", "enabled": true} + ]' \ + --only-show-errors 1>/dev/null + + if [[ $? == 0 ]]; then + echo "[$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$PE_SUBNET_NSG_NAME] network security group for the private endpoint subnet successfully created" + else + echo "Failed to create [$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$PE_SUBNET_NSG_NAME] network security group for the private endpoint subnet" + exit 1 + fi +else + echo "[$DIAGNOSTIC_SETTINGS_NAME] diagnostic settings for the [$PE_SUBNET_NSG_NAME] network security group for the private endpoint subnet already exist" +fi echo "Listing resources in resource group [$RESOURCE_GROUP_NAME]..." az resource list --resource-group "$RESOURCE_GROUP_NAME" --output table @@ -84,7 +1016,8 @@ echo "Deployment complete." echo "Resource Group: $RESOURCE_GROUP_NAME" echo "App Service Plan: $APP_SERVICE_PLAN_NAME" echo "Web App: $WEB_APP_NAME" -echo "ACR: $ACR_NAME ($LOGIN_SERVER)" -echo "Image: $WEBAPP_IMAGE" +echo "Azure Container Registry: $ACR_NAME ($ACR_LOGIN_SERVER)" +echo "Image: $FULL_IMAGE" +echo "Managed Identity: $MANAGED_IDENTITY_NAME" echo "" echo "Run 'bash scripts/validate.sh' to verify the deployment." diff --git a/samples/web-app-custom-image/python/scripts/validate.sh b/samples/web-app-custom-image/python/scripts/validate.sh index 8d89b8c..b58fb61 100755 --- a/samples/web-app-custom-image/python/scripts/validate.sh +++ b/samples/web-app-custom-image/python/scripts/validate.sh @@ -3,28 +3,48 @@ set -euo pipefail PREFIX='local' SUFFIX='test' -RESOURCE_GROUP_NAME="${PREFIX}-custom-image-rg" -ACR_NAME="${PREFIX}customimageacr" -APP_SERVICE_PLAN_NAME="${PREFIX}-custom-image-plan-${SUFFIX}" -WEB_APP_NAME="${PREFIX}-custom-image-webapp-${SUFFIX}" +RESOURCE_GROUP_NAME="${PREFIX}-rg" +ACR_NAME="${PREFIX}acr${SUFFIX}" +MANAGED_IDENTITY_NAME="${PREFIX}-identity-${SUFFIX}" +APP_SERVICE_PLAN_NAME="${PREFIX}-app-service-plan-${SUFFIX}" +WEB_APP_NAME="${PREFIX}-webapp-${SUFFIX}" +VIRTUAL_NETWORK_NAME="${PREFIX}-vnet-${SUFFIX}" +PRIVATE_DNS_ZONE_NAME="privatelink.azurecr.io" +PRIVATE_ENDPOINT_NAME="${PREFIX}-acr-pe-${SUFFIX}" +WEB_APP_SUBNET_NSG_NAME="${PREFIX}-webapp-subnet-nsg-${SUFFIX}" +PE_SUBNET_NSG_NAME="${PREFIX}-pe-subnet-nsg-${SUFFIX}" +NAT_GATEWAY_NAME="${PREFIX}-nat-gateway-${SUFFIX}" +PIP_PREFIX_NAME="${PREFIX}-nat-gateway-pip-prefix-${SUFFIX}" +LOG_ANALYTICS_NAME="${PREFIX}-log-analytics-${SUFFIX}" +# Check resource group echo -e "[$RESOURCE_GROUP_NAME] resource group:\n" az group show \ --name "$RESOURCE_GROUP_NAME" \ --output table +# Check managed identity +echo -e "[$MANAGED_IDENTITY_NAME] managed identity:\n" +az identity show \ + --name "$MANAGED_IDENTITY_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --output table + +# Check App Service Plan echo -e "\n[$APP_SERVICE_PLAN_NAME] App Service Plan:\n" az appservice plan show \ --name "$APP_SERVICE_PLAN_NAME" \ --resource-group "$RESOURCE_GROUP_NAME" \ --output table +# Check Azure Container Registry echo -e "\n[$ACR_NAME] Azure Container Registry:\n" az acr show \ --name "$ACR_NAME" \ --resource-group "$RESOURCE_GROUP_NAME" \ --output table +# Check Azure Web App echo -e "\n[$WEB_APP_NAME] Web App:\n" az webapp show \ --name "$WEB_APP_NAME" \ @@ -32,13 +52,80 @@ az webapp show \ --query "{name:name, state:state, defaultHostName:defaultHostName, kind:kind}" \ --output table +# Check App Settings echo -e "\n[$WEB_APP_NAME] app settings:\n" az webapp config appsettings list \ --name "$WEB_APP_NAME" \ --resource-group "$RESOURCE_GROUP_NAME" \ - --query "[?name=='IMAGE_NAME' || name=='WEBSITE_PORT' || name=='WEBSITES_PORT'].[name,value]" \ + --query "[?name=='IMAGE_NAME' || name=='APP_NAME' || name=='WEBSITES_PORT']" \ --output table +# Check Virtual Network +echo -e "\n[$VIRTUAL_NETWORK_NAME] virtual network:\n" +az network vnet show \ + --name "$VIRTUAL_NETWORK_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --output table \ + --only-show-errors + +# Check Private DNS Zone +echo -e "\n[$PRIVATE_DNS_ZONE_NAME] private dns zone:\n" +az network private-dns zone show \ + --name "$PRIVATE_DNS_ZONE_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --query '{Name:name,ResourceGroup:resourceGroup,RecordSets:recordSets,VirtualNetworkLinks:virtualNetworkLinks}' \ + --output table \ + --only-show-errors + +# Check Private Endpoint +echo -e "\n[$PRIVATE_ENDPOINT_NAME] private endpoint:\n" +az network private-endpoint show \ + --name "$PRIVATE_ENDPOINT_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --output table \ + --only-show-errors + +# Check Web App Subnet NSG +echo -e "\n[$WEB_APP_SUBNET_NSG_NAME] network security group:\n" +az network nsg show \ + --name "$WEB_APP_SUBNET_NSG_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --output table \ + --only-show-errors + +# Check Private Endpoint Subnet NSG +echo -e "\n[$PE_SUBNET_NSG_NAME] network security group:\n" +az network nsg show \ + --name "$PE_SUBNET_NSG_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --output table \ + --only-show-errors + +# Check NAT Gateway +echo -e "\n[$NAT_GATEWAY_NAME] nat gateway:\n" +az network nat gateway show \ + --name "$NAT_GATEWAY_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --output table \ + --only-show-errors + +# Check Public IP Prefix +echo -e "\n[$PIP_PREFIX_NAME] public ip prefix:\n" +az network public-ip prefix show \ + --name "$PIP_PREFIX_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --output table \ + --only-show-errors + +# Check Log Analytics Workspace +echo -e "\n[$LOG_ANALYTICS_NAME] log analytics workspace:\n" +az monitor log-analytics workspace show \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --workspace-name "$LOG_ANALYTICS_NAME" \ + --query '{Name:name,Location:location,ResourceGroup:resourceGroup}' \ + --output table \ + --only-show-errors + echo -e "\nResources in [$RESOURCE_GROUP_NAME]:\n" az resource list \ --resource-group "$RESOURCE_GROUP_NAME" \ diff --git a/samples/web-app-custom-image/python/src/app.py b/samples/web-app-custom-image/python/src/app.py index 7a4b31c..faccd67 100644 --- a/samples/web-app-custom-image/python/src/app.py +++ b/samples/web-app-custom-image/python/src/app.py @@ -12,7 +12,7 @@ def index(): return render_template( "index.html", app_name=os.environ.get("APP_NAME", "Custom Image Web App"), - image_name=os.environ.get("IMAGE_NAME", "vacation-planner-webapp:v1"), + image_name=os.environ.get("IMAGE_NAME", "custom-image-webapp:v1"), hostname=socket.gethostname(), ) @@ -23,7 +23,7 @@ def status(): { "status": "ok", "app": os.environ.get("APP_NAME", "Custom Image Web App"), - "image": os.environ.get("IMAGE_NAME", "vacation-planner-webapp:v1"), + "image": os.environ.get("IMAGE_NAME", "custom-image-webapp:v1"), "hostname": socket.gethostname(), } ) diff --git a/samples/web-app-custom-image/python/terraform/README.md b/samples/web-app-custom-image/python/terraform/README.md new file mode 100644 index 0000000..8f87db4 --- /dev/null +++ b/samples/web-app-custom-image/python/terraform/README.md @@ -0,0 +1,280 @@ +# Terraform Deployment + +This directory contains Terraform modules and a deployment script for provisioning Azure services in LocalStack for Azure. For further details about the sample application, refer to the [Azure Web App with Custom Docker Image](../README.md). + +## Prerequisites + +Before deploying this solution, ensure you have the following tools installed: + +- [LocalStack for Azure](https://docs.localstack.cloud/azure/): Local Azure cloud emulator for development and testing +- [Visual Studio Code](https://code.visualstudio.com/): Code editor installed on one of the [supported platforms](https://code.visualstudio.com/docs/supporting/requirements#_platforms) +- [Terraform](https://developer.hashicorp.com/terraform/downloads): Infrastructure as Code tool for provisioning Azure resources +- [Docker](https://docs.docker.com/get-docker/): Container runtime required for LocalStack and building the custom image +- [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli): Azure command-line interface +- [Azlocal CLI](https://azure.localstack.cloud/user-guides/sdks/az/): LocalStack Azure CLI wrapper +- [Python](https://www.python.org/downloads/): Python runtime (version 3.12 or above) +- [jq](https://jqlang.org/): JSON processor for scripting and parsing command outputs + +### Installing azlocal CLI + +The [deploy.sh](deploy.sh) Bash script uses the `azlocal` CLI instead of the standard Azure CLI to work with LocalStack. Install it using: + +```bash +pip install azlocal +``` + +For more information, see [Get started with the az tool on LocalStack](https://azure.localstack.cloud/user-guides/sdks/az/). + +## Architecture Overview + +The Terraform modules create the following Azure resources: + +1. [Azure Resource Group](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/manage-resource-groups-cli): A logical container scoping all resources in this sample. +2. [Azure Virtual Network](https://learn.microsoft.com/azure/virtual-network/virtual-networks-overview): Hosts two subnets: + - *app-subnet*: Dedicated to [regional VNet integration](https://learn.microsoft.com/azure/azure-functions/functions-networking-options?tabs=azure-portal#outbound-networking-features) with the Web App. + - *pe-subnet*: Used for hosting Azure Private Endpoints. +3. [Azure Private DNS Zone](https://learn.microsoft.com/azure/dns/private-dns-privatednszone): Handles DNS resolution for the Azure Container Registry Private Endpoint within the virtual network. +4. [Azure Private Endpoint](https://learn.microsoft.com/azure/private-link/private-endpoint-overview): Secures network access to the Azure Container Registry via a private IP within the VNet. +5. [Azure NAT Gateway](https://learn.microsoft.com/azure/nat-gateway/nat-overview): Provides deterministic outbound connectivity for the Web App. Included for completeness; the sample app does not call any external services. +6. [Azure Network Security Group](https://learn.microsoft.com/en-us/azure/virtual-network/network-security-groups-overview): Enforces inbound and outbound traffic rules across the virtual network's subnets. +7. [Azure Log Analytics Workspace](https://learn.microsoft.com/azure/azure-monitor/logs/log-analytics-overview): Centralizes diagnostic logs and metrics from all resources in the solution. +8. [Azure Container Registry](https://learn.microsoft.com/azure/container-registry/container-registry-intro): A fully-managed container registry service based on the open-source [Docker platform](https://docs.docker.com/get-started/docker-overview/) used to hold the container image used by the web app. +9. [User-Assigned Managed Identity](https://learn.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview): Created and associated with the Web App. +10. [Azure App Service Plan](https://learn.microsoft.com/en-us/azure/app-service/overview-hosting-plans): The underlying compute tier that hosts the web application. +11. [Azure Web App](https://learn.microsoft.com/en-us/azure/app-service/overview): Runs the Python Flask application from the custom container image stored in the Azure Container Registry. + +> **Note** +> The Terraform [azurerm_linux_web_app](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/linux_web_app) resource does not support using a managed identity for container image pull from Azure Container Registry. The Terraform deployment uses ACR admin username and password instead of a managed identity. + +## Provisioning Scripts + +You can use the [deploy.sh](deploy.sh) script to automate the deployment of all Azure resources in a single step, streamlining setup and reducing manual configuration. The script executes the following steps: + +- Cleans up any previous Terraform state and plan files to ensure a fresh deployment. +- Initializes the Terraform working directory and downloads required plugins. +- Creates and validates a Terraform execution plan for the Azure infrastructure. +- Applies the Terraform plan to provision all necessary Azure resources. +- Uses a [`local-exec` provisioner](https://developer.hashicorp.com/terraform/language/resources/provisioners/local-exec) on a `null_resource` to build and push the container image to the Azure Container Registry locally before deploying the Web App. +- Deploys the Web App configured to pull the container image using ACR admin credentials. + +## Configuration + +When using LocalStack for Azure, configure the `metadata_host` and `subscription_id` settings in the [Azure Provider for Terraform](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs) to ensure proper connectivity: + +```hcl +provider "azurerm" { + features { + resource_group { + prevent_deletion_if_contains_resources = false + } + } + + # Set the hostname of the Azure Metadata Service (for example management.azure.com) + # used to obtain the Cloud Environment when using LocalStack's Azure emulator. + # This allows the provider to correctly identify the environment and avoid making calls to the real Azure endpoints. + metadata_host = "localhost.localstack.cloud:4566" + + # Set the subscription ID to a dummy value when using LocalStack's Azure emulator. + subscription_id = "00000000-0000-0000-0000-000000000000" +} +``` + +## Deployment + +You can set up the Azure emulator by utilizing the LocalStack for Azure Docker image. Before starting, ensure you have a valid `LOCALSTACK_AUTH_TOKEN` to access the Azure emulator. Refer to the [Auth Token guide](https://docs.localstack.cloud/getting-started/auth-token/?__hstc=108988063.8aad2b1a7229945859f4d9b9bb71e05d.1743148429561.1758793541854.1758810151462.32&__hssc=108988063.3.1758810151462&__hsfp=3945774529) to obtain your Auth Token and specify it in the `LOCALSTACK_AUTH_TOKEN` environment variable. The Azure Docker image is available on the [LocalStack Docker Hub](https://hub.docker.com/r/localstack/localstack-azure-alpha). To pull the Azure Docker image, execute the following command: + +```bash +docker pull localstack/localstack-azure-alpha +``` + +Start the LocalStack Azure emulator using the localstack CLI, execute the following command: + +```bash +# Set the authentication token +export LOCALSTACK_AUTH_TOKEN= + +# Start the LocalStack Azure emulator +IMAGE_NAME=localstack/localstack-azure-alpha localstack start -d +localstack wait -t 60 + +# Route all Azure CLI calls to the LocalStack Azure emulator +azlocal start-interception +``` + +Navigate to the `terraform` folder: + +```bash +cd samples/web-app-custom-image/python/terraform +``` + +Make the script executable: + +```bash +chmod +x deploy.sh +``` + +Run the deployment script: + +```bash +./deploy.sh +``` + +## Validation + +Once the deployment completes, run the [validate.sh](../scripts/validate.sh) script to confirm that all resources were provisioned and configured as expected: + +```bash +#!/bin/bash +set -euo pipefail + +PREFIX='local' +SUFFIX='test' +RESOURCE_GROUP_NAME="${PREFIX}-rg" +ACR_NAME="${PREFIX}acr${SUFFIX}" +MANAGED_IDENTITY_NAME="${PREFIX}-identity-${SUFFIX}" +APP_SERVICE_PLAN_NAME="${PREFIX}-app-service-plan-${SUFFIX}" +WEB_APP_NAME="${PREFIX}-webapp-${SUFFIX}" +VIRTUAL_NETWORK_NAME="${PREFIX}-vnet-${SUFFIX}" +PRIVATE_DNS_ZONE_NAME="privatelink.azurecr.io" +PRIVATE_ENDPOINT_NAME="${PREFIX}-acr-pe-${SUFFIX}" +WEB_APP_SUBNET_NSG_NAME="${PREFIX}-webapp-subnet-nsg-${SUFFIX}" +PE_SUBNET_NSG_NAME="${PREFIX}-pe-subnet-nsg-${SUFFIX}" +NAT_GATEWAY_NAME="${PREFIX}-nat-gateway-${SUFFIX}" +PIP_PREFIX_NAME="${PREFIX}-nat-gateway-pip-prefix-${SUFFIX}" +LOG_ANALYTICS_NAME="${PREFIX}-log-analytics-${SUFFIX}" + +# Check resource group +echo -e "[$RESOURCE_GROUP_NAME] resource group:\n" +az group show \ + --name "$RESOURCE_GROUP_NAME" \ + --output table + +# Check managed identity +echo -e "[$MANAGED_IDENTITY_NAME] managed identity:\n" +az identity show \ + --name "$MANAGED_IDENTITY_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --output table + +# Check App Service Plan +echo -e "\n[$APP_SERVICE_PLAN_NAME] App Service Plan:\n" +az appservice plan show \ + --name "$APP_SERVICE_PLAN_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --output table + +# Check Azure Container Registry +echo -e "\n[$ACR_NAME] Azure Container Registry:\n" +az acr show \ + --name "$ACR_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --output table + +# Check Azure Web App +echo -e "\n[$WEB_APP_NAME] Web App:\n" +az webapp show \ + --name "$WEB_APP_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --query "{name:name, state:state, defaultHostName:defaultHostName, kind:kind}" \ + --output table + +# Check App Settings +echo -e "\n[$WEB_APP_NAME] app settings:\n" +az webapp config appsettings list \ + --name "$WEB_APP_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --query "[?name=='IMAGE_NAME' || name=='APP_NAME' || name=='WEBSITES_PORT']" \ + --output table + +# Check Virtual Network +echo -e "\n[$VIRTUAL_NETWORK_NAME] virtual network:\n" +az network vnet show \ + --name "$VIRTUAL_NETWORK_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --output table \ + --only-show-errors + +# Check Private DNS Zone +echo -e "\n[$PRIVATE_DNS_ZONE_NAME] private dns zone:\n" +az network private-dns zone show \ + --name "$PRIVATE_DNS_ZONE_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --query '{Name:name,ResourceGroup:resourceGroup,RecordSets:recordSets,VirtualNetworkLinks:virtualNetworkLinks}' \ + --output table \ + --only-show-errors + +# Check Private Endpoint +echo -e "\n[$PRIVATE_ENDPOINT_NAME] private endpoint:\n" +az network private-endpoint show \ + --name "$PRIVATE_ENDPOINT_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --output table \ + --only-show-errors + +# Check Web App Subnet NSG +echo -e "\n[$WEB_APP_SUBNET_NSG_NAME] network security group:\n" +az network nsg show \ + --name "$WEB_APP_SUBNET_NSG_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --output table \ + --only-show-errors + +# Check Private Endpoint Subnet NSG +echo -e "\n[$PE_SUBNET_NSG_NAME] network security group:\n" +az network nsg show \ + --name "$PE_SUBNET_NSG_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --output table \ + --only-show-errors + +# Check NAT Gateway +echo -e "\n[$NAT_GATEWAY_NAME] nat gateway:\n" +az network nat gateway show \ + --name "$NAT_GATEWAY_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --output table \ + --only-show-errors + +# Check Public IP Prefix +echo -e "\n[$PIP_PREFIX_NAME] public ip prefix:\n" +az network public-ip prefix show \ + --name "$PIP_PREFIX_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --output table \ + --only-show-errors + +# Check Log Analytics Workspace +echo -e "\n[$LOG_ANALYTICS_NAME] log analytics workspace:\n" +az monitor log-analytics workspace show \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --workspace-name "$LOG_ANALYTICS_NAME" \ + --query '{Name:name,Location:location,ResourceGroup:resourceGroup}' \ + --output table \ + --only-show-errors + +echo -e "\nResources in [$RESOURCE_GROUP_NAME]:\n" +az resource list \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --output table +``` + +## Cleanup + +To destroy all created resources: + +```bash +# Delete resource group and all contained resources +az group delete --name local-rg --yes --no-wait + +# Verify deletion +az group list --output table +``` + +This will remove all Azure resources created by the Terraform deployment. + +## Related Documentation + +- [Terraform Azure Provider Documentation](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs) +- [Terraform local-exec Provisioner](https://developer.hashicorp.com/terraform/language/resources/provisioners/local-exec) +- [LocalStack for Azure Documentation](https://docs.localstack.cloud/azure/) + diff --git a/samples/web-app-custom-image/python/terraform/deploy.sh b/samples/web-app-custom-image/python/terraform/deploy.sh new file mode 100755 index 0000000..63e42fb --- /dev/null +++ b/samples/web-app-custom-image/python/terraform/deploy.sh @@ -0,0 +1,53 @@ +#!/bin/bash + +# Variables +PREFIX='local' +SUFFIX='test' +LOCATION='westeurope' +CURRENT_DIR="$(cd "$(dirname "$0")" && pwd)" +IMAGE_NAME="custom-image-webapp" +IMAGE_TAG="v1" + +# Change the current directory to the script's directory +cd "$CURRENT_DIR" || exit + +# Intialize Terraform +echo "Initializing Terraform..." +terraform init -upgrade + +# Run terraform plan and check for errors +echo "Planning Terraform deployment..." +terraform plan -out=tfplan \ + -var "prefix=$PREFIX" \ + -var "suffix=$SUFFIX" \ + -var "location=$LOCATION" \ + -var "image_name=$IMAGE_NAME" \ + -var "image_tag=$IMAGE_TAG" + +if [[ $? != 0 ]]; then + echo "Terraform plan failed. Exiting." + exit 1 +fi + +# Apply the Terraform configuration +echo "Applying Terraform configuration..." +terraform apply -auto-approve tfplan + +if [[ $? != 0 ]]; then + echo "Terraform apply failed. Exiting." + exit 1 +fi + +# Get the output values +RESOURCE_GROUP_NAME=$(terraform output -raw resource_group_name) +WEB_APP_NAME=$(terraform output -raw web_app_name) +ACR_NAME=$(terraform output -raw container_registry_name) + +if [[ -z "$RESOURCE_GROUP_NAME" || -z "$WEB_APP_NAME" || -z "$ACR_NAME" ]]; then + echo "Resource Group Name, Web App Name, or ACR Name is empty. Exiting." + exit 1 +fi + +# Print the list of resources in the resource group +echo "Listing resources in resource group [$RESOURCE_GROUP_NAME]..." +az resource list --resource-group "$RESOURCE_GROUP_NAME" --output table \ No newline at end of file diff --git a/samples/web-app-custom-image/python/terraform/main.tf b/samples/web-app-custom-image/python/terraform/main.tf new file mode 100644 index 0000000..5af101e --- /dev/null +++ b/samples/web-app-custom-image/python/terraform/main.tf @@ -0,0 +1,225 @@ +# Local Variables +locals { + prefix = lower(var.prefix) + suffix = lower(var.suffix) + resource_group_name = "${var.prefix}-rg" + log_analytics_name = "${local.prefix}-log-analytics-${local.suffix}" + virtual_network_name = "${local.prefix}-vnet-${local.suffix}" + nat_gateway_name = "${local.prefix}-nat-gateway-${local.suffix}" + nat_gateway_ip_prefix_name = "${local.prefix}-nat-gateway-pip-prefix-${local.suffix}" + private_endpoint_name = "${local.prefix}-acr-pe-${local.suffix}" + webapp_subnet_nsg_name = "${local.prefix}-webapp-subnet-nsg-${local.suffix}" + pe_subnet_nsg_name = "${local.prefix}-pe-subnet-nsg-${local.suffix}" + acr_name = "${local.prefix}acr${local.suffix}" + managed_identity_name = "${local.prefix}-identity-${local.suffix}" + app_service_plan_name = "${local.prefix}-app-service-plan-${local.suffix}" + web_app_name = "${local.prefix}-webapp-${local.suffix}" +} + +# Data Sources +data "azurerm_client_config" "current" { +} + +# Create a resource group +resource "azurerm_resource_group" "example" { + name = local.resource_group_name + location = var.location + tags = var.tags +} + +# Create a log analytics workspace +module "log_analytics_workspace" { + source = "./modules/log_analytics" + name = local.log_analytics_name + location = var.location + resource_group_name = azurerm_resource_group.example.name + tags = var.tags +} + +# Create a container registry +module "container_registry" { + source = "./modules/container_registry" + name = local.acr_name + resource_group_name = azurerm_resource_group.example.name + location = var.location + sku = var.acr_sku + admin_enabled = var.acr_admin_enabled + georeplication_locations = var.acr_georeplication_locations + log_analytics_workspace_id = module.log_analytics_workspace.id + tags = var.tags +} + +# Create a virtual network with subnets +module "virtual_network" { + source = "./modules/virtual_network" + resource_group_name = azurerm_resource_group.example.name + location = var.location + vnet_name = local.virtual_network_name + address_space = var.vnet_address_space + log_analytics_workspace_id = module.log_analytics_workspace.id + tags = var.tags + + subnets = [ + { + name : var.webapp_subnet_name + address_prefixes : var.webapp_subnet_address_prefix + private_endpoint_network_policies : "Enabled" + private_link_service_network_policies_enabled : false + delegation : "Microsoft.Web/serverFarms" + }, + { + name : var.pe_subnet_name + address_prefixes : var.pe_subnet_address_prefix + private_endpoint_network_policies : "Enabled" + private_link_service_network_policies_enabled : false + delegation : null + } + ] +} + +# Create a network security group and associate it with the webapp subnet +module "webapp_subnet_network_security_group" { + source = "./modules/network_security_group" + name = local.webapp_subnet_nsg_name + resource_group_name = azurerm_resource_group.example.name + location = var.location + log_analytics_workspace_id = module.log_analytics_workspace.id + tags = var.tags + subnet_ids = { + (var.webapp_subnet_name) = module.virtual_network.subnet_ids[var.webapp_subnet_name] + } +} + +# Create a network security group and associate it with the private endpoint subnet +module "pe_subnet_network_security_group" { + source = "./modules/network_security_group" + name = local.pe_subnet_nsg_name + resource_group_name = azurerm_resource_group.example.name + location = var.location + log_analytics_workspace_id = module.log_analytics_workspace.id + tags = var.tags + subnet_ids = { + (var.pe_subnet_name) = module.virtual_network.subnet_ids[var.pe_subnet_name] + } +} + +# Create a NAT gateway and associate it with the webapp subnet +module "nat_gateway" { + source = "./modules/nat_gateway" + name = local.nat_gateway_name + resource_group_name = azurerm_resource_group.example.name + location = var.location + sku_name = var.nat_gateway_sku_name + public_ip_prefix_name = local.nat_gateway_ip_prefix_name + public_ip_prefix_length = 31 + idle_timeout_in_minutes = var.nat_gateway_idle_timeout_in_minutes + zones = var.nat_gateway_zones + subnet_ids = { + (var.webapp_subnet_name) = module.virtual_network.subnet_ids[var.webapp_subnet_name] + } + tags = var.tags +} + +# Create a private DNS zone for the CosmosDB MongoDB account and link it to the virtual network +module "private_dns_zone" { + source = "./modules/private_dns_zone" + name = "privatelink.azurecr.io" + resource_group_name = azurerm_resource_group.example.name + tags = var.tags + virtual_networks_to_link = { + (module.virtual_network.name) = { + subscription_id = data.azurerm_client_config.current.subscription_id + resource_group_name = azurerm_resource_group.example.name + } + } +} + +# Create a private endpoint for the CosmosDB MongoDB account in the pe_subnet subnet +module "private_endpoint" { + source = "./modules/private_endpoint" + name = local.private_endpoint_name + location = var.location + resource_group_name = azurerm_resource_group.example.name + subnet_id = module.virtual_network.subnet_ids[var.pe_subnet_name] + tags = var.tags + private_connection_resource_id = module.container_registry.id + is_manual_connection = false + subresource_name = "registry" + private_dns_zone_group_name = "private-dns-zone-group" + private_dns_zone_group_ids = [module.private_dns_zone.id] +} + +# Create App Service Plan using module +module "app_service_plan" { + source = "./modules/app_service_plan" + name = local.app_service_plan_name + resource_group_name = azurerm_resource_group.example.name + location = azurerm_resource_group.example.location + sku_name = var.sku_name + os_type = var.os_type + zone_balancing_enabled = var.zone_balancing_enabled + log_analytics_workspace_id = module.log_analytics_workspace.id + tags = var.tags +} + +# Create a user-assigned managed identity +module "managed_identity" { + source = "./modules/managed_identity" + name = local.managed_identity_name + resource_group_name = azurerm_resource_group.example.name + location = var.location + acr_id = module.container_registry.id + tags = var.tags +} + +# Push container image to the registry +resource "null_resource" "push_image" { + count = (var.image_name != null && var.image_name != "" && var.image_tag != null && var.image_tag != "") ? 1 : 0 + + provisioner "local-exec" { + command = "${path.root}/push_image.sh" + environment = { + ACR_NAME = local.acr_name + ACR_LOGIN_SERVER = module.container_registry.login_server + IMAGE_NAME = var.image_name + IMAGE_TAG = var.image_tag + } + } + + depends_on = [module.container_registry] +} + +# Create Web App using module +module "web_app" { + source = "./modules/web_app" + name = local.web_app_name + resource_group_name = azurerm_resource_group.example.name + location = azurerm_resource_group.example.location + managed_identity_id = module.managed_identity.id + service_plan_id = module.app_service_plan.id + https_only = var.https_only + virtual_network_subnet_id = module.virtual_network.subnet_ids[var.webapp_subnet_name] + vnet_route_all_enabled = true + public_network_access_enabled = var.public_network_access_enabled + always_on = var.always_on + http2_enabled = var.http2_enabled + minimum_tls_version = var.minimum_tls_version + image_name = var.image_name + image_tag = var.image_tag + docker_registry_url = module.container_registry.login_server_url + docker_registry_username = module.container_registry.admin_username + docker_registry_password = module.container_registry.admin_password + repo_url = var.repo_url + log_analytics_workspace_id = module.log_analytics_workspace.id + tags = var.tags + + app_settings = { + SCM_DO_BUILD_DURING_DEPLOYMENT = "true" + ENABLE_ORYX_BUILD = "true" + WEBSITES_PORT = var.websites_port + APP_NAME = "Custom Image" + IMAGE_NAME = "${module.container_registry.login_server}/${var.image_name}:${var.image_tag}" + } + + depends_on = [null_resource.push_image] +} \ No newline at end of file diff --git a/samples/web-app-custom-image/python/terraform/modules/app_service_plan/main.tf b/samples/web-app-custom-image/python/terraform/modules/app_service_plan/main.tf new file mode 100644 index 0000000..98a3e4d --- /dev/null +++ b/samples/web-app-custom-image/python/terraform/modules/app_service_plan/main.tf @@ -0,0 +1,25 @@ +resource "azurerm_service_plan" "example" { + name = var.name + resource_group_name = var.resource_group_name + location = var.location + sku_name = var.sku_name + os_type = var.os_type + zone_balancing_enabled = var.zone_balancing_enabled + tags = var.tags + + lifecycle { + ignore_changes = [ + tags + ] + } +} + +resource "azurerm_monitor_diagnostic_setting" "example" { + name = "DiagnosticsSettings" + target_resource_id = azurerm_service_plan.example.id + log_analytics_workspace_id = var.log_analytics_workspace_id + + enabled_metric { + category = "AllMetrics" + } +} \ No newline at end of file diff --git a/samples/web-app-custom-image/python/terraform/modules/app_service_plan/outputs.tf b/samples/web-app-custom-image/python/terraform/modules/app_service_plan/outputs.tf new file mode 100644 index 0000000..f1455ea --- /dev/null +++ b/samples/web-app-custom-image/python/terraform/modules/app_service_plan/outputs.tf @@ -0,0 +1,19 @@ +output "id" { + value = azurerm_service_plan.example.id + description = "Specifies the resource id of the App Service Plan" +} + +output "name" { + value = azurerm_service_plan.example.name + description = "Specifies the name of the App Service Plan" +} + +output "location" { + value = azurerm_service_plan.example.location + description = "Specifies the location of the App Service Plan" +} + +output "resource_group_name" { + value = azurerm_service_plan.example.resource_group_name + description = "Specifies the resource group name of the App Service Plan" +} diff --git a/samples/web-app-custom-image/python/terraform/modules/app_service_plan/variables.tf b/samples/web-app-custom-image/python/terraform/modules/app_service_plan/variables.tf new file mode 100644 index 0000000..e543066 --- /dev/null +++ b/samples/web-app-custom-image/python/terraform/modules/app_service_plan/variables.tf @@ -0,0 +1,42 @@ +variable "resource_group_name" { + description = "(Required) Specifies the name of the resource group." + type = string +} + +variable "location" { + description = "(Required) Specifies the location for the App Service Plan." + type = string +} + +variable "name" { + description = "(Required) Specifies the name of the App Service Plan." + type = string +} + +variable "sku_name" { + description = "(Required) Specifies the SKU name for the App Service Plan." + type = string +} + +variable "os_type" { + description = "(Required) Specifies the O/S type for the App Services to be hosted in this plan." + type = string + default = "Linux" +} + +variable "zone_balancing_enabled" { + description = "(Optional) Should the Service Plan balance across Availability Zones in the region." + type = bool + default = false +} + +variable "tags" { + description = "(Optional) Specifies the tags to be applied to the resources." + type = map(any) + default = {} +} + +variable "log_analytics_workspace_id" { + description = "Specifies the resource id of the Azure Log Analytics workspace." + type = string +} \ No newline at end of file diff --git a/samples/web-app-custom-image/python/terraform/modules/container_registry/main.tf b/samples/web-app-custom-image/python/terraform/modules/container_registry/main.tf new file mode 100644 index 0000000..8bbc17d --- /dev/null +++ b/samples/web-app-custom-image/python/terraform/modules/container_registry/main.tf @@ -0,0 +1,62 @@ +resource "azurerm_container_registry" "example" { + name = var.name + resource_group_name = var.resource_group_name + location = var.location + sku = var.sku + admin_enabled = var.admin_enabled + tags = var.tags + + identity { + type = "UserAssigned" + identity_ids = [ + azurerm_user_assigned_identity.identity.id + ] + } + + dynamic "georeplications" { + for_each = var.georeplication_locations + + content { + location = georeplications.value + tags = var.tags + } + } + + lifecycle { + ignore_changes = [ + tags + ] + } +} + +resource "azurerm_user_assigned_identity" "identity" { + resource_group_name = var.resource_group_name + location = var.location + tags = var.tags + + name = "${var.name}Identity" + + lifecycle { + ignore_changes = [ + tags + ] + } +} + +resource "azurerm_monitor_diagnostic_setting" "example" { + name = "DiagnosticsSettings" + target_resource_id = azurerm_container_registry.example.id + log_analytics_workspace_id = var.log_analytics_workspace_id + + enabled_log { + category = "ContainerRegistryRepositoryEvents" + } + + enabled_log { + category = "ContainerRegistryLoginEvents" + } + + enabled_metric { + category = "AllMetrics" + } +} \ No newline at end of file diff --git a/samples/web-app-custom-image/python/terraform/modules/container_registry/output.tf b/samples/web-app-custom-image/python/terraform/modules/container_registry/output.tf new file mode 100644 index 0000000..5e6fcd1 --- /dev/null +++ b/samples/web-app-custom-image/python/terraform/modules/container_registry/output.tf @@ -0,0 +1,34 @@ +output "name" { + description = "Specifies the name of the container registry." + value = azurerm_container_registry.example.name +} + +output "id" { + description = "Specifies the resource id of the container registry." + value = azurerm_container_registry.example.id +} + +output "resource_group_name" { + description = "Specifies the name of the resource group." + value = var.resource_group_name +} + +output "login_server" { + description = "Specifies the login server of the container registry." + value = azurerm_container_registry.example.login_server +} + +output "login_server_url" { + description = "Specifies the login server url of the container registry." + value = "https://${azurerm_container_registry.example.login_server}" +} + +output "admin_username" { + description = "Specifies the admin username of the container registry." + value = azurerm_container_registry.example.admin_username +} + +output "admin_password" { + description = "Specifies the admin password of the container registry." + value = azurerm_container_registry.example.admin_password +} \ No newline at end of file diff --git a/samples/web-app-custom-image/python/terraform/modules/container_registry/variables.tf b/samples/web-app-custom-image/python/terraform/modules/container_registry/variables.tf new file mode 100644 index 0000000..c3fc7cc --- /dev/null +++ b/samples/web-app-custom-image/python/terraform/modules/container_registry/variables.tf @@ -0,0 +1,60 @@ +variable "name" { + description = "(Required) Specifies the name of the Container Registry. Changing this forces a new resource to be created." + type = string +} + +variable "resource_group_name" { + description = "(Required) The name of the resource group in which to create the Container Registry. Changing this forces a new resource to be created." + type = string +} + +variable "location" { + description = "(Required) Specifies the supported Azure location where the resource exists. Changing this forces a new resource to be created." + type = string +} + +variable "admin_enabled" { + description = "(Optional) Specifies whether the admin user is enabled. Defaults to false." + type = bool + default = false +} + +variable "sku" { + description = "(Optional) The SKU name of the container registry. Possible values are Basic, Standard and Premium. Defaults to Basic" + type = string + default = "Basic" + + validation { + condition = contains(["Basic", "Standard", "Premium"], var.sku) + error_message = "The container registry sku is invalid." + } +} + +variable "tags" { + description = "(Optional) A mapping of tags to assign to the resource." + type = map(any) + default = {} +} + +variable "georeplication_locations" { + description = "(Optional) A list of Azure locations where the container registry should be geo-replicated." + type = list(string) + default = [] +} + +variable "log_analytics_workspace_id" { + description = "Specifies the resource id of the Azure Log Analytics workspace." + type = string +} + +variable "image_name" { + description = "(Required) Specifies the name of the container image to deploy to the Web App." + type = string + default = "custom-image-webapp" +} + +variable "image_tag" { + description = "(Required) Specifies the tag of the container image to deploy to the Web App." + type = string + default = "v1" +} \ No newline at end of file diff --git a/samples/web-app-custom-image/python/terraform/modules/log_analytics/main.tf b/samples/web-app-custom-image/python/terraform/modules/log_analytics/main.tf new file mode 100644 index 0000000..fcd4398 --- /dev/null +++ b/samples/web-app-custom-image/python/terraform/modules/log_analytics/main.tf @@ -0,0 +1,14 @@ +resource "azurerm_log_analytics_workspace" "example" { + name = var.name + location = var.location + resource_group_name = var.resource_group_name + sku = var.sku + tags = var.tags + retention_in_days = var.retention_in_days != null ? var.retention_in_days : null + + lifecycle { + ignore_changes = [ + tags + ] + } +} diff --git a/samples/web-app-custom-image/python/terraform/modules/log_analytics/output.tf b/samples/web-app-custom-image/python/terraform/modules/log_analytics/output.tf new file mode 100644 index 0000000..fe2c398 --- /dev/null +++ b/samples/web-app-custom-image/python/terraform/modules/log_analytics/output.tf @@ -0,0 +1,30 @@ +output "id" { + value = azurerm_log_analytics_workspace.example.id + description = "Specifies the resource id of the log analytics workspace" +} + +output "location" { + value = azurerm_log_analytics_workspace.example.location + description = "Specifies the location of the log analytics workspace" +} + +output "name" { + value = azurerm_log_analytics_workspace.example.name + description = "Specifies the name of the log analytics workspace" +} + +output "resource_group_name" { + value = azurerm_log_analytics_workspace.example.resource_group_name + description = "Specifies the name of the resource group that contains the log analytics workspace" +} + +output "workspace_id" { + value = azurerm_log_analytics_workspace.example.workspace_id + description = "Specifies the workspace id of the log analytics workspace" +} + +output "primary_shared_key" { + value = azurerm_log_analytics_workspace.example.primary_shared_key + description = "Specifies the workspace key of the log analytics workspace" + sensitive = true +} diff --git a/samples/web-app-custom-image/python/terraform/modules/log_analytics/variables.tf b/samples/web-app-custom-image/python/terraform/modules/log_analytics/variables.tf new file mode 100644 index 0000000..2db6a01 --- /dev/null +++ b/samples/web-app-custom-image/python/terraform/modules/log_analytics/variables.tf @@ -0,0 +1,37 @@ +variable "resource_group_name" { + description = "(Required) Specifies the name of the resource group." + type = string +} + +variable "location" { + description = "(Required) Specifies the location of the Azure Log Analytics workspace" + type = string +} + +variable "name" { + description = "(Required) Specifies the name of the Azure Log Analytics workspace" + type = string +} + +variable "sku" { + description = "(Optional) Specifies the sku of the Azure Log Analytics workspace" + type = string + default = "PerGB2018" + + validation { + condition = contains(["Free", "Standalone", "PerNode", "PerGB2018"], var.sku) + error_message = "The log analytics sku is incorrect." + } +} + +variable "tags" { + description = "(Optional) Specifies the tags of the Azure Log Analytics workspace." + type = map(any) + default = {} +} + +variable "retention_in_days" { + description = " (Optional) Specifies the workspace data retention in days. Possible values are either 7 (Free Tier only) or range between 30 and 730." + type = number + default = 30 +} diff --git a/samples/web-app-custom-image/python/terraform/modules/managed_identity/main.tf b/samples/web-app-custom-image/python/terraform/modules/managed_identity/main.tf new file mode 100644 index 0000000..95f89a8 --- /dev/null +++ b/samples/web-app-custom-image/python/terraform/modules/managed_identity/main.tf @@ -0,0 +1,20 @@ + +resource "azurerm_user_assigned_identity" "example" { + name = var.name + resource_group_name = var.resource_group_name + location = var.location + tags = var.tags + + lifecycle { + ignore_changes = [ + tags + ] + } +} + +resource "azurerm_role_assignment" "example" { + scope = var.acr_id + role_definition_name = "AcrPull" + principal_id = azurerm_user_assigned_identity.example.principal_id + skip_service_principal_aad_check = true +} diff --git a/samples/web-app-custom-image/python/terraform/modules/managed_identity/output.tf b/samples/web-app-custom-image/python/terraform/modules/managed_identity/output.tf new file mode 100644 index 0000000..a22e571 --- /dev/null +++ b/samples/web-app-custom-image/python/terraform/modules/managed_identity/output.tf @@ -0,0 +1,25 @@ + +output "id" { + value = azurerm_user_assigned_identity.example.id + description = "Specifies the resource id of the workload user-defined managed identity" +} + +output "location" { + value = azurerm_user_assigned_identity.example.location + description = "Specifies the location of the workload user-defined managed identity" +} + +output "name" { + value = azurerm_user_assigned_identity.example.name + description = "Specifies the name of the workload user-defined managed identity" +} + +output "client_id" { + value = azurerm_user_assigned_identity.example.client_id + description = "Specifies the client id of the workload user-defined managed identity" +} + +output "principal_id" { + value = azurerm_user_assigned_identity.example.principal_id + description = "Specifies the principal id of the workload user-defined managed identity" +} \ No newline at end of file diff --git a/samples/web-app-custom-image/python/terraform/modules/managed_identity/variables.tf b/samples/web-app-custom-image/python/terraform/modules/managed_identity/variables.tf new file mode 100644 index 0000000..5da194a --- /dev/null +++ b/samples/web-app-custom-image/python/terraform/modules/managed_identity/variables.tf @@ -0,0 +1,26 @@ +variable "name" { + description = "(Required) Specifies the name of the log analytics workspace" + type = string +} + +variable "resource_group_name" { + description = "(Required) Specifies the resource group name" + type = string +} + +variable "location" { + description = "(Required) Specifies the location of the log analytics workspace" + type = string +} + +variable "tags" { + description = "(Optional) Specifies the tags of the log analytics workspace" + type = map(any) + default = {} +} + +variable "acr_id" { + description = "(Required) Specifies resource id of the Azure Container Registry resource" + type = string +} + diff --git a/samples/web-app-custom-image/python/terraform/modules/nat_gateway/main.tf b/samples/web-app-custom-image/python/terraform/modules/nat_gateway/main.tf new file mode 100644 index 0000000..0dea868 --- /dev/null +++ b/samples/web-app-custom-image/python/terraform/modules/nat_gateway/main.tf @@ -0,0 +1,42 @@ +resource "azurerm_public_ip_prefix" "example" { + name = var.public_ip_prefix_name + location = var.location + resource_group_name = var.resource_group_name + sku = var.sku_name + zones = var.zones + tags = var.tags + prefix_length = var.public_ip_prefix_length + + lifecycle { + ignore_changes = [ + tags + ] + } +} + +resource "azurerm_nat_gateway" "example" { + name = var.name + location = var.location + resource_group_name = var.resource_group_name + sku_name = var.sku_name + idle_timeout_in_minutes = var.idle_timeout_in_minutes + zones = var.zones + tags = var.tags + + lifecycle { + ignore_changes = [ + tags + ] + } +} + +resource "azurerm_nat_gateway_public_ip_prefix_association" "example" { + nat_gateway_id = azurerm_nat_gateway.example.id + public_ip_prefix_id = azurerm_public_ip_prefix.example.id +} + +resource "azurerm_subnet_nat_gateway_association" "example" { + for_each = var.subnet_ids + subnet_id = each.value + nat_gateway_id = azurerm_nat_gateway.example.id +} diff --git a/samples/web-app-custom-image/python/terraform/modules/nat_gateway/output.tf b/samples/web-app-custom-image/python/terraform/modules/nat_gateway/output.tf new file mode 100644 index 0000000..3f5d284 --- /dev/null +++ b/samples/web-app-custom-image/python/terraform/modules/nat_gateway/output.tf @@ -0,0 +1,9 @@ +output "name" { + value = azurerm_nat_gateway.example.name + description = "Specifies the name of the Azure NAT Gateway" +} + +output "id" { + value = azurerm_nat_gateway.example.id + description = "Specifies the resource id of the Azure NAT Gateway" +} diff --git a/samples/web-app-custom-image/python/terraform/modules/nat_gateway/variables.tf b/samples/web-app-custom-image/python/terraform/modules/nat_gateway/variables.tf new file mode 100644 index 0000000..a6b8e69 --- /dev/null +++ b/samples/web-app-custom-image/python/terraform/modules/nat_gateway/variables.tf @@ -0,0 +1,55 @@ +variable "resource_group_name" { + description = "(Required) Specifies the name of the resource group." + type = string +} + +variable "location" { + description = "(Required) Specifies the location of the Azure NAT Gateway" + type = string +} + +variable "name" { + description = "(Required) Specifies the name of the Azure NAT Gateway" + type = string +} + +variable "tags" { + description = "(Optional) Specifies the tags of the Azure NAT Gateway" + type = map(any) + default = {} +} + +variable "sku_name" { + description = "(Optional) The SKU which should be used. At this time the only supported value is Standard. Defaults to Standard" + type = string + default = "Standard" +} + +variable "idle_timeout_in_minutes" { + description = "(Optional) The idle timeout which should be used in minutes. Defaults to 4." + type = number + default = 4 +} + +variable "zones" { + description = " (Optional) A list of Availability Zones in which this NAT Gateway should be located. Changing this forces a new NAT Gateway to be created." + type = list(string) + default = [] +} + +variable "subnet_ids" { + description = "(Required) A map of subnet ids to associate with the NAT Gateway" + type = map(string) +} + +variable "public_ip_prefix_name" { + description = "(Required) The name of the public IP prefix to create and associate with the NAT Gateway." + type = string + default = null +} + +variable "public_ip_prefix_length" { + description = "(Required) The length of the public IP prefix to create and associate with the NAT Gateway. Must be between 28 and 31." + type = number + default = 31 +} \ No newline at end of file diff --git a/samples/web-app-custom-image/python/terraform/modules/network_security_group/main.tf b/samples/web-app-custom-image/python/terraform/modules/network_security_group/main.tf new file mode 100644 index 0000000..c649652 --- /dev/null +++ b/samples/web-app-custom-image/python/terraform/modules/network_security_group/main.tf @@ -0,0 +1,53 @@ +resource "azurerm_network_security_group" "example" { + name = var.name + resource_group_name = var.resource_group_name + location = var.location + tags = var.tags + + dynamic "security_rule" { + for_each = try(var.security_rules, []) + content { + name = try(security_rule.value.name, null) + priority = try(security_rule.value.priority, null) + direction = try(security_rule.value.direction, null) + access = try(security_rule.value.access, null) + protocol = try(security_rule.value.protocol, null) + source_port_range = try(security_rule.value.source_port_range, null) + source_port_ranges = try(security_rule.value.source_port_ranges, null) + destination_port_range = try(security_rule.value.destination_port_range, null) + destination_port_ranges = try(security_rule.value.destination_port_ranges, null) + source_address_prefix = try(security_rule.value.source_address_prefix, null) + source_address_prefixes = try(security_rule.value.source_address_prefixes, null) + destination_address_prefix = try(security_rule.value.destination_address_prefix, null) + destination_address_prefixes = try(security_rule.value.destination_address_prefixes, null) + source_application_security_group_ids = try(security_rule.value.source_application_security_group_ids, null) + destination_application_security_group_ids = try(security_rule.value.destination_application_security_group_ids, null) + } + } + + lifecycle { + ignore_changes = [ + tags + ] + } +} + +resource "azurerm_subnet_network_security_group_association" "example" { + for_each = var.subnet_ids + subnet_id = each.value + network_security_group_id = azurerm_network_security_group.example.id +} + +resource "azurerm_monitor_diagnostic_setting" "settings" { + name = "DiagnosticsSettings" + target_resource_id = azurerm_network_security_group.example.id + log_analytics_workspace_id = var.log_analytics_workspace_id + + enabled_log { + category = "NetworkSecurityGroupEvent" + } + + enabled_log { + category = "NetworkSecurityGroupRuleCounter" + } +} diff --git a/samples/web-app-custom-image/python/terraform/modules/network_security_group/outputs.tf b/samples/web-app-custom-image/python/terraform/modules/network_security_group/outputs.tf new file mode 100644 index 0000000..b8ca8d5 --- /dev/null +++ b/samples/web-app-custom-image/python/terraform/modules/network_security_group/outputs.tf @@ -0,0 +1,9 @@ +output "name" { + description = "Specifies the name of the network security group" + value = azurerm_network_security_group.example.name +} + +output "id" { + description = "Specifies the resource id of the network security group" + value = azurerm_network_security_group.example.id +} diff --git a/samples/web-app-custom-image/python/terraform/modules/network_security_group/variables.tf b/samples/web-app-custom-image/python/terraform/modules/network_security_group/variables.tf new file mode 100644 index 0000000..04eb07e --- /dev/null +++ b/samples/web-app-custom-image/python/terraform/modules/network_security_group/variables.tf @@ -0,0 +1,51 @@ +variable "name" { + description = "(Required) Specifies the name of the Azure Network Security Group" + type = string +} + +variable "resource_group_name" { + description = "(Required) Specifies the name of the resource group. of the Azure Network Security Group" + type = string +} + +variable "location" { + description = "(Required) Specifies the location of the Azure Network Security Group" + type = string +} + +variable "security_rules" { + description = "(Optional) Specifies the security rules of the Azure Network Security Group" + type = list(object({ + name = string + priority = number + direction = string + access = string + protocol = string + source_port_range = string + source_port_ranges = list(string) + destination_port_range = string + destination_port_ranges = list(string) + source_address_prefix = string + source_address_prefixes = list(string) + destination_address_prefix = string + destination_address_prefixes = list(string) + source_application_security_group_ids = list(string) + destination_application_security_group_ids = list(string) + })) + default = [] +} + +variable "subnet_ids" { + description = "(Required) A map of subnet ids to associate with the Azure Network Security Group" + type = map(string) +} + +variable "tags" { + description = "(Optional) Specifies the tags of the Azure Network Security Group" + default = {} +} + +variable "log_analytics_workspace_id" { + description = "Specifies the resource id of the Azure Log Analytics workspace" + type = string +} diff --git a/samples/web-app-custom-image/python/terraform/modules/private_dns_zone/main.tf b/samples/web-app-custom-image/python/terraform/modules/private_dns_zone/main.tf new file mode 100644 index 0000000..393f9dc --- /dev/null +++ b/samples/web-app-custom-image/python/terraform/modules/private_dns_zone/main.tf @@ -0,0 +1,26 @@ +resource "azurerm_private_dns_zone" "example" { + name = var.name + resource_group_name = var.resource_group_name + tags = var.tags + + lifecycle { + ignore_changes = [ + tags + ] + } +} + +resource "azurerm_private_dns_zone_virtual_network_link" "example" { + for_each = var.virtual_networks_to_link + + name = "link_to_${lower(basename(each.key))}" + resource_group_name = var.resource_group_name + private_dns_zone_name = azurerm_private_dns_zone.example.name + virtual_network_id = "/subscriptions/${each.value.subscription_id}/resourceGroups/${each.value.resource_group_name}/providers/Microsoft.Network/virtualNetworks/${each.key}" + + lifecycle { + ignore_changes = [ + tags + ] + } +} diff --git a/samples/web-app-custom-image/python/terraform/modules/private_dns_zone/outputs.tf b/samples/web-app-custom-image/python/terraform/modules/private_dns_zone/outputs.tf new file mode 100644 index 0000000..ca141f3 --- /dev/null +++ b/samples/web-app-custom-image/python/terraform/modules/private_dns_zone/outputs.tf @@ -0,0 +1,9 @@ +output "name" { + description = "Specifies the name of the private dns zone" + value = azurerm_private_dns_zone.example.name +} + +output "id" { + description = "Specifies the resource id of the private dns zone" + value = azurerm_private_dns_zone.example.id +} diff --git a/samples/web-app-custom-image/python/terraform/modules/private_dns_zone/variables.tf b/samples/web-app-custom-image/python/terraform/modules/private_dns_zone/variables.tf new file mode 100644 index 0000000..8d0c0cc --- /dev/null +++ b/samples/web-app-custom-image/python/terraform/modules/private_dns_zone/variables.tf @@ -0,0 +1,20 @@ +variable "name" { + description = "(Required) Specifies the name of the Azure Private DNS Zone" + type = string +} + +variable "resource_group_name" { + description = "(Required) Specifies the name of the resource group. of the Azure Private DNS Zone" + type = string +} + +variable "tags" { + description = "(Optional) Specifies the tags of the Azure Private DNS Zone" + default = {} +} + +variable "virtual_networks_to_link" { + description = "(Optional) Specifies the subscription id, resource group name, and name of the virtual networks to which create a virtual network link" + type = map(any) + default = {} +} diff --git a/samples/web-app-custom-image/python/terraform/modules/private_endpoint/main.tf b/samples/web-app-custom-image/python/terraform/modules/private_endpoint/main.tf new file mode 100644 index 0000000..b21566e --- /dev/null +++ b/samples/web-app-custom-image/python/terraform/modules/private_endpoint/main.tf @@ -0,0 +1,26 @@ +resource "azurerm_private_endpoint" "example" { + name = var.name + location = var.location + resource_group_name = var.resource_group_name + subnet_id = var.subnet_id + tags = var.tags + + private_service_connection { + name = "${var.name}Connection" + private_connection_resource_id = var.private_connection_resource_id + is_manual_connection = var.is_manual_connection + subresource_names = var.subresource_name != null ? [var.subresource_name] : null + request_message = try(var.request_message, null) + } + + private_dns_zone_group { + name = var.private_dns_zone_group_name + private_dns_zone_ids = var.private_dns_zone_group_ids + } + + lifecycle { + ignore_changes = [ + tags + ] + } +} diff --git a/samples/web-app-custom-image/python/terraform/modules/private_endpoint/outputs.tf b/samples/web-app-custom-image/python/terraform/modules/private_endpoint/outputs.tf new file mode 100644 index 0000000..367ab51 --- /dev/null +++ b/samples/web-app-custom-image/python/terraform/modules/private_endpoint/outputs.tf @@ -0,0 +1,19 @@ +output "name" { + description = "Specifies the name of the private endpoint." + value = azurerm_private_endpoint.example.name +} + +output "id" { + description = "Specifies the resource id of the private endpoint." + value = azurerm_private_endpoint.example.id +} + +output "private_dns_zone_group" { + description = "Specifies the private dns zone group of the private endpoint." + value = azurerm_private_endpoint.example.private_dns_zone_group +} + +output "private_dns_zone_configs" { + description = "Specifies the private dns zone(s) configuration" + value = azurerm_private_endpoint.example.private_dns_zone_configs +} diff --git a/samples/web-app-custom-image/python/terraform/modules/private_endpoint/variables.tf b/samples/web-app-custom-image/python/terraform/modules/private_endpoint/variables.tf new file mode 100644 index 0000000..ca1cde1 --- /dev/null +++ b/samples/web-app-custom-image/python/terraform/modules/private_endpoint/variables.tf @@ -0,0 +1,61 @@ +variable "name" { + description = "(Required) Specifies the name of the Azure Private Endpoint. Changing this forces a new resource to be created." + type = string +} + +variable "resource_group_name" { + description = "(Required) The name of the resource group. Changing this forces a new resource to be created." + type = string +} + +variable "private_connection_resource_id" { + description = "(Required) Specifies the resource id of the private link service" + type = string +} + +variable "location" { + description = "(Required) Specifies the supported Azure location where the resource exists. Changing this forces a new resource to be created." + type = string +} + +variable "subnet_id" { + description = "(Required) Specifies the resource id of the subnet" + type = string +} + +variable "is_manual_connection" { + description = "(Optional) Specifies whether the Azure Private Endpoint connection requires manual approval from the remote resource owner." + type = bool + default = false +} + +variable "subresource_name" { + description = "(Optional) Specifies a subresource name which the Azure Private Endpoint is able to connect to." + type = string + default = null +} + +variable "request_message" { + description = "(Optional) Specifies a message passed to the owner of the remote resource when the Azure Private Endpoint attempts to establish the connection to the remote resource." + type = string + default = null +} + +variable "private_dns_zone_group_name" { + description = "(Required) Specifies the Name of the Private DNS Zone Group. Changing this forces a new private_dns_zone_group resource to be created." + type = string +} + +variable "private_dns_zone_group_ids" { + description = "(Required) Specifies the list of Private DNS Zones to include within the private_dns_zone_group." + type = list(string) +} + +variable "tags" { + description = "(Optional) Specifies the tags of the Azure Azure Private Endpoint." + default = {} +} + +variable "private_dns" { + default = {} +} diff --git a/samples/web-app-custom-image/python/terraform/modules/virtual_network/main.tf b/samples/web-app-custom-image/python/terraform/modules/virtual_network/main.tf new file mode 100644 index 0000000..cec00f4 --- /dev/null +++ b/samples/web-app-custom-image/python/terraform/modules/virtual_network/main.tf @@ -0,0 +1,55 @@ +resource "azurerm_virtual_network" "example" { + name = var.vnet_name + address_space = var.address_space + location = var.location + resource_group_name = var.resource_group_name + tags = var.tags + + lifecycle { + ignore_changes = [ + tags + ] + } +} + +resource "azurerm_subnet" "example" { + for_each = { for subnet in var.subnets : subnet.name => subnet if subnet != null } + + name = each.key + resource_group_name = var.resource_group_name + virtual_network_name = azurerm_virtual_network.example.name + address_prefixes = each.value.address_prefixes + private_endpoint_network_policies = each.value.private_endpoint_network_policies + private_link_service_network_policies_enabled = each.value.private_link_service_network_policies_enabled + + dynamic "delegation" { + for_each = each.value.delegation != null ? [each.value.delegation] : [] + content { + name = "delegation" + + service_delegation { + name = delegation.value + } + } + } + + lifecycle { + ignore_changes = [ + delegation + ] + } +} + +resource "azurerm_monitor_diagnostic_setting" "example" { + name = "DiagnosticsSettings" + target_resource_id = azurerm_virtual_network.example.id + log_analytics_workspace_id = var.log_analytics_workspace_id + + enabled_log { + category = "VMProtectionAlerts" + } + + enabled_metric { + category = "AllMetrics" + } +} diff --git a/samples/web-app-custom-image/python/terraform/modules/virtual_network/outputs.tf b/samples/web-app-custom-image/python/terraform/modules/virtual_network/outputs.tf new file mode 100644 index 0000000..b464308 --- /dev/null +++ b/samples/web-app-custom-image/python/terraform/modules/virtual_network/outputs.tf @@ -0,0 +1,19 @@ +output "name" { + description = "Specifies the name of the virtual network" + value = azurerm_virtual_network.example.name +} + +output "vnet_id" { + description = "Specifies the resource id of the virtual network" + value = azurerm_virtual_network.example.id +} + +output "subnet_ids" { + description = "Contains a list of the the resource id of the subnets" + value = { for subnet in azurerm_subnet.example : subnet.name => subnet.id } +} + +output "subnet_ids_as_list" { + description = "Returns the list of the subnet ids as a list of strings." + value = [for subnet in azurerm_subnet.example : subnet.id] +} diff --git a/samples/web-app-custom-image/python/terraform/modules/virtual_network/variables.tf b/samples/web-app-custom-image/python/terraform/modules/virtual_network/variables.tf new file mode 100644 index 0000000..f8c0b0e --- /dev/null +++ b/samples/web-app-custom-image/python/terraform/modules/virtual_network/variables.tf @@ -0,0 +1,40 @@ +variable "resource_group_name" { + description = "Resource Group name" + type = string +} + +variable "location" { + description = "Location in which to deploy the network" + type = string +} + +variable "vnet_name" { + description = "VNET name" + type = string +} + +variable "address_space" { + description = "VNET address space" + type = list(string) +} + +variable "subnets" { + description = "Subnets configuration" + type = list(object({ + name = string + address_prefixes = list(string) + private_endpoint_network_policies = string + private_link_service_network_policies_enabled = bool + delegation = string + })) +} + +variable "tags" { + description = "(Optional) Specifies the tags of the Azure Virtual Network resource." + default = {} +} + +variable "log_analytics_workspace_id" { + description = "Specifies the resource id of the Azure Log Analytics workspace." + type = string +} diff --git a/samples/web-app-custom-image/python/terraform/modules/web_app/main.tf b/samples/web-app-custom-image/python/terraform/modules/web_app/main.tf new file mode 100644 index 0000000..ad2ec1a --- /dev/null +++ b/samples/web-app-custom-image/python/terraform/modules/web_app/main.tf @@ -0,0 +1,88 @@ +resource "azurerm_linux_web_app" "example" { + name = var.name + resource_group_name = var.resource_group_name + location = var.location + service_plan_id = var.service_plan_id + https_only = var.https_only + virtual_network_subnet_id = var.virtual_network_subnet_id + public_network_access_enabled = var.public_network_access_enabled + client_affinity_enabled = false + tags = var.tags + + dynamic "identity" { + for_each = var.managed_identity_id != null ? [1] : [] + content { + type = "UserAssigned" + identity_ids = [var.managed_identity_id] + } + } + + site_config { + always_on = var.always_on + http2_enabled = var.http2_enabled + minimum_tls_version = var.minimum_tls_version + vnet_route_all_enabled = var.vnet_route_all_enabled + application_stack { + docker_image_name = "${var.image_name}:${var.image_tag}" + docker_registry_url = var.docker_registry_url + docker_registry_username = var.docker_registry_username + docker_registry_password = var.docker_registry_password + } + } + + app_settings = var.app_settings + + lifecycle { + ignore_changes = [ + tags + ] + } +} + +# Deploy code from a public GitHub repo +resource "azurerm_app_service_source_control" "example" { + count = var.repo_url == "" ? 0 : 1 + app_id = azurerm_linux_web_app.example.id + repo_url = var.repo_url + branch = var.repo_branch + use_manual_integration = true + use_mercurial = false +} + +resource "azurerm_monitor_diagnostic_setting" "example" { + name = "DiagnosticsSettings" + target_resource_id = azurerm_linux_web_app.example.id + log_analytics_workspace_id = var.log_analytics_workspace_id + + enabled_log { + category = "AppServiceHTTPLogs" + } + + enabled_log { + category = "AppServiceConsoleLogs" + } + + enabled_log { + category = "AppServiceAppLogs" + } + + enabled_log { + category = "AppServiceAuditLogs" + } + + enabled_log { + category = "AppServiceIPSecAuditLogs" + } + + enabled_log { + category = "AppServicePlatformLogs" + } + + enabled_log { + category = "AppServiceAuthenticationLogs" + } + + enabled_metric { + category = "AllMetrics" + } +} \ No newline at end of file diff --git a/samples/web-app-custom-image/python/terraform/modules/web_app/outputs.tf b/samples/web-app-custom-image/python/terraform/modules/web_app/outputs.tf new file mode 100644 index 0000000..d7b6981 --- /dev/null +++ b/samples/web-app-custom-image/python/terraform/modules/web_app/outputs.tf @@ -0,0 +1,24 @@ +output "id" { + value = azurerm_linux_web_app.example.id + description = "Specifies the resource id of the Web App" +} + +output "name" { + value = azurerm_linux_web_app.example.name + description = "Specifies the name of the Web App" +} + +output "default_hostname" { + value = azurerm_linux_web_app.example.default_hostname + description = "Specifies the default hostname of the Web App" +} + +output "outbound_ip_addresses" { + value = azurerm_linux_web_app.example.outbound_ip_addresses + description = "Specifies the outbound IP addresses of the Web App" +} + +output "principal_id" { + value = azurerm_linux_web_app.example.identity[0].principal_id + description = "Specifies the Principal ID of the System Assigned Managed Identity" +} diff --git a/samples/web-app-custom-image/python/terraform/modules/web_app/variables.tf b/samples/web-app-custom-image/python/terraform/modules/web_app/variables.tf new file mode 100644 index 0000000..be77f72 --- /dev/null +++ b/samples/web-app-custom-image/python/terraform/modules/web_app/variables.tf @@ -0,0 +1,126 @@ +variable "resource_group_name" { + description = "(Required) Specifies the name of the resource group." + type = string +} + +variable "location" { + description = "(Required) Specifies the location for the Web App." + type = string +} + +variable "name" { + description = "(Required) Specifies the name of the Web App." + type = string +} + +variable "service_plan_id" { + description = "(Required) Specifies the ID of the App Service Plan within which to create this Web App." + type = string +} + +variable "https_only" { + description = "(Optional) Specifies whether the Web App requires HTTPS connections." + type = bool + default = false +} + +variable "virtual_network_subnet_id" { + description = "(Optional) The subnet id which will be used by this Web App for regional virtual network integration." + type = string + default = null +} + +variable "vnet_route_all_enabled" { + description = "(Optional) Specifies whether to route all traffic from the Web App into the virtual network. This is only applicable if virtual_network_subnet_id is specified. Defaults to false." + type = bool + default = false +} + +variable "public_network_access_enabled" { + description = "(Optional) Specifies whether the public network access is enabled or disabled." + type = bool + default = true +} + +variable "always_on" { + description = "(Optional) Specifies whether the Web App is Always On enabled." + type = bool + default = true +} + +variable "http2_enabled" { + description = "(Optional) Specifies whether HTTP/2 is enabled for the Web App." + type = bool + default = false +} + +variable "minimum_tls_version" { + description = "(Optional) Specifies the minimum version of TLS required for SSL requests." + type = string + default = "1.2" +} + +variable "app_settings" { + description = "(Optional) A map of key-value pairs for App Settings." + type = map(string) + default = {} +} + +variable "repo_url" { + description = "(Optional) Specifies the Git repository URL." + type = string + default = "" +} + +variable "repo_branch" { + description = "(Optional) Specifies the Git repository branch." + type = string + default = "main" +} + +variable "tags" { + description = "(Optional) Specifies the tags to be applied to the resources." + type = map(any) + default = {} +} + +variable "log_analytics_workspace_id" { + description = "Specifies the resource id of the Azure Log Analytics workspace." + type = string +} + +variable "image_name" { + description = "(Required) Specifies the name of the container image to deploy to the Web App." + type = string + default = "custom-image-webapp" +} + +variable "image_tag" { + description = "(Required) Specifies the tag of the container image to deploy to the Web App." + type = string + default = "v1" +} + +variable "docker_registry_url" { + description = "(Optional) Specifies the URL of the Docker registry where the container image is stored. This is required if the container image is stored in a private registry." + type = string + default = null +} + +variable "managed_identity_id" { + description = "(Optional) Specifies the ID of the user-assigned managed identity to be assigned to the Web App." + type = string + default = null +} + +variable "docker_registry_username" { + description = "Specifies the username of the Docker registry. This is required if the container image is stored in a private registry." + type = string + default = null +} + +variable "docker_registry_password" { + description = "Specifies the password of the Docker registry. This is required if the container image is stored in a private registry." + type = string + default = null +} \ No newline at end of file diff --git a/samples/web-app-custom-image/python/terraform/outputs.tf b/samples/web-app-custom-image/python/terraform/outputs.tf new file mode 100644 index 0000000..a4d9b25 --- /dev/null +++ b/samples/web-app-custom-image/python/terraform/outputs.tf @@ -0,0 +1,23 @@ +output "resource_group_name" { + value = local.resource_group_name +} + +output "container_registry_name" { + value = module.container_registry.name +} + +output "container_registry_login_server" { + value = module.container_registry.login_server +} + +output "app_service_plan_name" { + value = module.app_service_plan.name +} + +output "web_app_name" { + value = module.web_app.name +} + +output "web_app_url" { + value = module.web_app.default_hostname +} \ No newline at end of file diff --git a/samples/web-app-custom-image/python/terraform/providers.tf b/samples/web-app-custom-image/python/terraform/providers.tf new file mode 100644 index 0000000..e9f72ea --- /dev/null +++ b/samples/web-app-custom-image/python/terraform/providers.tf @@ -0,0 +1,28 @@ +terraform { + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "=4.60.0" + } + null = { + source = "hashicorp/null" + version = "~> 3.0" + } + } +} + +provider "azurerm" { + features { + resource_group { + prevent_deletion_if_contains_resources = false + } + } + + # Set the hostname of the Azure Metadata Service (for example management.azure.com) + # used to obtain the Cloud Environment when using LocalStack's Azure emulator. + # This allows the provider to correctly identify the environment and avoid making calls to the real Azure endpoints. + metadata_host = "localhost.localstack.cloud:4566" + + # Set the subscription ID to a dummy value when using LocalStack's Azure emulator. + subscription_id = "00000000-0000-0000-0000-000000000000" +} \ No newline at end of file diff --git a/samples/web-app-custom-image/python/terraform/push_image.sh b/samples/web-app-custom-image/python/terraform/push_image.sh new file mode 100755 index 0000000..a86add2 --- /dev/null +++ b/samples/web-app-custom-image/python/terraform/push_image.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +LOCAL_IMAGE="${IMAGE_NAME}:${IMAGE_TAG}" +FULL_IMAGE="${ACR_LOGIN_SERVER}/${IMAGE_NAME}:${IMAGE_TAG}" + +echo "Logging into Azure Container Registry [$ACR_NAME]..." +az acr login --name "$ACR_NAME" --only-show-errors + +if [ $? -eq 0 ]; then + echo "Logged into Azure Container Registry [$ACR_NAME] successfully." +else + echo "Failed to log into Azure Container Registry [$ACR_NAME]." + exit 1 +fi + +echo "Building custom Docker image [$LOCAL_IMAGE]..." +docker build -t "$LOCAL_IMAGE" ../src/ + +if [ $? -eq 0 ]; then + echo "Docker image [$LOCAL_IMAGE] built successfully." +else + echo "Failed to build Docker image [$LOCAL_IMAGE]." + exit 1 +fi + +echo "Tagging Docker image [$LOCAL_IMAGE] as [$FULL_IMAGE]..." +docker tag "$LOCAL_IMAGE" "$FULL_IMAGE" + +if [ $? -eq 0 ]; then + echo "Docker image [$LOCAL_IMAGE] tagged as [$FULL_IMAGE] successfully." +else + echo "Failed to tag Docker image [$LOCAL_IMAGE] as [$FULL_IMAGE]." + exit 1 +fi + +echo "Pushing image [$FULL_IMAGE] to ACR..." +docker push "$FULL_IMAGE" + +if [ $? -eq 0 ]; then + echo "Docker image [$FULL_IMAGE] pushed to ACR successfully." +else + echo "Failed to push Docker image [$FULL_IMAGE] to ACR." + exit 1 +fi \ No newline at end of file diff --git a/samples/web-app-custom-image/python/terraform/terraform.tfvars b/samples/web-app-custom-image/python/terraform/terraform.tfvars new file mode 100644 index 0000000..919af4f --- /dev/null +++ b/samples/web-app-custom-image/python/terraform/terraform.tfvars @@ -0,0 +1,3 @@ +prefix = "local" +suffix = "test" +location = "westeurope" \ No newline at end of file diff --git a/samples/web-app-custom-image/python/terraform/variables.tf b/samples/web-app-custom-image/python/terraform/variables.tf new file mode 100644 index 0000000..598df33 --- /dev/null +++ b/samples/web-app-custom-image/python/terraform/variables.tf @@ -0,0 +1,280 @@ +variable "prefix" { + description = "(Optional) Specifies the prefix for the name of the Azure resources." + type = string + default = "local" + + validation { + condition = var.prefix == null || length(var.prefix) >= 2 + error_message = "The prefix must be at least 2 characters long." + } +} + +variable "suffix" { + description = "(Optional) Specifies the suffix for the name of the Azure resources." + type = string + default = "test" + + validation { + condition = var.suffix == null || length(var.suffix) >= 2 + error_message = "The suffix must be at least 2 characters long." + } +} + +variable "location" { + description = "(Required) Specifies the location for all resources." + type = string + default = "westeurope" +} + +variable "acr_sku" { + description = "Specifies the name of the container registry" + type = string + default = "Premium" + + validation { + condition = contains(["Basic", "Standard", "Premium"], var.acr_sku) + error_message = "The container registry sku is invalid." + } +} + +variable "acr_admin_enabled" { + description = "Specifies whether admin is enabled for the container registry" + type = bool + default = true +} + +variable "acr_georeplication_locations" { + description = "(Optional) A list of Azure locations where the container registry should be geo-replicated." + type = list(string) + default = [] +} + +variable "os_type" { + description = "(Required) Specifies the O/S type for the App Services to be hosted in this plan. Possible values include Windows, Linux, and WindowsContainer. Changing this forces a new resource to be created." + type = string + default = "Linux" + + validation { + condition = contains([ + "Windows", + "Linux", + "WindowsContainer" + ], var.os_type) + error_message = "The os_type must be either 'Windows', 'Linux', or 'WindowsContainer'." + } +} + +variable "zone_balancing_enabled" { + description = "(Optional) Should the Service Plan balance across Availability Zones in the region." + type = bool + default = false +} + +variable "sku_tier" { + description = "(Optional) Specifies the tier name for the hosting plan." + type = string + default = "Standard" + + validation { + condition = contains([ + "Basic", + "Standard", + "ElasticPremium", + "Premium", + "PremiumV2", + "Premium0V3", + "PremiumV3", + "PremiumMV3", + "Isolated", + "IsolatedV2", + "WorkflowStandard", + "FlexConsumption" + ], var.sku_tier) + error_message = "The sku_tier must be one of the allowed values." + } +} +variable "sku_name" { + description = "(Optional) Specifies the SKU name for the hosting plan." + type = string + default = "S1" + + validation { + condition = contains([ + "B1", "B2", "B3", + "S1", "S2", "S3", + "EP1", "EP2", "EP3", + "P1", "P2", "P3", + "P1V2", "P2V2", "P3V2", + "P0V3", "P1V3", "P2V3", "P3V3", + "P1MV3", "P2MV3", "P3MV3", "P4MV3", "P5MV3", + "I1", "I2", "I3", + "I1V2", "I2V2", "I3V2", "I4V2", "I5V2", "I6V2", + "WS1", "WS2", "WS3", + "FC1" + ], var.sku_name) + error_message = "The sku_name must be one of the allowed values." + } +} + +variable "python_version" { + description = "(Optional) Specifies the version of Python to run. Possible values include 3.13, 3.12, 3.11, 3.10, 3.9, 3.8 and 3.7." + type = string + default = "3.12" + + validation { + condition = contains([ + "3.13", + "3.12", + "3.11", + "3.10", + "3.9", + "3.8", + "3.7" + ], var.python_version) + error_message = "The python_version must be one of the supported versions: 3.13, 3.12, 3.11, 3.10, 3.9, 3.8, 3.7." + } +} + +variable "https_only" { + description = "(Optional) Specifies whether the Linux Web App require HTTPS connections. Defaults to false." + type = bool + default = false +} + +variable "minimum_tls_version" { + description = "(Optional) Specifies the minimum version of TLS required for SSL requests. Possible values include: 1.0, 1.1, 1.2 and 1.3. Defaults to 1.2." + type = string + default = "1.2" + + validation { + condition = contains([ + "1.0", + "1.1", + "1.2", + "1.3" + ], var.minimum_tls_version) + error_message = "The minimum_tls_version must be one of the allowed values." + } +} + +variable "always_on" { + description = "(Optional) Specifies whether the Linux Web App is Always On enabled. Defaults to true." + type = bool + default = true +} + +variable "http2_enabled" { + description = "(Optional) Specifies whether HTTP/2 is enabled for the Linux Web App." + type = bool + default = false +} + +variable "public_network_access_enabled" { + description = "(Optional) Specifies whether the public network access is enabled or disabled." + type = bool + default = true +} + +variable "repo_url" { + description = "(Optional) Specifies the Git repository URL." + type = string + default = "" + + validation { + condition = var.repo_url == "" || can(regex("^https?://", var.repo_url)) + error_message = "The repo_url must be empty or a valid HTTP/HTTPS URL." + } +} + +variable "login_name" { + description = "(Required) Specifies the login name for the application." + type = string + default = "paolo" +} + +variable "tags" { + description = "(Optional) Specifies the tags to be applied to the resources." + type = map(string) + default = { + environment = "test" + iac = "terraform" + } +} + +variable "vnet_name" { + description = "Specifies the name of the virtual network." + default = "VNet" + type = string +} + +variable "vnet_address_space" { + description = "Specifies the address space of the virtual network." + default = ["10.0.0.0/8"] + type = list(string) +} + +variable "webapp_subnet_name" { + description = "Specifies the name of the web app subnet." + default = "app-subnet" + type = string +} + +variable "webapp_subnet_address_prefix" { + description = "Specifies the address prefix of the web app subnet." + default = ["10.0.0.0/24"] + type = list(string) +} + +variable "pe_subnet_name" { + description = "Specifies the name of the subnet that contains the private endpoints." + default = "pe-subnet" + type = string +} + +variable "pe_subnet_address_prefix" { + description = "Specifies the address prefix of the subnet that contains the private endpoints." + default = ["10.0.1.0/24"] + type = list(string) +} + +variable "nat_gateway_name" { + description = "(Required) Specifies the name of the NAT Gateway" + type = string + default = "NatGateway" +} + +variable "nat_gateway_sku_name" { + description = "(Optional) The SKU which should be used. At this time the only supported value is Standard. Defaults to Standard" + type = string + default = "Standard" +} + +variable "nat_gateway_idle_timeout_in_minutes" { + description = "(Optional) The idle timeout which should be used in minutes. Defaults to 4." + type = number + default = 4 +} + +variable "nat_gateway_zones" { + description = " (Optional) A list of Availability Zones in which this NAT Gateway should be located. Changing this forces a new NAT Gateway to be created." + type = list(string) + default = ["1"] +} + +variable "websites_port" { + description = "(Optional) Specifies the port on which the Web App will listen. Defaults to 8000." + type = number + default = 80 +} + +variable "image_name" { + description = "(Required) Specifies the name of the container image to deploy to the Web App." + type = string + default = "custom-image-webapp" +} + +variable "image_tag" { + description = "(Required) Specifies the tag of the container image to deploy to the Web App." + type = string + default = "v1" +} \ No newline at end of file diff --git a/samples/web-app-custom-image/python/visio/web-app-custom-image.vsdx b/samples/web-app-custom-image/python/visio/web-app-custom-image.vsdx new file mode 100644 index 0000000..bd6c655 Binary files /dev/null and b/samples/web-app-custom-image/python/visio/web-app-custom-image.vsdx differ diff --git a/samples/web-app-managed-identity/python/README.md b/samples/web-app-managed-identity/python/README.md index f905992..ea334af 100644 --- a/samples/web-app-managed-identity/python/README.md +++ b/samples/web-app-managed-identity/python/README.md @@ -115,4 +115,4 @@ You can use [Azure Storage Explorer](https://learn.microsoft.com/en-us/azure/sto - [What is Azure Blob storage?](https://learn.microsoft.com/en-us/azure/storage/blobs/storage-blobs-overview) - [What is managed identities for Azure resources?](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview) - [How managed identities for Azure resources work with Azure virtual machines](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/how-managed-identities-work-vm) -- [LocalStack for Azure](https://azure.localstack.cloud/) +- [LocalStack for Azure](https://docs.localstack.cloud/azure/) diff --git a/samples/web-app-managed-identity/python/bicep/README.md b/samples/web-app-managed-identity/python/bicep/README.md index 08c82ca..aa66008 100644 --- a/samples/web-app-managed-identity/python/bicep/README.md +++ b/samples/web-app-managed-identity/python/bicep/README.md @@ -6,7 +6,7 @@ This directory contains the Bicep template and a deployment script for provision Before deploying this solution, ensure you have the following tools installed: -- [LocalStack for Azure](https://azure.localstack.cloud/): Local Azure cloud emulator for development and testing +- [LocalStack for Azure](https://docs.localstack.cloud/azure/): Local Azure cloud emulator for development and testing - [Visual Studio Code](https://code.visualstudio.com/): Code editor installed on one of the [supported platforms](https://code.visualstudio.com/docs/supporting/requirements#_platforms) - [Bicep extension](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-bicep): VS Code extension for Bicep language support and IntelliSense - [Docker](https://docs.docker.com/get-docker/): Container runtime required for LocalStack @@ -147,4 +147,4 @@ This will remove all Azure resources created by the CLI deployment script. - [Azure Bicep Documentation](https://docs.microsoft.com/en-us/azure/azure-resource-manager/bicep/) - [Bicep Language Reference](https://docs.microsoft.com/en-us/azure/azure-resource-manager/bicep/bicep-functions) -- [LocalStack for Azure Documentation](https://azure.localstack.cloud/) \ No newline at end of file +- [LocalStack for Azure Documentation](https://docs.localstack.cloud/azure/) \ No newline at end of file diff --git a/samples/web-app-managed-identity/python/scripts/README.md b/samples/web-app-managed-identity/python/scripts/README.md index 53041f3..9eaa585 100644 --- a/samples/web-app-managed-identity/python/scripts/README.md +++ b/samples/web-app-managed-identity/python/scripts/README.md @@ -6,7 +6,7 @@ This directory includes Bash scripts designed for deploying and testing the samp Before deploying this solution, ensure you have the following tools installed: -- [LocalStack for Azure](https://azure.localstack.cloud/): Local Azure cloud emulator for development and testing +- [LocalStack for Azure](https://docs.localstack.cloud/azure/): Local Azure cloud emulator for development and testing - [Visual Studio Code](https://code.visualstudio.com/): Code editor installed on one of the [supported platforms](https://code.visualstudio.com/docs/supporting/requirements#_platforms) - [Docker](https://docs.docker.com/get-docker/): Container runtime required for LocalStack - [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli): Azure command-line interface @@ -162,4 +162,4 @@ This will remove all Azure resources created by the CLI deployment script. ## Related Documentation - [Azure CLI Documentation](https://docs.microsoft.com/en-us/cli/azure/) -- [LocalStack for Azure Documentation](https://azure.localstack.cloud/) +- [LocalStack for Azure Documentation](https://docs.localstack.cloud/azure/) diff --git a/samples/web-app-managed-identity/python/terraform/README.md b/samples/web-app-managed-identity/python/terraform/README.md index 4378d35..a82a275 100644 --- a/samples/web-app-managed-identity/python/terraform/README.md +++ b/samples/web-app-managed-identity/python/terraform/README.md @@ -6,7 +6,7 @@ This directory contains Terraform modules and a deployment script for provisioni Before deploying this solution, ensure you have the following tools installed: -- [LocalStack for Azure](https://azure.localstack.cloud/): Local Azure cloud emulator for development and testing +- [LocalStack for Azure](https://docs.localstack.cloud/azure/): Local Azure cloud emulator for development and testing - [Visual Studio Code](https://code.visualstudio.com/): Code editor installed on one of the [supported platforms](https://code.visualstudio.com/docs/supporting/requirements#_platforms) - [Terraform](https://developer.hashicorp.com/terraform/downloads): Infrastructure as Code tool for provisioning Azure resources - [Python 3.11+](https://www.python.org/downloads/): Required for running the Flask web application @@ -160,4 +160,4 @@ This will remove all Azure resources created by the CLI deployment script. ## Related Documentation - [Terraform Azure Provider](https://registry.terraform.io/providers/hashicorp/azurerm/latest) -- [LocalStack for Azure Documentation](https://azure.localstack.cloud/) \ No newline at end of file +- [LocalStack for Azure Documentation](https://docs.localstack.cloud/azure/) \ No newline at end of file diff --git a/samples/web-app-sql-database/python/README.md b/samples/web-app-sql-database/python/README.md index 363d83d..9b7dc65 100644 --- a/samples/web-app-sql-database/python/README.md +++ b/samples/web-app-sql-database/python/README.md @@ -41,7 +41,7 @@ The Vacation Planner Web App supports two common approaches for accessing Azure - [AZURE_CLIENT_SECRET](https://learn.microsoft.com/en-us/python/api/azure-identity/azure.identity.environmentcredential): One of the service principal's client secrets. - [AZURE_TENANT_ID](https://learn.microsoft.com/en-us/python/api/azure-identity/azure.identity.environmentcredential): The Microsoft Entra Tenant ID. -This flexibility allows the app to run securely in Azure or in emulated environments like [LocalStack for Azure](https://azure.localstack.cloud/). The client code supports both authentication modes using [`ClientSecretCredential`](https://learn.microsoft.com/en-us/python/api/azure-identity/azure.identity.clientsecretcredential?view=azure-python) or [`DefaultAzureCredential`](https://learn.microsoft.com/en-us/python/api/azure-identity/azure.identity.defaultazurecredential?view=azure-python) from the Azure SDK. +This flexibility allows the app to run securely in Azure or in emulated environments like [LocalStack for Azure](https://docs.localstack.cloud/azure/). The client code supports both authentication modes using [`ClientSecretCredential`](https://learn.microsoft.com/en-us/python/api/azure-identity/azure.identity.clientsecretcredential?view=azure-python) or [`DefaultAzureCredential`](https://learn.microsoft.com/en-us/python/api/azure-identity/azure.identity.defaultazurecredential?view=azure-python) from the Azure SDK. ## Azure Key Vault Integration The application integrates with Azure Key Vault for managing secrets and certificates: @@ -131,4 +131,4 @@ e844433e-f36b-1410-88e7-0034efb7413b paolo Go to Mexi - [Quickstart: Python Flask on Azure](https://learn.microsoft.com/en-us/azure/app-service/quickstart-python?tabs=flask%2Cbrowser) - [pyodbc](https://github.com/mkleehammer/pyodbc) - [Azure Identity Client Library for Python](https://learn.microsoft.com/en-us/python/api/overview/azure/identity-readme?view=azure-python) -- [LocalStack for Azure](https://azure.localstack.cloud/) \ No newline at end of file +- [LocalStack for Azure](https://docs.localstack.cloud/azure/) \ No newline at end of file diff --git a/samples/web-app-sql-database/python/bicep/README.md b/samples/web-app-sql-database/python/bicep/README.md index 4acf2ed..4b6d031 100644 --- a/samples/web-app-sql-database/python/bicep/README.md +++ b/samples/web-app-sql-database/python/bicep/README.md @@ -6,7 +6,7 @@ This directory contains the Bicep template and a deployment script for provision Before deploying this solution, ensure you have the following tools installed: -- [LocalStack for Azure](https://azure.localstack.cloud/): Local Azure cloud emulator for development and testing +- [LocalStack for Azure](https://docs.localstack.cloud/azure/): Local Azure cloud emulator for development and testing - [Visual Studio Code](https://code.visualstudio.com/): Code editor installed on one of the [supported platforms](https://code.visualstudio.com/docs/supporting/requirements#_platforms) - [Bicep extension](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-bicep): VS Code extension for Bicep language support and IntelliSense - [Docker](https://docs.docker.com/get-docker/): Container runtime required for LocalStack @@ -134,4 +134,4 @@ This will remove all Azure resources created by the CLI deployment script. - [Azure Bicep Documentation](https://docs.microsoft.com/en-us/azure/azure-resource-manager/bicep/) - [Bicep Language Reference](https://docs.microsoft.com/en-us/azure/azure-resource-manager/bicep/bicep-functions) -- [LocalStack for Azure Documentation](https://azure.localstack.cloud/) \ No newline at end of file +- [LocalStack for Azure Documentation](https://docs.localstack.cloud/azure/) \ No newline at end of file diff --git a/samples/web-app-sql-database/python/scripts/README.md b/samples/web-app-sql-database/python/scripts/README.md index 1dda2c3..1c220ed 100644 --- a/samples/web-app-sql-database/python/scripts/README.md +++ b/samples/web-app-sql-database/python/scripts/README.md @@ -6,7 +6,7 @@ This directory includes Bash scripts designed for deploying and testing the samp Before deploying this solution, ensure you have the following tools installed: -- [LocalStack for Azure](https://azure.localstack.cloud/): Local Azure cloud emulator for development and testing +- [LocalStack for Azure](https://docs.localstack.cloud/azure/): Local Azure cloud emulator for development and testing - [Visual Studio Code](https://code.visualstudio.com/): Code editor installed on one of the [supported platforms](https://code.visualstudio.com/docs/supporting/requirements#_platforms) - [Docker](https://docs.docker.com/get-docker/): Container runtime required for LocalStack - [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli): Azure command-line interface @@ -133,4 +133,4 @@ This will remove all Azure resources created by the CLI deployment script. ## Related Documentation - [Azure CLI Documentation](https://docs.microsoft.com/en-us/cli/azure/) -- [LocalStack for Azure Documentation](https://azure.localstack.cloud/) \ No newline at end of file +- [LocalStack for Azure Documentation](https://docs.localstack.cloud/azure/) \ No newline at end of file diff --git a/samples/web-app-sql-database/python/terraform/README.md b/samples/web-app-sql-database/python/terraform/README.md index 47ff666..dbf2a62 100644 --- a/samples/web-app-sql-database/python/terraform/README.md +++ b/samples/web-app-sql-database/python/terraform/README.md @@ -6,7 +6,7 @@ This directory contains Terraform modules and a deployment script for provisioni Before deploying this solution, ensure you have the following tools installed: -- [LocalStack for Azure](https://azure.localstack.cloud/): Local Azure cloud emulator for development and testing +- [LocalStack for Azure](https://docs.localstack.cloud/azure/): Local Azure cloud emulator for development and testing - [Visual Studio Code](https://code.visualstudio.com/): Code editor installed on one of the [supported platforms](https://code.visualstudio.com/docs/supporting/requirements#_platforms) - [Terraform](https://developer.hashicorp.com/terraform/downloads): Infrastructure as Code tool for provisioning Azure resources - [Python 3.11+](https://www.python.org/downloads/): Required for running the Flask web application @@ -157,4 +157,4 @@ This will remove all Azure resources created by the CLI deployment script. ## Related Documentation - [Terraform Azure Provider](https://registry.terraform.io/providers/hashicorp/azurerm/latest) -- [LocalStack for Azure Documentation](https://azure.localstack.cloud/) \ No newline at end of file +- [LocalStack for Azure Documentation](https://docs.localstack.cloud/azure/) \ No newline at end of file