anton-abyzov

Azure Bicep & AKS Expert

Azure Bicep and AKS expert for infrastructure-as-code deployments, Kubernetes cluster management, networking, identity, monitoring, and Container Apps. Generates modular Bicep templates with environment-specific parameter files.

anton-abyzov 158 22 Updated 7mo ago
GitHub

Install

npx skillscat add anton-abyzov/specweave/plugins-specweave-infrastructure-skills-azure-bicep-aks

Install via the SkillsCat registry.

About this skill

We need to produce a 2-3 sentence plain-text summary, objective, factual, no marketing language, no superlatives, no calls to action. At most 60 words. No quotes, no markdown. Just plain text. We need to explain what the skill does, what problem it solves, when to use it. We must not use bullet points, headings, markdown. Just natural prose. We need 2-3 sentences. Let's craft maybe 3 sentences, under 60 words. Possible summary: "The skill designs and implements Azure infrastructure using Bicep templates, focusing on AKS clusters, Container Apps, networking, identity, and monitoring.

SKILL.md

Azure Bicep & AKS Expert

Purpose

Design and implement Azure infrastructure using Bicep templates with a focus on AKS cluster management, Container Apps, networking, identity, and operational excellence. Produces modular, reusable Bicep code following Azure Well-Architected Framework principles.

When to Use

  • Writing Bicep templates for Azure resource deployments
  • Creating or managing AKS clusters
  • Designing Azure networking (VNet, NSG, Application Gateway)
  • Implementing Managed Identity and Workload Identity
  • Setting up monitoring with Azure Monitor and Container Insights
  • Choosing between AKS and Azure Container Apps
  • Building CI/CD pipelines with Azure DevOps or GitHub Actions for Azure

Bicep Language

Resource Declarations

// Parameters with decorators
@description('The Azure region for all resources')
param location string = resourceGroup().location

@allowed(['dev', 'staging', 'production'])
param environment string

@minLength(3)
@maxLength(24)
param storageAccountName string

@secure()
param adminPassword string

// Variables
var prefix = 'myapp-${environment}'
var tags = {
  Environment: environment
  ManagedBy: 'Bicep'
  Project: 'MyApp'
}

// Resource declaration
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' = {
  name: storageAccountName
  location: location
  tags: tags
  kind: 'StorageV2'
  sku: {
    name: environment == 'production' ? 'Standard_GRS' : 'Standard_LRS'
  }
  properties: {
    minimumTlsVersion: 'TLS1_2'
    supportsHttpsTrafficOnly: true
    allowBlobPublicAccess: false
    networkAcls: {
      defaultAction: 'Deny'
      bypass: 'AzureServices'
    }
  }
}

// Outputs
output storageId string = storageAccount.id
output storageName string = storageAccount.name

Conditions and Loops

// Conditional deployment
param deployRedis bool = false

resource redis 'Microsoft.Cache/redis@2023-08-01' = if (deployRedis) {
  name: '${prefix}-redis'
  location: location
  properties: {
    sku: {
      name: environment == 'production' ? 'Premium' : 'Basic'
      family: environment == 'production' ? 'P' : 'C'
      capacity: environment == 'production' ? 1 : 0
    }
    enableNonSslPort: false
    minimumTlsVersion: '1.2'
  }
}

// Loop over array
param appServices array = [
  { name: 'api', sku: 'P1v3' }
  { name: 'web', sku: 'P1v3' }
  { name: 'worker', sku: 'B1' }
]

resource appServicePlans 'Microsoft.Web/serverfarms@2023-12-01' = [
  for app in appServices: {
    name: '${prefix}-${app.name}-plan'
    location: location
    tags: tags
    sku: {
      name: app.sku
    }
    kind: 'linux'
    properties: {
      reserved: true
    }
  }
]

// Loop with index
resource nsgRules 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = [
  for (rule, i) in allowedPorts: {
    name: 'Allow-${rule.name}'
    parent: nsg
    properties: {
      priority: 100 + (i * 10)
      direction: 'Inbound'
      access: 'Allow'
      protocol: 'Tcp'
      sourceAddressPrefix: rule.source
      destinationPortRange: string(rule.port)
      sourcePortRange: '*'
      destinationAddressPrefix: '*'
    }
  }
]

User-Defined Types and Functions

