├── 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 |
--------------------------------------------------------------------------------