diff --git a/plugin/skills/azure-prepare/references/services/app-service/README.md b/plugin/skills/azure-prepare/references/services/app-service/README.md index 07c7f0a04..1e76199c9 100644 --- a/plugin/skills/azure-prepare/references/services/app-service/README.md +++ b/plugin/skills/azure-prepare/references/services/app-service/README.md @@ -63,3 +63,6 @@ Endpoint should return 200 OK when healthy. - [Bicep Patterns](bicep.md) - [Deployment Slots](deployment-slots.md) - [Auto-Scaling](scaling.md) +- [Networking](networking.md) +- [SKU Selection](sku-selection.md) +- [Custom Domains](custom-domains.md) diff --git a/plugin/skills/azure-prepare/references/services/app-service/custom-domains.md b/plugin/skills/azure-prepare/references/services/app-service/custom-domains.md new file mode 100644 index 000000000..505824b90 --- /dev/null +++ b/plugin/skills/azure-prepare/references/services/app-service/custom-domains.md @@ -0,0 +1,173 @@ +# App Service Custom Domains and Managed TLS + +## Prerequisites + +| Requirement | Details | +|------------|---------| +| SKU tier | Basic (B1) or higher | +| DNS access | Ability to create CNAME, A, and TXT records | +| Domain ownership | Verified via TXT record | + +## DNS Configuration + +### Subdomain (CNAME) + +| Record Type | Name | Value | +|------------|------|-------| +| CNAME | `www` | `.azurewebsites.net` | +| TXT | `asuid.www` | `` | + +### Apex / Root Domain (A Record) + +| Record Type | Name | Value | +|------------|------|-------| +| A | `@` | `` | +| TXT | `asuid` | `` | + +Get the verification ID and IP address: + +```bash +# Get verification ID +az webapp show -n $APP -g $RG --query "customDomainVerificationId" -o tsv + +# Get IP address (for A records) +az webapp show -n $APP -g $RG --query "inboundIpAddress" -o tsv +``` + +> 💡 **Tip:** Prefer CNAME records for subdomains. For apex domains, consider using an Azure DNS alias record to avoid hardcoding IP addresses that may change. + +## Bind Custom Domain via CLI + +```bash +# Add custom domain +az webapp config hostname add -n $APP -g $RG --hostname www.contoso.com + +# Create managed certificate (free) +az webapp config ssl create -n $APP -g $RG --hostname www.contoso.com + +# Capture certificate thumbprint +THUMBPRINT=$(az webapp config ssl list -n $APP -g $RG \ + --query "[?contains(hostNames, 'www.contoso.com')].thumbprint | [0]" -o tsv) + +# Bind the certificate +az webapp config ssl bind -n $APP -g $RG \ + --certificate-thumbprint $THUMBPRINT --ssl-type SNI +``` + +## Bicep — Custom Domain with Managed Certificate + +```bicep +resource customDomain 'Microsoft.Web/sites/hostNameBindings@2022-09-01' = { + parent: webApp + name: 'www.contoso.com' + properties: { + siteName: webApp.name + hostNameType: 'Verified' + sslState: 'Disabled' // enable after cert is created + } +} + +resource managedCert 'Microsoft.Web/certificates@2022-09-01' = { + name: 'www.contoso.com' + location: location + properties: { + serverFarmId: appServicePlan.id + canonicalName: 'www.contoso.com' + } + dependsOn: [customDomain] +} +``` + +Then run a follow-up Bicep deployment to enable SNI and bind the managed certificate to the hostname: + +```bicep +resource managedCert 'Microsoft.Web/certificates@2022-09-01' existing = { + name: 'www.contoso.com' +} + +resource customDomainTlsBinding 'Microsoft.Web/sites/hostNameBindings@2022-09-01' = { + parent: webApp + name: 'www.contoso.com' + properties: { + siteName: webApp.name + hostNameType: 'Verified' + sslState: 'SniEnabled' + thumbprint: managedCert.properties.thumbprint + } +} +``` + +> ⚠️ **Warning:** Managed certificate creation requires the DNS records to be in place first. The hostname binding must exist before requesting the certificate. + +## Terraform — Custom Domain with Managed Certificate + +```hcl +resource "azurerm_app_service_custom_hostname_binding" "domain" { + hostname = "www.contoso.com" + app_service_name = azurerm_linux_web_app.app.name + resource_group_name = azurerm_resource_group.rg.name +} + +resource "azurerm_app_service_managed_certificate" "cert" { + custom_hostname_binding_id = azurerm_app_service_custom_hostname_binding.domain.id +} + +resource "azurerm_app_service_certificate_binding" "binding" { + hostname_binding_id = azurerm_app_service_custom_hostname_binding.domain.id + certificate_id = azurerm_app_service_managed_certificate.cert.id + ssl_state = "SniEnabled" +} +``` + +## TLS Options + +| Option | Cost | Renewal | Use Case | +|--------|------|---------|----------| +| App Service Managed Certificate | Free | Auto-renewed | Standard custom domains | +| App Service Certificate (purchased) | ~$70/yr | Auto-renewed | Extended validation, wildcard | +| Bring your own certificate | Varies | Manual | Enterprise PKI, specific CA | + +### Enforce HTTPS Only + +```bicep +resource webApp 'Microsoft.Web/sites@2022-09-01' = { + name: appName + location: location + properties: { + httpsOnly: true + // ... + } +} +``` + +```hcl +resource "azurerm_linux_web_app" "app" { + name = var.app_name + # ... + https_only = true +} +``` + +## Minimum TLS Version + +```bash +# Set minimum TLS version to 1.2 +az webapp config set -n $APP -g $RG --min-tls-version 1.2 +``` + +```bicep +siteConfig: { + minTlsVersion: '1.2' +} +``` + +> ⚠️ **Warning:** TLS 1.0 and 1.1 are deprecated. Always set minimum TLS version to 1.2 for production workloads. + +## Troubleshooting + +| Issue | Cause | Fix | +|-------|-------|-----| +| Domain verification fails | Missing TXT record | Add `asuid` TXT record and wait for DNS propagation | +| Certificate creation fails | DNS not yet propagated | Wait 5-15 min for propagation; verify with `nslookup` | +| SSL binding error | SKU too low | Upgrade to Basic (B1) or higher | +| Managed cert not renewing | DNS record changed | Verify CNAME/A record still points to the app | diff --git a/plugin/skills/azure-prepare/references/services/app-service/networking.md b/plugin/skills/azure-prepare/references/services/app-service/networking.md new file mode 100644 index 000000000..d3b5e9543 --- /dev/null +++ b/plugin/skills/azure-prepare/references/services/app-service/networking.md @@ -0,0 +1,200 @@ +# App Service Networking + +VNet integration, Private Endpoints, Access Restrictions, and Hybrid Connections. + +## Feature Availability by SKU + +| Feature | Free | Basic | Standard | Premium | Isolated | +|---------|:-:|:-:|:-:|:-:|:-:| +| VNet integration (outbound) | ❌ | ✅ | ✅ | ✅ | ✅ (native) | +| Private Endpoints (inbound) | ❌ | ✅ | ✅ | ✅ | ✅ | +| Access Restrictions | ✅ | ✅ | ✅ | ✅ | ✅ | +| Hybrid Connections | ❌ | 5 | 25 | 200 | 200 | +| Access to service-endpoint-protected resources | ❌ | ✅ | ✅ | ✅ | ✅ | +> Note: Service endpoints are configured on VNets/subnets and downstream services (e.g., Storage, SQL). App Service accesses them via VNet integration rather than enabling service endpoints directly on the app. + +## VNet Integration (Outbound) + +Routes outbound traffic from the app through a VNet subnet, enabling access to private resources (databases, storage, VMs). + +### Subnet Requirements + +| Requirement | Value | +|------------|-------| +| Minimum subnet size | `/26` (64 addresses) recommended | +| Delegation | `Microsoft.Web/serverFarms` | +| Dedicated | One subnet per App Service plan | + +### Bicep — VNet Integration + +```bicep +resource subnet 'Microsoft.Network/virtualNetworks/subnets@2023-11-01' = { + parent: vnet + name: 'app-service-subnet' + properties: { + addressPrefix: '10.0.1.0/26' + delegations: [ + { + name: 'Microsoft.Web.serverFarms' + properties: { serviceName: 'Microsoft.Web/serverFarms' } + } + ] + } +} + +resource webApp 'Microsoft.Web/sites@2024-11-01' = { + name: appName + location: location + properties: { + serverFarmId: appServicePlan.id + virtualNetworkSubnetId: subnet.id + outboundVnetRouting: { + allTraffic: true // route all outbound through VNet + } + } +} +``` + +### CLI - VNet Integration + +```bash +# Configure virtual network integration +az webapp vnet-integration add --resource-group RG --name APP --vnet VNET --subnet SUBNET + +# Update app configuration to route all outbound traffic through the virtual network integration +az resource update --resource-group RG --name APP --resource-type "Microsoft.Web/sites" --set properties.outboundVnetRouting.allTraffic=true +``` + + +> 💡 **Tip:** Set `outboundVnetRouting.allTraffic: true` to route ALL outbound traffic through the VNet. Without this, only RFC1918 traffic is routed through the VNet. + +## Private Endpoints (Inbound) + +Expose the app on a private IP address within your VNet. Public access can be disabled entirely. + +### Bicep — Private Endpoint + +```bicep +resource privateEndpoint 'Microsoft.Network/privateEndpoints@2023-11-01' = { + name: '${appName}-pe' + location: location + properties: { + subnet: { id: privateEndpointSubnet.id } + privateLinkServiceConnections: [ + { + name: '${appName}-connection' + properties: { + privateLinkServiceId: webApp.id + groupIds: ['sites'] + } + } + ] + } +} + +resource privateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = { + name: 'privatelink.azurewebsites.net' + location: 'global' +} + +resource dnsLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = { + parent: privateDnsZone + name: '${vnet.name}-link' + location: 'global' + properties: { + virtualNetwork: { id: vnet.id } + registrationEnabled: false + } +} + +resource privateDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2023-11-01' = { + parent: privateEndpoint + name: 'default' + properties: { + privateDnsZoneConfigs: [ + { + name: 'webapp-dns-zone' + properties: { + privateDnsZoneId: privateDnsZone.id + } + } + ] + } +} +``` + +### CLI - Private Endpoint + +```bash +# Retrieve web app resource id +id=$(az webapp show --name APP --resource-group RG --query id --output tsv) + +# Create Private Endpoint +az network private-endpoint create --connection-name CONNECTIONNAME --name private-endpoint --private-connection-resource-id $id --resource-group RG --subnet SUBNET --group-id sites --vnet-name VNET + +# Create Private DNS Zone +az network private-dns zone create --resource-group RG --name "privatelink.azurewebsites.net" + +# Link the DNS Zone to virtual network +az network private-dns link vnet create --resource-group RG --zone-name "privatelink.azurewebsites.net" --name dns-link --virtual-network VNET --registration-enabled false + +``` + +> ⚠️ **Warning:** Private Endpoints require Basic (B1+) or higher tier. The private DNS zone `privatelink.azurewebsites.net` must be linked to the VNet for name resolution. + +## Access Restrictions + +Control inbound access with IP-based or service-tag rules. Available on all SKUs. + +### Bicep — Access Restrictions + +```bicep +siteConfig: { + ipSecurityRestrictions: [ + { + name: 'allow-office' + priority: 100 + action: 'Allow' + ipAddress: '203.0.113.0/24' + } + { + name: 'deny-all' + priority: 2147483647 + action: 'Deny' + ipAddress: 'Any' + } + ] + scmIpSecurityRestrictionsUseMain: true +} +``` + +### CLI - Access Restrictions + +```bash +# Add restriction to allow traffic from set range used by the office +az webapp config access-restriction add --resource-group RG --name APP --rule-name 'allow-office' --action Allow --ip-address 203.0.113.0/24 --priority 100 + +# Add restriction to deny access from any other address range +az webapp config access-restriction add --resource-group RG --name APP --rule-name 'deny-all' --action Deny --ip-address Any --priority 2147483647 + +# Set SCM Site (Kudu) to use same access restrictions as main site +az webapp config access-restriction set -g RG -n APP --use-same-restrictions-for-scm-site true +``` + +> 💡 **Tip:** Always restrict the SCM/Kudu site too. Use `scmIpSecurityRestrictionsUseMain: true` to inherit main site rules, or define separate SCM rules. + +## Hybrid Connections + +Connect to on-premises resources without VPN. Requires Basic tier or higher. Uses Hybrid Connection Manager (HCM) agent on-premises relaying through Azure Relay. + +> ⚠️ **Warning:** Each Hybrid Connection maps to a single host:port endpoint. Basic tier supports 5; Standard tier supports 25; Premium/Isolated support 200. + +## Troubleshooting + +| Issue | Cause | Fix | +|-------|-------|-----| +| Cannot reach private DB | VNet integration not enabled | Enable VNet integration; check `outboundVnetRouting.allTraffic` | +| DNS resolution fails | Private DNS zone not linked | Link `privatelink.*` DNS zone to VNet | +| Access restriction not working | Priority ordering wrong | Lower numbers = higher priority; check rule order | +| Hybrid Connection timeout | HCM not running | Verify HCM service status on-premises | +| Outbound traffic blocked | NSG rules on subnet | Allow outbound to required services in NSG | diff --git a/plugin/skills/azure-prepare/references/services/app-service/sku-selection.md b/plugin/skills/azure-prepare/references/services/app-service/sku-selection.md new file mode 100644 index 000000000..681ff1d92 --- /dev/null +++ b/plugin/skills/azure-prepare/references/services/app-service/sku-selection.md @@ -0,0 +1,118 @@ +# App Service SKU Selection + +## SKU Comparison Matrix + +| Feature | Free (F1) | Basic (B1-B3) | Standard (S1-S3) | Premium (P0v3-P3v3 and P1Mv3-P5Mv3;P0v4-P3v4 and P1Mv4-P5Mv4) | Isolated (I1v2-I6v2) | +|---------|:-:|:-:|:-:|:-:|:-:| +| **Custom domains** | ❌ | ✅ | ✅ | ✅ | ✅ | +| **TLS/SSL bindings** | ❌ | ✅ (SNI) | ✅ (SNI + IP) | ✅ (SNI + IP) | ✅ (SNI + IP) | +| **Deployment slots** | ❌ | ❌ | 5 | 20 | 20 | +| **Auto-scale** | ❌ | ❌ | ✅ (10 inst.) | ✅ (30 inst.) | ✅ (100 inst.) | +| **VNet integration** | ❌ | ✅ | ✅ | ✅ | ✅ (ASE is in VNet) | +| **Private endpoints** | ❌ | ✅ | ✅ | ✅ | ✅ | +| **Always On** | ❌ | ✅ | ✅ | ✅ | ✅ | +| **Backup/Restore** | ❌ | ❌ | ✅ | ✅ | ✅ | +| **Hybrid Connections** | ❌ | 5 | 25 | 200 | 200 | +| **Traffic Manager** | ✅ | ✅ | ✅ | ✅ | ✅ | +| **SLA** | None | None | 99.95% | 99.95% | 99.95% | + +## Pricing Overview + +| SKU | vCPU | RAM | Storage | Approx. Monthly Cost | +|-----|------|-----|---------|---------------------| +| F1 | Shared | 1 GB | 1 GB | Free | +| B1 | 1 | 1.75 GB | 10 GB | ~$55 | +| B2 | 2 | 3.5 GB | 10 GB | ~$110 | +| S1 | 1 | 1.75 GB | 50 GB | ~$73 | +| S2 | 2 | 3.5 GB | 50 GB | ~$146 | +| P1v3 | 2 | 8 GB | 250 GB | ~$138 | +| P2v3 | 4 | 16 GB | 250 GB | ~$276 | +| P3v3 | 8 | 32 GB | 250 GB | ~$552 | +| I1v2 | 2 | 8 GB | 1 TB | ~$460 | + +> 💡 **Tip:** Figures are representative for **Windows OS** in **Central US**, **as of 2026-04**. Prices vary by region, OS, and offer. Use the [Azure Pricing Calculator](https://azure.microsoft.com/pricing/calculator/) for exact figures. + +### Save by using Reserved Instances and Savings Plans + +Cost savings can be made on Premium V3, Premium V4 and Isolated V2 plans by committing to reserved instances for 1 or 3 year terms, details can found at [https://learn.microsoft.com/azure/cost-management-billing/reservations/prepay-app-service](https://learn.microsoft.com/azure/cost-management-billing/reservations/prepay-app-service). + +Alternatively cost savings can be made using [Azure Savings plans](https://learn.microsoft.com/en-us/azure/cost-management-billing/savings-plan/). + +## Decision Criteria + +``` +Production workload? +├─ No → Free (F1) or Basic (B1) for dev/test +└─ Yes + Need deployment slots, auto-scale, or backups? + ├─ No → Basic (B1-B3) if budget-constrained (supports VNet integration and Private Endpoints) + └─ Yes + Need network isolation (dedicated ASE)? + ├─ Yes → Isolated (I1v2+) + └─ No + Need more than 5 deployment slots or more than 10 instances? + ├─ Yes → Premium (P1v3+) + └─ No → Standard (S1-S3) with Private Endpoints and VNet integration +``` + +## Feature Unlock Summary + +Key features unlocked at each tier: + +| Upgrade Path | Features Gained | +|-------------|-----------------| +| Free → Basic | Custom domains, TLS/SSL, Always On, VNet Integration, Private Endpoints, Hybrid Connections (5) | +| Basic → Standard | Deployment slots, auto-scale, backups | +| Standard → Premium | More slots (20), higher scale (30 inst.) | +| Premium → Isolated | Full network isolation (ASE), dedicated infrastructure | + +## Bicep — App Service Plan with SKU + +```bicep +resource appServicePlan 'Microsoft.Web/serverfarms@2025-03-01' = { + name: planName + location: location + sku: { + name: 'P1v3' + tier: 'PremiumV3' + capacity: 2 // number of instances + } + kind: 'linux' + properties: { + reserved: true // required for Linux + } +} +``` + +## Terraform — App Service Plan with SKU + +```hcl +resource "azurerm_service_plan" "plan" { + name = var.plan_name + location = azurerm_resource_group.rg.location + resource_group_name = azurerm_resource_group.rg.name + os_type = "Linux" + sku_name = "P1v3" +} +``` + +## Scaling Within a Tier + +Scale up (change SKU) vs scale out (add instances): + +| Strategy | When to Use | How | +|----------|-------------|-----| +| Scale up | App needs more CPU/RAM | Change SKU (e.g., S1 → S2) | +| Scale out | Handle more concurrent load | Increase instance count or enable auto-scale | + +> ⚠️ **Warning:** Scaling from one tier family to another (e.g., Standard to Premium) may cause a brief restart. Schedule changes during low-traffic windows. + +## Recommendations by Workload + +| Workload | Recommended SKU | Reason | +|----------|----------------|--------| +| Personal blog / prototype | F1 or B1 | Minimal cost, no SLA needed | +| Team dev/test | B1-B2 | Always On, custom domain | +| Production API | S1-S3 (P0v3/P0v4+ for higher scale/perf) | Auto-scale, slots, VNet | +| Enterprise with compliance | P1v3+/P1v4+ | Private endpoints, 20 slots, 30 instances | +| Regulated / multi-tenant SaaS | I1v2+ | Full network isolation |