// User-defined type
@export()
type environmentConfig = {
  name: 'dev' | 'staging' | 'production'
  skuTier: 'Basic' | 'Standard' | 'Premium'
  instanceCount: int
  enableHA: bool
}

// Using the type
param config environmentConfig = {
  name: 'production'
  skuTier: 'Premium'
  instanceCount: 3
  enableHA: true
}

// User-defined function
@export()
func getSubnetId(vnetId string, subnetName string) string =>
  '${vnetId}/subnets/${subnetName}'

// Discriminated union type
@export()
type storageConfig = {
  kind: 'blob'
  containerName: string
} | {
  kind: 'table'
  tableName: string
}

Module Composition

// main.bicep - orchestrator
targetScope = 'subscription'

param location string = 'eastus2'
param environment string

// Resource group
resource rg 'Microsoft.Resources/resourceGroups@2024-03-01' = {
  name: 'rg-myapp-${environment}'
  location: location
}

// Network module
module network 'modules/network.bicep' = {
  name: 'network-deployment'
  scope: rg
  params: {
    location: location
    environment: environment
    vnetAddressPrefix: '10.0.0.0/16'
  }
}

// AKS module (depends on network)
module aks 'modules/aks.bicep' = {
  name: 'aks-deployment'
  scope: rg
  params: {
    location: location
    environment: environment
    subnetId: network.outputs.aksSubnetId
    logAnalyticsWorkspaceId: monitoring.outputs.workspaceId
  }
}

// Monitoring module
module monitoring 'modules/monitoring.bicep' = {
  name: 'monitoring-deployment'
  scope: rg
  params: {
    location: location
    environment: environment
  }
}

Scope Targeting

// Subscription-level deployment
targetScope = 'subscription'

resource rg 'Microsoft.Resources/resourceGroups@2024-03-01' = {
  name: 'rg-platform'
  location: 'eastus2'
}

// Management group deployment
targetScope = 'managementGroup'

resource policy 'Microsoft.Authorization/policyDefinitions@2023-04-01' = {
  name: 'require-tags'
  properties: {
    policyType: 'Custom'
    mode: 'All'
    policyRule: {
      if: {
        field: 'tags.Environment'
        exists: 'false'
      }
      then: {
        effect: 'deny'
      }
    }
  }
}

What-If Deployment Preview

# Preview changes before deploying
az deployment group what-if \
  --resource-group rg-myapp-production \
  --template-file main.bicep \
  --parameters @parameters/production.bicepparam

# Subscription-level what-if
az deployment sub what-if \
  --location eastus2 \
  --template-file main.bicep \
  --parameters environment=production

AKS (Azure Kubernetes Service)

Cluster Creation with Bicep

// modules/aks.bicep
param location string
param environment string
param subnetId string
param logAnalyticsWorkspaceId string

var clusterName = 'aks-myapp-${environment}'

// Managed Identity for AKS
resource aksIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
  name: '${clusterName}-identity'
  location: location
}

// AKS Cluster
resource aksCluster 'Microsoft.ContainerService/managedClusters@2024-02-01' = {
  name: clusterName
  location: location
  identity: {
    type: 'UserAssigned'
    userAssignedIdentities: {
      '${aksIdentity.id}': {}
    }
  }
  sku: {
    name: 'Base'
    tier: environment == 'production' ? 'Standard' : 'Free'
  }
  properties: {
    dnsPrefix: clusterName
    kubernetesVersion: '1.30'
    networkProfile: {
      networkPlugin: 'azure'
      networkPluginMode: 'overlay'
      networkPolicy: 'calico'
      serviceCidr: '10.1.0.0/16'
      dnsServiceIP: '10.1.0.10'
      loadBalancerSku: 'standard'
    }

    // System node pool (required)
    agentPoolProfiles: [
      {
        name: 'system'
        count: environment == 'production' ? 3 : 1
        vmSize: 'Standard_D4s_v5'
        mode: 'System'
        osType: 'Linux'
        osSKU: 'AzureLinux'
        vnetSubnetID: subnetId
        enableAutoScaling: true
        minCount: environment == 'production' ? 3 : 1
        maxCount: environment == 'production' ? 5 : 3
        availabilityZones: environment == 'production' ? ['1', '2', '3'] : []
        nodeTaints: ['CriticalAddonsOnly=true:NoSchedule']
      }
    ]

    // Azure AD integration
    aadProfile: {
      managed: true
      enableAzureRBAC: true
    }

    // Monitoring
    addonProfiles: {
      omsagent: {
        enabled: true
        config: {
          logAnalyticsWorkspaceResourceID: logAnalyticsWorkspaceId
        }
      }
      azureKeyvaultSecretsProvider: {
        enabled: true
        config: {
          enableSecretRotation: 'true'
          rotationPollInterval: '120s'
        }
      }
    }

    // Security
    enableRBAC: true
    disableLocalAccounts: true
    securityProfile: {
      defender: {
        securityMonitoring: {
          enabled: environment == 'production'
        }
      }
      workloadIdentity: {
        enabled: true
      }
    }
    oidcIssuerProfile: {
      enabled: true
    }

    autoUpgradeProfile: {
      upgradeChannel: 'stable'
      nodeOSUpgradeChannel: 'NodeImage'
    }
  }
}

