diff --git a/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/Readme.md b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/Readme.md index f5aca9894..634bba53f 100644 --- a/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/Readme.md +++ b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/Readme.md @@ -8,7 +8,7 @@ * [**Objectives**](#objectives) * [**General Prerequisites**](#general-prerequisites) * [**MicroHack Challenges**](#microhack-challenges) - * [Challenge 01 - Onboarding your Kubernetes Cluster](challenges/challenge-01.md)) + * [Challenge 01 - Onboarding your Kubernetes Cluster](challenges/challenge-01.md) * [Challenge 02 - Enable Azure Monitor for Containers](challenges/challenge-02.md) * [Challenge 03 - Deploy CPU based Large & Small Language Models (LLM/SLM) on Azure Arc-enabled Kubernetes](challenges/challenge-03.md) * [Challenge 04 - Deploy SQL Managed Instance](challenges/challenge-04.md) diff --git a/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/challenges/challenge-01.md b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/challenges/challenge-01.md index 13c086b33..1ce8fa719 100644 --- a/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/challenges/challenge-01.md +++ b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/challenges/challenge-01.md @@ -7,7 +7,7 @@ In challenge 1 you will connect/onboard your existing K8s cluster to Azure Arc. * Verify all prerequisites are in place * Resource Providers * Azure CLI extensions - * Resource group (Name: mh-arc-k8s-) + * Resource group (Name: -k8s-arc) * Connectivity to required Azure endpoints * Deploy the Azure Arc agent pods to your k8s cluster * Assign permissions to view k8s resources in the Azure portal @@ -18,13 +18,14 @@ In challenge 1 you will connect/onboard your existing K8s cluster to Azure Arc. ## Learning Resources * (https://learn.microsoft.com/en-us/azure/azure-arc/kubernetes/overview) -* (https://learn.microsoft.com/en-us/azure/azure-arc/kubernetes/quickstart-connect-cluster?tabs=azure-cli) +* (https://learn.microsoft.com/en-us/azure/azure-arc/kubernetes/quickstart-connect-cluster) * (https://learn.microsoft.com/en-us/azure/azure-arc/kubernetes/cluster-connect) +* (https://learn.microsoft.com/en-us/azure/azure-arc/kubernetes/arc-gateway-simplify-networking) * (https://learn.microsoft.com/en-us/azure/azure-arc/kubernetes/azure-rbac) * (https://learn.microsoft.com/en-us/azure/azure-arc/kubernetes/kubernetes-resource-view) * (https://learn.microsoft.com/en-us/cli/azure/connectedk8s?view=azure-cli-latest) ## Solution - Spoilerwarning -[Solution Steps](../walkthroughs/challenge-01/solution.md) +[Solution Steps](../walkthrough/challenge-01/solution.md) [Next challenge](challenge-02.md) | [Back](../Readme.md) \ No newline at end of file diff --git a/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/challenges/challenge-02.md b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/challenges/challenge-02.md index c623a54a1..033183bea 100644 --- a/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/challenges/challenge-02.md +++ b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/challenges/challenge-02.md @@ -32,6 +32,6 @@ All telemetry, logs, and security signals generated by these services flow into * https://learn.microsoft.com/en-us/azure/defender-for-cloud/defender-for-containers-arc-enable-programmatically ## Solution - Spoilerwarning -[Solution Steps](../walkthroughs/challenge-02/solution.md) +[Solution Steps](../walkthrough/challenge-02/solution.md) [Next challenge](challenge-03.md) | [Back](../Readme.md) \ No newline at end of file diff --git a/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/challenges/challenge-03.md b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/challenges/challenge-03.md index 5e0af10bd..0114fcb3b 100644 --- a/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/challenges/challenge-03.md +++ b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/challenges/challenge-03.md @@ -20,6 +20,6 @@ In challenge 3 you will deploy a LLM/SLM to your Azure Arc-enabled Kubernetes Cl * (https://github.com/kaito-project/kaito) GPU Based LLM/SLM ### Solution - Spoilerwarning -[Solution Steps](../walkthroughs/challenge-03/solution.md) +[Solution Steps](../walkthrough/challenge-03/solution.md) [Next challenge](challenge-04.md) | [Back](../Readme.md) \ No newline at end of file diff --git a/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/challenges/challenge-04.md b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/challenges/challenge-04.md index 6cd12e6c1..c2230721c 100644 --- a/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/challenges/challenge-04.md +++ b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/challenges/challenge-04.md @@ -25,6 +25,6 @@ In challenge 4 you will deploy Azure Arc-enabled Data Services to your cluster a * [Azure Arc Data Controller deployment](https://learn.microsoft.com/en-us/azure/azure-arc/data/create-data-controller) ## Solution - Spoilerwarning -[Solution Steps](../walkthroughs/challenge-04/solution.md) +[Solution Steps](../walkthrough/challenge-04/solution.md) [Next challenge](challenge-05.md) | [Back](../Readme.md) \ No newline at end of file diff --git a/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/challenges/challenge-05.md b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/challenges/challenge-05.md index fe0fa78d8..0c603ab1e 100644 --- a/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/challenges/challenge-05.md +++ b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/challenges/challenge-05.md @@ -21,6 +21,6 @@ Configure GitOps with Flux for cluster management. In this microhack we chose to * [Flux documentation - Get started](https://fluxcd.io/docs/get-started/) ## Solution - Spoilerwarning -[Solution Steps](../walkthroughs/challenge-05/solution.md) +[Solution Steps](../walkthrough/challenge-05/solution.md) -[Next challenge](challenge-06.md) | [Back](../Readme.md) +[Back](../Readme.md) diff --git a/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/ansible/.gitignore b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/ansible/.gitignore new file mode 100644 index 000000000..1e3a3b14c --- /dev/null +++ b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/ansible/.gitignore @@ -0,0 +1,4 @@ +# Generated by Terraform — environment-specific +inventory.yml +open-bastion-tunnels.sh +.bastion-tunnel-pids diff --git a/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/ansible/inventory.yml.tpl b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/ansible/inventory.yml.tpl new file mode 100644 index 000000000..4c2567c55 --- /dev/null +++ b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/ansible/inventory.yml.tpl @@ -0,0 +1,19 @@ +# Ansible connects via Bastion tunnels to localhost with per-VM ports. +# Run open-bastion-tunnels.sh BEFORE running the playbook. +all: + children: + workstations: + hosts: +%{ for i, idx in indices ~} + workstation-${format("%02d", idx)}: + ansible_host: 127.0.0.1 + ansible_winrm_port: ${55986 + i} + bastion_vm_id: ${workstation_vm_ids[i]} + private_ip: ${workstation_ips[i]} +%{ endfor ~} + vars: + ansible_user: ${admin_user} + ansible_connection: winrm + ansible_winrm_scheme: https + ansible_winrm_transport: basic + ansible_winrm_server_cert_validation: ignore diff --git a/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/ansible/open-bastion-tunnels.sh.tpl b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/ansible/open-bastion-tunnels.sh.tpl new file mode 100644 index 000000000..132aa0a6b --- /dev/null +++ b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/ansible/open-bastion-tunnels.sh.tpl @@ -0,0 +1,56 @@ +#!/bin/bash +# Auto-generated by Terraform — opens Bastion tunnels for all workstations. +# Each tunnel forwards remote WinRM port 5986 to a unique local port. +# Run this BEFORE running the Ansible playbook. +# +# Usage: ./open-bastion-tunnels.sh +# Stop: ./open-bastion-tunnels.sh --stop + +set -e + +BASTION_NAME="${bastion_name}" +BASTION_RG="${bastion_rg}" +PIDS_FILE="$(dirname "$0")/.bastion-tunnel-pids" + +if [[ "$1" == "--stop" ]]; then + echo "Stopping Bastion tunnels..." + # Kill tracked PIDs + if [[ -f "$PIDS_FILE" ]]; then + while read -r pid; do + kill "$pid" 2>/dev/null && echo " Stopped tunnel (PID $pid)" || true + done < "$PIDS_FILE" + rm -f "$PIDS_FILE" + fi + # Also kill any orphaned bastion tunnel processes for this bastion + pkill -f "az network bastion tunnel --name $BASTION_NAME" 2>/dev/null && echo " Cleaned up orphaned tunnel processes" || true + exit 0 +fi + +# Kill any leftover tunnels before starting new ones +pkill -f "az network bastion tunnel --name $BASTION_NAME" 2>/dev/null || true +sleep 1 + +echo "Opening Bastion tunnels (Bastion: $BASTION_NAME in $BASTION_RG)..." +echo "" + +> "$PIDS_FILE" + +%{ for i, idx in indices ~} +echo " workstation-${format("%02d", idx)}: localhost:${55986 + i} -> ${workstation_ips[i]}:5986" +az network bastion tunnel \ + --name "$BASTION_NAME" \ + --resource-group "$BASTION_RG" \ + --target-resource-id "${workstation_vm_ids[i]}" \ + --resource-port 5986 \ + --port ${55986 + i} & +echo $! >> "$PIDS_FILE" + +%{ endfor ~} + +echo "" +echo "All tunnels started. Waiting for tunnels to establish..." +sleep 10 +echo "Ready. Run the Ansible playbook now:" +echo " ansible-playbook -i inventory.yml playbook-workstation.yml --extra-vars \"ansible_password=\"" +echo "" +echo "To stop tunnels later: $0 --stop" diff --git a/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/ansible/playbook-workstation.yml b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/ansible/playbook-workstation.yml new file mode 100644 index 000000000..0132113c4 --- /dev/null +++ b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/ansible/playbook-workstation.yml @@ -0,0 +1,124 @@ +--- +# Ansible playbook to configure Windows 11 workstation VMs with all +# MicroHack prerequisites: VS Code, Git, WSL2 + Ubuntu, Azure CLI, +# kubectl, and Helm. +# +# Usage: +# ansible-galaxy collection install -r requirements.yml +# ./open-bastion-tunnels.sh +# ansible-playbook -i inventory.yml playbook-workstation.yml \ +# --extra-vars "ansible_password=" +# ./open-bastion-tunnels.sh --stop + +- name: Configure Windows 11 Workstation for MicroHack + hosts: workstations + gather_facts: true + + tasks: + # ── Windows tools via Chocolatey ────────────────────────────────── + + - name: Install Git + chocolatey.chocolatey.win_chocolatey: + name: git + state: present + + - name: Install Visual Studio Code + chocolatey.chocolatey.win_chocolatey: + name: vscode + state: present + package_params: "/NoDesktopIcon" + + - name: Install VS Code WSL extension + ansible.windows.win_command: cmd /c code --install-extension ms-vscode-remote.remote-wsl + register: vscode_wsl_ext + changed_when: "'was successfully installed' in vscode_wsl_ext.stdout" + failed_when: false + + # ── Install WSL2 + Ubuntu ──────────────────────────────────────── + # On Win11 24H2 the in-box wsl.exe is a stub that requires Store + # install (which fails in non-interactive WinRM sessions). + # Use Chocolatey to install the WSL2 kernel and Ubuntu distro. + + - name: Create temp directory on Windows + ansible.windows.win_file: + path: C:\Temp + state: directory + + - name: Enable VirtualMachinePlatform feature + ansible.windows.win_optional_feature: + name: VirtualMachinePlatform + state: present + register: vm_platform + + - name: Enable Microsoft-Windows-Subsystem-Linux feature + ansible.windows.win_optional_feature: + name: Microsoft-Windows-Subsystem-Linux + state: present + register: wsl_feature + + - name: Reboot after enabling WSL features + ansible.windows.win_reboot: + reboot_timeout: 600 + when: wsl_feature.reboot_required | default(false) or + vm_platform.reboot_required | default(false) + + - name: Install WSL2 via Chocolatey + chocolatey.chocolatey.win_chocolatey: + name: wsl2 + state: present + register: wsl2_choco + + - name: Reboot after WSL2 install + ansible.windows.win_reboot: + reboot_timeout: 600 + when: wsl2_choco.changed | default(false) + + - name: Set WSL default version to 2 + ansible.windows.win_command: wsl --set-default-version 2 + register: wsl_ver + retries: 3 + delay: 10 + until: wsl_ver.rc == 0 + + - name: Check if Ubuntu WSL distribution is already installed + ansible.windows.win_shell: (wsl -l -q) -contains 'Ubuntu' + register: wsl_list + changed_when: false + failed_when: false + + - name: Install Ubuntu WSL distribution + ansible.windows.win_command: wsl --install -d Ubuntu --no-launch + register: wsl_install_ubuntu + failed_when: wsl_install_ubuntu.rc not in [0, 1] + when: "'True' not in wsl_list.stdout" + + - name: Reboot after Ubuntu install + ansible.windows.win_reboot: + reboot_timeout: 600 + when: wsl_install_ubuntu is changed + + - name: Initialize Ubuntu distro (first launch) + ansible.windows.win_command: >- + wsl -d Ubuntu -- echo "WSL Ubuntu initialized" + register: wsl_init + retries: 5 + delay: 15 + until: wsl_init.rc == 0 + + # ── Install tools inside WSL Ubuntu ────────────────────────────── + + - name: Copy WSL tools setup script + ansible.windows.win_copy: + src: ../scripts/setup-wsl-tools.sh + dest: C:\Temp\setup-wsl-tools.sh + + - name: Install prerequisite tools in WSL Ubuntu + ansible.windows.win_command: wsl -d Ubuntu -u root -- bash /mnt/c/Temp/setup-wsl-tools.sh + register: wsl_tools + async: 1200 + poll: 30 + + - name: Show WSL tools installation output + ansible.builtin.debug: + var: wsl_tools.stdout_lines + when: wsl_tools.stdout_lines is defined diff --git a/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/ansible/requirements.yml b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/ansible/requirements.yml new file mode 100644 index 000000000..c87cda45b --- /dev/null +++ b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/ansible/requirements.yml @@ -0,0 +1,6 @@ +--- +collections: + - name: ansible.windows + version: ">=2.3.0" + - name: chocolatey.chocolatey + version: ">=1.5.0" diff --git a/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/bastion.tf b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/bastion.tf new file mode 100644 index 000000000..124b1d101 --- /dev/null +++ b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/bastion.tf @@ -0,0 +1,107 @@ +# ============================================================================== +# Central Azure Bastion for secure RDP/SSH access to all participant VMs. +# Uses Standard SKU with IP-based connection to reach VMs across peered VNets. +# ============================================================================== + +# --- Bastion Resource Group --- + +resource "azurerm_resource_group" "bastion" { + name = "mh-bastion" + location = var.bastion_location + tags = { "SecurityControl" = "Ignore" } +} + +# --- Bastion VNet --- + +resource "azurerm_virtual_network" "bastion" { + name = "mh-bastion-vnet" + address_space = [var.bastion_vnet_address_space] + location = azurerm_resource_group.bastion.location + resource_group_name = azurerm_resource_group.bastion.name + + tags = { + Project = "simulated onprem k8s cluster for microhack" + } +} + +# Azure Bastion requires a subnet named exactly "AzureBastionSubnet" +resource "azurerm_subnet" "bastion" { + name = "AzureBastionSubnet" + resource_group_name = azurerm_resource_group.bastion.name + virtual_network_name = azurerm_virtual_network.bastion.name + address_prefixes = [cidrsubnet(var.bastion_vnet_address_space, 8, 0)] # /24 +} + +# --- Bastion Public IP --- + +resource "azurerm_public_ip" "bastion" { + name = "mh-bastion-ip" + resource_group_name = azurerm_resource_group.bastion.name + location = azurerm_resource_group.bastion.location + allocation_method = "Static" + sku = "Standard" + + tags = { + Project = "simulated onprem k8s cluster for microhack" + } +} + +# --- Azure Bastion Host (Standard SKU) --- + +resource "azurerm_bastion_host" "bastion" { + name = "mh-bastion" + resource_group_name = azurerm_resource_group.bastion.name + location = azurerm_resource_group.bastion.location + sku = "Standard" + + # Required for cross-VNet connections via peering + ip_connect_enabled = true + tunneling_enabled = true + shareable_link_enabled = false + + ip_configuration { + name = "bastion-ip-config" + subnet_id = azurerm_subnet.bastion.id + public_ip_address_id = azurerm_public_ip.bastion.id + } + + tags = { + Project = "simulated onprem k8s cluster for microhack" + } +} + +# --- Global VNet Peering: Bastion <-> each participant VNet --- + +resource "azurerm_virtual_network_peering" "bastion_to_onprem" { + count = length(local.indices) + name = "bastion-to-${format("%02d", local.indices[count.index])}-onprem" + resource_group_name = azurerm_resource_group.bastion.name + virtual_network_name = azurerm_virtual_network.bastion.name + remote_virtual_network_id = azurerm_virtual_network.onprem[count.index].id + allow_virtual_network_access = true + allow_forwarded_traffic = true + allow_gateway_transit = false + use_remote_gateways = false +} + +resource "azurerm_virtual_network_peering" "onprem_to_bastion" { + count = length(local.indices) + name = "${format("%02d", local.indices[count.index])}-onprem-to-bastion" + resource_group_name = azurerm_resource_group.mh_k8s_onprem[count.index].name + virtual_network_name = azurerm_virtual_network.onprem[count.index].name + remote_virtual_network_id = azurerm_virtual_network.bastion.id + allow_virtual_network_access = true + allow_forwarded_traffic = true + allow_gateway_transit = false + use_remote_gateways = false +} + +# --- Outputs --- + +output "bastion_name" { + value = azurerm_bastion_host.bastion.name +} + +output "bastion_resource_group" { + value = azurerm_resource_group.bastion.name +} diff --git a/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/cleanup.sh b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/cleanup.sh new file mode 100644 index 000000000..60e64724e --- /dev/null +++ b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/cleanup.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Usage: +# ./delete-all-rgs.sh [exclude-regex] +# +# Example: +# ./delete-all-rgs.sh 6a8f6f8b-c0fa-4f8d-a338-a2abec5dfa4b +# ./delete-all-rgs.sh 6a8f6f8b-c0fa-4f8d-a338-a2abec5dfa4b '^(NetworkWatcherRG|MH-SubKeepAlive-DoNotDelete|Default-ActivityLogAlerts|MA_defaultazuremonitorworkspace-weu_westeurope_managed)$' + +SUBSCRIPTION_ID="${1:-}" +EXCLUDE_REGEX="${2:-^$}" # default: exclude nothing + +if [[ -z "$SUBSCRIPTION_ID" ]]; then + echo "ERROR: subscription id is required" + exit 1 +fi + +az account set --subscription "$SUBSCRIPTION_ID" + +mapfile -t ALL_RGS < <(az group list --query "[].name" -o tsv) + +if [[ ${#ALL_RGS[@]} -eq 0 ]]; then + echo "No resource groups found in subscription $SUBSCRIPTION_ID" + exit 0 +fi + +TO_DELETE=() +for rg in "${ALL_RGS[@]}"; do + if [[ "$rg" =~ $EXCLUDE_REGEX ]]; then + continue + fi + TO_DELETE+=("$rg") +done + +if [[ ${#TO_DELETE[@]} -eq 0 ]]; then + echo "No resource groups matched for deletion after exclusions." + exit 0 +fi + +echo "Subscription: $SUBSCRIPTION_ID" +echo "Resource groups to delete (${#TO_DELETE[@]}):" +printf ' - %s\n' "${TO_DELETE[@]}" +echo +read -r -p "Type DELETE to continue: " CONFIRM +if [[ "$CONFIRM" != "DELETE" ]]; then + echo "Aborted." + exit 1 +fi + +echo "Submitting deletions..." +for rg in "${TO_DELETE[@]}"; do + echo "Deleting $rg" + az group delete --name "$rg" --yes --no-wait +done + +echo "Waiting for deletions to complete..." +for rg in "${TO_DELETE[@]}"; do + while az group exists --name "$rg" | grep -q true; do + echo "Still deleting: $rg" + sleep 10 + done + echo "Deleted: $rg" +done + +echo "Done." \ No newline at end of file diff --git a/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/k3s-master-setup.sh b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/k3s-master-setup.sh index d86f2eed0..914b54f9b 100644 --- a/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/k3s-master-setup.sh +++ b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/k3s-master-setup.sh @@ -26,27 +26,14 @@ fi export INSTALL_K3S_VERSION=${k3s_version} export K3S_TOKEN=${cluster_token} -# Get external IP reliably -EXTERNAL_IP=$(curl -s --max-time 10 https://ipinfo.io/ip 2>/dev/null || curl -s --max-time 10 http://checkip.amazonaws.com 2>/dev/null || echo "") - # Install K3s with embedded etcd and disable traefik (we might want to use nginx-ingress) -if [ -n "$EXTERNAL_IP" ] && [[ "$EXTERNAL_IP" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - curl -sfL https://get.k3s.io | sh -s - server \ - --cluster-init \ - --disable traefik \ - --disable servicelb \ - --write-kubeconfig-mode 644 \ - --node-external-ip "$EXTERNAL_IP" \ - --token $${K3S_TOKEN} -else - # Fallback without external IP if detection fails - curl -sfL https://get.k3s.io | sh -s - server \ - --cluster-init \ - --disable traefik \ - --disable servicelb \ - --write-kubeconfig-mode 644 \ - --token $${K3S_TOKEN} -fi +# All cluster communication uses private IPs (workstations access via VNet, external via Bastion) +curl -sfL https://get.k3s.io | sh -s - server \ + --cluster-init \ + --disable traefik \ + --disable servicelb \ + --write-kubeconfig-mode 644 \ + --token $${K3S_TOKEN} # Wait for K3s to be ready while ! kubectl get nodes > /dev/null 2>&1; do diff --git a/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/k3s-worker-setup.sh b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/k3s-worker-setup.sh index d326915b1..b66b28ed0 100644 --- a/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/k3s-worker-setup.sh +++ b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/k3s-worker-setup.sh @@ -37,21 +37,10 @@ while ! curl -k -s $${K3S_URL}/ping > /dev/null 2>&1; do sleep 10 done -# Get external IP reliably -EXTERNAL_IP=$(curl -s --max-time 10 https://ipinfo.io/ip 2>/dev/null || curl -s --max-time 10 http://checkip.amazonaws.com 2>/dev/null || echo "") - -# Install K3s agent -if [ -n "$EXTERNAL_IP" ] && [[ "$EXTERNAL_IP" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - curl -sfL https://get.k3s.io | sh -s - agent \ - --server $${K3S_URL} \ - --token $${K3S_TOKEN} \ - --node-external-ip "$EXTERNAL_IP" -else - # Fallback without external IP if detection fails - curl -sfL https://get.k3s.io | sh -s - agent \ - --server $${K3S_URL} \ - --token $${K3S_TOKEN} -fi +# Install K3s agent — all cluster communication uses private IPs +curl -sfL https://get.k3s.io | sh -s - agent \ + --server $${K3S_URL} \ + --token $${K3S_TOKEN} # Enable and start K3s agent systemctl enable k3s-agent diff --git a/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/k8s-cluster.tf b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/k8s-cluster.tf index 4c514b4ef..029682e8f 100644 --- a/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/k8s-cluster.tf +++ b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/k8s-cluster.tf @@ -28,14 +28,14 @@ resource "azurerm_network_security_group" "onprem" { resource_group_name = azurerm_resource_group.mh_k8s_onprem[count.index].name security_rule { - name = "SSH" + name = "SSH-from-Workstation" priority = 1001 direction = "Inbound" access = "Allow" protocol = "Tcp" source_port_range = "*" destination_port_range = "22" - source_address_prefix = "*" + source_address_prefix = "VirtualNetwork" destination_address_prefix = "*" } @@ -47,7 +47,7 @@ resource "azurerm_network_security_group" "onprem" { protocol = "Tcp" source_port_range = "*" destination_port_range = "6443" - source_address_prefix = "*" + source_address_prefix = "VirtualNetwork" destination_address_prefix = "*" } @@ -59,7 +59,31 @@ resource "azurerm_network_security_group" "onprem" { protocol = "Tcp" source_port_range = "*" destination_port_range = "30000-32767" - source_address_prefix = "*" + source_address_prefix = "VirtualNetwork" + destination_address_prefix = "*" + } + + security_rule { + name = "K3s-Kubelet" + priority = 1004 + direction = "Inbound" + access = "Allow" + protocol = "Tcp" + source_port_range = "*" + destination_port_range = "10250" + source_address_prefix = "VirtualNetwork" + destination_address_prefix = "*" + } + + security_rule { + name = "K3s-VXLAN" + priority = 1005 + direction = "Inbound" + access = "Allow" + protocol = "Udp" + source_port_range = "*" + destination_port_range = "8472" + source_address_prefix = "VirtualNetwork" destination_address_prefix = "*" } diff --git a/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/provider-template.txt b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/provider-template.txt index da37e755b..fc7451547 100644 --- a/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/provider-template.txt +++ b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/provider-template.txt @@ -18,6 +18,11 @@ provider "azurerm" { resource_group { prevent_deletion_if_contains_resources = false } + + virtual_machine { + graceful_shutdown = false + skip_shutdown_and_force_delete = true + } } subscription_id = "REPLACE-ME" } \ No newline at end of file diff --git a/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/readme.md b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/readme.md index 30860dd22..ed316e12b 100644 --- a/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/readme.md +++ b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/readme.md @@ -1,57 +1,83 @@ # Environment Setup When working through the challenges of this microhack, it's assumed that you have an onprem k8s cluster available which you can use to arc-enable it. Also, it's assumed that you have a container registry, which you can use for the gitops challenge. -In this folder you find terraform code to deploy a **K3s cluster** and container registry in Azure for each participant of the microhack. It's intended that coaches create these resources for their participants before the microhack starts, so the participants can directly start with challenge 1 (onboarding/arc-enabling their cluster). +In this folder you find terraform code to deploy a **K3s cluster**, a **Windows 11 workstation VM**, a **central Azure Bastion**, and a container registry in Azure for each participant of the microhack. It's intended that coaches create these resources for their participants before the microhack starts, so the participants can directly start with challenge 1 (onboarding/arc-enabling their cluster). -## K3s Architecture +Participants connect via **Azure Bastion** (RDP) to their Windows 11 workstation, which comes pre-installed with **WSL2 + Ubuntu**, **VS Code**, **Azure CLI**, **kubectl**, **Helm**, and **git** — everything needed for the challenges. -This Terraform configuration deploys a **3-node K3s cluster** in Azure VMs, simulating an on-premises Kubernetes environment for Azure Arc enablement. +## Architecture -Each deployment creates: +This Terraform configuration deploys the following per participant: + +### K3s Cluster (simulated on-premises) - **1 Master Node**: K3s server with embedded etcd (10.{100+index}.1.10) - **2 Worker Nodes**: K3s agents (10.{100+index}.1.11, 10.{100+index}.1.12) -- **Virtual Network**: Isolated network per cluster (10.{100+index}.0.0/16) -- **Network Security Groups**: Allow SSH and Kubernetes traffic -- **Public IPs**: For SSH access to all nodes + +### Windows 11 Workstation +- **1 Windows 11 VM**: Pre-configured dev workstation (10.{100+index}.2.10) +- Installed via Ansible: WSL2 + Ubuntu, VS Code (with WSL extension), Git, Azure CLI, kubectl, Helm +- Uses the same `admin_user` / `admin_password` credentials as the K3s nodes (defined in `fixtures.tfvars`) + +### Shared Infrastructure (deployed once) +- **Azure Bastion** (Standard SKU): Central jump host in its own VNet (10.200.0.0/16) +- **VNet Peering**: Bastion VNet peered to every participant VNet for cross-VNet RDP/SSH access +- No management ports exposed to the internet + +### Networking +- **Virtual Network** per participant: Isolated network (10.{100+index}.0.0/16) + - `k3s-subnet` (10.{100+index}.1.0/24) — K3s master and worker nodes + - `workstation-subnet` (10.{100+index}.2.0/24) — Windows 11 workstation +- **Network Security Groups**: + - K3s nodes: SSH, K3s API, and NodePort restricted to `VirtualNetwork` (accessible from the workstation on the same VNet) + - Workstation: RDP and WinRM restricted to the Bastion subnet only ## Resources to be deployed -2 resource groups, 1 K3s cluster (3 VMs), 1 container registry per participant where xy represents the participant number: +2 resource groups per participant, 1 K3s cluster (3 VMs), 1 Windows workstation, 1 container registry per participant, plus shared Bastion infrastructure. `xy` represents the participant number: ``` subscription | +├── mh-bastion (resource group — shared, deployed once) +│ ├── mh-bastion (Azure Bastion Host, Standard SKU) +│ ├── mh-bastion-vnet (Virtual Network 10.200.0.0/16) +│ └── mh-bastion-ip (Public IP) +| ├── -k8s-arc (resource group) -| | -│ └── mhacr (container registry) -| | -| └── -law (log analytics workspace) +│ ├── mhacr (container registry) +│ └── -law (log analytics workspace) | └── -k8s-onprem (resource group) - | ├── -k8s-master (VM - K3s server) ├── -k8s-worker1 (VM - K3s agent) ├── -k8s-worker2 (VM - K3s agent) - ├── -k8s-vnet (Virtual Network) - └── Associated NICs, NSGs, and Public IPs + ├── -workstation (VM - Windows 11 Pro) + ├── -k8s-vnet (Virtual Network, peered to Bastion VNet) + │ ├── k3s-subnet + │ └── workstation-subnet + └── Associated NICs, NSGs (no public IPs on workstation) ``` ## Prerequisites * bash shell (tested with Ubuntu 22.04) * Azure CLI * terraform +* Ansible (with `ansible.windows` and `chocolatey.chocolatey` collections — see `ansible/requirements.yml`) * clone this repo locally, so you can adjust the deployment files according to your needs * Azure subscription * User account with subscription owner permissions -* Sufficient quota limits to support creation of K3s VMs per participant +* Sufficient quota limits to support creation of K3s VMs + Windows workstation VMs per participant -## K3s Default Configuration -- **VM Size**: Standard_D4ds_v6 (sufficient for K3s, smaller than AKS requirements) -- **OS**: Ubuntu 22.04 LTS +## Default Configuration +- **K3s VM Size**: Standard_D4ds_v6 (sufficient for K3s and Arc data services) +- **Workstation VM Size**: Standard_D4ds_v6 +- **K3s OS**: Ubuntu 22.04 LTS +- **Workstation OS**: Windows 11 24H2 Pro - **K3s Version**: v1.33.6+k3s1 -- **Admin User**: Set via admin_user in fixtures.tfvars -- **VMs per cluster**: 3 VMs (1 master + 2 workers) -- **Password**: Must be set in fixtures.tfvars (no default value) +- **Admin User**: Set via `admin_user` in `fixtures.tfvars` (shared by K3s and workstation VMs) +- **Password**: Set via `admin_password` in `fixtures.tfvars` (shared by K3s and workstation VMs) +- **VMs per participant**: 4 (1 K3s master + 2 K3s workers + 1 Windows workstation) +- **Bastion**: 1 Azure Bastion (Standard SKU), shared across all participants -If you don't change the default value of parameter "vm_size" in variables.tf, three Standard_D4ds_v6 VMs per cluster are used (1 master + 2 workers). If you have many participants you need to ensure that the quota limit in your subscription is sufficient to support the required cores. The terraform code will distribute the K3s clusters to 10 different regions. This setting can be adjusted via the parameter "onprem_resources" (variables.tf) value. +If you don't change the default value of parameter "vm_size" in variables.tf, three Standard_D4ds_v6 VMs per cluster plus one Standard_D4ds_v6 workstation VM per participant are used. If you have many participants you need to ensure that the quota limit in your subscription is sufficient to support the required cores (4 VMs × participants). The terraform code will distribute the participant VNets across multiple regions. This setting can be adjusted via the parameter "onprem_resources" (variables.tf) value. You can check this limit via Azure Portal (subscription > settings > Usage & Quotas): @@ -120,14 +146,43 @@ terraform plan -var-file=fixtures.tfvars -out=tfplan terraform apply tfplan ``` +### Step 2: Install Ansible collections and configure workstations + +After Terraform provisioning completes, run the Ansible playbook to install all prerequisite tools on the Windows workstations. Since the workstations have no public IPs, Ansible connects through **Bastion tunnels** that forward the remote WinRM port to localhost. Terraform auto-generates both the inventory (`inventory.yml`) and a helper script (`open-bastion-tunnels.sh`). + +```bash +# Install required Ansible collections +cd ansible +ansible-galaxy collection install -r requirements.yml + +# Open Bastion tunnels for all workstations (runs in background) +./open-bastion-tunnels.sh + +# Run the Ansible playbook (via tunnels on localhost) +ansible-playbook -i inventory.yml playbook-workstation.yml \ + --extra-vars "ansible_password=" + +# When done, stop the tunnels +./open-bastion-tunnels.sh --stop +cd .. +``` + +The Ansible playbook installs: +- **Git** and **Visual Studio Code** (with WSL extension) on Windows via Chocolatey +- **WSL2** runtime via Chocolatey (`wsl2` package) + **Ubuntu** distribution via `wsl --install -d Ubuntu` +- **Azure CLI**, **kubectl**, and **Helm** inside WSL Ubuntu + ### What Happens After Deployment -1. **VMs are created** with Ubuntu 22.04 LTS +1. **VMs are created** — K3s nodes with Ubuntu 22.04 LTS, workstation with Windows 11 24H2 Pro 2. **K3s setup scripts run automatically** via cloud-init: - Master node: Installs K3s server, configures networking, sets up kubeconfig - Worker nodes: Wait for master, then join the cluster as K3s agents -3. **Cluster becomes ready** in ~5-10 minutes after VM deployment -4. **SSH access** is available immediately with the admin_user and your password +3. **Windows workstation** gets WinRM HTTPS enabled via CustomScriptExtension (for Ansible) +4. **Azure Bastion** is deployed and peered to all participant VNets +5. **Coach runs Ansible playbook** to install prerequisites on workstations (see Step 2 above) +6. **K3s cluster becomes ready** in ~5-10 minutes after VM deployment +7. **Participants connect** via Azure Bastion (RDP) to their Windows workstation The expected output looks approximately like this depending on the start_index and end_index parameters: ```bash @@ -137,6 +192,8 @@ acr_names = { "37" = "37mhacr" "38" = "38mhacr" } +bastion_name = "mh-bastion" +bastion_resource_group = "mh-bastion" k3s_cluster_info = { "37" = { "kubeconfig_setup" = "mkdir -p ~/.kube && scp @x.x.x.x:/home//.kube/config ~/.kube/config && sed -i 's/127.0.0.1/x.x.x.x/g' ~/.kube/config" @@ -144,12 +201,15 @@ k3s_cluster_info = { "worker1_ssh" = "ssh @y.y.y.y" "worker2_ssh" = "ssh @z.z.z.z" } - "38" = { - "kubeconfig_setup" = "mkdir -p ~/.kube && scp @20.19.166.105:/home//.kube/config ~/.kube/config && sed -i 's/127.0.0.1/a.a.a.a/g' ~/.kube/config" - "master_ssh" = "ssh @a.a.a.a" - "worker1_ssh" = "ssh @b.b.b.b" - "worker2_ssh" = "ssh @c.c.c.c" + "38" = { ... } +} +workstation_info = { + "37" = { + "bastion_rdp" = "az network bastion rdp --name mh-bastion --resource-group mh-bastion --target-resource-id /subscriptions/.../37-workstation" + "private_ip" = "10.137.2.10" + "vm_id" = "/subscriptions/.../37-workstation" } + "38" = { ... } } law = { "37" = "/subscriptions/.../resourceGroups/37-k8s-arc/providers/Microsoft.OperationalInsights/workspaces/37-law" @@ -167,36 +227,51 @@ rg_names_onprem = { **Important**: Wait 5-10 minutes after terraform completes before trying to access the K3s cluster to allow the setup scripts to finish. -## Post-Deployment - Accessing Your K3s Cluster +## Post-Deployment - Accessing Your Environment + +### 1. Connect to your Windows workstation via Bastion + +Participants connect to their Windows workstation via Azure Bastion in the Azure Portal: + +1. Navigate to the Azure Portal → search for **Bastion** +2. Open **mh-bastion** → click **Connect** → select your workstation VM (`-workstation`) +3. Enter the credentials provided by your coach (same `admin_user` / `admin_password` as the K3s nodes) +4. A browser-based RDP session opens to your Windows workstation -### 1. Access your cluster +Alternatively, use the Azure CLI native client: ```bash -# Set admin username (must match the admin_user value in fixtures.tfvars) -admin_user="" +az network bastion rdp --name mh-bastion --resource-group mh-bastion \ + --target-resource-id +``` + +### 2. Access your K3s cluster from the workstation -# Extract user number from Azure username before '@' (e.g., LabUser-37@... -> 37) -azure_user=$(az account show --query user.name --output tsv) -user_number=$(echo "${azure_user%@*}" | grep -oE '[0-9]+' | tail -n1 | sed 's/^0*//; s/^$/0/') +Once connected to the workstation, open **VS Code** → open a **WSL terminal** (Terminal → New Terminal). The K3s master node is reachable via its private IP from the workstation (same VNet): -# Get public ip of master node via Azure CLI according to user-number -master_pip=$(az vm list-ip-addresses --resource-group "${user_number}-k8s-onprem" --name "${user_number}-k8s-master" --query "[0].virtualMachine.network.publicIpAddresses[0].ipAddress" --output tsv) +```bash +# Set admin username (must match the admin_user value provided by your coach) +admin_user="" -echo "Connecting to master node: $master_pip with user: $admin_user" +# The master node private IP follows the pattern 10.{100+user_number}.1.10 +# For user 37: 10.137.1.10, for user 38: 10.138.1.10, etc. +master_ip="10.<100+your_number>.1.10" # Create .kube directory if it doesn't exist mkdir -p ~/.kube -# Copy the kubeconfig to standard location -scp $admin_user@$master_pip:/home/$admin_user/.kube/config ~/.kube/config +# Copy the kubeconfig (via private IP — no public IP needed) +scp $admin_user@$master_ip:/home/$admin_user/.kube/config ~/.kube/config -# Replace localhost address with the public ip of master node -sed -i "s/127.0.0.1/$master_pip/g" ~/.kube/config +# Replace localhost address with the master's private IP +sed -i "s/127.0.0.1/$master_ip/g" ~/.kube/config -# Now kubectl works directly +# Verify kubectl get nodes ``` -### 2. Verify K3s Installation +### 3. Verify K3s Installation + +SSH into the K3s master via Bastion tunnel (coach) or from the workstation WSL terminal: ```bash # On master node kubectl get nodes @@ -239,8 +314,11 @@ sudo /usr/local/bin/k3s-agent-uninstall.sh ``` ## Security Notes +- **RDP** access to the workstation is routed through **Azure Bastion** — no management ports are exposed to the internet +- **SSH** to K3s nodes is restricted to `VirtualNetwork` — participants SSH from the workstation (same VNet), not from the internet +- **WinRM** on the workstation is restricted to the Bastion subnet (used by the coach for Ansible provisioning via Bastion tunnels) - VMs use password authentication (consider using SSH keys for production) -- Network security groups restrict access to necessary ports only +- K3s API and NodePort access is restricted to `VirtualNetwork` - K3s runs without Traefik (disabled for flexibility) - Docker is installed for container runtime and additional workloads @@ -250,9 +328,12 @@ When done with the microhack, call terraform destroy to clean up. ```bash terraform destroy -var-file=fixtures.tfvars + +# if there are remaining resources after terraform delete, use this script to remove the rest: + ``` -This will remove all created resources including VMs, networks, and public IPs. +This will remove all created resources including VMs, networks, Bastion, and VNet peerings. [To challenge 01](../challenges/challenge-01.md) diff --git a/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/scripts/enable-winrm.ps1 b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/scripts/enable-winrm.ps1 new file mode 100644 index 000000000..62428181b --- /dev/null +++ b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/scripts/enable-winrm.ps1 @@ -0,0 +1,40 @@ +# Enable WinRM HTTPS for Ansible management +$ErrorActionPreference = "Stop" + +# Enable PowerShell remoting +Enable-PSRemoting -Force -SkipNetworkProfileCheck + +# Create self-signed certificate for WinRM HTTPS +$cert = New-SelfSignedCertificate ` + -DnsName $env:COMPUTERNAME ` + -CertStoreLocation Cert:\LocalMachine\My ` + -NotAfter (Get-Date).AddYears(1) + +# Remove any existing HTTPS WinRM listeners +Get-ChildItem WSMan:\localhost\Listener | + Where-Object { $_.Keys -match "Transport=HTTPS" } | + Remove-Item -Recurse -Force -ErrorAction SilentlyContinue + +# Create HTTPS WinRM listener with self-signed certificate +New-Item -Path WSMan:\localhost\Listener ` + -Transport HTTPS ` + -Address * ` + -CertificateThumbPrint $cert.Thumbprint ` + -Force + +# Configure WinRM service authentication +Set-Item WSMan:\localhost\Service\Auth\Basic -Value $true +Set-Item WSMan:\localhost\Shell\MaxMemoryPerShellMB -Value 1024 + +# Open Windows Firewall for WinRM HTTPS +New-NetFirewallRule ` + -Name "WinRM-HTTPS-Inbound" ` + -DisplayName "WinRM HTTPS Inbound" ` + -Enabled True ` + -Direction Inbound ` + -Protocol TCP ` + -Action Allow ` + -LocalPort 5986 + +# Restart WinRM service +Restart-Service WinRM diff --git a/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/scripts/setup-wsl-tools.sh b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/scripts/setup-wsl-tools.sh new file mode 100644 index 000000000..6d0b05641 --- /dev/null +++ b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/scripts/setup-wsl-tools.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# Install MicroHack prerequisite tools inside WSL Ubuntu +set -e + +export DEBIAN_FRONTEND=noninteractive + +echo "==> Updating package lists..." +apt-get update + +echo "==> Installing git..." +apt-get install -y git ca-certificates curl apt-transport-https + +echo "==> Installing Azure CLI..." +curl -sL https://aka.ms/InstallAzureCLIDeb | bash + +echo "==> Installing kubectl..." +KUBECTL_VERSION=$(curl -sL https://dl.k8s.io/release/stable.txt) +curl -LO "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl" +install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl +rm -f kubectl +echo " kubectl ${KUBECTL_VERSION} installed" + +echo "==> Installing Helm..." +curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash + +echo "" +echo "==> All MicroHack prerequisite tools installed successfully!" +echo " git: $(git --version)" +echo " az: $(az version --query '\"azure-cli\"' -o tsv)" +echo " kubectl: $(kubectl version --client --short 2>/dev/null || kubectl version --client)" +echo " helm: $(helm version --short)" diff --git a/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/start_here.sh b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/start_here.sh old mode 100644 new mode 100755 index ae3624dad..644830699 --- a/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/start_here.sh +++ b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/start_here.sh @@ -14,6 +14,11 @@ echo "Using subcription ID: $subscription_id" # sed -i "s|client_id=\".*\"|client_id=\"$client_id\"|" fixtures.tfvars # sed -i "s|client_secret=\".*\"|client_secret=\"$client_secret\"|" fixtures.tfvars +if [ ! -f provider.tf ]; then + echo "provider.tf not found — creating from provider-template.txt..." + cp provider-template.txt provider.tf +fi + echo "replacing subscription_id in provider.tf..." # replace the subscription id in provider.tf sed -i "s|subscription_id = \".*\"|subscription_id = \"$subscription_id\"|" provider.tf diff --git a/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/variables.tf b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/variables.tf index a88008a40..14cc87d61 100644 --- a/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/variables.tf +++ b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/variables.tf @@ -62,6 +62,21 @@ variable "vm_size" { default = "Standard_D4ds_v6" # For arc-enabled Managed SQL Instances, ARM cores not supported } +variable "workstation_vm_size" { + description = "The Azure VM size for Windows 11 workstation VMs" + default = "Standard_D4ds_v6" +} + +variable "bastion_location" { + description = "The Azure Region for the central Bastion host" + default = "francecentral" +} + +variable "bastion_vnet_address_space" { + description = "Address space for the Bastion VNet (must not overlap with participant VNets 10.1xx.0.0/16)" + default = "10.200.0.0/16" +} + # container reguistry variables for gitops challenge variable "acr_name" { description = "The name of the Azure Container Registry" diff --git a/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/windows-vm.tf b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/windows-vm.tf new file mode 100644 index 000000000..72b860de0 --- /dev/null +++ b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/lab/windows-vm.tf @@ -0,0 +1,190 @@ +# ============================================================================== +# Windows 11 Workstation VM for MicroHack participants +# Provides a dev workstation with WSL, VS Code, and all prerequisite tools. +# After Terraform apply, run the Ansible playbook to install prerequisites. +# ============================================================================== + +# --- Networking --- + +resource "azurerm_subnet" "workstation" { + count = length(local.indices) + name = "workstation-subnet" + resource_group_name = azurerm_resource_group.mh_k8s_onprem[count.index].name + virtual_network_name = azurerm_virtual_network.onprem[count.index].name + address_prefixes = ["10.${100 + local.indices[count.index]}.2.0/24"] +} + +resource "azurerm_network_security_group" "workstation" { + count = length(local.indices) + name = "${format("%02d", local.indices[count.index])}-${var.resource_group_base_name}-workstation-nsg" + location = azurerm_resource_group.mh_k8s_onprem[count.index].location + resource_group_name = azurerm_resource_group.mh_k8s_onprem[count.index].name + + security_rule { + name = "RDP-from-Bastion" + priority = 1001 + direction = "Inbound" + access = "Allow" + protocol = "Tcp" + source_port_range = "*" + destination_port_range = "3389" + source_address_prefix = cidrsubnet(var.bastion_vnet_address_space, 8, 0) + destination_address_prefix = "*" + } + + security_rule { + name = "WinRM-HTTPS-from-Bastion" + priority = 1002 + direction = "Inbound" + access = "Allow" + protocol = "Tcp" + source_port_range = "*" + destination_port_range = "5986" + source_address_prefix = cidrsubnet(var.bastion_vnet_address_space, 8, 0) + destination_address_prefix = "*" + } + + tags = { + Project = "simulated onprem k8s cluster for microhack" + } +} + +resource "azurerm_subnet_network_security_group_association" "workstation" { + count = length(local.indices) + subnet_id = azurerm_subnet.workstation[count.index].id + network_security_group_id = azurerm_network_security_group.workstation[count.index].id +} + +# --- NIC (no public IP — access via Bastion) --- + +resource "azurerm_network_interface" "workstation" { + count = length(local.indices) + name = "${format("%02d", local.indices[count.index])}-${var.resource_group_base_name}-workstation-nic" + location = azurerm_resource_group.mh_k8s_onprem[count.index].location + resource_group_name = azurerm_resource_group.mh_k8s_onprem[count.index].name + + ip_configuration { + name = "internal" + subnet_id = azurerm_subnet.workstation[count.index].id + private_ip_address_allocation = "Static" + private_ip_address = "10.${100 + local.indices[count.index]}.2.10" + } + + tags = { + Project = "simulated onprem k8s cluster for microhack" + } +} + +# --- Windows 11 VM --- + +resource "azurerm_windows_virtual_machine" "workstation" { + count = length(local.indices) + name = "${format("%02d", local.indices[count.index])}-workstation" + resource_group_name = azurerm_resource_group.mh_k8s_onprem[count.index].name + location = azurerm_resource_group.mh_k8s_onprem[count.index].location + size = var.workstation_vm_size + admin_username = var.admin_user + admin_password = var.admin_password + + network_interface_ids = [ + azurerm_network_interface.workstation[count.index].id, + ] + + identity { + type = "SystemAssigned" + } + + os_disk { + caching = "ReadWrite" + storage_account_type = "Premium_LRS" + } + + # Windows 11 24H2 Pro — change sku to "win11-24h2-ent" if Pro is unavailable + source_image_reference { + publisher = "MicrosoftWindowsDesktop" + offer = "windows-11" + sku = "win11-24h2-pro" + version = "latest" + } + + tags = { + Project = "simulated onprem k8s cluster for microhack" + Role = "workstation" + } +} + +# --- Enable WinRM via CustomScriptExtension for Ansible --- + +resource "azurerm_virtual_machine_extension" "winrm" { + count = length(local.indices) + name = "enable-winrm" + virtual_machine_id = azurerm_windows_virtual_machine.workstation[count.index].id + publisher = "Microsoft.Compute" + type = "CustomScriptExtension" + type_handler_version = "1.10" + + settings = jsonencode({ + commandToExecute = "powershell -ExecutionPolicy Unrestricted -EncodedCommand ${textencodebase64(file("${path.module}/scripts/enable-winrm.ps1"), "UTF-16LE")}" + }) + + tags = { + Project = "simulated onprem k8s cluster for microhack" + } +} + +# --- Auto-shutdown schedule --- + +resource "azurerm_dev_test_global_vm_shutdown_schedule" "workstation" { + count = length(local.indices) + virtual_machine_id = azurerm_windows_virtual_machine.workstation[count.index].id + location = azurerm_resource_group.mh_k8s_onprem[count.index].location + enabled = true + + daily_recurrence_time = "1800" + timezone = "W. Europe Standard Time" + + notification_settings { + enabled = false + } + + tags = { + Project = "simulated onprem k8s cluster for microhack" + } +} + +# --- Generate Ansible inventory and Bastion tunnel script --- + +resource "local_file" "ansible_inventory" { + content = templatefile("${path.module}/ansible/inventory.yml.tpl", { + indices = local.indices + workstation_ips = [for nic in azurerm_network_interface.workstation : nic.private_ip_address] + workstation_vm_ids = [for vm in azurerm_windows_virtual_machine.workstation : vm.id] + admin_user = var.admin_user + }) + filename = "${path.module}/ansible/inventory.yml" +} + +resource "local_file" "bastion_tunnel_script" { + content = templatefile("${path.module}/ansible/open-bastion-tunnels.sh.tpl", { + indices = local.indices + workstation_ips = [for nic in azurerm_network_interface.workstation : nic.private_ip_address] + workstation_vm_ids = [for vm in azurerm_windows_virtual_machine.workstation : vm.id] + bastion_name = azurerm_bastion_host.bastion.name + bastion_rg = azurerm_resource_group.bastion.name + }) + filename = "${path.module}/ansible/open-bastion-tunnels.sh" + file_permission = "0755" +} + +# --- Outputs --- + +output "workstation_info" { + value = { + for i in range(length(local.indices)) : + format("%02d", local.indices[i]) => { + bastion_rdp = "az network bastion rdp --name ${azurerm_bastion_host.bastion.name} --resource-group ${azurerm_resource_group.bastion.name} --target-resource-id ${azurerm_windows_virtual_machine.workstation[i].id}" + private_ip = azurerm_network_interface.workstation[i].private_ip_address + vm_id = azurerm_windows_virtual_machine.workstation[i].id + } + } +} diff --git a/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/walkthrough/challenge-01/az_connect_k8s.sh b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/walkthrough/challenge-01/az_connect_k8s.sh index 5c83bdd8b..fe1c629c6 100644 --- a/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/walkthrough/challenge-01/az_connect_k8s.sh +++ b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/walkthrough/challenge-01/az_connect_k8s.sh @@ -6,8 +6,8 @@ set -e echo "Exporting environment variables" # Extract user number from Azure username (e.g., LabUser-37 -> 37) -azure_user=$(az account show --query user.name --output tsv) -user_number=$(echo "$azure_user" | sed -E -n 's/.*[^0-9]([0-9]+)$/\1/p' | sed 's/^0*//; s/^$/0/') +azure_user=$(az account show --query user.name --output tsv | tr -d '\r') +user_number=$(echo "$azure_user" | cut -d'@' -f1 | sed -E -n 's/.*[^0-9]([0-9]+)$/\1/p' | sed 's/^0*//') if [ -z "$user_number" ]; then echo "Error: Could not extract user number from Azure username: $azure_user" diff --git a/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/walkthrough/challenge-01/solution.md b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/walkthrough/challenge-01/solution.md index 333d69846..64dede320 100644 --- a/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/walkthrough/challenge-01/solution.md +++ b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/walkthrough/challenge-01/solution.md @@ -20,34 +20,45 @@ In case you are prompted to select a subscription, please do so. In the microhac Validate that you can see your two resource groups in the [Azure portal](https://portal.azure.com) depending on your LabUser number. I.e. if you are LabUser-37, you should see the resource groups "37-k8s-arc" and "37-k8s-onprem". Click on your onprem resource group's name (i.e. 37-k8s-onprem). -There should be 3 VMs in this resource group. Make sure that all VMs are in state 'running'. +There should be 4 VMs in this resource group (master, worker1, worker2, workstation). Make sure that all VMs are in state 'running'. ![img-start-vm](img/vm-start.png) To connect to your k8s cluster, we first need to merge the cluster credentials into your local ~/.kube/config file. You can use the following bash script for this: + ```bash # Set admin username (use the admin_user value provided by your coach) admin_user="" # Extract trailing number from Azure username before '@' (e.g., LabUser-37@... or hackuser-067@... -> 37 or 67) -azure_user=$(az account show --query user.name --output tsv) -user_number=$(echo "${azure_user%@*}" | grep -oE '[0-9]+' | tail -n1 | sed 's/^0*//; s/^$/0/') +azure_user=$(az account show --query user.name --output tsv | tr -d '\r') +user_number=$(echo "$azure_user" | cut -d'@' -f1 | sed -E -n 's/.*[^0-9]([0-9]+)$/\1/p' | sed 's/^0*//') + +# Get private IP of master node +master_ip=$(az vm show --resource-group "${user_number}-k8s-onprem" --name "${user_number}-k8s-master" --show-details --query privateIps --output tsv) -# Get public ip of master node via Azure cli according to user-number -master_pip=$(az vm list-ip-addresses --resource-group "${user_number}-k8s-onprem" --name "${user_number}-k8s-master" --query "[0].virtualMachine.network.publicIpAddresses[0].ipAddress" --output tsv) +# ── Optional: only if working from your LOCAL machine instead of the workstation VM ── +# Uncomment the following lines to tunnel SSH and the K3s API through Azure Bastion: +# master_vm_id=$(az vm show --resource-group "${user_number}-k8s-onprem" --name "${user_number}-k8s-master" --query id --output tsv) +# az network bastion tunnel --name mh-bastion --resource-group mh-bastion --target-resource-id "$master_vm_id" --resource-port 22 --port 2222 & +# az network bastion tunnel --name mh-bastion --resource-group mh-bastion --target-resource-id "$master_vm_id" --resource-port 6443 --port 6443 & +# sleep 5 ; master_ip="127.0.0.1" ; scp_port="-P 2222" +# ── End optional block ── # Create .kube directory if it doesn't exist mkdir -p ~/.kube # Copy the kubeconfig to standard location -scp $admin_user@$master_pip:~/.kube/config ~/.kube/config +scp ${scp_port:-} $admin_user@$master_ip:~/.kube/config ~/.kube/config -# replace localhost address with the public ip of master node -sed -i "s/127.0.0.1/$master_pip/g" ~/.kube/config +# Replace localhost address with the master IP +sed -i "s/127.0.0.1/$master_ip/g" ~/.kube/config -# Now kubectl works directly on your local client - no need to ssh into the master node anymore +# Now kubectl works directly — no need to ssh into the master node anymore kubectl get nodes ``` +💡 **How does this work?** From the workstation VM, `master_ip` is the private IP and all traffic stays on the VNet. If the optional block is uncommented, `master_ip` is overridden to `127.0.0.1` — SCP goes through the SSH tunnel (port 2222) and the sed becomes a no-op, so `kubectl` talks to `127.0.0.1:6443` through the K3s API tunnel. + ## Task 2 - Connect K8s cluster using script * In your shell go to the folder where you cloned the microhack repository @@ -65,10 +76,16 @@ kubectl get nodes 💡 *Important*: Make sure that your kubectl works and is pointing to the k8s cluster you want to onboard before executing the script! +💡 *WSL users*: Clone to a native Linux path (e.g. `~`), **not** to `/mnt/c/...`. The Windows NTFS mount does not support POSIX file permissions and `git clone` will fail there. + ```bash -# Clone the repository (or copy the script content) -git clone https://github.com/microsoft/MicroHack.git -cd MicroHack/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/walkthroughs/challenge-01 +# Clone only the Arc Kubernetes microhack using sparse-checkout +cd ~ +git clone --no-checkout --filter=blob:none https://github.com/microsoft/MicroHack.git +cd MicroHack +git sparse-checkout set 03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes +git checkout +cd 03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/walkthrough/challenge-01 # Make the script executable and run it chmod +x az_connect_k8s.sh @@ -199,4 +216,4 @@ Now, reload the resources page in the Azure portl. You should see at least the f You successfully completed challenge 1! 🚀🚀🚀 -[Next challenge](../challenge-02/solution.md) - [Next Challenge's Solution](../../walkthroughs/challenge-02/solution.md) \ No newline at end of file +[Next challenge](../../challenges/challenge-02.md) - [Next Challenge's Solution](../challenge-02/solution.md) \ No newline at end of file diff --git a/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/walkthrough/challenge-02/solution.md b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/walkthrough/challenge-02/solution.md index 368886f88..0b4e9c373 100644 --- a/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/walkthrough/challenge-02/solution.md +++ b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/walkthrough/challenge-02/solution.md @@ -13,9 +13,9 @@ Duration: 30-45 min ## Task 1 - Enable Azure Monitor for k8s Execute the following commands in your bash shell to install the container log extension with default settings: ```bash -# Extract user number from Azure username before '@' (e.g., LabUser-37@... -> 37) +# Extract user number from Azure username before '@' (e.g., LabUser-37@... or hackuser-067@... -> 37 or 67) azure_user=$(az account show --query user.name --output tsv) -user_number=$(echo "${azure_user%@*}" | grep -oE '[0-9]+' | tail -n1 | sed 's/^0*//; s/^$/0/') +user_number=$(echo "$azure_user" | cut -d'@' -f1 | sed -E -n 's/.*[^0-9]([0-9]+)$/\1/p' | sed 's/^0*//') echo $user_number # if you are running this in a non-microhack env, adjust the values to match your env @@ -365,4 +365,4 @@ kubectl describe $constraint_name You successfully completed challenge 2! 🚀🚀🚀 -[Back to challenge 01](../challenge-01/solution.md) - [Next challenge](../challenge-03/solution.md) - [Next Challenge's Solution](../../walkthroughs/challenge-03/solution.md) \ No newline at end of file +[Back to challenge 01](../challenge-01/solution.md) - [Next challenge](../../challenges/challenge-03.md) - [Next Challenge's Solution](../challenge-03/solution.md) \ No newline at end of file diff --git a/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/walkthrough/challenge-03/solution.md b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/walkthrough/challenge-03/solution.md index 5c09c1de4..623fe038f 100644 --- a/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/walkthrough/challenge-03/solution.md +++ b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/walkthrough/challenge-03/solution.md @@ -33,15 +33,14 @@ kubectl create namespace aimh helm install ollama otwld/ollama \ --namespace aimh \ --set service.type=ClusterIP \ - --set ollama.port=15000 \ --set persistentVolume.enabled=true \ --set persistentVolume.size=20Gi \ --set ollama.models.pull[0]=phi4-mini:latest \ --set ollama.models.run[0]=phi4-mini:latest \ --set resources.requests.cpu="2" \ - --set resources.requests.memory="2Gi" \ + --set resources.requests.memory="4Gi" \ --set resources.limits.cpu="4" \ - --set resources.limits.memory="4Gi" + --set resources.limits.memory="8Gi" ``` ### Step 2: Install openwebUI @@ -52,36 +51,32 @@ helm install openwebui open-webui/open-webui \ --set service.type=NodePort \ --set service.nodePort=30080 \ --set ollama.enabled=false \ - --set ollamaUrls[0]="http://ollama.slm.svc.cluster.local:15000" \ + --set ollamaUrls[0]="http://ollama.aimh.svc.cluster.local:11434" \ --set persistence.enabled=true \ --set persistence.size=5Gi ``` -Get the external IP of the openwebui +Get the the URL of the openwebui. ```bash -kubectl get svc -n aimh - -azure_user=$(az account show --query user.name -o tsv) -user_number=$(echo "${azure_user%@*}" | grep -oE '[0-9]+' | tail -n1 | sed 's/^0*//; s/^$/0/') -node_pip=$(az vm list-ip-addresses \ - --resource-group "${user_number}-k8s-onprem" \ - --name "${user_number}-k8s-master" \ - --query "[0].virtualMachine.network.publicIpAddresses[0].ipAddress" -o tsv) - -echo "Open: http://${node_pip}:30080" +export NODE_PORT=$(kubectl get -n aimh -o jsonpath="{.spec.ports[0].nodePort}" services openwebui-open-webui) +export NODE_IP=$(kubectl get nodes -o jsonpath="{.items[0].status.addresses[0].address}") +echo "Open http://$NODE_IP:$NODE_PORT" ``` +**Note:** In this microhack lab K3s is configured to use internal IP addresses per default. Use the provided Windows workstation in the lab to open the URL. ## Task 5 - Access the openwebUI and run a prompt -open webbrowser to access the external IP, create user login to the openwebUI portal +* Open webbrowser to access URL provided in the previous step, create user login to the openwebUI portal. +* Then click on the arrow icon next to "Select a model" and in the pop up click "Manage Connections". + ![add-connection](img/01_add_connection.png) -add an ollama connection +* Add a new Ollama API connection, by clicking the "+" sign on the right hand side next to Ollama API ![add-connection-ollama](img/02_add_connection_ollama.png) -add IP Address and port and click in save(2x) +* Add the URL of your ollama deployment including port ("http://ollama.aimh.svc.cluster.local:11434") and click on save (2x). + ![add-connection-IPandPort](img/03_add_connection_ip_port.png) -and try out your first prompt -If your connection works, you should see in the upper left corner the deployed model "phi4-mini". +* Try out your first prompt. If your connection works, you should see in the upper left corner the deployed model "phi4-mini". You successfully completed challenge 3! 🚀🚀🚀 diff --git a/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/walkthrough/challenge-04/solution.md b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/walkthrough/challenge-04/solution.md index 6879859d2..a08bd72f3 100644 --- a/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/walkthrough/challenge-04/solution.md +++ b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/walkthrough/challenge-04/solution.md @@ -21,9 +21,9 @@ The deployment uses ARM templates as this is the most robust way for deployment ### Step 1: Gather required values ```bash -# Get current LabUser's number +# Get current user's number (e.g., LabUser-37@... or hackuser-067@... -> 37 or 67) export azure_user=$(az account show --query user.name --output tsv) -export user_number=$(echo "${azure_user%@*}" | grep -oE '[0-9]+' | tail -n1 | sed 's/^0*//; s/^$/0/') +export user_number=$(echo "$azure_user" | cut -d'@' -f1 | sed -E -n 's/.*[^0-9]([0-9]+)$/\1/p' | sed 's/^0*//') export subscription_id=$(az account show --query id -o tsv) export resource_group="$user_number-k8s-arc" @@ -48,7 +48,7 @@ echo "GUID 2: $guid2" Copy the template file and replace placeholders: ```bash -cd walkthroughs/challenge-04/templates +cd walkthrough/challenge-04/templates # Remove the old one and start fresh rm parameters-my.json @@ -145,25 +145,25 @@ kubectl get pods -n ${user_number}-onprem -l app.kubernetes.io/name=$sqlmi_name ## Task 3 - Connect to your SQL Managed Instance ### Prerequisites - -**Network Security Group (NSG)**: The Terraform deployment has already configured the NSG to allow external access to the NodePort range (30000-32767) required for SQL MI connectivity. No additional NSG configuration is needed. +* In this microhack the K3s cluster is configured to expose services on the private IP address of the master node per default. So you should use the Windows workstation in your lab environment to connect to the SQL MI. +* **Network Security Group (NSG)**: The Terraform deployment has already configured the NSG to allow external access to the NodePort range (30000-32767) required for SQL MI connectivity. No additional NSG configuration is needed. ### Get connection details ```bash # Get public ip of master node via Azure cli according to user-number -master_pip=$(az vm list-ip-addresses --resource-group "${user_number}-k8s-onprem" --name "${user_number}-k8s-master" --query "[0].virtualMachine.network.publicIpAddresses[0].ipAddress" --output tsv) +master_ip=$(az vm list-ip-addresses --resource-group "${user_number}-k8s-onprem" --name "${user_number}-k8s-master" --query "[0].virtualMachine.network.privateIpAddresses[0]" --output tsv) # Get the NodePort assigned to SQL MI node_port=$(kubectl get svc ${sqlmi_name}-external-svc -n ${user_number}-onprem -o jsonpath='{.spec.ports[0].nodePort}') echo "Connection details:" -echo "Server: $master_pip,$node_port" +echo "Server: $master_ip,$node_port" echo "Username: " echo "Password: " echo "" echo "Connection string:" -echo "Server=$master_pip,$node_port;Database=master;User Id=sa;Password=;TrustServerCertificate=true;" +echo "Server=$master_ip,$node_port;Database=master;User Id=sa;Password=;TrustServerCertificate=true;" ``` ### Connect using VS Code SQL Server extension @@ -172,14 +172,14 @@ echo "Server=$master_pip,$node_port;Database=master;User Id=sa;Password=,` (e.g., `20.123.45.67,31433`, optionally, use the following command: ```echo "$master_pip,$(kubectl get svc ${sqlmi_name}-external-svc -n ${custom_location} -o jsonpath='{.spec.ports[0].nodePort}')"``` + - **Server:** `,` (e.g., `20.123.45.67,31433`, optionally, use the following command: ```echo "$master_ip,$(kubectl get svc ${sqlmi_name}-external-svc -n ${custom_location} -o jsonpath='{.spec.ports[0].nodePort}')"``` - **Trust Server Certificate:** Yes - **Authentication Type:** SQL Login - **User name:** (the admin account you entered during SQL MI creation) diff --git a/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/walkthrough/challenge-05/solution.md b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/walkthrough/challenge-05/solution.md index c520d103e..a18f2422a 100644 --- a/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/walkthrough/challenge-05/solution.md +++ b/03-Azure/01-03-Infrastructure/03_Hybrid_Azure_Arc_Kubernetes/walkthrough/challenge-05/solution.md @@ -1,6 +1,6 @@ # Walkthrough Challenge 5 - Configure Gitops for cluster management -[Back to challenge](../../challenges/challenge-05.md) - [Next Challenge's Solution](../challenge-06/solution.md) +[Back to challenge](../../challenges/challenge-05.md) ### Prerequisites * [helm](https://helm.sh/docs/intro/install/) @@ -23,9 +23,9 @@ az extension add -n k8s-extension * Flux CLI installed (optional; not required for this challenge) * Extension microsoft.flux installed on your kubernetes cluster ```bash -# Extract user number from Azure username before '@' (e.g., LabUser-37@... -> 37) +# Extract user number from Azure username before '@' (e.g., LabUser-37@... or hackuser-067@... -> 37 or 67) azure_user=$(az account show --query user.name --output tsv) -user_number=$(echo "${azure_user%@*}" | grep -oE '[0-9]+' | tail -n1 | sed 's/^0*//; s/^$/0/') +user_number=$(echo "$azure_user" | cut -d'@' -f1 | sed -E -n 's/.*[^0-9]([0-9]+)$/\1/p' | sed 's/^0*//') export arc_resource_group="${user_number}-k8s-arc" export arc_cluster_name="${user_number}-k8s-arc-enabled"