diff --git a/samples/function-app-front-door/python/README.md b/samples/function-app-front-door/python/README.md index efcfbe1..52d48a5 100644 --- a/samples/function-app-front-door/python/README.md +++ b/samples/function-app-front-door/python/README.md @@ -1,108 +1,127 @@ -Function App + Azure Front Door (az CLI) +# Azure Function App and Azure Front Door (Azure CLI) -This sample creates a minimal Python Azure Function App that responds to /{name} and configures Azure Front Door (Standard SKU) to route traffic to it. It can target real Azure or LocalStack’s Azure emulation via azlocal interception. +This sample creates a minimal Python Azure Function App that responds to `/{name}` and configures Azure Front Door (Standard SKU) to route traffic to it. It can target real Azure or LocalStack's Azure emulation via `azlocal` interception. -- scripts/deploy_all.sh: One script that provisions all scenarios below in a single resource group: - 1) Basic single-origin routing - 2) Multiple origins with priority/weight selection - 3) Route specificity/precedence - 4) Rules Engine demo (three rules: response header, rewrite, redirect) - 5) Endpoint enabled/disabled state toggle -- scripts/cleanup_all.sh: Deletes the resource group created by deploy_all.sh. +## Overview -Architecture at a glance (diagrams) ------------------------------------ +- **`scripts/deploy_all.sh`**: One script that provisions all scenarios below in a single resource group: + 1. Basic single-origin routing + 2. Multiple origins with priority/weight selection + 3. Route specificity/precedence + 4. Rules Engine demo (three rules: response header, rewrite, redirect) + 5. Endpoint enabled/disabled state toggle +- **`scripts/cleanup_all.sh`**: Deletes the resource group created by `deploy_all.sh` + +## Architecture at a Glance (Diagrams) The following diagrams visualize each scenario provisioned by `deploy_all.sh`. They help you see the wiring between AFD endpoints, routes, origin groups/origins, and the Function App(s). -- Basic single‑origin - - ![Basic single-origin](./images/basic.png) +### Basic Single-Origin - What to notice: one Endpoint → one Route (`/*`) → one Origin Group → one Origin → Function App. +![Basic single-origin](./images/basic.png) -- Multi‑origin (priority/weight) - - ![Multi-origin (priority/weight)](./images/nulti.png) - - What to notice: two Origins in a single Origin Group with explicit `priority` and `weight`. A group‑level health probe (HEAD /, 120s) gates origin eligibility; selection prefers the lowest priority and distributes by weight among equally prioritized healthy origins. +**What to notice:** One Endpoint → one Route (`/*`) → one Origin Group → one Origin → Function App. -- Route specificity +### Multi-Origin (Priority/Weight) - ![Route specificity](./images/spec.png) +![Multi-origin (priority/weight)](./images/nulti.png) + +**What to notice:** Two Origins in a single Origin Group with explicit `priority` and `weight`. A group-level health probe (HEAD `/`, 120s) gates origin eligibility; selection prefers the lowest priority and distributes by weight among equally prioritized healthy origins. + +### Route Specificity - What to notice: two Routes on the same Endpoint and Origin Group: a catch‑all (`/*`) and a specific (`/john`). The most specific matching route should be chosen by the data plane. +![Route specificity](./images/spec.png) -- Rules engine +**What to notice:** Two Routes on the same Endpoint and Origin Group: a catch-all (`/*`) and a specific (`/john`). The most specific matching route should be chosen by the data plane. + +### Rules Engine - ![Rules engine](./images/rules.png) +![Rules engine](./images/rules.png) - What to notice: a Route with an attached Rule Set (three rules): - - Rule 1: ModifyResponseHeader on GET → `X‑CDN: MSFT` - - Rule 2: UrlRewrite when path begins with `/api` → `/` - - Rule 3: UrlRedirect when path begins with `/old` → `/new` (302 Found) +**What to notice:** A Route with an attached Rule Set (three rules): +- **Rule 1**: ModifyResponseHeader on GET → `X-CDN: MSFT` +- **Rule 2**: UrlRewrite when path begins with `/api` → `/` +- **Rule 3**: UrlRedirect when path begins with `/old` → `/new` (302 Found) -- Endpoint enabled/disabled state - - ![Endpoint enabled/disabled state](./images/disabled_state.png) +### Endpoint Enabled/Disabled State - What to notice: the Endpoint’s `enabled-state` can be toggled; when Disabled, requests should return a 4xx (e.g., 403). Re‑enabling restores normal behavior. +![Endpoint enabled/disabled state](./images/disabled_state.png) -Notes for LocalStack runs: -- The printed test URLs use `*.afd.localhost.localstack.cloud:4566` for AFD and `*website.localhost.localstack.cloud:4566` for the Function App, so requests flow through the emulator’s edge. +**What to notice:** The Endpoint's `enabled-state` can be toggled; when Disabled, requests should return a 4xx (e.g., 403). Re-enabling restores normal behavior. + +### Notes for LocalStack Runs + +- The printed test URLs use `*.afd.localhost.localstack.cloud:4566` for AFD and `*.website.localhost.localstack.cloud:4566` for the Function App, so requests flow through the emulator's edge. + +## Prerequisites -Prerequisites - Bash (e.g., Git Bash, WSL, or Linux/macOS shell) -- Azure CLI installed and logged in (az login) for real Azure -- Optional: azlocal (LocalStack’s Azure interception helper) in PATH to target the emulator -- zip utility in PATH (used for zip deploy to Azure) -- For LocalStack: funclocal and Azure Functions Core Tools ('func') for publishing +- Azure CLI installed and logged in (`az login`) for real Azure +- **Optional**: `azlocal` (LocalStack's Azure interception helper) in PATH to target the emulator +- `zip` utility in PATH (used for zip deploy to Azure) +- **For LocalStack**: `funclocal` and Azure Functions Core Tools (`func`) for publishing + +## Quick Start -Quick start -1) Deploy against real Azure (eastus by default): +1. **Deploy against real Azure** (eastus by default): + ```bash bash ./scripts/deploy_all.sh --name-prefix mydemo + ``` -2) Deploy against LocalStack emulator: +2. **Deploy against LocalStack emulator**: + ```bash bash ./scripts/deploy_all.sh --name-prefix mydemo --use-localstack + ``` The script prints: - Resource group name -- AFD endpoint hostnames for each scenario and sample URLs (e.g., https://.z01.azurefd.net/john) - -Cleanup -- Delete the resource group created by the deploy script: - bash ./scripts/cleanup_all.sh --env-file ./scripts/.last_deploy_all.env - or - bash ./scripts/cleanup_all.sh --resource-group - -Scenarios deployed by deploy_all.sh -1) Basic single-origin - - One Function App, one AFD endpoint with a catch‑all route. - - Test URL: printed as [Basic] in the output. - -2) Multiple origins (priority/weight) - - Two Function Apps (A primary, B secondary by default), one origin group with priorities/weights. - - Call repeatedly to observe distribution; the function response includes "from " to visualize selected origin. - -3) Route specificity - - One endpoint with two routes pointing to the same origin group: catch‑all ('/*') and a specific ['/john'] route. - - Compare responses for /john vs other paths. - -4) Rules Engine demo - - Creates a Rule Set with three rules and attaches it to the route: - • ModifyResponseHeader on GET: sets header `X-CDN: MSFT` - • UrlRewrite: when UrlPath begins with `/api`, rewrites to `/` - • UrlRedirect: when UrlPath begins with `/old`, redirects (302 Found) to `/new` - - If the `az afd rule-set`/`az afd rule` commands are unavailable, the script skips rule creation gracefully. - -5) Endpoint enabled/disabled state - - Provisions a dedicated endpoint you can toggle with: - az afd endpoint update -g --profile-name --endpoint-name --enabled-state Disabled - az afd endpoint update -g --profile-name --endpoint-name --enabled-state Enabled - -Unified scripts details -======================= - -deploy_all.sh (what it provisions/tests) +- AFD endpoint hostnames for each scenario and sample URLs (e.g., `https://.z01.azurefd.net/john`) + +## Cleanup + +Delete the resource group created by the deploy script: + +```bash +bash ./scripts/cleanup_all.sh --env-file ./scripts/.last_deploy_all.env +``` + +or + +```bash +bash ./scripts/cleanup_all.sh --resource-group +``` + +## Scenarios Deployed by deploy_all.sh + +### 1. Basic Single-Origin +- One Function App, one AFD endpoint with a catch-all route +- Test URL: printed as `[Basic]` in the output + +### 2. Multiple Origins (Priority/Weight) +- Two Function Apps (A primary, B secondary by default), one origin group with priorities/weights +- Call repeatedly to observe distribution; the function response includes `"from "` to visualize selected origin + +### 3. Route Specificity +- One endpoint with two routes pointing to the same origin group: catch-all (`/*`) and a specific (`/john`) route +- Compare responses for `/john` vs other paths + +### 4. Rules Engine Demo +Creates a Rule Set with three rules and attaches it to the route: +- **ModifyResponseHeader** on GET: sets header `X-CDN: MSFT` +- **UrlRewrite**: when UrlPath begins with `/api`, rewrites to `/` +- **UrlRedirect**: when UrlPath begins with `/old`, redirects (302 Found) to `/new` + +If the `az afd rule-set`/`az afd rule` commands are unavailable, the script skips rule creation gracefully. + +### 5. Endpoint Enabled/Disabled State +Provisions a dedicated endpoint you can toggle with: +```bash +az afd endpoint update -g --profile-name --endpoint-name --enabled-state Disabled +az afd endpoint update -g --profile-name --endpoint-name --enabled-state Enabled +``` + +## Unified Scripts Details + +### deploy_all.sh (What It Provisions/Tests) - Creates one AFD Profile and up to five Endpoints (one per scenario): - Basic: single origin, catch‑all route - Multi: two origins in one origin group with priority/weight and a HEAD health probe @@ -111,50 +130,72 @@ deploy_all.sh (what it provisions/tests) - State: an endpoint to toggle Enabled/Disabled - Creates the necessary Function App(s): one main app for Basic/Spec/Rules/State, and two apps (A/B) for Multi. - Publishes the function code (zip deploy for Azure; `funclocal` + `func` for LocalStack). -- Prints convenient test URLs for each scenario. -- Writes an environment file for cleanup at: `scripts/.last_deploy_all.env`. - -deploy_all.sh (how to run) -- Azure (cloud): - - `bash ./scripts/deploy_all.sh --name-prefix mydemo` -- LocalStack (emulator): - - `bash ./scripts/deploy_all.sh --name-prefix mydemo --use-localstack` -- Useful flags: - - `-p, --name-prefix`: base name used for resources (auto-sanitized to lowercase/digits) - - `-l, --location`: Azure region (default: eastus) - - `-g, --resource-group`: use a specific RG instead of an auto-generated one - - `--python-version`: Python runtime for Function Apps (default: 3.11) - - `--use-localstack`: target LocalStack via azlocal interception and publish via funclocal/func - - Scenario toggles (all enabled by default): `--no-basic`, `--no-multi`, `--no-spec`, `--no-rules`, `--no-state` - -deploy_all.sh (outputs to expect) +- Prints convenient test URLs for each scenario +- Writes an environment file for cleanup at: `scripts/.last_deploy_all.env` + +### deploy_all.sh (How to Run) + +**Azure (cloud):** +```bash +bash ./scripts/deploy_all.sh --name-prefix mydemo +``` + +**LocalStack (emulator):** +```bash +bash ./scripts/deploy_all.sh --name-prefix mydemo --use-localstack +``` + +**Useful flags:** +- `-p, --name-prefix`: base name used for resources (auto-sanitized to lowercase/digits) +- `-l, --location`: Azure region (default: `eastus`) +- `-g, --resource-group`: use a specific RG instead of an auto-generated one +- `--python-version`: Python runtime for Function Apps (default: `3.11`) +- `--use-localstack`: target LocalStack via `azlocal` interception and publish via `funclocal`/`func` +- **Scenario toggles** (all enabled by default): `--no-basic`, `--no-multi`, `--no-spec`, `--no-rules`, `--no-state` + +### deploy_all.sh (Outputs to Expect) + - Resource group name, e.g., `rg--` - Scenario endpoints (Azure or LocalStack hosts) and example URLs, e.g.: - - `[Rules] AFD Local Endpoint: https://ep--rules-.afd.localhost.localstack.cloud:4566/john` -- For LocalStack runs, the function host and AFD local endpoint names will use `*.localhost.localstack.cloud:4566`. + ``` + [Rules] AFD Local Endpoint: https://ep--rules-.afd.localhost.localstack.cloud:4566/john + ``` +- For LocalStack runs, the function host and AFD local endpoint names will use `*.localhost.localstack.cloud:4566` - The script also writes `scripts/.last_deploy_all.env` with variables like: - - `RESOURCE_GROUP`, `PROFILE_NAME`, `EP_BASIC`, `EP_MULTI`, `EP_SPEC`, `EP_RULES`, `EP_STATE`, `FUNC_MAIN`, `FUNC_A`, `FUNC_B`. + - `RESOURCE_GROUP`, `PROFILE_NAME`, `EP_BASIC`, `EP_MULTI`, `EP_SPEC`, `EP_RULES`, `EP_STATE`, `FUNC_MAIN`, `FUNC_A`, `FUNC_B` - The Rules Engine rule set name follows the pattern `rs` (alphanumeric), which can be derived from `PROFILE_NAME`: - - Example in bash: `BASE="${PROFILE_NAME#afd-}"; RULE_SET="rs${BASE//-/}"` + ```bash + BASE="${PROFILE_NAME#afd-}"; RULE_SET="rs${BASE//-/}" + ``` -cleanup_all.sh (what it does) -- Deletes the entire resource group created by `deploy_all.sh` using `az group delete --no-wait`. +### cleanup_all.sh (What It Does) + +- Deletes the entire resource group created by `deploy_all.sh` using `az group delete --no-wait` - Supports two ways to specify the resource group: - 1) `--env-file ./scripts/.last_deploy_all.env` (recommended after a fresh deploy) - 2) `-g/--resource-group ` -- Supports `--use-localstack` to intercept the az CLI for emulator cleanup. - -cleanup_all.sh (how to run) -- Using the env file created by the deploy: - - `bash ./scripts/cleanup_all.sh --env-file ./scripts/.last_deploy_all.env` -- Passing RG explicitly: - - `bash ./scripts/cleanup_all.sh --resource-group rg--` -- LocalStack cleanup: - - add `--use-localstack` to either command above. - -Verifying the Rules Engine scenario quickly (LocalStack) -- Assume you ran with `--name-prefix mydemo` and got `ep-mydemo-rules-12345`: + 1. `--env-file ./scripts/.last_deploy_all.env` (recommended after a fresh deploy) + 2. `-g/--resource-group ` +- Supports `--use-localstack` to intercept the `az` CLI for emulator cleanup + +### cleanup_all.sh (How to Run) + +**Using the env file created by the deploy:** +```bash +bash ./scripts/cleanup_all.sh --env-file ./scripts/.last_deploy_all.env +``` + +**Passing RG explicitly:** +```bash +bash ./scripts/cleanup_all.sh --resource-group rg-- ``` + +**LocalStack cleanup:** +Add `--use-localstack` to either command above. + +## Verifying the Rules Engine Scenario Quickly (LocalStack) + +Assume you ran with `--name-prefix mydemo` and got `ep-mydemo-rules-12345`: + +```bash HOST="https://ep-mydemo-rules-12345.afd.localhost.localstack.cloud:4566" # 1) ModifyResponseHeader on GET → expect X-CDN: MSFT @@ -167,43 +208,77 @@ curl -i "$HOST/api" | head -n 1 curl -i -L "$HOST/old" | head -n 5 ``` -Deploy to Azure (cloud) and test -1) Sign in/select subscription: - - az login - - az account set --subscription "" - -2) Run deployment (avoid --use-localstack): - - cd samples/function-app-front-door/python - - bash ./scripts/deploy_all.sh --name-prefix mydemo --location eastus - -3) Note the printed outputs (resource group and endpoints) and test as instructed. Allow 2–10 minutes for AFD readiness. - -If you closed the terminal and need hostnames later -- Function host: - az functionapp show -g -n --query defaultHostName -o tsv -- AFD endpoint hostname: - az afd endpoint show -g --profile-name --endpoint-name --query hostName -o tsv -- List resources by RG: - - Function Apps: az functionapp list -g --query "[].{name:name,host:defaultHostName}" - - AFD profiles: az afd profile list -g --query "[].name" - - AFD endpoints: az afd endpoint list -g --profile-name --query "[].{name:name,host:hostName}" - -Common notes and troubleshooting -- Windows users: use Git Bash or WSL to run bash scripts. -- Authentication: the function trigger is Anonymous; no keys required. -- The function returns plain text and echoes WEBSITE_HOSTNAME to help testing multi-origins. -- Application Insights: disabled by default via --disable-app-insights. -- Azure deploy uses zip deploy; LocalStack uses funclocal + func publish. -- AFD readiness: 2–10 minutes typical; check provisioning state: +## Deploy to Azure (Cloud) and Test + +1. **Sign in/select subscription:** + ```bash + az login + az account set --subscription "" + ``` + +2. **Run deployment** (avoid `--use-localstack`): + ```bash + cd samples/function-app-front-door/python + bash ./scripts/deploy_all.sh --name-prefix mydemo --location eastus + ``` + +3. **Note the printed outputs** (resource group and endpoints) and test as instructed. Allow 2–10 minutes for AFD readiness. + +## If You Closed the Terminal and Need Hostnames Later + +**Function host:** +```bash +az functionapp show -g -n --query defaultHostName -o tsv +``` + +**AFD endpoint hostname:** +```bash +az afd endpoint show -g --profile-name --endpoint-name --query hostName -o tsv +``` + +**List resources by RG:** +- Function Apps: + ```bash + az functionapp list -g --query "[].{name:name,host:defaultHostName}" + ``` +- AFD profiles: + ```bash + az afd profile list -g --query "[].name" + ``` +- AFD endpoints: + ```bash + az afd endpoint list -g --profile-name --query "[].{name:name,host:hostName}" + ``` + +## Common Notes and Troubleshooting + +- **Windows users**: Use Git Bash or WSL to run bash scripts +- **Authentication**: The function trigger is Anonymous; no keys required +- **Function response**: Returns plain text and echoes `WEBSITE_HOSTNAME` to help testing multi-origins +- **Application Insights**: Disabled by default via `--disable-app-insights` +- **Deployment method**: Azure uses zip deploy; LocalStack uses `funclocal` + `func` publish +- **AFD readiness**: 2–10 minutes typical; check provisioning state: + ```bash az afd endpoint show -g --profile-name --endpoint-name --query provisioningState -o tsv -- Region/runtime: change with --location/--python-version in deploy_all.sh. + ``` +- **Region/runtime**: Change with `--location`/`--python-version` in `deploy_all.sh` + +## Cleanup + +Delete all resources by removing the resource group (non-blocking delete): + +**Using env file:** +```bash +bash ./scripts/cleanup_all.sh --env-file ./scripts/.last_deploy_all.env +``` + +**Or directly:** +```bash +bash ./scripts/cleanup_all.sh --resource-group +``` -Cleanup -- Delete all resources by removing the resource group (non-blocking delete): - - Using env file: `bash ./scripts/cleanup_all.sh --env-file ./scripts/.last_deploy_all.env` - - Or directly: `bash ./scripts/cleanup_all.sh --resource-group ` +## Additional Notes -Notes -- Azure Front Door is a global resource; the script uses Standard_AzureFrontDoor SKU and links the route to the default domain of the endpoint. -- The function removes the /api prefix so you can call /john directly. -- The deployment uses zip deploy; because the function has no heavy dependencies, it should work without additional build steps. If you add dependencies that require native builds, consider using the Azure Functions Core Tools for publishing. +- Azure Front Door is a global resource; the script uses `Standard_AzureFrontDoor` SKU and links the route to the default domain of the endpoint +- The function removes the `/api` prefix so you can call `/john` directly +- The deployment uses zip deploy; because the function has no heavy dependencies, it should work without additional build steps. If you add dependencies that require native builds, consider using the Azure Functions Core Tools for publishing diff --git a/samples/function-app-managed-identity/python/bicep/README.md b/samples/function-app-managed-identity/python/bicep/README.md index 11d0df2..b31fe38 100644 --- a/samples/function-app-managed-identity/python/bicep/README.md +++ b/samples/function-app-managed-identity/python/bicep/README.md @@ -17,7 +17,7 @@ Before deploying this solution, ensure you have the following tools installed: ### Installing azlocal CLI -The deployment script uses the `azlocal` CLI instead of the standard Azure CLI to work with LocalStack. Install it using: +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 @@ -27,7 +27,7 @@ For more information, see [Get started with the az tool on LocalStack](https://a ## Architecture Overview -The Bicep template deploys the following Azure resources: +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, while the [main.bicep](main.bicep) Bicep module creates the following Azure resources: 1. [Azure Storage Account](https://learn.microsoft.com/en-us/azure/storage/common/storage-account-overview): Provides blob storage with `input` and `output` containers for storing text blobs processed by the function app. 2. [Azure App Service Plan](https://learn.microsoft.com/en-us/azure/app-service/overview-hosting-plans): Defines the compute resources (CPU, memory, and scaling options) that host the Azure Functions app. @@ -37,323 +37,6 @@ The Bicep template deploys the following Azure resources: For more information on the sample application, see [Azure Functions App with Managed Identity](../README.md). -## Bicep Templates - -The `main.bicep` Bicep template defines all Azure resources using declarative syntax. The module uses conditional provisioning for the user-assigned managed identity and role assignments resources. In Bicep, you can conditionally deploy a resource by passing in a parameter that specifies if the resource is deployed. Test the condition with an if expression in the resource declaration. For more information, see [Conditional deployments in Bicep with the if expression](https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/conditional-resource-deployment). - -```bicep -@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 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 language runtime used by the Azure Functions App.') -@allowed([ - 'dotnet' - 'dotnet-isolated' - 'python' - 'java' - 'node' - 'powerShell' - 'custom' -]) -param runtimeName string - -@description('Specifies the target language version used by the Azure Functions App.') -param runtimeVersion string - -@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 functionAppKind string = 'functionapp,linux' - -@description('Specifies whether HTTPS is enforced for the Azure Functions App.') -param httpsOnly bool = false - -@description('Specifies the minimum TLS version for the Azure Functions App.') -@allowed([ - '1.0' - '1.1' - '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 Always On is enabled for the Azure Functions App.') -param alwaysOn bool = true - -@description('Specifies the optional Git Repo URL.') -param repoUrl string = '' - -@description('Specifies the tags to be applied to the resources.') -param tags object = { - environment: 'test' - iac: 'bicep' -} - -@description('Specifies the sku of the Azure Storage account.') -param storageAccountSku string = 'Standard_LRS' - -@description('Specifies the name of the input container.') -param inputContainerName string = 'input' - -@description('Specifies the name of the output container.') -param outputContainerName string = 'output' - -@description('Specifies the type of managed identity.') -@allowed([ - 'SystemAssigned' - 'UserAssigned' -]) -param managedIdentityType string = 'SystemAssigned' - -var functionAppName = '${prefix}-functionapp-${suffix}' -var appServicePlanName = '${prefix}-app-service-plan-${suffix}' -var storageAccountName = '${prefix}storage${suffix}' -var managedIdentityName = '${prefix}-identity-${suffix}' -var storageAccountConnectionString = 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};EndpointSuffix=${environment().suffixes.storage};AccountKey=${storageAccount.listKeys().keys[0].value}' - -resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = if (managedIdentityType == 'UserAssigned') { - name: managedIdentityName - location: location - tags: tags -} - -resource storageBlobDataContributorRoleDefinition 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { - name: 'ba92f5b4-2d11-453d-a403-e96b0029c9fe' - scope: subscription() -} - -resource storageQueueDataContributorRoleDefinition 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { - name: '974c5e8b-45b9-4653-ba55-5f855dd0fb88' - scope: subscription() -} - -resource storageBlobDataContributorRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(storageAccount.id, functionApp.id, storageBlobDataContributorRoleDefinition.id) - scope: storageAccount - properties: { - roleDefinitionId: storageBlobDataContributorRoleDefinition.id - principalId: managedIdentityType == 'SystemAssigned' ? functionApp.identity.principalId : managedIdentity.properties.principalId - principalType: 'ServicePrincipal' - } -} - -resource storageQueueDataContributorRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(storageAccount.id, functionApp.id, storageQueueDataContributorRoleDefinition.id) - scope: storageAccount - properties: { - roleDefinitionId: storageQueueDataContributorRoleDefinition.id - principalId: managedIdentityType == 'SystemAssigned' ? functionApp.identity.principalId : managedIdentity.properties.principalId - principalType: 'ServicePrincipal' - } -} - -resource storageAccount 'Microsoft.Storage/storageAccounts@2025-01-01' = { - name: storageAccountName - location: location - tags: tags - sku: { - name: storageAccountSku - } - kind: 'StorageV2' - properties: { - accessTier: 'Hot' - } -} - -resource blobServices 'Microsoft.Storage/storageAccounts/blobServices@2025-01-01' = { - parent: storageAccount - name: 'default' -} - -resource inputContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2025-01-01' = { - parent: blobServices - name: inputContainerName -} - -resource outputContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2025-01-01' = { - parent: blobServices - name: outputContainerName -} - -resource appServicePlan 'Microsoft.Web/serverfarms@2024-11-01' = { - name: appServicePlanName - location: location - tags: tags - kind: appServicePlanKind - sku: { - tier: skuTier - name: skuName - } - properties: { - reserved: reserved - zoneRedundant: zoneRedundant - maximumElasticWorkerCount: skuTier == 'FlexConsumption' ? 1 : 20 - } -} - -resource functionApp 'Microsoft.Web/sites@2024-11-01' = { - name: functionAppName - location: location - tags: tags - kind: functionAppKind - properties: { - httpsOnly: httpsOnly - reserved: true - serverFarmId: appServicePlan.id - virtualNetworkSubnetId: null - siteConfig: { - alwaysOn: alwaysOn - linuxFxVersion: toUpper('${runtimeName}|${runtimeVersion}') - minTlsVersion: minTlsVersion - ftpsState: 'FtpsOnly' - publicNetworkAccess: publicNetworkAccess - } - } - identity: { - type: managedIdentityType - userAssignedIdentities : managedIdentityType == 'SystemAssigned' ? null : { - '${managedIdentity.id}': {} - } - } -} - -resource configAppSettings 'Microsoft.Web/sites/config@2024-11-01' = { - parent: functionApp - name: 'appsettings' - properties: { - SCM_DO_BUILD_DURING_DEPLOYMENT: 'true' - ENABLE_ORYX_BUILD: 'true' - AzureWebJobsStorage: storageAccountConnectionString - FUNCTIONS_WORKER_RUNTIME: runtimeName - FUNCTIONS_EXTENSION_VERSION: '~4' - STORAGE_ACCOUNT_CONNECTION_STRING__blobServiceUri: storageAccount.properties.primaryEndpoints.blob - STORAGE_ACCOUNT_CONNECTION_STRING__queueServiceUri: storageAccount.properties.primaryEndpoints.queue - STORAGE_ACCOUNT_CONNECTION_STRING__tableServiceUri: storageAccount.properties.primaryEndpoints.table - INPUT_STORAGE_CONTAINER_NAME: inputContainer.name - OUTPUT_STORAGE_CONTAINER_NAME: outputContainer.name - AZURE_CLIENT_ID: managedIdentityType == 'SystemAssigned' ? '' : managedIdentity.properties.clientId - } - dependsOn: [ - storageBlobDataContributorRoleAssignment - ] -} - -resource functionAppSourceControl 'Microsoft.Web/sites/sourcecontrols@2024-11-01' = if (contains(repoUrl,'http')){ - name: 'web' - parent: functionApp - properties: { - repoUrl: repoUrl - branch: 'master' - isManualIntegration: true - } -} - -output appServicePlanName string = appServicePlan.name -output functionAppName string = functionApp.name -output functionAppUrl string = functionApp.properties.defaultHostName -output storageAccountName string = storageAccountName -``` ## Configuration Before deploying the `main.bicep` template, update the `bicep.bicepparam` file with your specific values. Note that the `deploy.sh` script overrides some of these parameters. @@ -367,190 +50,24 @@ param runtimeName = 'python' param runtimeVersion = '3.13' ``` -## Deployment Script - -You can use the `deploy.sh` script to automate the deployment of all Azure resources and the sample application in a single step, streamlining setup and reducing manual configuration. Before running the script, customize the variable values based on your needs. In particular, use the `MANAGED_IDENTITY_TYPE` variable to specify the type of managed identity to provision: `SystemAssigned` or `UserAssigned`. - -```bash -#!/bin/bash - -# Variables -PREFIX='local' -SUFFIX='test' -TEMPLATE="main.bicep" -PARAMETERS="main.bicepparam" -RESOURCE_GROUP_NAME="$PREFIX-rg" -LOCATION="westeurope" -MANAGED_IDENTITY_TYPE='UserAssigned' # SystemAssigned or UserAssigned -VALIDATE_TEMPLATE=1 -USE_WHAT_IF=0 -SUBSCRIPTION_NAME=$(az account show --query name --output tsv) -CURRENT_DIR="$(cd "$(dirname "$0")" && pwd)" -ZIPFILE="function_app.zip" -ENVIRONMENT=$(az account show --query environmentName --output tsv) - -# Change the current directory to the script's directory -cd "$CURRENT_DIR" || exit +## Provisioning Scripts -# Choose the appropriate CLI based on the environment -if [[ $ENVIRONMENT == "LocalStack" ]]; then - echo "Using azlocal for LocalStack emulator environment." - AZ="azlocal" -else - echo "Using standard az for AzureCloud environment." - AZ="az" -fi - -# 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 - -# 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 [$TEMPLATE]..." - $AZ deployment group what-if \ - --resource-group $RESOURCE_GROUP_NAME \ - --template-file $TEMPLATE \ - --parameters $PARAMETERS \ - --parameters location=$LOCATION \ - prefix=$PREFIX \ - suffix=$SUFFIX \ - managedIdentityType=$MANAGED_IDENTITY_TYPE \ - --only-show-errors - - if [[ $? == 0 ]]; then - echo "Bicep template [$TEMPLATE] validation succeeded" - else - echo "Failed to validate Bicep template [$TEMPLATE]" - exit - fi - else - # Validate the Bicep template - echo "Validating Bicep template [$TEMPLATE]..." - output=$($AZ deployment group validate \ - --resource-group $RESOURCE_GROUP_NAME \ - --template-file $TEMPLATE \ - --parameters $PARAMETERS \ - --parameters location=$LOCATION \ - prefix=$PREFIX \ - suffix=$SUFFIX \ - managedIdentityType=$MANAGED_IDENTITY_TYPE \ - --only-show-errors) - - if [[ $? == 0 ]]; then - echo "Bicep template [$TEMPLATE] validation succeeded" - else - echo "Failed to validate Bicep template [$TEMPLATE]" - echo "$output" - exit - fi - fi -fi - -# Deploy the Bicep template -echo "Deploying Bicep template [$TEMPLATE]..." -if DEPLOYMENT_OUTPUTS=$($AZ deployment group create \ - --resource-group $RESOURCE_GROUP_NAME \ - --only-show-errors \ - --template-file $TEMPLATE \ - --parameters $PARAMETERS \ - --parameters location=$LOCATION \ - prefix=$PREFIX \ - suffix=$SUFFIX \ - managedIdentityType=$MANAGED_IDENTITY_TYPE \ - --query 'properties.outputs' -o json); then - echo "Bicep template [$TEMPLATE] deployed successfully. Outputs:" - echo "$DEPLOYMENT_OUTPUTS" | jq . - APP_SERVICE_PLAN_NAME=$(echo "$DEPLOYMENT_OUTPUTS" | jq -r '.appServicePlanName.value') - FUNCTION_APP_NAME=$(echo "$DEPLOYMENT_OUTPUTS" | jq -r '.functionAppName.value') - FUNCTION_APP_URL=$(echo "$DEPLOYMENT_OUTPUTS" | jq -r '.functionAppUrl.value') - STORAGE_ACCOUNT_NAME=$(echo "$DEPLOYMENT_OUTPUTS" | jq -r '.storageAccountName.value') - echo "Deployment details:" - echo "- appServicePlanName: $APP_SERVICE_PLAN_NAME" - echo "- functionAppName: $FUNCTION_APP_NAME" - echo "- functionAppUrl: $FUNCTION_APP_URL" - echo "- storageAccountName: $STORAGE_ACCOUNT_NAME" -else - echo "Failed to deploy Bicep template [$TEMPLATE]" - exit 1 -fi - -# Validation before deploying the function app -if [[ -z "$FUNCTION_APP_NAME" ]]; then - echo "Function App Name is empty. Exiting." - exit 1 -fi - -# CD into the function app directory -cd ../src || exit - -# Remove any existing zip package of the function app -if [ -f "$ZIPFILE" ]; then - rm "$ZIPFILE" -fi - -# Create the zip package of the function app -echo "Creating zip package of the function app..." -zip -r "$ZIPFILE" function_app.py host.json requirements.txt - -# Deploy the function app -echo "Deploying function app [$FUNCTION_APP_NAME] with zip file [$ZIPFILE]..." -$AZ functionapp deploy \ - --resource-group "$RESOURCE_GROUP_NAME" \ - --name "$FUNCTION_APP_NAME" \ - --src-path "$ZIPFILE" \ - --type zip \ - --async true 1>/dev/null - -if [ $? -eq 0 ]; then - echo "Function app [$FUNCTION_APP_NAME] deployed successfully." -else - echo "Failed to deploy function app [$FUNCTION_APP_NAME]." - exit 1 -fi - -# Remove the zip package of the function app -if [ -f "$ZIPFILE" ]; then - rm "$ZIPFILE" -fi -``` +The [deploy.sh](deploy.sh) script automates the deployment of all Azure resources and the sample application in a single step. Before running the script, customize the variable values based on your needs. In particular, use the `MANAGED_IDENTITY_TYPE` variable to specify the type of managed identity to provision: `SystemAssigned` or `UserAssigned`. > **Note** > You can use the `azlocal` CLI as a drop-in replacement for the `az` CLI to direct all commands to the LocalStack for Azure emulator. Alternatively, run `azlocal start_interception` to automatically intercept and redirect all `az` commands to LocalStack. For more information, see [Get started with the az tool on LocalStack](https://azure.localstack.cloud/user-guides/sdks/az/). -The `deploy.sh` script executes the following steps: - -- Specifies the variables used during deployment. -- Creates the resource group if it does not exist. -- Conditionally validates the `main.bicep` module to check its syntax is correct and all parameters make sense. -- Conditionally runs a what-if deployment to execute a dry run to preview the resources that will be created, updated, or deleted. -- Runs the `main.bicep` template to create all the Azure resources. -- Collects important information from the deployment (like resource names) for later use. -- Uses jq (a JSON tool) to extract the names of resources we just created. -- Creates zip archive in format expected by Function App. -- Uploads pre-built application package to the newly created Function App. +The [deploy.sh](deploy.sh) script executes the following steps: + +- Specifies the variables used during deployment +- Creates the resource group if it does not exist +- Conditionally validates the `main.bicep` module to check its syntax is correct and all parameters make sense +- Conditionally runs a what-if deployment to execute a dry run to preview the resources that will be created, updated, or deleted +- Runs the `main.bicep` template to create all the Azure resources +- Collects important information from the deployment (like resource names) for later use +- Uses jq (a JSON tool) to extract the names of resources we just created +- Creates zip archive in format expected by Function App +- Uploads pre-built application package to the newly created Function App > **Note** > Azure CLI commands use `--verbose` argument to print execution details and the `--debug` flag to show low-level REST calls for debugging. For more information, see [Get started with Azure CLI](https://learn.microsoft.com/en-us/cli/azure/get-started-with-azure-cli) diff --git a/samples/function-app-managed-identity/python/scripts/README.md b/samples/function-app-managed-identity/python/scripts/README.md index 4029289..6c03036 100644 --- a/samples/function-app-managed-identity/python/scripts/README.md +++ b/samples/function-app-managed-identity/python/scripts/README.md @@ -16,7 +16,7 @@ Before deploying this solution, ensure you have the following tools installed: ### Installing azlocal CLI -The deployment script uses the `azlocal` CLI to work with LocalStack. Install it using: +The [deploy.sh](deploy.sh) Bash script uses the `azlocal` CLI to work with LocalStack. Install it using: ```bash pip install azlocal @@ -26,7 +26,7 @@ For more information, see [Get started with the az tool on LocalStack](https://a ## Architecture Overview -This CLI deployment creates the following Azure resources using direct Azure CLI commands: +This [deploy.sh](deploy.sh) script creates the following Azure resources using Azure CLI commands: 1. [Azure Storage Account](https://learn.microsoft.com/en-us/azure/storage/common/storage-account-overview): Provides blob storage with `input` and `output` containers for storing text blobs processed by the function app. 2. [Azure App Service Plan](https://learn.microsoft.com/en-us/azure/app-service/overview-hosting-plans): Defines the compute resources (CPU, memory, and scaling options) that host the Azure Functions app. @@ -36,442 +36,19 @@ This CLI deployment creates the following Azure resources using direct Azure CLI For more information on the sample application, see [Azure Functions App with Managed Identity](../README.md). -## Deployment Script +## Provisioning Scriptss -## Automation Scripts +This sample provides two Bash scripts to streamline the deployment process by automating the provisioning of Azure resources and the sample application: -This sample provides two bash scripts to streamline the deployment process by automating the provisioning of Azure resources and the sample application: +- [user-managed-identity.sh](user-managed-identity.sh): Configures the Azure Functions App to authenticate with Azure Storage using a *user-assigned managed identity* +- [system-managed-identity.sh](system-managed-identity.sh): Configures the Azure Functions App to authenticate with Azure Storage using a *system-assigned managed identity* -- `user-assigned.sh`: Configures the Azure Functions App to authenticate with Azure Storage using a *user-assigned managed identity* -- `system-assigned.sh`: Configures the Azure Functions App to authenticate with Azure Storage using a *system-assigned managed identity* - -These scripts eliminate manual configuration steps and enable one-command deployment of the entire infrastructure. For brevity, we only report the code of the `user-assigned.sh` script in this article. - -```bash -#!/bin/bash - -# Variables -PREFIX='local' -SUFFIX='test' -LOCATION='northeurope' -STORAGE_ACCOUNT_NAME="${PREFIX}storage${SUFFIX}" -MANAGED_IDENTITY_NAME="${PREFIX}-identity-${SUFFIX}" -FUNCTION_APP_NAME="${PREFIX}-functionapp-${SUFFIX}" -RESOURCE_GROUP_NAME="${PREFIX}-rg" -RUNTIME="python" -RUNTIME_VERSION="3.12" -INPUT_STORAGE_CONTAINER_NAME='input' -OUTPUT_STORAGE_CONTAINER_NAME='output' -ZIPFILE="function_app.zip" -CURRENT_DIR="$(cd "$(dirname "$0")" && pwd)" -SUBSCRIPTION_ID=$(az account show --query id --output tsv) -SUBSCRIPTION_NAME=$(az account show --query name --output tsv) -ENVIRONMENT=$(az account show --query environmentName --output tsv) -RETRY_COUNT=3 -SLEEP=5 - -# Change the current directory to the script's directory -cd "$CURRENT_DIR" || exit - -# Choose the appropriate CLI based on the environment -if [[ $ENVIRONMENT == "LocalStack" ]]; then - echo "Using azlocal for LocalStack emulator environment." - AZ="azlocal" -else - echo "Using standard az for AzureCloud environment." - AZ="az" -fi - -# Create a resource group -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 - -# Create a storage account -echo "Checking if storage account [$STORAGE_ACCOUNT_NAME] exists in the resource group [$RESOURCE_GROUP_NAME]..." -$AZ storage account show \ - --name $STORAGE_ACCOUNT_NAME \ - --resource-group $RESOURCE_GROUP_NAME &>/dev/null - -if [[ $? != 0 ]]; then - echo "No storage account [$STORAGE_ACCOUNT_NAME] exists in the [$RESOURCE_GROUP_NAME] resource group." - echo "Creating storage account [$STORAGE_ACCOUNT_NAME] in the [$RESOURCE_GROUP_NAME] resource group..." - $AZ storage account create \ - --name $STORAGE_ACCOUNT_NAME \ - --location $LOCATION \ - --resource-group $RESOURCE_GROUP_NAME \ - --sku Standard_LRS 1>/dev/null - - if [ $? -eq 0 ]; then - echo "Storage account [$STORAGE_ACCOUNT_NAME] created successfully in the [$RESOURCE_GROUP_NAME] resource group." - else - echo "Failed to create storage account [$STORAGE_ACCOUNT_NAME] in the [$RESOURCE_GROUP_NAME] resource group." - exit 1 - fi -else - echo "Storage account [$STORAGE_ACCOUNT_NAME] already exists in the [$RESOURCE_GROUP_NAME] resource group." -fi - -# Get the storage account key -echo "Getting storage account key for [$STORAGE_ACCOUNT_NAME]..." -STORAGE_ACCOUNT_KEY=$($AZ storage account keys list \ - --account-name $STORAGE_ACCOUNT_NAME \ - --resource-group $RESOURCE_GROUP_NAME \ - --query "[0].value" \ - --output tsv) - -if [ -n "$STORAGE_ACCOUNT_KEY" ]; then - echo "Storage account key retrieved successfully: [$STORAGE_ACCOUNT_KEY]" -else - echo "Failed to retrieve storage account key." - exit 1 -fi - -# Construct the storage connection string for LocalStack -STORAGE_CONNECTION_STRING="DefaultEndpointsProtocol=https;AccountName=$STORAGE_ACCOUNT_NAME;AccountKey=$STORAGE_ACCOUNT_KEY;EndpointSuffix=core.windows.net" -echo "Storage connection string constructed: [$STORAGE_CONNECTION_STRING]" - -# Get the storage account resource ID -STORAGE_ACCOUNT_RESOURCE_ID=$($AZ storage account show \ - --name $STORAGE_ACCOUNT_NAME \ - --resource-group $RESOURCE_GROUP_NAME \ - --query "id" \ - --output tsv \ - --only-show-errors) - -if [ -n "$STORAGE_ACCOUNT_RESOURCE_ID" ]; then - echo "Storage account resource ID retrieved successfully: $STORAGE_ACCOUNT_RESOURCE_ID" -else - echo "Failed to retrieve storage account resource ID." - exit 1 -fi - -# Get the storage account blob primary endpoint -AZURE_STORAGE_ACCOUNT_URL=$($AZ storage account show \ - --name $STORAGE_ACCOUNT_NAME \ - --resource-group $RESOURCE_GROUP_NAME \ - --query "primaryEndpoints.blob" \ - --output tsv \ - --only-show-errors) - -if [ -n "$AZURE_STORAGE_ACCOUNT_URL" ]; then - echo "Storage account blob primary endpoint retrieved successfully: $AZURE_STORAGE_ACCOUNT_URL" -else - echo "Failed to retrieve storage account blob primary endpoint." - exit 1 -fi - -# Check if the input blob container exists -echo "Checking if input blob container [$INPUT_STORAGE_CONTAINER_NAME] exists in the [$STORAGE_ACCOUNT_NAME] storage account..." -$AZ storage container show \ - --name "$INPUT_STORAGE_CONTAINER_NAME" \ - --account-name "$STORAGE_ACCOUNT_NAME" \ - --account-key "$STORAGE_ACCOUNT_KEY" &>/dev/null - -if [[ $? != 0 ]]; then - - # Create input blob container - echo "Creating input blob container [$INPUT_STORAGE_CONTAINER_NAME] in the [$STORAGE_ACCOUNT_NAME] storage account..." - $AZ storage container create \ - --name "$INPUT_STORAGE_CONTAINER_NAME" \ - --account-name "$STORAGE_ACCOUNT_NAME" \ - --account-key "$STORAGE_ACCOUNT_KEY" - - if [ $? -eq 0 ]; then - echo "Input blob container [$INPUT_STORAGE_CONTAINER_NAME] created successfully in the [$STORAGE_ACCOUNT_NAME] storage account." - else - echo "Failed to create input blob container [$INPUT_STORAGE_CONTAINER_NAME] in the [$STORAGE_ACCOUNT_NAME] storage account." - exit 1 - fi -fi - -# Check if the output blob container exists -echo "Checking if output blob container [$OUTPUT_STORAGE_CONTAINER_NAME] exists in the [$STORAGE_ACCOUNT_NAME] storage account..." -$AZ storage container show \ - --name "$OUTPUT_STORAGE_CONTAINER_NAME" \ - --account-name "$STORAGE_ACCOUNT_NAME" \ - --account-key "$STORAGE_ACCOUNT_KEY" &>/dev/null - -if [[ $? != 0 ]]; then - # Create output blob container - echo "Creating output blob container [$OUTPUT_STORAGE_CONTAINER_NAME] in the [$STORAGE_ACCOUNT_NAME] storage account..." - $AZ storage container create \ - --name "$OUTPUT_STORAGE_CONTAINER_NAME" \ - --account-name "$STORAGE_ACCOUNT_NAME" \ - --account-key "$STORAGE_ACCOUNT_KEY" - - if [ $? -eq 0 ]; then - echo "Output blob container [$OUTPUT_STORAGE_CONTAINER_NAME] created successfully in the [$STORAGE_ACCOUNT_NAME] storage account." - else - echo "Failed to create output blob container [$OUTPUT_STORAGE_CONTAINER_NAME] in the [$STORAGE_ACCOUNT_NAME] storage account." - exit 1 - fi -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" \ - --subscription "$SUBSCRIPTION_ID" 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 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 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 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 - -# Create the function app -echo "Creating function app [$FUNCTION_APP_NAME]..." -$AZ functionapp create \ - --resource-group $RESOURCE_GROUP_NAME \ - --consumption-plan-location $LOCATION \ - --assign-identity "${IDENTITY_ID}" \ - --runtime $RUNTIME \ - --runtime-version $RUNTIME_VERSION \ - --functions-version 4 \ - --name $FUNCTION_APP_NAME \ - --os-type linux \ - --storage-account $STORAGE_ACCOUNT_NAME \ - --only-show-errors 1>/dev/null - -if [ $? -eq 0 ]; then - echo "Function app [$FUNCTION_APP_NAME] created successfully." -else - echo "Failed to create function app [$FUNCTION_APP_NAME]." - exit 1 -fi - -# Assign the Storage Blob Data Contributor role to the managed identity with the storage account as scope -ROLE="Storage Blob Data Contributor" -echo "Checking if the managed identity with principal ID [$PRINCIPAL_ID] has the [$ROLE] role assignment on storage account [$STORAGE_ACCOUNT_NAME]..." -current=$($AZ role assignment list \ - --assignee "$PRINCIPAL_ID" \ - --scope "$STORAGE_ACCOUNT_RESOURCE_ID" \ - --query "[?roleDefinitionName=='$ROLE'].roleDefinitionName" \ - --output tsv 2>/dev/null) - -if [[ $current == "$ROLE" ]]; then - echo "Managed identity already has the [$ROLE] role assignment on storage account [$STORAGE_ACCOUNT_NAME]" -else - echo "Managed identity does not have the [$ROLE] role assignment on storage account [$STORAGE_ACCOUNT_NAME]" - echo "Creating role assignment: assigning [$ROLE] role to managed identity on storage account [$STORAGE_ACCOUNT_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 "$STORAGE_ACCOUNT_RESOURCE_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 on storage account [$STORAGE_ACCOUNT_NAME]" - else - echo "Failed to assign [$ROLE] role to managed identity on storage account [$STORAGE_ACCOUNT_NAME]" - exit - fi -fi - -# Assign the Storage Queue Data Contributor role to the managed identity with the storage account as scope -ROLE="Storage Queue Data Contributor" -echo "Checking if the managed identity with principal ID [$PRINCIPAL_ID] has the [$ROLE] role assignment on storage account [$STORAGE_ACCOUNT_NAME]..." -current=$($AZ role assignment list \ - --assignee "$PRINCIPAL_ID" \ - --scope "$STORAGE_ACCOUNT_RESOURCE_ID" \ - --query "[?roleDefinitionName=='$ROLE'].roleDefinitionName" \ - --output tsv 2>/dev/null) - -if [[ $current == "$ROLE" ]]; then - echo "Managed identity already has the [$ROLE] role assignment on storage account [$STORAGE_ACCOUNT_NAME]" -else - echo "Managed identity does not have the [$ROLE] role assignment on storage account [$STORAGE_ACCOUNT_NAME]" - echo "Creating role assignment: assigning [$ROLE] role to managed identity on storage account [$STORAGE_ACCOUNT_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 "$STORAGE_ACCOUNT_RESOURCE_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 on storage account [$STORAGE_ACCOUNT_NAME]" - else - echo "Failed to assign [$ROLE] role to managed identity on storage account [$STORAGE_ACCOUNT_NAME]" - exit - fi -fi - -# Set function app settings -echo "Setting function app settings for [$FUNCTION_APP_NAME]..." - -# Set storage URIs based on environment -BLOB_SERVICE_URI="https://${STORAGE_ACCOUNT_NAME}.blob.core.windows.net" -QUEUE_SERVICE_URI="https://${STORAGE_ACCOUNT_NAME}.queue.core.windows.net" -TABLE_SERVICE_URI="https://${STORAGE_ACCOUNT_NAME}.table.core.windows.net" - - -$AZ functionapp config appsettings set \ - --name $FUNCTION_APP_NAME \ - --resource-group $RESOURCE_GROUP_NAME \ - --settings \ - SCM_DO_BUILD_DURING_DEPLOYMENT='true' \ - ENABLE_ORYX_BUILD='true' \ - AZURE_CLIENT_ID="$CLIENT_ID" \ - AzureWebJobsStorage="$STORAGE_CONNECTION_STRING" \ - STORAGE_ACCOUNT_CONNECTION_STRING__blobServiceUri="$BLOB_SERVICE_URI" \ - STORAGE_ACCOUNT_CONNECTION_STRING__queueServiceUri="$QUEUE_SERVICE_URI" \ - STORAGE_ACCOUNT_CONNECTION_STRING__tableServiceUri="$TABLE_SERVICE_URI" \ - INPUT_STORAGE_CONTAINER_NAME="$INPUT_STORAGE_CONTAINER_NAME" \ - OUTPUT_STORAGE_CONTAINER_NAME="$OUTPUT_STORAGE_CONTAINER_NAME" \ - FUNCTIONS_WORKER_RUNTIME="$RUNTIME" \ - FUNCTIONS_EXTENSION_VERSION="~4" \ - --only-show-errors 1>/dev/null - -if [ $? -eq 0 ]; then - echo "Function app settings for [$FUNCTION_APP_NAME] set successfully." -else - echo "Failed to set function app settings for [$FUNCTION_APP_NAME]." - exit 1 -fi - -# CD into the function app directory -cd ../src || exit - -# Remove any existing zip package of the function app -if [ -f "$ZIPFILE" ]; then - rm "$ZIPFILE" -fi - -# Create the zip package of the function app -echo "Creating zip package of the function app..." -zip -r "$ZIPFILE" function_app.py host.json requirements.txt - -# Deploy the function app -echo "Deploying function app [$FUNCTION_APP_NAME] with zip file [$ZIPFILE]..." -$AZ functionapp deploy \ - --resource-group "$RESOURCE_GROUP_NAME" \ - --name "$FUNCTION_APP_NAME" \ - --src-path "$ZIPFILE" \ - --type zip \ - --async true 1>/dev/null - -if [ $? -eq 0 ]; then - echo "Function app [$FUNCTION_APP_NAME] deployed successfully." -else - echo "Failed to deploy function app [$FUNCTION_APP_NAME]." - exit 1 -fi - -# Remove the zip package of the function app -if [ -f "$ZIPFILE" ]; then - rm "$ZIPFILE" -fi -``` +These scripts eliminate manual configuration steps and enable one-command deployment of the entire infrastructure. > [!NOTE] > You can use the `azlocal` CLI as a drop-in replacement for the `az` CLI to direct all commands to the LocalStack for Azure emulator. Alternatively, run `azlocal start_interception` to automatically intercept and redirect all `az` commands to LocalStack. To revert back to the default behavior and send commands to the Azure cloud, run `azlocal stop_interception`. + ## Deployment You can set up the Azure emulator by utilizing 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: diff --git a/samples/function-app-managed-identity/python/terraform/README.md b/samples/function-app-managed-identity/python/terraform/README.md index 8e26171..0c6a601 100644 --- a/samples/function-app-managed-identity/python/terraform/README.md +++ b/samples/function-app-managed-identity/python/terraform/README.md @@ -17,7 +17,7 @@ Before deploying this solution, ensure you have the following tools installed: ### Installing azlocal CLI -The deployment script uses the `azlocal` CLI instead of the standard Azure CLI to work with LocalStack. Install it using: +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 @@ -27,7 +27,7 @@ For more information, see [Get started with the az tool on LocalStack](https://a ## Architecture Overview -The Terraform modules deploy the following Azure resources: +The [main.tf](main.tf) Terraform module creates the following Azure resources: 1. [Azure Storage Account](https://learn.microsoft.com/en-us/azure/storage/common/storage-account-overview): Provides blob storage with `input` and `output` containers for storing text blobs processed by the function app. 2. [Azure App Service Plan](https://learn.microsoft.com/en-us/azure/app-service/overview-hosting-plans): Defines the compute resources (CPU, memory, and scaling options) that host the Azure Functions app. @@ -37,281 +37,22 @@ The Terraform modules deploy the following Azure resources: For more information on the sample application, see [Azure Functions App with Managed Identity](../README.md). -## Terraform Modules - -Below is a summary of the key Terraform modules included in this deployment: - -- **`main.tf`**: Defines all Azure resources and their configuration. -- **`variables.tf`**: Declares input variables and validation rules. -- **`outputs.tf`**: Specifies output values after deployment. -- **`providers.tf`**: Configures the Terraform provider for Azure. - -Below you can read the declarative code in HashiCorp Configuration Language (HCL). The `main.tf` module uses conditional provisioning for the user-assigned managed identity and role assignments resources. In Terraform, you can use the `count` argument in a conditional expression to decide whether creating resources or not. For example, the `count = var.managed_identity_type == "UserAssigned" ? 1 : 0` expression instructs Terraform to create the user-assigned managed identity resource when the value of the variable named `managed_identity_type` is set to `UserAssigned`. Refer to ​​[Conditional Expressions](https://developer.hashicorp.com/terraform/language/expressions/conditionals) for more information. - -```terraform -# Local Variables -locals { - resource_group_name = "${var.prefix}-rg" - storage_account_name = "${var.prefix}storage${var.suffix}" - app_service_plan_name = "${var.prefix}-app-service-plan-${var.suffix}" - function_app_name = "${var.prefix}-functionapp-${var.suffix}" - managed_identity_name = "${var.prefix}-identity-${var.suffix}" -} - -# Create a resource group -resource "azurerm_resource_group" "example" { - location = var.location - name = local.resource_group_name - tags = var.tags -} - -# Create a storage account -resource "azurerm_storage_account" "example" { - name = local.storage_account_name - resource_group_name = azurerm_resource_group.example.name - location = azurerm_resource_group.example.location - account_replication_type = var.account_replication_type - account_kind = var.account_kind - account_tier = var.account_tier - tags = var.tags - - identity { - type = "SystemAssigned" - } - - lifecycle { - ignore_changes = [ - tags - ] - } -} - -# Create input storage container -resource "azurerm_storage_container" "input" { - name = var.input_container_name - storage_account_id = azurerm_storage_account.example.id - container_access_type = "private" -} - -# Create output storage container -resource "azurerm_storage_container" "output" { - name = var.output_container_name - storage_account_id = azurerm_storage_account.example.id - container_access_type = "private" -} - -# Conditionally create a user assigned identity for the function app -resource "azurerm_user_assigned_identity" "identity" { - count = var.managed_identity_type == "UserAssigned" ? 1 : 0 - - name = local.managed_identity_name - resource_group_name = azurerm_resource_group.example.name - location = azurerm_resource_group.example.location -} - -# Assign Storage Blob Data Contributor role to the function app identity -resource "azurerm_role_assignment" "blob_contributor" { - scope = azurerm_storage_account.example.id - role_definition_name = "Storage Blob Data Contributor" - principal_id = var.managed_identity_type == "UserAssigned" ? azurerm_user_assigned_identity.identity[0].principal_id : azurerm_linux_function_app.example.identity[0].principal_id -} - -# Assign Storage Queue Data Contributor role to the function app identity -resource "azurerm_role_assignment" "queue_contributor" { - scope = azurerm_storage_account.example.id - role_definition_name = "Storage Queue Data Contributor" - principal_id = var.managed_identity_type == "UserAssigned" ? azurerm_user_assigned_identity.identity[0].principal_id : azurerm_linux_function_app.example.identity[0].principal_id -} - -# Create a service plan -resource "azurerm_service_plan" "example" { - 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 - tags = var.tags - - lifecycle { - ignore_changes = [ - tags - ] - } -} - -# Create a function app -resource "azurerm_linux_function_app" "example" { - name = local.function_app_name - resource_group_name = azurerm_resource_group.example.name - location = azurerm_resource_group.example.location - service_plan_id = azurerm_service_plan.example.id - storage_account_name = azurerm_storage_account.example.name - storage_account_access_key = azurerm_storage_account.example.primary_access_key - https_only = var.https_only - public_network_access_enabled = var.public_network_access_enabled - tags = var.tags - functions_extension_version = "~4" - - identity { - type = var.managed_identity_type - identity_ids = var.managed_identity_type == "UserAssigned" ? [ - azurerm_user_assigned_identity.identity[0].id - ] : [] - } - - site_config { - always_on = var.always_on - minimum_tls_version = var.minimum_tls_version - application_stack { - python_version = var.python_version - } - } - - app_settings = { - SCM_DO_BUILD_DURING_DEPLOYMENT = "true" - ENABLE_ORYX_BUILD = "true" - AZURE_CLIENT_ID = var.managed_identity_type == "UserAssigned" ? azurerm_user_assigned_identity.identity[0].client_id : "" - AzureWebJobsStorage = "DefaultEndpointsProtocol=https;AccountName=${azurerm_storage_account.example.name};AccountKey=${azurerm_storage_account.example.primary_access_key};EndpointSuffix=core.windows.net;" - STORAGE_ACCOUNT_CONNECTION_STRING__blobServiceUri = azurerm_storage_account.example.primary_blob_endpoint - STORAGE_ACCOUNT_CONNECTION_STRING__queueServiceUri = azurerm_storage_account.example.primary_queue_endpoint - STORAGE_ACCOUNT_CONNECTION_STRING__tableServiceUri = azurerm_storage_account.example.primary_table_endpoint - INPUT_STORAGE_CONTAINER_NAME = var.input_container_name - OUTPUT_STORAGE_CONTAINER_NAME = var.output_container_name - FUNCTIONS_WORKER_RUNTIME = var.runtime_name - FUNCTIONS_EXTENSION_VERSION = "~4" - } - - lifecycle { - ignore_changes = [ - tags - ] - } -} - -# Create an app source control configuration -resource "azurerm_app_service_source_control" "example" { - count = var.repo_url == "" ? 0 : 1 - app_id = azurerm_linux_function_app.example.id - repo_url = var.repo_url - branch = "main" -} -``` - -## Deployment Script - -You can use the `deploy.sh` script to automate the deployment of all Azure resources and the sample application in a single step, streamlining setup and reducing manual configuration. Before running the script, customize the variable values based on your needs. In particular, use the `MANAGED_IDENTITY_TYPE` variable to specify the type of managed identity to provision: `SystemAssigned` or `UserAssigned`. - -```bash -#!/bin/bash - -# Variables -PREFIX='local' -SUFFIX='test' -LOCATION='westeurope' -MANAGED_IDENTITY_TYPE='UserAssigned' # SystemAssigned or UserAssigned -CURRENT_DIR="$(cd "$(dirname "$0")" && pwd)" -ZIPFILE="function_app.zip" -ENVIRONMENT=$(az account show --query environmentName --output tsv) - -# Change the current directory to the script's directory -cd "$CURRENT_DIR" || exit - -# Run terraform init and apply -if [[ $ENVIRONMENT == "LocalStack" ]]; then - echo "Using tflocal and azlocal for LocalStack emulator environment and ." - TERRAFORM="tflocal" - AZ="azlocal" -else - echo "Using standard terraform and az for AzureCloud environment." - TERRAFORM="terraform" - AZ="az" -fi - -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 "managed_identity_type=$MANAGED_IDENTITY_TYPE" +## Provisioning Scripts -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) -FUNCTION_APP_NAME=$(terraform output -raw function_app_name) - -if [[ -z "$RESOURCE_GROUP_NAME" || -z "$FUNCTION_APP_NAME" ]]; then - echo "Resource Group Name or Function App Name is empty. Exiting." - exit 1 -fi - -# Print the variables -echo "Resource Group: $RESOURCE_GROUP_NAME" -echo "Function App: $FUNCTION_APP_NAME" - -# CD into the function app directory -cd ../src || exit - -# Remove any existing zip package of the function app -if [ -f "$ZIPFILE" ]; then - rm "$ZIPFILE" -fi - -# Create the zip package of the function app -echo "Creating zip package of the function app..." -zip -r "$ZIPFILE" function_app.py host.json requirements.txt - -# Deploy the function app -echo "Deploying function app [$FUNCTION_APP_NAME] with zip file [$ZIPFILE]..." -$AZ functionapp deploy \ - --resource-group "$RESOURCE_GROUP_NAME" \ - --name "$FUNCTION_APP_NAME" \ - --src-path "$ZIPFILE" \ - --type zip \ - --async true 1>/dev/null - -if [ $? -eq 0 ]; then - echo "Function app [$FUNCTION_APP_NAME] deployed successfully." -else - echo "Failed to deploy function app [$FUNCTION_APP_NAME]." - exit 1 -fi - -# Remove the zip package of the function app -if [ -f "$ZIPFILE" ]; then - rm "$ZIPFILE" -fi -``` +The [deploy.sh](deploy.sh) script automates the deployment of all Azure resources and the sample application in a single step. Before running the script, customize the variable values based on your needs. In particular, use the `MANAGED_IDENTITY_TYPE` variable to specify the type of managed identity to provision: `SystemAssigned` or `UserAssigned`. > **Note** > You can use the `azlocal` CLI as a drop-in replacement for the `az` CLI to direct all commands to the LocalStack for Azure emulator. Alternatively, run `azlocal start_interception` to automatically intercept and redirect all `az` commands to LocalStack. Likewise, the `tflocal` is a local replacement for the standard `terraform` CLI, allowing you to run Terraform commands against LocalStack's Azure emulation environment. For more information, see [Get started with the az tool on LocalStack](https://azure.localstack.cloud/user-guides/sdks/az/). -The `deploy.sh` script executes the following steps: +The [deploy.sh](deploy.sh) 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. -- Extracts resource names and outputs from the Terraform deployment. -- Packages the code of the serverless application into a zip file for deployment. -- Deploys the zip package to the Azure Functions App using the LocalStack Azure CLI. +- 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 +- Extracts resource names and outputs from the Terraform deployment +- Packages the code of the serverless application into a zip file for deployment +- Deploys the zip package to the Azure Functions App using the LocalStack Azure CLI ## Configuration diff --git a/samples/function-app-storage-http/dotnet/bicep/README.md b/samples/function-app-storage-http/dotnet/bicep/README.md index bdec8a3..6c6b402 100644 --- a/samples/function-app-storage-http/dotnet/bicep/README.md +++ b/samples/function-app-storage-http/dotnet/bicep/README.md @@ -17,7 +17,7 @@ Before deploying this solution, ensure you have the following tools installed: ### Installing azlocal CLI -The deployment script uses the `azlocal` CLI instead of the standard Azure CLI to work with LocalStack. Install it using: +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 @@ -27,7 +27,7 @@ For more information, see [Get started with the az tool on LocalStack](https://a ## Architecture Overview -The `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, while the `main.bicep` Bicep module creates the following Azure resources: +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, while the [main.bicep](main.bicep) Bicep module creates the following Azure resources: 1. [Azure Storage Account](https://learn.microsoft.com/en-us/azure/storage/common/storage-account-overview): Provides blob containers, queues, and tables for the gaming system - StorageV2 kind with Hot access tier @@ -41,455 +41,18 @@ The `deploy.sh` script creates the [Azure Resource Group](https://learn.microsof The system implements a complete gaming scoreboard with multiple Azure Functions that handle HTTP requests, process blob uploads, manage queue messages, and maintain game statistics. For more information, see [Azure Functions Sample with LocalStack for Azure](../README.md). -## Bicep Templates - -The `main.bicep` Bicep template defines all Azure resources using declarative syntax: - -```bicep -@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('Location for all resources.') -param location string = resourceGroup().location - -@description('Specifies the sku of the Azure Storage account.') -param storageAccountSku string = 'Standard_LRS' - -@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 language runtime used by the Azure Functions App.') -@allowed([ - 'dotnet' - 'dotnet-isolated' - 'python' - 'java' - 'node' - 'powerShell' - 'custom' -]) -param runtimeName string - -@description('Specifies the target language version used by the Azure Functions App.') -param runtimeVersion string - -@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 functionAppKind string = 'functionapp,linux' - -@description('Specifies whether HTTPS is enforced for the Azure Functions App.') -param httpsOnly bool = false - -@description('Specifies the minimum TLS version for the Azure Functions App.') -@allowed([ - '1.0' - '1.1' - '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('Optional Git Repo URL') -param repoUrl string = ' ' - -@description('Specifies the name of the input container.') -param inputContainerName string = 'input' - -@description('Specifies the name of the output container.') -param outputContainerName string = 'output' - -@description('Specifies the name of the input queue.') -param inputQueueName string = 'input' - -@description('Specifies the name of the output queue.') -param outputQueueName string = 'output' - -@description('Specifies the name of the trigger queue.') -param triggerQueueName string = 'trigger' - -@description('Specifies the name of the input table.') -param inputTableName string = 'input' - -@description('Specifies the name of the output table.') -param outputTableName string = 'output' - -@description('Specifies the comma-separated list of player names.') -param playerNames string = 'Alice,Anastasia,Paolo,Leo,Mia' - -@description('Specifies the timer schedule for the timer triggered function.') -param timerSchedule string = '0 */1 * * * *' - -@description('Specifies the tags to be applied to the resources.') -param tags object = { - environment: 'test' - iac: 'bicep' -} - -var functionAppName = '${prefix}-functionapp-${suffix}' -var appServicePlanPortalName = '${prefix}-app-service-plan-${suffix}' -var storageAccountName = '${prefix}storage${suffix}' -var storageAccountConnectionString = 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};EndpointSuffix=${environment().suffixes.storage};AccountKey=${storageAccount.listKeys().keys[0].value}' - -resource storageAccount 'Microsoft.Storage/storageAccounts@2025-01-01' = { - name: storageAccountName - location: location - tags: tags - sku: { - name: storageAccountSku - } - kind: 'StorageV2' - properties: { - accessTier: 'Hot' - } -} - -resource appServicePlan 'Microsoft.Web/serverfarms@2024-11-01' = { - name: appServicePlanPortalName - location: location - tags: tags - kind: appServicePlanKind - sku: { - tier: skuTier - name: skuName - } - properties: { - reserved: reserved - zoneRedundant: zoneRedundant - maximumElasticWorkerCount: skuTier == 'FlexConsumption' ? 1 : 20 - } -} - -resource functionApp 'Microsoft.Web/sites@2024-11-01' = { - name: functionAppName - location: location - tags: tags - kind: functionAppKind - properties: { - httpsOnly: httpsOnly - reserved: true - serverFarmId: appServicePlan.id - virtualNetworkSubnetId: null - siteConfig: { - linuxFxVersion: toUpper('${runtimeName}|${runtimeVersion}') - minTlsVersion: minTlsVersion - ftpsState: 'FtpsOnly' - publicNetworkAccess: publicNetworkAccess - } - } - identity: { - type: 'SystemAssigned' - } - - resource configAppSettings 'config' = { - name: 'appsettings' - properties: { - SCM_DO_BUILD_DURING_DEPLOYMENT: 'true' - AzureWebJobsStorage: storageAccountConnectionString - WEBSITE_STORAGE_ACCOUNT_CONNECTION_STRING: storageAccountConnectionString - WEBSITE_CONTENTAZUREFILECONNECTIONSTRING: storageAccountConnectionString - STORAGE_ACCOUNT_CONNECTION_STRING: storageAccountConnectionString - INPUT_STORAGE_CONTAINER_NAME: inputContainerName - OUTPUT_STORAGE_CONTAINER_NAME: outputContainerName - INPUT_QUEUE_NAME: inputQueueName - OUTPUT_QUEUE_NAME: outputQueueName - TRIGGER_QUEUE_NAME: triggerQueueName - INPUT_TABLE_NAME: inputTableName - OUTPUT_TABLE_NAME: outputTableName - PLAYER_NAMES: playerNames - TIMER_SCHEDULE: timerSchedule - FUNCTIONS_WORKER_RUNTIME: runtimeName - } - } -} - -resource functionAppSourceControl 'Microsoft.Web/sites/sourcecontrols@2024-11-01' = if (contains(repoUrl,'http')){ - name: 'web' - parent: functionApp - properties: { - repoUrl: repoUrl - branch: 'main' - isManualIntegration: true - } -} - -output functionAppName string = functionAppName -output storageAccountName string = storageAccountName -output storageAccountConnectionString string = storageAccountConnectionString -output test1 string = storageAccount.properties.accessTier -output test2 string = functionApp.properties.enabledHostNames[0] -output test3 string = '${functionApp.kind} + ${appServicePlan.kind}' -output test4 string = split(functionApp.id, '/')[3] -``` - -## Deployment Script - -Use the `deploy.sh` script to automate the provisioning of Azure resources and deployment of the Azure Functions App. - -```bash -#!/bin/bash - -# Variables -TEMPLATE="main.bicep" -PARAMETERS="main.bicepparam" -RESOURCE_GROUP_NAME="bingo-rg" -LOCATION="westeurope" -VALIDATE_TEMPLATE=1 -USE_WHAT_IF=0 -SUBSCRIPTION_NAME=$(az account show --query name --output tsv) -CURRENT_DIR="$(cd "$(dirname "$0")" && pwd)" -ZIPFILE="function_app.zip" -SUBSCRIPTION_NAME=$(az account show --query name --output tsv) -ENVIRONMENT=$(az account show --query environmentName --output tsv) +## Provisioning Scripts -# Change the current directory to the script's directory -cd "$CURRENT_DIR" || exit +See [deploy.sh](deploy.sh) for the complete deployment automation script. The script performs: -# Choose the appropriate CLI based on the environment -if [[ $ENVIRONMENT == "LocalStack" ]]; then - echo "Using azlocal for LocalStack emulator environment." - AZ="azlocal" -else - echo "Using standard az for AzureCloud environment." - AZ="az" -fi - -# 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 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 - -# 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 [$TEMPLATE]..." - $AZ deployment group what-if \ - --resource-group $RESOURCE_GROUP_NAME \ - --template-file $TEMPLATE \ - --parameters $PARAMETERS \ - --parameters \ - location=$LOCATION - - if [[ $? == 0 ]]; then - echo "Bicep template [$TEMPLATE] validation succeeded" - else - echo "Failed to validate Bicep template [$TEMPLATE]" - exit - fi - else - # Validate the Bicep template - echo "Validating Bicep template [$TEMPLATE]..." - output=$($AZ deployment group validate \ - --resource-group $RESOURCE_GROUP_NAME \ - --template-file $TEMPLATE \ - --parameters $PARAMETERS \ - --parameters \ - location=$LOCATION) - - if [[ $? == 0 ]]; then - echo "Bicep template [$TEMPLATE] validation succeeded" - else - echo "Failed to validate Bicep template [$TEMPLATE]" - echo "$output" - exit - fi - fi -fi - -# Deploy the Bicep template -echo "Deploying Bicep template [$TEMPLATE]..." -if DEPLOYMENT_OUTPUTS=$($AZ deployment group create \ - --resource-group $RESOURCE_GROUP_NAME \ - --only-show-errors \ - --template-file $TEMPLATE \ - --parameters $PARAMETERS \ - --parameters location=$LOCATION \ - --query 'properties.outputs' -o json); then - echo "Bicep template [$TEMPLATE] deployed successfully. Outputs:" - echo "$DEPLOYMENT_OUTPUTS" | jq . - FUNCTION_APP_NAME=$(echo "$DEPLOYMENT_OUTPUTS" | jq -r '.functionAppName.value') - STORAGE_ACCOUNT_NAME=$(echo "$DEPLOYMENT_OUTPUTS" | jq -r '.storageAccountName.value') - STORAGE_ACCOUNT_CONNECTION_STRING=$(echo "$DEPLOYMENT_OUTPUTS" | jq -r '.storageAccountConnectionString.value') - echo "Function App Name: $FUNCTION_APP_NAME" - echo "Storage Account Name: $STORAGE_ACCOUNT_NAME" - echo "Storage Account Connection String: $STORAGE_ACCOUNT_CONNECTION_STRING" -else - echo "Failed to deploy Bicep template [$TEMPLATE]" - exit 1 -fi - -if [[ -z "$FUNCTION_APP_NAME" || -z "$STORAGE_ACCOUNT_NAME" ]]; then - echo "Function App Name or Storage Account Name is empty. Exiting." - exit 1 -fi - -# Print the application settings of the function app -echo "Retrieving application settings for function app [$FUNCTION_APP_NAME]..." -$AZ webapp config appsettings list \ - --resource-group "$RESOURCE_GROUP_NAME" \ - --name "$FUNCTION_APP_NAME" - -# CD into the function app directory -cd ../src/sample || exit - -# Clean and build the project in Release configuration -dotnet clean -dotnet build -c Release - -# Publish the project to a publish directory -dotnet publish -c Release -o publish - -# Create deployment zip from the published output -cd publish || exit -zip -r ../$ZIPFILE . -cd .. || exit - -# Deploy the function app using the zip file -echo "Deploying function app [$FUNCTION_APP_NAME]..." -$AZ functionapp deploy \ - --resource-group "$RESOURCE_GROUP_NAME" \ - --name "$FUNCTION_APP_NAME" \ - --src-path $ZIPFILE \ - --type zip 1>/dev/null - -# Remove the zip package of the function app -if [ -f "$ZIPFILE" ]; then - rm "$ZIPFILE" -fi -``` +- Creates the resource group if it doesn't exist +- Optionally validates the Bicep template +- Optionally runs what-if deployment for preview +- Deploys the**main.bicep** template with parameters from [main.bicepparam](main.bicepparam) +- Extracts deployment outputs (Function App name, Storage Account details) +- Builds and publishes the .NET application +- Creates a zip package and deploys to the Function App -> **Note** -> You can use the `azlocal` CLI as a drop-in replacement for the `az` CLI to direct all commands to the LocalStack for Azure emulator. Alternatively, run `azlocal start_interception` to automatically intercept and redirect all `az` commands to LocalStack. For more information, see [Get started with the az tool on LocalStack](https://azure.localstack.cloud/user-guides/sdks/az/). - -The `deploy.sh` script executes the following steps: - -- Specifies the variables used during deployment. -- Creates the resource group if it does not exist. -- Conditionally validates the `main.bicep` module to check its syntax is correct and all parameters make sense. -- Conditionally runs a what-if deployment to execute a dry run to preview the resources that will be created, updated, or deleted. -- Runs the `main.bicep` template to create all the Azure resources. -- Collects important information from the deployment (like resource names) for later use. -- Uses jq (a JSON tool) to extract the names of resources we just created. -- Shows us all the settings that got applied to the Function App. -- Removes previous build artifacts for consistency. -- Creates self-contained deployment with all dependencies. -- Creates zip archive in format expected by Azure Functions. -- Uploads pre-built application package to the newly created Azure Functions app. - -> **Note** -> Azure CLI commands supports `--verbose` argument to print execution details and the `--debug` flag to show low-level REST calls for debugging. For more information, see [Get started with Azure CLI](https://learn.microsoft.com/en-us/cli/azure/get-started-with-azure-cli) ## Deployment diff --git a/samples/function-app-storage-http/dotnet/scripts/README.md b/samples/function-app-storage-http/dotnet/scripts/README.md index a81349b..94b1618 100644 --- a/samples/function-app-storage-http/dotnet/scripts/README.md +++ b/samples/function-app-storage-http/dotnet/scripts/README.md @@ -17,7 +17,7 @@ Before deploying this solution, ensure you have the following tools installed: ### Installing azlocal CLI -The deployment script uses the `azlocal` CLI to work with LocalStack. Install it using: +The [deploy.sh](deploy.sh) Bash script uses the `azlocal` CLI to work with LocalStack. Install it using: ```bash pip install azlocal @@ -27,179 +27,25 @@ For more information, see [Get started with the az tool on LocalStack](https://a ## Architecture Overview -This CLI deployment creates the following Azure resources using direct Azure CLI commands: +This [deploy.sh](deploy.sh) script creates the following Azure resources using Azure CLI commands: -1. [Azure Resource Group](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/manage-resource-groups-cli): Logical container for all gaming system resources -2. [Azure Storage Account](https://learn.microsoft.com/en-us/azure/storage/common/storage-account-overview): Provides blob containers, queues, and tables for the gaming system -3. [Azure Linux Function App](https://learn.microsoft.com/en-us/azure/azure-functions/functions-overview) Serverless compute platform hosting the gaming logic with consumption plan +1. [Azure Resource Group](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/manage-resource-groups-cli): Logical container for all gaming system resources. +2. [Azure Storage Account](https://learn.microsoft.com/en-us/azure/storage/common/storage-account-overview): Provides blob containers, queues, and tables for the gaming system. +3. [Azure Linux Function App](https://learn.microsoft.com/en-us/azure/azure-functions/functions-overview) Serverless compute platform hosting the gaming logic with consumption plan. The system implements a complete gaming scoreboard with multiple Azure Functions that handle HTTP requests, process blob uploads, manage queue messages, and maintain game statistics. For more information, see [Azure Functions Sample with LocalStack for Azure](../README.md). -## Deployment Script +## Provisioning Scripts -The [deploy.sh](deploy.sh) script creates resources and deploys the .NET application using native Azure CLI command. - -```bash -#!/bin/bash - -# Variables -PREFIX='local' -SUFFIX='test' -LOCATION='westeurope' -FUNCTION_APP_NAME="${PREFIX}-func-${SUFFIX}" -STORAGE_ACCOUNT_NAME="${PREFIX}storage${SUFFIX}" -RESOURCE_GROUP_NAME="${PREFIX}-rg" -RUNTIME="DOTNET-ISOLATED" -RUNTIME_VERSION="9" -PLAYER_NAMES="Alice,Anastasia,Paolo,Leo,Mia" -INPUT_STORAGE_CONTAINER_NAME="input" -OUTPUT_STORAGE_CONTAINER_NAME="output" -INPUT_QUEUE_NAME="input" -OUTPUT_QUEUE_NAME="output" -TRIGGER_QUEUE_NAME="trigger" -INPUT_TABLE_NAME="scoreboards" -OUTPUT_TABLE_NAME="winners" -CURRENT_DIR="$(cd "$(dirname "$0")" && pwd)" -SUBSCRIPTION_NAME=$(az account show --query name --output tsv) -ENVIRONMENT=$(az account show --query environmentName --output tsv) - -# Change the current directory to the script's directory -cd "$CURRENT_DIR" || exit - -# Choose the appropriate CLI based on the environment -if [[ $ENVIRONMENT == "LocalStack" ]]; then - echo "Using azlocal for LocalStack emulator environment." - AZ="azlocal" - FUNC="funclocal" -else - echo "Using standard az for AzureCloud environment." - AZ="az" - FUNC="func" -fi - -# Create a resource group -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 - -# Create a storage account -echo "Checking if storage account [$STORAGE_ACCOUNT_NAME] exists in the resource group [$RESOURCE_GROUP_NAME]..." -$AZ storage account show \ - --name $STORAGE_ACCOUNT_NAME \ - --resource-group $RESOURCE_GROUP_NAME &>/dev/null - -if [[ $? != 0 ]]; then - echo "No storage account [$STORAGE_ACCOUNT_NAME] exists in the [$RESOURCE_GROUP_NAME] resource group." - echo "Creating storage account [$STORAGE_ACCOUNT_NAME] in the [$RESOURCE_GROUP_NAME] resource group..." - $AZ storage account create \ - --name $STORAGE_ACCOUNT_NAME \ - --location $LOCATION \ - --resource-group $RESOURCE_GROUP_NAME \ - --sku Standard_LRS 1>/dev/null - - if [ $? -eq 0 ]; then - echo "Storage account [$STORAGE_ACCOUNT_NAME] created successfully in the [$RESOURCE_GROUP_NAME] resource group." - else - echo "Failed to create storage account [$STORAGE_ACCOUNT_NAME] in the [$RESOURCE_GROUP_NAME] resource group." - exit 1 - fi -else - echo "Storage account [$STORAGE_ACCOUNT_NAME] already exists in the [$RESOURCE_GROUP_NAME] resource group." -fi - -# Get the storage account key -echo "Getting storage account key for [$STORAGE_ACCOUNT_NAME]..." -STORAGE_ACCOUNT_KEY=$($AZ storage account keys list \ - --account-name $STORAGE_ACCOUNT_NAME \ - --resource-group $RESOURCE_GROUP_NAME \ - --query "[0].value" \ - --output tsv) - -if [ -n "$STORAGE_ACCOUNT_KEY" ]; then - echo "Storage account key retrieved successfully: [$STORAGE_ACCOUNT_KEY]" -else - echo "Failed to retrieve storage account key." - exit 1 -fi - -# Create the function app -echo "Creating function app [$FUNCTION_APP_NAME]..." -$AZ functionapp create \ - --resource-group $RESOURCE_GROUP_NAME \ - --consumption-plan-location $LOCATION \ - --runtime $RUNTIME \ - --runtime-version $RUNTIME_VERSION \ - --functions-version 4 \ - --name $FUNCTION_APP_NAME \ - --os-type linux \ - --storage-account $STORAGE_ACCOUNT_NAME 1>/dev/null - -if [ $? -eq 0 ]; then - echo "Function app [$FUNCTION_APP_NAME] created successfully." -else - echo "Failed to create function app [$FUNCTION_APP_NAME]." - exit 1 -fi - -# Construct the storage connection string for LocalStack -STORAGE_CONNECTION_STRING="DefaultEndpointsProtocol=https;AccountName=$STORAGE_ACCOUNT_NAME;AccountKey=$STORAGE_ACCOUNT_KEY;EndpointSuffix=core.windows.net" - -# Set function app settings -echo "Setting function app settings for [$FUNCTION_APP_NAME]..." -$AZ functionapp config appsettings set \ - --name $FUNCTION_APP_NAME \ - --resource-group $RESOURCE_GROUP_NAME \ - --settings \ - AzureWebJobsStorage="$STORAGE_CONNECTION_STRING" \ - STORAGE_ACCOUNT_CONNECTION_STRING="$STORAGE_CONNECTION_STRING" \ - WEBSITE_CONTENTAZUREFILECONNECTIONSTRING="$STORAGE_CONNECTION_STRING" \ - INPUT_STORAGE_CONTAINER_NAME="$INPUT_STORAGE_CONTAINER_NAME" \ - OUTPUT_STORAGE_CONTAINER_NAME="$OUTPUT_STORAGE_CONTAINER_NAME" \ - INPUT_QUEUE_NAME="$INPUT_QUEUE_NAME" \ - OUTPUT_QUEUE_NAME="$OUTPUT_QUEUE_NAME" \ - TRIGGER_QUEUE_NAME="$TRIGGER_QUEUE_NAME" \ - INPUT_TABLE_NAME="$INPUT_TABLE_NAME" \ - OUTPUT_TABLE_NAME="$OUTPUT_TABLE_NAME" \ - PLAYER_NAMES="$PLAYER_NAMES" \ - TIMER_SCHEDULE="0 */1 * * * *" \ - FUNCTIONS_WORKER_RUNTIME="dotnet-isolated" 1>/dev/null - -if [ $? -eq 0 ]; then - echo "Function app settings for [$FUNCTION_APP_NAME] set successfully." -else - echo "Failed to set function app settings for [$FUNCTION_APP_NAME]." - exit 1 -fi - -# CD into the function app directory -cd ../src/sample || exit - -# Publish the function app -echo "Publishing function app [$FUNCTION_APP_NAME]..." -$FUNC azure functionapp publish $FUNCTION_APP_NAME --dotnet-isolated --verbose --debug -``` - -> [!NOTE] -> You can use the `azlocal` CLI as a drop-in replacement for the `az` CLI to direct all commands to the LocalStack for Azure emulator. Alternatively, run `azlocal start_interception` to automatically intercept and redirect all `az` commands to LocalStack. To revert back to the default behavior and send commands to the Azure cloud, run `azlocal stop_interception`. +See [deploy.sh](deploy.sh) for the complete deployment script. The script performs: +- Detects environment (LocalStack vs Azure Cloud) and selects appropriate CLI +- Creates resource group if it doesn't exist +- Creates Storage Account and retrieves access key +- Creates Function App with consumption plan +- Constructs storage connection string +- Configures Function App settings (storage, queue, table, timer configurations) +- Publishes the .NET application using `funclocal` or `func azure functionapp publish` ## Deployment diff --git a/samples/function-app-storage-http/dotnet/terraform/README.md b/samples/function-app-storage-http/dotnet/terraform/README.md index b29ce6f..919a2f9 100644 --- a/samples/function-app-storage-http/dotnet/terraform/README.md +++ b/samples/function-app-storage-http/dotnet/terraform/README.md @@ -17,7 +17,7 @@ Before deploying this solution, ensure you have the following tools installed: ### Installing azlocal CLI -The deployment script uses the `azlocal` CLI instead of the standard Azure CLI to work with LocalStack. Install it using: +The [deploy.sh](deploy.sh) script uses the `azlocal` CLI instead of the standard Azure CLI to work with LocalStack. Install it using: ```bash pip install azlocal @@ -27,7 +27,7 @@ For more information, see [Get started with the az tool on LocalStack](https://a ## Architecture Overview -This Terraform deployment creates the following Azure resources: +The [main.tf](main.tf) Terraform module creates the following Azure resources: 1. [Azure Resource Group](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/manage-resource-groups-cli): Logical container for all gaming system resources 2. [Azure Storage Account](https://learn.microsoft.com/en-us/azure/storage/common/storage-account-overview): Provides blob containers, queues, and tables for the gaming system @@ -36,231 +36,18 @@ This Terraform deployment creates the following Azure resources: The system implements a complete gaming scoreboard with multiple Azure Functions that handle HTTP requests, process blob uploads, manage queue messages, and maintain game statistics. -## Terraform Modules - -Below is a summary of the key Terraform modules included in this deployment: - -- **`main.tf`**: Defines all Azure resources and their configuration. -- **`variables.tf`**: Declares input variables and validation rules. -- **`outputs.tf`**: Specifies output values after deployment. -- **`providers.tf`**: Configures the Terraform provider for Azure. - -Below you can read the declarative code in HashiCorp Configuration Language (HCL): - -```terraform -# Local Variables -locals { - storage_account_name = "${var.prefix}storage${var.suffix}" - app_service_plan_name = "${var.prefix}-app-service-plan-${var.suffix}" - function_app_name = "${var.prefix}-functionapp-${var.suffix}" -} - -# Create a resource group -resource "azurerm_resource_group" "example" { - location = var.location - name = var.resource_group_name - tags = var.tags -} - -# Create a storage account -resource "azurerm_storage_account" "example" { - name = local.storage_account_name - resource_group_name = azurerm_resource_group.example.name - location = azurerm_resource_group.example.location - account_replication_type = var.account_replication_type - account_kind = var.account_kind - account_tier = var.account_tier - tags = var.tags - - identity { - type = "SystemAssigned" - } - - lifecycle { - ignore_changes = [ - tags - ] - } -} - -# Create a service plan -resource "azurerm_service_plan" "example" { - 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 - tags = var.tags - - lifecycle { - ignore_changes = [ - tags - ] - } -} - -# Create a function app -resource "azurerm_linux_function_app" "example" { - name = local.function_app_name - resource_group_name = azurerm_resource_group.example.name - location = azurerm_resource_group.example.location - service_plan_id = azurerm_service_plan.example.id - storage_account_name = azurerm_storage_account.example.name - storage_account_access_key = azurerm_storage_account.example.primary_access_key - https_only = var.https_only - public_network_access_enabled = var.public_network_access_enabled - tags = var.tags - functions_extension_version = "~4" - - identity { - type = "SystemAssigned" - } - - site_config { - minimum_tls_version = var.minimum_tls_version - application_stack { - dotnet_version = var.dotnet_version - use_dotnet_isolated_runtime = true - } - } - - app_settings = { - FUNCTIONS_WORKER_RUNTIME = var.runtime_name - SCM_DO_BUILD_DURING_DEPLOYMENT = "true" - AzureWebJobsStorage = "DefaultEndpointsProtocol=https;AccountName=${azurerm_storage_account.example.name};AccountKey=${azurerm_storage_account.example.primary_access_key};EndpointSuffix=core.windows.net;" - WEBSITE_STORAGE_ACCOUNT_CONNECTION_STRING = "DefaultEndpointsProtocol=https;AccountName=${azurerm_storage_account.example.name};AccountKey=${azurerm_storage_account.example.primary_access_key};EndpointSuffix=core.windows.net;" - WEBSITE_CONTENTAZUREFILECONNECTIONSTRING = "DefaultEndpointsProtocol=https;AccountName=${azurerm_storage_account.example.name};AccountKey=${azurerm_storage_account.example.primary_access_key};EndpointSuffix=core.windows.net;" - STORAGE_ACCOUNT_CONNECTION_STRING = "DefaultEndpointsProtocol=https;AccountName=${azurerm_storage_account.example.name};AccountKey=${azurerm_storage_account.example.primary_access_key};EndpointSuffix=core.windows.net;" - INPUT_STORAGE_CONTAINER_NAME = var.input_container_name - OUTPUT_STORAGE_CONTAINER_NAME = var.output_container_name - INPUT_QUEUE_NAME = var.input_queue_name - OUTPUT_QUEUE_NAME = var.output_queue_name - TRIGGER_QUEUE_NAME = var.trigger_queue_name - INPUT_TABLE_NAME = var.input_table_name - OUTPUT_TABLE_NAME = var.output_table_name - PLAYER_NAMES = var.player_names - TIMER_SCHEDULE = var.timer_schedule - } - - lifecycle { - ignore_changes = [ - tags - ] - } -} - -# Create an app source control configuration -resource "azurerm_app_service_source_control" "example" { - count = var.repo_url == "" ? 0 : 1 - app_id = azurerm_linux_function_app.example.id - repo_url = var.repo_url - branch = "main" -} -``` - -## Deployment Script - -You can use the `deploy.sh` script to automate the deployment of all Azure resources and the .NET Azure Functions application in a single step, streamlining setup and reducing manual configuration. - -```bash -#!/bin/bash - -# Variables -PREFIX='user' #system or user -SUFFIX='test' -LOCATION='westeurope' -CURRENT_DIR="$(cd "$(dirname "$0")" && pwd)" -ZIPFILE="function_app.zip" -ENVIRONMENT=$(az account show --query environmentName --output tsv) - -# Change the current directory to the script's directory -cd "$CURRENT_DIR" || exit - -# Run terraform init and apply -if [[ $ENVIRONMENT == "LocalStack" ]]; then - echo "Using tflocal and azlocal for LocalStack emulator environment and ." - TERRAFORM="tflocal" - AZ="azlocal" -else - echo "Using standard terraform and az for AzureCloud environment." - TERRAFORM="terraform" - AZ="az" -fi - -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" - -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) -FUNCTION_APP_NAME=$(terraform output -raw function_app_name) - -# Print the variables -echo "Resource Group: $RESOURCE_GROUP_NAME" -echo "Function App: $FUNCTION_APP_NAME" - -# CD into the function app directory -cd ../src/sample || exit - -# Clean and build the project in Release configuration -dotnet clean -dotnet build -c Release - -# Publish the project to a publish directory -dotnet publish -c Release -o publish - -# Create deployment zip from the published output -cd publish || exit -zip -r ../$ZIPFILE . -cd .. || exit - -# Deploy the function app using the zip file -echo "Deploying function app [$FUNCTION_APP_NAME]..." -$AZ functionapp deploy \ - --resource-group "$RESOURCE_GROUP_NAME" \ - --name "$FUNCTION_APP_NAME" \ - --src-path $ZIPFILE \ - --type zip 1> /dev/null -``` - -> **Note** -> You can use the `azlocal` CLI as a drop-in replacement for the `az` CLI to direct all commands to the LocalStack for Azure emulator. Alternatively, run `azlocal start_interception` to automatically intercept and redirect all `az` commands to LocalStack. Likewise, the `tflocal` is a local replacement for the standard `terraform` CLI, allowing you to run Terraform commands against LocalStack's Azure emulation environment. For more information, see [Get started with the az tool on LocalStack](https://azure.localstack.cloud/user-guides/sdks/az/). - -The `deploy.sh` 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. -- Extracts resource names and outputs from the Terraform deployment. -- Builds and publishes the .NET Azure Functions application. -- Packages the published application into a zip file for deployment. -- Deploys the zip package to the Azure Function App using the LocalStack Azure CLI. +## Provisioning Scripts -> **Note** -> Azure CLI commands supports `--verbose` argument to print execution details and the `--debug` flag to show low-level REST calls for debugging. For more information, see [Get started with Azure CLI](https://learn.microsoft.com/en-us/cli/azure/get-started-with-azure-cli) +See [deploy.sh](deploy.sh) for the complete deployment automation. The script performs: +- Detects environment (LocalStack vs Azure Cloud) and uses appropriate CLI (`tflocal`/`azlocal` or `terraform`/`az`) +- Initializes Terraform and downloads required providers +- Creates and validates Terraform execution plan with custom variables +- Applies Terraform configuration to provision Azure resources +- Extracts output values (resource group name, function app name) +- Builds and publishes the .NET application in Release configuration +- Creates deployment zip package from published output +- Deploys the zip to Azure Function App using Azure CLI ## Deployment 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 deb0b4c..e212e56 100644 --- a/samples/web-app-cosmosdb-mongodb-api/python/bicep/README.md +++ b/samples/web-app-cosmosdb-mongodb-api/python/bicep/README.md @@ -17,7 +17,7 @@ Before deploying this solution, ensure you have the following tools installed: ### Installing azlocal CLI -The deployment script uses the `azlocal` CLI instead of the standard Azure CLI to work with LocalStack. Install it using: +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 @@ -27,384 +27,17 @@ For more information, see [Get started with the az tool on LocalStack](https://a ## Architecture Overview -The Terraform modules deploy the following Azure resources: +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, while the [main.bicep](main.bicep) Bicep module creates the following Azure resources: -1. [Azure Resource Group](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/manage-resource-groups-cli): Logical container for all resources in the sample. -2. [Azure CosmosDB Account (MongoDB API)](https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/introduction): A globally distributed database account configured for MongoDB workloads, with multi-region failover. -3. [MongoDB Database](https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/overview): The `sampledb` database for storing application data. -4. [MongoDB Collection](https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/overview): The `activities` collection within `sampledb` for storing vacation activity records. -5. [Azure App Service Plan](https://learn.microsoft.com/en-us/azure/app-service/overview-hosting-plans): The compute resource that hosts the web application. -6. [Azure Web App](https://learn.microsoft.com/en-us/azure/app-service/overview): Hosts the Python Flask single-page application (*Vacation Planner*), connected to CosmosDB for MongoDB. -7. [App Service Source Control](https://learn.microsoft.com/en-us/rest/api/appservice/web-apps/create-or-update-source-control?view=rest-appservice-2024-11-01): (Optional) Configures automatic deployment from a public GitHub repository. +1. [Azure CosmosDB Account (MongoDB API)](https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/introduction): A globally distributed database account configured for MongoDB workloads, with multi-region failover. +2. [MongoDB Database](https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/overview): The `sampledb` database for storing application data. +3. [MongoDB Collection](https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/overview): The `activities` collection within `sampledb` for storing vacation activity records. +4. [Azure App Service Plan](https://learn.microsoft.com/en-us/azure/app-service/overview-hosting-plans): The compute resource that hosts the web application. +5. [Azure Web App](https://learn.microsoft.com/en-us/azure/app-service/overview): Hosts the Python Flask single-page application (*Vacation Planner*), connected to CosmosDB for MongoDB. +6. [App Service Source Control](https://learn.microsoft.com/en-us/rest/api/appservice/web-apps/create-or-update-source-control?view=rest-appservice-2024-11-01): (Optional) Configures automatic deployment from a public GitHub repository. -The web app allows users to plan and manage vacation activities, storing all activity data in the CosmosDB-backed MongoDB collection. All resources are provisioned and configured using Terraform for easy reproducibility and local development with LocalStack for Azure. +The web app allows users to plan and manage vacation activities, storing all activity data in a MongoDB collection. -## Bicep Templates - -The `main.bicep` Bicep template defines all Azure resources using declarative syntax: - -```bicep -@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 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 language runtime used by the Azure Web App.') -@allowed([ - 'dotnet' - 'dotnet-isolated' - 'python' - 'java' - 'node' - 'powerShell' - 'custom' -]) -param runtimeName string - -@description('Specifies the target language version used by the Azure Web App.') -param runtimeVersion string - -@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.0' - '1.1' - '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 tags to be applied to the resources.') -param tags object = { - environment: 'test' - iac: 'bicep' -} - -@description('Specifies the primary replica region for the Cosmos DB account.') -param primaryRegion string = 'westeurope' - -@description('Specifies the secondary replica region for the Cosmos DB account.') -param secondaryRegion string = 'northeurope' - -@allowed([ - 'Eventual' - 'ConsistentPrefix' - 'Session' - 'BoundedStaleness' - 'Strong' -]) -@description('Specifies the default consistency level of the Cosmos DB account.') -param defaultConsistencyLevel string = 'Eventual' - -@allowed([ - '3.2' - '3.6' - '4.0' - '4.2' -]) -@description('Specifies the Cosmos DB server version to use.') -param serverVersion string = '4.2' - -@minValue(10) -@maxValue(2147483647) -@description('Specifies the max stale requests. Required for BoundedStaleness. Valid ranges, Single Region: 10 to 2147483647. Multi Region: 100000 to 2147483647.') -param maxStalenessPrefix int = 100000 - -@minValue(5) -@maxValue(86400) -@description('Specifies the max lag time (seconds). Required for BoundedStaleness. Valid ranges, Single Region: 5 to 84600. Multi Region: 300 to 86400.') -param maxIntervalInSeconds int = 300 - -@description('Specifies the name for the Mongo DB database.') -param databaseName string = 'sampledb' - -@minValue(400) -@maxValue(1000000) -@description('Specifies the shared throughput for the Mongo DB database, up to 25 collections.') -param sharedThroughput int = 400 - -@description('Specifies the name for the Mongo DB collection.') -param collectionName string = 'activities' - -@minValue(400) -@maxValue(1000000) -@description('Specifies the dedicated throughput for the Mongo DB collection.') -param dedicatedThroughput int = 400 - -@description('Specifies a list of field names for which to create single-field indexes on the MongoDB collection.') -param mongoDbIndexKeys array = ['_id','username', 'activity', 'timestamp'] - -@description('Specifies the username for the application.') -param username string = 'paolo' - -var webAppName = '${prefix}-webapp-${suffix}' -var appServicePlanPortalName = '${prefix}-app-service-plan-${suffix}' -var accountName = '${prefix}-mongodb-${suffix}' -var consistencyPolicy = { - Eventual: { - defaultConsistencyLevel: 'Eventual' - } - ConsistentPrefix: { - defaultConsistencyLevel: 'ConsistentPrefix' - } - Session: { - defaultConsistencyLevel: 'Session' - } - BoundedStaleness: { - defaultConsistencyLevel: 'BoundedStaleness' - maxStalenessPrefix: maxStalenessPrefix - maxIntervalInSeconds: maxIntervalInSeconds - } - Strong: { - defaultConsistencyLevel: 'Strong' - } -} -var locations = [ - { - locationName: primaryRegion - failoverPriority: 0 - isZoneRedundant: false - } - { - locationName: secondaryRegion - failoverPriority: 1 - isZoneRedundant: false - } -] - -resource appServicePlan 'Microsoft.Web/serverfarms@2024-11-01' = { - name: appServicePlanPortalName - location: location - tags: tags - kind: appServicePlanKind - sku: { - tier: skuTier - name: skuName - } - properties: { - reserved: reserved - zoneRedundant: zoneRedundant - maximumElasticWorkerCount: skuTier == 'FlexConsumption' ? 1 : 20 - } -} - -resource webApp 'Microsoft.Web/sites@2024-11-01' = { - name: webAppName - location: location - tags: tags - kind: webAppKind - properties: { - httpsOnly: httpsOnly - serverFarmId: appServicePlan.id - siteConfig: { - linuxFxVersion: toUpper('${runtimeName}|${runtimeVersion}') - minTlsVersion: minTlsVersion - publicNetworkAccess: publicNetworkAccess - } - } - identity: { - type: 'SystemAssigned' - } -} - -resource configAppSettings 'Microsoft.Web/sites/config@2024-11-01' = { - parent: webApp - name: 'appsettings' - properties: { - SCM_DO_BUILD_DURING_DEPLOYMENT: 'true' - ENABLE_ORYX_BUILD: 'true' - COSMOSDB_CONNECTION_STRING: account.listConnectionStrings().connectionStrings[0].connectionString - COSMOSDB_DATABASE_NAME: databaseName - COSMOSDB_COLLECTION_NAME: collectionName - LOGIN_NAME: username - } - dependsOn: [ - collection - ] -} - -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 account 'Microsoft.DocumentDB/databaseAccounts@2025-04-15' = { - name: toLower(accountName) - location: location - kind: 'MongoDB' - properties: { - consistencyPolicy: consistencyPolicy[defaultConsistencyLevel] - locations: locations - databaseAccountOfferType: 'Standard' - enableAutomaticFailover: true - apiProperties: { - serverVersion: serverVersion - } - capabilities: [ - { - name: 'DisableRateLimitingResponses' - } - ] - } -} - -resource database 'Microsoft.DocumentDB/databaseAccounts/mongodbDatabases@2025-04-15' = { - parent: account - name: databaseName - properties: { - resource: { - id: databaseName - } - options: { - throughput: sharedThroughput - } - } -} - -resource collection 'Microsoft.DocumentDB/databaseAccounts/mongodbDatabases/collections@2025-04-15' = { - parent: database - name: collectionName - properties: { - resource: { - id: collectionName - shardKey: { - username: 'Hash' - } - // Use a for loop to dynamically create the 'indexes' array based on the 'mongoDbIndexKeys' parameter - indexes: [for key in mongoDbIndexKeys: { - key: { - keys: [ - key - ] - } - }] - } - options: { - throughput: dedicatedThroughput - } - } -} - -output webAppName string = webAppName -output accountName string = accountName -output databaseName string = databaseName -output collectionName string = collectionName -output documentEndpoint string = account.properties.documentEndpoint -``` ## Configuration Before deploying the `main.bicep` template, update the `bicep.bicepparam` file with your specific values: @@ -421,195 +54,20 @@ param collectionName = 'activities' param username = 'paolo' param primaryRegion = 'westeurope' param secondaryRegion = 'northeurope' - ``` -## Deployment Script - -You can use the `deploy.sh` script to automate the deployment of all Azure resources and the sample application in a single step, streamlining setup and reducing manual configuration. - -```bash -#!/bin/bash - -# Variables -PREFIX='local' -SUFFIX='test' -TEMPLATE="main.bicep" -PARAMETERS="main.bicepparam" -RESOURCE_GROUP_NAME="${PREFIX}-rg" -LOCATION="westeurope" -VALIDATE_TEMPLATE=1 -USE_WHAT_IF=0 -SUBSCRIPTION_NAME=$(az account show --query name --output tsv) -CURRENT_DIR="$(cd "$(dirname "$0")" && pwd)" -ZIPFILE="planner_website.zip" -ENVIRONMENT=$(az account show --query environmentName --output tsv) - -# Change the current directory to the script's directory -cd "$CURRENT_DIR" || exit - -# Choose the appropriate CLI based on the environment -if [[ $ENVIRONMENT == "LocalStack" ]]; then - echo "Using azlocal for LocalStack emulator environment." - AZ="azlocal" -else - echo "Using standard az for AzureCloud environment." - AZ="az" -fi - -# 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 - -# 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 [$TEMPLATE]..." - $AZ deployment group what-if \ - --resource-group $RESOURCE_GROUP_NAME \ - --template-file $TEMPLATE \ - --parameters $PARAMETERS \ - --parameters location=$LOCATION \ - prefix=$PREFIX \ - suffix=$SUFFIX \ - --only-show-errors - - if [[ $? == 0 ]]; then - echo "Bicep template [$TEMPLATE] validation succeeded" - else - echo "Failed to validate Bicep template [$TEMPLATE]" - exit - fi - else - # Validate the Bicep template - echo "Validating Bicep template [$TEMPLATE]..." - output=$($AZ deployment group validate \ - --resource-group $RESOURCE_GROUP_NAME \ - --template-file $TEMPLATE \ - --parameters $PARAMETERS \ - --parameters location=$LOCATION \ - prefix=$PREFIX \ - suffix=$SUFFIX \ - --only-show-errors) - - if [[ $? == 0 ]]; then - echo "Bicep template [$TEMPLATE] validation succeeded" - else - echo "Failed to validate Bicep template [$TEMPLATE]" - echo "$output" - exit - fi - fi -fi +## Provisioning Scripts -# Deploy the Bicep template -echo "Deploying Bicep template [$TEMPLATE]..." -if DEPLOYMENT_OUTPUTS=$($AZ deployment group create \ - --resource-group $RESOURCE_GROUP_NAME \ - --only-show-errors \ - --template-file $TEMPLATE \ - --parameters $PARAMETERS \ - --parameters location=$LOCATION \ - prefix=$PREFIX \ - suffix=$SUFFIX \ - --query 'properties.outputs' -o json); then - echo "Bicep template [$TEMPLATE] deployed successfully. Outputs:" - echo "$DEPLOYMENT_OUTPUTS" | jq . - WEB_APP_NAME=$(echo "$DEPLOYMENT_OUTPUTS" | jq -r '.webAppName.value') - ACCOUNT_NAME=$(echo "$DEPLOYMENT_OUTPUTS" | jq -r '.accountName.value') - DATABASE_NAME=$(echo "$DEPLOYMENT_OUTPUTS" | jq -r '.databaseName.value') - COLLECTION_NAME=$(echo "$DEPLOYMENT_OUTPUTS" | jq -r '.collectionName.value') - DOCUMENT_ENDPOINT=$(echo "$DEPLOYMENT_OUTPUTS" | jq -r '.documentEndpoint.value') - echo "Deployment details:" - echo "Web App Name: $WEB_APP_NAME" - echo "Database Account Name: $ACCOUNT_NAME" - echo "Database Name: $DATABASE_NAME" - echo "Collection Name: $COLLECTION_NAME" - echo "Document Endpoint: $DOCUMENT_ENDPOINT" -else - echo "Failed to deploy Bicep template [$TEMPLATE]" - exit 1 -fi - -if [[ -z "$WEB_APP_NAME" || -z "$ACCOUNT_NAME" ]]; then - echo "Web App Name or Cosmos DB Account Name is empty. Exiting." - exit 1 -fi - -# Print the application settings of the web app -echo "Retrieving application settings for web app [$WEB_APP_NAME]..." -$AZ webapp config appsettings list \ - --resource-group "$RESOURCE_GROUP_NAME" \ - --name "$WEB_APP_NAME" - -# Change current directory to source folder -cd "../src" || exit - -# Remove any existing zip package of the web app -if [ -f "$ZIPFILE" ]; then - rm "$ZIPFILE" -fi - -# Create the zip package of the web app -echo "Creating zip package of the web app..." -zip -r "$ZIPFILE" app.py mongodb.py static templates requirements.txt - -# Deploy the web app -# Deploy the web app -echo "Deploying web app [$WEB_APP_NAME] with zip file [$ZIPFILE]..." -$AZ webapp deploy \ - --resource-group "$RESOURCE_GROUP_NAME" \ - --name "$WEB_APP_NAME" \ - --src-path "$ZIPFILE" \ - --type zip \ - --async true 1>/dev/null - -# Remove the zip package of the web app -if [ -f "$ZIPFILE" ]; then - rm "$ZIPFILE" -fi -``` +See [deploy.sh](deploy.sh) for the complete deployment automation. The script performs: -> **Note** -> You can use the `azlocal` CLI as a drop-in replacement for the `az` CLI to direct all commands to the LocalStack for Azure emulator. Alternatively, run `azlocal start_interception` to automatically intercept and redirect all `az` commands to LocalStack. For more information, see [Get started with the az tool on LocalStack](https://azure.localstack.cloud/user-guides/sdks/az/). - -The `deploy.sh` script executes the following steps: - -- Specifies the variables used during deployment. -- Creates the resource group if it does not exist. -- Conditionally validates the `main.bicep` module to check its syntax is correct and all parameters make sense. -- Conditionally runs a what-if deployment to execute a dry run to preview the resources that will be created, updated, or deleted. -- Runs the `main.bicep` template to create all the Azure resources. -- Collects important information from the deployment (like resource names) for later use. -- Uses jq (a JSON tool) to extract the names of resources we just created. -- Shows us all the settings that got applied to the Web App. -- Removes previous build artifacts for consistency. -- Creates zip archive in format expected by Web App. -- Uploads pre-built application package to the newly created Web App. - -> **Note** -> Azure CLI commands use `--verbose` argument to print execution details and the `--debug` flag to show low-level REST calls for debugging. For more information, see [Get started with Azure CLI](https://learn.microsoft.com/en-us/cli/azure/get-started-with-azure-cli) +- 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 the main.bicep template with parameters from [main.bicepparam](main.bicepparam) +- Extracts deployment outputs (Web App name, CosmosDB details) +- Creates zip package of the Python application +- Deploys the zip to Azure Web App ## Deployment 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 86b1437..84e092f 100644 --- a/samples/web-app-cosmosdb-mongodb-api/python/scripts/README.md +++ b/samples/web-app-cosmosdb-mongodb-api/python/scripts/README.md @@ -16,7 +16,7 @@ Before deploying this solution, ensure you have the following tools installed: ### Installing azlocal CLI -The deployment script uses the `azlocal` CLI to work with LocalStack. Install it using: +The [deploy.sh](deploy.sh) Bash script uses the `azlocal` CLI to work with LocalStack. Install it using: ```bash pip install azlocal @@ -24,233 +24,35 @@ pip install azlocal For more information, see [Get started with the az tool on LocalStack](https://azure.localstack.cloud/user-guides/sdks/az/). -## Deployment Script +## Architecture Overview -You can use the `deploy.sh` script to automate the deployment of all Azure resources and the sample application in a single step, streamlining setup and reducing manual configuration. +This [deploy.sh](deploy.sh) script creates the following Azure resources using Azure CLI commands: -```bash -#!/bin/bash - -# Variables -PREFIX='local' -SUFFIX='test' -LOCATION='westeurope' -RESOURCE_GROUP_NAME="${PREFIX}-rg" -APP_SERVICE_PLAN_NAME="${PREFIX}-app-service-plan-${SUFFIX}" -APP_SERVICE_PLAN_SKU="S1" -WEB_APP_NAME="${PREFIX}-webapp-${SUFFIX}" -COSMOSDB_ACCOUNT_NAME="${PREFIX}-mongodb-${SUFFIX}" -MONGODB_API_VERSION="7.0" -MONGODB_DATABASE_NAME="sampledb" -COLLECTION_NAME="activities" -INDEXES='[{"key":{"keys":["_id"]}},{"key":{"keys":["username"]}},{"key":{"keys":["activity"]}},{"key":{"keys":["timestamp"]}}]' -SHARD="username" -THROUGHPUT=400 -RUNTIME="python" -RUNTIME_VERSION="3.13" -LOGIN_NAME="paolo" -CURRENT_DIR="$(cd "$(dirname "$0")" && pwd)" -ZIPFILE="planner_website.zip" -ENVIRONMENT=$(az account show --query environmentName --output tsv) - -# Change the current directory to the script's directory -cd "$CURRENT_DIR" || exit - -# Choose the appropriate CLI based on the environment -if [[ $ENVIRONMENT == "LocalStack" ]]; then - echo "Using azlocal for LocalStack emulator environment." - AZ="azlocal" -else - echo "Using standard az for AzureCloud environment." - AZ="az" -fi - -# Create a resource group -echo "Creating resource group [$RESOURCE_GROUP_NAME]..." -$AZ group create \ - --name $RESOURCE_GROUP_NAME \ - --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 - -# Create a CosmosDB account with MongoDB kind -echo "Creating [$COSMOSDB_ACCOUNT_NAME] CosmosDB account in the [$RESOURCE_GROUP_NAME] resource group..." -$AZ cosmosdb create \ - --name $COSMOSDB_ACCOUNT_NAME \ - --resource-group $RESOURCE_GROUP_NAME \ - --locations regionName=$LOCATION \ - --kind MongoDB \ - --server-version $MONGODB_API_VERSION \ - --default-consistency-level Session \ - --only-show-errors 1>/dev/null - -if [ $? -eq 0 ]; then - echo "[$COSMOSDB_ACCOUNT_NAME] CosmosDB account successfully created in the [$RESOURCE_GROUP_NAME] resource group" -else - echo "Failed to create [$COSMOSDB_ACCOUNT_NAME] CosmosDB account in the [$RESOURCE_GROUP_NAME] resource group" - exit 1 -fi +1. [Azure Resource Group](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/manage-resource-groups-cli): Logical container for all gaming system resources. +2. [Azure CosmosDB Account (MongoDB API)](https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/introduction): A globally distributed database account configured for MongoDB workloads, with multi-region failover. +3. [MongoDB Database](https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/overview): The `sampledb` database for storing application data. +4. [MongoDB Collection](https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/overview): The `activities` collection within `sampledb` for storing vacation activity records. +5. [Azure App Service Plan](https://learn.microsoft.com/en-us/azure/app-service/overview-hosting-plans): The compute resource that hosts the web application. +6. [Azure Web App](https://learn.microsoft.com/en-us/azure/app-service/overview): Hosts the Python Flask single-page application (*Vacation Planner*), connected to CosmosDB for MongoDB. +7. [App Service Source Control](https://learn.microsoft.com/en-us/rest/api/appservice/web-apps/create-or-update-source-control?view=rest-appservice-2024-11-01): (Optional) Configures automatic deployment from a public GitHub repository. -# Retrieve document endpoint -DOCUMENT_ENDPOINT=$($AZ cosmosdb show \ - --name $COSMOSDB_ACCOUNT_NAME \ - --resource-group $RESOURCE_GROUP_NAME \ - --query "documentEndpoint" \ - --output tsv \ - --only-show-errors) +The web app allows users to plan and manage vacation activities, storing all activity data in a MongoDB collection. For more information on the sample application, see [Azure Web App with Azure CosmosDB for MongoDB](../README.md). -if [ -n "$DOCUMENT_ENDPOINT" ]; then - echo "Document endpoint retrieved successfully: $DOCUMENT_ENDPOINT" -else - echo "Failed to retrieve document endpoint." - exit 1 -fi +## Provisioning Scripts -# Create MongoDB database -echo "Creating [$MONGODB_DATABASE_NAME] MongoDB database in the [$COSMOSDB_ACCOUNT_NAME] CosmosDB account..." -$AZ cosmosdb mongodb database create \ - --account-name $COSMOSDB_ACCOUNT_NAME \ - --name $MONGODB_DATABASE_NAME \ - --resource-group $RESOURCE_GROUP_NAME \ - --output json \ - --only-show-errors 1>/dev/null - -if [ $? -eq 0 ]; then - echo "[$MONGODB_DATABASE_NAME] MongoDB database successfully created in the [$COSMOSDB_ACCOUNT_NAME] CosmosDB account" -else - echo "Failed to create [$MONGODB_DATABASE_NAME] MongoDB database in the [$COSMOSDB_ACCOUNT_NAME] CosmosDB account" - exit 1 -fi - -# Create a MongoDB database collection -echo "Creating [$COLLECTION_NAME] collection in the [$MONGODB_DATABASE_NAME] MongoDB database..." -$AZ cosmosdb mongodb collection create \ - --account-name $COSMOSDB_ACCOUNT_NAME \ - --resource-group $RESOURCE_GROUP_NAME \ - --database-name $MONGODB_DATABASE_NAME \ - --name $COLLECTION_NAME \ - --idx "$INDEXES" \ - --shard $SHARD \ - --throughput $THROUGHPUT \ - --only-show-errors 1>/dev/null - -if [ $? -eq 0 ]; then - echo "[$COLLECTION_NAME] collection successfully created in the [$MONGODB_DATABASE_NAME] MongoDB database" -else - echo "Failed to create [$COLLECTION_NAME] collection in the [$MONGODB_DATABASE_NAME] MongoDB database" - exit 1 -fi - -# List CosmosDB connection strings -echo "Listing connection strings for CosmosDB account [$COSMOSDB_ACCOUNT_NAME]..." -COSMOSDB_CONNECTION_STRING=$($AZ cosmosdb keys list \ - --name $COSMOSDB_ACCOUNT_NAME \ - --resource-group $RESOURCE_GROUP_NAME \ - --type connection-strings \ - --query "connectionStrings[0].connectionString" \ - --output tsv) - -if [ $? -eq 0 ]; then - echo "CosmosDB connection strings retrieved successfully." - echo "Connection String: $COSMOSDB_CONNECTION_STRING" -else - echo "Failed to retrieve CosmosDB connection strings." -fi - -# Create App Service Plan -echo "Creating 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 - -# Create the web app -echo "Creating web app [$WEB_APP_NAME]..." -$AZ webapp create \ - --resource-group "$RESOURCE_GROUP_NAME" \ - --plan "$APP_SERVICE_PLAN_NAME" \ - --name "$WEB_APP_NAME" \ - --runtime "$RUNTIME:$RUNTIME_VERSION" \ - --only-show-errors 1>/dev/null - -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 - -# Set web app settings -echo "Setting web app settings for [$WEB_APP_NAME]..." -$AZ webapp config appsettings set \ - --name $WEB_APP_NAME \ - --resource-group $RESOURCE_GROUP_NAME \ - --settings \ - SCM_DO_BUILD_DURING_DEPLOYMENT='true' \ - ENABLE_ORYX_BUILD='true' \ - COSMOSDB_CONNECTION_STRING="$COSMOSDB_CONNECTION_STRING" \ - COSMOSDB_DATABASE_NAME="$MONGODB_DATABASE_NAME" \ - COSMOSDB_COLLECTION_NAME="$COLLECTION_NAME" \ - LOGIN_NAME="$LOGIN_NAME" \ - --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 - -# Change current directory to source folder -cd "../src" || exit - -# Remove any existing zip package of the web app -if [ -f "$ZIPFILE" ]; then - rm "$ZIPFILE" -fi - -# Create the zip package of the web app -echo "Creating zip package of the web app..." -zip -r "$ZIPFILE" app.py mongodb.py static templates requirements.txt - -# List the contents of the zip package -echo "Contents of the zip package [$ZIPFILE]:" -unzip -l "$ZIPFILE" - -# Deploy the web app -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 "$WEB_APP_NAME" \ - --src-path "$ZIPFILE" \ - --type zip \ - --async true 1>/dev/null - -# Remove the zip package of the web app -if [ -f "$ZIPFILE" ]; then - rm "$ZIPFILE" -fi -``` +See [deploy.sh](deploy.sh) for the complete deployment script. The script performs: -> [!NOTE] -> You can use the `azlocal` CLI as a drop-in replacement for the `az` CLI to direct all commands to the LocalStack for Azure emulator. Alternatively, run `azlocal start_interception` to automatically intercept and redirect all `az` commands to LocalStack. To revert back to the default behavior and send commands to the Azure cloud, run `azlocal stop_interception`. +- Detects environment (LocalStack vs Azure Cloud) and uses appropriate CLI +- Creates resource group +- Creates CosmosDB account with MongoDB kind (API version 7.0) +- Retrieves document endpoint +- Creates MongoDB database and collection with indexes and sharding +- Retrieves CosmosDB connection string +- Creates App Service Plan (Linux) +- Creates Web App with Python runtime +- Configures Web App settings (CosmosDB connection, database/collection names) +- Creates zip package of the application +- Deploys the zip to Azure Web App ## Deployment 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 fc63f42..9e1947e 100644 --- a/samples/web-app-cosmosdb-mongodb-api/python/terraform/README.md +++ b/samples/web-app-cosmosdb-mongodb-api/python/terraform/README.md @@ -17,7 +17,7 @@ Before deploying this solution, ensure you have the following tools installed: ### Installing azlocal CLI -The deployment script uses the `azlocal` CLI instead of the standard Azure CLI to work with LocalStack. Install it using: +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 @@ -27,7 +27,7 @@ For more information, see [Get started with the az tool on LocalStack](https://a ## Architecture Overview -The Terraform modules deploy the following Azure resources: +The [main.tf](main.tf) Terraform module creates the following Azure resources: 1. [Azure Resource Group](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/manage-resource-groups-cli): Logical container for all resources in the sample. 2. [Azure CosmosDB Account (MongoDB API)](https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/introduction): A globally distributed database account configured for MongoDB workloads, with multi-region failover. @@ -39,254 +39,9 @@ The Terraform modules deploy the following Azure resources: The web app allows users to plan and manage vacation activities, storing all activity data in the CosmosDB-backed MongoDB collection. All resources are provisioned and configured using Terraform for easy reproducibility and local development with LocalStack for Azure. -## Terraform Modules - -Below is a summary of the key Terraform modules included in this deployment: - -- **`main.tf`**: Defines all Azure resources and their configuration. -- **`variables.tf`**: Declares input variables and validation rules. -- **`outputs.tf`**: Specifies output values after deployment. -- **`providers.tf`**: Configures the Terraform provider for Azure. - -Below you can read the declarative code in HashiCorp Configuration Language (HCL): - -```terraform -# Local Variables -locals { - resource_group_name = "${var.prefix}-rg" - cosmosdb_account_name = "${var.prefix}-mongodb-${var.suffix}" - app_service_plan_name = "${var.prefix}-app-service-plan-${var.suffix}" - web_app_name = "${var.prefix}-webapp-${var.suffix}" -} - -# Create a resource group -resource "azurerm_resource_group" "example" { - name = local.resource_group_name - location = var.location - tags = var.tags -} - -# Create a cosmosdb account -resource "azurerm_cosmosdb_account" "example" { - name = local.cosmosdb_account_name - resource_group_name = azurerm_resource_group.example.name - location = azurerm_resource_group.example.location - offer_type = "Standard" - kind = "MongoDB" - mongo_server_version = var.mongodb_server_version - automatic_failover_enabled = false - tags = var.tags - - consistency_policy { - consistency_level = var.consistency_level - max_interval_in_seconds = 300 - max_staleness_prefix = 100000 - } - - geo_location { - location = var.primary_region - failover_priority = 0 - } - - geo_location { - location = var.secondary_region - failover_priority = 1 - } - - lifecycle { - ignore_changes = [ - tags - ] - } -} - -resource "azurerm_cosmosdb_mongo_database" "example" { - name = var.cosmosdb_database_name - resource_group_name = azurerm_resource_group.example.name - account_name = azurerm_cosmosdb_account.example.name - throughput = 400 -} - -resource "azurerm_cosmosdb_mongo_collection" "example" { - name = var.cosmosdb_collection_name - resource_group_name = azurerm_resource_group.example.name - account_name = azurerm_cosmosdb_account.example.name - database_name = azurerm_cosmosdb_mongo_database.example.name - - default_ttl_seconds = "777" - shard_key = "username" - throughput = 400 - - # Dynamically create the 'index' blocks using a for_each loop over the variable - dynamic "index" { - # The for_each expression iterates over the list of keys from the variable - for_each = var.mongodb_index_keys - content { - # The value of the current item in the iteration (e.g., "$**", "_id", etc.) - keys = [index.value] - } - } -} - -# Create a service plan -resource "azurerm_service_plan" "example" { - 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 - tags = var.tags - - lifecycle { - ignore_changes = [ - tags - ] - } -} - -# Create a web app -resource "azurerm_linux_web_app" "example" { - name = local.web_app_name - resource_group_name = azurerm_resource_group.example.name - location = azurerm_resource_group.example.location - service_plan_id = azurerm_service_plan.example.id - https_only = var.https_only - public_network_access_enabled = var.public_network_access_enabled - client_affinity_enabled = false - tags = var.tags - - identity { - type = "SystemAssigned" - } - - site_config { - always_on = var.always_on - http2_enabled = var.http2_enabled - minimum_tls_version = var.minimum_tls_version - application_stack { - python_version = var.python_version - } - } - - app_settings = { - SCM_DO_BUILD_DURING_DEPLOYMENT = "true" - COSMOSDB_CONNECTION_STRING = azurerm_cosmosdb_account.example.primary_mongodb_connection_string - COSMOSDB_DATABASE_NAME = azurerm_cosmosdb_mongo_database.example.name - COSMOSDB_COLLECTION_NAME = var.cosmosdb_collection_name - LOGIN_NAME = var.login_name - } - - 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 = "main" - use_manual_integration = true - use_mercurial = false -} -``` - -## Deployment Script - -You can use the `deploy.sh` script to automate the deployment of all Azure resources and the sample application in a single step, streamlining setup and reducing manual configuration. - -```bash -#!/bin/bash - -# Variables -PREFIX='local' -SUFFIX='test' -LOCATION='westeurope' -CURRENT_DIR="$(cd "$(dirname "$0")" && pwd)" -ZIPFILE="planner_website.zip" -ENVIRONMENT=$(az account show --query environmentName --output tsv) - -# Change the current directory to the script's directory -cd "$CURRENT_DIR" || exit - -# Run terraform init and apply -if [[ $ENVIRONMENT == "LocalStack" ]]; then - echo "Using tflocal and azlocal for LocalStack emulator environment and ." - TERRAFORM="tflocal" - AZ="azlocal" -else - echo "Using standard terraform and az for AzureCloud environment." - TERRAFORM="terraform" - AZ="az" -fi - -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" - -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) - -# Print the variables -echo "Resource Group: $RESOURCE_GROUP_NAME" -echo "Web App: $WEB_APP_NAME" - -# Change current directory to source folder -cd "../src" || exit - -# Remove any existing zip package of the web app -if [ -f "$ZIPFILE" ]; then - rm "$ZIPFILE" -fi - -# Create the zip package of the web app -echo "Creating zip package of the web app..." -zip -r "$ZIPFILE" app.py mongodb.py static templates requirements.txt - -# Deploy the web app -echo "Deploying web app [$WEB_APP_NAME] with zip file [$ZIPFILE]..." -$AZ webapp deploy \ - --resource-group "$RESOURCE_GROUP_NAME" \ - --name "$WEB_APP_NAME" \ - --src-path "$ZIPFILE" \ - --type zip \ - --async true 1>/dev/null - -# Remove the zip package of the web app -if [ -f "$ZIPFILE" ]; then - rm "$ZIPFILE" -fi -``` - -> **Note** -> You can use the `azlocal` CLI as a drop-in replacement for the `az` CLI to direct all commands to the LocalStack for Azure emulator. Alternatively, run `azlocal start_interception` to automatically intercept and redirect all `az` commands to LocalStack. Likewise, the `tflocal` is a local replacement for the standard `terraform` CLI, allowing you to run Terraform commands against LocalStack's Azure emulation environment. For more information, see [Get started with the az tool on LocalStack](https://azure.localstack.cloud/user-guides/sdks/az/). +## Provisioning Scripts -The `deploy.sh` script executes the following steps: +You can use the [deploy.sh](deploy.sh) script to automate the deployment of all Azure resources and the sample application 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. diff --git a/samples/web-app-managed-identity/python/bicep/README.md b/samples/web-app-managed-identity/python/bicep/README.md index 46035a9..a9fe28f 100644 --- a/samples/web-app-managed-identity/python/bicep/README.md +++ b/samples/web-app-managed-identity/python/bicep/README.md @@ -17,7 +17,7 @@ Before deploying this solution, ensure you have the following tools installed: ### Installing azlocal CLI -The deployment script uses the `azlocal` CLI instead of the standard Azure CLI to work with LocalStack. Install it using: +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 @@ -27,7 +27,7 @@ For more information, see [Get started with the az tool on LocalStack](https://a ## Architecture Overview -The Bicep template deploys the following Azure resources: +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, while the [main.bicep](main.bicep) Bicep module creates the following Azure resources: 1. [Azure Storage Account](https://learn.microsoft.com/en-us/azure/storage/common/storage-account-overview): Provides blob storage for persisting vacation activity data. The web application stores each activity as a JSON blob file in the `activities` container. 2. [Azure App Service Plan](https://learn.microsoft.com/en-us/azure/app-service/overview-hosting-plans): Defines the compute resources (CPU, memory, and scaling options) that host the web application. @@ -38,486 +38,19 @@ The Bicep template deploys the following Azure resources: The web app allows users to plan and manage vacation activities, storing all activity data as blob files in the `activities` containers in the [Azure Storage Account](https://learn.microsoft.com/en-us/azure/storage/common/storage-account-overview). For more information, see [Azure Web App with Managed Identity](../README.md). -## Bicep Templates - -The `main.bicep` Bicep template defines all Azure resources using declarative syntax. The module uses conditional provisioning for the user-assigned managed identity and role assignments resources. In Bicep, you can conditionally deploy a resource by passing in a parameter that specifies if the resource is deployed. Test the condition with an if expression in the resource declaration. For more information, see [Conditional deployments in Bicep with the if expression](https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/conditional-resource-deployment). - -```bicep -@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 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 appServicePlanZoneRedundant bool = false - -@description('Specifies the language runtime used by the Azure Web App.') -@allowed([ - 'dotnet' - 'dotnet-isolated' - 'python' - 'java' - 'node' - 'powerShell' - 'custom' -]) -param runtimeName string - -@description('Specifies the target language version used by the Azure Web App.') -param runtimeVersion string - -@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.0' - '1.1' - '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 tags to be applied to the resources.') -param tags object = { - environment: 'test' - iac: 'bicep' -} - -@description('Specifies the sku of the Azure Storage account.') -param storageAccountSku string = 'Standard_LRS' - -@description('Specifies the name of the blob container.') -param containerName string = 'activities' - -@description('Specifies the type of managed identity.') -@allowed([ - 'SystemAssigned' - 'UserAssigned' -]) -param managedIdentityType string = 'SystemAssigned' - -var webAppName = '${prefix}-webapp-${suffix}' -var appServicePlanName = '${prefix}-app-service-plan-${suffix}' -var storageAccountName = '${prefix}storage${suffix}' -var managedIdentityName = '${prefix}-identity-${suffix}' - -resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = if (managedIdentityType == 'UserAssigned') { - name: managedIdentityName - location: location - tags: tags -} - -resource storageBlobDataContributorRoleDefinition 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { - name: 'ba92f5b4-2d11-453d-a403-e96b0029c9fe' - scope: subscription() -} - -resource storageBlobDataOwnerRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(storageAccount.id, webApp.id, storageBlobDataContributorRoleDefinition.id) - scope: storageAccount - properties: { - roleDefinitionId: storageBlobDataContributorRoleDefinition.id - principalId: managedIdentityType == 'SystemAssigned' ? webApp.identity.principalId : managedIdentity.properties.principalId - principalType: 'ServicePrincipal' - } -} - -resource storageAccount 'Microsoft.Storage/storageAccounts@2025-01-01' = { - name: storageAccountName - location: location - tags: tags - sku: { - name: storageAccountSku - } - kind: 'StorageV2' - properties: { - accessTier: 'Hot' - } -} - -resource blobServices 'Microsoft.Storage/storageAccounts/blobServices@2025-01-01' = { - parent: storageAccount - name: 'default' -} - -resource container 'Microsoft.Storage/storageAccounts/blobServices/containers@2025-01-01' = { - parent: blobServices - name: containerName -} - -resource appServicePlan 'Microsoft.Web/serverfarms@2024-11-01' = { - name: appServicePlanName - location: location - tags: tags - kind: appServicePlanKind - sku: { - tier: skuTier - name: skuName - } - properties: { - reserved: reserved - zoneRedundant: appServicePlanZoneRedundant - maximumElasticWorkerCount: skuTier == 'FlexConsumption' ? 1 : 20 - } -} - -resource webApp 'Microsoft.Web/sites@2024-11-01' = { - name: webAppName - location: location - tags: tags - kind: webAppKind - properties: { - httpsOnly: httpsOnly - serverFarmId: appServicePlan.id - siteConfig: { - linuxFxVersion: toUpper('${runtimeName}|${runtimeVersion}') - 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' - CONTAINER_NAME: container.name - AZURE_STORAGE_ACCOUNT_URL: storageAccount.properties.primaryEndpoints.blob - AZURE_CLIENT_ID: managedIdentityType == 'SystemAssigned' ? '' : managedIdentity.properties.clientId - } - dependsOn: [ - storageBlobDataOwnerRoleAssignment - ] -} - -resource webAppSourceControl 'Microsoft.Web/sites/sourcecontrols@2024-11-01' = if (contains(repoUrl,'http')){ - name: 'web' - parent: webApp - properties: { - repoUrl: repoUrl - branch: 'master' - isManualIntegration: true - } -} - -output appServicePlanName string = appServicePlan.name -output webAppName string = webApp.name -output webAppUrl string = webApp.properties.defaultHostName -output storageAccountName string = storageAccountName -``` -## Configuration - -Before deploying the `main.bicep` template, update the `bicep.bicepparam` file with your specific values. Note that the `deploy.sh` script overrides some of these parameters. - -```bicep -using 'main.bicep' - -param prefix = 'local' -param suffix = 'test' -param runtimeName = 'python' -param runtimeVersion = '3.13' -``` - -## Deployment Script - -You can use the `deploy.sh` script to automate the deployment of all Azure resources and the sample application in a single step, streamlining setup and reducing manual configuration. Before running the script, customize the variable values based on your needs. In particular, use the `MANAGED_IDENTITY_TYPE` variable to specify the type of managed identity to provision: `SystemAssigned` or `UserAssigned`. - -```bash -#!/bin/bash - -# Variables -PREFIX='local' -SUFFIX='test' -TEMPLATE="main.bicep" -PARAMETERS="main.bicepparam" -RESOURCE_GROUP_NAME="$PREFIX-rg" -LOCATION="westeurope" -VALIDATE_TEMPLATE=1 -USE_WHAT_IF=0 -SUBSCRIPTION_NAME=$(az account show --query name --output tsv) -CURRENT_DIR="$(cd "$(dirname "$0")" && pwd)" -ZIPFILE="webapp_app.zip" -MANAGED_IDENTITY_TYPE="UserAssigned" # SystemAssigned or UserAssigned -ENVIRONMENT=$(az account show --query environmentName --output tsv) - -# Change the current directory to the script's directory -cd "$CURRENT_DIR" || exit - -# Choose the appropriate CLI based on the environment -if [[ $ENVIRONMENT == "LocalStack" ]]; then - echo "Using azlocal for LocalStack emulator environment." - AZ="azlocal" -else - echo "Using standard az for AzureCloud environment." - AZ="az" -fi - -# 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 - -# 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 [$TEMPLATE]..." - $AZ deployment group what-if \ - --resource-group $RESOURCE_GROUP_NAME \ - --template-file $TEMPLATE \ - --parameters $PARAMETERS \ - --parameters location=$LOCATION \ - prefix=$PREFIX \ - suffix=$SUFFIX \ - managedIdentityType=$MANAGED_IDENTITY_TYPE \ - --only-show-errors - - if [[ $? == 0 ]]; then - echo "Bicep template [$TEMPLATE] validation succeeded" - else - echo "Failed to validate Bicep template [$TEMPLATE]" - exit - fi - else - # Validate the Bicep template - echo "Validating Bicep template [$TEMPLATE]..." - output=$($AZ deployment group validate \ - --resource-group $RESOURCE_GROUP_NAME \ - --template-file $TEMPLATE \ - --parameters $PARAMETERS \ - --parameters location=$LOCATION \ - prefix=$PREFIX \ - suffix=$SUFFIX \ - managedIdentityType=$MANAGED_IDENTITY_TYPE \ - --only-show-errors) - - if [[ $? == 0 ]]; then - echo "Bicep template [$TEMPLATE] validation succeeded" - else - echo "Failed to validate Bicep template [$TEMPLATE]" - echo "$output" - exit - fi - fi -fi - -# Deploy the Bicep template -echo "Deploying Bicep template [$TEMPLATE]..." -if DEPLOYMENT_OUTPUTS=$($AZ deployment group create \ - --resource-group $RESOURCE_GROUP_NAME \ - --only-show-errors \ - --template-file $TEMPLATE \ - --parameters $PARAMETERS \ - --parameters location=$LOCATION \ - prefix=$PREFIX \ - suffix=$SUFFIX \ - managedIdentityType=$MANAGED_IDENTITY_TYPE \ - --query 'properties.outputs' -o json); then - echo "Bicep template [$TEMPLATE] deployed successfully. Outputs:" - echo "$DEPLOYMENT_OUTPUTS" | jq . - APP_SERVICE_PLAN_NAME=$(echo "$DEPLOYMENT_OUTPUTS" | jq -r '.appServicePlanName.value') - WEB_APP_NAME=$(echo "$DEPLOYMENT_OUTPUTS" | jq -r '.webAppName.value') - WEB_APP_URL=$(echo "$DEPLOYMENT_OUTPUTS" | jq -r '.webAppUrl.value') - STORAGE_ACCOUNT_NAME=$(echo "$DEPLOYMENT_OUTPUTS" | jq -r '.storageAccountName.value') - echo "Deployment details:" - echo "- appServicePlanName: $APP_SERVICE_PLAN_NAME" - echo "- webAppName: $WEB_APP_NAME" - echo "- webAppUrl: $WEB_APP_URL" - echo "- storageAccountName: $STORAGE_ACCOUNT_NAME" -else - echo "Failed to deploy Bicep template [$TEMPLATE]" - exit 1 -fi - -# Validation before deploying the web app -if [[ -z "$WEB_APP_NAME" ]]; then - echo "Web App Name is empty. Exiting." - exit 1 -fi - -# CD into the web app directory -cd ../src || exit - -# Remove any existing zip package of the web app -if [ -f "$ZIPFILE" ]; then - rm "$ZIPFILE" -fi - -# Create the zip package of the web app -echo "Creating zip package of the web app..." -zip -r "$ZIPFILE" app.py requirements.txt static templates - -# Deploy the web app -echo "Deploying web app [$WEB_APP_NAME] with zip file [$ZIPFILE]..." -$AZ webapp deploy \ - --resource-group "$RESOURCE_GROUP_NAME" \ - --name "$WEB_APP_NAME" \ - --src-path "$ZIPFILE" \ - --type zip \ - --async true 1>/dev/null - -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 - -# Remove the zip package of the web app -if [ -f "$ZIPFILE" ]; then - rm "$ZIPFILE" -fi -``` - -> **Note** -> You can use the `azlocal` CLI as a drop-in replacement for the `az` CLI to direct all commands to the LocalStack for Azure emulator. Alternatively, run `azlocal start_interception` to automatically intercept and redirect all `az` commands to LocalStack. For more information, see [Get started with the az tool on LocalStack](https://azure.localstack.cloud/user-guides/sdks/az/). - -The `deploy.sh` script executes the following steps: +## Provisioning Scripts -- Specifies the variables used during deployment. -- Creates the resource group if it does not exist. -- Conditionally validates the `main.bicep` module to check its syntax is correct and all parameters make sense. -- Conditionally runs a what-if deployment to execute a dry run to preview the resources that will be created, updated, or deleted. -- Runs the `main.bicep` template to create all the Azure resources. -- Collects important information from the deployment (like resource names) for later use. -- Uses jq (a JSON tool) to extract the names of resources we just created. -- Creates zip archive in format expected by Web App. -- Uploads pre-built application package to the newly created Web App. +See [deploy.sh](deploy.sh) for the complete deployment script. The script performs the following operations: -> **Note** -> Azure CLI commands use `--verbose` argument to print execution details and the `--debug` flag to show low-level REST calls for debugging. For more information, see [Get started with Azure CLI](https://learn.microsoft.com/en-us/cli/azure/get-started-with-azure-cli) +- Detects environment (LocalStack or Azure Cloud) and selects appropriate CLI +- Creates resource group if it doesn't exist +- Validates Bicep template syntax and parameters +- Optionally runs what-if deployment for preview +- Deploys Bicep template to create all Azure resources +- Extracts deployment outputs (resource names, URLs) using jq +- Packages web application code into zip file +- Deploys zip package to Azure Web App +- Cleans up temporary artifacts ## Deployment diff --git a/samples/web-app-managed-identity/python/scripts/README.md b/samples/web-app-managed-identity/python/scripts/README.md index 5b2b10a..b94c523 100644 --- a/samples/web-app-managed-identity/python/scripts/README.md +++ b/samples/web-app-managed-identity/python/scripts/README.md @@ -16,7 +16,7 @@ Before deploying this solution, ensure you have the following tools installed: ### Installing azlocal CLI -The deployment script uses the `azlocal` CLI to work with LocalStack. Install it using: +The [deploy.sh](deploy.sh) Bash script uses the `azlocal` CLI to work with LocalStack. Install it using: ```bash pip install azlocal @@ -26,7 +26,7 @@ For more information, see [Get started with the az tool on LocalStack](https://a ## Architecture Overview -This CLI deployment creates the following Azure resources using direct Azure CLI commands: +The [deploy.sh](deploy.sh) Bash script creates the following Azure resources using Azure CLI commands: 1. [Azure Storage Account](https://learn.microsoft.com/en-us/azure/storage/common/storage-account-overview): Provides blob storage for persisting vacation activity data. The web application stores each activity as a JSON blob file in the `activities` container. 2. [Azure App Service Plan](https://learn.microsoft.com/en-us/azure/app-service/overview-hosting-plans): Defines the compute resources (CPU, memory, and scaling options) that host the web application. @@ -37,385 +37,33 @@ This CLI deployment creates the following Azure resources using direct Azure CLI The web app allows users to plan and manage vacation activities, storing all activity data as blob files in the `activities` containers in the [Azure Storage Account](https://learn.microsoft.com/en-us/azure/storage/common/storage-account-overview). For more information, see [Azure Web App with Managed Identity](../README.md). -## Deployment Script +## Provisioning Scripts ## Automation Scripts This sample provides two bash scripts to streamline the deployment process by automating the provisioning of Azure resources and the sample application: -- `user-assigned.sh`: Configures the Azure Web App to authenticate with Azure Storage using a *user-assigned managed identity* -- `system-assigned.sh`: Configures the Azure Web App to authenticate with Azure Storage using a *system-assigned managed identity* - -These scripts eliminate manual configuration steps and enable one-command deployment of the entire infrastructure. For brevity, we only report the code of the `user-assigned.sh` script in this article. - -```bash -#!/bin/bash - -# Variables -PREFIX='local' -SUFFIX='test' -LOCATION='westeurope' -STORAGE_ACCOUNT_NAME="${PREFIX}storage${SUFFIX}" -APP_SERVICE_PLAN_NAME="${PREFIX}-app-service-plan-${SUFFIX}" -APP_SERVICE_PLAN_SKU="B1" -MANAGED_IDENTITY_NAME="${PREFIX}-identity-${SUFFIX}" -WEB_APP_NAME="${PREFIX}-webapp-${SUFFIX}" -RESOURCE_GROUP_NAME="${PREFIX}-web-app-rg" -RUNTIME="python" -RUNTIME_VERSION="3.13" -CONTAINER_NAME='activities' -ZIPFILE="webapp_app.zip" -SUBSCRIPTION_NAME=$(az account show --query name --output tsv) -SUBSCRIPTION_ID=$(az account show --query id --output tsv) -ENVIRONMENT=$(az account show --query environmentName --output tsv) -CURRENT_DIR="$(cd "$(dirname "$0")" && pwd)" -RETRY_COUNT=3 -SLEEP=5 - -# Change the current directory to the script's directory -cd "$CURRENT_DIR" || exit - -# Choose the appropriate CLI based on the environment -if [[ $ENVIRONMENT == "LocalStack" ]]; then - echo "Using azlocal for LocalStack emulator environment." - AZ="azlocal" -else - echo "Using standard az for AzureCloud environment." - AZ="az" -fi - -# Create a resource group -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 - -# Create a storage account -echo "Checking if storage account [$STORAGE_ACCOUNT_NAME] exists in the resource group [$RESOURCE_GROUP_NAME]..." -$AZ storage account show \ - --name $STORAGE_ACCOUNT_NAME \ - --resource-group $RESOURCE_GROUP_NAME &>/dev/null - -if [[ $? != 0 ]]; then - echo "No storage account [$STORAGE_ACCOUNT_NAME] exists in the [$RESOURCE_GROUP_NAME] resource group." - echo "Creating storage account [$STORAGE_ACCOUNT_NAME] in the [$RESOURCE_GROUP_NAME] resource group..." - $AZ storage account create \ - --name $STORAGE_ACCOUNT_NAME \ - --location "$LOCATION" \ - --resource-group $RESOURCE_GROUP_NAME \ - --sku Standard_LRS 1>/dev/null - - if [ $? -eq 0 ]; then - echo "Storage account [$STORAGE_ACCOUNT_NAME] created successfully in the [$RESOURCE_GROUP_NAME] resource group." - else - echo "Failed to create storage account [$STORAGE_ACCOUNT_NAME] in the [$RESOURCE_GROUP_NAME] resource group." - exit 1 - fi -else - echo "Storage account [$STORAGE_ACCOUNT_NAME] already exists in the [$RESOURCE_GROUP_NAME] resource group." -fi - -# Get the storage account key -echo "Getting storage account key for [$STORAGE_ACCOUNT_NAME]..." -STORAGE_ACCOUNT_KEY=$($AZ storage account keys list \ - --account-name $STORAGE_ACCOUNT_NAME \ - --resource-group $RESOURCE_GROUP_NAME \ - --query "[0].value" \ - --output tsv) - -if [ -n "$STORAGE_ACCOUNT_KEY" ]; then - echo "Storage account key retrieved successfully: [$STORAGE_ACCOUNT_KEY]" -else - echo "Failed to retrieve storage account key." - exit 1 -fi - -# Get the storage account resource ID -STORAGE_ACCOUNT_RESOURCE_ID=$($AZ storage account show \ - --name $STORAGE_ACCOUNT_NAME \ - --resource-group $RESOURCE_GROUP_NAME \ - --query "id" \ - --output tsv \ - --only-show-errors) - -if [ -n "$STORAGE_ACCOUNT_RESOURCE_ID" ]; then - echo "Storage account resource ID retrieved successfully: $STORAGE_ACCOUNT_RESOURCE_ID" -else - echo "Failed to retrieve storage account resource ID." - exit 1 -fi - -# Get the storage account blob primary endpoint -AZURE_STORAGE_ACCOUNT_URL=$($AZ storage account show \ - --name $STORAGE_ACCOUNT_NAME \ - --resource-group $RESOURCE_GROUP_NAME \ - --query "primaryEndpoints.blob" \ - --output tsv \ - --only-show-errors) - -if [ -n "$AZURE_STORAGE_ACCOUNT_URL" ]; then - echo "Storage account blob primary endpoint retrieved successfully: $AZURE_STORAGE_ACCOUNT_URL" -else - echo "Failed to retrieve storage account blob primary endpoint." - exit 1 -fi - -# Create blob container -echo "Creating blob container [$CONTAINER_NAME] in the [$STORAGE_ACCOUNT_NAME] storage account..." -$AZ storage container create \ - --name $CONTAINER_NAME \ - --account-name $STORAGE_ACCOUNT_NAME \ - --account-key "$STORAGE_ACCOUNT_KEY" \ - --public-access blob 1>/dev/null - -if [ $? -eq 0 ]; then - echo "Blob container [$CONTAINER_NAME] created successfully in the [$STORAGE_ACCOUNT_NAME] storage account." -else - echo "Failed to create blob container [$CONTAINER_NAME] in the [$STORAGE_ACCOUNT_NAME] storage account." - exit 1 -fi - -# Check if the App Service Plan already exists -echo "Checking if App Service Plan [$APP_SERVICE_PLAN_NAME] exists in the resource group [$RESOURCE_GROUP_NAME]..." -$AZ appservice plan show \ - --name $APP_SERVICE_PLAN_NAME \ - --resource-group $RESOURCE_GROUP_NAME &>/dev/null - -if [[ $? != 0 ]]; then - echo "No App Service Plan [$APP_SERVICE_PLAN_NAME] exists in the [$RESOURCE_GROUP_NAME] resource group." - # Create App Service Plan - echo "Creating 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 [$APP_SERVICE_PLAN_NAME] already exists in the [$RESOURCE_GROUP_NAME] resource group." -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" \ - --subscription "$SUBSCRIPTION_ID" 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 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 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 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 - -# Check if the web app already exists -echo "Checking if web app [$WEB_APP_NAME] exists in the resource group [$RESOURCE_GROUP_NAME]..." -$AZ webapp show \ - --name $WEB_APP_NAME \ - --resource-group $RESOURCE_GROUP_NAME &>/dev/null - -if [[ $? != 0 ]]; then - echo "No web app [$WEB_APP_NAME] exists in the [$RESOURCE_GROUP_NAME] resource group." - # Create the web app - echo "Creating web app [$WEB_APP_NAME]..." - $AZ webapp create \ - --resource-group "$RESOURCE_GROUP_NAME" \ - --plan "$APP_SERVICE_PLAN_NAME" \ - --name "$WEB_APP_NAME" \ - --runtime "$RUNTIME:$RUNTIME_VERSION" \ - --assign-identity "${IDENTITY_ID}" \ - --only-show-errors 1>/dev/null - - 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 [$WEB_APP_NAME] already exists in the [$RESOURCE_GROUP_NAME] resource group." -fi - -# Assign the Storage Blob Data Contributor role to the managed identity with the storage account as scope -ROLE="Storage Blob Data Contributor" -echo "Checking if the managed identity with principal ID [$PRINCIPAL_ID] has the [$ROLE] role assignment on storage account [$STORAGE_ACCOUNT_NAME]..." -current=$($AZ role assignment list \ - --assignee "$PRINCIPAL_ID" \ - --scope "$STORAGE_ACCOUNT_RESOURCE_ID" \ - --query "[?roleDefinitionName=='$ROLE'].roleDefinitionName" \ - --output tsv 2>/dev/null) - -if [[ $current == $ROLE ]]; then - echo "Managed identity already has the [$ROLE] role assignment on storage account [$STORAGE_ACCOUNT_NAME]" -else - echo "Managed identity does not have the [$ROLE] role assignment on storage account [$STORAGE_ACCOUNT_NAME]" - echo "Creating role assignment: assigning [$ROLE] role to managed identity on storage account [$STORAGE_ACCOUNT_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 "$STORAGE_ACCOUNT_RESOURCE_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 on storage account [$STORAGE_ACCOUNT_NAME]" - else - echo "Failed to assign [$ROLE] role to managed identity on storage account [$STORAGE_ACCOUNT_NAME]" - exit - fi -fi - -# Set web app settings -echo "Setting web app settings for [$WEB_APP_NAME]..." -$AZ webapp config appsettings set \ - --name $WEB_APP_NAME \ - --resource-group $RESOURCE_GROUP_NAME \ - --settings \ - SCM_DO_BUILD_DURING_DEPLOYMENT='true' \ - ENABLE_ORYX_BUILD='true' \ - AZURE_CLIENT_ID="$CLIENT_ID" \ - AZURE_STORAGE_ACCOUNT_URL="$AZURE_STORAGE_ACCOUNT_URL" \ - CONTAINER_NAME="$CONTAINER_NAME" \ - --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 - -# CD into the web app directory -cd ../src || exit - -# Remove any existing zip package of the web app -if [ -f "$ZIPFILE" ]; then - rm "$ZIPFILE" -fi - -# Create the zip package of the web app -echo "Creating zip package of the web app..." -zip -r "$ZIPFILE" app.py requirements.txt static templates - -# Deploy the web app -echo "Deploying web app [$WEB_APP_NAME] with zip file [$ZIPFILE]..." -$AZ webapp deploy \ - --resource-group "$RESOURCE_GROUP_NAME" \ - --name "$WEB_APP_NAME" \ - --src-path "$ZIPFILE" \ - --type zip \ - --async true 1>/dev/null - -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 - -# Remove the zip package of the web app -if [ -f "$ZIPFILE" ]; then - rm "$ZIPFILE" -fi -``` +- [user-assigned.sh](user-assigned.sh): Configures the Azure Web App with a *user-assigned managed identity* +- [system-assigned.sh](system-assigned.sh): Configures the Azure Web App with a *system-assigned managed identity* + +See the script files for complete implementation. The scripts perform the following operations: + +- Detect environment (LocalStack or Azure Cloud) and select appropriate CLI +- Create resource group if it doesn't exist +- Provision storage account and retrieve access keys and endpoints +- Create blob container for activity data +- Create App Service Plan with Linux runtime +- Create user-assigned managed identity (user-assigned script only) +- Retrieve identity client ID, principal ID, and resource ID +- Create web app with specified Python runtime +- Assign managed identity to web app +- Configure Storage Blob Data Contributor role assignment with retry logic +- Set web app configuration settings (storage URL, container name, client ID) +- Package application code into zip file +- Deploy zip package to Azure Web App +- Clean up temporary artifacts + +These scripts eliminate manual configuration steps and enable one-command deployment of the entire infrastructure. > [!NOTE] > You can use the `azlocal` CLI as a drop-in replacement for the `az` CLI to direct all commands to the LocalStack for Azure emulator. Alternatively, run `azlocal start_interception` to automatically intercept and redirect all `az` commands to LocalStack. To revert back to the default behavior and send commands to the Azure cloud, run `azlocal stop_interception`. diff --git a/samples/web-app-managed-identity/python/terraform/README.md b/samples/web-app-managed-identity/python/terraform/README.md index 0ada235..af1341d 100644 --- a/samples/web-app-managed-identity/python/terraform/README.md +++ b/samples/web-app-managed-identity/python/terraform/README.md @@ -17,7 +17,7 @@ Before deploying this solution, ensure you have the following tools installed: ### Installing azlocal CLI -The deployment script uses the `azlocal` CLI instead of the standard Azure CLI to work with LocalStack. Install it using: +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 @@ -27,7 +27,7 @@ For more information, see [Get started with the az tool on LocalStack](https://a ## Architecture Overview -The Terraform modules deploy the following Azure resources: +The [main.tf](main.tf) Terraform module creates the following Azure resources: 1. [Azure Storage Account](https://learn.microsoft.com/en-us/azure/storage/common/storage-account-overview): Provides blob storage for persisting vacation activity data. The web application stores each activity as a JSON blob file in the `activities` container. 2. [Azure App Service Plan](https://learn.microsoft.com/en-us/azure/app-service/overview-hosting-plans): Defines the compute resources (CPU, memory, and scaling options) that host the web application. @@ -38,260 +38,10 @@ The Terraform modules deploy the following Azure resources: The web app allows users to plan and manage vacation activities, storing all activity data as blob files in the `activities` containers in the [Azure Storage Account](https://learn.microsoft.com/en-us/azure/storage/common/storage-account-overview). For more information, see [Azure Web App with Managed Identity](../README.md). -## Terraform Modules - -Below is a summary of the key Terraform modules included in this deployment: - -- **`main.tf`**: Defines all Azure resources and their configuration. -- **`variables.tf`**: Declares input variables and validation rules. -- **`outputs.tf`**: Specifies output values after deployment. -- **`providers.tf`**: Configures the Terraform provider for Azure. - -Below you can read the declarative code in HashiCorp Configuration Language (HCL). The `main.tf` module uses conditional provisioning for the user-assigned managed identity and role assignments resources. In Terraform, you can use the `count` argument in a conditional expression to decide whether creating resources or not. For example, the `count = var.managed_identity_type == "UserAssigned" ? 1 : 0` expression instructs Terraform to create the user-assigned managed identity resource when the value of the variable named `managed_identity_type` is set to `UserAssigned`. Refer to ​​[Conditional Expressions](https://developer.hashicorp.com/terraform/language/expressions/conditionals) for more information. - -```terraform -# Local Variables -locals { - resource_group_name = "${var.prefix}-rg" - storage_account_name = "${var.prefix}storage${var.suffix}" - app_service_plan_name = "${var.prefix}-app-service-plan-${var.suffix}" - web_app_name = "${var.prefix}-webapp-${var.suffix}" - managed_identity_name = "${var.prefix}-identity-${var.suffix}" -} - -# Create a resource group -resource "azurerm_resource_group" "example" { - name = local.resource_group_name - location = var.location - tags = var.tags -} - -# Create a storage account -resource "azurerm_storage_account" "example" { - name = local.storage_account_name - resource_group_name = azurerm_resource_group.example.name - location = azurerm_resource_group.example.location - account_replication_type = var.account_replication_type - account_kind = var.account_kind - account_tier = var.account_tier - tags = var.tags - - identity { - type = "SystemAssigned" - } - - lifecycle { - ignore_changes = [ - tags - ] - } -} - -# Create storage container -resource "azurerm_storage_container" "example" { - name = var.storage_container_name - storage_account_id = azurerm_storage_account.example.id - container_access_type = "private" -} - -# Conditionally create a user assigned identity for the function app -resource "azurerm_user_assigned_identity" "identity" { - count = var.managed_identity_type == "UserAssigned" ? 1 : 0 - - name = local.managed_identity_name - resource_group_name = azurerm_resource_group.example.name - location = azurerm_resource_group.example.location -} - -# Assign Storage Blob Data Contributor role to the function app identity -resource "azurerm_role_assignment" "blob_contributor" { - scope = azurerm_storage_account.example.id - role_definition_name = "Storage Blob Data Contributor" - principal_id = var.managed_identity_type == "UserAssigned" ? azurerm_user_assigned_identity.identity[0].principal_id : azurerm_linux_web_app.example.identity[0].principal_id -} - -# Create a service plan -resource "azurerm_service_plan" "example" { - 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 - tags = var.tags - - lifecycle { - ignore_changes = [ - tags - ] - } -} - -# Create a web app -resource "azurerm_linux_web_app" "example" { - name = local.web_app_name - resource_group_name = azurerm_resource_group.example.name - location = azurerm_resource_group.example.location - service_plan_id = azurerm_service_plan.example.id - https_only = var.https_only - public_network_access_enabled = var.webapp_public_network_access_enabled - client_affinity_enabled = false - tags = var.tags - - identity { - type = var.managed_identity_type - identity_ids = var.managed_identity_type == "UserAssigned" ? [ - azurerm_user_assigned_identity.identity[0].id - ] : [] - } - - site_config { - always_on = var.always_on - http2_enabled = var.http2_enabled - minimum_tls_version = var.minimum_tls_version - application_stack { - python_version = var.python_version - } - } - - app_settings = { - SCM_DO_BUILD_DURING_DEPLOYMENT = "true" - ENABLE_ORYX_BUILD = "true" - AZURE_STORAGE_ACCOUNT_URL = azurerm_storage_account.example.primary_blob_endpoint - CONTAINER_NAME = azurerm_storage_container.example.name - AZURE_CLIENT_ID = var.managed_identity_type == "UserAssigned" ? azurerm_user_assigned_identity.identity[0].client_id : "" - } - - 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 = "main" - use_manual_integration = true - use_mercurial = false -} -``` - -## Deployment Script - -You can use the `deploy.sh` script to automate the deployment of all Azure resources and the sample application in a single step, streamlining setup and reducing manual configuration. Before running the script, customize the variable values based on your needs. In particular, use the `MANAGED_IDENTITY_TYPE` variable to specify the type of managed identity to provision: `SystemAssigned` or `UserAssigned`. - -```bash -#!/bin/bash - -# Variables -PREFIX='local' -SUFFIX='test' -LOCATION='westeurope' -MANAGED_IDENTITY_TYPE='SystemAssigned' # SystemAssigned or UserAssigned -CURRENT_DIR="$(cd "$(dirname "$0")" && pwd)" -ZIPFILE="planner_website.zip" -ENVIRONMENT=$(az account show --query environmentName --output tsv) - -# Change the current directory to the script's directory -cd "$CURRENT_DIR" || exit - -# Run terraform init and apply -if [[ $ENVIRONMENT == "LocalStack" ]]; then - echo "Using tflocal and azlocal for LocalStack emulator environment and ." - TERRAFORM="tflocal" - AZ="azlocal" -else - echo "Using standard terraform and az for AzureCloud environment." - TERRAFORM="terraform" - AZ="az" -fi - -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 "managed_identity_type=$MANAGED_IDENTITY_TYPE" - -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) -STORAGE_ACCOUNT_NAME=$(terraform output -raw storage_account_name) -WEB_APP_NAME=$(terraform output -raw web_app_name) - -# Check if output values are empty -if [[ -z "$WEB_APP_NAME" || -z "$STORAGE_ACCOUNT_NAME" ]]; then - echo "Web App Name or Storage Account Name is empty. Exiting." - exit 1 -fi - -# Change current directory to source folder -cd "../src" || exit - -# Remove any existing zip package of the web app -if [ -f "$ZIPFILE" ]; then - rm "$ZIPFILE" -fi - -# Create the zip package of the web app -echo "Creating zip package of the web app..." -zip -r "$ZIPFILE" app.py activities.py database.py static templates requirements.txt - -# Deploy the web app -echo "Deploying web app [$WEB_APP_NAME] with zip file [$ZIPFILE]..." -$AZ webapp deploy \ - --resource-group "$RESOURCE_GROUP_NAME" \ - --name "$WEB_APP_NAME" \ - --src-path "$ZIPFILE" \ - --type zip \ - --async true 1>/dev/null - -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 - -# Remove the zip package of the web app -if [ -f "$ZIPFILE" ]; then - rm "$ZIPFILE" -fi -``` - -> **Note** -> You can use the `azlocal` CLI as a drop-in replacement for the `az` CLI to direct all commands to the LocalStack for Azure emulator. Alternatively, run `azlocal start_interception` to automatically intercept and redirect all `az` commands to LocalStack. Likewise, the `tflocal` is a local replacement for the standard `terraform` CLI, allowing you to run Terraform commands against LocalStack's Azure emulation environment. For more information, see [Get started with the az tool on LocalStack](https://azure.localstack.cloud/user-guides/sdks/az/). -The `deploy.sh` script executes the following steps: +## Provisioning Scripts -- 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. -- Extracts resource names and outputs from the Terraform deployment. -- Packages the code of the web application into a zip file for deployment. -- Deploys the zip package to the Azure Web App using the LocalStack Azure CLI. +You can use the [deploy.sh](deploy.sh) script to automate the deployment of all Azure resources and the sample application in a single step, streamlining setup and reducing manual configuration. Before running the script, customize the variable values based on your needs. In particular, use the `MANAGED_IDENTITY_TYPE` variable to specify the type of managed identity to provision: `SystemAssigned` or `UserAssigned`. ## Configuration diff --git a/samples/web-app-sql-database/python/README.md b/samples/web-app-sql-database/python/README.md index 67dc512..1b157f4 100644 --- a/samples/web-app-sql-database/python/README.md +++ b/samples/web-app-sql-database/python/README.md @@ -1,4 +1,4 @@ -# Azure Web App with Azure SQL Database +# Azure Web App with Azure SQL Database and Azure Key Vault This sample demonstrates a Python Flask single-page web application called *Vacation Planner* hosted on an [Azure Web App](https://learn.microsoft.com/en-us/azure/app-service/overview). The app runs on an Azure App Service Plan and stores activity data in an `activities` table within the `sampledb` database on an [Azure SQL Database](https://learn.microsoft.com/en-us/azure/azure-sql/database/) instance. diff --git a/samples/web-app-sql-database/python/bicep/README.md b/samples/web-app-sql-database/python/bicep/README.md index c9f2ef9..7717b57 100644 --- a/samples/web-app-sql-database/python/bicep/README.md +++ b/samples/web-app-sql-database/python/bicep/README.md @@ -17,7 +17,7 @@ Before deploying this solution, ensure you have the following tools installed: ### Installing azlocal CLI -The deployment script uses the `azlocal` CLI instead of the standard Azure CLI to work with LocalStack. Install it using: +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 @@ -27,851 +27,17 @@ For more information, see [Get started with the az tool on LocalStack](https://a ## Architecture Overview -The Bicep template deploys the following Azure resources: +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, while the [main.bicep](main.bicep) Bicep module creates the following Azure resources: 1. [Azure SQL Server](https://learn.microsoft.com/en-us/azure/azure-sql/database/sql-database-paas-overview): Logical server hosting one or more Azure SQL Databases. 2. [Azure SQL Database](https://learn.microsoft.com/en-us/azure/azure-sql/database/): The `PlannerDB` database storing relational vacation activity data. 3. [Azure App Service Plan](https://learn.microsoft.com/en-us/azure/app-service/overview-hosting-plans): The compute resource that hosts the web application. 4. [Azure Web App](https://learn.microsoft.com/en-us/azure/app-service/overview): Hosts the Python Flask single-page application (*Vacation Planner*), connected to Azure SQL Database. 5. [App Service Source Control](https://learn.microsoft.com/en-us/rest/api/appservice/web-apps/create-or-update-source-control?view=rest-appservice-2024-11-01): (Optional) Configures automatic deployment from a public GitHub repository. +6. [Azure Key Vault](https://learn.microsoft.com/en-us/azure/key-vault/general/overview): Stores the SQL connection string in a secret. The web app allows users to plan and manage vacation activities, storing all activity data in the `Activities` table in the `PlannerDB` database. For more information, see [Azure Web App with Azure SQL Database](../README.md). -## Bicep Templates - -The `main.bicep` Bicep template defines all Azure resources using declarative syntax: - -```bicep -@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 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 appServicePlanZoneRedundant bool = false - -@description('Specifies the language runtime used by the Azure Web App.') -@allowed([ - 'dotnet' - 'dotnet-isolated' - 'python' - 'java' - 'node' - 'powerShell' - 'custom' -]) -param runtimeName string - -@description('Specifies the target language version used by the Azure Web App.') -param runtimeVersion string - -@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.0' - '1.1' - '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 tags to be applied to the resources.') -param tags object = { - environment: 'test' - iac: 'bicep' -} - -@description('Specifies the administrator username of the SQL logical server.') -param administratorLogin string = 'sqladmin' - -@description('Specifies the administrator password of the SQL logical server.') -@secure() -param administratorLoginPassword string = 'P@ssw0rd1234!' - -@description('Conditional. The Azure Active Directory (AAD) administrator authentication. Required if no `administratorLogin` & `administratorLoginPassword` is provided.') -param administrators object? - -@description('Specifies the conditional Developmentresource ID of a user-assigned identityDevelopment to be used by default. This is required if `userAssignedIdentities` is not empty.') -param primaryUserAssignedIdentityResourceId string? - -@allowed([ - '1.0' - '1.1' - '1.2' - '1.3' -]) -@description('Specifies the optional Developmentminimal TLS versionDevelopment allowed for connections.') -param minimalTlsVersion string = '1.2' - -@allowed([ - 'Disabled' - 'Enabled' -]) -@description('Specifies whether or not to optionally enable IPv6 support for this server.') -param isIPv6Enabled string = 'Disabled' - -@description('Specifies the version of the SQL server to deploy.') -param version string = '12.0' - -@description('Specifies whether to optionally restrict outbound network access for this server.') -@allowed([ - 'Enabled' - 'Disabled' -]) -param restrictOutboundNetworkAccess string? - -@description('Specifies the name of the SQL Database.') -param sqlDatabaseName string = 'PlannerDB' - -@description('Specifies the optional SKU for the database.') -param sku object = { - name: 'Standard' - tier: 'Standard' - capacity: 10 -} - -@description('Specifies the optional time in minutes after which the database automatically pauses. A value of -1 disables automatic pausing.') -param autoPauseDelay int = -1 - -@description('Specifies the required Developmentavailability zoneDevelopment. A value of 1, 2, or 3 hardcodes the zone; -1 defines no zone. Note that these are logical availability zones within your Azure subscription. Refer to the Azure documentation for the mapping between physical and logical zones.') -@allowed([ - -1 - 1 - 2 - 3 -]) -param availabilityZone int = -1 - -@description('Specifies the optional collation for the metadata catalog.') -param catalogCollation string = 'DATABASE_DEFAULT' - -@description('Specifies the optional collation for the database.') -param collation string = 'SQL_Latin1_General_CP1_CI_AS' - -@description('Specifies the optional mode used for database creation.') -param createMode - | 'Default' - | 'Copy' - | 'OnlineSecondary' - | 'PointInTimeRestore' - | 'Recovery' - | 'Restore' - | 'RestoreExternalBackup' - | 'RestoreExternalBackupSecondary' - | 'RestoreLongTermRetentionBackup' - | 'Secondary' = 'Default' - -@description('Specifies the optional resource ID of the elastic pool containing this database.') -param elasticPoolResourceId string? - -@description('Specifies the optional Client ID for cross-tenant per-database Customer-Managed Key (CMK) scenarios.') -@minLength(36) -@maxLength(36) -param federatedClientId string? - -@description('Specifies the optional behavior when monthly free limits are exhausted for a free database.') -param freeLimitExhaustionBehavior 'AutoPause' | 'BillOverUsage'? - -@description('Specifies the optional number of read-only secondary replicas associated with the database.') -param highAvailabilityReplicaCount int = 0 - -@description('Specifies whether or not this database is a Developmentledger databaseDevelopment. All tables will be ledger tables. Note: this value cannot be changed after database creation.') -param isLedgerOn bool = false - -@description('Specifies the optional license type to apply for this database.') -param licenseType 'BasePrice' | 'LicenseIncluded'? - -@description('Specifies the optional resource identifier of the long-term retention backup used for the create operation.') -param longTermRetentionBackupResourceId string? - -@description('Specifies the optional Maintenance Configuration ID assigned to the database, which defines the period for maintenance updates.') -param maintenanceConfigurationId string? - -@description('Specifies whether optional customer-controlled manual cutover is required during an Update Database operation to the Hyperscale tier.') -param manualCutover bool? - -@description('Specifies the optional minimal capacity (vCores) that the database will always have allocated.') -param minCapacity string = '0' - -@description('Specifies the optional trigger for a customer-controlled manual cutover during a wait state while a scaling operation is in progress.') -param performCutover bool? - -@description('Specifies the optional type of enclave requested for the database, either Default or VBS enclaves.') -param preferredEnclaveType 'Default' | 'VBS'? - -@description('Specifies the optional state of read-only routing.') -param readScale 'Enabled' | 'Disabled' = 'Disabled' - -@description('Specifies the optional resource identifier of the recoverable database associated with the create operation.') -param recoverableDatabaseResourceId string? - -@description('Specifies the optional resource identifier of the recovery point associated with the create operation.') -param recoveryServicesRecoveryPointResourceId string? - -@description('Specifies the optional storage account type to be used for storing database backups.') -param requestedBackupStorageRedundancy 'Geo' | 'GeoZone' | 'Local' | 'Zone' = 'Local' - -@description('Specifies the optional resource identifier of the restorable dropped database associated with the create operation.') -param restorableDroppedDatabaseResourceId string? - -@description('Specifies the optional point in time (ISO8601 format) of the source database to restore when `createMode` is set to `Restore` or `PointInTimeRestore`.') -param restorePointInTime string? - -@description('Specifies the optional name of the sample schema to apply when creating this database.') -param sampleName string = '' - -@description('Specifies the optional secondary type of the database, if it is a secondary.') -param secondaryType 'Geo' | 'Named' | 'Standby'? - -@description('Specifies the optional time the database was deleted when restoring a deleted database.') -param sourceDatabaseDeletionDate string? - -@description('Specifies the optional resource identifier of the source database associated with the create operation.') -param sourceDatabaseResourceId string? - -@description('Specifies the optional resource identifier of the source associated with the create operation of this database.') -param sourceResourceId string? - -@description('Specifies whether or not the database uses free monthly limits. This is allowed for only one database per subscription.') -param useFreeLimit bool? - -@description('Specifies whether or not this database is Developmentzone redundantDevelopment.') -param sqlDatabaseZoneRedundant bool = false - -@description('Specifies the username for the SQL Database.') -param sqlDatabaseUsername string = 'testuser' - -@description('Specifies the password for the SQL Database.') -@secure() -param sqlDatabasePassword string = 'TestP@ssw0rd123' - -@description('Specifies the required name of the Server Firewall Rule.') -param sqlFirewallRuleName string = 'AllowAllIPs' - -@description('Specifies the optional end IP address of the firewall rule. Must be in IPv4 format and greater than or equal to `startIpAddress`. Use \'0.0.0.0\' to allow all Azure-internal IP addresses.') -param endIpAddress string = '255.255.255.255' - -@description('Specifies the optional start IP address of the firewall rule. Must be in IPv4 format. Use \'0.0.0.0\' to allow all Azure-internal IP addresses.') -param startIpAddress string = '0.0.0.0' - -@description('Specifies the username for the application.') -param username string = 'paolo' - -var sqlServerName = '${prefix}-sqlserver-${suffix}' -var webAppName = '${prefix}-webapp-${suffix}' -var appServicePlanName = '${prefix}-app-service-plan-${suffix}' -var identity = { - type: 'SystemAssigned' - } - -resource sqlServer 'Microsoft.Sql/servers@2024-05-01-preview' = { - name: sqlServerName - location: location - tags: tags - identity: identity - properties: { - administratorLogin: administratorLogin - administratorLoginPassword: administratorLoginPassword - administrators: union({ administratorType: 'ActiveDirectory' }, administrators ?? {}) - federatedClientId: federatedClientId - isIPv6Enabled: isIPv6Enabled - version: version - minimalTlsVersion: minimalTlsVersion - primaryUserAssignedIdentityId: primaryUserAssignedIdentityResourceId - publicNetworkAccess: publicNetworkAccess - restrictOutboundNetworkAccess: restrictOutboundNetworkAccess - } -} - -resource firewallRule 'Microsoft.Sql/servers/firewallRules@2024-11-01-preview' = { - name: sqlFirewallRuleName - parent: sqlServer - properties: { - endIpAddress: endIpAddress - startIpAddress: startIpAddress - } -} - -resource sqlDatabase 'Microsoft.Sql/servers/databases@2024-11-01-preview' = { - parent: sqlServer - name: sqlDatabaseName - location: location - tags: tags - sku: sku - properties: { - autoPauseDelay: autoPauseDelay - availabilityZone: availabilityZone != -1 ? string(availabilityZone) : 'NoPreference' - catalogCollation: catalogCollation - collation: collation - createMode: createMode - elasticPoolId: elasticPoolResourceId - federatedClientId: federatedClientId - freeLimitExhaustionBehavior: freeLimitExhaustionBehavior - highAvailabilityReplicaCount: highAvailabilityReplicaCount - isLedgerOn: isLedgerOn - licenseType: licenseType - longTermRetentionBackupResourceId: longTermRetentionBackupResourceId - maintenanceConfigurationId: maintenanceConfigurationId - manualCutover: manualCutover - minCapacity: !empty(minCapacity) ? json(minCapacity) : 0 - performCutover: performCutover - preferredEnclaveType: preferredEnclaveType - readScale: readScale - recoverableDatabaseId: recoverableDatabaseResourceId - recoveryServicesRecoveryPointId: recoveryServicesRecoveryPointResourceId - requestedBackupStorageRedundancy: requestedBackupStorageRedundancy - restorableDroppedDatabaseId: restorableDroppedDatabaseResourceId - restorePointInTime: restorePointInTime - sampleName: sampleName - secondaryType: secondaryType - sourceDatabaseDeletionDate: sourceDatabaseDeletionDate - sourceDatabaseId: sourceDatabaseResourceId - sourceResourceId: sourceResourceId - useFreeLimit: useFreeLimit - zoneRedundant: sqlDatabaseZoneRedundant - } -} - -resource appServicePlan 'Microsoft.Web/serverfarms@2024-11-01' = { - name: appServicePlanName - location: location - tags: tags - kind: appServicePlanKind - sku: { - tier: skuTier - name: skuName - } - properties: { - reserved: reserved - zoneRedundant: appServicePlanZoneRedundant - maximumElasticWorkerCount: skuTier == 'FlexConsumption' ? 1 : 20 - } -} - -resource webApp 'Microsoft.Web/sites@2024-11-01' = { - name: webAppName - location: location - tags: tags - kind: webAppKind - properties: { - httpsOnly: httpsOnly - serverFarmId: appServicePlan.id - siteConfig: { - linuxFxVersion: toUpper('${runtimeName}|${runtimeVersion}') - minTlsVersion: minTlsVersion - publicNetworkAccess: publicNetworkAccess - } - } - identity: { - type: 'SystemAssigned' - } -} - -resource configAppSettings 'Microsoft.Web/sites/config@2024-11-01' = { - parent: webApp - name: 'appsettings' - properties: { - SCM_DO_BUILD_DURING_DEPLOYMENT: 'true' - ENABLE_ORYX_BUILD: 'true' - SQL_SERVER: sqlServer.properties.fullyQualifiedDomainName - SQL_DATABASE: sqlDatabaseName - SQL_USERNAME: sqlDatabaseUsername - SQL_PASSWORD: sqlDatabasePassword - LOGIN_NAME: username - } -} - -resource webAppSourceControl 'Microsoft.Web/sites/sourcecontrols@2024-11-01' = if (contains(repoUrl,'http')){ - name: 'web' - parent: webApp - properties: { - repoUrl: repoUrl - branch: 'master' - isManualIntegration: true - } -} - -output appServicePlanName string = appServicePlan.name -output webAppName string = webApp.name -output webAppUrl string = webApp.properties.defaultHostName -output sqlServerName string = sqlServer.name -output sqlServerFqdn string = sqlServer.properties.fullyQualifiedDomainName -output sqlDatabaseName string = sqlDatabase.name -``` -## Configuration - -Before deploying the `main.bicep` template, update the `bicep.bicepparam` file with your specific values: - -```bicep -using 'main.bicep' - -param prefix = 'local' -param suffix = 'test' -param runtimeName = 'python' -param runtimeVersion = '3.13' -param username = 'paolo' -``` - -## Deployment Script - -You can use the `deploy.sh` script to automate the deployment of all Azure resources and the sample application in a single step, streamlining setup and reducing manual configuration. - -```bash -#!/bin/bash - -# Variables -PREFIX='local' -SUFFIX='test' -TEMPLATE="main.bicep" -PARAMETERS="main.bicepparam" -RESOURCE_GROUP_NAME="$PREFIX-rg" -LOCATION="westeurope" -VALIDATE_TEMPLATE=1 -USE_WHAT_IF=0 -SUBSCRIPTION_NAME=$(az account show --query name --output tsv) -ADMIN_USER='sqladmin' -ADMIN_PASSWORD='P@ssw0rd1234!' -DATABASE_USER_NAME='testuser' -DATABASE_USER_PASSWORD='TestP@ssw0rd123' -CURRENT_DIR="$(cd "$(dirname "$0")" && pwd)" -ZIPFILE="planner_website.zip" -ENVIRONMENT=$(az account show --query environmentName --output tsv) -DEPLOY_APP=1 - -# Change the current directory to the script's directory -cd "$CURRENT_DIR" || exit - -# Choose the appropriate CLI based on the environment -if [[ $ENVIRONMENT == "LocalStack" ]]; then - echo "Using azlocal for LocalStack emulator environment." - AZ="azlocal" -else - echo "Using standard az for AzureCloud environment." - AZ="az" -fi - -# 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 - -# 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 [$TEMPLATE]..." - $AZ deployment group what-if \ - --resource-group $RESOURCE_GROUP_NAME \ - --template-file $TEMPLATE \ - --parameters $PARAMETERS \ - --parameters location=$LOCATION \ - prefix=$PREFIX \ - suffix=$SUFFIX \ - administratorLogin=$ADMIN_USER \ - administratorLoginPassword=$ADMIN_PASSWORD \ - sqlDatabaseUsername=$DATABASE_USER_NAME \ - sqlDatabasePassword=$DATABASE_USER_PASSWORD \ - --only-show-errors - - if [[ $? == 0 ]]; then - echo "Bicep template [$TEMPLATE] validation succeeded" - else - echo "Failed to validate Bicep template [$TEMPLATE]" - exit - fi - else - # Validate the Bicep template - echo "Validating Bicep template [$TEMPLATE]..." - output=$($AZ deployment group validate \ - --resource-group $RESOURCE_GROUP_NAME \ - --template-file $TEMPLATE \ - --parameters $PARAMETERS \ - --parameters location=$LOCATION \ - prefix=$PREFIX \ - suffix=$SUFFIX \ - administratorLogin=$ADMIN_USER \ - administratorLoginPassword=$ADMIN_PASSWORD \ - sqlDatabaseUsername=$DATABASE_USER_NAME \ - sqlDatabasePassword=$DATABASE_USER_PASSWORD \ - --only-show-errors) - - if [[ $? == 0 ]]; then - echo "Bicep template [$TEMPLATE] validation succeeded" - else - echo "Failed to validate Bicep template [$TEMPLATE]" - echo "$output" - exit - fi - fi -fi - -# Deploy the Bicep template -echo "Deploying Bicep template [$TEMPLATE]..." -if DEPLOYMENT_OUTPUTS=$($AZ deployment group create \ - --resource-group $RESOURCE_GROUP_NAME \ - --only-show-errors \ - --template-file $TEMPLATE \ - --parameters $PARAMETERS \ - --parameters location=$LOCATION \ - prefix=$PREFIX \ - suffix=$SUFFIX \ - administratorLogin=$ADMIN_USER \ - administratorLoginPassword=$ADMIN_PASSWORD \ - sqlDatabaseUsername=$DATABASE_USER_NAME \ - sqlDatabasePassword=$DATABASE_USER_PASSWORD \ - --query 'properties.outputs' -o json); then - echo "Bicep template [$TEMPLATE] deployed successfully. Outputs:" - echo "$DEPLOYMENT_OUTPUTS" | jq . - APP_SERVICE_PLAN_NAME=$(echo "$DEPLOYMENT_OUTPUTS" | jq -r '.appServicePlanName.value') - WEB_APP_NAME=$(echo "$DEPLOYMENT_OUTPUTS" | jq -r '.webAppName.value') - SQL_SERVER_NAME=$(echo "$DEPLOYMENT_OUTPUTS" | jq -r '.sqlServerName.value') - SQL_DATABASE_NAME=$(echo "$DEPLOYMENT_OUTPUTS" | jq -r '.sqlDatabaseName.value') - echo "Deployment details:" - echo "appServicePlanName: $APP_SERVICE_PLAN_NAME" - echo "webAppName: $WEB_APP_NAME" - echo "webAppUrl: $WEB_APP_URL" - echo "sqlServerName: $SQL_SERVER_NAME" - echo "sqlDatabaseName: $SQL_DATABASE_NAME" -else - echo "Failed to deploy Bicep template [$TEMPLATE]" - exit 1 -fi - -if [[ -z "$WEB_APP_NAME" || -z "$SQL_SERVER_NAME" || -z "$SQL_DATABASE_NAME" ]]; then - echo "Web App Name, SQL Server Name, or SQL Database Name is empty. Exiting." - exit 1 -fi - -# Retrieve the fullyQualifiedDomainName of the SQL server -echo "Retrieving the fullyQualifiedDomainName of the [$SQL_SERVER_NAME] SQL server..." -SQL_SERVER_FQDN=$($AZ sql server show \ - --name "$SQL_SERVER_NAME" \ - --resource-group "$RESOURCE_GROUP_NAME" \ - --query "fullyQualifiedDomainName" \ - --output tsv) - -if [ -z "$SQL_SERVER_FQDN" ]; then - echo "Failed to retrieve the fullyQualifiedDomainName of the SQL server" - exit 1 -fi - -# Create server-level login -echo "Creating login [$DATABASE_USER_NAME] at server level..." -sqlcmd -S "$SQL_SERVER_FQDN" \ - -d master \ - -U "$ADMIN_USER" \ - -P "$ADMIN_PASSWORD" \ - -Q "IF NOT EXISTS (SELECT * FROM sys.sql_logins WHERE name = '$DATABASE_USER_NAME') - CREATE LOGIN [$DATABASE_USER_NAME] WITH PASSWORD = '$DATABASE_USER_PASSWORD';" \ - -V 1 - -if [ $? -eq 0 ]; then - echo "Login [$DATABASE_USER_NAME] created successfully" -else - echo "Failed to create login [$DATABASE_USER_NAME]" - exit 1 -fi - -# Create database user -echo "Creating user [$DATABASE_USER_NAME] in database [$SQL_DATABASE_NAME]..." -sqlcmd -S "$SQL_SERVER_FQDN" \ - -d "$SQL_DATABASE_NAME" \ - -U "$ADMIN_USER" \ - -P "$ADMIN_PASSWORD" \ - -Q "IF NOT EXISTS (SELECT * FROM sys.database_principals WHERE name = '$DATABASE_USER_NAME') - CREATE USER [$DATABASE_USER_NAME] FOR LOGIN [$DATABASE_USER_NAME];" \ - -V 1 - -if [ $? -eq 0 ]; then - echo "User [$DATABASE_USER_NAME] created successfully in database [$SQL_DATABASE_NAME]" -else - echo "Failed to create user [$DATABASE_USER_NAME]" - exit 1 -fi - -# Grant permissions including DDL rights -echo "Granting permissions to [$DATABASE_USER_NAME]..." -sqlcmd -S "$SQL_SERVER_FQDN" \ - -d "$SQL_DATABASE_NAME" \ - -U "$ADMIN_USER" \ - -P "$ADMIN_PASSWORD" \ - -Q "ALTER ROLE db_datareader ADD MEMBER [$DATABASE_USER_NAME]; - ALTER ROLE db_datawriter ADD MEMBER [$DATABASE_USER_NAME]; - ALTER ROLE db_ddladmin ADD MEMBER [$DATABASE_USER_NAME];" \ - -V 1 - -if [ $? -eq 0 ]; then - echo "Permissions granted successfully to [$DATABASE_USER_NAME]" -else - echo "Failed to grant permissions to [$DATABASE_USER_NAME]" - exit 1 -fi - -# Test connection -echo "Testing connection with user [$DATABASE_USER_NAME]..." -sqlcmd -S "$SQL_SERVER_FQDN" \ - -d "$SQL_DATABASE_NAME" \ - -U "$DATABASE_USER_NAME" \ - -P "$DATABASE_USER_PASSWORD" \ - -Q "SELECT SYSTEM_USER AS CurrentUser, DB_NAME() AS CurrentDatabase, GETDATE() AS CurrentTime;" \ - -V 1 - -if [ $? -eq 0 ]; then - echo "Connection test successful with user [$DATABASE_USER_NAME]" -else - echo "Connection test failed with user [$DATABASE_USER_NAME]" - exit 1 -fi - -# Create table -echo "Creating test [Products] table..." -sqlcmd -S "$SQL_SERVER_FQDN" \ - -d "$SQL_DATABASE_NAME" \ - -U "$DATABASE_USER_NAME" \ - -P "$DATABASE_USER_PASSWORD" \ - -Q "IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'Activities' AND schema_id = SCHEMA_ID('dbo')) - CREATE TABLE dbo.Activities ( - -- Primary Key: UNIQUEIDENTIFIER with a default of a new sequential GUID (best for indexing) - id UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWSEQUENTIALID(), - - -- Username field - username VARCHAR(32) NOT NULL, - - -- Description of the activity - activity VARCHAR(128) NOT NULL, - - -- Timestamp of the activity - timestamp DATETIME NOT NULL - );" \ - -V 1 - -if [ $? -eq 0 ]; then - echo "Test [Activities] table created successfully" -else - echo "Failed to create test [Activities] table" - exit 1 -fi - -# Insert data -echo "Inserting test data into [Activities] table..." -sqlcmd -S "$SQL_SERVER_FQDN" \ - -d "$SQL_DATABASE_NAME" \ - -U "$DATABASE_USER_NAME" \ - -P "$DATABASE_USER_PASSWORD" \ - -Q "INSERT INTO Activities (username, activity, timestamp) - VALUES - ('paolo', 'Go to Paris', GETDATE()), - ('paolo', 'Go to London', GETDATE()), - ('paolo', 'Go to Mexico', GETDATE());" \ - -V 1 - -if [ $? -eq 0 ]; then - echo "Test data inserted successfully into [Activities] table" -else - echo "Failed to insert test data into [Activities] table" - exit 1 -fi - -# Query data -echo "Querying test data from [Activities] table..." -sqlcmd -S "$SQL_SERVER_FQDN" \ - -d "$SQL_DATABASE_NAME" \ - -U "$DATABASE_USER_NAME" \ - -P "$DATABASE_USER_PASSWORD" \ - -Q "SELECT * FROM Activities;" \ - -V 1 - -if [ $? -eq 0 ]; then - echo "Test data queried successfully from [Activities] table" -else - echo "Failed to query test data from [Activities] table" - exit 1 -fi - -if [[ $DEPLOY_APP -eq 0 ]]; then - echo "Skipping web app deployment as DEPLOY_APP flag is set to 0." - exit 0 -fi - -# Change current directory to source folder -cd "../src" || exit - -# Remove any existing zip package of the web app -if [ -f "$ZIPFILE" ]; then - rm "$ZIPFILE" -fi - -# Create the zip package of the web app -echo "Creating zip package of the web app..." -zip -r "$ZIPFILE" app.py activities.py database.py static templates requirements.txt - -# Deploy the web app -echo "Deploying web app [$WEB_APP_NAME] with zip file [$ZIPFILE]..." -$AZ webapp deploy \ - --resource-group "$RESOURCE_GROUP_NAME" \ - --name "$WEB_APP_NAME" \ - --src-path "$ZIPFILE" \ - --type zip \ - --async true 1>/dev/null - -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 - -# Remove the zip package of the web app -if [ -f "$ZIPFILE" ]; then - rm "$ZIPFILE" -fi -``` - -> **Note** -> You can use the `azlocal` CLI as a drop-in replacement for the `az` CLI to direct all commands to the LocalStack for Azure emulator. Alternatively, run `azlocal start_interception` to automatically intercept and redirect all `az` commands to LocalStack. For more information, see [Get started with the az tool on LocalStack](https://azure.localstack.cloud/user-guides/sdks/az/). - -The `deploy.sh` script executes the following steps: - -- Specifies the variables used during deployment. -- Creates the resource group if it does not exist. -- Conditionally validates the `main.bicep` module to check its syntax is correct and all parameters make sense. -- Conditionally runs a what-if deployment to execute a dry run to preview the resources that will be created, updated, or deleted. -- Runs the `main.bicep` template to create all the Azure resources. -- Collects important information from the deployment (like resource names) for later use. -- Uses jq (a JSON tool) to extract the names of resources we just created. -- Shows us all the settings that got applied to the Web App. -- Removes previous build artifacts for consistency. -- Creates zip archive in format expected by Web App. -- Uploads pre-built application package to the newly created Web App. - -> **Note** -> Azure CLI commands use `--verbose` argument to print execution details and the `--debug` flag to show low-level REST calls for debugging. For more information, see [Get started with Azure CLI](https://learn.microsoft.com/en-us/cli/azure/get-started-with-azure-cli) - ## Deployment You can set up the Azure emulator by utilizing 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: diff --git a/samples/web-app-sql-database/python/images/architecture.png b/samples/web-app-sql-database/python/images/architecture.png index 13999c7..85dbd92 100644 Binary files a/samples/web-app-sql-database/python/images/architecture.png and b/samples/web-app-sql-database/python/images/architecture.png differ diff --git a/samples/web-app-sql-database/python/scripts/README.md b/samples/web-app-sql-database/python/scripts/README.md index 6f5786c..9ea342c 100644 --- a/samples/web-app-sql-database/python/scripts/README.md +++ b/samples/web-app-sql-database/python/scripts/README.md @@ -16,7 +16,7 @@ Before deploying this solution, ensure you have the following tools installed: ### Installing azlocal CLI -The deployment script uses the `azlocal` CLI to work with LocalStack. Install it using: +The [deploy.sh](deploy.sh) Bash script uses the `azlocal` CLI to work with LocalStack. Install it using: ```bash pip install azlocal @@ -26,7 +26,7 @@ For more information, see [Get started with the az tool on LocalStack](https://a ## Architecture Overview -This CLI deployment creates the following Azure resources using direct Azure CLI commands: +The [deploy.sh](deploy.sh) Bash script creates the following Azure resources using Azure CLI commands: 1. [Azure Resource Group](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/manage-resource-groups-cli): Logical container for all resources 2. [Azure SQL Server](https://learn.microsoft.com/en-us/azure/azure-sql/database/sql-database-paas-overview): Logical server hosting one or more Azure SQL Databases. @@ -34,387 +34,10 @@ This CLI deployment creates the following Azure resources using direct Azure CLI 4. [Azure App Service Plan](https://learn.microsoft.com/en-us/azure/app-service/overview-hosting-plans): The compute resource that hosts the web application. 5. [Azure Web App](https://learn.microsoft.com/en-us/azure/app-service/overview): Hosts the Python Flask single-page application (*Vacation Planner*), connected to Azure SQL Database. 6. [App Service Source Control](https://learn.microsoft.com/en-us/rest/api/appservice/web-apps/create-or-update-source-control?view=rest-appservice-2024-11-01): (Optional) Configures automatic deployment from a public GitHub repository. +7. [Azure Key Vault](https://learn.microsoft.com/en-us/azure/key-vault/general/overview): Stores the SQL connection string in a secret. The system implements a Vacation Planner web application that stores and retrieves activity data from Azure SQL Database. For more information, see [Azure Web App with Azure SQL Database](../README.md). -## Deployment Script - -You can use the `deploy.sh` script to automate the deployment of all Azure resources and the sample application in a single step, streamlining setup and reducing manual configuration. - -```bash -#!/bin/bash - -# Variables -PREFIX='local' -SUFFIX='test' -LOCATION='westeurope' -RESOURCE_GROUP_NAME="${PREFIX}-rg" -SQL_SERVER_NAME="${PREFIX}-sqlserver-${SUFFIX}" -FIREWALL_RULE_NAME="AllowAllIPs" -ADMIN_USER='sqladmin' -ADMIN_PASSWORD='P@ssw0rd1234!' -DATABASE_USER_NAME='testuser' -DATABASE_USER_PASSWORD='TestP@ssw0rd123' -SQL_DATABASE_NAME='PlannerDB' -APP_SERVICE_PLAN_NAME="${PREFIX}-app-service-plan-${SUFFIX}" -APP_SERVICE_PLAN_SKU="S1" -WEB_APP_NAME="${PREFIX}-webapp-${SUFFIX}" -LOGIN_NAME="Paolo" -CURRENT_DIR="$(cd "$(dirname "$0")" && pwd)" -ZIPFILE="planner_website.zip" -RUNTIME="python" -RUNTIME_VERSION="3.13" -DEPLOY_APP=1 -ENVIRONMENT=$(az account show --query environmentName --output tsv) - -# Change the current directory to the script's directory -cd "$CURRENT_DIR" || exit - -# Choose the appropriate CLI based on the environment -if [[ $ENVIRONMENT == "LocalStack" ]]; then - echo "Using azlocal for LocalStack emulator environment." - AZ="azlocal" -else - echo "Using standard az for AzureCloud environment." - AZ="az" -fi - -# Create a resource group -echo "Creating resource group [$RESOURCE_GROUP_NAME]..." -$AZ group create \ - --name $RESOURCE_GROUP_NAME \ - --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 - -# Create a sql server -echo "Checking if [$SQL_SERVER_NAME] sql server exists in the [$RESOURCE_GROUP_NAME] resource group..." -$AZ sql server show \ - --name $SQL_SERVER_NAME \ - --resource-group $RESOURCE_GROUP_NAME \ - --only-show-errors &>/dev/null - -if [ $? -eq 0 ]; then - echo "[$SQL_SERVER_NAME] sql server already exists in the [$RESOURCE_GROUP_NAME] resource group. Exiting script." -else - echo "[$SQL_SERVER_NAME] sql server does not exist in the [$RESOURCE_GROUP_NAME] resource group. Proceeding to create it." - echo "Creating [$SQL_SERVER_NAME] sql server in the [$RESOURCE_GROUP_NAME] resource group..." - $AZ sql server create \ - --name $SQL_SERVER_NAME \ - --resource-group $RESOURCE_GROUP_NAME \ - --location $LOCATION \ - --admin-user $ADMIN_USER \ - --admin-password $ADMIN_PASSWORD \ - --assign-identity \ - --identity-type SystemAssigned \ - --minimal-tls-version 1.2 \ - --tags environment=test \ - --only-show-errors 1>/dev/null - - if [ $? == 0 ]; then - echo "[$SQL_SERVER_NAME] sql server successfully created in the [$RESOURCE_GROUP_NAME] resource group" - else - echo "Failed to create [$SQL_SERVER_NAME] sql server in the [$RESOURCE_GROUP_NAME] resource group" - exit - fi -fi - -# Add firewall rule to allow all local network addresses (for testing/development) -echo "Creating firewall rule to allow all IP addresses..." -$AZ sql server firewall-rule create \ - --name $FIREWALL_RULE_NAME \ - --resource-group $RESOURCE_GROUP_NAME \ - --server $SQL_SERVER_NAME \ - --start-ip-address 0.0.0.0 \ - --end-ip-address 255.255.255.255 \ - --only-show-errors 1>/dev/null - -if [ $? -eq 0 ]; then - echo "Firewall rule [AllowLocalNetwork] created successfully" -else - echo "Failed to create firewall rule" - exit 1 -fi - -# Create database if it does not exist -echo "Checking if [$SQL_DATABASE_NAME] database exists in the [$SQL_SERVER_NAME] sql server..." -$AZ sql db show \ - --name $SQL_DATABASE_NAME \ - --resource-group $RESOURCE_GROUP_NAME \ - --server $SQL_SERVER_NAME \ - --only-show-errors &>/dev/null - -if [ $? -eq 0 ]; then - echo "[$SQL_DATABASE_NAME] database already exists in the [$SQL_SERVER_NAME] sql server." -else - echo "Creating [$SQL_DATABASE_NAME] database with Provisioned compute model in the [$SQL_SERVER_NAME] sql server..." - $AZ sql db create \ - --name $SQL_DATABASE_NAME \ - --resource-group $RESOURCE_GROUP_NAME \ - --server $SQL_SERVER_NAME \ - --service-objective S0 \ - --compute-model Provisioned \ - --zone-redundant false \ - --tags environment=test \ - --only-show-errors 1>/dev/null - - if [ $? == 0 ]; then - echo "[$SQL_DATABASE_NAME] database with Provisioned compute model successfully created in the [$SQL_SERVER_NAME] sql server" - else - echo "Failed to create [$SQL_DATABASE_NAME] with Provisioned compute model database in the [$SQL_SERVER_NAME] sql server" - exit 1 - fi -fi - -# Retrieve the fullyQualifiedDomainName of the SQL server -echo "Retrieving the fullyQualifiedDomainName of the [$SQL_SERVER_NAME] SQL server..." -SQL_SERVER_FQDN=$($AZ sql server show \ - --name "$SQL_SERVER_NAME" \ - --resource-group "$RESOURCE_GROUP_NAME" \ - --query "fullyQualifiedDomainName" \ - --output tsv) - -if [ -z "$SQL_SERVER_FQDN" ]; then - echo "Failed to retrieve the fullyQualifiedDomainName of the SQL server" - exit 1 -fi - -# Create server-level login -echo "Creating login [$DATABASE_USER_NAME] at server level..." -sqlcmd -S "$SQL_SERVER_FQDN" \ - -d master \ - -U "$ADMIN_USER" \ - -P "$ADMIN_PASSWORD" \ - -Q "IF NOT EXISTS (SELECT * FROM sys.sql_logins WHERE name = '$DATABASE_USER_NAME') - CREATE LOGIN [$DATABASE_USER_NAME] WITH PASSWORD = '$DATABASE_USER_PASSWORD';" \ - -V 1 - -if [ $? -eq 0 ]; then - echo "Login [$DATABASE_USER_NAME] created successfully" -else - echo "Failed to create login [$DATABASE_USER_NAME]" - exit 1 -fi - -# Create database user -echo "Creating user [$DATABASE_USER_NAME] in database [$SQL_DATABASE_NAME]..." -sqlcmd -S "$SQL_SERVER_FQDN" \ - -d "$SQL_DATABASE_NAME" \ - -U "$ADMIN_USER" \ - -P "$ADMIN_PASSWORD" \ - -Q "IF NOT EXISTS (SELECT * FROM sys.database_principals WHERE name = '$DATABASE_USER_NAME') - CREATE USER [$DATABASE_USER_NAME] FOR LOGIN [$DATABASE_USER_NAME];" \ - -V 1 - -if [ $? -eq 0 ]; then - echo "User [$DATABASE_USER_NAME] created successfully in database [$SQL_DATABASE_NAME]" -else - echo "Failed to create user [$DATABASE_USER_NAME]" - exit 1 -fi - -# Grant permissions including DDL rights -echo "Granting permissions to [$DATABASE_USER_NAME]..." -sqlcmd -S "$SQL_SERVER_FQDN" \ - -d "$SQL_DATABASE_NAME" \ - -U "$ADMIN_USER" \ - -P "$ADMIN_PASSWORD" \ - -Q "ALTER ROLE db_datareader ADD MEMBER [$DATABASE_USER_NAME]; - ALTER ROLE db_datawriter ADD MEMBER [$DATABASE_USER_NAME]; - ALTER ROLE db_ddladmin ADD MEMBER [$DATABASE_USER_NAME];" \ - -V 1 - -if [ $? -eq 0 ]; then - echo "Permissions granted successfully to [$DATABASE_USER_NAME]" -else - echo "Failed to grant permissions to [$DATABASE_USER_NAME]" - exit 1 -fi - -# Test connection -echo "Testing connection with user [$DATABASE_USER_NAME]..." -sqlcmd -S "$SQL_SERVER_FQDN" \ - -d "$SQL_DATABASE_NAME" \ - -U "$DATABASE_USER_NAME" \ - -P "$DATABASE_USER_PASSWORD" \ - -Q "SELECT SYSTEM_USER AS CurrentUser, DB_NAME() AS CurrentDatabase, GETDATE() AS CurrentTime;" \ - -V 1 - -if [ $? -eq 0 ]; then - echo "Connection test successful with user [$DATABASE_USER_NAME]" -else - echo "Connection test failed with user [$DATABASE_USER_NAME]" - exit 1 -fi - -# Create table -echo "Creating test [Products] table..." -sqlcmd -S "$SQL_SERVER_FQDN" \ - -d "$SQL_DATABASE_NAME" \ - -U "$DATABASE_USER_NAME" \ - -P "$DATABASE_USER_PASSWORD" \ - -Q "IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'Activities' AND schema_id = SCHEMA_ID('dbo')) - CREATE TABLE dbo.Activities ( - -- Primary Key: UNIQUEIDENTIFIER with a default of a new sequential GUID (best for indexing) - id UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWSEQUENTIALID(), - - -- Username field - username VARCHAR(32) NOT NULL, - - -- Description of the activity - activity VARCHAR(128) NOT NULL, - - -- Timestamp of the activity - timestamp DATETIME NOT NULL - );" \ - -V 1 - -if [ $? -eq 0 ]; then - echo "Test [Activities] table created successfully" -else - echo "Failed to create test [Activities] table" - exit 1 -fi - -# Insert data -echo "Inserting test data into [Activities] table..." -sqlcmd -S "$SQL_SERVER_FQDN" \ - -d "$SQL_DATABASE_NAME" \ - -U "$DATABASE_USER_NAME" \ - -P "$DATABASE_USER_PASSWORD" \ - -Q "INSERT INTO Activities (username, activity, timestamp) - VALUES - ('paolo', 'Go to Paris', GETDATE()), - ('paolo', 'Go to London', GETDATE()), - ('paolo', 'Go to Mexico', GETDATE());" \ - -V 1 - -if [ $? -eq 0 ]; then - echo "Test data inserted successfully into [Activities] table" -else - echo "Failed to insert test data into [Activities] table" - exit 1 -fi - -# Query data -echo "Querying test data from [Activities] table..." -sqlcmd -S "$SQL_SERVER_FQDN" \ - -d "$SQL_DATABASE_NAME" \ - -U "$DATABASE_USER_NAME" \ - -P "$DATABASE_USER_PASSWORD" \ - -Q "SELECT * FROM Activities;" \ - -V 1 - -if [ $? -eq 0 ]; then - echo "Test data queried successfully from [Activities] table" -else - echo "Failed to query test data from [Activities] table" - exit 1 -fi - -# Create App Service Plan -echo "Creating 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 - -# Create the web app -echo "Creating web app [$WEB_APP_NAME]..." -$AZ webapp create \ - --resource-group "$RESOURCE_GROUP_NAME" \ - --plan "$APP_SERVICE_PLAN_NAME" \ - --name "$WEB_APP_NAME" \ - --runtime "$RUNTIME:$RUNTIME_VERSION" \ - --only-show-errors 1>/dev/null - -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 - -# Set web app settings -echo "Setting web app settings for [$WEB_APP_NAME]..." -$AZ webapp config appsettings set \ - --name "$WEB_APP_NAME" \ - --resource-group "$RESOURCE_GROUP_NAME" \ - --settings \ - SCM_DO_BUILD_DURING_DEPLOYMENT='true' \ - ENABLE_ORYX_BUILD='true' \ - SQL_SERVER="$SQL_SERVER_FQDN" \ - SQL_DATABASE="$SQL_DATABASE_NAME" \ - SQL_USERNAME="$DATABASE_USER_NAME" \ - SQL_PASSWORD="$DATABASE_USER_PASSWORD" \ - LOGIN_NAME="$LOGIN_NAME" \ - --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 - -if [[ $DEPLOY_APP -eq 0 ]]; then - echo "Skipping web app deployment as DEPLOY_APP flag is set to 0." - exit 0 -fi - -# Change current directory to source folder -cd "../src" || exit - -# Remove any existing zip package of the web app -if [ -f "$ZIPFILE" ]; then - rm "$ZIPFILE" -fi - -# Create the zip package of the web app -echo "Creating zip package of the web app..." -zip -r "$ZIPFILE" app.py activities.py database.py static templates requirements.txt - -# Deploy the web app -echo "Deploying web app [$WEB_APP_NAME] with zip file [$ZIPFILE]..." -$AZ webapp deploy \ - --resource-group "$RESOURCE_GROUP_NAME" \ - --name "$WEB_APP_NAME" \ - --src-path "$ZIPFILE" \ - --type zip \ - --async true 1>/dev/null - -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 - -# Remove the zip package of the web app -if [ -f "$ZIPFILE" ]; then - rm "$ZIPFILE" -fi -``` - -> [!NOTE] -> You can use the `azlocal` CLI as a drop-in replacement for the `az` CLI to direct all commands to the LocalStack for Azure emulator. Alternatively, run `azlocal start_interception` to automatically intercept and redirect all `az` commands to LocalStack. To revert back to the default behavior and send commands to the Azure cloud, run `azlocal stop_interception`. - ## Deployment You can set up the Azure emulator by utilizing 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: diff --git a/samples/web-app-sql-database/python/terraform/README.md b/samples/web-app-sql-database/python/terraform/README.md index ea1bde4..5c11824 100644 --- a/samples/web-app-sql-database/python/terraform/README.md +++ b/samples/web-app-sql-database/python/terraform/README.md @@ -2,6 +2,8 @@ This directory contains Terraform modules and a deployment script for provisioning Azure services in LocalStack for Azure. Refer to the [Azure Web App with Azure SQL Database](../README.md) guide for details about the sample application. +> **NOTE**: Terraform modules do not install Azure Key Vault. This will be fixed soon. + ## Prerequisites Before deploying this solution, ensure you have the following tools installed: @@ -17,7 +19,7 @@ Before deploying this solution, ensure you have the following tools installed: ### Installing azlocal CLI -The deployment script uses the `azlocal` CLI instead of the standard Azure CLI to work with LocalStack. Install it using: +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 @@ -27,7 +29,7 @@ For more information, see [Get started with the az tool on LocalStack](https://a ## Architecture Overview -The Terraform modules deploy the following Azure resources: +The [main.tf](main.tf) Terraform module creates the following Azure resources: 1. [Azure Resource Group](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/manage-resource-groups-cli): Logical container for all resources in the sample. 1. [Azure Resource Group](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/manage-resource-groups-cli): Logical container for all resources @@ -36,431 +38,10 @@ The Terraform modules deploy the following Azure resources: 4. [Azure App Service Plan](https://learn.microsoft.com/en-us/azure/app-service/overview-hosting-plans): The compute resource that hosts the web application. 5. [Azure Web App](https://learn.microsoft.com/en-us/azure/app-service/overview): Hosts the Python Flask single-page application (*Vacation Planner*), connected to Azure SQL Database. 6. [App Service Source Control](https://learn.microsoft.com/en-us/rest/api/appservice/web-apps/create-or-update-source-control?view=rest-appservice-2024-11-01): (Optional) Configures automatic deployment from a public GitHub repository. +7. [Azure Key Vault](https://learn.microsoft.com/en-us/azure/key-vault/general/overview): Stores the SQL connection string in a secret. The system implements a Vacation Planner web application that stores and retrieves activity data from Azure SQL Database. For more information, see [Azure Web App with Azure SQL Database](../README.md). -## Terraform Modules - -Below is a summary of the key Terraform modules included in this deployment: - -- **`main.tf`**: Defines all Azure resources and their configuration. -- **`variables.tf`**: Declares input variables and validation rules. -- **`outputs.tf`**: Specifies output values after deployment. -- **`providers.tf`**: Configures the Terraform provider for Azure. - -Below you can read the declarative code in HashiCorp Configuration Language (HCL): - -```terraform -# Local Variables -locals { - firewall_rule_name = "AllowAllIPs" - resource_group_name = "${var.prefix}-rg" - sql_server_name = "${var.prefix}-sqlserver-${var.suffix}" - app_service_plan_name = "${var.prefix}-app-service-plan-${var.suffix}" - web_app_name = "${var.prefix}-webapp-${var.suffix}" -} - -# Create a resource group -resource "azurerm_resource_group" "example" { - name = local.resource_group_name - location = var.location - tags = var.tags -} - -# Create a SQL server -resource "azurerm_mssql_server" "example" { - name = local.sql_server_name - resource_group_name = azurerm_resource_group.example.name - location = azurerm_resource_group.example.location - administrator_login = var.administrator_login - administrator_login_password = var.administrator_login_password - minimum_tls_version = var.minimum_tls_version - public_network_access_enabled = var.public_network_access_enabled - outbound_network_restriction_enabled = var.outbound_network_restriction_enabled - version = var.sql_version - tags = var.tags - - lifecycle { - ignore_changes = [ - tags - ] - } -} - -# Create a firewall rule -resource "azurerm_mssql_firewall_rule" "example" { - name = local.firewall_rule_name - server_id = azurerm_mssql_server.example.id - start_ip_address = var.start_ip_address - end_ip_address = var.end_ip_address -} - -# Create a database -resource "azurerm_mssql_database" "example" { - name = var.sql_database_name - server_id = azurerm_mssql_server.example.id - sku_name = var.sku.name - auto_pause_delay_in_minutes = var.auto_pause_delay - collation = var.collation - create_mode = var.create_mode - elastic_pool_id = var.elastic_pool_resource_id - max_size_gb = var.max_size_gb - min_capacity = var.min_capacity != "0" ? tonumber(var.min_capacity) : null - read_replica_count = var.high_availability_replica_count - read_scale = var.read_scale == "Enabled" ? true : false - zone_redundant = var.sql_database_zone_redundant - license_type = var.license_type - ledger_enabled = var.is_ledger_on - tags = var.tags - - lifecycle { - ignore_changes = [ - tags - ] - } -} - -# Create a service plan -resource "azurerm_service_plan" "example" { - 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 - tags = var.tags - - lifecycle { - ignore_changes = [ - tags - ] - } -} - -# Create a web app -resource "azurerm_linux_web_app" "example" { - name = local.web_app_name - resource_group_name = azurerm_resource_group.example.name - location = azurerm_resource_group.example.location - service_plan_id = azurerm_service_plan.example.id - https_only = var.https_only - public_network_access_enabled = var.webapp_public_network_access_enabled - client_affinity_enabled = false - tags = var.tags - - identity { - type = "SystemAssigned" - } - - site_config { - always_on = var.always_on - http2_enabled = var.http2_enabled - minimum_tls_version = var.minimum_tls_version - application_stack { - python_version = var.python_version - } - } - - app_settings = { - SCM_DO_BUILD_DURING_DEPLOYMENT = "true" - ENABLE_ORYX_BUILD = "true" - SQL_SERVER = azurerm_mssql_server.example.fully_qualified_domain_name - SQL_DATABASE = azurerm_mssql_database.example.name - SQL_USERNAME = var.sql_database_username - SQL_PASSWORD = var.sql_database_password - LOGIN_NAME = var.login_name - } - - 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 = "main" - use_manual_integration = true - use_mercurial = false -} -``` - -## Deployment Script - -You can use the `deploy.sh` script to automate the deployment of all Azure resources and the sample application in a single step, streamlining setup and reducing manual configuration. - -```bash -#!/bin/bash - -# Variables -PREFIX='local' -SUFFIX='test' -LOCATION='westeurope' -ADMIN_USER='sqladmin' -ADMIN_PASSWORD='P@ssw0rd1234!' -DATABASE_USER_NAME='testuser' -DATABASE_USER_PASSWORD='TestP@ssw0rd123' -CURRENT_DIR="$(cd "$(dirname "$0")" && pwd)" -ZIPFILE="planner_website.zip" -ENVIRONMENT=$(az account show --query environmentName --output tsv) -DEPLOY_APP=1 - -# Change the current directory to the script's directory -cd "$CURRENT_DIR" || exit - -# Run terraform init and apply -if [[ $ENVIRONMENT == "LocalStack" ]]; then - echo "Using tflocal and azlocal for LocalStack emulator environment and ." - TERRAFORM="tflocal" - AZ="azlocal" -else - echo "Using standard terraform and az for AzureCloud environment." - TERRAFORM="terraform" - AZ="az" -fi - -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 "administrator_login=$ADMIN_USER" \ - -var "administrator_login_password=$ADMIN_PASSWORD" \ - -var "sql_database_username=$DATABASE_USER_NAME" \ - -var "sql_database_password=$DATABASE_USER_PASSWORD" - -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) -SQL_SERVER_NAME=$(terraform output -raw sql_server_name) -SQL_DATABASE_NAME=$(terraform output -raw sql_database_name) - -if [[ -z "$WEB_APP_NAME" || -z "$SQL_SERVER_NAME" || -z "$SQL_DATABASE_NAME" ]]; then - echo "Web App Name, SQL Server Name, or SQL Database Name is empty. Exiting." - exit 1 -fi - -# Retrieve the fullyQualifiedDomainName of the SQL server -echo "Retrieving the fullyQualifiedDomainName of the [$SQL_SERVER_NAME] SQL server..." -SQL_SERVER_FQDN=$($AZ sql server show \ - --name "$SQL_SERVER_NAME" \ - --resource-group "$RESOURCE_GROUP_NAME" \ - --query "fullyQualifiedDomainName" \ - --output tsv) - -if [ -z "$SQL_SERVER_FQDN" ]; then - echo "Failed to retrieve the fullyQualifiedDomainName of the SQL server" - exit 1 -fi - -# Create server-level login -echo "Creating login [$DATABASE_USER_NAME] at server level..." -sqlcmd -S "$SQL_SERVER_FQDN" \ - -d master \ - -U "$ADMIN_USER" \ - -P "$ADMIN_PASSWORD" \ - -Q "IF NOT EXISTS (SELECT * FROM sys.sql_logins WHERE name = '$DATABASE_USER_NAME') - CREATE LOGIN [$DATABASE_USER_NAME] WITH PASSWORD = '$DATABASE_USER_PASSWORD';" \ - -V 1 - -if [ $? -eq 0 ]; then - echo "Login [$DATABASE_USER_NAME] created successfully" -else - echo "Failed to create login [$DATABASE_USER_NAME]" - exit 1 -fi - -# Create database user -echo "Creating user [$DATABASE_USER_NAME] in database [$SQL_DATABASE_NAME]..." -sqlcmd -S "$SQL_SERVER_FQDN" \ - -d "$SQL_DATABASE_NAME" \ - -U "$ADMIN_USER" \ - -P "$ADMIN_PASSWORD" \ - -Q "IF NOT EXISTS (SELECT * FROM sys.database_principals WHERE name = '$DATABASE_USER_NAME') - CREATE USER [$DATABASE_USER_NAME] FOR LOGIN [$DATABASE_USER_NAME];" \ - -V 1 - -if [ $? -eq 0 ]; then - echo "User [$DATABASE_USER_NAME] created successfully in database [$SQL_DATABASE_NAME]" -else - echo "Failed to create user [$DATABASE_USER_NAME]" - exit 1 -fi - -# Grant permissions including DDL rights -echo "Granting permissions to [$DATABASE_USER_NAME]..." -sqlcmd -S "$SQL_SERVER_FQDN" \ - -d "$SQL_DATABASE_NAME" \ - -U "$ADMIN_USER" \ - -P "$ADMIN_PASSWORD" \ - -Q "ALTER ROLE db_datareader ADD MEMBER [$DATABASE_USER_NAME]; - ALTER ROLE db_datawriter ADD MEMBER [$DATABASE_USER_NAME]; - ALTER ROLE db_ddladmin ADD MEMBER [$DATABASE_USER_NAME];" \ - -V 1 - -if [ $? -eq 0 ]; then - echo "Permissions granted successfully to [$DATABASE_USER_NAME]" -else - echo "Failed to grant permissions to [$DATABASE_USER_NAME]" - exit 1 -fi - -# Test connection -echo "Testing connection with user [$DATABASE_USER_NAME]..." -sqlcmd -S "$SQL_SERVER_FQDN" \ - -d "$SQL_DATABASE_NAME" \ - -U "$DATABASE_USER_NAME" \ - -P "$DATABASE_USER_PASSWORD" \ - -Q "SELECT SYSTEM_USER AS CurrentUser, DB_NAME() AS CurrentDatabase, GETDATE() AS CurrentTime;" \ - -V 1 - -if [ $? -eq 0 ]; then - echo "Connection test successful with user [$DATABASE_USER_NAME]" -else - echo "Connection test failed with user [$DATABASE_USER_NAME]" - exit 1 -fi - -# Create table -echo "Creating test [Products] table..." -sqlcmd -S "$SQL_SERVER_FQDN" \ - -d "$SQL_DATABASE_NAME" \ - -U "$DATABASE_USER_NAME" \ - -P "$DATABASE_USER_PASSWORD" \ - -Q "IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'Activities' AND schema_id = SCHEMA_ID('dbo')) - CREATE TABLE dbo.Activities ( - -- Primary Key: UNIQUEIDENTIFIER with a default of a new sequential GUID (best for indexing) - id UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWSEQUENTIALID(), - - -- Username field - username VARCHAR(32) NOT NULL, - - -- Description of the activity - activity VARCHAR(128) NOT NULL, - - -- Timestamp of the activity - timestamp DATETIME NOT NULL - );" \ - -V 1 - -if [ $? -eq 0 ]; then - echo "Test [Activities] table created successfully" -else - echo "Failed to create test [Activities] table" - exit 1 -fi - -# Insert data -echo "Inserting test data into [Activities] table..." -sqlcmd -S "$SQL_SERVER_FQDN" \ - -d "$SQL_DATABASE_NAME" \ - -U "$DATABASE_USER_NAME" \ - -P "$DATABASE_USER_PASSWORD" \ - -Q "INSERT INTO Activities (username, activity, timestamp) - VALUES - ('paolo', 'Go to Paris', GETDATE()), - ('paolo', 'Go to London', GETDATE()), - ('paolo', 'Go to Mexico', GETDATE());" \ - -V 1 - -if [ $? -eq 0 ]; then - echo "Test data inserted successfully into [Activities] table" -else - echo "Failed to insert test data into [Activities] table" - exit 1 -fi - -# Query data -echo "Querying test data from [Activities] table..." -sqlcmd -S "$SQL_SERVER_FQDN" \ - -d "$SQL_DATABASE_NAME" \ - -U "$DATABASE_USER_NAME" \ - -P "$DATABASE_USER_PASSWORD" \ - -Q "SELECT * FROM Activities;" \ - -V 1 - -if [ $? -eq 0 ]; then - echo "Test data queried successfully from [Activities] table" -else - echo "Failed to query test data from [Activities] table" - exit 1 -fi - -if [[ $DEPLOY_APP -eq 0 ]]; then - echo "Skipping web app deployment as DEPLOY_APP flag is set to 0." - exit 0 -fi - -# Change current directory to source folder -cd "../src" || exit - -# Remove any existing zip package of the web app -if [ -f "$ZIPFILE" ]; then - rm "$ZIPFILE" -fi - -# Create the zip package of the web app -echo "Creating zip package of the web app..." -zip -r "$ZIPFILE" app.py activities.py database.py static templates requirements.txt - -# Deploy the web app -echo "Deploying web app [$WEB_APP_NAME] with zip file [$ZIPFILE]..." -$AZ webapp deploy \ - --resource-group "$RESOURCE_GROUP_NAME" \ - --name "$WEB_APP_NAME" \ - --src-path "$ZIPFILE" \ - --type zip \ - --async true 1>/dev/null - -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 - -# Remove the zip package of the web app -# Remove the zip package of the web app -if [ -f "$ZIPFILE" ]; then - rm "$ZIPFILE" -fi -``` - -> **Note** -> You can use the `azlocal` CLI as a drop-in replacement for the `az` CLI to direct all commands to the LocalStack for Azure emulator. Alternatively, run `azlocal start_interception` to automatically intercept and redirect all `az` commands to LocalStack. Likewise, the `tflocal` is a local replacement for the standard `terraform` CLI, allowing you to run Terraform commands against LocalStack's Azure emulation environment. For more information, see [Get started with the az tool on LocalStack](https://azure.localstack.cloud/user-guides/sdks/az/). - -The `deploy.sh` 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. -- Extracts resource names and outputs from the Terraform deployment. -- Packages the code of the web application into a zip file for deployment. -- Deploys the zip package to the Azure Web App using the LocalStack Azure CLI. - ## Configuration Before deploying the Terraform modules, update the `terraform.tfvars` file with your specific values: diff --git a/samples/web-app-sql-database/python/visio/architecture.vsdx b/samples/web-app-sql-database/python/visio/architecture.vsdx index b572243..1543af5 100644 Binary files a/samples/web-app-sql-database/python/visio/architecture.vsdx and b/samples/web-app-sql-database/python/visio/architecture.vsdx differ