diff --git a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/Readme.md b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/Readme.md index a880f027e..7e53dec6f 100644 --- a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/Readme.md +++ b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/Readme.md @@ -60,10 +60,10 @@ After completing this Hack, you will be able to: * Build an assessment and business case for a datacenter transformation. * Plan and execute a right-sized Hyper-V VM migration to Azure. * Apply security practices to migrated workloads. -* Enable Azure Monitor Agent, DCR-based telemetry, VM insights, and actionable alerting. -* Investigate a deterministic IIS or Apache incident with Azure Copilot Observability Agent or a KQL fallback. -* Restore and verify service using evidence from both the VM and Azure Monitor. -* Inspect a selected IIS or Apache workload and manually replatform its static content to Windows App Service with ZIP deployment. +* Enable OpenTelemetry VM metrics, DCR-based Windows event collection, and service-specific alerting. +* Investigate deterministic IIS and Apache incidents with Azure Copilot Observability Agent or the manual/KQL fallback. +* Restore and verify both services using evidence from the VMs and Azure Monitor. +* Select an IIS or Apache workload and manually replatform its static content to the appropriate Windows or Linux App Service target with ZIP deployment. * Explain why Azure Storage static website hosting or Azure Static Web Apps can be a better production target for a static workload. @@ -79,7 +79,7 @@ Complete these prerequisites before the session: * A Microsoft Entra user with Contributor or Owner permissions on the Azure subscription. * A client network and browser that support the Azure portal, Azure Bastion, and Azure Cloud Shell. -Challenge 7 includes a preflight for Azure Copilot tenant access, RBAC, network access, and supported regions. Azure Copilot is not mandatory because the challenge includes a full KQL/VM insights fallback. No Azure OpenAI resource, model deployment, or model quota is required anywhere in this Hack. +Challenge 7 assumes Hack participants have Azure Copilot access, suitable RBAC, network access, and a supported region. It also includes a concise manual/KQL fallback if agent output is unavailable. No Azure OpenAI resource, model deployment, or model quota is required anywhere in this Hack. ## Challenges @@ -89,8 +89,8 @@ Challenge 7 includes a preflight for Azure Copilot tenant access, RBAC, network * [Challenge 4 - Assess VMs for the migration](challenges/challenge-04.md) * [Challenge 5 - Migrate Hyper-V virtual machines to Azure](challenges/challenge-05.md) * [Optional Challenge 6 - Secure on Azure](challenges/challenge-06.md) -* [Challenge 7 - Operate the migrated workload with intelligent observability](challenges/challenge-07.md) -* [Challenge 8 - Replatform the selected migrated web workload to Azure App Service](challenges/challenge-08.md) +* [Challenge 7 - Operate the migrated workloads with intelligent observability](challenges/challenge-07.md) +* [Challenge 8 - Replatform a migrated web workload to Azure App Service](challenges/challenge-08.md) ## Solutions - Spoiler warning @@ -100,8 +100,8 @@ Challenge 7 includes a preflight for Azure Copilot tenant access, RBAC, network * [Solution 4 - Assess VMs for the migration](./walkthrough/challenge-04/solution-04.md) * [Solution 5 - Migrate Hyper-V virtual machines to Azure](./walkthrough/challenge-05/solution-05.md) * [Optional Solution 6 - Secure on Azure](./walkthrough/challenge-06/solution-06.md) -* [Solution 7 - Operate the migrated workload with intelligent observability](./walkthrough/challenge-07/solution-07.md) -* [Solution 8 - Replatform the selected migrated web workload to Azure App Service](./walkthrough/challenge-08/solution-08.md) +* [Solution 7 - Operate the migrated workloads with intelligent observability](./walkthrough/challenge-07/solution-07.md) +* [Solution 8 - Replatform a migrated web workload to Azure App Service](./walkthrough/challenge-08/solution-08.md) ## Contributors diff --git a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/challenges/challenge-05.md b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/challenges/challenge-05.md index 74aa66423..7e04c3c28 100644 --- a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/challenges/challenge-05.md +++ b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/challenges/challenge-05.md @@ -15,16 +15,17 @@ The goal of this exercise is to ... * Configure and register the Hyper-V replication provider and Recovery Services agent on the Hyper-V host. * Select the Windows and Ubuntu VMs and enable replication. * Review the compute, network, and disk target settings. -* Run and clean up test migrations. -* Prepare destination load balancing and cutover. +* Run, validate, and clean up test migrations for both VMs. * Perform the final migration with a planned shutdown. -* Validate the migrated workloads and complete the migration to stop replication. +* Directly validate both migrated web workloads in Azure. +* Complete both migrations to stop replication. ## Success criteria -* The Windows and Ubuntu web VMs are successfully migrated to and running in Azure. -* Both web workloads are accessible through the dedicated public load balancer. -* Test-migration resources are cleaned up, and the final migrations are completed. +* The Windows/IIS and Ubuntu/Apache VMs are successfully replicated and test-migrated. +* Both test-migrated web workloads return HTTP success with the correct hostname, platform, and web-server details. +* Test-migration resources are cleaned up. +* Both web VMs are successfully migrated, directly validated in Azure, and completed in Azure Migrate. ## Learning resources * [Migrate Hyper-V VMs to Azure](https://learn.microsoft.com/en-us/azure/migrate/tutorial-migrate-hyper-v?view=migrate) diff --git a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/challenges/challenge-07.md b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/challenges/challenge-07.md index c4fb41f56..1910ae2a5 100644 --- a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/challenges/challenge-07.md +++ b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/challenges/challenge-07.md @@ -1,89 +1,87 @@ -# Challenge 7 - Operate the migrated workload with intelligent observability +# Challenge 7 - Operate the migrated workloads with intelligent observability [Previous Challenge](challenge-06.md) - **[Home](../Readme.md)** - [Next Challenge](challenge-08.md) -Duration: 40 minutes +Duration: 70 minutes ## Goal -Operate one migrated web workload by combining Azure Monitor telemetry, an actionable alert, and an AI-assisted investigation. You will create a safe service-level incident, determine its scope, restore service, and prove recovery. +Operate both migrated web workloads through a closed-loop incident exercise. Enable modern OpenTelemetry monitoring, create an alert from the authoritative signal for each workload, induce controlled service failures, investigate the fired alerts with Azure Copilot Observability Agent, restore service under human control, and prove recovery. -## Select one track +## Workloads and authoritative signals -At the start of this challenge, select one track. Follow the same VM and track through Challenge 8. Teams do not need to complete both. +| Workload | Service | Authoritative alert signal | +| --- | --- | --- | +| Windows Server / IIS | `W3SVC` | Windows System event ID 7036 in Log Analytics | +| Ubuntu Linux / Apache | `apache2` | Absence of all matching `process.uptime` OpenTelemetry series | -| Track | Migrated workload | Service | Incident telemetry | -| --- | --- | --- | --- | -| A | Windows Server / IIS | `W3SVC` | Windows System event ID 7036 in `Event` | -| B | Ubuntu Linux / Apache | `apache2` | Tagged `daemon.warning` record in `Syslog` | +The Linux alert monitors every process whose `"process.executable.name"` is `apache2`. It becomes true only when all matching Apache worker and parent process series are absent for the configured window. The Windows alert intentionally uses the Service Control Manager event instead of `w3wp.exe`; IIS application-pool idle shutdown can stop a worker process while the web service remains healthy. -## Prerequisites and preflight +## Prerequisites and access assumptions -* Challenge 5 is complete and the selected migrated web VM in `destination-rg` is running. -* The selected IIS or Apache site responds locally and through its existing endpoint. -* You can create or reuse a Log Analytics workspace, install VM extensions, create data collection rules (DCRs), create alert rules, and create or reuse an action group. -* Use a Log Analytics workspace in the same region as the selected VM for this lab. -* Confirm that the workspace region is listed in the current [Azure Copilot Observability Agent regions](https://learn.microsoft.com/en-us/azure/azure-monitor/aiops/observability-agent-overview#regions). -* In the Azure portal, confirm that you can open Azure Copilot and that **Observability Agent** appears on the Log Analytics workspace **Logs** page. -* If tenant access is restricted, an administrator must grant the **Copilot for Azure User** role. Your account must also be able to read the VM and its monitoring data; **Monitoring Reader** and **Log Analytics Reader** are suitable least-privilege starting points. -* The client network must allow WebSocket connections to `https://directline.botframework.com`. +* Challenge 5 is complete and both migrated web VMs in `destination-rg` are running. +* You can connect to both VMs through Azure Bastion and each site responds locally. +* Before opening **Infrastructure monitoring**, you can create or reuse one user-assigned managed identity for both migrated VMs. Using the destination resource group and the same region is recommended for this lab. +* You can grant that identity **Monitoring Reader** on both monitored VMs or their containing destination resource group, and on the Azure Monitor workspace when the alert uses workspace scope. +* This user-assigned identity authorizes OpenTelemetry query-based and recommended alerts. It is separate from the VM's system-assigned identity used by Azure Monitor Agent; the portal doesn't automatically create the user-assigned alert identity. +* Azure Copilot access, a supported region, and the required telemetry permissions are available to Hack participants. No Azure OpenAI resource, model deployment, or model quota is required. +* The client network allows WebSocket connections to `https://directline.botframework.com`. > [!IMPORTANT] -> Azure Copilot availability is tenant-, user-, region-, and policy-dependent. No Azure OpenAI resource, model deployment, or model quota is required. If the preflight fails, complete the clearly labeled KQL and VM insights fallback. The monitoring, incident, remediation, and recovery outcomes remain the same. +> Azure Copilot Observability Agent, deep investigations, Azure Monitor issues, and query-based metric alerts include preview experiences whose portal labels and availability can vary by region and tenant. A deep investigation can consume Azure Agent Credits. Follow your instructor's lab access and cost guidance. > [!NOTE] -> On-demand chat and investigation do not require an Azure Copilot Observability Agent resource. Creating that preview resource is outside the required path. +> On-demand chat and deep investigations use the signed-in user's identity and require no `Microsoft.Monitor/observabilityAgents` resource. Do not provision an autonomous Observability Agent resource for this VM-only Hack. The portal preview scopes that resource to Application Insights, and virtual-machine scope isn't a supported new configuration. -## Actions - -### Common actions - -* Create or reuse a Log Analytics workspace. -* Enable enhanced monitoring/VM insights on the selected VM with Azure Monitor Agent. -* Verify heartbeat and performance telemetry. -* Create or reuse an action group and create an actionable log search alert. -* Use Azure Copilot Observability Agent chat, or the KQL/VM insights fallback, to distinguish a service outage from a VM outage. -* Restore the selected service and verify local, external, and telemetry recovery. -* Decide which lab monitoring resources should remain after the Hack. +## Monitoring models -### Track A - Windows Server / IIS +| | OpenTelemetry metrics-based | Classic logs-based | +| --- | --- | --- | +| Store | Azure Monitor workspace | Log Analytics workspace | +| Query language | PromQL | KQL | +| Typical latency | Near real-time | 1-3 minutes | +| Cost model | Default metrics are free; added metrics can incur cost | Log ingestion and retention charges | +| Strength | Consistent Windows/Linux schema and low-latency metrics | Multi-VM views and same-workspace metric/log correlation | +| Limitation | Separate queries are required to correlate with logs; built-in multi-VM views are limited | Platform-specific counters and higher ingestion cost | -* Associate a Windows event DCR that collects System event ID 7036. -* Alert on `W3SVC` stopping, safely stop IIS, and prove the local outage. -* Investigate `Event`, `Heartbeat`, and `InsightsMetrics`, then restore `W3SVC`. +Enable metrics-based OpenTelemetry monitoring for both VMs. Use classic log collection only on Windows where the authoritative `W3SVC` service event must be delivered to Log Analytics. -### Track B - Ubuntu Linux / Apache - -* Associate a Linux Syslog DCR that collects the `daemon` facility at `Warning` or higher. -* Stop `apache2` and emit an explicit tagged Syslog event that records the controlled lab action. -* Alert on the exact tag/message, investigate `Syslog`, `Heartbeat`, and `InsightsMetrics`, then restore `apache2`. +## Actions -> [!NOTE] -> The explicit Linux Syslog event makes lab ingestion deterministic without relying only on distribution-dependent systemd journal forwarding. +* Warm and validate both web pages before changing either service. +* Create one reusable user-assigned alert identity before opening Infrastructure monitoring, grant its required **Monitoring Reader** access, and select that existing identity in the portal's **Identity** field for both VMs. +* Let Infrastructure monitoring select or create the regional default Azure Monitor workspace and generated OpenTelemetry DCR; do not pre-create a custom workspace. +* Enable OpenTelemetry metrics, per-process metrics, built-in Grafana dashboards, and recommended infrastructure alerts. +* Confirm query-based and recommended alerts use the pre-created identity and that it retains the required monitoring read access. +* Locate the Linux VM's generated `MSVMOtel--` DCR and ensure that `process.uptime` is collected. +* Reuse the portal-selected compatible Log Analytics workspace for Windows events and associate a focused Event DCR. +* Create an Apache PromQL absence alert and an IIS KQL scheduled-query alert. +* Stop Apache and IIS in a controlled manner, prove local HTTP failure, and wait for both alerts. +* Start a deep investigation from each fired alert, evaluate evidence and ruled-out hypotheses, and save useful context as an Azure Monitor issue when available. +* Restore both services manually, validate both pages and telemetry recovery, confirm alert resolution, and request a post-incident summary and runbook improvements. ## Success criteria -* `AzureMonitorWindowsAgent` or `AzureMonitorLinuxAgent` is healthy and the VM has the intended DCR associations. -* `Heartbeat`, `InsightsMetrics`, and the selected track's `Event` or `Syslog` record are visible in the Log Analytics workspace. -* An enabled alert rule monitors the selected service-stop event and has an action group. -* The incident is proven by local HTTP failure, an inactive service, and the selected track's ingested event. -* The investigation shows whether the VM remained online and produces a safe service-level remediation. -* `W3SVC` or `apache2` is running again and the site responds successfully. - -> [!NOTE] -> Log ingestion and alert evaluation are asynchronous. The walkthrough uses the ingested event query, not a notification arrival time, as the deterministic success gate. Alert notification delivery is still reviewed as an operational outcome. - -## Optional stretch - autonomous operations (preview) - -Only if your instructor has prepared the required resources and permissions, explore an [Azure Copilot Observability Agent resource](https://learn.microsoft.com/en-us/azure/azure-monitor/aiops/observability-agent-resource) for autonomous alert correlation, issue creation, and custom instructions. The resource, autonomous operations, Azure Monitor issues, and custom-instruction experience are preview capabilities. They are not required for this challenge. +* Both VMs send default OpenTelemetry guest metrics to the portal-selected Azure Monitor workspace through their intended generated DCR associations. +* Linux `process.uptime` metrics include one or more `apache2` process series before the incident. +* Windows System event ID 7036 records for `W3SVC` are visible in the portal-selected Log Analytics workspace. +* The Apache PromQL alert and IIS KQL alert are enabled, use the intended signals, and fire after the controlled failures. +* An inactive service and local HTTP failure prove user-visible impact on each VM; process or service telemetry alone isn't treated as a synthetic availability test. +* The AI-assisted or manual investigation distinguishes service failure from VM/platform failure and produces evidence-backed remediation with rollback and prevention guidance. +* A human restores `W3SVC` and `apache2`; both sites return HTTP `200`, service/process telemetry returns, and alerts resolve. +* Both recovered workloads are available for the path selection at the start of Challenge 8. ## Learning resources -* [Azure Copilot Observability Agent overview](https://learn.microsoft.com/en-us/azure/azure-monitor/aiops/observability-agent-overview) -* [Chat with observability data](https://learn.microsoft.com/en-us/azure/azure-monitor/aiops/observability-agent-chat) -* [Manage access to Azure Copilot](https://learn.microsoft.com/en-us/azure/copilot/manage-access) -* [Enable VM monitoring in Azure Monitor](https://learn.microsoft.com/en-us/azure/azure-monitor/vm/vm-enable-monitoring) -* [Install and manage Azure Monitor Agent](https://learn.microsoft.com/en-us/azure/azure-monitor/agents/azure-monitor-agent-manage) -* [Collect Windows events with Azure Monitor Agent](https://learn.microsoft.com/en-us/azure/azure-monitor/vm/data-collection-windows-events) -* [Collect Syslog events with Azure Monitor Agent](https://learn.microsoft.com/en-us/azure/azure-monitor/vm/data-collection-syslog) -* [Create Azure Monitor log search alert rules](https://learn.microsoft.com/en-us/azure/azure-monitor/alerts/alerts-create-log-alert-rule) +* [Metrics experience for virtual machines](https://learn.microsoft.com/azure/azure-monitor/vm/metrics-opentelemetry-guest) +* [Enable enhanced monitoring for an Azure VM](https://learn.microsoft.com/azure/azure-monitor/vm/tutorial-enable-monitoring) +* [Customize OpenTelemetry metrics for VMs](https://learn.microsoft.com/azure/azure-monitor/vm/metrics-opentelemetry-guest-modify) +* [OpenTelemetry guest OS metrics reference](https://learn.microsoft.com/azure/azure-monitor/vm/metrics-guest-reference) +* [PromQL for system and guest OS metrics](https://learn.microsoft.com/azure/azure-monitor/metrics/prometheus-system-metrics-best-practices) +* [Query-based metric alerts](https://learn.microsoft.com/azure/azure-monitor/alerts/alerts-query-based-metric-alerts-overview) +* [Create a user-assigned managed identity in the Azure portal](https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/manage-user-assigned-managed-identities-azure-portal) +* [Assign Azure roles using the Azure portal](https://learn.microsoft.com/azure/role-based-access-control/role-assignments-portal) +* [Collect Windows events with Azure Monitor Agent](https://learn.microsoft.com/azure/azure-monitor/vm/data-collection-windows-events) +* [Create Azure Monitor log search alert rules](https://learn.microsoft.com/azure/azure-monitor/alerts/alerts-create-log-alert-rule) +* [Azure Copilot Observability Agent overview](https://learn.microsoft.com/azure/azure-monitor/aiops/observability-agent-overview) +* [Deep investigations with Observability Agent](https://learn.microsoft.com/azure/azure-monitor/aiops/observability-agent-deep-investigations) diff --git a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/challenges/challenge-08.md b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/challenges/challenge-08.md index 26bd75ff4..67d992c04 100644 --- a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/challenges/challenge-08.md +++ b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/challenges/challenge-08.md @@ -1,70 +1,74 @@ -# Challenge 8 - Replatform the selected migrated web workload to Azure App Service +# Challenge 8 - Replatform a migrated web workload to Azure App Service [Previous Challenge](challenge-07.md) - **[Home](../Readme.md)** - [Finish](finish.md) -Duration: 40 minutes +Duration: 45 minutes ## Goal -Replatform the web workload selected in Challenge 7 from an IaaS VM to a managed web app in Azure App Service. First discover what the site actually needs, then package and deploy it without a source repository integration. +Select either the migrated Windows/IIS workload or the Ubuntu/Apache workload for a guided manual replatform to Azure App Service. Inspect what the site needs, package its portable static content, deploy it to the appropriate managed App Service platform, and prove that the new endpoint operates independently of the source VM. -Continue with the same track: +## Select a path -* **Track A:** Windows Server / IIS -* **Track B:** Ubuntu Linux / Apache +| Path | Source | Guided target | +| --- | --- | --- | +| A | Windows Server / IIS | Windows App Service | +| B | Ubuntu Linux / Apache | Linux App Service with a supported Node.js LTS runtime | -Teams complete only their selected track. +Complete one path during the standard challenge time. If time permits, you may complete both. Use a distinct App Service plan and a distinct globally unique web-app name for each path because Windows and Linux apps require plans for their respective operating systems. > [!IMPORTANT] -> Azure Migrate's agentless at-scale web-app migration supports ASP.NET applications on Windows IIS servers hosted in VMware environments. It does not support direct web-app migration from this Hack's Hyper-V source scenario or this Linux/Apache workload. This challenge therefore performs a deliberate manual replatform from the already migrated Azure VM. +> Azure Migrate's agentless at-scale web-app migration supports ASP.NET applications on Windows IIS servers hosted in VMware environments. It doesn't directly modernize this Hack's Hyper-V-hosted IIS site or its Linux/Apache site. This challenge therefore performs a deliberate manual replatform from an already migrated Azure VM. ## Prerequisites -* Challenge 7 is complete and the selected `W3SVC` or `apache2` service has been restored. -* You can connect to the selected migrated VM by using Azure Bastion. -* You can create an App Service plan and web app in `destination-rg`. +* Challenge 7 is complete and both `W3SVC` and `apache2` have been restored. +* You can connect to the source VM for your chosen path through Azure Bastion. +* You know the Hack subscription ID and can create App Service resources in it. +* You know the exact destination resource group. The Bicep deployment names it `MHBox--destination-rg`, where `` is the deployer's user principal name before `@`. * The selected site content is available at `C:\inetpub\wwwroot` or `/var/www/html`. -* Track A has an interactive browser session available for Kudu ZIP upload, or can use the documented CLI alternative. -* Track B can make an interactive device-code sign-in from the migrated Linux VM and has permission to deploy the App Service resources. +* Path A can use Azure Cloud Shell and an interactive browser session for Kudu ZIP upload, or the documented CLI alternative. +* Path B can make an interactive device-code sign-in from the migrated Linux VM and has permission to deploy App Service resources. * No GitHub repository, repository connection, Azure OpenAI deployment, or model quota is required. ## Actions ### Common actions -* Inspect the site's content, bindings or virtual hosts, runtime/modules, dependencies, and state requirements. -* Confirm the workload consists of portable static HTML, CSS, and image assets. -* Replace the VM-specific hostname, platform, and web-server values in `index.html` with App Service values by using the page's stable metadata attributes. +* Select Path A or Path B and inspect the site's content, bindings or virtual host, runtime/modules, dependencies, and state requirements. +* Confirm that the site is portable static HTML, CSS, and image content. No additional Azure Migrate portal action is required at this stage. +* Replace the VM-specific hostname, platform, and web-server values in the package by using the page's stable metadata attributes. * Create a ZIP package whose root contains `index.html`. -* Create a low-cost Windows App Service plan and web app in `destination-rg`. -* Deploy with the supported App Service ZIP deployment experience. -* Enforce HTTPS and validate the home page and referenced assets. -* Stop the original web service and prove that the App Service endpoint remains available. +* Set and display the intended Azure subscription, tenant, and user before any resource operation. Stop if they don't identify the Hack subscription. +* Replace the destination resource-group placeholder with the exact `MHBox--destination-rg` name and verify that it exists. +* Create a low-cost App Service plan for the selected operating system and a globally unique web app. +* Deploy with supported App Service ZIP deployment, enforce HTTPS, and validate the page and assets. +* Stop the source web service, prove that App Service remains available, and restore the source service. * Compare App Service with Azure Storage static website hosting and Azure Static Web Apps for a production architecture decision. -### Track A - Windows Server / IIS +### Path A - Windows Server / IIS * Inventory IIS bindings, the application pool, runtime settings, and `C:\inetpub\wwwroot`. * Create the package with PowerShell `Compress-Archive`. -* Deploy with the Kudu ZIP deploy UI or the documented Azure CLI alternative. +* Create a Windows App Service plan and deploy through Kudu ZIP deploy or the documented Azure CLI alternative. -### Track B - Ubuntu Linux / Apache +### Path B - Ubuntu Linux / Apache * Inventory Apache virtual hosts, modules, configuration, and `/var/www/html`. * Stage a user-owned copy, make it portable, and create and verify the ZIP with Linux tools. -* Install Azure CLI from Microsoft's Debian/Ubuntu repository only if it is missing, use interactive device-code sign-in, verify the subscription, deploy the local ZIP with `az webapp deploy --type zip`, and sign out. - -> [!NOTE] -> Both tracks deploy the static artifact to the same pedagogical Windows App Service target. The source VM operating system does not dictate the App Service worker operating system when the artifact is portable static content. +* Use device-code authentication, deterministically select the newest advertised Node.js LTS runtime, create a Linux App Service plan, and configure PM2 to serve the static ZIP. +* Sign out of Azure CLI after deployment. ## Success criteria -* The discovery record shows that the selected site has no required server-side runtime, database, machine-local dependency, or session state. +* The selected site's inspection shows no required server-side runtime, database, machine-local dependency, or session state. +* The active subscription, tenant, user, and exact destination resource group are displayed and verified before deployment. * The ZIP root contains `index.html` rather than a nested content directory. -* A Windows App Service web app serves the selected site's page and assets over HTTPS. +* The chosen Windows or Linux App Service app serves the selected site's page and assets over HTTPS. +* A Path B app uses a runtime returned by `az webapp list-runtimes --os-type linux` and a PM2 static-content startup command. * The rendered page identifies the hostname as Azure App Service, the platform as managed PaaS, and the web server as Azure App Service. -* The App Service endpoint remains healthy after `W3SVC` or `apache2` is stopped. -* Track B signs out of Azure CLI after deployment. +* The App Service endpoint remains healthy while the selected source service is stopped, and the source service is restored afterward. +* Path B signs out of Azure CLI after deployment. * No Azure Load Balancer is placed in front of App Service and no GitHub integration is required. ## Architecture decision @@ -76,7 +80,8 @@ App Service is the pedagogical target because it demonstrates replatforming to a * [Azure Migrate web-app migration support matrix](https://learn.microsoft.com/en-us/azure/migrate/concepts-migration-webapps?view=migrate) * [Deploy files to Azure App Service](https://learn.microsoft.com/en-us/azure/app-service/deploy-zip) * [Azure CLI App Service plan reference](https://learn.microsoft.com/en-us/cli/azure/appservice/plan?view=azure-cli-latest#az-appservice-plan-create) -* [Azure CLI web app deployment reference](https://learn.microsoft.com/en-us/cli/azure/webapp?view=azure-cli-latest#az-webapp-deploy) +* [Azure CLI web app reference](https://learn.microsoft.com/en-us/cli/azure/webapp?view=azure-cli-latest) +* [Configure Node.js in App Service](https://learn.microsoft.com/en-us/azure/app-service/configure-language-nodejs) * [Install Azure CLI on Ubuntu or Debian](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli-linux?view=azure-cli-latest&pivots=apt) * [Sign in interactively with Azure CLI](https://learn.microsoft.com/en-us/cli/azure/authenticate-azure-cli-interactively?view=azure-cli-latest) * [Enforce HTTPS in Azure App Service](https://learn.microsoft.com/en-us/azure/app-service/configure-ssl-bindings#enforce-https) diff --git a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/challenges/finish.md b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/challenges/finish.md index 717880f72..c1fe94526 100644 --- a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/challenges/finish.md +++ b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/challenges/finish.md @@ -10,7 +10,7 @@ You finished *Migrate and Secure to be ready for AI Ops*. You followed the complete storyline: discover, assess, plan, migrate, secure, operate intelligently, and replatform. The final target includes an independently operating App Service web app, while Azure Monitor provides telemetry and alerting for the migrated VM estate. -The final architecture deliberately does not place Azure Load Balancer in front of App Service. The load balancer remains an IaaS publishing component only. For the static demo workload, Azure Storage static website hosting or Azure Static Web Apps should also be evaluated before choosing a production target. +The final architecture keeps App Service independent of the migrated VM networking. For the static demo workload, Azure Storage static website hosting or Azure Static Web Apps should also be evaluated before choosing a production target. Cleanup is optional and must be approved by the lab owner. Remember that stopping a VM or web app does not necessarily stop all charges; delete only the resources or resource groups that are not shared or needed for later work. diff --git a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-05/img/Challenge-5.jpg b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-05/img/Challenge-5.jpg deleted file mode 100644 index e15cda9ab..000000000 Binary files a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-05/img/Challenge-5.jpg and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-05/img/LB1.png b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-05/img/LB1.png deleted file mode 100644 index 1f3f34aeb..000000000 Binary files a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-05/img/LB1.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-05/img/LB2.png b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-05/img/LB2.png deleted file mode 100644 index fb8648f02..000000000 Binary files a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-05/img/LB2.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-05/img/LB3.png b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-05/img/LB3.png deleted file mode 100644 index 4251cce0a..000000000 Binary files a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-05/img/LB3.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-05/img/LB4.png b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-05/img/LB4.png deleted file mode 100644 index 8a2251eb4..000000000 Binary files a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-05/img/LB4.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-05/img/LB5.png b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-05/img/LB5.png deleted file mode 100644 index 0f52e6140..000000000 Binary files a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-05/img/LB5.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-05/img/LB6.png b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-05/img/LB6.png deleted file mode 100644 index bb644727c..000000000 Binary files a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-05/img/LB6.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-05/img/LB7.png b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-05/img/LB7.png deleted file mode 100644 index 29d8beb98..000000000 Binary files a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-05/img/LB7.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-05/img/TestMig6.png b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-05/img/TestMig6.png index 55e399fcc..63d633efe 100644 Binary files a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-05/img/TestMig6.png and b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-05/img/TestMig6.png differ diff --git a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-05/img/mig9.png b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-05/img/mig9.png deleted file mode 100644 index 6fab5bb84..000000000 Binary files a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-05/img/mig9.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-05/solution-05.md b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-05/solution-05.md index 3df186648..c508e82b2 100644 --- a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-05/solution-05.md +++ b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-05/solution-05.md @@ -152,7 +152,16 @@ Connect to each test VM via Azure Bastion. ![image](./img/TestMig5.png) -On the Windows VM, open a browser and navigate to *http://localhost*. Confirm that the dashboard shows the VM hostname, `Windows Server 2022`, and `IIS`. On the Ubuntu VM, run `curl -I http://localhost`, then open the page and confirm that it shows the VM hostname, `Ubuntu Linux`, and `Apache`. Both requests must return HTTP success. +On the Windows VM, open a browser and navigate to *http://localhost*. Confirm that the dashboard shows the VM hostname, `Windows Server 2022`, and `IIS`. + +On the Ubuntu VM, run the following commands: + +```bash +curl --fail --silent --show-error --head http://localhost +curl --fail --silent --show-error http://localhost | grep -E 'mhbox-ubuntu-01|Ubuntu Linux|Apache' +``` + +The first command must return HTTP success. The second command must show the Ubuntu hostname, `Ubuntu Linux`, and `Apache`. ![image](./img/TestMig6.png) @@ -173,47 +182,9 @@ Provide a comment, select *Testing is complete....*, and click *Cleanup Test* to > [!NOTE] > **Repeat the preceding steps for the other VM.** -### **Task 4: Prepare the final migration** - -Currently, the two servers are not published or directly accessible. After migration, the source servers will be turned off, so access to the systems must be updated. Perform the following premigration steps to keep downtime as short as possible. - -#### **Task 4.1: Create a new Azure public load balancer in the destination environment** - -From the Azure Portal, open the Load Balancing page, select *Load Balancer* from the navigation pane on the left, and click *Create*. - -![image](./img/LB1.png) - -Under *Basics*, select the *destination-rg* resource group and provide a name for the new load balancer. - -![image](./img/LB2.png) - -Under *Frontend IP configuration*, click *Add a frontend IP configuration* and create a new public IP address. - -![image](./img/LB3.png) - -Under *Backend Pools*, select *Add a backend Pool*. Provide a name and select *destination-vnet* as the virtual network. -Add *10.2.1.4* and *10.2.1.5* as the IP addresses. - -> [!NOTE] -> Azure reserves the first four addresses (0-3) in each subnet address range and doesn't assign them. Azure assigns the next available address to a resource from the subnet address range. Therefore, the IP addresses assigned to the destination VMs after migration are predictable in this lab. - -![image](./img/LB4.png) +### **Task 4: Perform the final migration** -Under *Inbound rules*, click *Add a load balancing rule* and create the rule as illustrated in the following diagram. - -![image](./img/LB5.png) - -We are already using a NAT gateway to provide outbound internet access. We don't need an outbound rule and can skip this part. - -Proceed to the *Review + create* section, review your configuration, and click *Create*. - -![image](./img/LB6.png) - -After the load balancer has been created, return to the *Load balancing* section and select the load balancer. From the *Overview* pane, copy the *Frontend IP address*. Record the load balancer's public IP address because you will need it after migration. - -![image](./img/LB7.png) - -### **Task 5: Perform the final migration** +After validating and cleaning up both test migrations, proceed with the planned cutover. The source VMs will be shut down during migration to keep the final replicated state consistent. Open the [Azure Portal](https://portal.azure.com), return to the *Migrations* section in the Azure Migrate project, and click *Action pending* for the VMs to be migrated. @@ -248,27 +219,37 @@ In the *Virtual machines* section of the Azure Portal, you should now see two ad ![image](./img/mig8.png) -You should now also be able to access the web server via the previously created load balancer frontend IP. +Connect to both migrated VMs through Azure Bastion and validate each workload directly. + +On **MHBox-Win2K22**, open a browser and navigate to *http://localhost*. Confirm that the dashboard returns HTTP success, its hostname matches the migrated Windows VM, and it shows `Windows Server 2022` and `IIS`. -![image](./img/mig9.png) +On **MHBox-Ubuntu-01**, run the following commands: -🚀🚀🚀🚀🚀🚀 Congratulations, you've successfully migrated the frontend application to Azure. 🚀🚀🚀🚀🚀🚀 +```bash +curl --fail --silent --show-error --head http://localhost +curl --fail --silent --show-error http://localhost | grep -E 'mhbox-ubuntu-01|Ubuntu Linux|Apache' +``` -### **Task 6: Complete the migration** +The first command must return HTTP success. The second command must show the Ubuntu hostname, `Ubuntu Linux`, and `Apache`. Validating the VMs separately confirms that both migrated workloads are healthy. -After validating both migrated web VMs and the load-balanced endpoint, complete the migration to stop replication and clean up each VM's replication state. Open the [Azure Portal](https://portal.azure.com), navigate to the Azure Migrate project, select *Migrations*, and open each migrated VM from the *Completion* stage. +🚀🚀🚀🚀🚀🚀 Congratulations, you've successfully migrated both web workloads to Azure. 🚀🚀🚀🚀🚀🚀 + +### **Task 5: Complete the migration** + +After validating both migrated web VMs, complete the migration to stop replication and clean up each VM's replication state. Open the [Azure Portal](https://portal.azure.com), navigate to the Azure Migrate project, select *Migrations*, and open each migrated VM from the *Completion* stage. ![image](./img/Clean1.png) Under *Completion*, select *Complete migration*. Repeat this action for both **MHBox-Win2K22** and **MHBox-Ubuntu-01**. > [!NOTE] -> Completing migration stops replication for the source VM and removes its replication-state information. Confirm that the migrated Azure VM is healthy before completing this action. +> In some Azure Migrate portal views, the equivalent action is labeled *Stop replication*. -You successfully completed Challenge 5. +![image](./img/Clean2.png) -The deployed architecture now looks like the following diagram. +> [!NOTE] +> Completing migration stops replication for the source VM and removes its replication-state information. Confirm that the migrated Azure VM is healthy before completing this action. -![image](./img/Challenge-5.jpg) +You successfully completed Challenge 5. Continue to Challenge 6 to secure the migrated environment. Do not remove `destination-rg` because Challenges 6 through 8 use the migrated workload. diff --git a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-06/solution-06.md b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-06/solution-06.md index fded2b1bc..b434e6f48 100644 --- a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-06/solution-06.md +++ b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-06/solution-06.md @@ -96,13 +96,6 @@ In this Hack, we deployed virtual machines that use public IP addresses and are ![image](./img/secpost04.png) -## **Task 5: Enable and configure Copilot for Security** - -Sign in to Copilot for Security (https://securitycopilot.microsoft.com). - - - - You successfully completed Challenge 6. -Continue to Challenge 7, select the migrated IIS or Apache workload, and configure intelligent observability. Do not remove `destination-rg`. +Continue to Challenge 7 and configure intelligent observability for both migrated web workloads. Do not remove `destination-rg`. diff --git a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-07/img/configure-infrastructure-monitoring.png b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-07/img/configure-infrastructure-monitoring.png new file mode 100644 index 000000000..05c783762 Binary files /dev/null and b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-07/img/configure-infrastructure-monitoring.png differ diff --git a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-07/solution-07.md b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-07/solution-07.md index fd255faa7..782a48316 100644 --- a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-07/solution-07.md +++ b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-07/solution-07.md @@ -1,154 +1,184 @@ -# Walkthrough Challenge 7 - Operate the migrated workload with intelligent observability +# Walkthrough Challenge 7 - Operate the migrated workloads with intelligent observability [Previous Challenge Solution](../challenge-06/solution-06.md) - **[Home](../../Readme.md)** - [Next Challenge Solution](../challenge-08/solution-08.md) -Duration: 40 minutes +Duration: 70 minutes -## Task 1: Select a track and complete the preflight (5 minutes) +## Task 1: Validate both workloads and complete preflight (5 minutes) -Select one migrated VM in `destination-rg` and follow the same track through Challenges 7 and 8: +Record both VM names, their region, and their Azure resource IDs. Connect to each VM with Azure Bastion. -| Track | VM | Web service | Service-event table | -| --- | --- | --- | --- | -| A | Windows Server | IIS `W3SVC` | `Event` | -| B | Ubuntu Linux | Apache `apache2` | `Syslog` | - -Record the VM name and region. Connect with Azure Bastion and verify the selected service and local website. - -### Track A - Windows +### Windows/IIS baseline Run in Windows PowerShell as administrator: ```powershell -Get-Service -Name W3SVC -(Invoke-WebRequest -Uri 'http://localhost/' -UseBasicParsing -TimeoutSec 10).StatusCode -``` +$service = Get-Service -Name W3SVC +$response = Invoke-WebRequest -Uri 'http://localhost/' -UseBasicParsing -TimeoutSec 10 -The service must be `Running` and HTTP must return `200`. +$service | Select-Object Name, Status, StartType +$response | Select-Object StatusCode -### Track B - Linux +if ($service.Status -ne 'Running' -or $response.StatusCode -ne 200) { + throw 'The IIS baseline is not healthy.' +} +``` + +### Linux/Apache baseline Run in Bash: ```bash +set -euo pipefail + sudo systemctl is-active apache2 curl --fail --silent --show-error --output /dev/null \ --write-out 'HTTP %{http_code}\n' http://localhost/ +pgrep -a -x apache2 ``` -The service must be `active` and HTTP must return `200`. +Opening each page before the incident both proves the baseline and warms IIS/Apache. Keep both Bastion sessions available for the recovery task. -### Common Azure Copilot preflight +### Azure Copilot and permissions -1. In the portal, select **Copilot**. If an unauthorized message appears, ask a tenant administrator to follow [Manage access to Azure Copilot](https://learn.microsoft.com/en-us/azure/copilot/manage-access). In a restricted tenant, the user or a group containing the user needs the **Copilot for Azure User** role. -2. Confirm the client network allows WebSocket connections to `https://directline.botframework.com`. -3. Open the [Observability Agent overview](https://learn.microsoft.com/en-us/azure/azure-monitor/aiops/observability-agent-overview#regions) and confirm that the intended Log Analytics workspace region is supported. -4. Confirm your signed-in account can read the VM and its monitoring data and create the required monitoring resources. The Contributor or Owner prerequisite for this Hack is sufficient. For least privilege, review **Monitoring Reader**, **Log Analytics Reader**, and the specific write roles required for DCRs and alerts. +Azure Copilot access is treated as available for this Hack. Confirm: + +1. The portal **Copilot** entry opens and the current region is listed in the [Observability Agent regions](https://learn.microsoft.com/azure/azure-monitor/aiops/observability-agent-overview#regions). +2. Your account can read both VMs and their monitoring data and create the lab monitoring resources. To grant **Monitoring Reader**, you also need Owner, Role Based Access Control Administrator, User Access Administrator, or equivalent role-assignment permissions at the chosen scope; Contributor alone can't create the role assignment. +3. The client network allows WebSocket connections to `https://directline.botframework.com`. +4. Your account can create or reuse a user-assigned managed identity and grant it **Monitoring Reader** at the scope used by the query-based metric alerts. > [!IMPORTANT] -> If Azure Copilot, the **Observability Agent** button, the selected region, or tenant access is unavailable, continue with Tasks 2 through 4 and use the fallback in Task 5. Do not provision an Azure OpenAI resource or model. Do not create an Observability Agent resource for the required path. +> Observability Agent, deep investigations, Azure Monitor issues, and query-based metric alerts include preview experiences. A deep investigation can use up to 300 Azure Agent Credits. Portal labels and availability can vary by tenant and region. + +No Azure OpenAI deployment or autonomous Observability Agent resource is required. + +## Task 2: Enable modern infrastructure monitoring (15 minutes) + +Azure Monitor provides two different guest-monitoring models: + +| | OpenTelemetry metrics-based | Classic logs-based | +| --- | --- | --- | +| Data store | Azure Monitor workspace | Log Analytics workspace | +| Query | PromQL | KQL | +| Latency | Near real-time | Typically 1-3 minutes | +| Cost | Default metric set is free; additional metrics can incur cost | Standard log ingestion and retention charges | +| Best fit | New deployments and consistent Windows/Linux metrics | Multi-VM views and single-query metric/log correlation | +| Current limitation | Metrics and logs require separate queries; built-in multi-VM views are limited | Platform-specific counters and higher ingestion cost | -## Task 2: Enable monitoring and collect the selected service event (10 minutes) +Use the recommended OpenTelemetry experience for both VMs. Windows also needs Log Analytics for the authoritative service-control event; this challenge doesn't query classic performance tables. -### Common workspace and VM insights setup +### Create the alert-query identity before onboarding -1. Search for **Log Analytics workspaces** in the Azure portal. -2. Reuse a workspace in the selected VM's region, or create one in `destination-rg` with a unique name such as `law-mh-migration-`. -3. Open the selected VM and go to **Monitoring** > **Insights**. Select **Enable**, **Configure**, or **Configure monitoring**, depending on the current portal blade. -4. Select the Log Analytics workspace and enable the logs-based VM insights metrics option so performance data is available in `InsightsMetrics`. -5. Apply the configuration. -6. On the VM, open **Extensions + applications** and confirm the selected track's extension is provisioned successfully: - * Track A: `AzureMonitorWindowsAgent` - * Track B: `AzureMonitorLinuxAgent` -7. Open **Monitor** > **Data Collection Rules** and confirm that the VM insights DCR is associated with the selected VM. +Create the identity before opening **Infrastructure monitoring** on either VM: + +1. In the Azure portal, search for **Managed Identities**, select **Create**, and create a user-assigned managed identity such as `id-mh-monitor-alerts`. For this lab, use the Hack subscription and preferably the destination resource group and the same region as the migrated VMs. +2. Open the destination resource group or each migrated VM, select **Access control (IAM)** > **Add role assignment**, choose **Monitoring Reader**, and assign it to the new user-assigned managed identity. Resource-group scope is the simplest reusable choice for both lab VMs. +3. If a query-based alert is scoped to the Azure Monitor workspace instead of a VM or resource group, also grant the identity **Monitoring Reader** on that workspace. +4. Reuse this identity for both migrated VMs. The [query-based metric alert identity requirements](https://learn.microsoft.com/azure/azure-monitor/alerts/alerts-query-based-metric-alerts-overview#managed-identities-for-query-based-alerts) require read access to the alert scope. + +This user-assigned identity authorizes OpenTelemetry alert queries. It is different from the VM's system-assigned identity that Azure Monitor Agent can use. The Infrastructure monitoring wizard doesn't create the user-assigned alert identity, so create and authorize it before selecting it in the wizard. + +### Onboard the Windows VM + +1. Open the Windows VM and select **Monitor**. If enhanced monitoring isn't enabled, select **Configure**. +2. Select **Customize infrastructure monitoring**. +3. Keep **OpenTelemetry metrics** selected. +4. Optionally select the OpenTelemetry detailed or per-process option to explore the cross-platform schema. The Windows alert doesn't use process metrics, and enabling them can incur ingestion cost. +5. Also select **Classic Log-based metrics** for the Windows VM so the portal selects or creates a compatible regional default Log Analytics workspace for the Windows Event DCR and Logs-scoped agent experience. This also enables classic performance ingestion as a side effect and can incur Log Analytics cost. Do not create a separately named lab workspace first. +6. Use the default regional Azure Monitor workspace, Log Analytics workspace, and generated DCR proposed by the portal. +7. Keep **Dashboards with Grafana** and **Recommended alerts** selected when offered. Recommended alerts cover infrastructure health; they don't replace the service-specific alerts created in Task 3. +8. In **Identity**, select the pre-created `id-mh-monitor-alerts` user-assigned identity. Confirm it has **Monitoring Reader** on the VM or containing destination resource group and, for a workspace-scoped rule, on the Azure Monitor workspace. +9. Review and enable monitoring. + +Before continuing, confirm that the review page names an Azure Monitor workspace, a compatible regional default Log Analytics workspace, and the generated DCR. If no Log Analytics workspace appears, return to customization and confirm **Classic Log-based metrics** is selected. If the portal still doesn't propose a default workspace, stop and ask your instructor to provide the lab's compatible default; don't independently recreate the obsolete custom-workspace flow. + +The configuration should show both **OpenTelemetry metrics** and **Classic Log-based metrics** selected, the portal-proposed Azure Monitor and Log Analytics workspaces, recommended alerts enabled, and the pre-created user-assigned identity selected. + +![Azure portal Infrastructure monitoring configuration showing OpenTelemetry and Classic Log-based metrics, proposed workspaces, recommended alerts, and a pre-created user-assigned identity](./img/configure-infrastructure-monitoring.png) > [!NOTE] -> Azure Monitor supports associating multiple DCRs with one Azure Monitor Agent. Keep the VM insights DCR and add the selected track's focused event DCR. +> As part of Azure Monitor Agent installation, Azure can enable a system-assigned identity on the VM. That VM identity is separate from the pre-created user-assigned identity selected for alert queries; the wizard doesn't create the user-assigned alert identity. -### Track A - Create the Windows service-event DCR +### Onboard the Linux VM -1. In **Monitor** > **Data Collection Rules**, select **Create**. -2. Use rule name `dcr-iis-service-events`, resource group `destination-rg`, the VM's region, and platform type **Windows**. -3. On **Resources**, add the selected VM. Leave **Enable Data Collection Endpoints** off unless your network design requires a DCE. -4. On **Collect and deliver**, select **Add data source** > **Windows Event Logs**. -5. Select **Custom** and enter: +Repeat the Infrastructure monitoring flow for the Linux VM: - ```text - System!*[System[(EventID=7036)]] - ``` +1. Keep **OpenTelemetry metrics** and its detailed/per-process option selected. +2. Leave **Classic Log-based metrics** off; Apache uses an OpenTelemetry process signal and doesn't require a Log Analytics workspace. +3. Let the portal select or create the same regional default Azure Monitor workspace. +4. Accept the generated DCR, dashboard, and recommended alerts. +5. Select the same pre-created user-assigned alert identity and enable monitoring. -6. Add a destination of **Azure Monitor Logs**, select the Log Analytics workspace, and create the DCR. -7. Return to the DCR **Resources** page and verify the VM association. +The portal installs `AzureMonitorWindowsAgent` or `AzureMonitorLinuxAgent`. Provisioning, DCR association, and the first metric samples take several minutes. -### Track B - Create the Linux Syslog DCR +### Verify and customize the generated OpenTelemetry DCR -1. In **Monitor** > **Data Collection Rules**, select **Create**. -2. Use rule name `dcr-apache-incident-syslog`, resource group `destination-rg`, the VM's region, and platform type **Linux**. -3. On **Resources**, add the selected VM. Leave **Enable Data Collection Endpoints** off unless your network design requires a DCE. -4. On **Collect and deliver**, select **Add data source** > **Linux Syslog**. -5. Set the `daemon` facility to minimum log level **Warning**. Set all other facilities to **NONE**. -6. Add a destination of **Azure Monitor Logs**, select the Log Analytics workspace, and create the DCR. -7. Return to the DCR **Resources** page and verify the VM association. -8. After the association is ready, emit a focused preflight record: +1. Open **Monitor** > **Data Collection Rules** > **Resources**. +2. Locate each VM and open its associated DCR whose name follows `MSVMOtel--`. +3. For the Linux DCR, open **Data sources** > **OpenTelemetry Performance Counters**. +4. Select **Custom** and confirm that `process.uptime` is included. Add it if it isn't present. +5. Optionally include `process.cpu.utilization` and `process.memory.usage` for richer investigation evidence. +6. Keep the default 60-second sampling interval and save. - ```bash - logger -p daemon.warning -t microhack \ - "MicroHack Syslog preflight for apache2 monitoring" - ``` +Default OpenTelemetry metrics such as `system.uptime`, `system.cpu.time`, and `system.memory.usage` are free. `process.uptime` and other metrics beyond the default set can incur ingestion cost. Remove unneeded additional metrics after the Hack. -The DCR collects `daemon.warning` and higher-severity records. The tag and exact message let the queries ignore unrelated daemon messages. +### Verify metrics on both VMs -### Verify telemetry before the incident +From each VM, open **Metrics** and choose **View Azure Monitor workspace metrics in editor**, or open the Azure Monitor workspace PromQL editor. When the query is launched from a VM resource, Azure applies that VM's resource scope. -Open the Log Analytics workspace **Logs** page, set the time range to **Last 30 minutes**, and run: +Run: -```kusto -Heartbeat -| summarize LastHeartbeat=max(TimeGenerated) by Computer, _ResourceId -| order by LastHeartbeat desc +```promql +{"system.uptime"} ``` -```kusto -InsightsMetrics -| where Origin == "vm.azm.ms" -| summarize LastMetric=max(TimeGenerated), Samples=count() by Computer, _ResourceId -| order by LastMetric desc +On the Linux VM, also run: + +```promql +count({"process.uptime", "process.executable.name"="apache2"}) ``` -The selected VM must appear in both queries. +The first query must return recent data for each VM. The Linux query must return a value greater than zero. `process.executable.name` is an enabled per-process resource attribute, and `process.uptime` is a gauge measured in seconds. + +> [!NOTE] +> Azure Monitor PromQL uses Prometheus 3 UTF-8 syntax. Metric and label names containing dots are quoted inside braces, for example `{"process.uptime", "process.executable.name"="apache2"}`. -Copy the selected VM's `_ResourceId` from the results. Replace `` in the track-specific queries below with that exact value. +## Task 3: Create service-specific alert paths (15 minutes) -Track B also verifies the preflight event: +Reuse the action group created by recommended monitoring alerts, or create `ag-mh-operators` in `destination-rg` with an email receiver you can check. -```kusto -Syslog -| where TimeGenerated >= ago(30m) -| where Facility =~ "daemon" and SeverityLevel =~ "warning" -| where ProcessName =~ "microhack" -| where SyslogMessage == "MicroHack Syslog preflight for apache2 monitoring" -| where _ResourceId =~ "" -| project TimeGenerated, Computer, ProcessName, Facility, SeverityLevel, SyslogMessage, _ResourceId -| order by TimeGenerated desc -``` +### Windows/IIS: collect the authoritative service event -> [!NOTE] -> Agent installation, DCR association, and ingestion are asynchronous. Refresh until the required records appear; do not assume a fixed ingestion time. +Create a focused DCR: -## Task 3: Create an actionable service alert (8 minutes) +1. Open **Monitor** > **Data Collection Rules** and select **Create**. +2. Name it `dcr-iis-service-events`, use `destination-rg`, the VM's region, and platform type **Windows**. +3. On **Resources**, add the Windows VM. Leave **Enable Data Collection Endpoints** off unless your network design requires a DCE. +4. Add a **Windows Event Logs** data source. +5. Select **Custom** and enter: -### Create or reuse an action group + ```text + System!*[System[(EventID=7036)]] + ``` -1. In **Monitor** > **Alerts**, select **Action groups** > **Create**. -2. Create `ag-mh-operators` in `destination-rg`, or reuse it if another team already created it. -3. Add an **Email/SMS message/Push/Voice** notification and send email to an address you can check. Name the receiver `LabOperator`. -4. Review and create the action group. +6. Set the destination to **Azure Monitor Logs** and select the compatible default Log Analytics workspace chosen during Windows onboarding. +7. Create the DCR and verify its VM association. -### Create the selected track's log search alert +Confirm that the DCR's **Resources** page shows the Windows VM. You can use the Log Analytics workspace's **Logs** page to inspect any service-control records collected after the association: -Open the Log Analytics workspace, select **Alerts** > **Create** > **Alert rule**, and choose **Custom log search**. +```kusto +Event +| where TimeGenerated >= ago(30m) +| where EventLog == "System" and EventID == 7036 +| where _ResourceId =~ "" +| project TimeGenerated, Computer, RenderedDescription, _ResourceId +| order by TimeGenerated desc +``` -Track A query: +Zero rows are valid when no service has changed state since the DCR became active. The deterministic ingestion gate is the `W3SVC` stopped event generated and queried in Task 4; don't create a false service interruption only to populate this preflight query. + +Create a **Custom log search** scheduled-query alert from the workspace: ```kusto Event @@ -158,55 +188,60 @@ Event or RenderedDescription has "W3SVC" or ParameterXml has "W3SVC" | where RenderedDescription has "stopped" or ParameterXml has "stopped" -| where _ResourceId =~ "" +| where _ResourceId =~ "" | project TimeGenerated, Computer, RenderedDescription, _ResourceId ``` -Track B query: +Configure: -```kusto -Syslog -| where TimeGenerated >= ago(10m) -| where Facility =~ "daemon" and SeverityLevel =~ "warning" -| where ProcessName =~ "microhack" -| where SyslogMessage == "apache2 service stopped for MicroHack incident" -| where _ResourceId =~ "" -| project TimeGenerated, Computer, ProcessName, SyslogMessage, _ResourceId +| Setting | Value | +| --- | --- | +| Alert name | `IIS W3SVC stopped` | +| Measure | Table rows | +| Aggregation | Count over 10 minutes | +| Condition | Greater than `0` | +| Evaluation frequency | 5 minutes | +| Severity | 2 - Warning | +| Action group | `ag-mh-operators` | +| Enable | Selected | + +Enable automatic resolution when offered. Do not alert on `w3wp.exe` absence: application-pool idle timeout can remove the worker process while `W3SVC` and the site remain healthy. + +### Linux/Apache: alert only when every Apache process disappears + +Open **Monitor** > **Alerts** > **Create** > **Alert rule** and scope the rule to the Linux VM. Select the query-based metric alert condition and use: + +```promql +absent_over_time({"process.uptime", "process.executable.name"="apache2"}[5m]) ``` -Select **Run**. A zero-row result is expected before the incident. Configure: +Configure: | Setting | Value | | --- | --- | -| Measure | Table rows | -| Aggregation type | Count | -| Aggregation granularity | 10 minutes | -| Operator | Greater than | -| Threshold value | `0` | -| Frequency of evaluation | 5 minutes | -| Action group | `ag-mh-operators` | +| Alert name | `Apache processes absent` | +| Condition | Query result greater than `0` | +| Evaluation frequency | 1 minute | +| Failing period | 1 minute | +| Auto resolution | Enabled | | Severity | 2 - Warning | -| Enable upon creation | Selected | +| Managed identity | Pre-created user-assigned identity with **Monitoring Reader** on the Linux VM or destination resource group and, for workspace scope, the Azure Monitor workspace | +| Action group | `ag-mh-operators` | -Use the selected track's alert details: +`apache2` normally has a parent and multiple workers, each with its own `process.uptime` series. `absent_over_time` returns no result while any matching series exists and returns `1` only after all matching series have been absent for the full five-minute window. Allow up to 10 minutes after the stop command for the final sample, absence window, collection, and alert evaluation before treating a missing alert as a configuration problem. -| Track | Alert rule name | Description | -| --- | --- | --- | -| A | `IIS W3SVC stopped` | `W3SVC stopped. Validate VM heartbeat, start W3SVC, and test HTTP.` | -| B | `Apache apache2 stopped` | `Controlled apache2 stop event detected. Validate VM heartbeat, start apache2, and test HTTP.` | +## Task 4: Induce and prove both controlled incidents (10 minutes) -If the portal offers automatic resolution, enable it. Create the rule and verify it is enabled. +Warm both pages once more before stopping either service. Process/service telemetry proves component state, while the local HTTP request proves user-visible impact in this private lab. Neither signal is a substitute for production synthetic availability monitoring. -## Task 4: Induce and prove a safe incident (5 minutes) - -### Track A - Windows +### Stop IIS and prove impact Run in elevated Windows PowerShell: ```powershell $incidentStart = Get-Date Stop-Service -Name W3SVC -Force -Get-Service -Name W3SVC +$service = Get-Service -Name W3SVC $siteFailed = $false try { @@ -216,187 +251,150 @@ catch { $siteFailed = $true Write-Host "Expected local IIS failure: $($_.Exception.Message)" } -if (-not $siteFailed) { - throw 'The local site unexpectedly remained available.' + +if ($service.Status -ne 'Stopped' -or -not $siteFailed) { + throw "The controlled IIS incident wasn't proven." } Get-WinEvent -FilterHashtable @{ - LogName = 'System' - Id = 7036 + LogName = 'System' + Id = 7036 StartTime = $incidentStart } | Where-Object { $_.Message -match 'World Wide Web Publishing Service|W3SVC' } | Select-Object TimeCreated, Id, Message ``` -### Track B - Linux +Wait for the stopped event to appear in the Task 3 KQL query, then wait for `IIS W3SVC stopped` to appear under **Monitor** > **Alerts** > **Alert instances**. + +### Stop Apache and prove impact Run in Bash: ```bash +set -euo pipefail + sudo systemctl stop apache2 -if sudo systemctl is-active --quiet apache2; then - echo "apache2 unexpectedly remained active." >&2 +if systemctl is-active --quiet apache2 || pgrep -x apache2 >/dev/null; then + echo "Apache unexpectedly remained active." >&2 exit 1 -else - sudo systemctl is-active apache2 || true fi if curl --fail --silent --show-error --max-time 10 http://localhost/ >/dev/null; then - echo "The local site unexpectedly remained available." >&2 + echo "The local Apache site unexpectedly remained available." >&2 exit 1 else echo "Expected local Apache HTTP failure confirmed." fi - -logger -p daemon.warning -t microhack \ - "apache2 service stopped for MicroHack incident" ``` -The `logger` command explicitly records the controlled stop action. This avoids depending solely on whether the Ubuntu systemd journal is forwarded to Syslog. - -### Verify the ingested incident - -Run the selected track's query. +In the Linux VM's resource-scoped PromQL editor, compare: -Track A: - -```kusto -Event -| where TimeGenerated >= ago(30m) -| where EventLog == "System" and EventID == 7036 -| where RenderedDescription has "World Wide Web Publishing Service" - or RenderedDescription has "W3SVC" - or ParameterXml has "W3SVC" -| where RenderedDescription has "stopped" or ParameterXml has "stopped" -| where _ResourceId =~ "" -| project TimeGenerated, Computer, RenderedDescription, ParameterXml, _ResourceId -| order by TimeGenerated desc +```promql +count({"process.uptime", "process.executable.name"="apache2"}) ``` -Track B: - -```kusto -Syslog -| where TimeGenerated >= ago(30m) -| where Facility =~ "daemon" and SeverityLevel =~ "warning" -| where ProcessName =~ "microhack" -| where SyslogMessage == "apache2 service stopped for MicroHack incident" -| where _ResourceId =~ "" -| project TimeGenerated, Computer, ProcessName, Facility, SeverityLevel, SyslogMessage, _ResourceId -| order by TimeGenerated desc +```promql +absent_over_time({"process.uptime", "process.executable.name"="apache2"}[5m]) ``` -Refresh until the stop event appears. This record is the deterministic incident gate. Then review **Monitor** > **Alerts** > **Alert instances** and the action-group destination. Ingestion, evaluation, and notification are separate asynchronous stages. +The count disappears after the final process series stops reporting. The absence query becomes `1` only after the complete five-minute gap. Allow up to 10 minutes from the stop command for `Apache processes absent` to fire. + +> [!NOTE] +> If either alert doesn't fire, keep the services stopped only long enough to verify DCR association, identity authorization, query scope, ingestion, and alert evaluation. Task 6 contains independent recovery commands and can be run at any time. -## Task 5: Investigate with Azure Copilot Observability Agent (7 minutes) +## Task 5: Investigate, decide, and preserve context with AI (15 minutes) -On the Log Analytics workspace **Logs** page, select **Observability Agent** and use the prompt for your track. +Open each fired alert and select **Investigate**. Review the Azure Agent Credit notice, select **Start chat**, and let the Observability Agent complete its deep investigation. -### Track A prompt +### Investigate the IIS alert ```text -Investigate the IIS availability incident on Windows VM during the last -30 minutes. Use Event, Heartbeat, and InsightsMetrics. Determine whether W3SVC -stopped, whether the VM stayed online, and whether host performance suggests a -broader failure. Cite the resource and timestamps, recommend the safest -remediation, and do not make changes. +Investigate this IIS availability incident on the Windows VM. Build an evidence-backed +timeline from the alert, Windows System event ID 7036 for W3SVC, OpenTelemetry host +metrics, Resource Health, Activity Log, and related alerts. State the user impact and +blast radius. Determine the most likely root cause, list plausible hypotheses that the +evidence rules out, and cite the resource and timestamps used. Recommend the safest +human-executed remediation, rollback, validation steps, and alert-tuning or prevention +improvements. Do not change resources or restart services. ``` -### Track B prompt +### Investigate the Apache alert ```text -Investigate the Apache availability incident on Ubuntu VM during the -last 30 minutes. Use the microhack-tagged daemon.warning record in Syslog, -Heartbeat, and InsightsMetrics. Determine whether apache2 stopped, whether the VM -stayed online, and whether host performance suggests a broader failure. Cite the -resource and timestamps, recommend the safest remediation, and do not make changes. +Investigate this Apache availability incident on the Ubuntu VM. Confirm that all +process.uptime series with process.executable.name=apache2 disappeared for the alert +window, then correlate that signal with OpenTelemetry host metrics, Resource Health, +Activity Log, and related alerts. State the user impact and blast radius. Determine the +most likely root cause, list hypotheses ruled out by the evidence, and cite resources +and timestamps. Recommend the safest human-executed remediation, rollback, validation, +and alert-tuning or prevention improvements. Do not change resources or start services. ``` -For either track, ask: +Then ask from either investigation: ```text -Correlate the service-stop event with heartbeat and CPU or memory signals. -Explain why this is a service-level or VM-level outage and provide recovery -verification steps. +Compare the two incidents. Did they share a platform, network, capacity, or regional +failure, or were they independent service-level failures? Separate observed evidence +from inference and identify any telemetry gap that prevents a firm conclusion. ``` -Confirm that the response identifies the selected event, checks whether heartbeat continued, reviews performance telemetry, and recommends starting the selected web service and testing HTTP. If the portal offers **Start investigation** from the fired alert, you may compare it with workspace-scoped chat. Saving an issue is optional. +Confirm that the report contains impact, scope, chronology, supporting evidence, a likely cause, ruled-out alternatives, safe remediation, rollback, and prevention guidance. Use **Create Issue** when available to preserve the investigation report and conversation as an Azure Monitor issue. -> [!NOTE] -> On-demand chat and investigation work without provisioning a `Microsoft.Monitor/observabilityAgents` resource. Chat context is temporary. +> [!IMPORTANT] +> This lab uses controlled autonomy. The agent can chat, correlate telemetry, run a user-invoked investigation, create issues, and recommend actions. It doesn't restart services, modify production resources, or resolve the incident automatically. The human operator evaluates and executes remediation. -### Fallback - KQL and VM insights when Azure Copilot is blocked +Do not create an autonomous Observability Agent resource. In the current preview, the portal scopes that resource to Application Insights, and new VM-only scope isn't a supported configuration for this Hack. -Use the selected track's event query to identify the incident's UTC `TimeGenerated`. +### Concise manual fallback -Track A: +If agent output is unavailable, retain the same evidence standard: -```kusto -Event -| where TimeGenerated >= ago(2h) -| where EventLog == "System" and EventID == 7036 -| where RenderedDescription has "World Wide Web Publishing Service" - or RenderedDescription has "W3SVC" - or ParameterXml has "W3SVC" -| where RenderedDescription has "stopped" or ParameterXml has "stopped" -| where _ResourceId =~ "" -| project TimeGenerated, Computer, RenderedDescription, _ResourceId -| order by TimeGenerated desc -``` +1. Use the IIS stopped-event KQL query from Task 3. +2. Use the Linux `count` and `absent_over_time` PromQL queries from Task 4. +3. At each VM's incident time, inspect the default host signals: -Track B: + ```promql + {"system.uptime"} + ``` -```kusto -Syslog -| where TimeGenerated >= ago(2h) -| where Facility =~ "daemon" and SeverityLevel =~ "warning" -| where ProcessName =~ "microhack" -| where SyslogMessage == "apache2 service stopped for MicroHack incident" -| where _ResourceId =~ "" -| project TimeGenerated, Computer, SyslogMessage, _ResourceId -| order by TimeGenerated desc -``` + ```promql + sum by (state) (rate({"system.cpu.time"}[5m])) + ``` -Replace the placeholder below with that UTC timestamp and run the common correlation queries: + ```promql + sum by (state) ({"system.memory.usage"}) + ``` -```kusto -let IncidentTime = datetime(); -Heartbeat -| where TimeGenerated between ((IncidentTime - 15m) .. (IncidentTime + 15m)) -| summarize Heartbeats=count(), LastHeartbeat=max(TimeGenerated) - by Computer, bin(TimeGenerated, 5m) -| order by TimeGenerated asc -``` +4. Review each VM's **Resource Health**, **Activity log**, and Grafana dashboard for the same window. -```kusto -let IncidentTime = datetime(); -InsightsMetrics -| where TimeGenerated between ((IncidentTime - 15m) .. (IncidentTime + 15m)) -| where Origin == "vm.azm.ms" -| summarize Samples=count() - by Computer, Namespace, Name, bin(TimeGenerated, 5m) -| order by TimeGenerated asc -``` +Continuing system uptime and normal host signals, combined with a stopped service signal and local HTTP failure, support a service-level diagnosis. Record evidence that rules out VM restart, resource exhaustion, Azure platform impact, or an unrelated configuration change before choosing remediation. + +## Task 6: Restore, validate, and improve the runbook (10 minutes) -Open the VM's **Monitoring** > **Insights** performance view for the same time range. Continuing heartbeat and performance samples show the VM remained available while the web service stopped, so service restoration is the first remediation. +The operator, not the agent, restores both services. -## Task 6: Restore service and verify recovery (5 minutes) +### Restore IIS -### Track A - Windows +Run in elevated Windows PowerShell: ```powershell Set-Service -Name W3SVC -StartupType Automatic Start-Service -Name W3SVC + $service = Get-Service -Name W3SVC $response = Invoke-WebRequest -Uri 'http://localhost/' -UseBasicParsing -TimeoutSec 10 $service | Select-Object Name, Status, StartType $response | Select-Object StatusCode + +if ($service.Status -ne 'Running' -or $response.StatusCode -ne 200) { + throw 'IIS recovery validation failed.' +} ``` -Verify `Running`, `Automatic`, and HTTP `200`. In Log Analytics, the latest matching event ID 7036 record should show the service returning to the running state. +Verify the running event: ```kusto Event @@ -406,46 +404,54 @@ Event or RenderedDescription has "W3SVC" or ParameterXml has "W3SVC" | where RenderedDescription has "running" or ParameterXml has "running" -| where _ResourceId =~ "" +| where _ResourceId =~ "" | project TimeGenerated, Computer, RenderedDescription, ParameterXml, _ResourceId | order by TimeGenerated desc ``` -### Track B - Linux +### Restore Apache + +Run in Bash: ```bash -sudo systemctl enable apache2 -sudo systemctl start apache2 -sudo systemctl is-active apache2 +set -euo pipefail +sudo systemctl enable --now apache2 +systemctl is-active --quiet apache2 +pgrep -a -x apache2 curl --fail --silent --show-error --output /dev/null \ --write-out 'HTTP %{http_code}\n' http://localhost/ - -logger -p daemon.warning -t microhack \ - "apache2 service restored after MicroHack incident" ``` -Verify `active` and HTTP `200`. Then verify the recovery record: +Verify that process telemetry returns: -```kusto -Syslog -| where TimeGenerated >= ago(30m) -| where Facility =~ "daemon" and SeverityLevel =~ "warning" -| where ProcessName =~ "microhack" -| where SyslogMessage == "apache2 service restored after MicroHack incident" -| where _ResourceId =~ "" -| project TimeGenerated, Computer, SyslogMessage, _ResourceId -| order by TimeGenerated desc +```promql +count({"process.uptime", "process.executable.name"="apache2"}) ``` -For either track, test the existing external endpoint. If automatic alert resolution is enabled, resolution occurs only after the stop event leaves the query window and a later evaluation completes; you do not need to wait for it. +The value must again be greater than zero. Use Bastion to validate both web pages and confirm their hostname, platform, and web-server details remain correct. + +The Apache alert can resolve after process samples return. The IIS alert resolves only after the stopped event leaves its query window and a later evaluation succeeds. Review the alert timelines rather than forcing closure. + +Ask the agent in the saved investigation or issue: + +```text +Create a concise post-incident summary for both service failures. Include impact, +timeline, authoritative evidence, root cause, remediation and validation, rollback, +alert behavior, remaining telemetry gaps, and three prioritized runbook or alert-tuning +improvements. Clearly state that local HTTP validation isn't continuous synthetic +availability monitoring. +``` -### Cleanup +### Cleanup and handoff -Keep VM monitoring for Challenge 8 unless your instructor asks you to remove it. After the Hack, you may independently remove the alert rule, action group, selected event DCR association/DCR, and Log Analytics workspace if they were created only for this lab. Do not delete shared resources. +Keep monitoring while completing Challenge 8. After the Hack: -### Optional stretch - autonomous operations (preview) +* Remove the two lab service alerts, their action-group links, and the Windows service-event DCR if they aren't shared. +* Remove additional per-process metrics if they were enabled only for this lab and their ongoing cost isn't justified. +* Don't delete portal-created Azure Monitor or Log Analytics workspaces, generated DCRs, dashboards, or action groups until you confirm no other resources use them. +* Don't delete the shared user-assigned alert identity until you confirm no other alert rules use it. -If your instructor has prepared an Azure Monitor workspace, an eligible monitored application, and the documented roles, review [Create an Azure Copilot Observability Agent resource](https://learn.microsoft.com/en-us/azure/azure-monitor/aiops/observability-agent-resource-create-portal). The resource enables preview autonomous correlation, issue creation, investigations, and custom instructions. It is intentionally excluded from the required lab path. +Continue to Challenge 8, where you select either recovered workload for the guided replatforming exercise. -You successfully completed Challenge 7. Continue to Challenge 8 with the same selected track. +You successfully completed Challenge 7. diff --git a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-08/solution-08.md b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-08/solution-08.md index 4d69b0ae9..689686896 100644 --- a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-08/solution-08.md +++ b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-08/solution-08.md @@ -1,21 +1,21 @@ -# Walkthrough Challenge 8 - Replatform the selected migrated web workload to Azure App Service +# Walkthrough Challenge 8 - Replatform a migrated web workload to Azure App Service [Previous Challenge Solution](../challenge-07/solution-07.md) - **[Home](../../Readme.md)** - [Finish](../../challenges/finish.md) -Duration: 40 minutes +Duration: 45 minutes -Continue with the same track selected in Challenge 7: +Select one path for the guided exercise: -* **Track A:** Windows Server / IIS -* **Track B:** Ubuntu Linux / Apache +* **Path A:** Windows Server / IIS to Windows App Service +* **Path B:** Ubuntu Linux / Apache to Linux App Service -Complete only your selected track. +Complete one path during the standard challenge time. If time permits, complete both paths with distinct App Service plans and distinct globally unique web-app names. Windows and Linux apps require plans for their respective operating systems. -## Task 1: Discover the web application before choosing a target (10 minutes) +## Task 1: Select and inspect a web workload (10 minutes) Azure Migrate's at-scale web-app migration flow supports ASP.NET web apps on Windows IIS servers hosted in VMware environments. This Hack uses Hyper-V, and the integrated flow doesn't support the Linux/Apache workload. We will deliberately inspect and manually replatform the selected migrated workload. -### Track A - Inventory Windows/IIS +### Path A - Inventory Windows/IIS Connect with Azure Bastion and run in elevated Windows PowerShell: @@ -37,57 +37,70 @@ Get-Content -Path 'C:\inetpub\wwwroot\index.html' (Invoke-WebRequest -Uri 'http://localhost/' -UseBasicParsing -TimeoutSec 10).StatusCode ``` -### Track B - Inventory Linux/Apache +### Path B - Inventory Linux/Apache Connect with Azure Bastion and run in Bash: +> [!NOTE] +> Paste each Path B Bash block into the Azure Bastion browser SSH terminal as one complete block, then wait for its completion or failure status before continuing. Each block runs its fail-fast commands in a child Bash process while leaving terminal input available for device-code sign-in. If a command fails, the child stops and control returns to your SSH prompt without changing your login shell options or traps. Correct the reported error and rerun that block before continuing. + ```bash +if bash /dev/fd/3 3<<'PATH_B_INVENTORY' +set -euo pipefail + sudo apache2ctl -S sudo apache2ctl -M -sudo systemctl show apache2 \ - --property=ActiveState,SubState,UnitFileState +systemctl_args=(show apache2 --property=ActiveState,SubState,UnitFileState) +sudo systemctl "${systemctl_args[@]}" -sudo find /var/www/html -type f \ - -printf '%p\t%f\t%s bytes\n' | sort +file_list_args=(/var/www/html -type f -printf '%p\t%f\t%s bytes\n') +sudo find "${file_list_args[@]}" | sort sudo find /var/www/html -type f -printf '%f\n' | awk -F. 'NF > 1 {print "." $NF}' | sort | uniq -c | sort -nr -sudo find /var/www/html -type f \ - \( -name '*.html' -o -name '*.css' -o -name '*.js' -o -name '*.json' \) \ - -exec grep -HnEi \ - 'localhost|127\.0\.0\.1|/var/|/home/|mysql|postgres|connection(string)?' {} + || true +dependency_pattern='localhost|127\.0\.0\.1|/var/|/home/|mysql|postgres|connection(string)?' +dependency_find_args=(/var/www/html -type f '(' -name '*.html' -o -name '*.css' -o -name '*.js' -o -name '*.json' ')' -exec grep -HnEi "${dependency_pattern}" '{}' +) +sudo find "${dependency_find_args[@]}" || true sudo sed -n '1,120p' /var/www/html/index.html -curl --fail --silent --show-error --output /dev/null \ - --write-out 'HTTP %{http_code}\n' http://localhost/ +curl_args=(--fail --silent --show-error --output /dev/null --write-out 'HTTP %{http_code}\n') +curl "${curl_args[@]}" http://localhost/ +PATH_B_INVENTORY +then + echo 'Path B inventory completed.' +else + path_b_status=$? + printf 'Path B step failed; your SSH session remains open (status %d).\n' "${path_b_status}" >&2 + echo 'Correct the error and rerun this block before continuing.' >&2 +fi ``` Review enabled virtual hosts/modules and any dependency matches. A text match is a prompt to investigate, not proof of a dependency. -### Complete the discovery record - -| Area | Track A finding | Track B finding | -| --- | --- | --- | -| Site/service | `Default Web Site` / `W3SVC` | Default Apache virtual host / `apache2` | -| Content path | `C:\inetpub\wwwroot` | `/var/www/html` | -| Binding | HTTP, normally port 80 | HTTP, normally `*:80` | -| Runtime/modules | No server-side runtime used by the site | No server-side runtime/module used by the site | -| Content | Static HTML, CSS, and images | Static HTML, CSS, and images | -| External dependencies | None required to render the page | None required to render the page | -| Persistent/session state | None | None | -| Machine-specific values | Hostname, platform, and web server in stable `data-workload-value` fields | Hostname, platform, and web server in stable `data-workload-value` fields | +Review the output for required runtimes, databases, machine-local dependencies, and session state. This site is a portable static artifact containing HTML, CSS, and images; only its hostname, platform, and web-server metadata is machine-specific. -The source is a static artifact. A Windows App Service worker can host it regardless of whether it came from Windows/IIS or Linux/Apache. +There is no additional Azure Migrate portal action to perform here. The inventory is the evidence for choosing an App Service target. ## Task 2: Make the content portable and create the package (10 minutes) -### Track A - PowerShell package +### Path A - PowerShell package + +Create a staging copy so packaging doesn't modify the IIS source content: ```powershell $siteRoot = 'C:\inetpub\wwwroot' -$indexPath = Join-Path $siteRoot 'index.html' +$packageDirectory = 'C:\temp' +$stagingDirectory = Join-Path $packageDirectory 'microhack-app' +$packagePath = Join-Path $packageDirectory 'microhack-app.zip' + +New-Item -Path $packageDirectory -ItemType Directory -Force | Out-Null +Remove-Item -Path $stagingDirectory -Recurse -Force -ErrorAction SilentlyContinue +Remove-Item -Path $packagePath -Force -ErrorAction SilentlyContinue +Copy-Item -LiteralPath $siteRoot -Destination $stagingDirectory -Recurse + +$indexPath = Join-Path $stagingDirectory 'index.html' $html = Get-Content -Path $indexPath -Raw $html = $html -replace '(]*\bdata-workload-value="hostname"[^>]*>)[^<]*()', '${1}Azure App Service${2}' $html = $html -replace '(]*\bdata-workload-value="platform"[^>]*>)[^<]*()', '${1}Managed PaaS${2}' @@ -108,11 +121,7 @@ foreach ($field in $expectedMetadata.Keys) { } } -$packageDirectory = 'C:\temp' -$packagePath = Join-Path $packageDirectory 'microhack-app.zip' -New-Item -Path $packageDirectory -ItemType Directory -Force | Out-Null -Remove-Item -Path $packagePath -Force -ErrorAction SilentlyContinue -Compress-Archive -Path "$siteRoot\*" -DestinationPath $packagePath -Force +Compress-Archive -Path "$stagingDirectory\*" -DestinationPath $packagePath -Force Add-Type -AssemblyName System.IO.Compression.FileSystem $archive = [System.IO.Compression.ZipFile]::OpenRead($packagePath) @@ -124,44 +133,46 @@ try { } finally { $archive.Dispose() + Remove-Item -Path $stagingDirectory -Recurse -Force } Get-Item -Path $packagePath | Select-Object FullName, Length, LastWriteTime ``` -### Track B - Linux package +### Path B - Linux package -Keep the same Bash session open through Task 6 so the recorded variables remain available. Create a temporary, user-owned staging copy. This keeps `/var/www/html` and its ownership unchanged: +Create a temporary, user-owned staging copy. This keeps `/var/www/html` and its ownership unchanged, and the published package path is reconstructed by later blocks: ```bash +if bash /dev/fd/3 3<<'PATH_B_PACKAGE' set -euo pipefail -site_root="/var/www/html" +site_root='/var/www/html' staging_dir="$(mktemp -d)" -package_path="$HOME/microhack-app.zip" +package_path="${HOME}/microhack-app.zip" +package_tmp="$(mktemp --tmpdir="${HOME}" '.microhack-app.XXXXXXXX.zip')" +rm -f "${package_tmp}" + +cleanup_package() { + local status=$? + trap - EXIT + rm -rf "${staging_dir}" || true + rm -f "${package_tmp}" || true + return "${status}" +} +trap cleanup_package EXIT sudo cp -a "${site_root}/." "${staging_dir}/" sudo chown -R "$(id -u):$(id -g)" "${staging_dir}" -sed -E -i \ - -e 's#(]*data-workload-value="hostname"[^>]*>)[^<]*()#\1Azure App Service\2#' \ - -e 's#(]*data-workload-value="platform"[^>]*>)[^<]*()#\1Managed PaaS\2#' \ - -e 's#(]*data-workload-value="web-server"[^>]*>)[^<]*()#\1Azure App Service\2#' \ - "${staging_dir}/index.html" - -while IFS='|' read -r field expected_value; do - grep -Eq \ - "data-workload-value=\"${field}\"[^>]*>${expected_value}" \ - "${staging_dir}/index.html" || { - echo "Expected App Service metadata was not written: ${field}" >&2 - rm -rf "${staging_dir}" - exit 1 - } -done <<'EOF' -hostname|Azure App Service -platform|Managed PaaS -web-server|Azure App Service -EOF +sed_args=( + -E + -i + -e 's#(]*data-workload-value="hostname"[^>]*>)[^<]*()#\1Azure App Service\2#' + -e 's#(]*data-workload-value="platform"[^>]*>)[^<]*()#\1Managed PaaS\2#' + -e 's#(]*data-workload-value="web-server"[^>]*>)[^<]*()#\1Azure App Service\2#' +) +sed "${sed_args[@]}" "${staging_dir}/index.html" if ! command -v zip >/dev/null 2>&1 || ! command -v unzip >/dev/null 2>&1; then @@ -169,58 +180,137 @@ if ! command -v zip >/dev/null 2>&1 || sudo apt-get install -y zip unzip fi -rm -f "${package_path}" ( cd "${staging_dir}" - zip -r "${package_path}" . + zip -qr "${package_tmp}" . ) -unzip -l "${package_path}" -unzip -Z1 "${package_path}" | grep -Fxq 'index.html' || { - echo "index.html is not at the ZIP root." >&2 - rm -rf "${staging_dir}" - exit 1 -} +archive_entries="$(unzip -Z1 "${package_tmp}")" +required_assets=( + 'index.html' + 'stylesheet.css' + 'GitHub_Logo.png' + 'MSLogo.png' + 'MSicon.png' +) +for asset in "${required_assets[@]}"; do + if ! grep -Fxq "${asset}" <<<"${archive_entries}"; then + echo "Required asset is missing from the ZIP root: ${asset}" >&2 + false + fi +done + +archive_html="$(unzip -p "${package_tmp}" index.html)" +metadata_checks=( + 'hostname|Azure App Service' + 'platform|Managed PaaS' + 'web-server|Azure App Service' +) +for check in "${metadata_checks[@]}"; do + field="${check%%|*}" + expected_value="${check#*|}" + if ! grep -Eq "data-workload-value=\"${field}\"[^>]*>${expected_value}" <<<"${archive_html}"; then + echo "Expected App Service metadata is missing from the ZIP: ${field}" >&2 + false + fi +done +if grep -Eq '<(HOSTNAME|PLATFORM|WEB_SERVER)>' <<<"${archive_html}"; then + echo 'The ZIP still contains unresolved workload template tokens.' >&2 + false +fi -rm -rf "${staging_dir}" +mv -f "${package_tmp}" "${package_path}" ls -lh "${package_path}" +PATH_B_PACKAGE +then + echo 'Path B package completed.' +else + path_b_status=$? + printf 'Path B step failed; your SSH session remains open (status %d).\n' "${path_b_status}" >&2 + echo 'Correct the error and rerun this block before continuing.' >&2 +fi ``` -For both tracks, the archive must contain `index.html` at its root, not under `wwwroot/`, `html/`, or another parent directory. +For both paths, the archive must contain `index.html` and all four image/style assets at its root, not under `wwwroot/`, `html/`, or another parent directory. Path B keeps any previously published `$HOME/microhack-app.zip` unchanged until a replacement ZIP passes every check and is moved into place atomically. + +## Task 3: Verify Azure scope and create the App Service target (10 minutes) -## Task 3: Create the low-cost Windows App Service target (7 minutes) +The Bicep deployment names the destination resource group `MHBox--destination-rg`, where `` is the deployer's user principal name before `@`. Find the exact name under **Resource groups** in the Azure portal. After selecting the intended subscription, you can also run `az group list --query "[?ends_with(name, '-destination-rg')].[name,location]" --output table`. -The target is a Windows App Service plan for both tracks. Static files are portable; the source OS does not determine the worker OS. +Replace every placeholder below before running a resource command. Each Path B block repeats the required subscription and resource-group values and derives the same stable plan and app names, so it can be rerun from a new SSH session. -### Track A - Create from Azure Cloud Shell +### Path A - Create a Windows target from Azure Cloud Shell -Open Azure Cloud Shell in the portal, select **PowerShell**, and run: +Open Azure Cloud Shell in the portal and select **PowerShell**. Verify the Az PowerShell and Azure CLI contexts before creating anything: ```powershell -$destinationRg = 'destination-rg' -$location = az group show ` - --name $destinationRg ` - --query location ` - --output tsv +$subscriptionId = '' +$destinationRg = 'MHBox--destination-rg' -$suffix = (Get-Random -Minimum 100000 -Maximum 999999).ToString() -$planName = "asp-mh-replatform-$suffix" -$appName = "mh-web-$suffix" +if ([string]::IsNullOrWhiteSpace($subscriptionId) -or + $subscriptionId -eq '') { + throw 'Replace $subscriptionId with the Hack subscription ID.' +} +if ([string]::IsNullOrWhiteSpace($destinationRg) -or + $destinationRg -match '[<>]') { + throw 'Replace $destinationRg with the exact destination resource-group name.' +} + +if (-not (Get-AzContext -ErrorAction SilentlyContinue)) { + Connect-AzAccount -UseDeviceAuthentication | Out-Null +} +$azContext = Set-AzContext -SubscriptionId $subscriptionId + +az account set --subscription $subscriptionId +if ($LASTEXITCODE -ne 0) { + throw 'Azure CLI could not select the requested subscription.' +} +$cliContextJson = az account show ` + --query '{subscription:name,subscriptionId:id,tenantId:tenantId,user:user.name}' ` + --output json +if ($LASTEXITCODE -ne 0) { + throw 'Azure CLI could not display the active subscription.' +} +$cliContext = $cliContextJson | ConvertFrom-Json + +[pscustomobject]@{ + Subscription = $azContext.Subscription.Name + SubscriptionId = $azContext.Subscription.Id + TenantId = $azContext.Tenant.Id + User = $azContext.Account.Id +} | Format-Table +$cliContext | Format-Table + +if ($azContext.Subscription.Id -ne $cliContext.subscriptionId) { + throw 'Az PowerShell and Azure CLI are using different subscriptions.' +} + +$resourceGroup = Get-AzResourceGroup -Name $destinationRg -ErrorAction Stop +$location = $resourceGroup.Location +$resourceGroup | Select-Object ResourceGroupName, Location +``` + +> [!WARNING] +> Stop if either displayed context isn't the Hack subscription or if the resource group isn't your `MHBox--destination-rg`. + +Create the Windows plan and app: + +```powershell +$suffix = [guid]::NewGuid().ToString('N').Substring(0, 10) +$planName = "asp-mh-win-$suffix" +$appName = "mh-web-win-$suffix" az appservice plan create ` --name $planName ` --resource-group $destinationRg ` --location $location ` - --sku B1 + --sku B1 ` + --is-linux false az webapp create ` --name $appName ` --resource-group $destinationRg ` - --plan $planName - -az webapp update ` - --name $appName ` - --resource-group $destinationRg ` + --plan $planName ` --https-only true az webapp show ` @@ -236,81 +326,241 @@ Write-Host "HTTPS URL: https://$appName.azurewebsites.net" Record `$planName` and `$appName`. -### Track B - Install Azure CLI if needed and sign in +### Path B - Install Azure CLI if needed and verify scope -Run from the migrated Ubuntu VM. First check for Azure CLI: +Run from the migrated Ubuntu VM. Replace both placeholders. The block installs Azure CLI when it is missing or unhealthy, then signs in and verifies the exact scope: ```bash -if command -v az >/dev/null 2>&1; then - az version -else - curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash - az version -fi -``` +if bash /dev/fd/3 3<<'PATH_B_SCOPE' +set -euo pipefail -The conditional uses the current Microsoft-supported Debian/Ubuntu installation script only when `az` is missing. +subscription_id='' +destination_rg='MHBox--destination-rg' +installer_path="$(mktemp)" -Sign in with device code and verify the active subscription: +cleanup_installer() { + local status=$? + trap - EXIT + rm -f "${installer_path}" || true + return "${status}" +} +trap cleanup_installer EXIT -```bash -az login --use-device-code -az account list --output table +if [[ -z "${subscription_id}" || + "${subscription_id}" == '' ]]; then + echo 'Replace subscription_id with the Hack subscription ID.' >&2 + false +fi +if [[ -z "${destination_rg}" || + "${destination_rg}" == *'<'* || + "${destination_rg}" == *'>'* ]]; then + echo 'Replace destination_rg with the exact destination resource-group name.' >&2 + false +fi + +if ! command -v az >/dev/null 2>&1 || + ! az version >/dev/null 2>&1; then + installer_curl_args=(--fail --silent --show-error --location --output "${installer_path}") + curl "${installer_curl_args[@]}" 'https://aka.ms/InstallAzureCLIDeb' + sudo bash "${installer_path}" +fi +az version -read -r -p "Enter the target subscription ID: " subscription_id +az login --use-device-code az account set --subscription "${subscription_id}" -az account show \ - --query '{subscription:name,subscriptionId:id,tenantId:tenantId,user:user.name}' \ - --output table +account_show_args=(account show --query '{subscription:name,subscriptionId:id,tenantId:tenantId,user:user.name}' --output table) +az "${account_show_args[@]}" + +active_subscription_id="$(az account show --query id --output tsv)" +destination_rg="$(az group show --name "${destination_rg}" --query name --output tsv)" +location="$(az group show --name "${destination_rg}" --query location --output tsv)" +if [[ -z "${location}" ]]; then + echo "Could not resolve ${destination_rg} in the active subscription." >&2 + false +fi + +name_suffix="$( + printf '%s\0%s' "${active_subscription_id}" "${destination_rg}" | + sha256sum | + cut -c1-12 +)" +plan_name="asp-mh-linux-${name_suffix}" +app_name="mh-web-linux-${name_suffix}" + +printf 'Destination resource group: %s (%s)\nPlan name: %s\nWeb app name: %s\n' "${destination_rg}" "${location}" "${plan_name}" "${app_name}" +PATH_B_SCOPE +then + echo 'Path B scope verification completed.' +else + path_b_status=$? + printf 'Path B step failed; your SSH session remains open (status %d).\n' "${path_b_status}" >&2 + echo 'Correct the error and rerun this block before continuing.' >&2 +fi ``` -Complete the sign-in in the browser using the displayed code. Do not paste passwords, tokens, or publishing credentials into the shell or lab notes. +Complete the sign-in in the browser using the displayed code. Do not paste passwords, tokens, or publishing credentials into the shell or lab notes. The stable 12-character suffix is derived only from Azure's validated subscription ID and canonical destination resource-group name; it contains no secret and is reproduced by every later block. -Create the target: +> [!WARNING] +> Stop if the displayed subscription, tenant, or user doesn't identify the Hack subscription, or if the resource group isn't your `MHBox--destination-rg`. + +Resolve the newest advertised Node.js LTS runtime and create a Linux target: ```bash +if bash /dev/fd/3 3<<'PATH_B_CREATE' set -euo pipefail -destination_rg="destination-rg" -location="$(az group show \ - --name "${destination_rg}" \ - --query location \ - --output tsv)" -suffix="$(date +%s)-${RANDOM}" -plan_name="asp-mh-replatform-${suffix}" -app_name="mh-web-${suffix}" - -az appservice plan create \ - --name "${plan_name}" \ - --resource-group "${destination_rg}" \ - --location "${location}" \ - --sku B1 - -az webapp create \ - --name "${app_name}" \ - --resource-group "${destination_rg}" \ - --plan "${plan_name}" - -az webapp update \ - --name "${app_name}" \ - --resource-group "${destination_rg}" \ - --https-only true - -az webapp show \ - --name "${app_name}" \ - --resource-group "${destination_rg}" \ - --query '{name:name,host:defaultHostName,httpsOnly:httpsOnly,state:state}' \ - --output table - -printf 'Plan name: %s\nWeb app name: %s\nHTTPS URL: https://%s.azurewebsites.net\n' \ - "${plan_name}" "${app_name}" "${app_name}" +subscription_id='' +destination_rg='MHBox--destination-rg' + +if [[ -z "${subscription_id}" || + "${subscription_id}" == '' ]]; then + echo 'Replace subscription_id with the Hack subscription ID.' >&2 + false +fi +if [[ -z "${destination_rg}" || + "${destination_rg}" == *'<'* || + "${destination_rg}" == *'>'* ]]; then + echo 'Replace destination_rg with the exact destination resource-group name.' >&2 + false +fi + +command -v az >/dev/null +az account set --subscription "${subscription_id}" +active_subscription_id="$(az account show --query id --output tsv)" +destination_rg="$(az group show --name "${destination_rg}" --query name --output tsv)" +location="$(az group show --name "${destination_rg}" --query location --output tsv)" + +name_suffix="$( + printf '%s\0%s' "${active_subscription_id}" "${destination_rg}" | + sha256sum | + cut -c1-12 +)" +plan_name="asp-mh-linux-${name_suffix}" +app_name="mh-web-linux-${name_suffix}" +startup_command='pm2 serve /home/site/wwwroot $PORT --no-daemon' + +runtime_list_args=(webapp list-runtimes --os-type linux --runtime node --output json) +runtime_raw="$(az "${runtime_list_args[@]}")" +if ! command -v python3 >/dev/null 2>&1; then + echo 'python3 is required to parse the Azure CLI runtime response.' >&2 + false +fi +runtime_candidates="$( + MH_RUNTIME_OUTPUT="${runtime_raw}" python3 <<'PY_RUNTIME' +import json +import os +import re + +raw = os.environ["MH_RUNTIME_OUTPUT"] +pattern = re.compile( + r"(?i)(?-lts Linux runtime was returned.' >&2 + echo 'Raw Azure CLI runtime response:' >&2 + printf '%s\n' "${runtime_raw}" >&2 + false +fi +runtime="${runtime_candidates%%$'\n'*}" +printf 'Discovered Node LTS runtimes, normalized for az webapp create:\n%s\n' "${runtime_candidates}" +printf 'Selected runtime: %s\n' "${runtime}" + +plan_list_args=(appservice plan list --resource-group "${destination_rg}" --query "[?name=='${plan_name}'].id | [0]" --output tsv) +plan_id="$(az "${plan_list_args[@]}")" +if [[ -z "${plan_id}" ]]; then + plan_create_args=(appservice plan create --name "${plan_name}" --resource-group "${destination_rg}" --location "${location}" --sku B1 --is-linux true --output none) + az "${plan_create_args[@]}" + plan_id="$(az appservice plan show --name "${plan_name}" --resource-group "${destination_rg}" --query id --output tsv)" + echo "Created Linux App Service plan: ${plan_name}" +else + plan_is_linux="$(az appservice plan show --name "${plan_name}" --resource-group "${destination_rg}" --query reserved --output tsv)" + plan_location="$(az appservice plan show --name "${plan_name}" --resource-group "${destination_rg}" --query location --output tsv)" + plan_sku="$(az appservice plan show --name "${plan_name}" --resource-group "${destination_rg}" --query sku.name --output tsv)" + normalized_plan_location="${plan_location// /}" + normalized_location="${location// /}" + if [[ "${plan_is_linux,,}" != 'true' || + "${normalized_plan_location,,}" != "${normalized_location,,}" || + "${plan_sku^^}" != 'B1' ]]; then + echo "Existing plan ${plan_name} isn't a compatible Linux B1 plan in ${location}." >&2 + false + fi + echo "Reusing compatible Linux App Service plan: ${plan_name}" +fi + +app_list_args=(webapp list --resource-group "${destination_rg}" --query "[?name=='${app_name}'].id | [0]" --output tsv) +app_id="$(az "${app_list_args[@]}")" +if [[ -z "${app_id}" ]]; then + app_create_args=(webapp create --name "${app_name}" --resource-group "${destination_rg}" --plan "${plan_name}" --runtime "${runtime}" --https-only true --output none) + az "${app_create_args[@]}" + echo "Created Linux web app: ${app_name}" +else + existing_plan_id="$(az webapp show --name "${app_name}" --resource-group "${destination_rg}" --query serverFarmId --output tsv)" + existing_https_only="$(az webapp show --name "${app_name}" --resource-group "${destination_rg}" --query httpsOnly --output tsv)" + existing_runtime="$(az webapp config show --name "${app_name}" --resource-group "${destination_rg}" --query linuxFxVersion --output tsv)" + existing_runtime_canonical="$(printf '%s' "${existing_runtime/|/:}" | tr '[:upper:]' '[:lower:]')" + supported_runtime_candidates="$(printf '%s\n' "${runtime_candidates}" | tr '[:upper:]' '[:lower:]')" + if [[ "${existing_plan_id,,}" != "${plan_id,,}" || + "${existing_https_only,,}" != 'true' || + -z "${existing_runtime_canonical}" ]] || + ! grep -Fxq "${existing_runtime_canonical}" <<<"${supported_runtime_candidates}"; then + echo "Existing app ${app_name} isn't compatible with the intended plan, HTTPS setting, or supported Node LTS runtimes." >&2 + false + fi + echo "Reusing compatible Linux web app: ${app_name} (${existing_runtime})" + runtime="${existing_runtime_canonical}" +fi + +appsettings_args=(webapp config appsettings set --resource-group "${destination_rg}" --name "${app_name}" --settings SCM_DO_BUILD_DURING_DEPLOYMENT=false --output none) +az "${appsettings_args[@]}" +startup_args=(webapp config set --resource-group "${destination_rg}" --name "${app_name}" --startup-file "${startup_command}" --output none) +az "${startup_args[@]}" +app_show_args=(webapp show --name "${app_name}" --resource-group "${destination_rg}" --query '{name:name,host:defaultHostName,httpsOnly:httpsOnly,state:state}' --output table) +az "${app_show_args[@]}" +config_show_args=(webapp config show --name "${app_name}" --resource-group "${destination_rg}" --query '{runtime:linuxFxVersion,startup:appCommandLine}' --output table) +az "${config_show_args[@]}" + +printf 'Runtime: %s\nPlan name: %s\nWeb app name: %s\nHTTPS URL: https://%s.azurewebsites.net\n' "${runtime}" "${plan_name}" "${app_name}" "${app_name}" +PATH_B_CREATE +then + echo 'Path B resource creation completed.' +else + path_b_status=$? + printf 'Path B step failed; your SSH session remains open (status %d).\n' "${path_b_status}" >&2 + echo 'Correct the error and rerun this block before continuing.' >&2 +fi ``` -The omitted `--is-linux` flag creates a Windows plan. `B1` is the lowest Basic dedicated tier and incurs charges until the plan is deleted. +If runtime discovery stopped this step on an earlier attempt, paste and run the entire corrected block again with the same subscription ID and destination resource group. Its stable names reconcile compatible existing resources instead of creating duplicates. Continue only after the block reports success. + +`az webapp list-runtimes` is the source of truth for currently available built-in runtimes. The block chooses the highest advertised Node.js LTS major version for a new app and fails rather than inventing a fallback. On rerun, it reuses only the same Linux B1 plan and HTTPS-enabled app when their plan association and Node LTS runtime remain compatible. PM2 runs in the foreground, listens on the App Service-provided `$PORT`, and serves the ready-to-run static ZIP from `/home/site/wwwroot`. `B1` incurs charges until the plan is deleted. ## Task 4: Deploy with supported App Service ZIP deployment (5 minutes) -### Track A - Kudu ZIP deploy +### Path A - Kudu ZIP deploy Keep the browser on the Windows VM so it can access `C:\temp\microhack-app.zip`. @@ -330,34 +580,129 @@ az webapp deploy ` --type zip ``` -### Track B - Deploy the local ZIP +Windows ZIP deployment extracts the package into `D:\home\site\wwwroot`. + +### Path B - Deploy the local ZIP -The variables from Task 3 remain in the same Bash session: +Replace both placeholders. This block reconstructs the package, plan, and app values and confirms the packaged content again before deployment: ```bash +if bash /dev/fd/3 3<<'PATH_B_DEPLOY' +set -euo pipefail + +subscription_id='' +destination_rg='MHBox--destination-rg' package_path="${HOME}/microhack-app.zip" -az webapp deploy \ - --resource-group "${destination_rg}" \ - --name "${app_name}" \ - --src-path "${package_path}" \ - --type zip +if [[ -z "${subscription_id}" || + "${subscription_id}" == '' ]]; then + echo 'Replace subscription_id with the Hack subscription ID.' >&2 + false +fi +if [[ -z "${destination_rg}" || + "${destination_rg}" == *'<'* || + "${destination_rg}" == *'>'* ]]; then + echo 'Replace destination_rg with the exact destination resource-group name.' >&2 + false +fi +if [[ ! -f "${package_path}" ]]; then + echo "Package not found: ${package_path}" >&2 + false +fi + +archive_entries="$(unzip -Z1 "${package_path}")" +for asset in index.html stylesheet.css GitHub_Logo.png MSLogo.png MSicon.png; do + if ! grep -Fxq "${asset}" <<<"${archive_entries}"; then + echo "Required asset is missing from the ZIP root: ${asset}" >&2 + false + fi +done +archive_html="$(unzip -p "${package_path}" index.html)" +metadata_checks=( + 'hostname|Azure App Service' + 'platform|Managed PaaS' + 'web-server|Azure App Service' +) +for check in "${metadata_checks[@]}"; do + field="${check%%|*}" + expected_value="${check#*|}" + if ! grep -Eq "data-workload-value=\"${field}\"[^>]*>${expected_value}" <<<"${archive_html}"; then + echo "Expected App Service metadata is missing from the ZIP: ${field}" >&2 + false + fi +done +if grep -Eq '<(HOSTNAME|PLATFORM|WEB_SERVER)>' <<<"${archive_html}"; then + echo 'The ZIP still contains unresolved workload template tokens.' >&2 + false +fi + +command -v az >/dev/null +az account set --subscription "${subscription_id}" +active_subscription_id="$(az account show --query id --output tsv)" +destination_rg="$(az group show --name "${destination_rg}" --query name --output tsv)" +name_suffix="$( + printf '%s\0%s' "${active_subscription_id}" "${destination_rg}" | + sha256sum | + cut -c1-12 +)" +plan_name="asp-mh-linux-${name_suffix}" +app_name="mh-web-linux-${name_suffix}" +app_list_args=(webapp list --resource-group "${destination_rg}" --query "[?name=='${app_name}'].id | [0]" --output tsv) +app_id="$(az "${app_list_args[@]}")" +if [[ -z "${app_id}" ]]; then + echo "Web app ${app_name} doesn't exist. Complete Task 3 first." >&2 + false +fi + +printf 'Plan name: %s\nWeb app name: %s\nPackage: %s\n' "${plan_name}" "${app_name}" "${package_path}" + +deploy_args=(webapp deploy --resource-group "${destination_rg}" --name "${app_name}" --src-path "${package_path}" --type zip) +az "${deploy_args[@]}" +PATH_B_DEPLOY +then + echo 'Path B ZIP deployment completed.' +else + path_b_status=$? + printf 'Path B step failed; your SSH session remains open (status %d).\n' "${path_b_status}" >&2 + echo 'Correct the error and rerun this block before continuing.' >&2 +fi ``` -App Service extracts the package into `D:\home\site\wwwroot`. Do not configure Deployment Center source control; neither track requires GitHub or Azure DevOps integration. +Linux ZIP deployment extracts the package into `/home/site/wwwroot`. Build automation remains disabled because the ZIP is already complete and PM2 serves it directly. Do not configure Deployment Center source control; neither path requires GitHub or Azure DevOps integration. ## Task 5: Validate HTTPS, assets, and independence (5 minutes) -### Track A - Validate and stop IIS +### Path A - Validate and stop IIS Run in Windows PowerShell, replacing the placeholder: ```powershell $appName = '' $appUrl = "https://$appName.azurewebsites.net" -$home = Invoke-WebRequest -Uri "$appUrl/" -UseBasicParsing -TimeoutSec 30 -$home.StatusCode -$home.Content -match 'data-workload-value="platform"[^>]*>Managed PaaS' +$home = $null +foreach ($attempt in 1..18) { + try { + $home = Invoke-WebRequest -Uri "$appUrl/" -UseBasicParsing -TimeoutSec 30 + break + } + catch { + Start-Sleep -Seconds 10 + } +} +if ($null -eq $home -or $home.StatusCode -ne 200) { + throw 'The App Service home page did not become ready within three minutes.' +} + +$requiredMetadata = @( + 'data-workload-value="hostname"[^>]*>Azure App Service' + 'data-workload-value="platform"[^>]*>Managed PaaS' + 'data-workload-value="web-server"[^>]*>Azure App Service' +) +foreach ($pattern in $requiredMetadata) { + if ($home.Content -notmatch $pattern) { + throw "The App Service page is missing expected metadata: $pattern" + } +} $assets = @('stylesheet.css', 'GitHub_Logo.png', 'MSLogo.png', 'MSicon.png') foreach ($asset in $assets) { @@ -369,54 +714,153 @@ foreach ($asset in $assets) { } } -Stop-Service -Name W3SVC -Force +try { + Stop-Service -Name W3SVC -Force + Get-Service -Name W3SVC + $independentResponse = Invoke-WebRequest ` + -Uri $appUrl ` + -UseBasicParsing ` + -TimeoutSec 30 + if ($independentResponse.StatusCode -ne 200) { + throw 'App Service failed while IIS was stopped.' + } +} +finally { + Set-Service -Name W3SVC -StartupType Automatic + Start-Service -Name W3SVC +} Get-Service -Name W3SVC -(Invoke-WebRequest -Uri $appUrl -UseBasicParsing -TimeoutSec 30).StatusCode ``` -`W3SVC` must be `Stopped` while App Service still returns `200`. To preserve the original site after the proof, run: +`W3SVC` must be stopped during the independent App Service check and running again afterward. -```powershell -Start-Service -Name W3SVC -``` +### Path B - Validate and stop Apache -### Track B - Validate and stop Apache +Replace both placeholders. Readiness is bounded to approximately three minutes: at most 18 five-second requests with up to 17 five-second waits. ```bash +if bash /dev/fd/3 3<<'PATH_B_VALIDATE' +set -euo pipefail + +subscription_id='' +destination_rg='MHBox--destination-rg' + +if [[ -z "${subscription_id}" || + "${subscription_id}" == '' ]]; then + echo 'Replace subscription_id with the Hack subscription ID.' >&2 + false +fi +if [[ -z "${destination_rg}" || + "${destination_rg}" == *'<'* || + "${destination_rg}" == *'>'* ]]; then + echo 'Replace destination_rg with the exact destination resource-group name.' >&2 + false +fi + +command -v az >/dev/null +az account set --subscription "${subscription_id}" +active_subscription_id="$(az account show --query id --output tsv)" +destination_rg="$(az group show --name "${destination_rg}" --query name --output tsv)" +name_suffix="$( + printf '%s\0%s' "${active_subscription_id}" "${destination_rg}" | + sha256sum | + cut -c1-12 +)" +plan_name="asp-mh-linux-${name_suffix}" +app_name="mh-web-linux-${name_suffix}" app_url="https://${app_name}.azurewebsites.net" +printf 'Plan name: %s\nWeb app name: %s\nHTTPS URL: %s\n' "${plan_name}" "${app_name}" "${app_url}" + +home_html='' +readiness_curl_args=(--fail --silent --show-error --max-time 5) +for ((attempt = 1; attempt <= 18; attempt++)); do + if home_html="$(curl "${readiness_curl_args[@]}" "${app_url}/")"; then + break + fi + if (( attempt < 18 )); then + sleep 5 + fi +done +if [[ -z "${home_html}" ]]; then + echo 'The App Service home page did not become ready within approximately three minutes.' >&2 + false +fi -curl --fail --silent --show-error "${app_url}/" | - grep -E 'data-workload-value="platform"[^>]*>Managed PaaS' +expected_metadata=( + 'data-workload-value="hostname"[^>]*>Azure App Service' + 'data-workload-value="platform"[^>]*>Managed PaaS' + 'data-workload-value="web-server"[^>]*>Azure App Service' +) +for expected in "${expected_metadata[@]}"; do + grep -Eq "${expected}" <<<"${home_html}" || { + echo "The App Service page is missing expected metadata: ${expected}" >&2 + false + } +done for asset in stylesheet.css GitHub_Logo.png MSLogo.png MSicon.png; do - curl --fail --silent --show-error --output /dev/null \ - --write-out "${asset}: HTTP %{http_code}, %{size_download} bytes\n" \ - "${app_url}/${asset}" + asset_curl_args=(--fail --silent --show-error --output /dev/null --write-out "${asset}: HTTP %{http_code}, %{size_download} bytes\n") + curl "${asset_curl_args[@]}" "${app_url}/${asset}" done -az webapp show \ - --resource-group "${destination_rg}" \ - --name "${app_name}" \ - --query '{state:state,httpsOnly:httpsOnly,host:defaultHostName}' \ - --output table +app_show_args=(webapp show --resource-group "${destination_rg}" --name "${app_name}" --query '{state:state,httpsOnly:httpsOnly,host:defaultHostName}' --output table) +az "${app_show_args[@]}" -sudo systemctl stop apache2 -sudo systemctl is-active apache2 || true +if ! sudo systemctl is-active --quiet apache2; then + echo 'Apache must be running before the source-independence test.' >&2 + false +fi -curl --fail --silent --show-error --output /dev/null \ - --write-out 'App Service after Apache stop: HTTP %{http_code}\n' \ - "${app_url}/" -``` +apache_may_be_stopped=0 +restore_apache_once() { + if (( ! apache_may_be_stopped )); then + return 0 + fi + if sudo systemctl start apache2 >/dev/null && + sudo systemctl is-active --quiet apache2; then + apache_may_be_stopped=0 + return 0 + fi + apache_may_be_stopped=0 + echo 'Apache restoration failed; start apache2 manually before continuing.' >&2 + return 1 +} +cleanup_validation() { + local status=$? + trap - EXIT + if (( apache_may_be_stopped )) && + ! restore_apache_once; then + status=1 + fi + return "${status}" +} +trap cleanup_validation EXIT -`apache2` must be inactive while App Service still returns `200`. To preserve the original site after the proof: +apache_may_be_stopped=1 +sudo systemctl stop apache2 +if sudo systemctl is-active --quiet apache2; then + echo 'Apache is still running after the stop command.' >&2 + false +fi +independence_curl_args=(--fail --silent --show-error --output /dev/null --max-time 30 --write-out 'App Service after Apache stop: HTTP %{http_code}\n') +curl "${independence_curl_args[@]}" "${app_url}/" -```bash -sudo systemctl enable apache2 -sudo systemctl start apache2 +restore_apache_once +trap - EXIT sudo systemctl is-active apache2 +PATH_B_VALIDATE +then + echo 'Path B validation and source restoration completed.' +else + path_b_status=$? + printf 'Path B step failed; your SSH session remains open (status %d).\n' "${path_b_status}" >&2 + echo 'Correct the error and rerun this block before continuing.' >&2 +fi ``` -For both tracks, visually check the HTTPS page and assets. App Service has its own public endpoint and is not a backend of the Azure Load Balancer used for the migrated VMs. +The child-shell trap makes one restoration attempt if any command fails after Apache might have stopped. It clears itself before reporting a restoration failure, so it cannot recurse. `apache2` must be inactive during the App Service check and active again afterward. + +For both paths, visually check the HTTPS page and assets. App Service has its own public endpoint and remains independent of the migrated VM networking. ## Task 6: Record the architecture decision and clean up (3 minutes) @@ -432,9 +876,9 @@ Document which target you would choose and why. ### Optional resource cleanup -Do not delete resources automatically. If the lab owner approves cleanup, delete the web app and plan with the variables from your track's shell: +Do not delete resources automatically. If the lab owner approves cleanup, delete the web app and plan. Path B reconstructs its stable names from the validated subscription and resource group: -Track A, Azure Cloud Shell PowerShell: +Path A, Azure Cloud Shell PowerShell: ```powershell az webapp delete ` @@ -447,28 +891,109 @@ az appservice plan delete ` --yes ``` -Track B, Bash: +Path B, Bash: ```bash -az webapp delete \ - --resource-group "${destination_rg}" \ - --name "${app_name}" - -az appservice plan delete \ - --resource-group "${destination_rg}" \ - --name "${plan_name}" \ - --yes +if bash /dev/fd/3 3<<'PATH_B_CLEANUP' +set -euo pipefail + +subscription_id='' +destination_rg='MHBox--destination-rg' + +if [[ -z "${subscription_id}" || + "${subscription_id}" == '' ]]; then + echo 'Replace subscription_id with the Hack subscription ID.' >&2 + false +fi +if [[ -z "${destination_rg}" || + "${destination_rg}" == *'<'* || + "${destination_rg}" == *'>'* ]]; then + echo 'Replace destination_rg with the exact destination resource-group name.' >&2 + false +fi + +command -v az >/dev/null +az account set --subscription "${subscription_id}" +active_subscription_id="$(az account show --query id --output tsv)" +destination_rg="$(az group show --name "${destination_rg}" --query name --output tsv)" +name_suffix="$( + printf '%s\0%s' "${active_subscription_id}" "${destination_rg}" | + sha256sum | + cut -c1-12 +)" +plan_name="asp-mh-linux-${name_suffix}" +app_name="mh-web-linux-${name_suffix}" +printf 'Plan name: %s\nWeb app name: %s\n' "${plan_name}" "${app_name}" + +cleanup_failed=0 +app_list_args=(webapp list --resource-group "${destination_rg}" --query "[?name=='${app_name}'].id | [0]" --output tsv) +app_id="$(az "${app_list_args[@]}")" +if [[ -n "${app_id}" ]]; then + app_delete_args=(webapp delete --resource-group "${destination_rg}" --name "${app_name}") + if ! az "${app_delete_args[@]}"; then + echo "Could not delete web app ${app_name}." >&2 + cleanup_failed=1 + fi +else + echo "Web app is already absent: ${app_name}" +fi + +plan_list_args=(appservice plan list --resource-group "${destination_rg}" --query "[?name=='${plan_name}'].id | [0]" --output tsv) +plan_id="$(az "${plan_list_args[@]}")" +if [[ -n "${plan_id}" ]]; then + plan_delete_args=(appservice plan delete --resource-group "${destination_rg}" --name "${plan_name}" --yes) + if ! az "${plan_delete_args[@]}"; then + echo "Could not delete App Service plan ${plan_name}." >&2 + cleanup_failed=1 + fi +else + echo "App Service plan is already absent: ${plan_name}" +fi + +if (( cleanup_failed )); then + false +fi +PATH_B_CLEANUP +then + echo 'Path B resource cleanup completed.' +else + path_b_status=$? + printf 'Path B step failed; your SSH session remains open (status %d).\n' "${path_b_status}" >&2 + echo 'Correct the error and rerun this block before continuing.' >&2 +fi ``` Deleting only the web app does not stop App Service plan charges. -### Track B - Sign out +### Path B - Sign out Whether or not you delete the resources, remove the interactive Azure CLI session from the migrated VM: ```bash -az logout -az account clear +if bash /dev/fd/3 3<<'PATH_B_SIGN_OUT' +set -euo pipefail + +command -v az >/dev/null +sign_out_failed=0 +if ! az logout; then + echo 'Azure CLI logout reported an error.' >&2 + sign_out_failed=1 +fi +if ! az account clear; then + echo 'Azure CLI account-cache cleanup reported an error.' >&2 + sign_out_failed=1 +fi +if (( sign_out_failed )); then + false +fi +PATH_B_SIGN_OUT +then + echo 'Path B Azure CLI sign-out completed.' +else + path_b_status=$? + printf 'Path B step failed; your SSH session remains open (status %d).\n' "${path_b_status}" >&2 + echo 'Correct the error and rerun this block before continuing.' >&2 +fi ``` Interactive sign-in is appropriate for this guided lab. Signing out and clearing the local subscription cache reduce credential exposure on the VM. No publishing credentials were used or exposed.