// User node pool (workloads)
resource userPool 'Microsoft.ContainerService/managedClusters/agentPools@2024-02-01' = {
  parent: aksCluster
  name: 'workload'
  properties: {
    mode: 'User'
    vmSize: 'Standard_D8s_v5'
    osType: 'Linux'
    osSKU: 'AzureLinux'
    vnetSubnetID: subnetId
    enableAutoScaling: true
    minCount: 2
    maxCount: 20
    availabilityZones: environment == 'production' ? ['1', '2', '3'] : []
    nodeLabels: {
      'workload-type': 'general'
    }
  }
}

// Spot instance pool (cost optimization)
resource spotPool 'Microsoft.ContainerService/managedClusters/agentPools@2024-02-01' = if (environment != 'production') {
  parent: aksCluster
  name: 'spot'
  properties: {
    mode: 'User'
    vmSize: 'Standard_D4s_v5'
    osType: 'Linux'
    scaleSetPriority: 'Spot'
    spotMaxPrice: -1  // Pay up to on-demand price
    scaleSetEvictionPolicy: 'Delete'
    enableAutoScaling: true
    minCount: 0
    maxCount: 10
    nodeTaints: ['kubernetes.azure.com/scalesetpriority=spot:NoSchedule']
    nodeLabels: {
      'kubernetes.azure.com/scalesetpriority': 'spot'
    }
  }
}

output clusterName string = aksCluster.name
output clusterFqdn string = aksCluster.properties.fqdn
output oidcIssuerUrl string = aksCluster.properties.oidcIssuerProfile.issuerURL

Workload Identity Setup

// Create a user-assigned managed identity for the workload
resource appIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
  name: 'id-myapp-${environment}'
  location: location
}

// Federated credential for Kubernetes service account
resource federatedCredential 'Microsoft.ManagedIdentity/userAssignedIdentities/federatedIdentityCredentials@2023-01-31' = {
  parent: appIdentity
  name: 'myapp-federated'
  properties: {
    issuer: aksCluster.properties.oidcIssuerProfile.issuerURL
    subject: 'system:serviceaccount:production:myapp-sa'
    audiences: ['api://AzureADTokenExchange']
  }
}
# Kubernetes ServiceAccount with Workload Identity annotation
apiVersion: v1
kind: ServiceAccount
metadata:
  name: myapp-sa
  namespace: production
  annotations:
    azure.workload.identity/client-id: "<managed-identity-client-id>"
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
  namespace: production
spec:
  template:
    metadata:
      labels:
        azure.workload.identity/use: "true"
    spec:
      serviceAccountName: myapp-sa
      containers:
        - name: myapp
          image: myacr.azurecr.io/myapp:latest

Networking: Azure CNI Overlay

// modules/network.bicep
param location string
param environment string
param vnetAddressPrefix string

resource vnet 'Microsoft.Network/virtualNetworks@2023-11-01' = {
  name: 'vnet-myapp-${environment}'
  location: location
  properties: {
    addressSpace: {
      addressPrefixes: [vnetAddressPrefix]
    }
    subnets: [
      {
        name: 'snet-aks'
        properties: {
          addressPrefix: cidrSubnet(vnetAddressPrefix, 20, 0)  // /20 for AKS nodes
          privateEndpointNetworkPolicies: 'Disabled'
        }
      }
      {
        name: 'snet-appgw'
        properties: {
          addressPrefix: cidrSubnet(vnetAddressPrefix, 24, 16)  // /24 for App Gateway
        }
      }
      {
        name: 'snet-private-endpoints'
        properties: {
          addressPrefix: cidrSubnet(vnetAddressPrefix, 24, 17)
          privateEndpointNetworkPolicies: 'Enabled'
        }
      }
    ]
  }
}

