From dd8b93be61615457283f42feb7f1c5194152c277 Mon Sep 17 00:00:00 2001 From: Paul Yu Date: Tue, 21 Nov 2023 04:20:52 +0000 Subject: [PATCH 1/2] feat: azd with terraform and helm resolves #58 --- .devcontainer/devcontainer.json | 4 +- .gitignore | 3 +- README.md | 22 ++++ azure.yaml | 18 +++ charts/aks-store-demo/.gitignore | 1 + charts/aks-store-demo/.helmignore | 23 ++++ charts/aks-store-demo/Chart.yaml | 24 ++++ .../aks-store-demo/templates/ai-service.yaml | 111 ++++++++++++++++++ .../templates/makeline-service.yaml | 85 ++++++++++++++ charts/aks-store-demo/templates/mongodb.yaml | 51 ++++++++ .../templates/order-service.yaml | 101 ++++++++++++++++ .../templates/product-service.yaml | 55 +++++++++ charts/aks-store-demo/templates/rabbitmq.yaml | 80 +++++++++++++ .../aks-store-demo/templates/store-admin.yaml | 69 +++++++++++ .../aks-store-demo/templates/store-front.yaml | 67 +++++++++++ .../templates/virtual-customer.yaml | 31 +++++ .../templates/virtual-worker.yaml | 31 +++++ charts/aks-store-demo/values.yaml | 58 +++++++++ infra/.terraform.lock.hcl | 65 ++++++++++ infra/azd-hooks/postprovision.sh | 29 +++++ infra/azd-hooks/preprovision.sh | 10 ++ infra/cosmosdb.tf | 61 ++++++++++ infra/kubernetes.tf | 27 +++++ infra/main.tf | 56 +++++++++ infra/main.tfvars.json | 4 + infra/openai.tf | 51 ++++++++ infra/outputs.tf | 68 +++++++++++ infra/servicebus.tf | 30 +++++ infra/variables.tf | 31 +++++ 29 files changed, 1264 insertions(+), 2 deletions(-) create mode 100644 azure.yaml create mode 100644 charts/aks-store-demo/.gitignore create mode 100644 charts/aks-store-demo/.helmignore create mode 100644 charts/aks-store-demo/Chart.yaml create mode 100644 charts/aks-store-demo/templates/ai-service.yaml create mode 100644 charts/aks-store-demo/templates/makeline-service.yaml create mode 100644 charts/aks-store-demo/templates/mongodb.yaml create mode 100644 charts/aks-store-demo/templates/order-service.yaml create mode 100644 charts/aks-store-demo/templates/product-service.yaml create mode 100644 charts/aks-store-demo/templates/rabbitmq.yaml create mode 100644 charts/aks-store-demo/templates/store-admin.yaml create mode 100644 charts/aks-store-demo/templates/store-front.yaml create mode 100644 charts/aks-store-demo/templates/virtual-customer.yaml create mode 100644 charts/aks-store-demo/templates/virtual-worker.yaml create mode 100644 charts/aks-store-demo/values.yaml create mode 100644 infra/.terraform.lock.hcl create mode 100755 infra/azd-hooks/postprovision.sh create mode 100755 infra/azd-hooks/preprovision.sh create mode 100644 infra/cosmosdb.tf create mode 100644 infra/kubernetes.tf create mode 100644 infra/main.tf create mode 100644 infra/main.tfvars.json create mode 100644 infra/openai.tf create mode 100644 infra/outputs.tf create mode 100644 infra/servicebus.tf create mode 100644 infra/variables.tf diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index a268e872..4979724c 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -7,6 +7,7 @@ "extensions": "aks-preview", "installBicep": true }, + "ghcr.io/azure/azure-dev/azd:latest": {}, "ghcr.io/devcontainers/features/common-utils:2": { "configureZshAsDefaultShell": true }, @@ -19,7 +20,8 @@ }, "ghcr.io/devcontainers/features/python:1": {}, "ghcr.io/devcontainers/features/rust:1": {}, - "ghcr.io/devcontainers/features/kubectl-helm-minikube:1": {} + "ghcr.io/devcontainers/features/kubectl-helm-minikube:1": {}, + "ghcr.io/devcontainers/features/terraform:1": {} }, "overrideFeatureInstallOrder": [ "ghcr.io/devcontainers/features/common-utils" diff --git a/.gitignore b/.gitignore index d2da3c19..8a748bd5 100644 --- a/.gitignore +++ b/.gitignore @@ -136,4 +136,5 @@ __pycache__/ .DS_Store **/.DS_Store .vscode -**/.vscode \ No newline at end of file +**/.vscode +.azure diff --git a/README.md b/README.md index 83cba2d5..47ca4006 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,28 @@ To stop the app, you can hit the `CTRL+C` key combination in the terminal window This repo also includes [DevContainer configuration](./.devcontainer/devcontainer.json), so you can open the repo using [GitHub Codespaces](https://docs.github.com/en/codespaces/overview). This will allow you to run the app in a container in the cloud, without having to install Docker on your local machine. When the Codespace is created, you can run the app using the same instructions as above. +## Run the app with Azure Service Bus and Azure Cosmos DB using Azure Developer CLI + +This repo also includes an alternate deployment type that uses Azure Service Bus and Azure Cosmos DB instead of RabbitMQ and MongoDB. To deploy this version of the app, you can use the [Azure Developer CLI](https://learn.microsoft.com/azure/developer/azure-developer-cli/overview) with a GitHub Codespace or DevContainer which has all the tools (e.g., `azure-cli`, `azd`, `terraform`, `kubectl`, and `helm`) pre-installed. This deployment will use Terraform to provision the Azure resources then retrieve output variables and pass them to Helm to deploy the app. + +To get started, authenticate to Azure using the Azure Developer CLI and Azure CLI. + +```bash +# authenticate to Azure Developer CLI +azd auth login + +# authenticate to Azure CLI +az login +``` + +Deploy the app with a single command. + +```bash +azd up +``` + +> Note: When selecting an Azure region, make sure to choose one that supports all the services used in this app including Azure OpenAI, Azure Kubernetes Service, Azure Service Bus, and Azure Cosmos DB. + ## Additional Resources - AKS Documentation. https://learn.microsoft.com/azure/aks diff --git a/azure.yaml b/azure.yaml new file mode 100644 index 00000000..dcb4c483 --- /dev/null +++ b/azure.yaml @@ -0,0 +1,18 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + +name: aks-store-demo +metadata: + template: aks-store-dmeo@1.0.0 +hooks: + preprovision: + shell: sh + continueOnError: false + interactive: false + run: infra/azd-hooks/preprovision.sh + postprovision: + shell: sh + continueOnError: false + interactive: false + run: infra/azd-hooks/postprovision.sh +infra: + provider: terraform \ No newline at end of file diff --git a/charts/aks-store-demo/.gitignore b/charts/aks-store-demo/.gitignore new file mode 100644 index 00000000..b691d38f --- /dev/null +++ b/charts/aks-store-demo/.gitignore @@ -0,0 +1 @@ +aks-store-demo-chart-*.tgz \ No newline at end of file diff --git a/charts/aks-store-demo/.helmignore b/charts/aks-store-demo/.helmignore new file mode 100644 index 00000000..0e8a0eb3 --- /dev/null +++ b/charts/aks-store-demo/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/charts/aks-store-demo/Chart.yaml b/charts/aks-store-demo/Chart.yaml new file mode 100644 index 00000000..94b0410b --- /dev/null +++ b/charts/aks-store-demo/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: aks-store-demo-chart +description: A Helm chart for deploying the aks-store-demo app to Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 1.0.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/charts/aks-store-demo/templates/ai-service.yaml b/charts/aks-store-demo/templates/ai-service.yaml new file mode 100644 index 00000000..4ccb06ed --- /dev/null +++ b/charts/aks-store-demo/templates/ai-service.yaml @@ -0,0 +1,111 @@ +{{- if .Values.aiService.create }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: ai-service-configmap +data: + AZURE_OPENAI_DEPLOYMENT_NAME: "{{ .Values.aiService.modelDeploymentName }}" + AZURE_OPENAI_ENDPOINT: "{{ .Values.aiService.openAiEndpoint }}" + {{- if .Values.aiService.useAzureOpenAi }} + USE_AZURE_OPENAI: "True" + {{- else }} + USE_AZURE_OPENAI: "False" + OPENAI_ORG_ID: "{{ .Values.aiService.openAiOrgId }}" + {{- end }} + {{- if .Values.aiService.useAzureAd }} + USE_AZURE_AD: "True" + {{- else }} + USE_AZURE_AD: "False" + {{- end }} +--- +{{- if eq .Values.aiService.useAzureAd false }} +apiVersion: v1 +kind: Secret +metadata: + name: ai-service-secrets +data: + OPENAI_API_KEY: "{{ .Values.aiService.openAiKey | b64enc }}" +--- +{{- end }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: ai-service-account + annotations: + azure.workload.identity/client-id: "{{ .Values.aiService.managedIdentityClientId }}" +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: ai-service +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + app: ai-service + template: + metadata: + labels: + app: ai-service + azure.workload.identity/use: "true" + spec: + serviceAccount: ai-service-account + nodeSelector: + "kubernetes.io/os": linux + containers: + - name: order-service + image: ghcr.io/azure-samples/aks-store-demo/ai-service:1.0.0 + ports: + - containerPort: 5001 + envFrom: + - configMapRef: + name: ai-service-configmap + {{- if eq .Values.aiService.useAzureAd false }} + - secretRef: + name: ai-service-secrets + {{- end }} + resources: + requests: + cpu: 20m + memory: 50Mi + limits: + cpu: 30m + memory: 65Mi + startupProbe: + httpGet: + path: /health + port: 5001 + initialDelaySeconds: 60 + failureThreshold: 3 + timeoutSeconds: 3 + periodSeconds: 5 + readinessProbe: + httpGet: + path: /health + port: 5001 + initialDelaySeconds: 3 + failureThreshold: 3 + timeoutSeconds: 3 + periodSeconds: 5 + livenessProbe: + httpGet: + path: /health + port: 5001 + failureThreshold: 3 + initialDelaySeconds: 3 + timeoutSeconds: 3 + periodSeconds: 3 +--- +apiVersion: v1 +kind: Service +metadata: + name: ai-service +spec: + type: ClusterIP + ports: + - name: http + port: 5001 + targetPort: 5001 + selector: + app: ai-service +{{- end }} \ No newline at end of file diff --git a/charts/aks-store-demo/templates/makeline-service.yaml b/charts/aks-store-demo/templates/makeline-service.yaml new file mode 100644 index 00000000..857df1cb --- /dev/null +++ b/charts/aks-store-demo/templates/makeline-service.yaml @@ -0,0 +1,85 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: makeline-service-configmap +data: + ORDER_QUEUE_URI: "{{ .Values.makelineService.orderQueueUri }}" + ORDER_QUEUE_USERNAME: "{{ .Values.makelineService.orderQueueUsername }}" + ORDER_QUEUE_PASSWORD: "{{ .Values.makelineService.orderQueuePassword }}" + ORDER_QUEUE_NAME: "{{ .Values.makelineService.orderQueueName }}" + ORDER_DB_URI: "{{ .Values.makelineService.orderDBUri }}" + ORDER_DB_NAME: "{{ .Values.makelineService.orderDBName }}" + ORDER_DB_COLLECTION_NAME: "{{ .Values.makelineService.orderDBCollectionName }}" +--- +{{- if and .Values.makelineService.orderDBUsername .Values.makelineService.orderDBPassword }} +apiVersion: v1 +kind: Secret +metadata: + name: makeline-service-secrets +data: + ORDER_DB_USERNAME: "{{ .Values.makelineService.orderDBUsername | b64enc }}" + ORDER_DB_PASSWORD: "{{ .Values.makelineService.orderDBPassword | b64enc }}" +--- +{{- end }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: makeline-service +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + app: makeline-service + template: + metadata: + labels: + app: makeline-service + spec: + nodeSelector: + "kubernetes.io/os": linux + containers: + - name: makeline-service + image: ghcr.io/azure-samples/aks-store-demo/makeline-service:1.0.0 + ports: + - containerPort: 3001 + envFrom: + - configMapRef: + name: makeline-service-configmap + {{- if and .Values.makelineService.orderDBUsername .Values.makelineService.orderDBPassword }} + - secretRef: + name: makeline-service-secrets + {{- end }} + resources: + requests: + cpu: 1m + memory: 6Mi + limits: + cpu: 5m + memory: 20Mi + readinessProbe: + httpGet: + path: /health + port: 3001 + failureThreshold: 3 + initialDelaySeconds: 3 + periodSeconds: 5 + livenessProbe: + httpGet: + path: /health + port: 3001 + failureThreshold: 5 + initialDelaySeconds: 3 + periodSeconds: 3 +--- +apiVersion: v1 +kind: Service +metadata: + name: makeline-service +spec: + type: ClusterIP + ports: + - name: http + port: 3001 + targetPort: 3001 + selector: + app: makeline-service \ No newline at end of file diff --git a/charts/aks-store-demo/templates/mongodb.yaml b/charts/aks-store-demo/templates/mongodb.yaml new file mode 100644 index 00000000..ac526dd7 --- /dev/null +++ b/charts/aks-store-demo/templates/mongodb.yaml @@ -0,0 +1,51 @@ +{{- if eq .Values.makelineService.useAzureCosmosDB false }} +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: mongodb +spec: + serviceName: mongodb + replicas: 1 + selector: + matchLabels: + app: mongodb + template: + metadata: + labels: + app: mongodb + spec: + nodeSelector: + "kubernetes.io/os": linux + containers: + - name: mongodb + image: mcr.microsoft.com/mirror/docker/library/mongo:4.2 + ports: + - containerPort: 27017 + name: mongodb + resources: + requests: + cpu: 5m + memory: 75Mi + limits: + cpu: 25m + memory: 1024Mi + livenessProbe: + exec: + command: + - mongosh + - --eval + - db.runCommand('ping').ok + initialDelaySeconds: 5 + periodSeconds: 5 +--- +apiVersion: v1 +kind: Service +metadata: + name: mongodb +spec: + ports: + - port: 27017 + selector: + app: mongodb + type: ClusterIP +{{- end }} \ No newline at end of file diff --git a/charts/aks-store-demo/templates/order-service.yaml b/charts/aks-store-demo/templates/order-service.yaml new file mode 100644 index 00000000..b2b25d67 --- /dev/null +++ b/charts/aks-store-demo/templates/order-service.yaml @@ -0,0 +1,101 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: order-service-configmap +data: + ORDER_QUEUE_HOSTNAME: "{{ .Values.orderService.queueHost }}" + ORDER_QUEUE_PORT: "{{ .Values.orderService.queuePort }}" + ORDER_QUEUE_NAME: "{{ .Values.orderService.queueName }}" + FASTIFY_ADDRESS: "0.0.0.0" + {{- if .Values.orderService.queueTransport }} + ORDER_QUEUE_TRANSPORT: "{{ .Values.orderService.queueTransport }}" + {{- end }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: order-service-secret +data: + ORDER_QUEUE_USERNAME: "{{ .Values.orderService.queueUsername | b64enc }}" + ORDER_QUEUE_PASSWORD: "{{ .Values.orderService.queuePassword | b64enc }}" +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: order-service +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + app: order-service + template: + metadata: + labels: + app: order-service + spec: + nodeSelector: + "kubernetes.io/os": linux + containers: + - name: order-service + image: ghcr.io/azure-samples/aks-store-demo/order-service:1.0.0 + ports: + - containerPort: 3000 + envFrom: + - configMapRef: + name: order-service-configmap + - secretRef: + name: order-service-secret + resources: + requests: + cpu: 1m + memory: 50Mi + limits: + cpu: 75m + memory: 128Mi + startupProbe: + httpGet: + path: /health + port: 3000 + failureThreshold: 3 + initialDelaySeconds: 15 + periodSeconds: 5 + readinessProbe: + httpGet: + path: /health + port: 3000 + failureThreshold: 3 + initialDelaySeconds: 3 + periodSeconds: 5 + livenessProbe: + httpGet: + path: /health + port: 3000 + failureThreshold: 5 + initialDelaySeconds: 3 + periodSeconds: 3 + {{- if eq .Values.orderService.useAzureServiceBus false }} + initContainers: + - name: wait-for-rabbitmq + image: busybox + command: ['sh', '-c', 'until nc -zv rabbitmq 5672; do echo waiting for rabbitmq; sleep 2; done;'] + resources: + requests: + cpu: 1m + memory: 50Mi + limits: + cpu: 75m + memory: 128Mi + {{- end }} +--- +apiVersion: v1 +kind: Service +metadata: + name: order-service +spec: + type: ClusterIP + ports: + - name: http + port: 3000 + targetPort: 3000 + selector: + app: order-service \ No newline at end of file diff --git a/charts/aks-store-demo/templates/product-service.yaml b/charts/aks-store-demo/templates/product-service.yaml new file mode 100644 index 00000000..1653cc62 --- /dev/null +++ b/charts/aks-store-demo/templates/product-service.yaml @@ -0,0 +1,55 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: product-service +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + app: product-service + template: + metadata: + labels: + app: product-service + spec: + nodeSelector: + "kubernetes.io/os": linux + containers: + - name: product-service + image: ghcr.io/azure-samples/aks-store-demo/product-service:1.0.0 + ports: + - containerPort: 3002 + resources: + requests: + cpu: 1m + memory: 1Mi + limits: + cpu: 1m + memory: 6Mi + readinessProbe: + httpGet: + path: /health + port: 3002 + failureThreshold: 3 + initialDelaySeconds: 3 + periodSeconds: 5 + livenessProbe: + httpGet: + path: /health + port: 3002 + failureThreshold: 5 + initialDelaySeconds: 3 + periodSeconds: 3 +--- +apiVersion: v1 +kind: Service +metadata: + name: product-service +spec: + type: ClusterIP + ports: + - name: http + port: 3002 + targetPort: 3002 + selector: + app: product-service \ No newline at end of file diff --git a/charts/aks-store-demo/templates/rabbitmq.yaml b/charts/aks-store-demo/templates/rabbitmq.yaml new file mode 100644 index 00000000..ab5537cd --- /dev/null +++ b/charts/aks-store-demo/templates/rabbitmq.yaml @@ -0,0 +1,80 @@ +{{- if eq .Values.orderService.useAzureServiceBus false }} +apiVersion: v1 +data: + rabbitmq_enabled_plugins: | + [rabbitmq_management,rabbitmq_prometheus,rabbitmq_amqp1_0]. +kind: ConfigMap +metadata: + name: rabbitmq-enabled-plugins +--- +apiVersion: v1 +kind: Secret +metadata: + name: rabbitmq-secret +data: + RABBITMQ_DEFAULT_USER: "{{ .Values.orderService.queueUsername | b64enc }}" + RABBITMQ_DEFAULT_PASS: "{{ .Values.orderService.queuePassword | b64enc }}" +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: rabbitmq +spec: + serviceName: rabbitmq + replicas: 1 + selector: + matchLabels: + app: rabbitmq + template: + metadata: + labels: + app: rabbitmq + spec: + nodeSelector: + "kubernetes.io/os": linux + containers: + - name: rabbitmq + image: mcr.microsoft.com/mirror/docker/library/rabbitmq:3.10-management-alpine + ports: + - containerPort: 5672 + name: rabbitmq-amqp + - containerPort: 15672 + name: rabbitmq-http + envFrom: + - secretRef: + name: rabbitmq-secret + resources: + requests: + cpu: 10m + memory: 128Mi + limits: + cpu: 250m + memory: 256Mi + volumeMounts: + - name: rabbitmq-enabled-plugins + mountPath: /etc/rabbitmq/enabled_plugins + subPath: enabled_plugins + volumes: + - name: rabbitmq-enabled-plugins + configMap: + name: rabbitmq-enabled-plugins + items: + - key: rabbitmq_enabled_plugins + path: enabled_plugins +--- +apiVersion: v1 +kind: Service +metadata: + name: rabbitmq +spec: + selector: + app: rabbitmq + ports: + - name: rabbitmq-amqp + port: 5672 + targetPort: 5672 + - name: rabbitmq-http + port: 15672 + targetPort: 15672 + type: ClusterIP +{{- end }} \ No newline at end of file diff --git a/charts/aks-store-demo/templates/store-admin.yaml b/charts/aks-store-demo/templates/store-admin.yaml new file mode 100644 index 00000000..06fc07e0 --- /dev/null +++ b/charts/aks-store-demo/templates/store-admin.yaml @@ -0,0 +1,69 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: store-admin +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + app: store-admin + template: + metadata: + labels: + app: store-admin + spec: + nodeSelector: + "kubernetes.io/os": linux + containers: + - name: store-admin + image: ghcr.io/azure-samples/aks-store-demo/store-admin:1.0.0 + ports: + - containerPort: 8081 + name: store-admin + env: + - name: VUE_APP_PRODUCT_SERVICE_URL + value: "http://product-service:3002/" + - name: VUE_APP_MAKELINE_SERVICE_URL + value: "http://makeline-service:3001/" + - name: VUE_APP_AI_SERVICE_URL + value: "http://ai-service:5001/" + resources: + requests: + cpu: 1m + memory: 200Mi + limits: + cpu: 1000m + memory: 512Mi + startupProbe: + httpGet: + path: /health + port: 8081 + failureThreshold: 3 + initialDelaySeconds: 15 + periodSeconds: 5 + readinessProbe: + httpGet: + path: /health + port: 8081 + failureThreshold: 3 + initialDelaySeconds: 3 + periodSeconds: 5 + livenessProbe: + httpGet: + path: /health + port: 8081 + failureThreshold: 5 + initialDelaySeconds: 3 + periodSeconds: 3 +--- +apiVersion: v1 +kind: Service +metadata: + name: store-admin +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 8081 + selector: + app: store-admin \ No newline at end of file diff --git a/charts/aks-store-demo/templates/store-front.yaml b/charts/aks-store-demo/templates/store-front.yaml new file mode 100644 index 00000000..9393f427 --- /dev/null +++ b/charts/aks-store-demo/templates/store-front.yaml @@ -0,0 +1,67 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: store-front +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + app: store-front + template: + metadata: + labels: + app: store-front + spec: + nodeSelector: + "kubernetes.io/os": linux + containers: + - name: store-front + image: ghcr.io/azure-samples/aks-store-demo/store-front:1.0.0 + ports: + - containerPort: 8080 + name: store-front + env: + - name: VUE_APP_ORDER_SERVICE_URL + value: "http://order-service:3000/" + - name: VUE_APP_PRODUCT_SERVICE_URL + value: "http://product-service:3002/" + resources: + requests: + cpu: 1m + memory: 200Mi + limits: + cpu: 1000m + memory: 512Mi + startupProbe: + httpGet: + path: /health + port: 8080 + failureThreshold: 3 + initialDelaySeconds: 15 + periodSeconds: 5 + readinessProbe: + httpGet: + path: /health + port: 8080 + failureThreshold: 3 + initialDelaySeconds: 3 + periodSeconds: 3 + livenessProbe: + httpGet: + path: /health + port: 8080 + failureThreshold: 5 + initialDelaySeconds: 3 + periodSeconds: 3 +--- +apiVersion: v1 +kind: Service +metadata: + name: store-front +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 8080 + selector: + app: store-front \ No newline at end of file diff --git a/charts/aks-store-demo/templates/virtual-customer.yaml b/charts/aks-store-demo/templates/virtual-customer.yaml new file mode 100644 index 00000000..80388502 --- /dev/null +++ b/charts/aks-store-demo/templates/virtual-customer.yaml @@ -0,0 +1,31 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: virtual-customer +spec: + replicas: 1 + selector: + matchLabels: + app: virtual-customer + template: + metadata: + labels: + app: virtual-customer + spec: + nodeSelector: + "kubernetes.io/os": linux + containers: + - name: virtual-customer + image: ghcr.io/azure-samples/aks-store-demo/virtual-customer:1.0.0 + env: + - name: ORDER_SERVICE_URL + value: http://order-service:3000/ + - name: ORDERS_PER_HOUR + value: "100" + resources: + requests: + cpu: 1m + memory: 1Mi + limits: + cpu: 1m + memory: 10Mi \ No newline at end of file diff --git a/charts/aks-store-demo/templates/virtual-worker.yaml b/charts/aks-store-demo/templates/virtual-worker.yaml new file mode 100644 index 00000000..f5d929e1 --- /dev/null +++ b/charts/aks-store-demo/templates/virtual-worker.yaml @@ -0,0 +1,31 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: virtual-worker +spec: + replicas: 1 + selector: + matchLabels: + app: virtual-worker + template: + metadata: + labels: + app: virtual-worker + spec: + nodeSelector: + "kubernetes.io/os": linux + containers: + - name: virtual-worker + image: ghcr.io/azure-samples/aks-store-demo/virtual-worker:1.0.0 + env: + - name: MAKELINE_SERVICE_URL + value: http://makeline-service:3001 + - name: ORDERS_PER_HOUR + value: "100" + resources: + requests: + cpu: 1m + memory: 1Mi + limits: + cpu: 1m + memory: 10Mi \ No newline at end of file diff --git a/charts/aks-store-demo/values.yaml b/charts/aks-store-demo/values.yaml new file mode 100644 index 00000000..e5e7334b --- /dev/null +++ b/charts/aks-store-demo/values.yaml @@ -0,0 +1,58 @@ +# Default values for aks-store-demo. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +namespace: "dev" + +aiService: + # Specifies whether a ai-service deployment and service should be created + create: false + modelDeploymentName: "" + openAiEndpoint: "" + openAiKey: "" + openAiOrgId: "" + managedIdentityClientId: "" + useAzureOpenAi: true + useAzureAd: true + +orderService: + useAzureServiceBus: false # when false, local rabbitmq will be used + queueHost: "rabbitmq" + queuePort: "5672" + queueUsername: "username" + queuePassword: "password" + queueName: "orders" + queueTransport: "" + +makelineService: + useAzureCosmosDB: false # when false, local mongodb will be used + orderQueueUri: "amqp://rabbitmq:5672" + orderQueueUsername: "username" + orderQueuePassword: "password" + orderQueueName: "orders" + orderDBUri: "mongodb://mongodb:27017" + orderDBName: "orderdb" + orderDBCollectionName: "orders" + orderDBUsername: "" + orderDBPassword: "" + +podAnnotations: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +nodeSelector: {} + +tolerations: [] + +affinity: {} \ No newline at end of file diff --git a/infra/.terraform.lock.hcl b/infra/.terraform.lock.hcl new file mode 100644 index 00000000..9e24c550 --- /dev/null +++ b/infra/.terraform.lock.hcl @@ -0,0 +1,65 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/azurerm" { + version = "3.80.0" + constraints = "3.80.0" + hashes = [ + "h1:B9BjHL/otNG7gG2jEPPCz554qbke3fXEH1kzlsg7pnU=", + "h1:INZGxLapgXtWJBI7MP6ltmfViGN4SeqHxw+XcE4vN28=", + "zh:11e3362a3d9ca9d6d57a3c863b26ddcb9e4366c6c99586a4149d2842976e998c", + "zh:3005446ee10379e5354b2388d788cad92068ff0988f50e49f6e8eeacdbd6e0a1", + "zh:45aa9f9d930e332fc68f2093232edf15ef0d087c9588317371838784382b9dbc", + "zh:4e1d57b6b909577b37e85117533eb0d6a2128e7ccfcf88e709ad890f86e67295", + "zh:5f777dbb7d72ab83a3a9b9412f8e341573fb34591c834fefa8fc7593e73232a2", + "zh:6fab9ea69a2177e29a0c0819f32c51245891fd19b807c4a3e2d4dd5c0a28b8fb", + "zh:77b1bdf671a83e17c82b30520312970109811c7725b61fcef69138244d7dd212", + "zh:8a1e8d9243f1149b7bcf7b8bda3cf5a1bcea6c709b55053fe7e8cd767dd5f632", + "zh:a7c1de65d5f1f241c4d3f30f24912cb9dc8315a1dc7d7bd0814cb4beb1e8f1fb", + "zh:b4385e7c84d708cd81647555dd12ade9fb128fd027eb59cd651f4cdcf98a437e", + "zh:d4ef3f5b126d5c9766300ffb6f1b3ed091484d46c742cfae2645261e749162e5", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} + +provider "registry.terraform.io/hashicorp/local" { + version = "2.4.0" + constraints = "2.4.0" + hashes = [ + "h1:R97FTYETo88sT2VHfMgkPU3lzCsZLunPftjSI5vfKe8=", + "h1:ZUEYUmm2t4vxwzxy1BvN1wL6SDWrDxfH7pxtzX8c6d0=", + "zh:53604cd29cb92538668fe09565c739358dc53ca56f9f11312b9d7de81e48fab9", + "zh:66a46e9c508716a1c98efbf793092f03d50049fa4a83cd6b2251e9a06aca2acf", + "zh:70a6f6a852dd83768d0778ce9817d81d4b3f073fab8fa570bff92dcb0824f732", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:82a803f2f484c8b766e2e9c32343e9c89b91997b9f8d2697f9f3837f62926b35", + "zh:9708a4e40d6cc4b8afd1352e5186e6e1502f6ae599867c120967aebe9d90ed04", + "zh:973f65ce0d67c585f4ec250c1e634c9b22d9c4288b484ee2a871d7fa1e317406", + "zh:c8fa0f98f9316e4cfef082aa9b785ba16e36ff754d6aba8b456dab9500e671c6", + "zh:cfa5342a5f5188b20db246c73ac823918c189468e1382cb3c48a9c0c08fc5bf7", + "zh:e0e2b477c7e899c63b06b38cd8684a893d834d6d0b5e9b033cedc06dd7ffe9e2", + "zh:f62d7d05ea1ee566f732505200ab38d94315a4add27947a60afa29860822d3fc", + "zh:fa7ce69dde358e172bd719014ad637634bbdabc49363104f4fca759b4b73f2ce", + ] +} + +provider "registry.terraform.io/hashicorp/random" { + version = "3.5.1" + constraints = "3.5.1" + hashes = [ + "h1:IL9mSatmwov+e0+++YX2V6uel+dV6bn+fC/cnGDK3Ck=", + "h1:VSnd9ZIPyfKHOObuQCaKfnjIHRtR7qTw19Rz8tJxm+k=", + "zh:04e3fbd610cb52c1017d282531364b9c53ef72b6bc533acb2a90671957324a64", + "zh:119197103301ebaf7efb91df8f0b6e0dd31e6ff943d231af35ee1831c599188d", + "zh:4d2b219d09abf3b1bb4df93d399ed156cadd61f44ad3baf5cf2954df2fba0831", + "zh:6130bdde527587bbe2dcaa7150363e96dbc5250ea20154176d82bc69df5d4ce3", + "zh:6cc326cd4000f724d3086ee05587e7710f032f94fc9af35e96a386a1c6f2214f", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:b6d88e1d28cf2dfa24e9fdcc3efc77adcdc1c3c3b5c7ce503a423efbdd6de57b", + "zh:ba74c592622ecbcef9dc2a4d81ed321c4e44cddf7da799faa324da9bf52a22b2", + "zh:c7c5cde98fe4ef1143bd1b3ec5dc04baf0d4cc3ca2c5c7d40d17c0e9b2076865", + "zh:dac4bad52c940cd0dfc27893507c1e92393846b024c5a9db159a93c534a3da03", + "zh:de8febe2a2acd9ac454b844a4106ed295ae9520ef54dc8ed2faf29f12716b602", + "zh:eab0d0495e7e711cca367f7d4df6e322e6c562fc52151ec931176115b83ed014", + ] +} diff --git a/infra/azd-hooks/postprovision.sh b/infra/azd-hooks/postprovision.sh new file mode 100755 index 00000000..f1fd3ca5 --- /dev/null +++ b/infra/azd-hooks/postprovision.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +echo "Retrieving cluster credentials" +az aks get-credentials --resource-group ${rg_name} --name ${aks_name} + +# echo "Deploy the manifests" +# kubectl apply -f manifests/ + +echo "Deploy Helm chart" +helm upgrade aks-store-demo ./charts/aks-store-demo \ + --install \ + --set aiService.create=true \ + --set aiService.modelDeploymentName=${ai_model_name} \ + --set aiService.openAiEndpoint=${ai_endpoint} \ + --set aiService.managedIdentityClientId=${ai_managed_identity_client_id} \ + --set orderService.useAzureServiceBus=true \ + --set orderService.queueHost=${sb_namespace_host} \ + --set orderService.queuePort=5671 \ + --set orderService.queueUsername=${sb_sender_username} \ + --set orderService.queuePassword=${sb_sender_key} \ + --set orderService.queueTransport=tls \ + --set makelineService.useAzureCosmosDB=true \ + --set makelineService.orderQueueUri=${sb_namespace_uri} \ + --set makelineService.orderQueueUsername=${sb_listener_username} \ + --set makelineService.orderQueuePassword=${sb_listener_key} \ + --set makelineService.orderDBUri=${db_uri} \ + --set makelineService.orderDBUsername=${db_account_name} \ + --set makelineService.orderDBPassword=${db_key} \ + \ No newline at end of file diff --git a/infra/azd-hooks/preprovision.sh b/infra/azd-hooks/preprovision.sh new file mode 100755 index 00000000..61b9fb43 --- /dev/null +++ b/infra/azd-hooks/preprovision.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +echo "Ensuring Azure CLI extensions and dependencies are installed" +az provider register --namespace Microsoft.ContainerService +az feature register --namespace Microsoft.ContainerService --name AKS-KedaPreview +az feature register --namespace Microsoft.ContainerService --name AKS-PrometheusAddonPreview +az feature register --namespace Microsoft.ContainerService --name EnableWorkloadIdentityPreview +az feature register --namespace Microsoft.ContainerService --name NetworkObservabilityPreview +az extension add --upgrade --name aks-preview +az extension add --upgrade --name amg \ No newline at end of file diff --git a/infra/cosmosdb.tf b/infra/cosmosdb.tf new file mode 100644 index 00000000..e7839b92 --- /dev/null +++ b/infra/cosmosdb.tf @@ -0,0 +1,61 @@ +resource "azurerm_cosmosdb_account" "example" { + name = "db-${local.name}" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + offer_type = "Standard" + kind = "MongoDB" + + enable_automatic_failover = false + + capabilities { + name = "EnableAggregationPipeline" + } + + capabilities { + name = "mongoEnableDocLevelTTL" + } + + capabilities { + name = "MongoDBv3.4" + } + + capabilities { + name = "EnableMongo" + } + + consistency_policy { + consistency_level = "BoundedStaleness" + max_interval_in_seconds = 300 + max_staleness_prefix = 100000 + } + + geo_location { + location = "eastus" + failover_priority = 1 + } + + geo_location { + location = "westus" + failover_priority = 0 + } +} + +resource "azurerm_cosmosdb_mongo_database" "example" { + name = "orderdb" + resource_group_name = azurerm_cosmosdb_account.example.resource_group_name + account_name = azurerm_cosmosdb_account.example.name + throughput = 400 +} + +resource "azurerm_cosmosdb_mongo_collection" "example" { + name = "orders" + resource_group_name = azurerm_cosmosdb_account.example.resource_group_name + account_name = azurerm_cosmosdb_account.example.name + database_name = azurerm_cosmosdb_mongo_database.example.name + throughput = 400 + + index { + keys = ["_id"] + unique = true + } +} \ No newline at end of file diff --git a/infra/kubernetes.tf b/infra/kubernetes.tf new file mode 100644 index 00000000..47255a3c --- /dev/null +++ b/infra/kubernetes.tf @@ -0,0 +1,27 @@ +resource "azurerm_kubernetes_cluster" "example" { + name = "aks-${local.name}" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + dns_prefix = "aks-${local.name}" + + default_node_pool { + name = "system" + vm_size = "Standard_D4s_v4" + node_count = 3 + } + + identity { + type = "SystemAssigned" + } + + oidc_issuer_enabled = true + workload_identity_enabled = true + + lifecycle { + ignore_changes = [ + monitor_metrics, + azure_policy_enabled, + microsoft_defender + ] + } +} \ No newline at end of file diff --git a/infra/main.tf b/infra/main.tf new file mode 100644 index 00000000..b2f27ae3 --- /dev/null +++ b/infra/main.tf @@ -0,0 +1,56 @@ +terraform { + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "=3.80.0" + } + + local = { + source = "hashicorp/local" + version = "=2.4.0" + } + + random = { + source = "hashicorp/random" + version = "=3.5.1" + } + } +} + +provider "azurerm" { + features { + resource_group { + prevent_deletion_if_contains_resources = false + } + + cognitive_account { + purge_soft_delete_on_destroy = true + } + } +} + +resource "random_integer" "example" { + min = 10 + max = 99 +} + +resource "random_pet" "example" { + length = 2 + separator = "" + keepers = { + location = var.location + } +} + +locals { + name = "${random_pet.example.id}${random_integer.example.result}" + location = var.location +} + +data "azurerm_subscription" "current" {} +data "azurerm_client_config" "current" {} + +resource "azurerm_resource_group" "example" { + name = "rg-${local.name}" + location = local.location +} \ No newline at end of file diff --git a/infra/main.tfvars.json b/infra/main.tfvars.json new file mode 100644 index 00000000..9618150e --- /dev/null +++ b/infra/main.tfvars.json @@ -0,0 +1,4 @@ +{ + "location": "${AZURE_LOCATION}", + "ai_location": "${AZURE_LOCATION}" +} \ No newline at end of file diff --git a/infra/openai.tf b/infra/openai.tf new file mode 100644 index 00000000..51f976ce --- /dev/null +++ b/infra/openai.tf @@ -0,0 +1,51 @@ +resource "azurerm_cognitive_account" "example" { + name = "aoai-${local.name}" + location = var.ai_location + resource_group_name = azurerm_resource_group.example.name + kind = "OpenAI" + sku_name = "S0" + custom_subdomain_name = "aoai-${local.name}" +} + +resource "azurerm_cognitive_deployment" "example" { + name = var.openai_model_name + cognitive_account_id = azurerm_cognitive_account.example.id + + model { + format = "OpenAI" + name = var.openai_model_name + version = var.openai_model_version + } + + scale { + type = "Standard" + capacity = var.openai_model_capacity + } +} + +resource "azurerm_user_assigned_identity" "example" { + location = var.ai_location + name = "aoai-${local.name}" + resource_group_name = azurerm_resource_group.example.name +} + +resource "azurerm_federated_identity_credential" "example" { + name = "aoai-${local.name}" + resource_group_name = azurerm_resource_group.example.name + parent_id = azurerm_user_assigned_identity.example.id + audience = ["api://AzureADTokenExchange"] + issuer = azurerm_kubernetes_cluster.example.oidc_issuer_url + subject = "system:serviceaccount:${var.k8s_namespace}:ai-service-account" +} + +resource "azurerm_role_assignment" "example_aoai_me" { + principal_id = data.azurerm_client_config.current.object_id + role_definition_name = "Cognitive Services OpenAI User" + scope = azurerm_cognitive_account.example.id +} + +resource "azurerm_role_assignment" "example_aoai_mi" { + principal_id = azurerm_user_assigned_identity.example.principal_id + role_definition_name = "Cognitive Services OpenAI User" + scope = azurerm_cognitive_account.example.id +} \ No newline at end of file diff --git a/infra/outputs.tf b/infra/outputs.tf new file mode 100644 index 00000000..52ed2216 --- /dev/null +++ b/infra/outputs.tf @@ -0,0 +1,68 @@ +output "rg_name" { + value = azurerm_resource_group.example.name +} + +output "aks_name" { + value = azurerm_kubernetes_cluster.example.name +} + +output "ai_model_name" { + value = var.openai_model_name +} + +output "ai_endpoint" { + value = azurerm_cognitive_account.example.endpoint +} + +output "ai_key" { + value = azurerm_cognitive_account.example.primary_access_key + sensitive = true +} + +output "ai_managed_identity_client_id" { + value = azurerm_user_assigned_identity.example.client_id +} + +output "sb_namespace_host" { + value = "${azurerm_servicebus_namespace.example.name}.servicebus.windows.net" +} + +output "sb_namespace_uri" { + value = "amqps://${azurerm_servicebus_namespace.example.name}.servicebus.windows.net" + sensitive = true +} + +output "sb_listener_username" { + value = azurerm_servicebus_namespace_authorization_rule.example.name +} + +output "sb_listener_key" { + value = azurerm_servicebus_namespace_authorization_rule.example.primary_key + sensitive = true +} + +output "sb_sender_username" { + value = azurerm_servicebus_queue_authorization_rule.example.name +} + +output "sb_sender_key" { + value = azurerm_servicebus_queue_authorization_rule.example.primary_key + sensitive = true +} + +output "db_account_name" { + value = azurerm_cosmosdb_account.example.name +} + +output "db_uri" { + value = "mongodb://${azurerm_cosmosdb_account.example.name}.mongo.cosmos.azure.com:10255/?retryWrites=false" +} + +output "db_key" { + value = azurerm_cosmosdb_account.example.primary_key + sensitive = true +} + +output "k8s_namespace" { + value = var.k8s_namespace +} \ No newline at end of file diff --git a/infra/servicebus.tf b/infra/servicebus.tf new file mode 100644 index 00000000..fcead1ef --- /dev/null +++ b/infra/servicebus.tf @@ -0,0 +1,30 @@ +resource "azurerm_servicebus_namespace" "example" { + name = "sb-${local.name}" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + sku = "Standard" +} + +resource "azurerm_servicebus_namespace_authorization_rule" "example" { + name = "listener" + namespace_id = azurerm_servicebus_namespace.example.id + + listen = true + send = false + manage = false +} + +resource "azurerm_servicebus_queue" "example" { + name = "orders" + namespace_id = azurerm_servicebus_namespace.example.id +} + +resource "azurerm_servicebus_queue_authorization_rule" "example" { + name = "sender" + queue_id = azurerm_servicebus_queue.example.id + + listen = false + send = true + manage = false +} + diff --git a/infra/variables.tf b/infra/variables.tf new file mode 100644 index 00000000..f9d5e04b --- /dev/null +++ b/infra/variables.tf @@ -0,0 +1,31 @@ +variable "location" { + type = string +} + +variable "ai_location" { + description = "value of azure region for deploying azure ai service" + type = string +} + +variable "openai_model_name" { + description = "value of azure openai model name" + type = string + default = "gpt-35-turbo" +} +variable "openai_model_version" { + description = "value of azure openai model version" + type = string + default = "0613" +} + +variable "openai_model_capacity" { + description = "value of azure openai model capacity" + type = number + default = 120 +} + +variable "k8s_namespace" { + description = "value of kubernetes namespace" + type = string + default = "default" +} \ No newline at end of file From a42e77b6391c8fd4154ccf8ec2d93036ec7a8360 Mon Sep 17 00:00:00 2001 From: Paul Yu Date: Mon, 27 Nov 2023 10:15:48 -0800 Subject: [PATCH 2/2] docs: adding validation steps after running azd up --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 47ca4006..05497cc4 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,16 @@ azd up > Note: When selecting an Azure region, make sure to choose one that supports all the services used in this app including Azure OpenAI, Azure Kubernetes Service, Azure Service Bus, and Azure Cosmos DB. +Once the deployment is complete, you can verify all the services are running and the app is working by following these steps: + +- In the Azure portal, navigate to your Azure Service Bus resource and use Azure Service Bus explorer to check for order messages +- In the Azure portal, navigate to your Azure Cosmos DB resource and use the database explorer to check for order records +- Port-forward the store-admin service (using the command below) then open http://localhost:8081 in your browser and ensure you can add product descriptions using the AI service + + ```bash + kubectl port-forward svc/store-admin 8081:80 + ``` + ## Additional Resources - AKS Documentation. https://learn.microsoft.com/azure/aks