Skip to content

Commit

Permalink
Merge pull request #5 from Azure-Samples/main
Browse files Browse the repository at this point in the history
Add Azure Container Apps as a host option (Azure-Samples#1952)
  • Loading branch information
pavsingh7 authored Sep 23, 2024
2 parents ef14c83 + 0225f75 commit 0f5e3c1
Show file tree
Hide file tree
Showing 19 changed files with 730 additions and 89 deletions.
2 changes: 2 additions & 0 deletions .azdo/pipelines/azure-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ steps:
AZURE_ADLS_GEN2_STORAGE_ACCOUNT: $(AZURE_ADLS_GEN2_STORAGE_ACCOUNT)
AZURE_ADLS_GEN2_FILESYSTEM_PATH: $(AZURE_ADLS_GEN2_FILESYSTEM_PATH)
AZURE_ADLS_GEN2_FILESYSTEM: $(AZURE_ADLS_GEN2_FILESYSTEM)
DEPLOYMENT_TARGET: $(DEPLOYMENT_TARGET)
AZURE_CONTAINER_APPS_WORKLOAD_PROFILE: $(AZURE_CONTAINER_APPS_WORKLOAD_PROFILE)

- task: AzureCLI@2
displayName: Deploy Application
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/azure-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ jobs:
AZURE_ADLS_GEN2_STORAGE_ACCOUNT: ${{ vars.AZURE_ADLS_GEN2_STORAGE_ACCOUNT }}
AZURE_ADLS_GEN2_FILESYSTEM_PATH: ${{ vars.AZURE_ADLS_GEN2_FILESYSTEM_PATH }}
AZURE_ADLS_GEN2_FILESYSTEM: ${{ vars.AZURE_ADLS_GEN2_FILESYSTEM }}
DEPLOYMENT_TARGET: ${{ vars.DEPLOYMENT_TARGET }}
AZURE_CONTAINER_APPS_WORKLOAD_PROFILE: ${{ vars.AZURE_CONTAINER_APPS_WORKLOAD_PROFILE }}

steps:
- name: Checkout
Expand Down
8 changes: 8 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ contact [[email protected]](mailto:[email protected]) with any additio
- [Running unit tests](#running-unit-tests)
- [Running E2E tests](#running-e2e-tests)
- [Code Style](#code-style)
- [Adding new azd environment variables](#add-new-azd-environment-variables)

## Code of Conduct

Expand Down Expand Up @@ -160,3 +161,10 @@ python -m black <path-to-file>
```

If you followed the steps above to install the pre-commit hooks, then you can just wait for those hooks to run `ruff` and `black` for you.

## Adding new azd environment variables

When adding new azd environment variables, please remember to update:
1. App Service's [azure.yaml](./azure.yaml)
1. [ADO pipeline](.azdo/pipelines/azure-dev.yml).
1. [Github workflows](.github/workflows/azure-dev.yml)
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ Pricing varies per region and usage, so it isn't possible to predict exact costs
However, you can try the [Azure pricing calculator](https://azure.com/e/a87a169b256e43c089015fda8182ca87) for the resources below.

- Azure App Service: Basic Tier with 1 CPU core, 1.75 GB RAM. Pricing per hour. [Pricing](https://azure.microsoft.com/pricing/details/app-service/linux/)
- Azure Container Apps: Only provisioned if you deploy to Azure Container Apps following [the ACA deployment guide](docs/azure_container_apps.md). Consumption plan with 1 CPU core, 2.0 GB RAM. Pricing with Pay-as-You-Go. [Pricing](https://azure.microsoft.com/pricing/details/container-apps/)
- Azure OpenAI: Standard tier, GPT and Ada models. Pricing per 1K tokens used, and at least 1K tokens are used per question. [Pricing](https://azure.microsoft.com/pricing/details/cognitive-services/openai-service/)
- Azure AI Document Intelligence: SO (Standard) tier using pre-built layout. Pricing per document page, sample documents have 261 pages total. [Pricing](https://azure.microsoft.com/pricing/details/form-recognizer/)
- Azure AI Search: Basic tier, 1 replica, free level of semantic search. Pricing per hour. [Pricing](https://azure.microsoft.com/pricing/details/search/)
Expand Down Expand Up @@ -126,17 +127,17 @@ A related option is VS Code Dev Containers, which will open the project in your

## Deploying

Follow these steps to provision Azure resources and deploy the application code:
The steps below will provision Azure resources and deploy the application code to Azure App Service. To deploy to Azure Container Apps instead, follow [the container apps deployment guide](docs/azure_container_apps.md).

1. Login to your Azure account:

```shell
azd auth login
```

For GitHub Codespaces users, if the previous command fails, try:
For GitHub Codespaces users, if the previous command fails, try:
```shell
azd auth login --use-device-code
azd auth login --use-device-code
```

1. Create a new azd environment:
Expand Down
7 changes: 7 additions & 0 deletions app/backend/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.git
__pycache__
*.pyc
*.pyo
*.pyd
.Python
env
11 changes: 11 additions & 0 deletions app/backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
FROM python:3.11-bullseye

WORKDIR /app

COPY ./ /app

RUN python -m pip install -r requirements.txt

RUN python -m pip install gunicorn

CMD ["python3", "-m", "gunicorn", "-b", "0.0.0.0:8000", "main:app"]
16 changes: 14 additions & 2 deletions app/backend/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -440,13 +440,25 @@ async def setup_clients():
USE_SPEECH_OUTPUT_BROWSER = os.getenv("USE_SPEECH_OUTPUT_BROWSER", "").lower() == "true"
USE_SPEECH_OUTPUT_AZURE = os.getenv("USE_SPEECH_OUTPUT_AZURE", "").lower() == "true"

# WEBSITE_HOSTNAME is always set by App Service, RUNNING_IN_PRODUCTION is set in main.bicep
RUNNING_ON_AZURE = os.getenv("WEBSITE_HOSTNAME") is not None or os.getenv("RUNNING_IN_PRODUCTION") is not None

# Use the current user identity for keyless authentication to Azure services.
# This assumes you use 'azd auth login' locally, and managed identity when deployed on Azure.
# The managed identity is setup in the infra/ folder.
azure_credential: Union[AzureDeveloperCliCredential, ManagedIdentityCredential]
if os.getenv("WEBSITE_HOSTNAME"): # Environment variable set on Azure Web Apps
if RUNNING_ON_AZURE:
current_app.logger.info("Setting up Azure credential using ManagedIdentityCredential")
azure_credential = ManagedIdentityCredential()
if AZURE_CLIENT_ID := os.getenv("AZURE_CLIENT_ID"):
# ManagedIdentityCredential should use AZURE_CLIENT_ID if set in env, but its not working for some reason,
# so we explicitly pass it in as the client ID here. This is necessary for user-assigned managed identities.
current_app.logger.info(
"Setting up Azure credential using ManagedIdentityCredential with client_id %s", AZURE_CLIENT_ID
)
azure_credential = ManagedIdentityCredential(client_id=AZURE_CLIENT_ID)
else:
current_app.logger.info("Setting up Azure credential using ManagedIdentityCredential")
azure_credential = ManagedIdentityCredential()
elif AZURE_TENANT_ID:
current_app.logger.info(
"Setting up Azure credential using AzureDeveloperCliCredential with tenant_id %s", AZURE_TENANT_ID
Expand Down
6 changes: 5 additions & 1 deletion azure.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ services:
backend:
project: ./app/backend
language: py
# Please check docs/azure_container_apps.md for more information on how to deploy to Azure Container Apps
# host: containerapp
host: appservice
hooks:
prepackage:
prebuild:
windows:
shell: pwsh
run: cd ../frontend;npm install;npm run build
Expand Down Expand Up @@ -86,6 +88,8 @@ pipeline:
- AZURE_ADLS_GEN2_STORAGE_ACCOUNT
- AZURE_ADLS_GEN2_FILESYSTEM_PATH
- AZURE_ADLS_GEN2_FILESYSTEM
- DEPLOYMENT_TARGET
- AZURE_CONTAINER_APPS_WORKLOAD_PROFILE
secrets:
- AZURE_SERVER_APP_SECRET
- AZURE_CLIENT_APP_SECRET
Expand Down
10 changes: 6 additions & 4 deletions docs/appservice.md
Original file line number Diff line number Diff line change
Expand Up @@ -631,15 +631,17 @@ To see any exceptions and server errors, navigate to the _Investigate -> Failure

## Configuring log levels

By default, the deployed app only logs messages with a level of `WARNING` or higher.
By default, the deployed app only logs messages from packages with a level of `WARNING` or higher,
but logs all messages from the app with a level of `INFO` or higher.

These lines of code in `app/backend/app.py` configure the logging level:

```python
# Set root level to WARNING to avoid seeing overly verbose logs from SDKS
logging.basicConfig(level=logging.WARNING)
# Set the app logger level to INFO by default
default_level = "INFO"
if os.getenv("WEBSITE_HOSTNAME"): # In production, don't log as heavily
default_level = "WARNING"
logging.basicConfig(level=os.getenv("APP_LOG_LEVEL", default_level))
app.logger.setLevel(os.getenv("APP_LOG_LEVEL", default_level))
```

To change the default level, either change `default_level` or set the `APP_LOG_LEVEL` environment variable
Expand Down
55 changes: 55 additions & 0 deletions docs/azure_container_apps.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Deploying on Azure Container Apps

Due to [a limitation](https://github.com/Azure/azure-dev/issues/2736) of the Azure Developer CLI (`azd`), there can be only one host option in the [azure.yaml](../azure.yaml) file.
By default, `host: appservice` is used and `host: containerapp` is commented out.

To deploy to Azure Container Apps, please follow the following steps:

1. Comment out `host: appservice` and uncomment `host: containerapp` in the [azure.yaml](../azure.yaml) file.

2. Login to your Azure account:

```bash
azd auth login
```

3. Create a new `azd` environment to store the deployment parameters:

```bash
azd env new
```

Enter a name that will be used for the resource group.
This will create a new folder in the `.azure` folder, and set it as the active environment for any calls to `azd` going forward.

4. Set the deployment target to `containerapps`:

```bash
azd env set DEPLOYMENT_TARGET containerapps
```

5. (Optional) This is the point where you can customize the deployment by setting other `azd1 environment variables, in order to [use existing resources](docs/deploy_existing.md), [enable optional features (such as auth or vision)](docs/deploy_features.md), or [deploy to free tiers](docs/deploy_lowcost.md).
6. Provision the resources and deploy the code:
```bash
azd up
```

This will provision Azure resources and deploy this sample to those resources, including building the search index based on the files found in the `./data` folder.

**Important**: Beware that the resources created by this command will incur immediate costs, primarily from the AI Search resource. These resources may accrue costs even if you interrupt the command before it is fully executed. You can run `azd down` or delete the resources manually to avoid unnecessary spending.

## Customizing Workload Profile

The default workload profile is Consumption. If you want to use a dedicated workload profile like D4, please run:

```bash
azd env AZURE_CONTAINER_APPS_WORKLOAD_PROFILE D4
```

For a full list of workload profiles, please check [here](https://learn.microsoft.com/azure/container-apps/workload-profiles-overview#profile-types).
Please note dedicated workload profiles have a different billing model than Consumption plan. Please check [here](https://learn.microsoft.com/azure/container-apps/billing) for details.

## Private endpoints

Private endpoints is still in private preview for Azure Conainer Apps and not supported for now.
1 change: 1 addition & 0 deletions infra/abbreviations.json
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@
"virtualNetworks": "vnet-",
"webServerFarms": "plan-",
"webSitesAppService": "app-",
"webSitesContainerApps": "capps-",
"webSitesAppServiceEnvironment": "ase-",
"webSitesFunctions": "func-",
"webStaticSites": "stapp-"
Expand Down
130 changes: 130 additions & 0 deletions infra/core/host/container-app-upsert.bicep
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
metadata description = 'Creates or updates an existing Azure Container App.'
param name string
param location string = resourceGroup().location
param tags object = {}


@description('The number of CPU cores allocated to a single container instance, e.g., 0.5')
param containerCpuCoreCount string = '0.5'

@description('The maximum number of replicas to run. Must be at least 1.')
@minValue(1)
param containerMaxReplicas int = 10

@description('The amount of memory allocated to a single container instance, e.g., 1Gi')
param containerMemory string = '1.0Gi'

@description('The minimum number of replicas to run. Must be at least 1.')
@minValue(1)
param containerMinReplicas int = 1

@description('The name of the container')
param containerName string = 'main'

@description('The environment name for the container apps')
param containerAppsEnvironmentName string = '${containerName}env'

@description('The name of the container registry')
param containerRegistryName string

@description('Hostname suffix for container registry. Set when deploying to sovereign clouds')
param containerRegistryHostSuffix string = 'azurecr.io'

@allowed(['http', 'grpc'])
@description('The protocol used by Dapr to connect to the app, e.g., HTTP or gRPC')
param daprAppProtocol string = 'http'

@description('Enable or disable Dapr for the container app')
param daprEnabled bool = false

@description('The Dapr app ID')
param daprAppId string = containerName

@description('Specifies if the resource already exists')
param exists bool = false

@description('Specifies if Ingress is enabled for the container app')
param ingressEnabled bool = true

@description('The type of identity for the resource')
@allowed(['None', 'SystemAssigned', 'UserAssigned'])
param identityType string = 'None'

@description('The name of the user-assigned identity')
param identityName string = ''

@description('The name of the container image')
param imageName string = ''

@description('The secrets required for the container')
@secure()
param secrets object = {}

@description('The keyvault identities required for the container')
@secure()
param keyvaultIdentities object = {}

@description('The environment variables for the container in key value pairs')
param env object = {}

@description('Specifies if the resource ingress is exposed externally')
param external bool = true

@description('The service binds associated with the container')
param serviceBinds array = []

@description('The target port for the container')
param targetPort int = 80

@allowed(['Consumption', 'D4', 'D8', 'D16', 'D32', 'E4', 'E8', 'E16', 'E32', 'NC24-A100', 'NC48-A100', 'NC96-A100'])
param workloadProfile string = 'Consumption'

param allowedOrigins array = []

resource existingApp 'Microsoft.App/containerApps@2023-05-02-preview' existing = if (exists) {
name: name
}

module app 'container-app.bicep' = {
name: '${deployment().name}-update'
params: {
name: name
workloadProfile: workloadProfile
location: location
tags: tags
identityType: identityType
identityName: identityName
ingressEnabled: ingressEnabled
containerName: containerName
containerAppsEnvironmentName: containerAppsEnvironmentName
containerRegistryName: containerRegistryName
containerRegistryHostSuffix: containerRegistryHostSuffix
containerCpuCoreCount: containerCpuCoreCount
containerMemory: containerMemory
containerMinReplicas: containerMinReplicas
containerMaxReplicas: containerMaxReplicas
daprEnabled: daprEnabled
daprAppId: daprAppId
daprAppProtocol: daprAppProtocol
secrets: secrets
keyvaultIdentities: keyvaultIdentities
allowedOrigins: allowedOrigins
external: external
env: [
for key in objectKeys(env): {
name: key
value: '${env[key]}'
}
]
imageName: !empty(imageName) ? imageName : exists ? existingApp.properties.template.containers[0].image : ''
targetPort: targetPort
serviceBinds: serviceBinds
}
}

output defaultDomain string = app.outputs.defaultDomain
output imageName string = app.outputs.imageName
output name string = app.outputs.name
output uri string = app.outputs.uri
output id string = app.outputs.id
output identityPrincipalId string = app.outputs.identityPrincipalId
Loading

0 comments on commit 0f5e3c1

Please sign in to comment.