output vnetId string = vnet.id
output aksSubnetId string = vnet.properties.subnets[0].id
output appGwSubnetId string = vnet.properties.subnets[1].id

Ingress: Application Gateway Ingress Controller (AGIC)

resource appGateway 'Microsoft.Network/applicationGateways@2023-11-01' = {
  name: 'agw-myapp-${environment}'
  location: location
  properties: {
    sku: {
      name: 'WAF_v2'
      tier: 'WAF_v2'
    }
    autoscaleConfiguration: {
      minCapacity: 1
      maxCapacity: environment == 'production' ? 10 : 2
    }
    webApplicationFirewallConfiguration: {
      enabled: true
      firewallMode: 'Prevention'
      ruleSetType: 'OWASP'
      ruleSetVersion: '3.2'
    }
    gatewayIPConfigurations: [
      {
        name: 'appGatewayIpConfig'
        properties: {
          subnet: { id: appGwSubnetId }
        }
      }
    ]
    // Frontend, backend, routing configured by AGIC controller
  }
}

Monitoring: Container Insights + Prometheus

// modules/monitoring.bicep
resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2023-09-01' = {
  name: 'log-myapp-${environment}'
  location: location
  properties: {
    sku: { name: 'PerGB2018' }
    retentionInDays: environment == 'production' ? 90 : 30
  }
}

resource prometheusWorkspace 'Microsoft.Monitor/accounts@2023-04-03' = {
  name: 'prometheus-myapp-${environment}'
  location: location
}

resource grafanaDashboard 'Microsoft.Dashboard/grafana@2023-09-01' = {
  name: 'grafana-myapp-${environment}'
  location: location
  sku: { name: 'Standard' }
  identity: { type: 'SystemAssigned' }
  properties: {
    grafanaIntegrations: {
      azureMonitorWorkspaceIntegrations: [
        { azureMonitorWorkspaceResourceId: prometheusWorkspace.id }
      ]
    }
  }
}

output workspaceId string = logAnalytics.id

AKS vs Container Apps Decision Framework

┌──────────────────────┬──────────────────────┬──────────────────────┐
│ Criteria             │ AKS                  │ Container Apps       │
├──────────────────────┼──────────────────────┼──────────────────────┤
│ K8s expertise needed │ Yes                  │ No                   │
│ Custom controllers   │ Full control         │ Not supported        │
│ GPU workloads        │ Supported            │ Limited              │
│ Scale to zero        │ With KEDA            │ Built-in             │
│ Service mesh         │ Istio/Linkerd/custom │ Built-in Dapr        │
│ Pricing model        │ Pay for nodes        │ Pay per request      │
│ Microservices <10    │ Overkill             │ Ideal                │
│ Microservices 10+    │ Ideal                │ Gets complex         │
│ Compliance (PCI/SOC) │ Full control         │ Shared responsibility│
│ Stateful workloads   │ Full support         │ Limited              │
│ Startup time         │ Days to configure    │ Hours                │
└──────────────────────┴──────────────────────┴──────────────────────┘

RULE OF THUMB:
- < 10 microservices, no K8s expertise → Container Apps
- Complex orchestration, custom operators → AKS
- Event-driven, scale-to-zero critical → Container Apps
- Strict compliance, full network control → AKS

Azure Container Apps

// Container Apps Environment
resource containerAppEnv 'Microsoft.App/managedEnvironments@2024-03-01' = {
  name: 'cae-myapp-${environment}'
  location: location
  properties: {
    appLogsConfiguration: {
      destination: 'log-analytics'
      logAnalyticsConfiguration: {
        customerId: logAnalytics.properties.customerId
        sharedKey: logAnalytics.listKeys().primarySharedKey
      }
    }
    zoneRedundant: environment == 'production'
    workloadProfiles: [
      { name: 'Consumption', workloadProfileType: 'Consumption' }
      {
        name: 'Dedicated-D4'
        workloadProfileType: 'D4'
        minimumCount: 1
        maximumCount: 5
      }
    ]
  }
}

