Azure Container Apps
- 4 minutes read - 784 wordsThe majority of Ackal’s components are deployed to Google Cloud. However, by its nature, Ackal benefits from deployments that span cloud platforms. I’ve deployed Ackal’s gRPC health checks to Fly, and managed Kubernetes services on Linode and Vultr.
Today, I decided to revisit¹ Azure. Ackal uses Azure (Active Directory) for one of its OAuth providers. This time, I wanted to deploy a containerized gRPC service. Azure provides several container-oriented services. I decided to use Azure Container Apps and, in hindsight, find it analogous to Google Cloud Run.
¹ – Many years ago now, I worked at Microsoft during Azure’s creation.
After installing Azure’s CLI (az
), creating a new Azure subscription and successfully az login
The container image I’m using is stored on GitHub Container Register (GHCR) and so I generated a token (read:packages
) to be used (as a secret
) when deploying to Container Apps.
It turned out that I need to understand some of Azure’s concepts before I could deploy:
- Locations
- Resource Groups
- Environments
Determining a deployment location was straightforward:
az account list-locations \
| jq -r '.[]|select(.name=="westus2")'
Yields:
{
"displayName": "West US 2",
"id": "/subscriptions/{subscription}/locations/westus2",
"metadata": {
"geographyGroup": "US",
"latitude": "47.233",
"longitude": "-119.852",
"pairedRegion": [
{
"id": "/subscriptions/{subscription}/locations/westcentralus",
"name": "westcentralus",
"subscriptionId": null
}
],
"physicalLocation": "Washington",
"regionCategory": "Recommended",
"regionType": "Physical"
},
"name": "westus2",
"regionalDisplayName": "(US) West US 2",
"subscriptionId": null
}
The id
values are scoped to the {subscription}
that I created. The value is redacted here.
Next, I needed a Resource Group. I found management levels and hierarchy too and remain unsure as to the distinction between Resource Groups and (Container Apps) Environments.
GROUP="..."
LOCATION="westus2" # From above
az group create \
--resource-group=${GROUP} \
--location=${LOCATION}
Yields:
{
"id": "/subscriptions/{subscription}/resourceGroups/{group}",
"location": "westus2",
"managedBy": null,
"name": "{group}",
"properties": {
"provisioningState": "Succeeded"
},
"tags": null,
"type": "Microsoft.Resources/resourceGroups"
}
And a Container Apps environment:
az containerapp env create \
--name=${GROUP} \
--resource-group=${GROUP} \
--logs-destination=none
NOTE
log-destination=none
is not advised but I learned that it’s still possible to view streamed logs in the Azure Console and the streamed logs are sufficient for my testing needs.
All good thus far but then I had real problems trying to construct the az containerapp
command to deploy the container. Initially, I tried az containerapp up
but was perplexed as to why this command doesn’t permit specifying --args
(I’d noticed that az containerapp update
does support --args
). I realized that I should use az containerapp create
. But, try as I might, I’ve been unable to determine how to pass an array of args correctly:
NAME="..."
IMAGE="..."
PORT="50051"
ARGS=(
"--flag1=value1"
"--flag2=value2"
"--endpoint=0.0.0.0:${PORT}"
...
)
TAGS=(
"key1=value1"
"key2=value2"
...
)
REGISTRY="ghcr.io" # GitHub Container Registry
USERNAME="..." # GitHub Username
az containerapp create \
--name=${NAME} \
--image="${IMAGE}" \
--args "${ARGS[@]}" \
--container-name=server \
--cpu=0.5 \
--memory=1Gi \
--min-replicas=0 \
--max-replicas=1 \
--ingress=external \
--target-port=${PORT} \
--transport=http2 \
--registry-password="secretref:token" \
--secrets="token=${TOKEN}" \
--registry-server=${REGISTRY} \
--registry-username=${USERNAME} \
--resource-group=${GROUP} \
--tags "${TAGS[@]}" \
--environment=${GROUP}
This either resulted in errors or a single args
value comprising the entire string.
I was able to avoid the issue because the container only requires the --endpoint
flag (the other flags default to values). But, it continues to bug me. Particularly because the TAGS
works.
Once I had the Container App deployed, I was able to:
HOST=$(\
az containerapp show \
--name=${NAME} \
--resource-group=${GROUP} \
| jq -r .properties.configuration.ingress.fqdn)
PORT="443"
grpcurl ${HOST}:${PORT} list
Yielding
grpc.health.v1.Health
grpc.reflection.v1alpha.ServerReflection
And:
grpcurl ${HOST}:${PORT} grpc.health.v1.Health/Check
{
"status": "SERVING"
}
During my adventures, I’d noticed that it’s possible to provide the configuration via a YAML spec. (I know there’s a tool called Bicep too but wanted to walk before running).
I tweaked the JSON (sic.) produced by successful az containerapp create
command removing what I thought were evident output-only values but there were some omissions.
I found the YAML configuration and tweaked the YAML that I’d tweaked to fit the model. This approach makes it easy to define the list of args and so I was able to deploy the Container App as I wanted:
type: Microsoft.App/containerApps
name: {name}
location: westus2
resourceGroup: {group}
tags:
application: ackal
system: healthcheck
properties:
managedEnvironmentId: /subscriptions/{subscription}/resourceGroups/{group}/providers/Microsoft.App/managedEnvironments/{group}
configuration:
ingress:
external: true
allowInsecure: false
targetPort: 50051
transport: Http2
registries:
- passwordSecretRef: token
server: ghcr.io
username: {username}
secrets:
- name: token
value: {token}
template:
containers:
- name: server
image: ghcr.io/{account}/{image}:{tag}
args:
- --...
- --...
- --endpoint=0.0.0.0:50051
- --services=,grpc.health.v1.Health
resources:
cpu: 0.5
ephemeralStorage: 2Gi
memory: 1Gi
scale:
minReplicas: 0
maxReplicas: 1
NOTE I Googled around to discover that
transport: Http2
is required for gRPC(’s required use of HTTP/2)
Once I was finished with the Container App, I deleted the Ackal health check and then:
az containerapp delete \
--name=${NAME} \
--resource-group=${GROUP} \
--yes
I’m assuming (!?) that Resource Groups and Environments incur no costs.
That’s all!