├── images ├── openai.png ├── magic8ball.png ├── architecture.png └── federatedidentitycredentials.png ├── scripts ├── .env ├── images │ ├── robot.png │ └── magic8ball.png ├── service.yml ├── 10-create-ingress.sh ├── 01-build-docker-image.sh ├── configMap.yml ├── cluster-issuer.yml ├── 03-push-docker-image.sh ├── 06-create-cluster-issuer.sh ├── 02-run-docker-container.sh ├── ingress.yml ├── 05-install-cert-manager.sh ├── 04-create-nginx-ingress-controller.sh ├── 09-deploy-app.sh ├── 00-variables.sh ├── requirements.txt ├── 11-configure-dns.sh ├── deployment.yml ├── 08-create-service-account.sh ├── 07-create-workload-managed-identity.sh ├── Dockerfile └── app.py ├── visio └── architecture.vsdx ├── CHANGELOG.md ├── bicep ├── networkInterface.bicep ├── kubeletManagedIdentity.bicep ├── logAnalytics.bicep ├── containerRegistry.bicep ├── openAi.bicep ├── storageAccount.bicep ├── keyVault.bicep ├── aksManagedIdentity.bicep ├── deploymentScript.bicep ├── virtualMachine.bicep ├── install-nginx-via-helm-and-create-sa.sh ├── main.parameters.json ├── applicationGateway.bicep ├── deploy.sh ├── metricAlerts.bicep └── network.bicep ├── .github ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── LICENSE.md ├── CONTRIBUTING.md └── .gitignore /images/openai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/aks-openai/HEAD/images/openai.png -------------------------------------------------------------------------------- /images/magic8ball.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/aks-openai/HEAD/images/magic8ball.png -------------------------------------------------------------------------------- /scripts/.env: -------------------------------------------------------------------------------- 1 | AZURE_OPENAI_TYPE="azure_ad" 2 | AZURE_OPENAI_BASE="https://baboopenai.openai.azure.com/" -------------------------------------------------------------------------------- /images/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/aks-openai/HEAD/images/architecture.png -------------------------------------------------------------------------------- /scripts/images/robot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/aks-openai/HEAD/scripts/images/robot.png -------------------------------------------------------------------------------- /visio/architecture.vsdx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/aks-openai/HEAD/visio/architecture.vsdx -------------------------------------------------------------------------------- /scripts/images/magic8ball.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/aks-openai/HEAD/scripts/images/magic8ball.png -------------------------------------------------------------------------------- /images/federatedidentitycredentials.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/aks-openai/HEAD/images/federatedidentitycredentials.png -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [project-title] Changelog 2 | 3 | 4 | # x.y.z (yyyy-mm-dd) 5 | 6 | *Features* 7 | * ... 8 | 9 | *Bug Fixes* 10 | * ... 11 | 12 | *Breaking Changes* 13 | * ... 14 | -------------------------------------------------------------------------------- /scripts/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: magic8ball 5 | labels: 6 | app: magic8ball 7 | spec: 8 | type: ClusterIP 9 | ports: 10 | - protocol: TCP 11 | port: 8501 12 | selector: 13 | app: magic8ball 14 | -------------------------------------------------------------------------------- /scripts/10-create-ingress.sh: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | 3 | # Variables 4 | source ./00-variables.sh 5 | 6 | # Create the ingress 7 | echo "[$ingressName] ingress does not exist" 8 | echo "Creating [$ingressName] ingress..." 9 | cat $ingressTemplate | 10 | yq "(.spec.tls[0].hosts[0])|="\""$host"\" | 11 | yq "(.spec.rules[0].host)|="\""$host"\" | 12 | kubectl apply -n $namespace -f - -------------------------------------------------------------------------------- /bicep/networkInterface.bicep: -------------------------------------------------------------------------------- 1 | // Parameters 2 | @description('Specifies the name of the Network Interface.') 3 | param name string 4 | 5 | // Resources 6 | resource networkInterface 'Microsoft.Network/networkInterfaces@2021-08-01' existing = { 7 | name: name 8 | } 9 | 10 | // Outputs 11 | output privateIPAddress string = networkInterface.properties.ipConfigurations[0].properties.privateIPAddress 12 | -------------------------------------------------------------------------------- /scripts/01-build-docker-image.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # For more information, see: 4 | # * https://hub.docker.com/_/python 5 | # * https://docs.streamlit.io/knowledge-base/tutorials/deploy/docker 6 | # * https://stackoverflow.com/questions/30494050/how-do-i-pass-environment-variables-to-docker-containers 7 | 8 | # Variables 9 | source ./00-variables.sh 10 | 11 | # Build the docker image 12 | docker build -t $imageName:$tag -f Dockerfile . -------------------------------------------------------------------------------- /scripts/configMap.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: magic8ball-configmap 5 | data: 6 | TITLE: "Magic 8 Ball" 7 | LABEL: "Pose your question and cross your fingers!" 8 | TEMPERATURE: "0.9" 9 | IMAGE_WIDTH: "80" 10 | AZURE_OPENAI_TYPE: azure_ad 11 | AZURE_OPENAI_BASE: https://baboopenai.openai.azure.com/ 12 | AZURE_OPENAI_KEY: "" 13 | AZURE_OPENAI_MODEL: gpt-35-turbo 14 | AZURE_OPENAI_DEPLOYMENT: magic8ballGPT 15 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /scripts/cluster-issuer.yml: -------------------------------------------------------------------------------- 1 | apiVersion: cert-manager.io/v1 2 | kind: ClusterIssuer 3 | metadata: 4 | name: letsencrypt-nginx 5 | spec: 6 | acme: 7 | server: https://acme-v02.api.letsencrypt.org/directory 8 | email: paolos@microsoft.com 9 | privateKeySecretRef: 10 | name: letsencrypt 11 | solvers: 12 | - http01: 13 | ingress: 14 | class: nginx 15 | podTemplate: 16 | spec: 17 | nodeSelector: 18 | "kubernetes.io/os": linux -------------------------------------------------------------------------------- /scripts/03-push-docker-image.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Variables 4 | source ./00-variables.sh 5 | 6 | # Login to ACR 7 | az acr login --name $acrName 8 | 9 | # Retrieve ACR login server. Each container image needs to be tagged with the loginServer name of the registry. 10 | loginServer=$(az acr show --name $acrName --query loginServer --output tsv) 11 | 12 | # Tag the local image with the loginServer of ACR 13 | docker tag ${imageName,,}:$tag $loginServer/${imageName,,}:$tag 14 | 15 | # Push latest container image to ACR 16 | docker push $loginServer/${imageName,,}:$tag -------------------------------------------------------------------------------- /scripts/06-create-cluster-issuer.sh: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | 3 | # Variables 4 | source ./00-variables.sh 5 | 6 | # Check if the cluster issuer already exists 7 | result=$(kubectl get ClusterIssuer -o json | jq -r '.items[].metadata.name | select(. == "'$clusterIssuerName'")') 8 | 9 | if [[ -n $result ]]; then 10 | echo "[$clusterIssuerName] cluster issuer already exists" 11 | exit 12 | else 13 | # Create the cluster issuer 14 | echo "[$clusterIssuerName] cluster issuer does not exist" 15 | echo "Creating [$clusterIssuerName] cluster issuer..." 16 | cat $clusterIssuerTemplate | 17 | yq "(.spec.acme.email)|="\""$email"\" | 18 | kubectl apply -f - 19 | fi 20 | -------------------------------------------------------------------------------- /scripts/02-run-docker-container.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # For more information, see: 4 | # * https://hub.docker.com/_/python 5 | # * https://docs.streamlit.io/knowledge-base/tutorials/deploy/docker 6 | # * https://stackoverflow.com/questions/30494050/how-do-i-pass-environment-variables-to-docker-containers 7 | 8 | # Variables 9 | source ./00-variables.sh 10 | 11 | # Run the docker container 12 | docker run -it \ 13 | --rm \ 14 | -p 8501:8501 \ 15 | -e TEMPERATURE=$temperature \ 16 | -e AZURE_OPENAI_BASE=$AZURE_OPENAI_BASE \ 17 | -e AZURE_OPENAI_KEY=$AZURE_OPENAI_KEY \ 18 | -e AZURE_OPENAI_MODEL=$AZURE_OPENAI_MODEL \ 19 | -e AZURE_OPENAI_DEPLOYMENT=$AZURE_OPENAI_DEPLOYMENT \ 20 | --name $containerName \ 21 | $imageName:$tag -------------------------------------------------------------------------------- /scripts/ingress.yml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | name: magic8ball-ingress 5 | annotations: 6 | cert-manager.io/cluster-issuer: letsencrypt-nginx 7 | cert-manager.io/acme-challenge-type: http01 8 | nginx.ingress.kubernetes.io/proxy-connect-timeout: "360" 9 | nginx.ingress.kubernetes.io/proxy-send-timeout: "360" 10 | nginx.ingress.kubernetes.io/proxy-read-timeout: "360" 11 | nginx.ingress.kubernetes.io/proxy-next-upstream-timeout: "360" 12 | nginx.ingress.kubernetes.io/configuration-snippet: | 13 | more_set_headers "X-Frame-Options: SAMEORIGIN"; 14 | spec: 15 | ingressClassName: nginx 16 | tls: 17 | - hosts: 18 | - magic8ball.babosbird.com 19 | secretName: tls-secret 20 | rules: 21 | - host: magic8ball.babosbird.com 22 | http: 23 | paths: 24 | - path: / 25 | pathType: Prefix 26 | backend: 27 | service: 28 | name: magic8ball 29 | port: 30 | number: 8501 -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 4 | > Please provide us with the following information: 5 | > --------------------------------------------------------------- 6 | 7 | ### This issue is for a: (mark with an `x`) 8 | ``` 9 | - [ ] bug report -> please search issues before submitting 10 | - [ ] feature request 11 | - [ ] documentation issue or request 12 | - [ ] regression (a behavior that used to work and stopped in a new release) 13 | ``` 14 | 15 | ### Minimal steps to reproduce 16 | > 17 | 18 | ### Any log messages given by the failure 19 | > 20 | 21 | ### Expected/desired behavior 22 | > 23 | 24 | ### OS and Version? 25 | > Windows 7, 8 or 10. Linux (which distribution). macOS (Yosemite? El Capitan? Sierra?) 26 | 27 | ### Versions 28 | > 29 | 30 | ### Mention any other details that might be useful 31 | 32 | > --------------------------------------------------------------- 33 | > Thanks! We'll be in touch soon. 34 | -------------------------------------------------------------------------------- /bicep/kubeletManagedIdentity.bicep: -------------------------------------------------------------------------------- 1 | // Parameters 2 | @description('Specifies the name of the existing AKS cluster.') 3 | param aksClusterName string 4 | 5 | @description('Specifies the name of the existing container registry.') 6 | param acrName string 7 | 8 | // Variables 9 | var acrPullRoleDefinitionId = resourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') 10 | 11 | // Resources 12 | resource aksCluster 'Microsoft.ContainerService/managedClusters@2022-03-02-preview' existing = { 13 | name: aksClusterName 14 | } 15 | 16 | resource containerRegistry 'Microsoft.ContainerRegistry/registries@2021-12-01-preview' existing = { 17 | name: acrName 18 | } 19 | 20 | resource acrPullRoleAssignmentName 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 21 | name: guid(aksCluster.name, containerRegistry.id, acrPullRoleDefinitionId) 22 | scope: containerRegistry 23 | properties: { 24 | roleDefinitionId: acrPullRoleDefinitionId 25 | principalId: any(aksCluster.properties.identityProfile.kubeletidentity).objectId 26 | principalType: 'ServicePrincipal' 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Purpose 2 | 3 | * ... 4 | 5 | ## Does this introduce a breaking change? 6 | 7 | ``` 8 | [ ] Yes 9 | [ ] No 10 | ``` 11 | 12 | ## Pull Request Type 13 | What kind of change does this Pull Request introduce? 14 | 15 | 16 | ``` 17 | [ ] Bugfix 18 | [ ] Feature 19 | [ ] Code style update (formatting, local variables) 20 | [ ] Refactoring (no functional changes, no api changes) 21 | [ ] Documentation content changes 22 | [ ] Other... Please describe: 23 | ``` 24 | 25 | ## How to Test 26 | * Get the code 27 | 28 | ``` 29 | git clone [repo-address] 30 | cd [repo-name] 31 | git checkout [branch-name] 32 | npm install 33 | ``` 34 | 35 | * Test the code 36 | 37 | ``` 38 | ``` 39 | 40 | ## What to Check 41 | Verify that the following are valid 42 | * ... 43 | 44 | ## Other Information 45 | -------------------------------------------------------------------------------- /scripts/05-install-cert-manager.sh: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | 3 | # Variables 4 | source ./00-variables.sh 5 | 6 | # Check if the ingress-nginx repository is not already added 7 | result=$(helm repo list | grep $cmRepoName | awk '{print $1}') 8 | 9 | if [[ -n $result ]]; then 10 | echo "[$cmRepoName] Helm repo already exists" 11 | else 12 | # Add the Jetstack Helm repository 13 | echo "Adding [$cmRepoName] Helm repo..." 14 | helm repo add $cmRepoName $cmRepoUrl 15 | fi 16 | 17 | # Update your local Helm chart repository cache 18 | echo 'Updating Helm repos...' 19 | helm repo update 20 | 21 | # Install cert-manager Helm chart 22 | result=$(helm list -n $cmNamespace | grep $cmReleaseName | awk '{print $1}') 23 | 24 | if [[ -n $result ]]; then 25 | echo "[$cmReleaseName] cert-manager already exists in the $cmNamespace namespace" 26 | else 27 | # Install the cert-manager Helm chart 28 | echo "Deploying [$cmReleaseName] cert-manager to the $cmNamespace namespace..." 29 | helm install $cmReleaseName $cmRepoName/$cmChartName \ 30 | --create-namespace \ 31 | --namespace $cmNamespace \ 32 | --set installCRDs=true \ 33 | --set nodeSelector."kubernetes\.io/os"=linux 34 | fi 35 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE -------------------------------------------------------------------------------- /bicep/logAnalytics.bicep: -------------------------------------------------------------------------------- 1 | // Parameters 2 | @description('Specifies the name of the Log Analytics workspace.') 3 | param name string 4 | 5 | @description('Specifies the service tier of the workspace: Free, Standalone, PerNode, Per-GB.') 6 | @allowed([ 7 | 'Free' 8 | 'Standalone' 9 | 'PerNode' 10 | 'PerGB2018' 11 | ]) 12 | param sku string = 'PerNode' 13 | 14 | @description('Specifies the workspace data retention in days. -1 means Unlimited retention for the Unlimited Sku. 730 days is the maximum allowed for all other Skus.') 15 | param retentionInDays int = 60 16 | 17 | @description('Specifies the location.') 18 | param location string = resourceGroup().location 19 | 20 | @description('Specifies the resource tags.') 21 | param tags object 22 | 23 | // Resources 24 | resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' = { 25 | name: name 26 | tags: tags 27 | location: location 28 | properties: { 29 | sku: { 30 | name: sku 31 | } 32 | retentionInDays: retentionInDays 33 | } 34 | } 35 | 36 | //Outputs 37 | output id string = logAnalyticsWorkspace.id 38 | output name string = logAnalyticsWorkspace.name 39 | output customerId string = logAnalyticsWorkspace.properties.customerId 40 | -------------------------------------------------------------------------------- /scripts/04-create-nginx-ingress-controller.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Variables 4 | source ./00-variables.sh 5 | 6 | # Use Helm to deploy an NGINX ingress controller 7 | result=$(helm list -n $nginxNamespace | grep $nginxReleaseName | awk '{print $1}') 8 | 9 | if [[ -n $result ]]; then 10 | echo "[$nginxReleaseName] ingress controller already exists in the [$nginxNamespace] namespace" 11 | else 12 | # Check if the ingress-nginx repository is not already added 13 | result=$(helm repo list | grep $nginxRepoName | awk '{print $1}') 14 | 15 | if [[ -n $result ]]; then 16 | echo "[$nginxRepoName] Helm repo already exists" 17 | else 18 | # Add the ingress-nginx repository 19 | echo "Adding [$nginxRepoName] Helm repo..." 20 | helm repo add $nginxRepoName $nginxRepoUrl 21 | fi 22 | 23 | # Update your local Helm chart repository cache 24 | echo 'Updating Helm repos...' 25 | helm repo update 26 | 27 | # Deploy NGINX ingress controller 28 | echo "Deploying [$nginxReleaseName] NGINX ingress controller to the [$nginxNamespace] namespace..." 29 | helm install $nginxReleaseName $nginxRepoName/$nginxChartName \ 30 | --create-namespace \ 31 | --namespace $nginxNamespace \ 32 | --set controller.nodeSelector."kubernetes\.io/os"=linux \ 33 | --set controller.replicaCount=$replicaCount \ 34 | --set defaultBackend.nodeSelector."kubernetes\.io/os"=linux \ 35 | --set controller.service.annotations."service\.beta\.kubernetes\.io/azure-load-balancer-health-probe-request-path"=/healthz 36 | fi 37 | 38 | # Get values 39 | helm get values $nginxReleaseName --namespace $nginxNamespace 40 | -------------------------------------------------------------------------------- /scripts/09-deploy-app.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Variables 4 | source ./00-variables.sh 5 | 6 | # Attach ACR to AKS cluster 7 | if [[ $attachAcr == true ]]; then 8 | echo "Attaching ACR $acrName to AKS cluster $aksClusterName..." 9 | az aks update \ 10 | --name $aksClusterName \ 11 | --resource-group $aksResourceGroupName \ 12 | --attach-acr $acrName 13 | fi 14 | 15 | # Check if namespace exists in the cluster 16 | result=$(kubectl get namespace -o jsonpath="{.items[?(@.metadata.name=='$namespace')].metadata.name}") 17 | 18 | if [[ -n $result ]]; then 19 | echo "$namespace namespace already exists in the cluster" 20 | else 21 | echo "$namespace namespace does not exist in the cluster" 22 | echo "creating $namespace namespace in the cluster..." 23 | kubectl create namespace $namespace 24 | fi 25 | 26 | # Create config map 27 | cat $configMapTemplate | 28 | yq "(.data.TITLE)|="\""$title"\" | 29 | yq "(.data.LABEL)|="\""$label"\" | 30 | yq "(.data.TEMPERATURE)|="\""$temperature"\" | 31 | yq "(.data.IMAGE_WIDTH)|="\""$imageWidth"\" | 32 | yq "(.data.AZURE_OPENAI_TYPE)|="\""$openAiType"\" | 33 | yq "(.data.AZURE_OPENAI_BASE)|="\""$openAiBase"\" | 34 | yq "(.data.AZURE_OPENAI_MODEL)|="\""$openAiModel"\" | 35 | yq "(.data.AZURE_OPENAI_DEPLOYMENT)|="\""$openAiDeployment"\" | 36 | kubectl apply -n $namespace -f - 37 | 38 | # Create deployment 39 | cat $deploymentTemplate | 40 | yq "(.spec.template.spec.containers[0].image)|="\""$image"\" | 41 | yq "(.spec.template.spec.containers[0].imagePullPolicy)|="\""$imagePullPolicy"\" | 42 | yq "(.spec.template.spec.serviceAccountName)|="\""$serviceAccountName"\" | 43 | kubectl apply -n $namespace -f - 44 | 45 | # Create deployment 46 | kubectl apply -f $serviceTemplate -n $namespace -------------------------------------------------------------------------------- /scripts/00-variables.sh: -------------------------------------------------------------------------------- 1 | # Variables 2 | acrName="GreyAcr" 3 | acrResourceGrougName="GreyRG" 4 | location="FranceCentral" 5 | attachAcr=false 6 | imageName="magic8ball" 7 | tag="v2" 8 | containerName="magic8ball" 9 | image="$acrName.azurecr.io/$imageName:$tag" 10 | imagePullPolicy="IfNotPresent" # Always, Never, IfNotPresent 11 | managedIdentityName="OpenAiManagedIdentity" 12 | federatedIdentityName="Magic8BallFederatedIdentity" 13 | 14 | # Azure Subscription and Tenant 15 | subscriptionId=$(az account show --query id --output tsv) 16 | subscriptionName=$(az account show --query name --output tsv) 17 | tenantId=$(az account show --query tenantId --output tsv) 18 | 19 | # Parameters 20 | title="Magic 8 Ball" 21 | label="Pose your question and cross your fingers!" 22 | temperature="0.9" 23 | imageWidth="80" 24 | 25 | # OpenAI 26 | openAiName="GreyOpenAi " 27 | openAiResourceGroupName="GreyRG" 28 | openAiType="azure_ad" 29 | openAiBase="https://greyopenai.openai.azure.com/" 30 | openAiModel="gpt-35-turbo" 31 | openAiDeployment="gpt-35-turbo" 32 | 33 | # Nginx Ingress Controller 34 | nginxNamespace="ingress-basic" 35 | nginxRepoName="ingress-nginx" 36 | nginxRepoUrl="https://kubernetes.github.io/ingress-nginx" 37 | nginxChartName="ingress-nginx" 38 | nginxReleaseName="nginx-ingress" 39 | nginxReplicaCount=3 40 | 41 | # Certificate Manager 42 | cmNamespace="cert-manager" 43 | cmRepoName="jetstack" 44 | cmRepoUrl="https://charts.jetstack.io" 45 | cmChartName="cert-manager" 46 | cmReleaseName="cert-manager" 47 | 48 | # Cluster Issuer 49 | email="paolos@microsoft.com" 50 | clusterIssuerName="letsencrypt-nginx" 51 | clusterIssuerTemplate="cluster-issuer.yml" 52 | 53 | # AKS Cluster 54 | aksClusterName="GreyAks" 55 | aksResourceGroupName="GreyRG" 56 | 57 | # Sample Application 58 | namespace="magic8ball" 59 | serviceAccountName="magic8ball-sa" 60 | deploymentTemplate="deployment.yml" 61 | serviceTemplate="service.yml" 62 | configMapTemplate="configMap.yml" 63 | secretTemplate="secret.yml" 64 | 65 | # Ingress and DNS 66 | ingressTemplate="ingress.yml" 67 | ingressName="magic8ball-ingress" 68 | dnsZoneName="babosbird.com" 69 | dnsZoneResourceGroupName="DnsResourceGroup" 70 | subdomain="magic8ball" 71 | host="$subdomain.$dnsZoneName" -------------------------------------------------------------------------------- /bicep/containerRegistry.bicep: -------------------------------------------------------------------------------- 1 | // Parameters 2 | @description('Name of your Azure Container Registry') 3 | @minLength(5) 4 | @maxLength(50) 5 | param name string = 'acr${uniqueString(resourceGroup().id)}' 6 | 7 | @description('Enable admin user that have push / pull permission to the registry.') 8 | param adminUserEnabled bool = false 9 | 10 | @description('Tier of your Azure Container Registry.') 11 | @allowed([ 12 | 'Basic' 13 | 'Standard' 14 | 'Premium' 15 | ]) 16 | param sku string = 'Premium' 17 | 18 | @description('Specifies the resource id of the Log Analytics workspace.') 19 | param workspaceId string 20 | 21 | @description('Specifies the workspace data retention in days.') 22 | param retentionInDays int = 60 23 | 24 | @description('Specifies the location.') 25 | param location string = resourceGroup().location 26 | 27 | @description('Specifies the resource tags.') 28 | param tags object 29 | 30 | // Variables 31 | var diagnosticSettingsName = 'diagnosticSettings' 32 | var logCategories = [ 33 | 'ContainerRegistryRepositoryEvents' 34 | 'ContainerRegistryLoginEvents' 35 | ] 36 | var metricCategories = [ 37 | 'AllMetrics' 38 | ] 39 | var logs = [for category in logCategories: { 40 | category: category 41 | enabled: true 42 | retentionPolicy: { 43 | enabled: true 44 | days: retentionInDays 45 | } 46 | }] 47 | var metrics = [for category in metricCategories: { 48 | category: category 49 | enabled: true 50 | retentionPolicy: { 51 | enabled: true 52 | days: retentionInDays 53 | } 54 | }] 55 | 56 | // Resources 57 | resource containerRegistry 'Microsoft.ContainerRegistry/registries@2021-12-01-preview' = { 58 | name: name 59 | location: location 60 | tags: tags 61 | sku: { 62 | name: sku 63 | } 64 | properties: { 65 | adminUserEnabled: adminUserEnabled 66 | } 67 | } 68 | 69 | resource diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { 70 | name: diagnosticSettingsName 71 | scope: containerRegistry 72 | properties: { 73 | workspaceId: workspaceId 74 | logs: logs 75 | metrics: metrics 76 | } 77 | } 78 | 79 | // Outputs 80 | output id string = containerRegistry.id 81 | output name string = containerRegistry.name 82 | -------------------------------------------------------------------------------- /scripts/requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.8.4 2 | aiosignal==1.3.1 3 | altair==4.2.2 4 | anyio==3.6.2 5 | async-timeout==4.0.2 6 | attrs==23.1.0 7 | autopep8==1.6.0 8 | azure-core==1.26.4 9 | azure-identity==1.13.0 10 | backoff==2.2.1 11 | blinker==1.6.2 12 | cachetools==5.3.0 13 | certifi==2021.10.8 14 | cffi==1.15.1 15 | charset-normalizer==2.0.7 16 | chromadb==0.3.22 17 | click==8.0.3 18 | clickhouse-connect==0.5.24 19 | cmake==3.26.3 20 | cryptography==40.0.2 21 | dataclasses-json==0.5.7 22 | debugpy==1.6.7 23 | decorator==5.1.1 24 | duckdb==0.7.1 25 | entrypoints==0.4 26 | et-xmlfile==1.1.0 27 | fastapi==0.95.1 28 | filelock==3.12.0 29 | Flask==2.0.2 30 | frozenlist==1.3.3 31 | fsspec==2023.5.0 32 | gitdb==4.0.10 33 | GitPython==3.1.31 34 | greenlet==2.0.2 35 | h11==0.14.0 36 | hnswlib==0.7.0 37 | httptools==0.5.0 38 | huggingface-hub==0.14.1 39 | idna==3.3 40 | importlib-metadata==6.6.0 41 | itsdangerous==2.0.1 42 | jc==1.23.1 43 | Jinja2==3.0.2 44 | joblib==1.2.0 45 | jsonschema==4.17.3 46 | langchain==0.0.169 47 | lit==16.0.3 48 | llama-index==0.6.8 49 | lz4==4.3.2 50 | markdown-it-py==2.2.0 51 | MarkupSafe==2.0.1 52 | marshmallow==3.19.0 53 | marshmallow-enum==1.5.1 54 | mdurl==0.1.2 55 | monotonic==1.6 56 | mpmath==1.3.0 57 | msal==1.22.0 58 | msal-extensions==1.0.0 59 | multidict==6.0.4 60 | mypy-extensions==1.0.0 61 | networkx==3.1 62 | nltk==3.8.1 63 | numexpr==2.8.4 64 | numpy==1.24.3 65 | nvidia-cublas-cu11==11.10.3.66 66 | nvidia-cuda-cupti-cu11==11.7.101 67 | nvidia-cuda-nvrtc-cu11==11.7.99 68 | nvidia-cuda-runtime-cu11==11.7.99 69 | nvidia-cudnn-cu11==8.5.0.96 70 | nvidia-cufft-cu11==10.9.0.58 71 | nvidia-curand-cu11==10.2.10.91 72 | nvidia-cusolver-cu11==11.4.0.1 73 | nvidia-cusparse-cu11==11.7.4.91 74 | nvidia-nccl-cu11==2.14.3 75 | nvidia-nvtx-cu11==11.7.91 76 | openai==0.27.7 77 | openapi-schema-pydantic==1.2.4 78 | openpyxl==3.0.9 79 | packaging==23.1 80 | pandas==2.0.1 81 | pandas-stubs==1.2.0.35 82 | Pillow==9.5.0 83 | pipdeptree==2.7.1 84 | portalocker==2.7.0 85 | posthog==3.0.1 86 | protobuf==3.20.3 87 | pyarrow==12.0.0 88 | pycodestyle==2.8.0 89 | pycparser==2.21 90 | pydantic==1.10.7 91 | pydeck==0.8.1b0 92 | Pygments==2.15.1 93 | PyJWT==2.7.0 94 | Pympler==1.0.1 95 | PyPDF2==3.0.1 96 | pyrsistent==0.19.3 97 | python-dateutil==2.8.2 98 | python-dotenv==0.19.2 99 | pytz==2021.3 100 | PyYAML==6.0 101 | regex==2023.5.5 102 | requests==2.29.0 103 | rich==13.3.5 104 | ruamel.yaml==0.17.21 105 | ruamel.yaml.clib==0.2.7 106 | scikit-learn==1.2.2 107 | scipy==1.10.1 108 | sentence-transformers==2.2.2 109 | sentencepiece==0.1.99 110 | six==1.16.0 111 | smmap==5.0.0 112 | sniffio==1.3.0 113 | SQLAlchemy==2.0.13 114 | starlette==0.26.1 115 | streamlit==1.22.0 116 | streamlit-chat==0.0.2.2 117 | sympy==1.12 118 | tenacity==8.2.2 119 | threadpoolctl==3.1.0 120 | tiktoken==0.4.0 121 | tokenizers==0.13.3 122 | toml==0.10.2 123 | toolz==0.12.0 124 | torch==2.0.1 125 | torchvision==0.15.2 126 | tornado==6.3.2 127 | tqdm==4.62.3 128 | transformers==4.29.1 129 | triton==2.0.0 130 | typing-inspect==0.8.0 131 | typing_extensions==4.5.0 132 | tzdata==2023.3 133 | tzlocal==5.0.1 134 | urllib3==1.26.7 135 | uvicorn==0.22.0 136 | uvloop==0.17.0 137 | validators==0.20.0 138 | watchdog==3.0.0 139 | watchfiles==0.19.0 140 | websockets==11.0.3 141 | Werkzeug==2.0.2 142 | xmltodict==0.13.0 143 | yarl==1.9.2 144 | zipp==3.15.0 145 | zstandard==0.21.0 146 | -------------------------------------------------------------------------------- /bicep/openAi.bicep: -------------------------------------------------------------------------------- 1 | // Parameters 2 | @description('Specifies the name of the Azure OpenAI resource.') 3 | param name string = 'aks-${uniqueString(resourceGroup().id)}' 4 | 5 | @description('Specifies the resource model definition representing SKU.') 6 | param sku object = { 7 | name: 'S0' 8 | } 9 | 10 | @description('Specifies the identity of the OpenAI resource.') 11 | param identity object = { 12 | type: 'SystemAssigned' 13 | } 14 | 15 | @description('Specifies the location.') 16 | param location string = resourceGroup().location 17 | 18 | @description('Specifies the resource tags.') 19 | param tags object 20 | 21 | @description('Specifies an optional subdomain name used for token-based authentication.') 22 | param customSubDomainName string = '' 23 | 24 | @description('Specifies whether or not public endpoint access is allowed for this account..') 25 | @allowed([ 26 | 'Enabled' 27 | 'Disabled' 28 | ]) 29 | param publicNetworkAccess string = 'Enabled' 30 | 31 | @description('Specifies the OpenAI deployments to create.') 32 | param deployments array = [ 33 | { 34 | name: 'text-embedding-ada-002' 35 | version: '2' 36 | raiPolicyName: '' 37 | capacity: 1 38 | scaleType: 'Standard' 39 | } 40 | { 41 | name: 'gpt-35-turbo' 42 | version: '0301' 43 | raiPolicyName: '' 44 | capacity: 1 45 | scaleType: 'Standard' 46 | } 47 | { 48 | name: 'text-davinci-003' 49 | version: '1' 50 | raiPolicyName: '' 51 | capacity: 1 52 | scaleType: 'Standard' 53 | } 54 | ] 55 | 56 | @description('Specifies the workspace id of the Log Analytics used to monitor the Application Gateway.') 57 | param workspaceId string 58 | 59 | // Variables 60 | var diagnosticSettingsName = 'diagnosticSettings' 61 | var openAiLogCategories = [ 62 | 'Audit' 63 | 'RequestResponse' 64 | 'Trace' 65 | ] 66 | var openAiMetricCategories = [ 67 | 'AllMetrics' 68 | ] 69 | var openAiLogs = [for category in openAiLogCategories: { 70 | category: category 71 | enabled: true 72 | }] 73 | var openAiMetrics = [for category in openAiMetricCategories: { 74 | category: category 75 | enabled: true 76 | }] 77 | 78 | // Resources 79 | resource openAi 'Microsoft.CognitiveServices/accounts@2022-12-01' = { 80 | name: name 81 | location: location 82 | sku: sku 83 | kind: 'OpenAI' 84 | identity: identity 85 | tags: tags 86 | properties: { 87 | customSubDomainName: customSubDomainName 88 | publicNetworkAccess: publicNetworkAccess 89 | } 90 | } 91 | 92 | resource model 'Microsoft.CognitiveServices/accounts/deployments@2022-12-01' = [for deployment in deployments: { 93 | name: deployment.name 94 | parent: openAi 95 | properties: { 96 | model: { 97 | format: 'OpenAI' 98 | name: deployment.name 99 | version: deployment.version 100 | } 101 | raiPolicyName: deployment.raiPolicyName 102 | scaleSettings: { 103 | capacity: deployment.capacity 104 | scaleType: deployment.scaleType 105 | } 106 | } 107 | }] 108 | 109 | resource openAiDiagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { 110 | name: diagnosticSettingsName 111 | scope: openAi 112 | properties: { 113 | workspaceId: workspaceId 114 | logs: openAiLogs 115 | metrics: openAiMetrics 116 | } 117 | } 118 | 119 | // Outputs 120 | output id string = openAi.id 121 | output name string = openAi.name 122 | -------------------------------------------------------------------------------- /scripts/11-configure-dns.sh: -------------------------------------------------------------------------------- 1 | # Variables 2 | source ./00-variables.sh 3 | 4 | # Retrieve the public IP address from the ingress 5 | echo "Retrieving the external IP address from the [$ingressName] ingress..." 6 | publicIpAddress=$(kubectl get ingress $ingressName -n $namespace -o jsonpath='{.status.loadBalancer.ingress[0].ip}') 7 | 8 | if [ -n $publicIpAddress ]; then 9 | echo "[$publicIpAddress] external IP address of the application gateway ingress controller successfully retrieved from the [$ingressName] ingress" 10 | else 11 | echo "Failed to retrieve the external IP address of the application gateway ingress controller from the [$ingressName] ingress" 12 | exit 13 | fi 14 | 15 | # Check if an A record for todolist subdomain exists in the DNS Zone 16 | echo "Retrieving the A record for the [$subdomain] subdomain from the [$dnsZoneName] DNS zone..." 17 | ipv4Address=$(az network dns record-set a list \ 18 | --zone-name $dnsZoneName \ 19 | --resource-group $dnsZoneResourceGroupName \ 20 | --query "[?name=='$subdomain'].arecords[].ipv4Address" \ 21 | --output tsv) 22 | 23 | if [[ -n $ipv4Address ]]; then 24 | echo "An A record already exists in [$dnsZoneName] DNS zone for the [$subdomain] subdomain with [$ipv4Address] IP address" 25 | 26 | if [[ $ipv4Address == $publicIpAddress ]]; then 27 | echo "The [$ipv4Address] ip address of the existing A record is equal to the ip address of the [$ingressName] ingress" 28 | echo "No additional step is required" 29 | exit 30 | else 31 | echo "The [$ipv4Address] ip address of the existing A record is different than the ip address of the [$ingressName] ingress" 32 | fi 33 | 34 | # Retrieving name of the record set relative to the zone 35 | echo "Retrieving the name of the record set relative to the [$dnsZoneName] zone..." 36 | 37 | recordSetName=$(az network dns record-set a list \ 38 | --zone-name $dnsZoneName \ 39 | --resource-group $dnsZoneResourceGroupName \ 40 | --query "[?name=='$subdomain'].name" \ 41 | --output name 2>/dev/null) 42 | 43 | if [[ -n $recordSetName ]]; then 44 | "[$recordSetName] record set name successfully retrieved" 45 | else 46 | "Failed to retrieve the name of the record set relative to the [$dnsZoneName] zone" 47 | exit 48 | fi 49 | 50 | # Remove the a record 51 | echo "Removing the A record from the record set relative to the [$dnsZoneName] zone..." 52 | 53 | az network dns record-set a remove-record \ 54 | --ipv4-address $ipv4Address \ 55 | --record-set-name $recordSetName \ 56 | --zone-name $dnsZoneName \ 57 | --resource-group $dnsZoneResourceGroupName 58 | 59 | if [[ $? == 0 ]]; then 60 | echo "[$ipv4Address] ip address successfully removed from the [$recordSetName] record set" 61 | else 62 | echo "Failed to remove the [$ipv4Address] ip address from the [$recordSetName] record set" 63 | exit 64 | fi 65 | fi 66 | 67 | # Create the a record 68 | echo "Creating an A record in [$dnsZoneName] DNS zone for the [$subdomain] subdomain with [$publicIpAddress] IP address..." 69 | az network dns record-set a add-record \ 70 | --zone-name $dnsZoneName \ 71 | --resource-group $dnsZoneResourceGroupName \ 72 | --record-set-name $subdomain \ 73 | --ipv4-address $publicIpAddress 1>/dev/null 74 | 75 | if [[ $? == 0 ]]; then 76 | echo "A record for the [$subdomain] subdomain with [$publicIpAddress] IP address successfully created in [$dnsZoneName] DNS zone" 77 | else 78 | echo "Failed to create an A record for the $subdomain subdomain with [$publicIpAddress] IP address in [$dnsZoneName] DNS zone" 79 | fi 80 | -------------------------------------------------------------------------------- /bicep/storageAccount.bicep: -------------------------------------------------------------------------------- 1 | // Parameters 2 | @description('Specifies the globally unique name for the storage account used to store the boot diagnostics logs of the virtual machine.') 3 | param name string = 'boot${uniqueString(resourceGroup().id)}' 4 | 5 | @description('Specifies whether to create containers.') 6 | param createContainers bool = true 7 | 8 | @description('Specifies an array of containers to create.') 9 | param containerNames array 10 | 11 | @description('Specifies the name of a Key Vault where to store secrets.') 12 | param keyVaultName string 13 | 14 | @description('Specifies the resource id of the Log Analytics workspace.') 15 | param workspaceId string 16 | 17 | @description('Specifies the workspace data retention in days.') 18 | param retentionInDays int = 60 19 | 20 | @description('Specifies the location.') 21 | param location string = resourceGroup().location 22 | 23 | @description('Specifies the resource tags.') 24 | param tags object 25 | 26 | // Variables 27 | var diagnosticSettingsName = 'diagnosticSettings' 28 | var logCategories = [ 29 | 'StorageRead' 30 | 'StorageWrite' 31 | 'StorageDelete' 32 | ] 33 | var metricCategories = [ 34 | 'Transaction' 35 | ] 36 | var logs = [for category in logCategories: { 37 | category: category 38 | enabled: true 39 | retentionPolicy: { 40 | enabled: true 41 | days: retentionInDays 42 | } 43 | }] 44 | var metrics = [for category in metricCategories: { 45 | category: category 46 | enabled: true 47 | retentionPolicy: { 48 | enabled: true 49 | days: retentionInDays 50 | } 51 | }] 52 | 53 | // Resources 54 | resource storageAccount 'Microsoft.Storage/storageAccounts@2021-09-01' = { 55 | name: name 56 | location: location 57 | tags: tags 58 | sku: { 59 | name: 'Standard_LRS' 60 | } 61 | kind: 'StorageV2' 62 | 63 | // Containers live inside of a blob service 64 | resource blobService 'blobServices' = { 65 | name: 'default' 66 | 67 | // Creating containers with provided names if contition is true 68 | resource containers 'containers' = [for containerName in containerNames: if(createContainers) { 69 | name: containerName 70 | properties: { 71 | publicAccess: 'None' 72 | } 73 | }] 74 | } 75 | } 76 | 77 | resource blobServiceDiagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { 78 | name: diagnosticSettingsName 79 | scope: storageAccount::blobService 80 | properties: { 81 | workspaceId: workspaceId 82 | logs: logs 83 | metrics: metrics 84 | } 85 | } 86 | 87 | resource keyVault 'Microsoft.KeyVault/vaults@2021-10-01' existing = { 88 | name: keyVaultName 89 | } 90 | 91 | resource storageAccountNameSecret 'Microsoft.KeyVault/vaults/secrets@2021-11-01-preview' = { 92 | parent: keyVault 93 | name: 'DataProtection--BlobStorage--AccountName' 94 | properties: { 95 | value: storageAccount.name 96 | } 97 | } 98 | 99 | resource storageAccountConnectionStringSecret 'Microsoft.KeyVault/vaults/secrets@2021-11-01-preview' = { 100 | parent: keyVault 101 | name: 'DataProtection--BlobStorage--ConnectionString' 102 | properties: { 103 | value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};AccountKey=${storageAccount.listKeys().keys[0].value}' 104 | } 105 | } 106 | 107 | resource storageAccountUseAzureCredentialSecret 'Microsoft.KeyVault/vaults/secrets@2021-11-01-preview' = { 108 | parent: keyVault 109 | name: 'DataProtection--BlobStorage--UseAzureCredential' 110 | properties: { 111 | value: 'true' 112 | } 113 | } 114 | 115 | // Outputs 116 | output id string = storageAccount.id 117 | output name string = storageAccount.name 118 | -------------------------------------------------------------------------------- /bicep/keyVault.bicep: -------------------------------------------------------------------------------- 1 | // Parameters 2 | @description('Specifies the name of the Key Vault resource.') 3 | param name string 4 | 5 | @description('Specifies the sku name of the Key Vault resource.') 6 | @allowed([ 7 | 'premium' 8 | 'standard' 9 | ]) 10 | param skuName string = 'standard' 11 | 12 | @description('Specifies the Azure Active Directory tenant ID that should be used for authenticating requests to the key vault.') 13 | param tenantId string = subscription().tenantId 14 | 15 | @description('The default action of allow or deny when no other rules match. Allowed values: Allow or Deny') 16 | @allowed([ 17 | 'Allow' 18 | 'Deny' 19 | ]) 20 | param networkAclsDefaultAction string = 'Allow' 21 | 22 | @description('Specifies whether the Azure Key Vault resource is enabled for deployments.') 23 | param enabledForDeployment bool = true 24 | 25 | @description('Specifies whether the Azure Key Vault resource is enabled for disk encryption.') 26 | param enabledForDiskEncryption bool = true 27 | 28 | @description('Specifies whether the Azure Key Vault resource is enabled for template deployment.') 29 | param enabledForTemplateDeployment bool = true 30 | 31 | @description('Specifies whether the soft deelete is enabled for this Azure Key Vault resource.') 32 | param enableSoftDelete bool = true 33 | 34 | @description('Specifies the object ID ofthe service principals to configure in Key Vault access policies.') 35 | param objectIds array = [] 36 | 37 | @description('Specifies the resource id of the Log Analytics workspace.') 38 | param workspaceId string 39 | 40 | @description('Specifies the workspace data retention in days.') 41 | param retentionInDays int = 60 42 | 43 | @description('Specifies the location.') 44 | param location string = resourceGroup().location 45 | 46 | @description('Specifies the resource tags.') 47 | param tags object 48 | 49 | // Variables 50 | var diagnosticSettingsName = 'diagnosticSettings' 51 | var logCategories = [ 52 | 'AuditEvent' 53 | 'AzurePolicyEvaluationDetails' 54 | ] 55 | var metricCategories = [ 56 | 'AllMetrics' 57 | ] 58 | var logs = [for category in logCategories: { 59 | category: category 60 | enabled: true 61 | retentionPolicy: { 62 | enabled: true 63 | days: retentionInDays 64 | } 65 | }] 66 | var metrics = [for category in metricCategories: { 67 | category: category 68 | enabled: true 69 | retentionPolicy: { 70 | enabled: true 71 | days: retentionInDays 72 | } 73 | }] 74 | 75 | // Resources 76 | resource keyVault 'Microsoft.KeyVault/vaults@2021-10-01' = { 77 | name: name 78 | location: location 79 | tags: tags 80 | properties: { 81 | accessPolicies: [for objectId in objectIds: { 82 | tenantId: subscription().tenantId 83 | objectId: objectId 84 | permissions: { 85 | keys: [ 86 | 'get' 87 | 'list' 88 | ] 89 | secrets: [ 90 | 'get' 91 | 'list' 92 | ] 93 | certificates: [ 94 | 'get' 95 | 'list' 96 | ] 97 | } 98 | }] 99 | sku: { 100 | family: 'A' 101 | name: skuName 102 | } 103 | tenantId: tenantId 104 | networkAcls: { 105 | bypass: 'AzureServices' 106 | defaultAction: networkAclsDefaultAction 107 | } 108 | enabledForDeployment: enabledForDeployment 109 | enabledForDiskEncryption: enabledForDiskEncryption 110 | enabledForTemplateDeployment: enabledForTemplateDeployment 111 | enableSoftDelete: enableSoftDelete 112 | } 113 | } 114 | 115 | resource diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { 116 | name: diagnosticSettingsName 117 | scope: keyVault 118 | properties: { 119 | workspaceId: workspaceId 120 | logs: logs 121 | metrics: metrics 122 | } 123 | } 124 | 125 | // Outputs 126 | output id string = keyVault.id 127 | output name string = keyVault.name 128 | -------------------------------------------------------------------------------- /scripts/deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: magic8ball 5 | labels: 6 | app: magic8ball 7 | spec: 8 | replicas: 3 9 | selector: 10 | matchLabels: 11 | app: magic8ball 12 | azure.workload.identity/use: "true" 13 | strategy: 14 | rollingUpdate: 15 | maxSurge: 1 16 | maxUnavailable: 1 17 | minReadySeconds: 5 18 | template: 19 | metadata: 20 | labels: 21 | app: magic8ball 22 | azure.workload.identity/use: "true" 23 | prometheus.io/scrape: "true" 24 | spec: 25 | serviceAccountName: magic8ball-sa 26 | topologySpreadConstraints: 27 | - maxSkew: 1 28 | topologyKey: topology.kubernetes.io/zone 29 | whenUnsatisfiable: DoNotSchedule 30 | labelSelector: 31 | matchLabels: 32 | app: magic8ball 33 | - maxSkew: 1 34 | topologyKey: kubernetes.io/hostname 35 | whenUnsatisfiable: DoNotSchedule 36 | labelSelector: 37 | matchLabels: 38 | app: magic8ball 39 | nodeSelector: 40 | "kubernetes.io/os": linux 41 | containers: 42 | - name: magic8ball 43 | image: paolosalvatori.azurecr.io/magic8ball:v1 44 | imagePullPolicy: Always 45 | resources: 46 | requests: 47 | memory: "128Mi" 48 | cpu: "250m" 49 | limits: 50 | memory: "256Mi" 51 | cpu: "500m" 52 | ports: 53 | - containerPort: 8501 54 | livenessProbe: 55 | httpGet: 56 | path: / 57 | port: 8501 58 | failureThreshold: 1 59 | initialDelaySeconds: 60 60 | periodSeconds: 30 61 | timeoutSeconds: 5 62 | readinessProbe: 63 | httpGet: 64 | path: / 65 | port: 8501 66 | failureThreshold: 1 67 | initialDelaySeconds: 60 68 | periodSeconds: 30 69 | timeoutSeconds: 5 70 | startupProbe: 71 | httpGet: 72 | path: / 73 | port: 8501 74 | failureThreshold: 1 75 | initialDelaySeconds: 60 76 | periodSeconds: 30 77 | timeoutSeconds: 5 78 | env: 79 | - name: TITLE 80 | valueFrom: 81 | configMapKeyRef: 82 | name: magic8ball-configmap 83 | key: TITLE 84 | - name: IMAGE_WIDTH 85 | valueFrom: 86 | configMapKeyRef: 87 | name: magic8ball-configmap 88 | key: IMAGE_WIDTH 89 | - name: LABEL 90 | valueFrom: 91 | configMapKeyRef: 92 | name: magic8ball-configmap 93 | key: LABEL 94 | - name: TEMPERATURE 95 | valueFrom: 96 | configMapKeyRef: 97 | name: magic8ball-configmap 98 | key: TEMPERATURE 99 | - name: AZURE_OPENAI_TYPE 100 | valueFrom: 101 | configMapKeyRef: 102 | name: magic8ball-configmap 103 | key: AZURE_OPENAI_TYPE 104 | - name: AZURE_OPENAI_BASE 105 | valueFrom: 106 | configMapKeyRef: 107 | name: magic8ball-configmap 108 | key: AZURE_OPENAI_BASE 109 | - name: AZURE_OPENAI_KEY 110 | valueFrom: 111 | configMapKeyRef: 112 | name: magic8ball-configmap 113 | key: AZURE_OPENAI_KEY 114 | - name: AZURE_OPENAI_MODEL 115 | valueFrom: 116 | configMapKeyRef: 117 | name: magic8ball-configmap 118 | key: AZURE_OPENAI_MODEL 119 | - name: AZURE_OPENAI_DEPLOYMENT 120 | valueFrom: 121 | configMapKeyRef: 122 | name: magic8ball-configmap 123 | key: AZURE_OPENAI_DEPLOYMENT -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to [project-title] 2 | 3 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 4 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 5 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 6 | 7 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 8 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 9 | provided by the bot. You will only need to do this once across all repos using our CLA. 10 | 11 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 12 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 13 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 14 | 15 | - [Code of Conduct](#coc) 16 | - [Issues and Bugs](#issue) 17 | - [Feature Requests](#feature) 18 | - [Submission Guidelines](#submit) 19 | 20 | ## Code of Conduct 21 | Help us keep this project open and inclusive. Please read and follow our [Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 22 | 23 | ## Found an Issue? 24 | If you find a bug in the source code or a mistake in the documentation, you can help us by 25 | [submitting an issue](#submit-issue) to the GitHub Repository. Even better, you can 26 | [submit a Pull Request](#submit-pr) with a fix. 27 | 28 | ## Want a Feature? 29 | You can *request* a new feature by [submitting an issue](#submit-issue) to the GitHub 30 | Repository. If you would like to *implement* a new feature, please submit an issue with 31 | a proposal for your work first, to be sure that we can use it. 32 | 33 | * **Small Features** can be crafted and directly [submitted as a Pull Request](#submit-pr). 34 | 35 | ## Submission Guidelines 36 | 37 | ### Submitting an Issue 38 | Before you submit an issue, search the archive, maybe your question was already answered. 39 | 40 | If your issue appears to be a bug, and hasn't been reported, open a new issue. 41 | Help us to maximize the effort we can spend fixing issues and adding new 42 | features, by not reporting duplicate issues. Providing the following information will increase the 43 | chances of your issue being dealt with quickly: 44 | 45 | * **Overview of the Issue** - if an error is being thrown a non-minified stack trace helps 46 | * **Version** - what version is affected (e.g. 0.1.2) 47 | * **Motivation for or Use Case** - explain what are you trying to do and why the current behavior is a bug for you 48 | * **Browsers and Operating System** - is this a problem with all browsers? 49 | * **Reproduce the Error** - provide a live example or a unambiguous set of steps 50 | * **Related Issues** - has a similar issue been reported before? 51 | * **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be 52 | causing the problem (line of code or commit) 53 | 54 | You can file new issues by providing the above information at the corresponding repository's issues link: https://github.com/[organization-name]/[repository-name]/issues/new]. 55 | 56 | ### Submitting a Pull Request (PR) 57 | Before you submit your Pull Request (PR) consider the following guidelines: 58 | 59 | * Search the repository (https://github.com/[organization-name]/[repository-name]/pulls) for an open or closed PR 60 | that relates to your submission. You don't want to duplicate effort. 61 | 62 | * Make your changes in a new git fork: 63 | 64 | * Commit your changes using a descriptive commit message 65 | * Push your fork to GitHub: 66 | * In GitHub, create a pull request 67 | * If we suggest changes then: 68 | * Make the required updates. 69 | * Rebase your fork and force push to your GitHub repository (this will update your Pull Request): 70 | 71 | ```shell 72 | git rebase master -i 73 | git push -f 74 | ``` 75 | 76 | That's it! Thank you for your contribution! 77 | -------------------------------------------------------------------------------- /bicep/aksManagedIdentity.bicep: -------------------------------------------------------------------------------- 1 | // Parameters 2 | @description('Specifies the name of the user-defined managed identity.') 3 | param managedIdentityName string 4 | 5 | @description('Specifies the name of the existing virtual network.') 6 | param virtualNetworkName string 7 | 8 | @description('Specifies the name of the subnet hosting the worker nodes of the default system agent pool of the AKS cluster.') 9 | param systemAgentPoolSubnetName string = 'SystemSubnet' 10 | 11 | @description('Specifies the name of the subnet hosting the worker nodes of the user agent pool of the AKS cluster.') 12 | param userAgentPoolSubnetName string = 'UserSubnet' 13 | 14 | @description('Specifies the name of the subnet hosting the pods running in the AKS cluster.') 15 | param podSubnetName string = 'PodSubnet' 16 | 17 | @description('Specifies the name of the subnet delegated to the API server when configuring the AKS cluster to use API server VNET integration.') 18 | param apiServerSubnetName string = 'ApiServerSubnet' 19 | 20 | @description('Specifies the location.') 21 | param location string = resourceGroup().location 22 | 23 | @description('Specifies the resource tags.') 24 | param tags object 25 | 26 | // Variables 27 | var networkContributorRoleDefinitionId = resourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7') 28 | 29 | // Resources 30 | resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { 31 | name: managedIdentityName 32 | location: location 33 | tags: tags 34 | } 35 | 36 | resource virtualNetwork 'Microsoft.Network/virtualNetworks@2021-08-01' existing = { 37 | name: virtualNetworkName 38 | } 39 | 40 | resource systemAgentPoolSubnet 'Microsoft.Network/virtualNetworks/subnets@2021-08-01' existing = { 41 | parent: virtualNetwork 42 | name: systemAgentPoolSubnetName 43 | } 44 | 45 | resource userAgentPoolSubnet 'Microsoft.Network/virtualNetworks/subnets@2021-08-01' existing = { 46 | parent: virtualNetwork 47 | name: userAgentPoolSubnetName 48 | } 49 | 50 | resource podSubnet 'Microsoft.Network/virtualNetworks/subnets@2021-08-01' existing = { 51 | parent: virtualNetwork 52 | name: podSubnetName 53 | } 54 | 55 | resource apiServerSubnet 'Microsoft.Network/virtualNetworks/subnets@2021-08-01' existing = { 56 | parent: virtualNetwork 57 | name: apiServerSubnetName 58 | } 59 | 60 | resource systemAgentPoolSubnetNetworkContributorRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 61 | name: guid(managedIdentity.id, systemAgentPoolSubnet.id, networkContributorRoleDefinitionId) 62 | scope: systemAgentPoolSubnet 63 | properties: { 64 | roleDefinitionId: networkContributorRoleDefinitionId 65 | principalId: managedIdentity.properties.principalId 66 | principalType: 'ServicePrincipal' 67 | } 68 | } 69 | 70 | resource userAgentPoolSubnetNetworkContributorRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 71 | name: guid(managedIdentity.id, userAgentPoolSubnet.id, networkContributorRoleDefinitionId) 72 | scope: userAgentPoolSubnet 73 | properties: { 74 | roleDefinitionId: networkContributorRoleDefinitionId 75 | principalId: managedIdentity.properties.principalId 76 | principalType: 'ServicePrincipal' 77 | } 78 | } 79 | 80 | resource podSubnetNetworkContributorRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 81 | name: guid(managedIdentity.id, podSubnet.id, networkContributorRoleDefinitionId) 82 | scope: podSubnet 83 | properties: { 84 | roleDefinitionId: networkContributorRoleDefinitionId 85 | principalId: managedIdentity.properties.principalId 86 | principalType: 'ServicePrincipal' 87 | } 88 | } 89 | 90 | resource apiServerSubnetNetworkContributorRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 91 | name: guid(managedIdentity.id, apiServerSubnet.id, networkContributorRoleDefinitionId) 92 | scope: apiServerSubnet 93 | properties: { 94 | roleDefinitionId: networkContributorRoleDefinitionId 95 | principalId: managedIdentity.properties.principalId 96 | principalType: 'ServicePrincipal' 97 | } 98 | } 99 | 100 | // Outputs 101 | output id string = managedIdentity.id 102 | output name string = managedIdentity.name 103 | -------------------------------------------------------------------------------- /scripts/08-create-service-account.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Variables for the user-assigned managed identity 4 | source ./00-variables.sh 5 | 6 | # Check if the namespace already exists 7 | result=$(kubectl get namespace -o 'jsonpath={.items[?(@.metadata.name=="'$namespace'")].metadata.name'}) 8 | 9 | if [[ -n $result ]]; then 10 | echo "[$namespace] namespace already exists" 11 | else 12 | # Create the namespace for your ingress resources 13 | echo "[$namespace] namespace does not exist" 14 | echo "Creating [$namespace] namespace..." 15 | kubectl create namespace $namespace 16 | fi 17 | 18 | # Check if the service account already exists 19 | result=$(kubectl get sa -n $namespace -o 'jsonpath={.items[?(@.metadata.name=="'$serviceAccountName'")].metadata.name'}) 20 | 21 | if [[ -n $result ]]; then 22 | echo "[$serviceAccountName] service account already exists" 23 | else 24 | # Retrieve the resource id of the user-assigned managed identity 25 | echo "Retrieving clientId for [$managedIdentityName] managed identity..." 26 | managedIdentityClientId=$(az identity show \ 27 | --name $managedIdentityName \ 28 | --resource-group $aksResourceGroupName \ 29 | --query clientId \ 30 | --output tsv) 31 | 32 | if [[ -n $managedIdentityClientId ]]; then 33 | echo "[$managedIdentityClientId] clientId for the [$managedIdentityName] managed identity successfully retrieved" 34 | else 35 | echo "Failed to retrieve clientId for the [$managedIdentityName] managed identity" 36 | exit 37 | fi 38 | 39 | # Create the service account 40 | echo "[$serviceAccountName] service account does not exist" 41 | echo "Creating [$serviceAccountName] service account..." 42 | cat </dev/null 68 | 69 | if [[ $? != 0 ]]; then 70 | echo "No [$federatedIdentityName] federated identity credential actually exists in the [$aksResourceGroupName] resource group" 71 | 72 | # Get the OIDC Issuer URL 73 | aksOidcIssuerUrl="$(az aks show \ 74 | --only-show-errors \ 75 | --name $aksClusterName \ 76 | --resource-group $aksResourceGroupName \ 77 | --query oidcIssuerProfile.issuerUrl \ 78 | --output tsv)" 79 | 80 | # Show OIDC Issuer URL 81 | if [[ -n $aksOidcIssuerUrl ]]; then 82 | echo "The OIDC Issuer URL of the $aksClusterName cluster is $aksOidcIssuerUrl" 83 | fi 84 | 85 | echo "Creating [$federatedIdentityName] federated identity credential in the [$aksResourceGroupName] resource group..." 86 | 87 | # Establish the federated identity credential between the managed identity, the service account issuer, and the subject. 88 | az identity federated-credential create \ 89 | --name $federatedIdentityName \ 90 | --identity-name $managedIdentityName \ 91 | --resource-group $aksResourceGroupName \ 92 | --issuer $aksOidcIssuerUrl \ 93 | --subject system:serviceaccount:$namespace:$serviceAccountName 94 | 95 | if [[ $? == 0 ]]; then 96 | echo "[$federatedIdentityName] federated identity credential successfully created in the [$aksResourceGroupName] resource group" 97 | else 98 | echo "Failed to create [$federatedIdentityName] federated identity credential in the [$aksResourceGroupName] resource group" 99 | exit 100 | fi 101 | else 102 | echo "[$federatedIdentityName] federated identity credential already exists in the [$aksResourceGroupName] resource group" 103 | fi -------------------------------------------------------------------------------- /scripts/07-create-workload-managed-identity.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Variables 4 | source ./00-variables.sh 5 | 6 | # Check if the user-assigned managed identity already exists 7 | echo "Checking if [$managedIdentityName] user-assigned managed identity actually exists in the [$aksResourceGroupName] resource group..." 8 | 9 | az identity show \ 10 | --name $managedIdentityName \ 11 | --resource-group $aksResourceGroupName &>/dev/null 12 | 13 | if [[ $? != 0 ]]; then 14 | echo "No [$managedIdentityName] user-assigned managed identity actually exists in the [$aksResourceGroupName] resource group" 15 | echo "Creating [$managedIdentityName] user-assigned managed identity in the [$aksResourceGroupName] resource group..." 16 | 17 | # Create the user-assigned managed identity 18 | az identity create \ 19 | --name $managedIdentityName \ 20 | --resource-group $aksResourceGroupName \ 21 | --location $location \ 22 | --subscription $subscriptionId 1>/dev/null 23 | 24 | if [[ $? == 0 ]]; then 25 | echo "[$managedIdentityName] user-assigned managed identity successfully created in the [$aksResourceGroupName] resource group" 26 | else 27 | echo "Failed to create [$managedIdentityName] user-assigned managed identity in the [$aksResourceGroupName] resource group" 28 | exit 29 | fi 30 | else 31 | echo "[$managedIdentityName] user-assigned managed identity already exists in the [$aksResourceGroupName] resource group" 32 | fi 33 | 34 | # Retrieve the clientId of the user-assigned managed identity 35 | echo "Retrieving clientId for [$managedIdentityName] managed identity..." 36 | clientId=$(az identity show \ 37 | --name $managedIdentityName \ 38 | --resource-group $aksResourceGroupName \ 39 | --query clientId \ 40 | --output tsv) 41 | 42 | if [[ -n $clientId ]]; then 43 | echo "[$clientId] clientId for the [$managedIdentityName] managed identity successfully retrieved" 44 | else 45 | echo "Failed to retrieve clientId for the [$managedIdentityName] managed identity" 46 | exit 47 | fi 48 | 49 | # Retrieve the principalId of the user-assigned managed identity 50 | echo "Retrieving principalId for [$managedIdentityName] managed identity..." 51 | principalId=$(az identity show \ 52 | --name $managedIdentityName \ 53 | --resource-group $aksResourceGroupName \ 54 | --query principalId \ 55 | --output tsv) 56 | 57 | if [[ -n $principalId ]]; then 58 | echo "[$principalId] principalId for the [$managedIdentityName] managed identity successfully retrieved" 59 | else 60 | echo "Failed to retrieve principalId for the [$managedIdentityName] managed identity" 61 | exit 62 | fi 63 | 64 | # Get the resource id of the Azure OpenAI resource 65 | openAiId=$(az cognitiveservices account show \ 66 | --name $openAiName \ 67 | --resource-group $openAiResourceGroupName \ 68 | --query id \ 69 | --output tsv) 70 | 71 | if [[ -n $openAiId ]]; then 72 | echo "Resource id for the [$openAiName] Azure OpenAI resource successfully retrieved" 73 | else 74 | echo "Failed to the resource id for the [$openAiName] Azure OpenAI resource" 75 | exit -1 76 | fi 77 | 78 | # Assign the Cognitive Services User role on the Azure OpenAI resource to the managed identity 79 | role="Cognitive Services User" 80 | echo "Checking if the [$managedIdentityName] managed identity has been assigned to [$role] role with [$openAiName] Azure OpenAI resource as a scope..." 81 | current=$(az role assignment list \ 82 | --assignee $principalId \ 83 | --scope $openAiId \ 84 | --query "[?roleDefinitionName=='$role'].roleDefinitionName" \ 85 | --output tsv 2>/dev/null) 86 | 87 | if [[ $current == $role ]]; then 88 | echo "[$managedIdentityName] managed identity is already assigned to the ["$current"] role with [$openAiName] Azure OpenAI resource as a scope" 89 | else 90 | echo "[$managedIdentityName] managed identity is not assigned to the [$role] role with [$openAiName] Azure OpenAI resource as a scope" 91 | echo "Assigning the [$role] role to the [$managedIdentityName] managed identity with [$openAiName] Azure OpenAI resource as a scope..." 92 | 93 | az role assignment create \ 94 | --assignee $principalId \ 95 | --role "$role" \ 96 | --scope $openAiId 1>/dev/null 97 | 98 | if [[ $? == 0 ]]; then 99 | echo "[$managedIdentityName] managed identity successfully assigned to the [$role] role with [$openAiName] Azure OpenAI resource as a scope" 100 | else 101 | echo "Failed to assign the [$managedIdentityName] managed identity to the [$role] role with [$openAiName] Azure OpenAI resource as a scope" 102 | exit 103 | fi 104 | fi -------------------------------------------------------------------------------- /scripts/Dockerfile: -------------------------------------------------------------------------------- 1 | # app/Dockerfile 2 | 3 | # # Stage 1 - Install build dependencies 4 | 5 | # A Dockerfile must start with a FROM instruction which sets the base image for the container. 6 | # The Python images come in many flavors, each designed for a specific use case. 7 | # The python:3.11-slim image is a good base image for most applications. 8 | # It is a minimal image built on top of Debian Linux and includes only the necessary packages to run Python. 9 | # The slim image is a good choice because it is small and contains only the packages needed to run Python. 10 | # For more information, see: 11 | # * https://hub.docker.com/_/python 12 | # * https://docs.streamlit.io/knowledge-base/tutorials/deploy/docker 13 | FROM python:3.11-slim AS builder 14 | 15 | # The WORKDIR instruction sets the working directory for any RUN, CMD, ENTRYPOINT, COPY and ADD instructions that follow it in the Dockerfile. 16 | # If the WORKDIR doesn’t exist, it will be created even if it’s not used in any subsequent Dockerfile instruction. 17 | # For more information, see: https://docs.docker.com/engine/reference/builder/#workdir 18 | WORKDIR /app 19 | 20 | # Set environment variables. 21 | # The ENV instruction sets the environment variable to the value . 22 | # This value will be in the environment of all “descendant” Dockerfile commands and can be replaced inline in many as well. 23 | # For more information, see: https://docs.docker.com/engine/reference/builder/#env 24 | ENV PYTHONDONTWRITEBYTECODE 1 25 | ENV PYTHONUNBUFFERED 1 26 | 27 | # Install git so that we can clone the app code from a remote repo using the RUN instruction. 28 | # The RUN comand has 2 forms: 29 | # * RUN (shell form, the command is run in a shell, which by default is /bin/sh -c on Linux or cmd /S /C on Windows) 30 | # * RUN ["executable", "param1", "param2"] (exec form) 31 | # The RUN instruction will execute any commands in a new layer on top of the current image and commit the results. 32 | # The resulting committed image will be used for the next step in the Dockerfile. 33 | # For more information, see: https://docs.docker.com/engine/reference/builder/#run 34 | RUN apt-get update && apt-get install -y \ 35 | build-essential \ 36 | curl \ 37 | software-properties-common \ 38 | git \ 39 | && rm -rf /var/lib/apt/lists/* 40 | 41 | # Create a virtualenv to keep dependencies together 42 | RUN python -m venv /opt/venv 43 | ENV PATH="/opt/venv/bin:$PATH" 44 | 45 | # Clone the requirements.txt which contains dependencies to WORKDIR 46 | # COPY has two forms: 47 | # * COPY (this copies the files from the local machine to the container's own filesystem) 48 | # * COPY ["",... ""] (this form is required for paths containing whitespace) 49 | # For more information, see: https://docs.docker.com/engine/reference/builder/#copy 50 | COPY requirements.txt . 51 | 52 | # Install the Python dependencies 53 | RUN pip install --no-cache-dir --no-deps -r requirements.txt 54 | 55 | # Stage 2 - Copy only necessary files to the runner stage 56 | 57 | # The FROM instruction initializes a new build stage for the application 58 | FROM python:3.11-slim 59 | 60 | # Sets the working directory to /app 61 | WORKDIR /app 62 | 63 | # Copy the virtual environment from the builder stage 64 | COPY --from=builder /opt/venv /opt/venv 65 | 66 | # Set environment variables 67 | ENV PATH="/opt/venv/bin:$PATH" 68 | 69 | # Clone the app.py containing the application code 70 | COPY app.py . 71 | 72 | # Copy the images folder to WORKDIR 73 | # The ADD instruction copies new files, directories or remote file URLs from and adds them to the filesystem of the image at the path . 74 | # For more information, see: https://docs.docker.com/engine/reference/builder/#add 75 | ADD images ./images 76 | 77 | # The EXPOSE instruction informs Docker that the container listens on the specified network ports at runtime. 78 | # For more information, see: https://docs.docker.com/engine/reference/builder/#expose 79 | EXPOSE 8501 80 | 81 | # The HEALTHCHECK instruction has two forms: 82 | # * HEALTHCHECK [OPTIONS] CMD command (check container health by running a command inside the container) 83 | # * HEALTHCHECK NONE (disable any healthcheck inherited from the base image) 84 | # The HEALTHCHECK instruction tells Docker how to test a container to check that it is still working. 85 | # This can detect cases such as a web server that is stuck in an infinite loop and unable to handle new connections, 86 | # even though the server process is still running. For more information, see: https://docs.docker.com/engine/reference/builder/#healthcheck 87 | HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health 88 | 89 | # The ENTRYPOINT instruction has two forms: 90 | # * ENTRYPOINT ["executable", "param1", "param2"] (exec form, preferred) 91 | # * ENTRYPOINT command param1 param2 (shell form) 92 | # The ENTRYPOINT instruction allows you to configure a container that will run as an executable. 93 | # For more information, see: https://docs.docker.com/engine/reference/builder/#entrypoint 94 | ENTRYPOINT ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0"] -------------------------------------------------------------------------------- /bicep/deploymentScript.bicep: -------------------------------------------------------------------------------- 1 | // For more information, see https://docs.microsoft.com/en-us/azure/azure-resource-manager/bicep/deployment-script-bicep 2 | @description('Specifies the name of the deployment script uri.') 3 | param name string = 'BashScript' 4 | 5 | @description('Specifies the name of the user-defined managed identity used by the deployment script.') 6 | param managedIdentityName string = 'ScriptManagedIdentity' 7 | 8 | @description('Specifies the primary script URI.') 9 | param primaryScriptUri string 10 | 11 | @description('Specifies the name of the AKS cluster.') 12 | param clusterName string 13 | 14 | @description('Specifies the resource group name') 15 | param resourceGroupName string = resourceGroup().name 16 | 17 | @description('Specifies the Azure AD tenant id.') 18 | param tenantId string = subscription().tenantId 19 | 20 | @description('Specifies the subscription id.') 21 | param subscriptionId string = subscription().subscriptionId 22 | 23 | @description('Specifies whether creating the Application Gateway and enabling the Application Gateway Ingress Controller or not.') 24 | param applicationGatewayEnabled string 25 | 26 | @description('Specifies the hostname of the application.') 27 | param hostName string 28 | 29 | @description('Specifies the namespace of the application.') 30 | param namespace string 31 | 32 | @description('Specifies the service account of the application.') 33 | param serviceAccountName string = 'magic8ball-sa' 34 | 35 | @description('Specifies the client id of the workload user-defined managed identity.') 36 | param workloadManagedIdentityClientId string 37 | 38 | @description('Specifies the email address for the cert-manager cluster issuer.') 39 | param email string = 'admin@contoso.com' 40 | 41 | @description('Specifies the current datetime') 42 | param utcValue string = utcNow() 43 | 44 | @description('Specifies the location.') 45 | param location string = resourceGroup().location 46 | 47 | @description('Specifies the resource tags.') 48 | param tags object 49 | 50 | // Variables 51 | var clusterAdminRoleDefinitionId = resourceId('Microsoft.Authorization/roleDefinitions', '0ab0b1a8-8aac-4efd-b8c2-3ee1fb270be8') 52 | 53 | // Resources 54 | resource aksCluster 'Microsoft.ContainerService/managedClusters@2022-11-02-preview' existing = { 55 | name: clusterName 56 | } 57 | 58 | resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { 59 | name: managedIdentityName 60 | location: location 61 | tags: tags 62 | } 63 | 64 | resource clusterAdminContributorRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 65 | name: guid(managedIdentity.id, aksCluster.id, clusterAdminRoleDefinitionId) 66 | scope: aksCluster 67 | properties: { 68 | roleDefinitionId: clusterAdminRoleDefinitionId 69 | principalId: managedIdentity.properties.principalId 70 | principalType: 'ServicePrincipal' 71 | } 72 | } 73 | 74 | // Script 75 | resource deploymentScript 'Microsoft.Resources/deploymentScripts@2020-10-01' = { 76 | name: name 77 | location: location 78 | kind: 'AzureCLI' 79 | identity: { 80 | type: 'UserAssigned' 81 | userAssignedIdentities: { 82 | '${managedIdentity.id}': {} 83 | } 84 | } 85 | properties: { 86 | forceUpdateTag: utcValue 87 | azCliVersion: '2.42.0' 88 | timeout: 'PT30M' 89 | environmentVariables: [ 90 | { 91 | name: 'clusterName' 92 | value: clusterName 93 | } 94 | { 95 | name: 'resourceGroupName' 96 | value: resourceGroupName 97 | } 98 | { 99 | name: 'applicationGatewayEnabled' 100 | value: applicationGatewayEnabled 101 | } 102 | { 103 | name: 'tenantId' 104 | value: tenantId 105 | } 106 | { 107 | name: 'subscriptionId' 108 | value: subscriptionId 109 | } 110 | { 111 | name: 'hostName' 112 | value: hostName 113 | } 114 | { 115 | name: 'namespace' 116 | value: namespace 117 | } 118 | { 119 | name: 'serviceAccountName' 120 | value: serviceAccountName 121 | } 122 | { 123 | name: 'workloadManagedIdentityClientId' 124 | value: workloadManagedIdentityClientId 125 | } 126 | { 127 | name: 'email' 128 | value: email 129 | } 130 | ] 131 | primaryScriptUri: primaryScriptUri 132 | cleanupPreference: 'OnSuccess' 133 | retentionInterval: 'P1D' 134 | } 135 | } 136 | 137 | /* 138 | resource log 'Microsoft.Resources/deploymentScripts/logs@2020-10-01' existing = { 139 | parent: deploymentScript 140 | name: 'default' 141 | } 142 | */ 143 | 144 | // output log string = log.properties.log 145 | output result object = deploymentScript.properties.outputs 146 | output namespace string = deploymentScript.properties.outputs.namespace 147 | output serviceAccountName string = deploymentScript.properties.outputs.serviceAccountName 148 | output prometheus string = deploymentScript.properties.outputs.prometheus 149 | output certManager string = deploymentScript.properties.outputs.certManager 150 | output nginxIngressController string = deploymentScript.properties.outputs.nginxIngressController 151 | -------------------------------------------------------------------------------- /bicep/virtualMachine.bicep: -------------------------------------------------------------------------------- 1 | // Parameters 2 | @description('Specifies the name of the virtual machine.') 3 | param vmName string = 'TestVm' 4 | 5 | @description('Specifies the size of the virtual machine.') 6 | param vmSize string = 'Standard_DS3_v2' 7 | 8 | @description('Specifies the resource id of the subnet hosting the virtual machine.') 9 | param vmSubnetId string 10 | 11 | @description('Specifies the name of the storage account where the bootstrap diagnostic logs of the virtual machine are stored.') 12 | param storageAccountName string 13 | 14 | @description('Specifies the image publisher of the disk image used to create the virtual machine.') 15 | param imagePublisher string = 'Canonical' 16 | 17 | @description('Specifies the offer of the platform image or marketplace image used to create the virtual machine.') 18 | param imageOffer string = '0001-com-ubuntu-server-jammy' 19 | 20 | @description('Specifies the Ubuntu version for the VM. This will pick a fully patched image of this given Ubuntu version.') 21 | param imageSku string = '22_04-lts-gen2' 22 | 23 | @description('Specifies the type of authentication when accessing the Virtual Machine. SSH key is recommended.') 24 | @allowed([ 25 | 'sshPublicKey' 26 | 'password' 27 | ]) 28 | param authenticationType string = 'password' 29 | 30 | @description('Specifies the name of the administrator account of the virtual machine.') 31 | param vmAdminUsername string 32 | 33 | @description('Specifies the SSH Key or password for the virtual machine. SSH key is recommended.') 34 | @secure() 35 | param vmAdminPasswordOrKey string 36 | 37 | @description('Specifies the storage account type for OS and data disk.') 38 | @allowed([ 39 | 'Premium_LRS' 40 | 'StandardSSD_LRS' 41 | 'Standard_LRS' 42 | 'UltraSSD_LRS' 43 | ]) 44 | param diskStorageAccountType string = 'Premium_LRS' 45 | 46 | @description('Specifies the number of data disks of the virtual machine.') 47 | @minValue(0) 48 | @maxValue(64) 49 | param numDataDisks int = 1 50 | 51 | @description('Specifies the size in GB of the OS disk of the VM.') 52 | param osDiskSize int = 50 53 | 54 | @description('Specifies the size in GB of the OS disk of the virtual machine.') 55 | param dataDiskSize int = 50 56 | 57 | @description('Specifies the caching requirements for the data disks.') 58 | param dataDiskCaching string = 'ReadWrite' 59 | 60 | @description('Specifies the name of the user-defined managed identity used by the Azure Monitor Agent.') 61 | param managedIdentityName string 62 | 63 | @description('Specifies the location.') 64 | param location string = resourceGroup().location 65 | 66 | @description('Specifies the resource tags.') 67 | param tags object 68 | 69 | // Variables 70 | var vmNicName = '${vmName}Nic' 71 | var linuxConfiguration = { 72 | disablePasswordAuthentication: true 73 | ssh: { 74 | publicKeys: [ 75 | { 76 | path: '/home/${vmAdminUsername}/.ssh/authorized_keys' 77 | keyData: vmAdminPasswordOrKey 78 | } 79 | ] 80 | } 81 | provisionVMAgent: true 82 | } 83 | 84 | // Resources 85 | resource virtualMachineNic 'Microsoft.Network/networkInterfaces@2021-08-01' = { 86 | name: vmNicName 87 | location: location 88 | tags: tags 89 | properties: { 90 | ipConfigurations: [ 91 | { 92 | name: 'ipconfig1' 93 | properties: { 94 | privateIPAllocationMethod: 'Dynamic' 95 | subnet: { 96 | id: vmSubnetId 97 | } 98 | } 99 | } 100 | ] 101 | } 102 | } 103 | 104 | resource storageAccount 'Microsoft.Storage/storageAccounts@2021-09-01' existing = { 105 | name: storageAccountName 106 | } 107 | 108 | resource virtualMachine 'Microsoft.Compute/virtualMachines@2021-11-01' = { 109 | name: vmName 110 | location: location 111 | tags: tags 112 | properties: { 113 | hardwareProfile: { 114 | vmSize: vmSize 115 | } 116 | osProfile: { 117 | computerName: vmName 118 | adminUsername: vmAdminUsername 119 | adminPassword: vmAdminPasswordOrKey 120 | linuxConfiguration: (authenticationType == 'password') ? null : linuxConfiguration 121 | } 122 | storageProfile: { 123 | imageReference: { 124 | publisher: imagePublisher 125 | offer: imageOffer 126 | sku: imageSku 127 | version: 'latest' 128 | } 129 | osDisk: { 130 | name: '${vmName}_OSDisk' 131 | caching: 'ReadWrite' 132 | createOption: 'FromImage' 133 | diskSizeGB: osDiskSize 134 | managedDisk: { 135 | storageAccountType: diskStorageAccountType 136 | } 137 | } 138 | dataDisks: [for j in range(0, numDataDisks): { 139 | caching: dataDiskCaching 140 | diskSizeGB: dataDiskSize 141 | lun: j 142 | name: '${vmName}-DataDisk${j}' 143 | createOption: 'Empty' 144 | managedDisk: { 145 | storageAccountType: diskStorageAccountType 146 | } 147 | }] 148 | } 149 | networkProfile: { 150 | networkInterfaces: [ 151 | { 152 | id: virtualMachineNic.id 153 | } 154 | ] 155 | } 156 | diagnosticsProfile: { 157 | bootDiagnostics: { 158 | enabled: true 159 | storageUri: storageAccount.properties.primaryEndpoints.blob 160 | } 161 | } 162 | } 163 | } 164 | 165 | resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { 166 | name: managedIdentityName 167 | location: location 168 | tags: tags 169 | } 170 | 171 | resource linuxAgent 'Microsoft.Compute/virtualMachines/extensions@2021-11-01' = { 172 | name: 'AzureMonitorLinuxAgent' 173 | parent: virtualMachine 174 | location: location 175 | properties: { 176 | publisher: 'Microsoft.Azure.Monitor' 177 | type: 'AzureMonitorLinuxAgent' 178 | typeHandlerVersion: '1.21' 179 | autoUpgradeMinorVersion: true 180 | enableAutomaticUpgrade: true 181 | settings: { 182 | authentication: { 183 | managedIdentity: { 184 | 'identifier-name': 'mi_res_id' 185 | 'identifier-value': managedIdentity.id 186 | } 187 | } 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /bicep/install-nginx-via-helm-and-create-sa.sh: -------------------------------------------------------------------------------- 1 | # Install kubectl 2 | az aks install-cli --only-show-errors 3 | 4 | # Get AKS credentials 5 | az aks get-credentials \ 6 | --admin \ 7 | --name $clusterName \ 8 | --resource-group $resourceGroupName \ 9 | --subscription $subscriptionId \ 10 | --only-show-errors 11 | 12 | # Check if the cluster is private or not 13 | private=$(az aks show --name $clusterName \ 14 | --resource-group $resourceGroupName \ 15 | --subscription $subscriptionId \ 16 | --query apiServerAccessProfile.enablePrivateCluster \ 17 | --output tsv) 18 | 19 | # Install Helm 20 | curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 -o get_helm.sh -s 21 | chmod 700 get_helm.sh 22 | ./get_helm.sh &>/dev/null 23 | 24 | # Add Helm repos 25 | helm repo add prometheus-community https://prometheus-community.github.io/helm-charts 26 | helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx 27 | helm repo add jetstack https://charts.jetstack.io 28 | 29 | # Update Helm repos 30 | helm repo update 31 | 32 | if [[ $private == 'true' ]]; then 33 | # Log whether the cluster is public or private 34 | echo "$clusterName AKS cluster is public" 35 | 36 | # Install Prometheus 37 | command="helm install prometheus prometheus-community/kube-prometheus-stack \ 38 | --create-namespace \ 39 | --namespace prometheus \ 40 | --set prometheus.prometheusSpec.podMonitorSelectorNilUsesHelmValues=false \ 41 | --set prometheus.prometheusSpec.serviceMonitorSelectorNilUsesHelmValues=false" 42 | 43 | az aks command invoke \ 44 | --name $clusterName \ 45 | --resource-group $resourceGroupName \ 46 | --subscription $subscriptionId \ 47 | --command "$command" 48 | 49 | # Install NGINX ingress controller using the internal load balancer 50 | command="helm install nginx-ingress ingress-nginx/ingress-nginx \ 51 | --create-namespace \ 52 | --namespace ingress-basic \ 53 | --set controller.replicaCount=3 \ 54 | --set controller.nodeSelector.\"kubernetes\.io/os\"=linux \ 55 | --set defaultBackend.nodeSelector.\"kubernetes\.io/os\"=linux \ 56 | --set controller.metrics.enabled=true \ 57 | --set controller.metrics.serviceMonitor.enabled=true \ 58 | --set controller.metrics.serviceMonitor.additionalLabels.release=\"prometheus\" \ 59 | --set controller.service.annotations.\"service\.beta\.kubernetes\.io/azure-load-balancer-health-probe-request-path\"=/healthz" 60 | 61 | az aks command invoke \ 62 | --name $clusterName \ 63 | --resource-group $resourceGroupName \ 64 | --subscription $subscriptionId \ 65 | --command "$command" 66 | 67 | # Install certificate manager 68 | command="helm install cert-manager jetstack/cert-manager \ 69 | --create-namespace \ 70 | --namespace cert-manager \ 71 | --set installCRDs=true \ 72 | --set nodeSelector.\"kubernetes\.io/os\"=linux" 73 | 74 | az aks command invoke \ 75 | --name $clusterName \ 76 | --resource-group $resourceGroupName \ 77 | --subscription $subscriptionId \ 78 | --command "$command" 79 | 80 | # Create cluster issuer 81 | command="cat <$AZ_SCRIPTS_OUTPUT_PATH -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET Core 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # ASP.NET Scaffolding 66 | ScaffoldingReadMe.txt 67 | 68 | # StyleCop 69 | StyleCopReport.xml 70 | 71 | # Files built by Visual Studio 72 | *_i.c 73 | *_p.c 74 | *_h.h 75 | *.ilk 76 | *.meta 77 | *.obj 78 | *.iobj 79 | *.pch 80 | *.pdb 81 | *.ipdb 82 | *.pgc 83 | *.pgd 84 | *.rsp 85 | *.sbr 86 | *.tlb 87 | *.tli 88 | *.tlh 89 | *.tmp 90 | *.tmp_proj 91 | *_wpftmp.csproj 92 | *.log 93 | *.tlog 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 298 | *.vbp 299 | 300 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 301 | *.dsw 302 | *.dsp 303 | 304 | # Visual Studio 6 technical files 305 | *.ncb 306 | *.aps 307 | 308 | # Visual Studio LightSwitch build output 309 | **/*.HTMLClient/GeneratedArtifacts 310 | **/*.DesktopClient/GeneratedArtifacts 311 | **/*.DesktopClient/ModelManifest.xml 312 | **/*.Server/GeneratedArtifacts 313 | **/*.Server/ModelManifest.xml 314 | _Pvt_Extensions 315 | 316 | # Paket dependency manager 317 | .paket/paket.exe 318 | paket-files/ 319 | 320 | # FAKE - F# Make 321 | .fake/ 322 | 323 | # CodeRush personal settings 324 | .cr/personal 325 | 326 | # Python Tools for Visual Studio (PTVS) 327 | __pycache__/ 328 | *.pyc 329 | 330 | # Cake - Uncomment if you are using it 331 | # tools/** 332 | # !tools/packages.config 333 | 334 | # Tabs Studio 335 | *.tss 336 | 337 | # Telerik's JustMock configuration file 338 | *.jmconfig 339 | 340 | # BizTalk build output 341 | *.btp.cs 342 | *.btm.cs 343 | *.odx.cs 344 | *.xsd.cs 345 | 346 | # OpenCover UI analysis results 347 | OpenCover/ 348 | 349 | # Azure Stream Analytics local run output 350 | ASALocalRun/ 351 | 352 | # MSBuild Binary and Structured Log 353 | *.binlog 354 | 355 | # NVidia Nsight GPU debugger configuration file 356 | *.nvuser 357 | 358 | # MFractors (Xamarin productivity tool) working folder 359 | .mfractor/ 360 | 361 | # Local History for Visual Studio 362 | .localhistory/ 363 | 364 | # Visual Studio History (VSHistory) files 365 | .vshistory/ 366 | 367 | # BeatPulse healthcheck temp database 368 | healthchecksdb 369 | 370 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 371 | MigrationBackup/ 372 | 373 | # Ionide (cross platform F# VS Code tools) working folder 374 | .ionide/ 375 | 376 | # Fody - auto-generated XML schema 377 | FodyWeavers.xsd 378 | 379 | # VS Code files for those working on multiple tools 380 | .vscode/* 381 | !.vscode/settings.json 382 | !.vscode/tasks.json 383 | !.vscode/launch.json 384 | !.vscode/extensions.json 385 | *.code-workspace 386 | 387 | # Local History for Visual Studio Code 388 | .history/ 389 | 390 | # Windows Installer files from build outputs 391 | *.cab 392 | *.msi 393 | *.msix 394 | *.msm 395 | *.msp 396 | 397 | # JetBrains Rider 398 | *.sln.iml 399 | -------------------------------------------------------------------------------- /bicep/main.parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "aksClusterNetworkPlugin": { 6 | "value": "azure" 7 | }, 8 | "aksClusterNetworkPluginMode": { 9 | "value": "" 10 | }, 11 | "aksClusterNetworkPolicy": { 12 | "value": "azure" 13 | }, 14 | "aksClusterPodCidr": { 15 | "value": "192.168.0.0/16" 16 | }, 17 | "aksClusterServiceCidr": { 18 | "value": "172.16.0.0/16" 19 | }, 20 | "aksClusterDnsServiceIP": { 21 | "value": "172.16.0.10" 22 | }, 23 | "aksClusterDockerBridgeCidr": { 24 | "value": "172.17.0.1/16" 25 | }, 26 | "aksClusterOutboundType": { 27 | "value": "userAssignedNATGateway" 28 | }, 29 | "aksClusterKubernetesVersion": { 30 | "value": "1.24.0" 31 | }, 32 | "aksClusterAdminUsername": { 33 | "value": "azadmin" 34 | }, 35 | "aksClusterSshPublicKey": { 36 | "value": "" 37 | }, 38 | "aadProfileManaged": { 39 | "value": true 40 | }, 41 | "aadProfileEnableAzureRBAC": { 42 | "value": true 43 | }, 44 | "aadProfileAdminGroupObjectIDs": { 45 | "value": [ 46 | "" 47 | ] 48 | }, 49 | "systemAgentPoolName": { 50 | "value": "system" 51 | }, 52 | "systemAgentPoolVmSize": { 53 | "value": "Standard_D4s_v3" 54 | }, 55 | "systemAgentPoolOsDiskSizeGB": { 56 | "value": 80 57 | }, 58 | "systemAgentPoolAgentCount": { 59 | "value": 3 60 | }, 61 | "systemAgentPoolMaxCount": { 62 | "value": 5 63 | }, 64 | "systemAgentPoolMinCount": { 65 | "value": 3 66 | }, 67 | "systemAgentPoolNodeTaints": { 68 | "value": [ 69 | "CriticalAddonsOnly=true:NoSchedule" 70 | ] 71 | }, 72 | "userAgentPoolName": { 73 | "value": "user" 74 | }, 75 | "userAgentPoolVmSize": { 76 | "value": "Standard_D4s_v3" 77 | }, 78 | "userAgentPoolOsDiskSizeGB": { 79 | "value": 80 80 | }, 81 | "userAgentPoolAgentCount": { 82 | "value": 3 83 | }, 84 | "userAgentPoolMaxCount": { 85 | "value": 5 86 | }, 87 | "userAgentPoolMinCount": { 88 | "value": 3 89 | }, 90 | "enableVnetIntegration": { 91 | "value": true 92 | }, 93 | "virtualNetworkAddressPrefixes": { 94 | "value": "10.0.0.0/8" 95 | }, 96 | "systemAgentPoolSubnetName": { 97 | "value": "SystemSubnet" 98 | }, 99 | "systemAgentPoolSubnetAddressPrefix": { 100 | "value": "10.240.0.0/16" 101 | }, 102 | "userAgentPoolSubnetName": { 103 | "value": "UserSubnet" 104 | }, 105 | "userAgentPoolSubnetAddressPrefix": { 106 | "value": "10.241.0.0/16" 107 | }, 108 | "podSubnetName": { 109 | "value": "PodSubnet" 110 | }, 111 | "podSubnetAddressPrefix": { 112 | "value": "10.242.0.0/16" 113 | }, 114 | "apiServerSubnetName": { 115 | "value": "ApiServerSubnet" 116 | }, 117 | "apiServerSubnetAddressPrefix": { 118 | "value": "10.243.0.0/27" 119 | }, 120 | "vmSubnetName": { 121 | "value": "VmSubnet" 122 | }, 123 | "vmSubnetAddressPrefix": { 124 | "value": "10.243.1.0/24" 125 | }, 126 | "bastionSubnetAddressPrefix": { 127 | "value": "10.243.2.0/24" 128 | }, 129 | "applicationGatewaySubnetName": { 130 | "value": "AppGatewaySubnet" 131 | }, 132 | "applicationGatewaySubnetAddressPrefix": { 133 | "value": "10.243.3.0/24" 134 | }, 135 | "applicationGatewayEnabled": { 136 | "value": false 137 | }, 138 | "applicationGatewayAvailabilityZones": { 139 | "value": [ 140 | "1", 141 | "2", 142 | "3" 143 | ] 144 | }, 145 | "applicationGatewayPrivateIpAddress": { 146 | "value": "10.243.3.4" 147 | }, 148 | "applicationGatewayPrivateLinkEnabled": { 149 | "value": true 150 | }, 151 | "applicationGatewayFrontendIpConfigurationType": { 152 | "value": "Public" 153 | }, 154 | "logAnalyticsSku": { 155 | "value": "PerGB2018" 156 | }, 157 | "logAnalyticsRetentionInDays": { 158 | "value": 60 159 | }, 160 | "vmEnabled": { 161 | "value": true 162 | }, 163 | "vmName": { 164 | "value": "TestVm" 165 | }, 166 | "vmSize": { 167 | "value": "Standard_F4s_v2" 168 | }, 169 | "imagePublisher": { 170 | "value": "Canonical" 171 | }, 172 | "imageOffer": { 173 | "value": "0001-com-ubuntu-server-jammy" 174 | }, 175 | "imageSku": { 176 | "value": "22_04-lts-gen2" 177 | }, 178 | "authenticationType": { 179 | "value": "sshPublicKey" 180 | }, 181 | "vmAdminUsername": { 182 | "value": "azadmin" 183 | }, 184 | "vmAdminPasswordOrKey": { 185 | "value": "" 186 | }, 187 | "diskStorageAccountType": { 188 | "value": "Premium_LRS" 189 | }, 190 | "numDataDisks": { 191 | "value": 1 192 | }, 193 | "osDiskSize": { 194 | "value": 50 195 | }, 196 | "dataDiskSize": { 197 | "value": 50 198 | }, 199 | "dataDiskCaching": { 200 | "value": "ReadWrite" 201 | }, 202 | "aksClusterEnablePrivateCluster": { 203 | "value": false 204 | }, 205 | "aksEnablePrivateClusterPublicFQDN": { 206 | "value": false 207 | }, 208 | "podIdentityProfileEnabled": { 209 | "value": false 210 | }, 211 | "keyVaultObjectIds": { 212 | "value": [ 213 | "" 214 | ] 215 | }, 216 | "openServiceMeshEnabled": { 217 | "value": false 218 | }, 219 | "istioServiceMeshEnabled": { 220 | "value": true 221 | }, 222 | "istioIngressGatewayEnabled": { 223 | "value": true 224 | }, 225 | "istioIngressGatewayType": { 226 | "value": "External" 227 | }, 228 | "kedaEnabled": { 229 | "value": true 230 | }, 231 | "daprEnabled": { 232 | "value": true 233 | }, 234 | "fluxGitOpsEnabled": { 235 | "value": true 236 | }, 237 | "verticalPodAutoscalerEnabled": { 238 | "value": true 239 | }, 240 | "azurePolicyEnabled": { 241 | "value": true 242 | }, 243 | "azureKeyvaultSecretsProviderEnabled": { 244 | "value": true 245 | }, 246 | "kubeDashboardEnabled": { 247 | "value": false 248 | }, 249 | 250 | "systemAgentPoolKubeletDiskType": { 251 | "value": "OS" 252 | }, 253 | "userAgentPoolKubeletDiskType": { 254 | "value": "OS" 255 | }, 256 | "systemAgentPoolOsDiskType": { 257 | "value": "Ephemeral" 258 | }, 259 | "userAgentPoolOsDiskType": { 260 | "value": "Ephemeral" 261 | }, 262 | "deploymentScriptUri": { 263 | "value": "https://paolosalvatori.blob.core.windows.net/scripts/install-nginx-via-helm-and-create-sa.sh" 264 | }, 265 | "blobCSIDriverEnabled": { 266 | "value": true 267 | }, 268 | "diskCSIDriverEnabled": { 269 | "value": true 270 | }, 271 | "fileCSIDriverEnabled": { 272 | "value": true 273 | }, 274 | "snapshotControllerEnabled": { 275 | "value": true 276 | }, 277 | "defenderSecurityMonitoringEnabled": { 278 | "value": true 279 | }, 280 | "imageCleanerEnabled": { 281 | "value": true 282 | }, 283 | "imageCleanerIntervalHours": { 284 | "value": 24 285 | }, 286 | "nodeRestrictionEnabled": { 287 | "value": true 288 | }, 289 | "workloadIdentityEnabled": { 290 | "value": true 291 | }, 292 | "oidcIssuerProfileEnabled": { 293 | "value": true 294 | }, 295 | "openAiEnabled": { 296 | "value": true 297 | }, 298 | "openAiDeployments": { 299 | "value": [ 300 | { 301 | "name": "gpt-35-turbo", 302 | "version": "0301", 303 | "raiPolicyName": "", 304 | "capacity": null, 305 | "scaleType": "Standard" 306 | } 307 | ] 308 | }, 309 | "domain": { 310 | "value": "contoso.internal" 311 | }, 312 | "subdomain": { 313 | "value": "magic8ball" 314 | }, 315 | "namespace": { 316 | "value": "magic8ball" 317 | }, 318 | "serviceAccountName": { 319 | "value": "magic8ball-sa" 320 | }, 321 | "tags": { 322 | "value": { 323 | "iaC": "Bicep" 324 | } 325 | }, 326 | "clusterTags": { 327 | "value": { 328 | "environment": "Development", 329 | "project": "AKS", 330 | "iaC": "Bicep", 331 | "serviceMesh": "Istio", 332 | "networkPlugin": "Azure CNI", 333 | "networkPolicy": "Azure", 334 | "private": "false", 335 | "apiServerVnetIntegration": true, 336 | "podSubnet": true, 337 | "perAgentPoolSubnet": true 338 | } 339 | } 340 | } 341 | } -------------------------------------------------------------------------------- /bicep/applicationGateway.bicep: -------------------------------------------------------------------------------- 1 | // Parameters 2 | @description('Specifies the name of the Application Gateway.') 3 | param name string 4 | 5 | @description('Specifies the sku of the Application Gateway.') 6 | param skuName string = 'WAF_v2' 7 | 8 | @description('Specifies the frontend IP configuration type.') 9 | @allowed([ 10 | 'Public' 11 | 'Private' 12 | 'Both' 13 | ]) 14 | param frontendIpConfigurationType string 15 | 16 | @description('Specifies the name of the public IP adddress used by the Application Gateway.') 17 | param publicIpAddressName string = '${name}PublicIp' 18 | 19 | @description('Specifies the location of the Application Gateway.') 20 | param location string 21 | 22 | @description('Specifies the resource tags.') 23 | param tags object 24 | 25 | @description('Specifies the resource id of the subnet used by the Application Gateway.') 26 | param subnetId string 27 | 28 | @description('Specifies the resource id of the subnet used by the Application Gateway Private Link.') 29 | param privateLinkSubnetId string 30 | 31 | @description('Specifies the private IP address of the Application Gateway.') 32 | param privateIpAddress string 33 | 34 | @description('Specifies the availability zones of the Application Gateway.') 35 | param availabilityZones array 36 | 37 | @description('Specifies the workspace id of the Log Analytics used to monitor the Application Gateway.') 38 | param workspaceId string 39 | 40 | @description('Specifies the lower bound on number of Application Gateway capacity.') 41 | param minCapacity int = 1 42 | 43 | @description('Specifies the upper bound on number of Application Gateway capacity.') 44 | param maxCapacity int = 10 45 | 46 | @description('Specifies whether create or not a Private Link for the Application Gateway.') 47 | param privateLinkEnabled bool = false 48 | 49 | @description('Specifies the name of the WAF policy') 50 | param wafPolicyName string = '${name}WafPolicy' 51 | 52 | @description('Specifies the mode of the WAF policy.') 53 | @allowed([ 54 | 'Detection' 55 | 'Prevention' 56 | ]) 57 | param wafPolicyMode string = 'Prevention' 58 | 59 | @description('Specifies the state of the WAF policy.') 60 | @allowed([ 61 | 'Enabled' 62 | 'Disabled ' 63 | ]) 64 | param wafPolicyState string = 'Enabled' 65 | 66 | @description('Specifies the maximum file upload size in Mb for the WAF policy.') 67 | param wafPolicyFileUploadLimitInMb int = 100 68 | 69 | @description('Specifies the maximum request body size in Kb for the WAF policy.') 70 | param wafPolicyMaxRequestBodySizeInKb int = 128 71 | 72 | @description('Specifies the whether to allow WAF to check request Body.') 73 | param wafPolicyRequestBodyCheck bool = true 74 | 75 | @description('Specifies the rule set type.') 76 | param wafPolicyRuleSetType string = 'OWASP' 77 | 78 | @description('Specifies the rule set version.') 79 | param wafPolicyRuleSetVersion string = '3.2' 80 | 81 | @description('Specifies the name of the Key Vault resource.') 82 | param keyVaultName string 83 | 84 | // Variables 85 | var diagnosticSettingsName = 'diagnosticSettings' 86 | var applicationGatewayResourceId = resourceId('Microsoft.Network/applicationGateways', name) 87 | var keyVaultSecretsUserRoleDefinitionId = resourceId('Microsoft.Authorization/roleDefinitions', '4633458b-17de-408a-b874-0445c86b69e6') 88 | var gatewayIPConfigurationName = 'DefaultGatewayIpConfiguration' 89 | var frontendPortName = 'DefaultFrontendPort' 90 | var backendAddressPoolName = 'DefaultBackendPool' 91 | var backendHttpSettingsName = 'DefaultBackendHttpSettings' 92 | var httpListenerName = 'DefaultHttpListener' 93 | var routingRuleName = 'DefaultRequestRoutingRule' 94 | var privateLinkName = 'DefaultPrivateLink' 95 | var publicFrontendIPConfigurationName = 'PublicFrontendIPConfiguration' 96 | var privateFrontendIPConfigurationName = 'PrivateFrontendIPConfiguration' 97 | var frontendIPConfigurationName = frontendIpConfigurationType == 'Public' ? publicFrontendIPConfigurationName : privateFrontendIPConfigurationName 98 | var applicationGatewayZones = !empty(availabilityZones) ? availabilityZones : [] 99 | 100 | var publicFrontendIPConfiguration = { 101 | name: publicFrontendIPConfigurationName 102 | properties: { 103 | privateIPAllocationMethod: 'Dynamic' 104 | publicIPAddress: { 105 | id: applicationGatewayPublicIpAddress.id 106 | } 107 | privateLinkConfiguration: privateLinkEnabled && frontendIpConfigurationType == 'Public' ? { 108 | id: '${applicationGatewayResourceId}/privateLinkConfigurations/${privateLinkName}' 109 | } : null 110 | } 111 | } 112 | 113 | var privateFrontendIPConfiguration = { 114 | name: privateFrontendIPConfigurationName 115 | properties: { 116 | privateIPAllocationMethod: 'Static' 117 | privateIPAddress: privateIpAddress 118 | subnet: { 119 | id: subnetId 120 | } 121 | privateLinkConfiguration: privateLinkEnabled && frontendIpConfigurationType != 'Public'? { 122 | id: '${applicationGatewayResourceId}/privateLinkConfigurations/${privateLinkName}' 123 | } : null 124 | } 125 | } 126 | 127 | var frontendIPConfigurations = union( 128 | frontendIpConfigurationType == 'Public' ? array(publicFrontendIPConfiguration) : [], 129 | frontendIpConfigurationType == 'Private' ? array(privateFrontendIPConfiguration) : [], 130 | frontendIpConfigurationType == 'Both' ? concat(array(publicFrontendIPConfiguration), array(privateFrontendIPConfiguration)) : [] 131 | ) 132 | 133 | var sku = union({ 134 | name: skuName 135 | tier: skuName 136 | }, maxCapacity == 0 ? { 137 | capacity: minCapacity 138 | } : {}) 139 | 140 | var applicationGatewayProperties = union({ 141 | sku: sku 142 | gatewayIPConfigurations: [ 143 | { 144 | name: gatewayIPConfigurationName 145 | properties: { 146 | subnet: { 147 | id: subnetId 148 | } 149 | } 150 | } 151 | ] 152 | frontendIPConfigurations: frontendIPConfigurations 153 | frontendPorts: [ 154 | { 155 | name: frontendPortName 156 | properties: { 157 | port: 80 158 | } 159 | } 160 | ] 161 | backendAddressPools: [ 162 | { 163 | name: backendAddressPoolName 164 | } 165 | ] 166 | backendHttpSettingsCollection: [ 167 | { 168 | name: backendHttpSettingsName 169 | properties: { 170 | port: 80 171 | protocol: 'Http' 172 | cookieBasedAffinity: 'Disabled' 173 | requestTimeout: 30 174 | pickHostNameFromBackendAddress: true 175 | } 176 | } 177 | ] 178 | httpListeners: [ 179 | { 180 | name: httpListenerName 181 | properties: { 182 | frontendIPConfiguration: { 183 | id: '${applicationGatewayResourceId}/frontendIPConfigurations/${frontendIPConfigurationName}' 184 | } 185 | frontendPort: { 186 | id: '${applicationGatewayResourceId}/frontendPorts/${frontendPortName}' 187 | } 188 | protocol: 'Http' 189 | } 190 | } 191 | ] 192 | requestRoutingRules: [ 193 | { 194 | name: routingRuleName 195 | properties: { 196 | ruleType: 'Basic' 197 | priority: 1000 198 | httpListener: { 199 | id: '${applicationGatewayResourceId}/httpListeners/${httpListenerName}' 200 | } 201 | backendAddressPool: { 202 | id: '${applicationGatewayResourceId}/backendAddressPools/${backendAddressPoolName}' 203 | } 204 | backendHttpSettings: { 205 | id: '${applicationGatewayResourceId}/backendHttpSettingsCollection/${backendHttpSettingsName}' 206 | } 207 | } 208 | } 209 | ] 210 | privateLinkConfigurations: privateLinkEnabled ? [ 211 | { 212 | name: privateLinkName 213 | properties: { 214 | ipConfigurations: [ 215 | { 216 | name: 'PrivateLinkDefaultIPConfiguration' 217 | properties: { 218 | privateIPAllocationMethod: 'Dynamic' 219 | subnet: { 220 | id: privateLinkSubnetId 221 | } 222 | } 223 | } 224 | ] 225 | } 226 | } 227 | ] : [] 228 | firewallPolicy: { 229 | id: wafPolicy.id 230 | } 231 | }, maxCapacity > 0 ? { 232 | autoscaleConfiguration: { 233 | minCapacity: minCapacity 234 | maxCapacity: maxCapacity 235 | } 236 | } : {}) 237 | 238 | var applicationGatewayLogCategories = [ 239 | 'ApplicationGatewayAccessLog' 240 | 'ApplicationGatewayFirewallLog' 241 | 'ApplicationGatewayPerformanceLog' 242 | ] 243 | var applicationGatewayMetricCategories = [ 244 | 'AllMetrics' 245 | ] 246 | var applicationGatewayLogs = [for category in applicationGatewayLogCategories: { 247 | category: category 248 | enabled: true 249 | }] 250 | var applicationGatewayMetrics = [for category in applicationGatewayMetricCategories: { 251 | category: category 252 | enabled: true 253 | }] 254 | 255 | // Resources 256 | resource applicationGatewayIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2018-11-30' = { 257 | name: '${name}Identity' 258 | location: location 259 | } 260 | 261 | resource applicationGatewayPublicIpAddress 'Microsoft.Network/publicIPAddresses@2022-07-01' = if (frontendIpConfigurationType != 'Private') { 262 | name: publicIpAddressName 263 | location: location 264 | zones: applicationGatewayZones 265 | sku: { 266 | name: 'Standard' 267 | } 268 | properties: { 269 | publicIPAllocationMethod: 'Static' 270 | } 271 | } 272 | 273 | resource applicationGateway 'Microsoft.Network/applicationGateways@2022-07-01' = { 274 | name: name 275 | location: location 276 | tags: tags 277 | zones: applicationGatewayZones 278 | identity: { 279 | type: 'UserAssigned' 280 | userAssignedIdentities: { 281 | '${applicationGatewayIdentity.id}': {} 282 | } 283 | } 284 | properties: applicationGatewayProperties 285 | } 286 | 287 | resource wafPolicy 'Microsoft.Network/ApplicationGatewayWebApplicationFirewallPolicies@2022-07-01' = { 288 | name: wafPolicyName 289 | location: location 290 | tags: tags 291 | properties: { 292 | customRules: [ 293 | { 294 | name: 'BlockMe' 295 | priority: 1 296 | ruleType: 'MatchRule' 297 | action: 'Block' 298 | matchConditions: [ 299 | { 300 | matchVariables: [ 301 | { 302 | variableName: 'QueryString' 303 | } 304 | ] 305 | operator: 'Contains' 306 | negationConditon: false 307 | matchValues: [ 308 | 'blockme' 309 | ] 310 | } 311 | ] 312 | } 313 | { 314 | name: 'BlockEvilBot' 315 | priority: 2 316 | ruleType: 'MatchRule' 317 | action: 'Block' 318 | matchConditions: [ 319 | { 320 | matchVariables: [ 321 | { 322 | variableName: 'RequestHeaders' 323 | selector: 'User-Agent' 324 | } 325 | ] 326 | operator: 'Contains' 327 | negationConditon: false 328 | matchValues: [ 329 | 'evilbot' 330 | ] 331 | transforms: [ 332 | 'Lowercase' 333 | ] 334 | } 335 | ] 336 | } 337 | ] 338 | policySettings: { 339 | requestBodyCheck: wafPolicyRequestBodyCheck 340 | maxRequestBodySizeInKb: wafPolicyMaxRequestBodySizeInKb 341 | fileUploadLimitInMb: wafPolicyFileUploadLimitInMb 342 | mode: wafPolicyMode 343 | state: wafPolicyState 344 | } 345 | managedRules: { 346 | managedRuleSets: [ 347 | { 348 | ruleSetType: wafPolicyRuleSetType 349 | ruleSetVersion: wafPolicyRuleSetVersion 350 | } 351 | ] 352 | } 353 | } 354 | } 355 | 356 | resource keyVault 'Microsoft.KeyVault/vaults@2021-10-01' existing = { 357 | name: keyVaultName 358 | } 359 | 360 | resource keyVaultSecretsUserApplicationGatewayIdentityRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 361 | scope: keyVault 362 | name: guid(keyVault.id, applicationGatewayIdentity.name, 'keyVaultSecretsUser') 363 | properties: { 364 | roleDefinitionId: keyVaultSecretsUserRoleDefinitionId 365 | principalType: 'ServicePrincipal' 366 | principalId: applicationGatewayIdentity.properties.principalId 367 | } 368 | } 369 | 370 | resource applicationGatewayDiagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { 371 | name: diagnosticSettingsName 372 | scope: applicationGateway 373 | properties: { 374 | workspaceId: workspaceId 375 | logs: applicationGatewayLogs 376 | metrics: applicationGatewayMetrics 377 | } 378 | } 379 | 380 | // Outputs 381 | output id string = applicationGateway.id 382 | output name string = applicationGateway.name 383 | output privateLinkFrontendIPConfigurationName string = privateLinkEnabled ? frontendIPConfigurationName : '' 384 | output principalId string = applicationGatewayIdentity.properties.principalId 385 | -------------------------------------------------------------------------------- /scripts/app.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2023 Paolo Salvatori 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | # This sample is based on the following article: 26 | # 27 | # - https://levelup.gitconnected.com/its-time-to-create-a-private-chatgpt-for-yourself-today-6503649e7bb6 28 | # 29 | # Use pip to install the following packages: 30 | # 31 | # - streamlit 32 | # - openai 33 | # - streamlit-chat 34 | # - azure.identity 35 | # - dotenv 36 | # 37 | # Make sure to provide a value for the following environment variables: 38 | # 39 | # - AZURE_OPENAI_BASE: the URL of your Azure OpenAI resource, for example https://eastus.api.cognitive.microsoft.com/ 40 | # - AZURE_OPENAI_KEY: the key of your Azure OpenAI resource 41 | # - AZURE_OPENAI_DEPLOYMENT: the name of the ChatGPT deployment used by your Azure OpenAI resource 42 | # - AZURE_OPENAI_MODEL: the name of the ChatGPT model used by your Azure OpenAI resource, for example gpt-35-turbo 43 | # - TITLE: the title of the Streamlit app 44 | # - TEMPERATURE: the temperature used by the OpenAI API to generate the response 45 | # - SYSTEM: give the model instructions about how it should behave and any context it should reference when generating a response. 46 | # Used to describe the assistant's personality. 47 | # 48 | # You can use two different authentication methods: 49 | # 50 | # - API key: set the AZURE_OPENAI_TYPE environment variable to azure and the AZURE_OPENAI_KEY environment variable to the key of 51 | # your Azure OpenAI resource. You can use the regional endpoint, such as https://eastus.api.cognitive.microsoft.com/, passed in 52 | # the AZURE_OPENAI_BASE environment variable, to connect to the Azure OpenAI resource. 53 | # - Azure Active Directory: set the AZURE_OPENAI_TYPE environment variable to azure_ad and use a service principal or managed 54 | # identity with the DefaultAzureCredential object to acquire a token. For more information on the DefaultAzureCredential in Python, 55 | # see https://docs.microsoft.com/en-us/azure/developer/python/azure-sdk-authenticate?tabs=cmd 56 | # Make sure to assign the "Cognitive Services User" role to the service principal or managed identity used to authenticate to 57 | # Azure OpenAI. For more information, see https://learn.microsoft.com/en-us/azure/cognitive-services/openai/how-to/managed-identity. 58 | # If you want to use Azure AD integrated security, you need to create a custom subdomain for your Azure OpenAI resource and use the 59 | # specific endpoint containing the custom domain, such as https://bingo.openai.azure.com/ where bingo is the custom subdomain. 60 | # If you specify the regional endpoint, you get a wonderful error: "Subdomain does not map to a resource.". 61 | # Hence, make sure to pass the endpoint containing the custom domain in the AZURE_OPENAI_BASE environment variable. 62 | # 63 | # Use the following command to run the app: 64 | # 65 | # - streamlit run app.py 66 | 67 | # Import packages 68 | import os 69 | import sys 70 | import time 71 | import openai 72 | import logging 73 | import streamlit as st 74 | from streamlit_chat import message 75 | from azure.identity import DefaultAzureCredential 76 | from dotenv import load_dotenv 77 | from dotenv import dotenv_values 78 | 79 | # Load environment variables from .env file 80 | if os.path.exists(".env"): 81 | load_dotenv(override=True) 82 | config = dotenv_values(".env") 83 | 84 | # Read environment variables 85 | assistan_profile = """ 86 | You are the infamous Magic 8 Ball. You need to randomly reply to any question with one of the following answers: 87 | 88 | - It is certain. 89 | - It is decidedly so. 90 | - Without a doubt. 91 | - Yes definitely. 92 | - You may rely on it. 93 | - As I see it, yes. 94 | - Most likely. 95 | - Outlook good. 96 | - Yes. 97 | - Signs point to yes. 98 | - Reply hazy, try again. 99 | - Ask again later. 100 | - Better not tell you now. 101 | - Cannot predict now. 102 | - Concentrate and ask again. 103 | - Don't count on it. 104 | - My reply is no. 105 | - My sources say no. 106 | - Outlook not so good. 107 | - Very doubtful. 108 | 109 | Add a short comment in a pirate style at the end! Follow your heart and be creative! 110 | For mor information, see https://en.wikipedia.org/wiki/Magic_8_Ball 111 | """ 112 | title = os.environ.get("TITLE", "Magic 8 Ball") 113 | text_input_label = os.environ.get("TEXT_INPUT_LABEL", "Pose your question and cross your fingers!") 114 | image_file_name = os.environ.get("IMAGE_FILE_NAME", "magic8ball.png") 115 | image_width = int(os.environ.get("IMAGE_WIDTH", 80)) 116 | temperature = float(os.environ.get("TEMPERATURE", 0.9)) 117 | system = os.environ.get("SYSTEM", assistan_profile) 118 | api_base = os.getenv("AZURE_OPENAI_BASE") 119 | api_key = os.getenv("AZURE_OPENAI_KEY") 120 | api_type = os.environ.get("AZURE_OPENAI_TYPE", "azure") 121 | api_version = os.environ.get("AZURE_OPENAI_VERSION", "2023-05-15") 122 | engine = os.getenv("AZURE_OPENAI_DEPLOYMENT") 123 | model = os.getenv("AZURE_OPENAI_MODEL") 124 | 125 | # Configure OpenAI 126 | openai.api_type = api_type 127 | openai.api_version = api_version 128 | openai.api_base = api_base 129 | 130 | # Set default Azure credential 131 | default_credential = DefaultAzureCredential() if openai.api_type == "azure_ad" else None 132 | 133 | # Configure a logger 134 | logging.basicConfig(stream = sys.stdout, 135 | format = '[%(asctime)s] {%(filename)s:%(lineno)d} %(levelname)s - %(message)s', 136 | level = logging.INFO) 137 | logger = logging.getLogger(__name__) 138 | 139 | # Log variables 140 | logger.info(f"title: {title}") 141 | logger.info(f"text_input_label: {text_input_label}") 142 | logger.info(f"image_file_name: {image_file_name}") 143 | logger.info(f"image_width: {image_width}") 144 | logger.info(f"temperature: {temperature}") 145 | logger.info(f"system: {system}") 146 | logger.info(f"api_base: {api_base}") 147 | logger.info(f"api_key: {api_key}") 148 | logger.info(f"api_type: {api_type}") 149 | logger.info(f"api_version: {api_version}") 150 | logger.info(f"engine: {engine}") 151 | logger.info(f"model: {model}") 152 | 153 | # Authenticate to Azure OpenAI 154 | if openai.api_type == "azure": 155 | openai.api_key = api_key 156 | elif openai.api_type == "azure_ad": 157 | openai_token = default_credential.get_token("https://cognitiveservices.azure.com/.default") 158 | openai.api_key = openai_token.token 159 | if 'openai_token' not in st.session_state: 160 | st.session_state['openai_token'] = openai_token 161 | else: 162 | logger.error("Invalid API type. Please set the AZURE_OPENAI_TYPE environment variable to azure or azure_ad.") 163 | raise ValueError("Invalid API type. Please set the AZURE_OPENAI_TYPE environment variable to azure or azure_ad.") 164 | 165 | # Customize Streamlit UI using CSS 166 | st.markdown(""" 167 | 244 | """, unsafe_allow_html=True) 245 | 246 | # Initialize Streamlit session state 247 | if 'prompts' not in st.session_state: 248 | st.session_state['prompts'] = [{"role": "system", "content": system}] 249 | 250 | if 'generated' not in st.session_state: 251 | st.session_state['generated'] = [] 252 | 253 | if 'past' not in st.session_state: 254 | st.session_state['past'] = [] 255 | 256 | # Refresh the OpenAI security token every 45 minutes 257 | def refresh_openai_token(): 258 | if st.session_state['openai_token'].expires_on < int(time.time()) - 45 * 60: 259 | st.session_state['openai_token'] = default_credential.get_token("https://cognitiveservices.azure.com/.default") 260 | openai.api_key = st.session_state['openai_token'].token 261 | 262 | # Send user prompt to Azure OpenAI 263 | def generate_response(prompt): 264 | try: 265 | st.session_state['prompts'].append({"role": "user", "content": prompt}) 266 | 267 | if openai.api_type == "azure_ad": 268 | refresh_openai_token() 269 | 270 | completion = openai.ChatCompletion.create( 271 | engine = engine, 272 | model = model, 273 | messages = st.session_state['prompts'], 274 | temperature = temperature, 275 | ) 276 | 277 | message = completion.choices[0].message.content 278 | return message 279 | except Exception as e: 280 | logging.exception(f"Exception in generate_response: {e}") 281 | 282 | # Reset Streamlit session state to start a new chat from scratch 283 | def new_click(): 284 | st.session_state['prompts'] = [{"role": "system", "content": system}] 285 | st.session_state['past'] = [] 286 | st.session_state['generated'] = [] 287 | st.session_state['user'] = "" 288 | 289 | # Handle on_change event for user input 290 | def user_change(): 291 | # Avoid handling the event twice when clicking the Send button 292 | chat_input = st.session_state['user'] 293 | st.session_state['user'] = "" 294 | if (chat_input == '' or 295 | (len(st.session_state['past']) > 0 and chat_input == st.session_state['past'][-1])): 296 | return 297 | 298 | # Generate response invoking Azure OpenAI LLM 299 | if chat_input != '': 300 | output = generate_response(chat_input) 301 | 302 | # store the output 303 | st.session_state['past'].append(chat_input) 304 | st.session_state['generated'].append(output) 305 | st.session_state['prompts'].append({"role": "assistant", "content": output}) 306 | 307 | # Create a 2-column layout. Note: Streamlit columns do not properly render on mobile devices. 308 | # For more information, see https://github.com/streamlit/streamlit/issues/5003 309 | col1, col2 = st.columns([1, 7]) 310 | 311 | # Display the robot image 312 | with col1: 313 | st.image(image = os.path.join("images", image_file_name), width = image_width) 314 | 315 | # Display the title 316 | with col2: 317 | st.title(title) 318 | 319 | # Create a 3-column layout. Note: Streamlit columns do not properly render on mobile devices. 320 | # For more information, see https://github.com/streamlit/streamlit/issues/5003 321 | col3, col4, col5 = st.columns([7, 1, 1]) 322 | 323 | # Create text input in column 1 324 | with col3: 325 | user_input = st.text_input(text_input_label, key = "user", on_change = user_change) 326 | 327 | # Create send button in column 2 328 | with col4: 329 | st.button(label = "Send") 330 | 331 | # Create new button in column 3 332 | with col5: 333 | st.button(label = "New", on_click = new_click) 334 | 335 | # Display the chat history in two separate tabs 336 | # - normal: display the chat history as a list of messages using the streamlit_chat message() function 337 | # - rich: display the chat history as a list of messages using the Streamlit markdown() function 338 | if st.session_state['generated']: 339 | tab1, tab2 = st.tabs(["normal", "rich"]) 340 | with tab1: 341 | for i in range(len(st.session_state['generated']) - 1, -1, -1): 342 | message(st.session_state['past'][i], is_user = True, key = str(i) + '_user', avatar_style = "fun-emoji", seed = "Nala") 343 | message(st.session_state['generated'][i], key = str(i), avatar_style = "bottts", seed = "Fluffy") 344 | with tab2: 345 | for i in range(len(st.session_state['generated']) - 1, -1, -1): 346 | st.markdown(st.session_state['past'][i]) 347 | st.markdown(st.session_state['generated'][i]) -------------------------------------------------------------------------------- /bicep/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Template 4 | template="main.bicep" 5 | parameters="main.parameters.json" 6 | 7 | # AKS cluster name 8 | prefix="Grey" 9 | aksName="${prefix}Aks" 10 | validateTemplate=1 11 | useWhatIf=1 12 | update=1 13 | installExtensions=0 14 | 15 | # Name and location of the resource group for the Azure Kubernetes Service (AKS) cluster 16 | aksResourceGroupName="${prefix}RG" 17 | location="FranceCentral" 18 | 19 | # Name and resource group name of the Azure Container Registry used by the AKS cluster. 20 | # The name of the cluster is also used to create or select an existing admin group in the Azure AD tenant. 21 | acrName="${prefix}Acr" 22 | acrResourceGroupName="$aksResourceGroupName" 23 | acrSku="Premium" 24 | 25 | # Name of Key Vault 26 | keyVaultName="${prefix}KeyVault" 27 | 28 | # Name of the Log Analytics 29 | logAnalyticsWorkspaceName="${prefix}LogAnalytics" 30 | 31 | # Name of the virtual machine 32 | vmName="${prefix}Vm" 33 | 34 | # Subscription id, subscription name, and tenant id of the current subscription 35 | subscriptionId=$(az account show --query id --output tsv) 36 | subscriptionName=$(az account show --query name --output tsv) 37 | tenantId=$(az account show --query tenantId --output tsv) 38 | 39 | # Install aks-preview Azure extension 40 | if [[ $installExtensions == 1 ]]; then 41 | echo "Checking if [aks-preview] extension is already installed..." 42 | az extension show --name aks-preview &>/dev/null 43 | 44 | if [[ $? == 0 ]]; then 45 | echo "[aks-preview] extension is already installed" 46 | 47 | # Update the extension to make sure you have the latest version installed 48 | echo "Updating [aks-preview] extension..." 49 | az extension update --name aks-preview &>/dev/null 50 | else 51 | echo "[aks-preview] extension is not installed. Installing..." 52 | 53 | # Install aks-preview extension 54 | az extension add --name aks-preview 1>/dev/null 55 | 56 | if [[ $? == 0 ]]; then 57 | echo "[aks-preview] extension successfully installed" 58 | else 59 | echo "Failed to install [aks-preview] extension" 60 | exit 61 | fi 62 | fi 63 | 64 | # Registering AKS features 65 | aksExtensions=( 66 | "AzureServiceMeshPreview" 67 | "AKS-KedaPreview" 68 | "RunCommandPreview" 69 | "EnableOIDCIssuerPreview" 70 | "EnableWorkloadIdentityPreview" 71 | "EnableImageCleanerPreview" 72 | "AKS-VPAPreview") 73 | ok=0 74 | registeringExtensions=() 75 | for aksExtension in ${aksExtensions[@]}; do 76 | echo "Checking if [$aksExtension] extension is already registered..." 77 | extension=$(az feature list -o table --query "[?contains(name, 'Microsoft.ContainerService/$aksExtension') && @.properties.state == 'Registered'].{Name:name}" --output tsv) 78 | if [[ -z $extension ]]; then 79 | echo "[$aksExtension] extension is not registered." 80 | echo "Registering [$aksExtension] extension..." 81 | az feature register --name $aksExtension --namespace Microsoft.ContainerService 82 | registeringExtensions+=("$aksExtension") 83 | ok=1 84 | else 85 | echo "[$aksExtension] extension is already registered." 86 | fi 87 | done 88 | echo $registeringExtensions 89 | delay=1 90 | for aksExtension in ${registeringExtensions[@]}; do 91 | echo -n "Checking if [$aksExtension] extension is already registered..." 92 | while true; do 93 | extension=$(az feature list -o table --query "[?contains(name, 'Microsoft.ContainerService/$aksExtension') && @.properties.state == 'Registered'].{Name:name}" --output tsv) 94 | if [[ -z $extension ]]; then 95 | echo -n "." 96 | sleep $delay 97 | else 98 | echo "." 99 | break 100 | fi 101 | done 102 | done 103 | 104 | if [[ $ok == 1 ]]; then 105 | echo "Refreshing the registration of the Microsoft.ContainerService resource provider..." 106 | az provider register --namespace Microsoft.ContainerService 107 | echo "Microsoft.ContainerService resource provider registration successfully refreshed" 108 | fi 109 | fi 110 | 111 | # Get the last Kubernetes version available in the region 112 | kubernetesVersion=$(az aks get-versions --location $location --query "orchestrators[?isPreview==false].orchestratorVersion | sort(@) | [-1]" --output tsv) 113 | 114 | if [[ -n $kubernetesVersion ]]; then 115 | echo "Successfully retrieved the last Kubernetes version [$kubernetesVersion] supported by AKS in [$location] Azure region" 116 | else 117 | echo "Failed to retrieve the last Kubernetes version supported by AKS in [$location] Azure region" 118 | exit 119 | fi 120 | 121 | # Check if the resource group already exists 122 | echo "Checking if [$aksResourceGroupName] resource group actually exists in the [$subscriptionName] subscription..." 123 | 124 | az group show --name $aksResourceGroupName &>/dev/null 125 | 126 | if [[ $? != 0 ]]; then 127 | echo "No [$aksResourceGroupName] resource group actually exists in the [$subscriptionName] subscription" 128 | echo "Creating [$aksResourceGroupName] resource group in the [$subscriptionName] subscription..." 129 | 130 | # Create the resource group 131 | az group create --name $aksResourceGroupName --location $location 1>/dev/null 132 | 133 | if [[ $? == 0 ]]; then 134 | echo "[$aksResourceGroupName] resource group successfully created in the [$subscriptionName] subscription" 135 | else 136 | echo "Failed to create [$aksResourceGroupName] resource group in the [$subscriptionName] subscription" 137 | exit 138 | fi 139 | else 140 | echo "[$aksResourceGroupName] resource group already exists in the [$subscriptionName] subscription" 141 | fi 142 | 143 | # Create AKS cluster if does not exist 144 | echo "Checking if [$aksName] aks cluster actually exists in the [$aksResourceGroupName] resource group..." 145 | 146 | az aks show --name $aksName --resource-group $aksResourceGroupName &>/dev/null 147 | notExists=$? 148 | 149 | if [[ $notExists != 0 || $update == 1 ]]; then 150 | 151 | if [[ $notExists != 0 ]]; then 152 | echo "No [$aksName] aks cluster actually exists in the [$aksResourceGroupName] resource group" 153 | else 154 | echo "[$aksName] aks cluster already exists in the [$aksResourceGroupName] resource group. Updating the cluster..." 155 | fi 156 | 157 | # Delete any existing role assignments for the user-defined managed identity of the AKS cluster 158 | # in case you are re-deploying the solution in an existing resource group 159 | echo "Retrieving the list of role assignments on [$aksResourceGroupName] resource group..." 160 | assignmentIds=$(az role assignment list \ 161 | --scope "/subscriptions/${subscriptionId}/resourceGroups/${aksResourceGroupName}" \ 162 | --query [].id \ 163 | --output tsv \ 164 | --only-show-errors) 165 | 166 | if [[ -n $assignmentIds ]]; then 167 | echo "[${#assignmentIds[@]}] role assignments have been found on [$aksResourceGroupName] resource group" 168 | for assignmentId in ${assignmentIds[@]}; do 169 | if [[ -n $assignmentId ]]; then 170 | az role assignment delete --ids $assignmentId 171 | 172 | if [[ $? == 0 ]]; then 173 | assignmentName=$(echo $assignmentId | awk -F '/' '{print $NF}') 174 | echo "[$assignmentName] role assignment on [$aksResourceGroupName] resource group successfully deleted" 175 | fi 176 | fi 177 | done 178 | else 179 | echo "No role assignment actually exists on [$aksResourceGroupName] resource group" 180 | fi 181 | 182 | # Get the kubelet managed identity used by the AKS cluster 183 | echo "Retrieving the kubelet identity from the [$aksName] AKS cluster..." 184 | clientId=$(az aks show \ 185 | --name $aksName \ 186 | --resource-group $aksResourceGroupName \ 187 | --query identityProfile.kubeletidentity.clientId \ 188 | --output tsv 2>/dev/null) 189 | 190 | if [[ -n $clientId ]]; then 191 | # Delete any role assignment to kubelet managed identity on any ACR in the resource group 192 | echo "kubelet identity of the [$aksName] AKS cluster successfully retrieved" 193 | echo "Retrieving the list of ACR resources in the [$aksResourceGroupName] resource group..." 194 | acrIds=$(az acr list \ 195 | --resource-group $aksResourceGroupName \ 196 | --query [].id \ 197 | --output tsv) 198 | 199 | if [[ -n $acrIds ]]; then 200 | echo "[${#acrIds[@]}] ACR resources have been found in [$aksResourceGroupName] resource group" 201 | for acrId in ${acrIds[@]}; do 202 | if [[ -n $acrId ]]; then 203 | acrName=$(echo $acrId | awk -F '/' '{print $NF}') 204 | echo "Retrieving the list of role assignments on [$acrName] ACR..." 205 | assignmentIds=$(az role assignment list \ 206 | --scope "$acrId" \ 207 | --query [].id \ 208 | --output tsv \ 209 | --only-show-errors) 210 | 211 | if [[ -n $assignmentIds ]]; then 212 | echo "[${#assignmentIds[@]}] role assignments have been found on [$acrName] ACR" 213 | for assignmentId in ${assignmentIds[@]}; do 214 | if [[ -n $assignmentId ]]; then 215 | az role assignment delete --ids $assignmentId 216 | 217 | if [[ $? == 0 ]]; then 218 | assignmentName=$(echo $assignmentId | awk -F '/' '{print $NF}') 219 | echo "[$assignmentName] role assignment on [$acrName] ACR successfully deleted" 220 | fi 221 | fi 222 | done 223 | else 224 | echo "No role assignment actually exists on [$acrName] ACR" 225 | fi 226 | fi 227 | done 228 | else 229 | echo "No ACR actually exists in [$aksResourceGroupName] resource group" 230 | fi 231 | else 232 | echo "No kubelet identity exists for the [$aksName] AKS cluster" 233 | fi 234 | 235 | # Validate the Bicep template 236 | if [[ $validateTemplate == 1 ]]; then 237 | if [[ $useWhatIf == 1 ]]; then 238 | # Execute a deployment What-If operation at resource group scope. 239 | echo "Previewing changes deployed by [$template] Bicep template..." 240 | az deployment group what-if \ 241 | --resource-group $aksResourceGroupName \ 242 | --template-file $template \ 243 | --parameters $parameters \ 244 | --parameters \ 245 | prefix=$prefix \ 246 | aksClusterName=$aksName \ 247 | aksClusterKubernetesVersion=$kubernetesVersion \ 248 | acrName=$acrName \ 249 | keyVaultName=$keyVaultName \ 250 | logAnalyticsWorkspaceName=$logAnalyticsWorkspaceName \ 251 | vmName=$vmName 252 | 253 | if [[ $? == 0 ]]; then 254 | echo "[$template] Bicep template validation succeeded" 255 | else 256 | echo "Failed to validate [$template] Bicep template" 257 | exit 258 | fi 259 | else 260 | # Validate the Bicep template 261 | echo "Validating [$template] Bicep template..." 262 | output=$(az deployment group validate \ 263 | --resource-group $aksResourceGroupName \ 264 | --template-file $template \ 265 | --parameters $parameters \ 266 | --parameters \ 267 | prefix=$prefix \ 268 | aksClusterName=$aksName \ 269 | aksClusterKubernetesVersion=$kubernetesVersion \ 270 | acrName=$acrName \ 271 | keyVaultName=$keyVaultName \ 272 | logAnalyticsWorkspaceName=$logAnalyticsWorkspaceName \ 273 | vmName=$vmName) 274 | 275 | if [[ $? == 0 ]]; then 276 | echo "[$template] Bicep template validation succeeded" 277 | else 278 | echo "Failed to validate [$template] Bicep template" 279 | echo $output 280 | exit 281 | fi 282 | fi 283 | fi 284 | 285 | # Deploy the Bicep template 286 | echo "Deploying [$template] Bicep template..." 287 | az deployment group create \ 288 | --resource-group $aksResourceGroupName \ 289 | --only-show-errors \ 290 | --template-file $template \ 291 | --parameters $parameters \ 292 | --parameters \ 293 | prefix=$prefix \ 294 | aksClusterName=$aksName \ 295 | aksClusterKubernetesVersion=$kubernetesVersion \ 296 | acrName=$acrName \ 297 | keyVaultName=$keyVaultName \ 298 | logAnalyticsWorkspaceName=$logAnalyticsWorkspaceName \ 299 | vmName=$vmName 1>/dev/null 300 | 301 | if [[ $? == 0 ]]; then 302 | echo "[$template] Bicep template successfully provisioned" 303 | else 304 | echo "Failed to provision the [$template] Bicep template" 305 | exit 306 | fi 307 | else 308 | echo "[$aksName] aks cluster already exists in the [$aksResourceGroupName] resource group" 309 | fi 310 | 311 | # Create AKS cluster if does not exist 312 | echo "Checking if [$aksName] aks cluster actually exists in the [$aksResourceGroupName] resource group..." 313 | 314 | az aks show --name $aksName --resource-group $aksResourceGroupName &>/dev/null 315 | 316 | if [[ $? != 0 ]]; then 317 | echo "No [$aksName] aks cluster actually exists in the [$aksResourceGroupName] resource group" 318 | exit 319 | fi 320 | 321 | # Get the user principal name of the current user 322 | echo "Retrieving the user principal name of the current user from the [$tenantId] Azure AD tenant..." 323 | userPrincipalName=$(az account show --query user.name --output tsv) 324 | if [[ -n $userPrincipalName ]]; then 325 | echo "[$userPrincipalName] user principal name successfully retrieved from the [$tenantId] Azure AD tenant" 326 | else 327 | echo "Failed to retrieve the user principal name of the current user from the [$tenantId] Azure AD tenant" 328 | exit 329 | fi 330 | 331 | # Retrieve the objectId of the user in the Azure AD tenant used by AKS for user authentication 332 | echo "Retrieving the objectId of the [$userPrincipalName] user principal name from the [$tenantId] Azure AD tenant..." 333 | userObjectId=$(az ad user show --id $userPrincipalName --query id --output tsv 2>/dev/null) 334 | 335 | if [[ -n $userObjectId ]]; then 336 | echo "[$userObjectId] objectId successfully retrieved for the [$userPrincipalName] user principal name" 337 | else 338 | echo "Failed to retrieve the objectId of the [$userPrincipalName] user principal name" 339 | exit 340 | fi 341 | 342 | # Retrieve the resource id of the AKS cluster 343 | echo "Retrieving the resource id of the [$aksName] AKS cluster..." 344 | aksClusterId=$(az aks show \ 345 | --name "$aksName" \ 346 | --resource-group "$aksResourceGroupName" \ 347 | --query id \ 348 | --output tsv 2>/dev/null) 349 | 350 | if [[ -n $aksClusterId ]]; then 351 | echo "Resource id of the [$aksName] AKS cluster successfully retrieved" 352 | else 353 | echo "Failed to retrieve the resource id of the [$aksName] AKS cluster" 354 | exit 355 | fi 356 | 357 | # Assign Azure Kubernetes Service RBAC Cluster Admin role to the current user 358 | role="Azure Kubernetes Service RBAC Cluster Admin" 359 | echo "Checking if [$userPrincipalName] user has been assigned to [$role] role on the [$aksName] AKS cluster..." 360 | current=$(az role assignment list \ 361 | --assignee $userObjectId \ 362 | --scope $aksClusterId \ 363 | --query "[?roleDefinitionName=='$role'].roleDefinitionName" \ 364 | --output tsv 2>/dev/null) 365 | 366 | if [[ $current == "Owner" ]] || [[ $current == "Contributor" ]] || [[ $current == "$role" ]]; then 367 | echo "[$userPrincipalName] user is already assigned to the [$current] role on the [$aksName] AKS cluster" 368 | else 369 | echo "[$userPrincipalName] user is not assigned to the [$role] role on the [$aksName] AKS cluster" 370 | echo "Assigning the [$userPrincipalName] user to the [$role] role on the [$aksName] AKS cluster..." 371 | 372 | az role assignment create \ 373 | --role "$role" \ 374 | --assignee $userObjectId \ 375 | --scope $aksClusterId \ 376 | --only-show-errors 1>/dev/null 377 | 378 | if [[ $? == 0 ]]; then 379 | echo "[$userPrincipalName] user successfully assigned to the [$role] role on the [$aksName] AKS cluster" 380 | else 381 | echo "Failed to assign the [$userPrincipalName] user to the [$role] role on the [$aksName] AKS cluster" 382 | exit 383 | fi 384 | fi 385 | 386 | # Assign Azure Kubernetes Service Cluster Admin Role role to the current user 387 | role="Azure Kubernetes Service Cluster Admin Role" 388 | echo "Checking if [$userPrincipalName] user has been assigned to [$role] role on the [$aksName] AKS cluster..." 389 | current=$(az role assignment list \ 390 | --assignee $userObjectId \ 391 | --scope $aksClusterId \ 392 | --query "[?roleDefinitionName=='$role'].roleDefinitionName" \ 393 | --output tsv 2>/dev/null) 394 | 395 | if [[ $current == "Owner" ]] || [[ $current == "Contributor" ]] || [[ $current == "$role" ]]; then 396 | echo "[$userPrincipalName] user is already assigned to the [$current] role on the [$aksName] AKS cluster" 397 | else 398 | echo "[$userPrincipalName] user is not assigned to the [$role] role on the [$aksName] AKS cluster" 399 | echo "Assigning the [$userPrincipalName] user to the [$role] role on the [$aksName] AKS cluster..." 400 | 401 | az role assignment create \ 402 | --role "$role" \ 403 | --assignee $userObjectId \ 404 | --scope $aksClusterId \ 405 | --only-show-errors 1>/dev/null 406 | 407 | if [[ $? == 0 ]]; then 408 | echo "[$userPrincipalName] user successfully assigned to the [$role] role on the [$aksName] AKS cluster" 409 | else 410 | echo "Failed to assign the [$userPrincipalName] user to the [$role] role on the [$aksName] AKS cluster" 411 | exit 412 | fi 413 | fi 414 | -------------------------------------------------------------------------------- /bicep/metricAlerts.bicep: -------------------------------------------------------------------------------- 1 | // Parameters 2 | @description('The name of the AKS Cluster to configure the alerts on.') 3 | param aksClusterName string 4 | 5 | @description('Specifies the resource tags.') 6 | param tags object 7 | 8 | @description('Select the frequency on how often the alert rule should be run. Selecting frequency smaller than granularity of datapoints grouping will result in sliding window evaluation') 9 | @allowed([ 10 | 'PT1M' 11 | 'PT15M' 12 | ]) 13 | param evalFrequency string = 'PT1M' 14 | 15 | @description('Specifies whether metric alerts as either enabled or disabled.') 16 | param metricAlertsEnabled bool = true 17 | 18 | @description('Defines the interval over which datapoints are grouped using the aggregation type function') 19 | @allowed([ 20 | 'PT5M' 21 | 'PT1H' 22 | ]) 23 | param windowSize string = 'PT5M' 24 | 25 | @allowed([ 26 | 'Critical' 27 | 'Error' 28 | 'Warning' 29 | 'Informational' 30 | 'Verbose' 31 | ]) 32 | param alertSeverity string = 'Informational' 33 | 34 | var alertServerityLookup = { 35 | Critical: 0 36 | Error: 1 37 | Warning: 2 38 | Informational: 3 39 | Verbose: 4 40 | } 41 | var alertSeverityNumber = alertServerityLookup[alertSeverity] 42 | 43 | var AksResourceId = resourceId('Microsoft.ContainerService/managedClusters', aksClusterName) 44 | 45 | resource nodeCpuUtilizationHighForAksCluster 'Microsoft.Insights/metricAlerts@2018-03-01' = { 46 | name: '${aksClusterName} | Node CPU utilization high' 47 | location: 'global' 48 | tags: tags 49 | properties: { 50 | criteria: { 51 | allOf: [ 52 | { 53 | criterionType: 'StaticThresholdCriterion' 54 | dimensions: [ 55 | { 56 | name: 'host' 57 | operator: 'Include' 58 | values: [ 59 | '*' 60 | ] 61 | } 62 | ] 63 | metricName: 'cpuUsagePercentage' 64 | metricNamespace: 'Insights.Container/nodes' 65 | name: 'Metric1' 66 | operator: 'GreaterThan' 67 | threshold: 80 68 | timeAggregation: 'Average' 69 | skipMetricValidation: true 70 | } 71 | ] 72 | 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' 73 | } 74 | description: 'Node CPU utilization across the cluster.' 75 | enabled: metricAlertsEnabled 76 | evaluationFrequency: evalFrequency 77 | scopes: [ 78 | AksResourceId 79 | ] 80 | severity: alertSeverityNumber 81 | targetResourceType: 'microsoft.containerservice/managedclusters' 82 | windowSize: windowSize 83 | } 84 | } 85 | 86 | resource nodeWorkingSetMemoryUtilizationHighForAksCluster 'Microsoft.Insights/metricAlerts@2018-03-01' = { 87 | name: '${aksClusterName} | Node working set memory utilization high' 88 | location: 'global' 89 | tags: tags 90 | properties: { 91 | criteria: { 92 | allOf: [ 93 | { 94 | criterionType: 'StaticThresholdCriterion' 95 | dimensions: [ 96 | { 97 | name: 'host' 98 | operator: 'Include' 99 | values: [ 100 | '*' 101 | ] 102 | } 103 | ] 104 | metricName: 'memoryWorkingSetPercentage' 105 | metricNamespace: 'Insights.Container/nodes' 106 | name: 'Metric1' 107 | operator: 'GreaterThan' 108 | threshold: 80 109 | timeAggregation: 'Average' 110 | skipMetricValidation: true 111 | } 112 | ] 113 | 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' 114 | } 115 | description: 'Node working set memory utilization across the cluster.' 116 | enabled: metricAlertsEnabled 117 | evaluationFrequency: evalFrequency 118 | scopes: [ 119 | AksResourceId 120 | ] 121 | severity: alertSeverityNumber 122 | targetResourceType: 'microsoft.containerservice/managedclusters' 123 | windowSize: windowSize 124 | } 125 | } 126 | 127 | resource jobsCompletedMoreThanSixHoursAgoForAksCluster 'Microsoft.Insights/metricAlerts@2018-03-01' = { 128 | name: '${aksClusterName} | Jobs completed more than 6 hours ago' 129 | location: 'global' 130 | tags: tags 131 | properties: { 132 | criteria: { 133 | allOf: [ 134 | { 135 | criterionType: 'StaticThresholdCriterion' 136 | dimensions: [ 137 | { 138 | name: 'controllerName' 139 | operator: 'Include' 140 | values: [ 141 | '*' 142 | ] 143 | } 144 | { 145 | name: 'kubernetes namespace' 146 | operator: 'Include' 147 | values: [ 148 | '*' 149 | ] 150 | } 151 | ] 152 | metricName: 'completedJobsCount' 153 | metricNamespace: 'Insights.Container/pods' 154 | name: 'Metric1' 155 | operator: 'GreaterThan' 156 | threshold: 0 157 | timeAggregation: 'Average' 158 | skipMetricValidation: true 159 | } 160 | ] 161 | 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' 162 | } 163 | description: 'This alert monitors completed jobs (more than 6 hours ago).' 164 | enabled: metricAlertsEnabled 165 | evaluationFrequency: evalFrequency 166 | scopes: [ 167 | AksResourceId 168 | ] 169 | severity: alertSeverityNumber 170 | targetResourceType: 'microsoft.containerservice/managedclusters' 171 | windowSize: windowSize 172 | } 173 | } 174 | 175 | resource containerCpuUsageHighForAksCluster 'Microsoft.Insights/metricAlerts@2018-03-01' = { 176 | name: '${aksClusterName} | Container CPU usage high' 177 | location: 'global' 178 | tags: tags 179 | properties: { 180 | criteria: { 181 | allOf: [ 182 | { 183 | criterionType: 'StaticThresholdCriterion' 184 | dimensions: [ 185 | { 186 | name: 'controllerName' 187 | operator: 'Include' 188 | values: [ 189 | '*' 190 | ] 191 | } 192 | { 193 | name: 'kubernetes namespace' 194 | operator: 'Include' 195 | values: [ 196 | '*' 197 | ] 198 | } 199 | ] 200 | metricName: 'cpuExceededPercentage' 201 | metricNamespace: 'Insights.Container/containers' 202 | name: 'Metric1' 203 | operator: 'GreaterThan' 204 | threshold: 90 205 | timeAggregation: 'Average' 206 | skipMetricValidation: true 207 | } 208 | ] 209 | 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' 210 | } 211 | description: 'This alert monitors container CPU utilization.' 212 | enabled: metricAlertsEnabled 213 | evaluationFrequency: evalFrequency 214 | scopes: [ 215 | AksResourceId 216 | ] 217 | severity: alertSeverityNumber 218 | targetResourceType: 'microsoft.containerservice/managedclusters' 219 | windowSize: windowSize 220 | } 221 | } 222 | 223 | resource containerWorkingSetMemoryUsageHighForAksCluster 'Microsoft.Insights/metricAlerts@2018-03-01' = { 224 | name: '${aksClusterName} | Container working set memory usage high' 225 | location: 'global' 226 | tags: tags 227 | properties: { 228 | criteria: { 229 | allOf: [ 230 | { 231 | criterionType: 'StaticThresholdCriterion' 232 | dimensions: [ 233 | { 234 | name: 'controllerName' 235 | operator: 'Include' 236 | values: [ 237 | '*' 238 | ] 239 | } 240 | { 241 | name: 'kubernetes namespace' 242 | operator: 'Include' 243 | values: [ 244 | '*' 245 | ] 246 | } 247 | ] 248 | metricName: 'memoryWorkingSetExceededPercentage' 249 | metricNamespace: 'Insights.Container/containers' 250 | name: 'Metric1' 251 | operator: 'GreaterThan' 252 | threshold: 90 253 | timeAggregation: 'Average' 254 | skipMetricValidation: true 255 | } 256 | ] 257 | 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' 258 | } 259 | description: 'This alert monitors container working set memory utilization.' 260 | enabled: metricAlertsEnabled 261 | evaluationFrequency: evalFrequency 262 | scopes: [ 263 | AksResourceId 264 | ] 265 | severity: alertSeverityNumber 266 | targetResourceType: 'microsoft.containerservice/managedclusters' 267 | windowSize: windowSize 268 | } 269 | } 270 | 271 | resource podsInFailedStateForAksCluster 'Microsoft.Insights/metricAlerts@2018-03-01' = { 272 | name: '${aksClusterName} | Pods in failed state' 273 | location: 'global' 274 | tags: tags 275 | properties: { 276 | criteria: { 277 | allOf: [ 278 | { 279 | criterionType: 'StaticThresholdCriterion' 280 | dimensions: [ 281 | { 282 | name: 'phase' 283 | operator: 'Include' 284 | values: [ 285 | 'Failed' 286 | ] 287 | } 288 | ] 289 | metricName: 'podCount' 290 | metricNamespace: 'Insights.Container/pods' 291 | name: 'Metric1' 292 | operator: 'GreaterThan' 293 | threshold: 0 294 | timeAggregation: 'Average' 295 | skipMetricValidation: true 296 | } 297 | ] 298 | 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' 299 | } 300 | description: 'Pod status monitoring.' 301 | enabled: metricAlertsEnabled 302 | evaluationFrequency: evalFrequency 303 | scopes: [ 304 | AksResourceId 305 | ] 306 | severity: alertSeverityNumber 307 | targetResourceType: 'microsoft.containerservice/managedclusters' 308 | windowSize: windowSize 309 | } 310 | } 311 | 312 | resource diskUsageHighForAksCluster 'Microsoft.Insights/metricAlerts@2018-03-01' = { 313 | name: '${aksClusterName} | Disk usage high' 314 | location: 'global' 315 | tags: tags 316 | properties: { 317 | criteria: { 318 | allOf: [ 319 | { 320 | criterionType: 'StaticThresholdCriterion' 321 | dimensions: [ 322 | { 323 | name: 'host' 324 | operator: 'Include' 325 | values: [ 326 | '*' 327 | ] 328 | } 329 | { 330 | name: 'device' 331 | operator: 'Include' 332 | values: [ 333 | '*' 334 | ] 335 | } 336 | ] 337 | metricName: 'DiskUsedPercentage' 338 | metricNamespace: 'Insights.Container/nodes' 339 | name: 'Metric1' 340 | operator: 'GreaterThan' 341 | threshold: 80 342 | timeAggregation: 'Average' 343 | skipMetricValidation: true 344 | } 345 | ] 346 | 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' 347 | } 348 | description: 'This alert monitors disk usage for all nodes and storage devices.' 349 | enabled: metricAlertsEnabled 350 | evaluationFrequency: evalFrequency 351 | scopes: [ 352 | AksResourceId 353 | ] 354 | severity: alertSeverityNumber 355 | targetResourceType: 'microsoft.containerservice/managedclusters' 356 | windowSize: windowSize 357 | } 358 | } 359 | 360 | resource nodesInNotReadyStateForAksCluster 'Microsoft.Insights/metricAlerts@2018-03-01' = { 361 | name: '${aksClusterName} | Nodes in not ready state' 362 | location: 'global' 363 | tags: tags 364 | properties: { 365 | criteria: { 366 | allOf: [ 367 | { 368 | criterionType: 'StaticThresholdCriterion' 369 | dimensions: [ 370 | { 371 | name: 'status' 372 | operator: 'Include' 373 | values: [ 374 | 'NotReady' 375 | ] 376 | } 377 | ] 378 | metricName: 'nodesCount' 379 | metricNamespace: 'Insights.Container/nodes' 380 | name: 'Metric1' 381 | operator: 'GreaterThan' 382 | threshold: 0 383 | timeAggregation: 'Average' 384 | skipMetricValidation: true 385 | } 386 | ] 387 | 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' 388 | } 389 | description: 'Node status monitoring.' 390 | enabled: metricAlertsEnabled 391 | evaluationFrequency: evalFrequency 392 | scopes: [ 393 | AksResourceId 394 | ] 395 | severity: alertSeverityNumber 396 | targetResourceType: 'microsoft.containerservice/managedclusters' 397 | windowSize: windowSize 398 | } 399 | } 400 | 401 | resource containersGettingOomKilledForAksCluster 'Microsoft.Insights/metricAlerts@2018-03-01' = { 402 | name: '${aksClusterName} | Containers getting OOM killed' 403 | location: 'global' 404 | tags: tags 405 | properties: { 406 | criteria: { 407 | allOf: [ 408 | { 409 | criterionType: 'StaticThresholdCriterion' 410 | dimensions: [ 411 | { 412 | name: 'kubernetes namespace' 413 | operator: 'Include' 414 | values: [ 415 | '*' 416 | ] 417 | } 418 | { 419 | name: 'controllerName' 420 | operator: 'Include' 421 | values: [ 422 | '*' 423 | ] 424 | } 425 | ] 426 | metricName: 'oomKilledContainerCount' 427 | metricNamespace: 'Insights.Container/pods' 428 | name: 'Metric1' 429 | operator: 'GreaterThan' 430 | threshold: 0 431 | timeAggregation: 'Average' 432 | skipMetricValidation: true 433 | } 434 | ] 435 | 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' 436 | } 437 | description: 'This alert monitors number of containers killed due to out of memory (OOM) error.' 438 | enabled: metricAlertsEnabled 439 | evaluationFrequency: evalFrequency 440 | scopes: [ 441 | AksResourceId 442 | ] 443 | severity: alertSeverityNumber 444 | targetResourceType: 'microsoft.containerservice/managedclusters' 445 | windowSize: windowSize 446 | } 447 | } 448 | 449 | resource persistentVolumeUsageHighForAksCluster 'Microsoft.Insights/metricAlerts@2018-03-01' = { 450 | name: '${aksClusterName} | Persistent volume usage high' 451 | location: 'global' 452 | tags: tags 453 | properties: { 454 | criteria: { 455 | allOf: [ 456 | { 457 | criterionType: 'StaticThresholdCriterion' 458 | dimensions: [ 459 | { 460 | name: 'podName' 461 | operator: 'Include' 462 | values: [ 463 | '*' 464 | ] 465 | } 466 | { 467 | name: 'kubernetesNamespace' 468 | operator: 'Include' 469 | values: [ 470 | '*' 471 | ] 472 | } 473 | ] 474 | metricName: 'pvUsageExceededPercentage' 475 | metricNamespace: 'Insights.Container/persistentvolumes' 476 | name: 'Metric1' 477 | operator: 'GreaterThan' 478 | threshold: 80 479 | timeAggregation: 'Average' 480 | skipMetricValidation: true 481 | } 482 | ] 483 | 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' 484 | } 485 | description: 'This alert monitors persistent volume utilization.' 486 | enabled: false 487 | evaluationFrequency: evalFrequency 488 | scopes: [ 489 | AksResourceId 490 | ] 491 | severity: alertSeverityNumber 492 | targetResourceType: 'microsoft.containerservice/managedclusters' 493 | windowSize: windowSize 494 | } 495 | } 496 | 497 | resource podsNotInReadyStateForAksCluster 'Microsoft.Insights/metricAlerts@2018-03-01' = { 498 | name: '${aksClusterName} | Pods not in ready state' 499 | location: 'global' 500 | tags: tags 501 | properties: { 502 | criteria: { 503 | allOf: [ 504 | { 505 | criterionType: 'StaticThresholdCriterion' 506 | dimensions: [ 507 | { 508 | name: 'controllerName' 509 | operator: 'Include' 510 | values: [ 511 | '*' 512 | ] 513 | } 514 | { 515 | name: 'kubernetes namespace' 516 | operator: 'Include' 517 | values: [ 518 | '*' 519 | ] 520 | } 521 | ] 522 | metricName: 'PodReadyPercentage' 523 | metricNamespace: 'Insights.Container/pods' 524 | name: 'Metric1' 525 | operator: 'LessThan' 526 | threshold: 80 527 | timeAggregation: 'Average' 528 | skipMetricValidation: true 529 | } 530 | ] 531 | 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' 532 | } 533 | description: 'This alert monitors for excessive pods not in the ready state.' 534 | enabled: metricAlertsEnabled 535 | evaluationFrequency: evalFrequency 536 | scopes: [ 537 | AksResourceId 538 | ] 539 | severity: alertSeverityNumber 540 | targetResourceType: 'microsoft.containerservice/managedclusters' 541 | windowSize: windowSize 542 | } 543 | } 544 | 545 | resource restartingContainerCountForAksCluster 'Microsoft.Insights/metricAlerts@2018-03-01' = { 546 | name: '${aksClusterName} | Restarting container count' 547 | location: 'global' 548 | tags: tags 549 | properties: { 550 | criteria: { 551 | allOf: [ 552 | { 553 | criterionType: 'StaticThresholdCriterion' 554 | dimensions: [ 555 | { 556 | name: 'kubernetes namespace' 557 | operator: 'Include' 558 | values: [ 559 | '*' 560 | ] 561 | } 562 | { 563 | name: 'controllerName' 564 | operator: 'Include' 565 | values: [ 566 | '*' 567 | ] 568 | } 569 | ] 570 | metricName: 'restartingContainerCount' 571 | metricNamespace: 'Insights.Container/pods' 572 | name: 'Metric1' 573 | operator: 'GreaterThan' 574 | threshold: 0 575 | timeAggregation: 'Average' 576 | skipMetricValidation: true 577 | } 578 | ] 579 | 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' 580 | } 581 | description: 'This alert monitors number of containers restarting across the cluster.' 582 | enabled: metricAlertsEnabled 583 | evaluationFrequency: evalFrequency 584 | scopes: [ 585 | AksResourceId 586 | ] 587 | severity: alertSeverityNumber 588 | targetResourceType: 'Microsoft.ContainerService/managedClusters' 589 | windowSize: windowSize 590 | } 591 | } 592 | 593 | resource containerCpuUsageViolatesTheConfiguredThresholdForAksCluster 'microsoft.insights/metricAlerts@2018-03-01' = { 594 | name: '${aksClusterName} | Container CPU usage violates the configured threshold' 595 | location: 'global' 596 | tags: tags 597 | properties: { 598 | description: 'This alert monitors container CPU usage. It uses the threshold defined in the config map.' 599 | severity: alertSeverityNumber 600 | enabled: true 601 | scopes: [ 602 | AksResourceId 603 | ] 604 | evaluationFrequency: evalFrequency 605 | windowSize: windowSize 606 | criteria: { 607 | allOf: [ 608 | { 609 | threshold: 0 610 | name: 'Metric1' 611 | metricNamespace: 'Insights.Container/containers' 612 | metricName: 'cpuThresholdViolated' 613 | dimensions: [ 614 | { 615 | name: 'controllerName' 616 | operator: 'Include' 617 | values: [ 618 | '*' 619 | ] 620 | } 621 | { 622 | name: 'kubernetes namespace' 623 | operator: 'Include' 624 | values: [ 625 | '*' 626 | ] 627 | } 628 | ] 629 | operator: 'GreaterThan' 630 | timeAggregation: 'Average' 631 | skipMetricValidation: true 632 | criterionType: 'StaticThresholdCriterion' 633 | } 634 | ] 635 | 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' 636 | } 637 | } 638 | } 639 | 640 | resource containerWorkingSetMemoryUsageViolatesTheConfiguredThresholdForAksCluster 'microsoft.insights/metricAlerts@2018-03-01' = { 641 | name: '${aksClusterName} | Container working set memory usage violates the configured threshold' 642 | location: 'global' 643 | tags: tags 644 | properties: { 645 | description: 'This alert monitors container working set memory usage. It uses the threshold defined in the config map.' 646 | severity: alertSeverityNumber 647 | enabled: metricAlertsEnabled 648 | scopes: [ 649 | AksResourceId 650 | ] 651 | evaluationFrequency: evalFrequency 652 | windowSize: windowSize 653 | criteria: { 654 | allOf: [ 655 | { 656 | threshold: 0 657 | name: 'Metric1' 658 | metricNamespace: 'Insights.Container/containers' 659 | metricName: 'memoryWorkingSetThresholdViolated' 660 | dimensions: [ 661 | { 662 | name: 'controllerName' 663 | operator: 'Include' 664 | values: [ 665 | '*' 666 | ] 667 | } 668 | { 669 | name: 'kubernetes namespace' 670 | operator: 'Include' 671 | values: [ 672 | '*' 673 | ] 674 | } 675 | ] 676 | operator: 'GreaterThan' 677 | timeAggregation: 'Average' 678 | skipMetricValidation: true 679 | criterionType: 'StaticThresholdCriterion' 680 | } 681 | ] 682 | 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' 683 | } 684 | } 685 | } 686 | 687 | 688 | resource pvUsageViolatesTheConfiguredThresholdForAksCluster 'Microsoft.Insights/metricAlerts@2018-03-01' = { 689 | name: '${aksClusterName} | Persistent Volume usage violates the configured threshold' 690 | location: 'global' 691 | tags: tags 692 | properties: { 693 | description: 'This alert monitors Persistent Volume usage. It uses the threshold defined in the config map.' 694 | severity: alertSeverityNumber 695 | enabled: metricAlertsEnabled 696 | scopes: [ 697 | AksResourceId 698 | ] 699 | evaluationFrequency: evalFrequency 700 | windowSize: windowSize 701 | criteria: { 702 | allOf: [ 703 | { 704 | threshold: 0 705 | name: 'Metric1' 706 | metricNamespace: 'Insights.Container/persistentvolumes' 707 | metricName: 'pvUsageThresholdViolated' 708 | dimensions: [ 709 | { 710 | name: 'podName' 711 | operator: 'Include' 712 | values: [ 713 | '*' 714 | ] 715 | } 716 | { 717 | name: 'kubernetesNamespace' 718 | operator: 'Include' 719 | values: [ 720 | '*' 721 | ] 722 | } 723 | ] 724 | operator: 'GreaterThan' 725 | timeAggregation: 'Average' 726 | skipMetricValidation: true 727 | criterionType: 'StaticThresholdCriterion' 728 | } 729 | ] 730 | 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' 731 | } 732 | } 733 | } 734 | -------------------------------------------------------------------------------- /bicep/network.bicep: -------------------------------------------------------------------------------- 1 | // Parameters 2 | @description('Specifies whether the podSubnet is enabled.') 3 | param podSubnetEnabled bool = true 4 | 5 | @description('Specifies whether to enable API server VNET integration for the cluster or not.') 6 | param enableVnetIntegration bool = true 7 | 8 | @description('Specifies the name of the virtual network.') 9 | param virtualNetworkName string 10 | 11 | @description('Specifies the address prefixes of the virtual network.') 12 | param virtualNetworkAddressPrefixes string = '10.0.0.0/8' 13 | 14 | @description('Specifies the name of the subnet hosting the worker nodes of the default system agent pool of the AKS cluster.') 15 | param systemAgentPoolSubnetName string = 'SystemSubnet' 16 | 17 | @description('Specifies the address prefix of the subnet hosting the worker nodes of the default system agent pool of the AKS cluster.') 18 | param systemAgentPoolSubnetAddressPrefix string = '10.0.0.0/16' 19 | 20 | @description('Specifies the name of the subnet hosting the worker nodes of the user agent pool of the AKS cluster.') 21 | param userAgentPoolSubnetName string = 'UserSubnet' 22 | 23 | @description('Specifies the address prefix of the subnet hosting the worker nodes of the user agent pool of the AKS cluster.') 24 | param userAgentPoolSubnetAddressPrefix string = '10.1.0.0/16' 25 | 26 | @description('Specifies the name of the subnet hosting the pods running in the AKS cluster.') 27 | param podSubnetName string = 'PodSubnet' 28 | 29 | @description('Specifies the address prefix of the subnet hosting the pods running in the AKS cluster.') 30 | param podSubnetAddressPrefix string = '10.2.0.0/16' 31 | 32 | @description('Specifies the name of the subnet delegated to the API server when configuring the AKS cluster to use API server VNET integration.') 33 | param apiServerSubnetName string = 'ApiServerSubnet' 34 | 35 | @description('Specifies the address prefix of the subnet delegated to the API server when configuring the AKS cluster to use API server VNET integration.') 36 | param apiServerSubnetAddressPrefix string = '10.3.0.0/28' 37 | 38 | @description('Specifies whether creating or not a jumpbox virtual machine in the AKS cluster virtual network.') 39 | param vmEnabled bool = true 40 | 41 | @description('Specifies the name of the subnet which contains the virtual machine.') 42 | param vmSubnetName string = 'VmSubnet' 43 | 44 | @description('Specifies the address prefix of the subnet which contains the virtual machine.') 45 | param vmSubnetAddressPrefix string = '10.3.1.0/24' 46 | 47 | @description('Specifies the name of the network security group associated to the subnet hosting the virtual machine.') 48 | param vmSubnetNsgName string = 'VmSubnetNsg' 49 | 50 | @description('Specifies the Bastion subnet IP prefix. This prefix must be within vnet IP prefix address space.') 51 | param bastionSubnetAddressPrefix string = '10.3.2.0/24' 52 | 53 | @description('Specifies whether creating the Application Gateway and enabling the Application Gateway Ingress Controller or not.') 54 | param applicationGatewayEnabled bool = false 55 | 56 | @description('Specifies the name of the subnet which contains the Application Gateway.') 57 | param applicationGatewaySubnetName string = 'AppGatewaySubnet' 58 | 59 | @description('Specifies the address prefix of the subnet which contains the Application Gateway.') 60 | param applicationGatewaySubnetAddressPrefix string = '10.3.3.0/24' 61 | 62 | @description('Specifies the name of the network security group associated to the subnet hosting Azure Bastion.') 63 | param bastionSubnetNsgName string = 'AzureBastionNsg' 64 | 65 | @description('Specifies whether Azure Bastion should be created.') 66 | param bastionHostEnabled bool = true 67 | 68 | @description('Specifies the name of the Azure Bastion resource.') 69 | param bastionHostName string 70 | 71 | @description('Enable/Disable Copy/Paste feature of the Bastion Host resource.') 72 | param bastionHostDisableCopyPaste bool = false 73 | 74 | @description('Enable/Disable File Copy feature of the Bastion Host resource.') 75 | param bastionHostEnableFileCopy bool = false 76 | 77 | @description('Enable/Disable IP Connect feature of the Bastion Host resource.') 78 | param bastionHostEnableIpConnect bool = false 79 | 80 | @description('Enable/Disable Shareable Link of the Bastion Host resource.') 81 | param bastionHostEnableShareableLink bool = false 82 | 83 | @description('Enable/Disable Tunneling feature of the Bastion Host resource.') 84 | param bastionHostEnableTunneling bool = false 85 | 86 | @description('Specifies the name of the Azure NAT Gateway.') 87 | param natGatewayName string 88 | 89 | @description('Specifies whether creating an Azure NAT Gateway for outbound connections.') 90 | param natGatewayEnabled bool = false 91 | 92 | @description('Specifies a list of availability zones denoting the zone in which Nat Gateway should be deployed.') 93 | param natGatewayZones array = [] 94 | 95 | @description('Specifies the number of Public IPs to create for the Azure NAT Gateway.') 96 | param natGatewayPublicIps int = 1 97 | 98 | @description('Specifies the idle timeout in minutes for the Azure NAT Gateway.') 99 | param natGatewayIdleTimeoutMins int = 30 100 | 101 | @description('Specifies the name of the private link to the boot diagnostics storage account.') 102 | param storageAccountPrivateEndpointName string = 'BlobStorageAccountPrivateEndpoint' 103 | 104 | @description('Specifies the resource id of the Azure Storage Account.') 105 | param storageAccountId string 106 | 107 | @description('Specifies the name of the private link to the Key Vault.') 108 | param keyVaultPrivateEndpointName string = 'KeyVaultPrivateEndpoint' 109 | 110 | @description('Specifies the resource id of the Azure Key vault.') 111 | param keyVaultId string 112 | 113 | @description('Specifies whether to create a private endpoint for the Azure Container Registry') 114 | param createAcrPrivateEndpoint bool = false 115 | 116 | @description('Specifies the name of the private link to the Azure Container Registry.') 117 | param acrPrivateEndpointName string = 'AcrPrivateEndpoint' 118 | 119 | @description('Specifies the resource id of the Azure Container Registry.') 120 | param acrId string 121 | 122 | @description('Specifies whether creating the Azure OpenAi resource or not.') 123 | param openAiEnabled bool = false 124 | 125 | @description('Specifies the name of the private link to the Azure OpenAI resource.') 126 | param openAiPrivateEndpointName string = 'OpenAiPrivateEndpoint' 127 | 128 | @description('Specifies the resource id of the Azure OpenAi.') 129 | param openAiId string 130 | 131 | @description('Specifies the resource id of the Log Analytics workspace.') 132 | param workspaceId string 133 | 134 | @description('Specifies the workspace data retention in days.') 135 | param retentionInDays int = 60 136 | 137 | @description('Specifies the location.') 138 | param location string = resourceGroup().location 139 | 140 | @description('Specifies the resource tags.') 141 | param tags object 142 | 143 | // Variables 144 | var diagnosticSettingsName = 'diagnosticSettings' 145 | var nsgLogCategories = [ 146 | 'NetworkSecurityGroupEvent' 147 | 'NetworkSecurityGroupRuleCounter' 148 | ] 149 | var nsgLogs = [for category in nsgLogCategories: { 150 | category: category 151 | enabled: true 152 | retentionPolicy: { 153 | enabled: true 154 | days: retentionInDays 155 | } 156 | }] 157 | var vnetLogCategories = [ 158 | 'VMProtectionAlerts' 159 | ] 160 | var vnetMetricCategories = [ 161 | 'AllMetrics' 162 | ] 163 | var vnetLogs = [for category in vnetLogCategories: { 164 | category: category 165 | enabled: true 166 | retentionPolicy: { 167 | enabled: true 168 | days: retentionInDays 169 | } 170 | }] 171 | var vnetMetrics = [for category in vnetMetricCategories: { 172 | category: category 173 | enabled: true 174 | retentionPolicy: { 175 | enabled: true 176 | days: retentionInDays 177 | } 178 | }] 179 | var bastionLogCategories = [ 180 | 'BastionAuditLogs' 181 | ] 182 | var bastionMetricCategories = [ 183 | 'AllMetrics' 184 | ] 185 | var bastionLogs = [for category in bastionLogCategories: { 186 | category: category 187 | enabled: true 188 | retentionPolicy: { 189 | enabled: true 190 | days: retentionInDays 191 | } 192 | }] 193 | var bastionMetrics = [for category in bastionMetricCategories: { 194 | category: category 195 | enabled: true 196 | retentionPolicy: { 197 | enabled: true 198 | days: retentionInDays 199 | } 200 | }] 201 | var bastionSubnetName = 'AzureBastionSubnet' 202 | var bastionPublicIpAddressName = '${bastionHostName}PublicIp' 203 | var systemAgentPoolSubnet = { 204 | name: systemAgentPoolSubnetName 205 | properties: { 206 | addressPrefix: systemAgentPoolSubnetAddressPrefix 207 | privateEndpointNetworkPolicies: 'Disabled' 208 | privateLinkServiceNetworkPolicies: 'Enabled' 209 | natGateway: natGatewayEnabled ? { 210 | id: natGateway.id 211 | } : null 212 | } 213 | } 214 | var userAgentPoolSubnet = { 215 | name: userAgentPoolSubnetName 216 | properties: { 217 | addressPrefix: userAgentPoolSubnetAddressPrefix 218 | privateEndpointNetworkPolicies: 'Disabled' 219 | privateLinkServiceNetworkPolicies: 'Enabled' 220 | natGateway: natGatewayEnabled ? { 221 | id: natGateway.id 222 | } : null 223 | } 224 | } 225 | var podSubnet = { 226 | name: podSubnetName 227 | properties: { 228 | addressPrefix: podSubnetAddressPrefix 229 | privateEndpointNetworkPolicies: 'Disabled' 230 | privateLinkServiceNetworkPolicies: 'Enabled' 231 | natGateway: natGatewayEnabled ? { 232 | id: natGateway.id 233 | } : null 234 | delegations: [ 235 | { 236 | name: 'aks-delegation' 237 | properties: { 238 | serviceName: 'Microsoft.ContainerService/managedClusters' 239 | } 240 | } 241 | ] 242 | } 243 | } 244 | var apiServerSubnet = { 245 | name: apiServerSubnetName 246 | properties: { 247 | addressPrefix: apiServerSubnetAddressPrefix 248 | privateEndpointNetworkPolicies: 'Disabled' 249 | privateLinkServiceNetworkPolicies: 'Enabled' 250 | delegations: [ 251 | { 252 | name: 'aks-delegation' 253 | properties: { 254 | serviceName: 'Microsoft.ContainerService/managedClusters' 255 | } 256 | } 257 | ] 258 | } 259 | } 260 | var vmSubnet = { 261 | name: vmSubnetName 262 | properties: { 263 | addressPrefix: vmSubnetAddressPrefix 264 | networkSecurityGroup: { 265 | id: vmSubnetNsg.id 266 | } 267 | privateEndpointNetworkPolicies: 'Enabled' 268 | privateLinkServiceNetworkPolicies: 'Disabled' 269 | natGateway: natGatewayEnabled ? { 270 | id: natGateway.id 271 | } : null 272 | } 273 | } 274 | var bastionSubnet = { 275 | name: bastionSubnetName 276 | properties: { 277 | addressPrefix: bastionSubnetAddressPrefix 278 | networkSecurityGroup: { 279 | id: bastionSubnetNsg.id 280 | } 281 | } 282 | } 283 | var applicationGatewaySubnet = { 284 | name: applicationGatewaySubnetName 285 | properties: { 286 | addressPrefix: applicationGatewaySubnetAddressPrefix 287 | privateEndpointNetworkPolicies: 'Enabled' 288 | privateLinkServiceNetworkPolicies: 'Disabled' 289 | } 290 | } 291 | var subnets = union( 292 | array(systemAgentPoolSubnet), 293 | array(userAgentPoolSubnet), 294 | podSubnetEnabled ? array(podSubnet) : [], 295 | enableVnetIntegration ? array(apiServerSubnet) : [], 296 | array(vmSubnet), 297 | bastionHostEnabled ? array(bastionSubnet) : [], 298 | applicationGatewayEnabled ? array(applicationGatewaySubnet) : [] 299 | ) 300 | 301 | // Resources 302 | 303 | // Network Security Groups 304 | resource bastionSubnetNsg 'Microsoft.Network/networkSecurityGroups@2021-08-01' = if (bastionHostEnabled) { 305 | name: bastionSubnetNsgName 306 | location: location 307 | tags: tags 308 | properties: { 309 | securityRules: [ 310 | { 311 | name: 'AllowHttpsInBound' 312 | properties: { 313 | protocol: 'Tcp' 314 | sourcePortRange: '*' 315 | sourceAddressPrefix: 'Internet' 316 | destinationPortRange: '443' 317 | destinationAddressPrefix: '*' 318 | access: 'Allow' 319 | priority: 100 320 | direction: 'Inbound' 321 | } 322 | } 323 | { 324 | name: 'AllowGatewayManagerInBound' 325 | properties: { 326 | protocol: 'Tcp' 327 | sourcePortRange: '*' 328 | sourceAddressPrefix: 'GatewayManager' 329 | destinationPortRange: '443' 330 | destinationAddressPrefix: '*' 331 | access: 'Allow' 332 | priority: 110 333 | direction: 'Inbound' 334 | } 335 | } 336 | { 337 | name: 'AllowLoadBalancerInBound' 338 | properties: { 339 | protocol: 'Tcp' 340 | sourcePortRange: '*' 341 | sourceAddressPrefix: 'AzureLoadBalancer' 342 | destinationPortRange: '443' 343 | destinationAddressPrefix: '*' 344 | access: 'Allow' 345 | priority: 120 346 | direction: 'Inbound' 347 | } 348 | } 349 | { 350 | name: 'AllowBastionHostCommunicationInBound' 351 | properties: { 352 | protocol: '*' 353 | sourcePortRange: '*' 354 | sourceAddressPrefix: 'VirtualNetwork' 355 | destinationPortRanges: [ 356 | '8080' 357 | '5701' 358 | ] 359 | destinationAddressPrefix: 'VirtualNetwork' 360 | access: 'Allow' 361 | priority: 130 362 | direction: 'Inbound' 363 | } 364 | } 365 | { 366 | name: 'DenyAllInBound' 367 | properties: { 368 | protocol: '*' 369 | sourcePortRange: '*' 370 | sourceAddressPrefix: '*' 371 | destinationPortRange: '*' 372 | destinationAddressPrefix: '*' 373 | access: 'Deny' 374 | priority: 1000 375 | direction: 'Inbound' 376 | } 377 | } 378 | { 379 | name: 'AllowSshRdpOutBound' 380 | properties: { 381 | protocol: 'Tcp' 382 | sourcePortRange: '*' 383 | sourceAddressPrefix: '*' 384 | destinationPortRanges: [ 385 | '22' 386 | '3389' 387 | ] 388 | destinationAddressPrefix: 'VirtualNetwork' 389 | access: 'Allow' 390 | priority: 100 391 | direction: 'Outbound' 392 | } 393 | } 394 | { 395 | name: 'AllowAzureCloudCommunicationOutBound' 396 | properties: { 397 | protocol: 'Tcp' 398 | sourcePortRange: '*' 399 | sourceAddressPrefix: '*' 400 | destinationPortRange: '443' 401 | destinationAddressPrefix: 'AzureCloud' 402 | access: 'Allow' 403 | priority: 110 404 | direction: 'Outbound' 405 | } 406 | } 407 | { 408 | name: 'AllowBastionHostCommunicationOutBound' 409 | properties: { 410 | protocol: '*' 411 | sourcePortRange: '*' 412 | sourceAddressPrefix: 'VirtualNetwork' 413 | destinationPortRanges: [ 414 | '8080' 415 | '5701' 416 | ] 417 | destinationAddressPrefix: 'VirtualNetwork' 418 | access: 'Allow' 419 | priority: 120 420 | direction: 'Outbound' 421 | } 422 | } 423 | { 424 | name: 'AllowGetSessionInformationOutBound' 425 | properties: { 426 | protocol: '*' 427 | sourcePortRange: '*' 428 | sourceAddressPrefix: '*' 429 | destinationAddressPrefix: 'Internet' 430 | destinationPortRanges: [ 431 | '80' 432 | '443' 433 | ] 434 | access: 'Allow' 435 | priority: 130 436 | direction: 'Outbound' 437 | } 438 | } 439 | { 440 | name: 'DenyAllOutBound' 441 | properties: { 442 | protocol: '*' 443 | sourcePortRange: '*' 444 | destinationPortRange: '*' 445 | sourceAddressPrefix: '*' 446 | destinationAddressPrefix: '*' 447 | access: 'Deny' 448 | priority: 1000 449 | direction: 'Outbound' 450 | } 451 | } 452 | ] 453 | } 454 | } 455 | 456 | resource vmSubnetNsg 'Microsoft.Network/networkSecurityGroups@2021-08-01' = { 457 | name: vmSubnetNsgName 458 | location: location 459 | tags: tags 460 | properties: { 461 | securityRules: [ 462 | { 463 | name: 'AllowSshInbound' 464 | properties: { 465 | priority: 100 466 | access: 'Allow' 467 | direction: 'Inbound' 468 | destinationPortRange: '22' 469 | protocol: 'Tcp' 470 | sourceAddressPrefix: '*' 471 | sourcePortRange: '*' 472 | destinationAddressPrefix: '*' 473 | } 474 | } 475 | ] 476 | } 477 | } 478 | 479 | // Virtual Network 480 | resource vnet 'Microsoft.Network/virtualNetworks@2021-08-01' = { 481 | name: virtualNetworkName 482 | location: location 483 | tags: tags 484 | properties: { 485 | addressSpace: { 486 | addressPrefixes: [ 487 | virtualNetworkAddressPrefixes 488 | ] 489 | } 490 | subnets: subnets 491 | } 492 | } 493 | 494 | // NAT Gateway 495 | resource natGatewayPublicIp 'Microsoft.Network/publicIPAddresses@2021-08-01' = [for i in range(0, natGatewayPublicIps): if (natGatewayEnabled) { 496 | name: natGatewayPublicIps == 1 ? '${natGatewayName}PublicIp' : '${natGatewayName}PublicIp${i + 1}' 497 | location: location 498 | sku: { 499 | name: 'Standard' 500 | } 501 | zones: !empty(natGatewayZones) ? natGatewayZones : [] 502 | properties: { 503 | publicIPAllocationMethod: 'Static' 504 | } 505 | }] 506 | 507 | resource natGateway 'Microsoft.Network/natGateways@2021-08-01' = if (natGatewayEnabled) { 508 | name: natGatewayName 509 | location: location 510 | sku: { 511 | name: 'Standard' 512 | } 513 | zones: !empty(natGatewayZones) ? natGatewayZones : [] 514 | properties: { 515 | publicIpAddresses: [for i in range(0, natGatewayPublicIps): { 516 | id: natGatewayPublicIp[i].id 517 | }] 518 | idleTimeoutInMinutes: natGatewayIdleTimeoutMins 519 | } 520 | dependsOn: [ 521 | natGatewayPublicIp 522 | ] 523 | } 524 | 525 | // Azure Bastion Host 526 | resource bastionPublicIpAddress 'Microsoft.Network/publicIPAddresses@2021-08-01' = if (bastionHostEnabled) { 527 | name: bastionPublicIpAddressName 528 | location: location 529 | tags: tags 530 | sku: { 531 | name: 'Standard' 532 | } 533 | properties: { 534 | publicIPAllocationMethod: 'Static' 535 | } 536 | } 537 | 538 | resource bastionHost 'Microsoft.Network/bastionHosts@2021-08-01' = if (bastionHostEnabled) { 539 | name: bastionHostName 540 | location: location 541 | tags: tags 542 | properties: { 543 | disableCopyPaste: bastionHostDisableCopyPaste 544 | enableFileCopy: bastionHostEnableFileCopy 545 | enableIpConnect: bastionHostEnableIpConnect 546 | enableShareableLink: bastionHostEnableShareableLink 547 | enableTunneling: bastionHostEnableTunneling 548 | ipConfigurations: [ 549 | { 550 | name: 'IpConf' 551 | properties: { 552 | subnet: { 553 | id: '${vnet.id}/subnets/${bastionSubnetName}' 554 | } 555 | publicIPAddress: { 556 | id: bastionPublicIpAddress.id 557 | } 558 | } 559 | } 560 | ] 561 | } 562 | } 563 | 564 | // Private DNS Zones 565 | resource acrPrivateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = { 566 | name: 'privatelink.${toLower(environment().name) == 'azureusgovernment' ? 'azurecr.us' : 'azurecr.io'}' 567 | location: 'global' 568 | tags: tags 569 | } 570 | 571 | resource blobPrivateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = if (vmEnabled) { 572 | name: 'privatelink.blob.${environment().suffixes.storage}' 573 | location: 'global' 574 | tags: tags 575 | } 576 | 577 | resource keyVaultPrivateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = { 578 | name: 'privatelink.${toLower(environment().name) == 'azureusgovernment' ? 'vaultcore.usgovcloudapi.net' : 'vaultcore.azure.net'}' 579 | location: 'global' 580 | tags: tags 581 | } 582 | 583 | resource openAiPrivateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = if (openAiEnabled) { 584 | name: 'privatelink.${toLower(environment().name) == 'azureusgovernment' ? 'openai.usgovcloudapi.net' : 'openai.azure.com'}' 585 | location: 'global' 586 | tags: tags 587 | } 588 | 589 | // Virtual Network Links 590 | resource acrPrivateDnsZoneVirtualNetworkLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = { 591 | parent: acrPrivateDnsZone 592 | name: 'link_to_${toLower(virtualNetworkName)}' 593 | location: 'global' 594 | properties: { 595 | registrationEnabled: false 596 | virtualNetwork: { 597 | id: vnet.id 598 | } 599 | } 600 | } 601 | 602 | resource blobPrivateDnsZoneVirtualNetworkLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = if (vmEnabled) { 603 | parent: blobPrivateDnsZone 604 | name: 'link_to_${toLower(virtualNetworkName)}' 605 | location: 'global' 606 | properties: { 607 | registrationEnabled: false 608 | virtualNetwork: { 609 | id: vnet.id 610 | } 611 | } 612 | } 613 | 614 | resource keyVaultPrivateDnsZoneVirtualNetworkLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = { 615 | parent: keyVaultPrivateDnsZone 616 | name: 'link_to_${toLower(virtualNetworkName)}' 617 | location: 'global' 618 | properties: { 619 | registrationEnabled: false 620 | virtualNetwork: { 621 | id: vnet.id 622 | } 623 | } 624 | } 625 | 626 | resource openAiPrivateDnsZoneVirtualNetworkLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = if (openAiEnabled) { 627 | parent: openAiPrivateDnsZone 628 | name: 'link_to_${toLower(virtualNetworkName)}' 629 | location: 'global' 630 | properties: { 631 | registrationEnabled: false 632 | virtualNetwork: { 633 | id: vnet.id 634 | } 635 | } 636 | } 637 | 638 | // Private Endpoints 639 | resource acrPrivateEndpoint 'Microsoft.Network/privateEndpoints@2022-09-01' = if (createAcrPrivateEndpoint) { 640 | name: acrPrivateEndpointName 641 | location: location 642 | tags: tags 643 | properties: { 644 | privateLinkServiceConnections: [ 645 | { 646 | name: acrPrivateEndpointName 647 | properties: { 648 | privateLinkServiceId: acrId 649 | groupIds: [ 650 | 'registry' 651 | ] 652 | } 653 | } 654 | ] 655 | subnet: { 656 | id: '${vnet.id}/subnets/${vmSubnetName}' 657 | } 658 | } 659 | } 660 | 661 | resource acrPrivateDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2022-09-01' = if (createAcrPrivateEndpoint) { 662 | parent: acrPrivateEndpoint 663 | name: 'acrPrivateDnsZoneGroup' 664 | properties: { 665 | privateDnsZoneConfigs: [ 666 | { 667 | name: 'dnsConfig' 668 | properties: { 669 | privateDnsZoneId: acrPrivateDnsZone.id 670 | } 671 | } 672 | ] 673 | } 674 | } 675 | 676 | resource blobStorageAccountPrivateEndpoint 'Microsoft.Network/privateEndpoints@2022-09-01' = if (vmEnabled) { 677 | name: storageAccountPrivateEndpointName 678 | location: location 679 | tags: tags 680 | properties: { 681 | privateLinkServiceConnections: [ 682 | { 683 | name: storageAccountPrivateEndpointName 684 | properties: { 685 | privateLinkServiceId: storageAccountId 686 | groupIds: [ 687 | 'blob' 688 | ] 689 | } 690 | } 691 | ] 692 | subnet: { 693 | id: '${vnet.id}/subnets/${vmSubnetName}' 694 | } 695 | } 696 | } 697 | 698 | resource blobStorageAccountPrivateDnsZoneGroupName 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2022-09-01' = if (vmEnabled) { 699 | parent: blobStorageAccountPrivateEndpoint 700 | name: 'PrivateDnsZoneGroupName' 701 | properties: { 702 | privateDnsZoneConfigs: [ 703 | { 704 | name: 'dnsConfig' 705 | properties: { 706 | privateDnsZoneId: blobPrivateDnsZone.id 707 | } 708 | } 709 | ] 710 | } 711 | } 712 | 713 | resource keyVaultPrivateEndpoint 'Microsoft.Network/privateEndpoints@2022-09-01' = { 714 | name: keyVaultPrivateEndpointName 715 | location: location 716 | tags: tags 717 | properties: { 718 | privateLinkServiceConnections: [ 719 | { 720 | name: keyVaultPrivateEndpointName 721 | properties: { 722 | privateLinkServiceId: keyVaultId 723 | groupIds: [ 724 | 'vault' 725 | ] 726 | } 727 | } 728 | ] 729 | subnet: { 730 | id: '${vnet.id}/subnets/${vmSubnetName}' 731 | } 732 | } 733 | } 734 | 735 | resource keyVaultPrivateDnsZoneGroupName 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2022-09-01' = { 736 | parent: keyVaultPrivateEndpoint 737 | name: 'PrivateDnsZoneGroupName' 738 | properties: { 739 | privateDnsZoneConfigs: [ 740 | { 741 | name: 'dnsConfig' 742 | properties: { 743 | privateDnsZoneId: keyVaultPrivateDnsZone.id 744 | } 745 | } 746 | ] 747 | } 748 | } 749 | 750 | resource openAiPrivateEndpoint 'Microsoft.Network/privateEndpoints@2022-09-01' = if (openAiEnabled) { 751 | name: openAiPrivateEndpointName 752 | location: location 753 | tags: tags 754 | properties: { 755 | privateLinkServiceConnections: [ 756 | { 757 | name: openAiPrivateEndpointName 758 | properties: { 759 | privateLinkServiceId: openAiId 760 | groupIds: [ 761 | 'account' 762 | ] 763 | } 764 | } 765 | ] 766 | subnet: { 767 | id: '${vnet.id}/subnets/${vmSubnetName}' 768 | } 769 | } 770 | } 771 | 772 | resource openAiPrivateDnsZoneGroupName 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2022-09-01' = if (openAiEnabled) { 773 | parent: openAiPrivateEndpoint 774 | name: 'PrivateDnsZoneGroupName' 775 | properties: { 776 | privateDnsZoneConfigs: [ 777 | { 778 | name: 'dnsConfig' 779 | properties: { 780 | privateDnsZoneId: openAiPrivateDnsZone.id 781 | } 782 | } 783 | ] 784 | } 785 | } 786 | 787 | // Diagnostic Settings 788 | resource vmSubnetNsgDiagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { 789 | name: diagnosticSettingsName 790 | scope: vmSubnetNsg 791 | properties: { 792 | workspaceId: workspaceId 793 | logs: nsgLogs 794 | } 795 | } 796 | 797 | resource bastionSubnetNsgDiagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (bastionHostEnabled) { 798 | name: diagnosticSettingsName 799 | scope: bastionSubnetNsg 800 | properties: { 801 | workspaceId: workspaceId 802 | logs: nsgLogs 803 | } 804 | } 805 | 806 | resource vnetDiagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { 807 | name: diagnosticSettingsName 808 | scope: vnet 809 | properties: { 810 | workspaceId: workspaceId 811 | logs: vnetLogs 812 | metrics: vnetMetrics 813 | } 814 | } 815 | 816 | resource bastionDiagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (bastionHostEnabled) { 817 | name: diagnosticSettingsName 818 | scope: bastionHost 819 | properties: { 820 | workspaceId: workspaceId 821 | logs: bastionLogs 822 | metrics: bastionMetrics 823 | } 824 | } 825 | 826 | // Outputs 827 | output virtualNetworkId string = vnet.id 828 | output virtualNetworkName string = vnet.name 829 | output aksSubnetId string = resourceId('Microsoft.Network/virtualNetworks/subnets', vnet.name, systemAgentPoolSubnetName) 830 | output vmSubnetId string = resourceId('Microsoft.Network/virtualNetworks/subnets', vnet.name, vmSubnetName) 831 | output bastionSubnetId string = resourceId('Microsoft.Network/virtualNetworks/subnets', vnet.name, bastionSubnetName) 832 | output applicationGatewaySubnetId string = resourceId('Microsoft.Network/virtualNetworks/subnets', vnet.name, applicationGatewaySubnetName) 833 | output systemAgentPoolSubnetName string = systemAgentPoolSubnetName 834 | output vmSubnetName string = vmSubnetName 835 | output bastionSubnetName string = bastionSubnetName 836 | output applicationGatewaySubnetName string = applicationGatewaySubnetName 837 | --------------------------------------------------------------------------------