// Container App with Dapr and scaling rules
resource apiApp 'Microsoft.App/containerApps@2024-03-01' = {
  name: 'ca-api-${environment}'
  location: location
  properties: {
    managedEnvironmentId: containerAppEnv.id
    configuration: {
      ingress: {
        external: true
        targetPort: 8080
        transport: 'http2'
        corsPolicy: {
          allowedOrigins: ['https://myapp.com']
          allowedMethods: ['GET', 'POST', 'PUT', 'DELETE']
        }
      }
      dapr: {
        enabled: true
        appId: 'api'
        appPort: 8080
        appProtocol: 'http'
      }
      secrets: [
        {
          name: 'db-connection'
          keyVaultUrl: 'https://kv-myapp.vault.azure.net/secrets/db-connection'
          identity: appIdentity.id
        }
      ]
    }
    template: {
      containers: [
        {
          name: 'api'
          image: 'myacr.azurecr.io/api:latest'
          resources: {
            cpu: json('0.5')
            memory: '1Gi'
          }
          env: [
            { name: 'DB_CONNECTION', secretRef: 'db-connection' }
          ]
        }
      ]
      scale: {
        minReplicas: environment == 'production' ? 2 : 0
        maxReplicas: 20
        rules: [
          {
            name: 'http-scaling'
            http: { metadata: { concurrentRequests: '50' } }
          }
          {
            name: 'queue-scaling'
            custom: {
              type: 'azure-servicebus'
              metadata: {
                queueName: 'orders'
                messageCount: '10'
              }
              auth: [
                { secretRef: 'sb-connection', triggerParameter: 'connection' }
              ]
            }
          }
        ]
      }
    }
  }
}

Bicep Deployment Patterns

Parameter Files (.bicepparam)

// parameters/production.bicepparam
using '../main.bicep'

param environment = 'production'
param location = 'eastus2'
param deployRedis = true
param appServices = [
  { name: 'api', sku: 'P2v3' }
  { name: 'web', sku: 'P1v3' }
  { name: 'worker', sku: 'P1v3' }
]

CI/CD with GitHub Actions

name: Deploy Infrastructure
on:
  push:
    branches: [main]
    paths: ['infra/**']

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4

      - uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

      - name: What-If Preview
        uses: azure/arm-deploy@v2
        with:
          scope: subscription
          region: eastus2
          template: infra/main.bicep
          parameters: infra/parameters/production.bicepparam
          additionalArguments: --what-if

      - name: Deploy
        uses: azure/arm-deploy@v2
        with:
          scope: subscription
          region: eastus2
          template: infra/main.bicep
          parameters: infra/parameters/production.bicepparam

Common Azure Resources Reference

SQL Database

resource sqlServer 'Microsoft.Sql/servers@2023-08-01-preview' = {
  name: 'sql-myapp-${environment}'
  location: location
  properties: {
    administratorLogin: 'sqladmin'
    administratorLoginPassword: sqlAdminPassword
    minimalTlsVersion: '1.2'
    publicNetworkAccess: 'Disabled'
  }
}

resource sqlDb 'Microsoft.Sql/servers/databases@2023-08-01-preview' = {
  parent: sqlServer
  name: 'db-myapp'
  location: location
  sku: {
    name: environment == 'production' ? 'P2' : 'S1'
    tier: environment == 'production' ? 'Premium' : 'Standard'
  }
  properties: {
    collation: 'SQL_Latin1_General_CP1_CI_AS'
    maxSizeBytes: 268435456000  // 250 GB
    zoneRedundant: environment == 'production'
  }
}

Cosmos DB

resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2024-02-15-preview' = {
  name: 'cosmos-myapp-${environment}'
  location: location
  kind: 'GlobalDocumentDB'
  properties: {
    consistencyPolicy: {
      defaultConsistencyLevel: 'Session'
    }
    locations: [
      { locationName: location, failoverPriority: 0, isZoneRedundant: true }
    ]
    databaseAccountOfferType: 'Standard'
    enableAutomaticFailover: true
    enableMultipleWriteLocations: false
    publicNetworkAccess: 'Disabled'
    networkAclBypass: 'AzureServices'
    capacity: {
      totalThroughputLimit: environment == 'production' ? 10000 : 1000
    }
  }
}